@musnows/scriverse 0.3.0 → 0.3.1

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/README.en.md CHANGED
@@ -203,7 +203,7 @@ Expected response:
203
203
  {
204
204
  "data": {
205
205
  "status": "ok",
206
- "version": "0.3.0",
206
+ "version": "0.3.1",
207
207
  "protocol": "openai-chat-completions"
208
208
  }
209
209
  }
package/README.md CHANGED
@@ -209,7 +209,7 @@ curl http://127.0.0.1:13210/api/health
209
209
  {
210
210
  "data": {
211
211
  "status": "ok",
212
- "version": "0.3.0",
212
+ "version": "0.3.1",
213
213
  "protocol": "openai-chat-completions"
214
214
  }
215
215
  }
package/dist/ai.js CHANGED
@@ -1,13 +1,24 @@
1
1
  import { PLATFORM_AI_WORK_ID } from "./database.js";
2
2
  import { AppError, notFound } from "./errors.js";
3
+ import { logger, sanitizeError } from "./logger.js";
3
4
  import { currentRequestActor } from "./request-context.js";
4
5
  import { fetchSafeAiEndpoint } from "./security.js";
5
6
  import { clamp, id, json, maskSecret, normalizeBaseUrl, now } from "./utils.js";
6
7
  import { z } from "zod";
8
+ export function aiErrorForLog(error) {
9
+ const sanitized = sanitizeError(error);
10
+ const message = typeof sanitized.message === "string" ? sanitized.message : "AI operation failed";
11
+ const httpStatus = message.match(/^HTTP (\d{3}):/u)?.[1];
12
+ if (httpStatus)
13
+ return { name: sanitized.name ?? "Error", message: `Provider returned HTTP ${httpStatus}` };
14
+ if (message.includes("returned invalid JSON"))
15
+ return { name: sanitized.name ?? "Error", message: "Provider returned invalid JSON" };
16
+ return sanitized;
17
+ }
7
18
  const allowedParameters = new Set(["temperature", "top_p", "max_tokens", "presence_penalty", "frequency_penalty", "seed"]);
8
19
  const DEFAULT_MAX_TOKENS = 32_000;
9
20
  const DEFAULT_CONTEXT_WINDOW = 128_000;
10
- const AGENT_TOOL_IDS = ["story_index", "read_chapters", "query_story_knowledge"];
21
+ const AGENT_TOOL_IDS = ["story_index", "read_chapters", "grep", "query_story_knowledge"];
11
22
  const MAX_AGENT_TOOL_ROUNDS = 6;
12
23
  const MAX_AGENT_TOOL_CALLS = 12;
13
24
  const storyIndexArguments = z.object({
@@ -18,6 +29,10 @@ const readChaptersArguments = z.object({
18
29
  chapterIds: z.array(z.string().min(1).max(200)).min(1).max(3),
19
30
  include: z.enum(["summary", "content", "both"]).default("both")
20
31
  }).strict();
32
+ const grepArguments = z.object({
33
+ keyword: z.string().trim().min(1).max(200),
34
+ limit: z.number().int().min(1).max(100).default(20)
35
+ }).strict();
21
36
  const queryStoryKnowledgeArguments = z.object({
22
37
  query: z.string().trim().min(1).max(200),
23
38
  categories: z.array(z.enum(["setting", "character", "race", "organization", "timeline", "relationship", "outline", "foreshadow"])).max(8).default([])
@@ -39,6 +54,14 @@ const AGENT_TOOL_DEFINITIONS = {
39
54
  parameters: { type: "object", properties: { chapterIds: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 3 }, include: { type: "string", enum: ["summary", "content", "both"] } }, required: ["chapterIds"], additionalProperties: false }
40
55
  }
41
56
  },
57
+ grep: {
58
+ type: "function",
59
+ function: {
60
+ name: "grep",
61
+ description: "在当前作品的章节正文索引中查询关键字,返回关键字所在的完整段落及章节标题和 ID。默认返回前 20 条,可按需调整 limit。",
62
+ parameters: { type: "object", properties: { keyword: { type: "string", minLength: 1, maxLength: 200 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 20 } }, required: ["keyword"], additionalProperties: false }
63
+ }
64
+ },
42
65
  query_story_knowledge: {
43
66
  type: "function",
44
67
  function: {
@@ -642,6 +665,7 @@ export class AiManager {
642
665
  this.validateOutboundUrl = validateOutboundUrl;
643
666
  this.contextBuilder = new ContextBuilder(store);
644
667
  this.store.setAnalysisTaskQueuedHandler((workId) => this.scheduleAutoRun(workId));
668
+ logger.info("ai.manager.ready");
645
669
  }
646
670
  resetAutoRunBatch(workId) {
647
671
  this.autoRunBatches.set(workId, { claimed: 0, starting: new Set() });
@@ -662,6 +686,7 @@ export class AiManager {
662
686
  void this.drainAutoRun(workId);
663
687
  }, 0);
664
688
  this.autoRunTimers.set(workId, timer);
689
+ logger.debug("ai.auto_run.scheduled", { workId });
665
690
  }
666
691
  startAutoRunBatch(workId) {
667
692
  this.store.getWork(workId);
@@ -671,6 +696,11 @@ export class AiManager {
671
696
  }
672
697
  this.resetAutoRunBatch(workId);
673
698
  this.scheduleAutoRun(workId);
699
+ logger.info("ai.auto_run.batch_started", {
700
+ workId,
701
+ concurrency: settings.autoRunConcurrency,
702
+ batchLimit: settings.autoRunBatchLimit
703
+ });
674
704
  return {
675
705
  workId,
676
706
  autoRunEnabled: true,
@@ -681,11 +711,13 @@ export class AiManager {
681
711
  };
682
712
  }
683
713
  dispose() {
714
+ logger.info("ai.manager.disposing", { scheduledWorks: this.autoRunTimers.size, activeTasks: this.taskControllers.size });
684
715
  for (const timer of this.autoRunTimers.values())
685
716
  clearTimeout(timer);
686
717
  this.autoRunTimers.clear();
687
718
  this.autoRunBatches.clear();
688
719
  this.store.setAnalysisTaskQueuedHandler(null);
720
+ logger.info("ai.manager.disposed");
689
721
  }
690
722
  getAutoRunBatch(workId) {
691
723
  const existing = this.autoRunBatches.get(workId);
@@ -697,6 +729,7 @@ export class AiManager {
697
729
  }
698
730
  async drainAutoRun(workId) {
699
731
  try {
732
+ logger.debug("ai.auto_run.drain_started", { workId });
700
733
  const settings = this.store.getWorkAiSettings(workId);
701
734
  if (!settings.autoRunEnabled)
702
735
  return;
@@ -726,7 +759,8 @@ export class AiManager {
726
759
  });
727
760
  }
728
761
  }
729
- catch {
762
+ catch (error) {
763
+ logger.warn("ai.auto_run.drain_failed", { workId, error: aiErrorForLog(error) });
730
764
  // 数据库已关闭或作品不存在时忽略自动调度
731
765
  }
732
766
  }
@@ -795,6 +829,8 @@ export class AiManager {
795
829
  const apiKey = this.decryptKey(row);
796
830
  const controller = new AbortController();
797
831
  const timeout = setTimeout(() => controller.abort(), 10_000);
832
+ const startedAt = process.hrtime.bigint();
833
+ logger.info("ai.provider_test.started", { providerId });
798
834
  try {
799
835
  const endpoint = `${normalizeBaseUrl(stringValue(row, "base_url"))}/models`;
800
836
  const response = await this.outboundFetch(endpoint, {
@@ -809,11 +845,23 @@ export class AiManager {
809
845
  const availableModels = Array.isArray(payload.data) ? payload.data.map((item) => item.id).filter(Boolean) : [];
810
846
  const timestamp = now();
811
847
  this.store.db.run("UPDATE providers SET connection_status = 'success', last_error = NULL, last_success_at = ?, updated_at = ? WHERE id = ?", timestamp, timestamp, providerId);
848
+ logger.info("ai.provider_test.completed", {
849
+ providerId,
850
+ ok: true,
851
+ availableModelCount: availableModels.length,
852
+ durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000
853
+ });
812
854
  return { ok: true, availableModels, provider: this.getProvider(providerId) };
813
855
  }
814
856
  catch (error) {
815
857
  const message = error instanceof Error ? error.message : "连接失败";
816
858
  this.store.db.run("UPDATE providers SET connection_status = 'failed', last_error = ?, updated_at = ? WHERE id = ?", message, now(), providerId);
859
+ logger.warn("ai.provider_test.completed", {
860
+ providerId,
861
+ ok: false,
862
+ durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000,
863
+ error: aiErrorForLog(error)
864
+ });
817
865
  return { ok: false, error: message, provider: this.getProvider(providerId) };
818
866
  }
819
867
  finally {
@@ -1061,12 +1109,15 @@ export class AiManager {
1061
1109
  const task = this.store.getTask(taskId);
1062
1110
  const workId = String(task.workId);
1063
1111
  const batch = this.getAutoRunBatch(workId);
1112
+ const startedAt = process.hrtime.bigint();
1113
+ logger.info("ai.task.started", { taskId, workId, taskType: task.taskType, modelId: modelId ?? null });
1064
1114
  if (task.status !== "pending")
1065
1115
  throw new AppError(409, "TASK_NOT_PENDING", "只有待执行任务可以运行");
1066
1116
  if (!this.store.isTaskSourceCurrent(taskId)) {
1067
1117
  const expired = this.store.updateTask(taskId, { status: "expired" });
1068
1118
  batch.starting.delete(taskId);
1069
1119
  this.scheduleAutoRun(workId);
1120
+ logger.warn("ai.task.expired", { taskId, workId, durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000 });
1070
1121
  return expired;
1071
1122
  }
1072
1123
  const settings = this.store.getWorkAiSettings(workId);
@@ -1113,15 +1164,20 @@ export class AiManager {
1113
1164
  });
1114
1165
  result = { content: generated.content, callId: generated.callId };
1115
1166
  }
1116
- if (!this.taskCanCommit(taskId))
1167
+ if (!this.taskCanCommit(taskId)) {
1168
+ logger.warn("ai.task.result_discarded", { taskId, workId, durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000 });
1117
1169
  return this.store.getTask(taskId);
1118
- return this.store.updateTask(taskId, { status: "review", progress: 100, result });
1170
+ }
1171
+ const completed = this.store.updateTask(taskId, { status: "review", progress: 100, result });
1172
+ logger.info("ai.task.completed", { taskId, workId, durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000 });
1173
+ return completed;
1119
1174
  }
1120
1175
  catch (error) {
1121
1176
  if (this.store.getTask(taskId).status !== "running")
1122
1177
  return this.store.getTask(taskId);
1123
1178
  const message = error instanceof Error ? error.message : "分析失败";
1124
1179
  this.store.updateTask(taskId, { status: "partial", progress: 100, failures: [{ message }] });
1180
+ logger.error("ai.task.failed", { taskId, workId, durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000, error: aiErrorForLog(error) });
1125
1181
  throw error;
1126
1182
  }
1127
1183
  finally {
@@ -1135,6 +1191,7 @@ export class AiManager {
1135
1191
  this.taskControllers.get(taskId)?.abort(new Error("分析任务已取消"));
1136
1192
  this.taskControllers.delete(taskId);
1137
1193
  this.scheduleAutoRun(String(task.workId));
1194
+ logger.warn("ai.task.cancelled", { taskId, workId: task.workId });
1138
1195
  return task;
1139
1196
  }
1140
1197
  contextBudget(input, model) {
@@ -1288,7 +1345,7 @@ export class AiManager {
1288
1345
  ? [
1289
1346
  `当前可用作品查询工具:${enabledToolIds.join("、")}。`,
1290
1347
  "当作者询问当前作品、项目、章节、情节、人物、关系、世界观或设定,而预加载上下文为空或不足时,必须先调用工具主动查询;不得直接声称没有上下文,也不得先要求作者补充本系统已经能够查询的信息。",
1291
- "整体介绍、作品基本信息、目录或章节定位优先调用 story_index;需要原文事实或精确措辞时调用 read_chapters;查询设定、人物、组织、时间线、关系、大纲或伏笔时调用 query_story_knowledge。",
1348
+ "整体介绍、作品基本信息、目录或章节定位优先调用 story_index;按关键字定位正文段落时调用 grep;已知章节 ID 且需要原文事实或精确措辞时调用 read_chapters;查询设定、人物、组织、时间线、关系、大纲或伏笔时调用 query_story_knowledge。",
1292
1349
  "根据问题选择最少且必要的工具。工具结果仍不足时才说明未知,并明确已经查询过什么;不要重复无效调用。"
1293
1350
  ].join("\n")
1294
1351
  : "";
@@ -1370,8 +1427,9 @@ export class AiManager {
1370
1427
  : null;
1371
1428
  const schema = name === "story_index" ? storyIndexArguments
1372
1429
  : name === "read_chapters" ? readChaptersArguments
1373
- : name === "query_story_knowledge" ? queryStoryKnowledgeArguments
1374
- : null;
1430
+ : name === "grep" ? grepArguments
1431
+ : name === "query_story_knowledge" ? queryStoryKnowledgeArguments
1432
+ : null;
1375
1433
  if (!schema) {
1376
1434
  return {
1377
1435
  id: toolCall.id,
@@ -1450,6 +1508,18 @@ export class AiManager {
1450
1508
  });
1451
1509
  return { id: toolCall.id, name, calledAt, arguments: { chapterIds, include }, status: "completed", result: { ok: true, data: { chapters, contentLimitChars: 36_000 } } };
1452
1510
  }
1511
+ if (name === "grep") {
1512
+ const { keyword, limit } = args;
1513
+ const matches = this.store.searchChapterParagraphs(workId, keyword, limit);
1514
+ return {
1515
+ id: toolCall.id,
1516
+ name,
1517
+ calledAt,
1518
+ arguments: { keyword, limit },
1519
+ status: "completed",
1520
+ result: { ok: true, data: { keyword, limit, matches } }
1521
+ };
1522
+ }
1453
1523
  if (name === "query_story_knowledge") {
1454
1524
  const { query, categories: categoryList } = args;
1455
1525
  const categories = new Set(categoryList);
@@ -1500,6 +1570,18 @@ export class AiManager {
1500
1570
  const timestamp = now();
1501
1571
  this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
1502
1572
  status, input_chars, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?)`, callId, input.workId, input.taskType, stringValue(provider, "id"), stringValue(model, "id"), JSON.stringify(input.scope), JSON.stringify(parameters), context.length + input.instruction.length, timestamp, currentRequestActor()?.userId ?? null);
1573
+ const callStartedAt = process.hrtime.bigint();
1574
+ logger.info("ai.call.started", {
1575
+ callId,
1576
+ workId: input.workId,
1577
+ taskType: input.taskType,
1578
+ providerId: stringValue(provider, "id"),
1579
+ modelId: stringValue(model, "id"),
1580
+ streaming: false,
1581
+ contextChars: context.length,
1582
+ instructionChars: input.instruction.length,
1583
+ toolCount: tools.length
1584
+ });
1503
1585
  try {
1504
1586
  const apiKey = this.decryptKey(provider);
1505
1587
  const endpoint = `${normalizeBaseUrl(stringValue(provider, "base_url"))}/chat/completions`;
@@ -1509,6 +1591,8 @@ export class AiManager {
1509
1591
  let lastFailure = null;
1510
1592
  for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
1511
1593
  let retryable = true;
1594
+ const attemptStartedAt = process.hrtime.bigint();
1595
+ logger.info("ai.call.attempt_started", { callId, attempt, maximumAttempts, toolChoice });
1512
1596
  try {
1513
1597
  const candidate = await this.scheduleProviderRequest(provider, input.signal, async () => {
1514
1598
  const controller = new AbortController();
@@ -1532,6 +1616,13 @@ export class AiManager {
1532
1616
  input.signal?.removeEventListener("abort", forwardAbort);
1533
1617
  }
1534
1618
  });
1619
+ logger.info("ai.call.attempt_completed", {
1620
+ callId,
1621
+ attempt,
1622
+ status: candidate.status,
1623
+ ok: candidate.ok,
1624
+ durationMs: Number(process.hrtime.bigint() - attemptStartedAt) / 1_000_000
1625
+ });
1535
1626
  if (candidate.ok) {
1536
1627
  try {
1537
1628
  return JSON.parse(candidate.body);
@@ -1548,6 +1639,13 @@ export class AiManager {
1548
1639
  }
1549
1640
  catch (error) {
1550
1641
  lastFailure = error;
1642
+ logger.warn("ai.call.attempt_failed", {
1643
+ callId,
1644
+ attempt,
1645
+ retryable: retryable && attempt < maximumAttempts && !input.signal?.aborted,
1646
+ durationMs: Number(process.hrtime.bigint() - attemptStartedAt) / 1_000_000,
1647
+ error: aiErrorForLog(error)
1648
+ });
1551
1649
  if (input.signal?.aborted)
1552
1650
  throw error;
1553
1651
  if (!retryable || attempt >= maximumAttempts)
@@ -1599,6 +1697,7 @@ export class AiManager {
1599
1697
  });
1600
1698
  for (const toolCall of toolCalls) {
1601
1699
  const execution = this.executeAgentTool(input.workId, toolCall);
1700
+ logger.info("ai.tool_call.completed", { callId, toolName: execution.name, status: execution.status, round });
1602
1701
  executedToolCalls.push(execution);
1603
1702
  processSteps.push({ id: id("process"), type: "tool", round, toolCall: execution, createdAt: execution.calledAt });
1604
1703
  input.onToolCall?.(execution, round);
@@ -1622,11 +1721,30 @@ export class AiManager {
1622
1721
  throw new Error(`Chat Completions 响应缺少可用正文,finish_reason=${choice?.finish_reason ?? "unknown"}${suffix}`);
1623
1722
  }
1624
1723
  this.store.db.run("UPDATE ai_calls SET status = 'completed', output_chars = ?, completed_at = ? WHERE id = ?", content.length, now(), callId);
1625
- return { callId, content, outputTokens: resolveOutputTokens(payload.usage, content), provider: this.mapProvider(provider), model: this.mapModel(model), context, toolCalls: executedToolCalls, processSteps };
1724
+ const outputTokens = resolveOutputTokens(payload.usage, content);
1725
+ logger.info("ai.call.completed", {
1726
+ callId,
1727
+ workId: input.workId,
1728
+ taskType: input.taskType,
1729
+ streaming: false,
1730
+ durationMs: Number(process.hrtime.bigint() - callStartedAt) / 1_000_000,
1731
+ outputChars: content.length,
1732
+ outputTokens,
1733
+ toolCallCount: executedToolCalls.length
1734
+ });
1735
+ return { callId, content, outputTokens, provider: this.mapProvider(provider), model: this.mapModel(model), context, toolCalls: executedToolCalls, processSteps };
1626
1736
  }
1627
1737
  catch (error) {
1628
1738
  const message = error instanceof Error ? error.message : "AI 调用失败";
1629
1739
  this.store.db.run("UPDATE ai_calls SET status = 'failed', failure = ?, completed_at = ? WHERE id = ?", message, now(), callId);
1740
+ logger.error("ai.call.failed", {
1741
+ callId,
1742
+ workId: input.workId,
1743
+ taskType: input.taskType,
1744
+ streaming: false,
1745
+ durationMs: Number(process.hrtime.bigint() - callStartedAt) / 1_000_000,
1746
+ error: aiErrorForLog(error)
1747
+ });
1630
1748
  throw new AppError(502, "AI_CALL_FAILED", "AI 调用失败", { callId, failure: message });
1631
1749
  }
1632
1750
  }
@@ -1642,6 +1760,17 @@ export class AiManager {
1642
1760
  const callId = id("call");
1643
1761
  this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
1644
1762
  status, input_chars, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?)`, callId, input.workId, input.taskType, stringValue(provider, "id"), stringValue(model, "id"), JSON.stringify(input.scope), JSON.stringify(parameters), context.length + input.instruction.length, now(), currentRequestActor()?.userId ?? null);
1763
+ const callStartedAt = process.hrtime.bigint();
1764
+ logger.info("ai.call.started", {
1765
+ callId,
1766
+ workId: input.workId,
1767
+ taskType: input.taskType,
1768
+ providerId: stringValue(provider, "id"),
1769
+ modelId: stringValue(model, "id"),
1770
+ streaming: true,
1771
+ contextChars: context.length,
1772
+ instructionChars: input.instruction.length
1773
+ });
1645
1774
  try {
1646
1775
  const apiKey = this.decryptKey(provider);
1647
1776
  const endpoint = `${normalizeBaseUrl(stringValue(provider, "base_url"))}/chat/completions`;
@@ -1652,6 +1781,8 @@ export class AiManager {
1652
1781
  const thinkingStepId = id("process");
1653
1782
  const thinkingCreatedAt = now();
1654
1783
  for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
1784
+ const attemptStartedAt = process.hrtime.bigint();
1785
+ logger.info("ai.call.attempt_started", { callId, attempt, maximumAttempts, streaming: true });
1655
1786
  try {
1656
1787
  const candidate = await this.scheduleProviderRequest(provider, input.signal, async () => {
1657
1788
  const controller = new AbortController();
@@ -1684,6 +1815,14 @@ export class AiManager {
1684
1815
  input.signal?.removeEventListener("abort", forwardAbort);
1685
1816
  }
1686
1817
  });
1818
+ logger.info("ai.call.attempt_completed", {
1819
+ callId,
1820
+ attempt,
1821
+ status: candidate.status,
1822
+ ok: candidate.ok,
1823
+ durationMs: Number(process.hrtime.bigint() - attemptStartedAt) / 1_000_000,
1824
+ streaming: true
1825
+ });
1687
1826
  if (candidate.ok) {
1688
1827
  streamedResult = candidate.result;
1689
1828
  break;
@@ -1694,6 +1833,14 @@ export class AiManager {
1694
1833
  }
1695
1834
  catch (error) {
1696
1835
  lastFailure = error;
1836
+ logger.warn("ai.call.attempt_failed", {
1837
+ callId,
1838
+ attempt,
1839
+ retryable: !input.signal?.aborted && !emitted && attempt < maximumAttempts,
1840
+ durationMs: Number(process.hrtime.bigint() - attemptStartedAt) / 1_000_000,
1841
+ streaming: true,
1842
+ error: aiErrorForLog(error)
1843
+ });
1697
1844
  if (input.signal?.aborted || emitted || attempt >= maximumAttempts)
1698
1845
  throw error;
1699
1846
  }
@@ -1707,11 +1854,28 @@ export class AiManager {
1707
1854
  ? [{ id: thinkingStepId, type: "thinking", round: 1, content: reasoning, createdAt: thinkingCreatedAt }]
1708
1855
  : [];
1709
1856
  this.store.db.run("UPDATE ai_calls SET status = 'completed', output_chars = ?, completed_at = ? WHERE id = ?", content.length, now(), callId);
1857
+ logger.info("ai.call.completed", {
1858
+ callId,
1859
+ workId: input.workId,
1860
+ taskType: input.taskType,
1861
+ streaming: true,
1862
+ durationMs: Number(process.hrtime.bigint() - callStartedAt) / 1_000_000,
1863
+ outputChars: content.length,
1864
+ outputTokens
1865
+ });
1710
1866
  return { callId, content, outputTokens, provider: this.mapProvider(provider), model: this.mapModel(model), context, toolCalls: [], processSteps };
1711
1867
  }
1712
1868
  catch (error) {
1713
1869
  const message = error instanceof Error ? error.message : "AI 流式调用失败";
1714
1870
  this.store.db.run("UPDATE ai_calls SET status = 'failed', failure = ?, completed_at = ? WHERE id = ?", message, now(), callId);
1871
+ logger.error("ai.call.failed", {
1872
+ callId,
1873
+ workId: input.workId,
1874
+ taskType: input.taskType,
1875
+ streaming: true,
1876
+ durationMs: Number(process.hrtime.bigint() - callStartedAt) / 1_000_000,
1877
+ error: aiErrorForLog(error)
1878
+ });
1715
1879
  throw new AppError(502, "AI_CALL_FAILED", "AI 调用失败", { callId, failure: message });
1716
1880
  }
1717
1881
  }
@@ -3206,6 +3370,13 @@ export class AiManager {
3206
3370
  };
3207
3371
  signal?.addEventListener("abort", onAbort, { once: true });
3208
3372
  schedule.queue.push(entry);
3373
+ logger.debug("ai.provider_queue.enqueued", {
3374
+ providerId,
3375
+ active: schedule.active,
3376
+ queued: schedule.queue.length,
3377
+ concurrencyLimit: schedule.concurrencyLimit,
3378
+ rpmLimit: schedule.rpmLimit
3379
+ });
3209
3380
  this.pumpProviderSchedule(providerId);
3210
3381
  });
3211
3382
  }
@@ -3230,16 +3401,19 @@ export class AiManager {
3230
3401
  }
3231
3402
  schedule.active += 1;
3232
3403
  schedule.starts.push(Date.now());
3404
+ logger.debug("ai.provider_queue.dispatched", { providerId, active: schedule.active, queued: schedule.queue.length });
3233
3405
  void Promise.resolve()
3234
3406
  .then(entry.run)
3235
3407
  .then(entry.resolve, entry.reject)
3236
3408
  .finally(() => {
3237
3409
  schedule.active -= 1;
3410
+ logger.debug("ai.provider_queue.finished", { providerId, active: schedule.active, queued: schedule.queue.length });
3238
3411
  this.pumpProviderSchedule(providerId);
3239
3412
  });
3240
3413
  }
3241
3414
  if (schedule.queue.length > 0 && schedule.active < schedule.concurrencyLimit && schedule.starts.length >= schedule.rpmLimit) {
3242
3415
  const delay = Math.max(1, (schedule.starts[0] ?? Date.now()) + 60_000 - Date.now() + 1);
3416
+ logger.info("ai.provider_queue.rate_limited", { providerId, queued: schedule.queue.length, retryInMs: delay });
3243
3417
  schedule.timer = setTimeout(() => this.pumpProviderSchedule(providerId), delay);
3244
3418
  schedule.timer.unref?.();
3245
3419
  }