@musnows/scriverse 0.4.12 → 0.5.0

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/dist/ai.js CHANGED
@@ -33,6 +33,59 @@ function thinkingParameters(provider, model) {
33
33
  return { thinking: { type: boolValue(model, "thinking_enabled") ? "enabled" : "disabled" } };
34
34
  }
35
35
  const AGENT_TOOL_IDS = ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections"];
36
+ function redactProviderSecret(value, apiKey) {
37
+ if (!apiKey)
38
+ return value;
39
+ return value.split(apiKey).join("[REDACTED]");
40
+ }
41
+ function redactProviderSecrets(value, apiKey, depth = 0) {
42
+ if (typeof value === "string")
43
+ return redactProviderSecret(value, apiKey);
44
+ if (value === null || typeof value === "number" || typeof value === "boolean")
45
+ return value;
46
+ if (depth >= 32)
47
+ return "[REDACTED_DEPTH_LIMIT]";
48
+ if (Array.isArray(value))
49
+ return value.map((item) => redactProviderSecrets(item, apiKey, depth + 1));
50
+ if (!value || typeof value !== "object")
51
+ return null;
52
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactProviderSecrets(item, apiKey, depth + 1)]));
53
+ }
54
+ function sanitizeCompletionTraceResponse(value) {
55
+ const response = value && typeof value === "object" && !Array.isArray(value) ? value : {};
56
+ const choices = Array.isArray(response.choices) ? response.choices : [];
57
+ return {
58
+ choices: choices.map((choice) => {
59
+ const choiceRecord = choice && typeof choice === "object" && !Array.isArray(choice) ? choice : {};
60
+ const message = choiceRecord.message && typeof choiceRecord.message === "object" && !Array.isArray(choiceRecord.message)
61
+ ? choiceRecord.message
62
+ : {};
63
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
64
+ return {
65
+ finish_reason: typeof choiceRecord.finish_reason === "string" || choiceRecord.finish_reason === null ? choiceRecord.finish_reason : null,
66
+ message: {
67
+ content: typeof message.content === "string" || message.content === null ? message.content : null,
68
+ reasoning_content: typeof message.reasoning_content === "string" || message.reasoning_content === null ? message.reasoning_content : null,
69
+ tool_calls: toolCalls.map((toolCall) => {
70
+ const toolCallRecord = toolCall && typeof toolCall === "object" && !Array.isArray(toolCall) ? toolCall : {};
71
+ const fn = toolCallRecord.function && typeof toolCallRecord.function === "object" && !Array.isArray(toolCallRecord.function)
72
+ ? toolCallRecord.function
73
+ : {};
74
+ return {
75
+ id: typeof toolCallRecord.id === "string" ? toolCallRecord.id : "",
76
+ type: "function",
77
+ function: {
78
+ name: typeof fn.name === "string" ? fn.name : "",
79
+ arguments: fn.arguments ?? ""
80
+ }
81
+ };
82
+ })
83
+ }
84
+ };
85
+ }),
86
+ ...(response.usage && typeof response.usage === "object" && !Array.isArray(response.usage) ? { usage: response.usage } : {})
87
+ };
88
+ }
36
89
  const MAX_AGENT_TOOL_ROUNDS = 6;
37
90
  const MAX_AGENT_TOOL_CALLS = 12;
38
91
  const MAX_CONFIGURED_AGENT_TOOL_CALLS = 48;
@@ -1223,6 +1276,7 @@ export class AiManager {
1223
1276
  return this.store.db.all("SELECT * FROM ai_calls WHERE work_id = ? ORDER BY created_at DESC LIMIT 200", workId).map((row) => ({
1224
1277
  id: stringValue(row, "id"),
1225
1278
  workId: stringValue(row, "work_id"),
1279
+ taskId: row.task_id === null ? null : stringValue(row, "task_id"),
1226
1280
  taskType: stringValue(row, "task_type"),
1227
1281
  provider: this.getProvider(stringValue(row, "provider_id")),
1228
1282
  model: this.getModel(stringValue(row, "model_id")),
@@ -1243,6 +1297,7 @@ export class AiManager {
1243
1297
  return paginated(rows.map((row) => ({
1244
1298
  id: stringValue(row, "id"),
1245
1299
  workId: stringValue(row, "work_id"),
1300
+ taskId: row.task_id === null ? null : stringValue(row, "task_id"),
1246
1301
  taskType: stringValue(row, "task_type"),
1247
1302
  provider: this.getProvider(stringValue(row, "provider_id")),
1248
1303
  model: this.getModel(stringValue(row, "model_id")),
@@ -1256,6 +1311,55 @@ export class AiManager {
1256
1311
  completedAt: row.completed_at === null ? null : stringValue(row, "completed_at")
1257
1312
  })), pagination);
1258
1313
  }
1314
+ getTaskTrace(taskId) {
1315
+ this.store.getTask(taskId);
1316
+ const rows = this.store.db.all(`SELECT call.*, trace.initial_messages_json, trace.rounds_json, trace.created_at AS trace_created_at,
1317
+ trace.updated_at AS trace_updated_at, provider.name AS provider_name,
1318
+ model.display_name AS model_display_name, model.model_id AS external_model_id
1319
+ FROM ai_calls call
1320
+ LEFT JOIN ai_call_traces trace ON trace.call_id = call.id
1321
+ LEFT JOIN providers provider ON provider.id = call.provider_id
1322
+ LEFT JOIN models model ON model.id = call.model_id
1323
+ WHERE call.task_id = ?
1324
+ ORDER BY call.created_at ASC, call.id ASC`, taskId);
1325
+ const calls = rows.map((row) => {
1326
+ const hasTrace = row.initial_messages_json !== null && row.initial_messages_json !== undefined;
1327
+ return {
1328
+ id: stringValue(row, "id"),
1329
+ taskType: stringValue(row, "task_type"),
1330
+ provider: {
1331
+ id: stringValue(row, "provider_id"),
1332
+ name: row.provider_name === null ? "已删除的供应商" : stringValue(row, "provider_name"),
1333
+ deleted: row.provider_name === null
1334
+ },
1335
+ model: {
1336
+ id: stringValue(row, "model_id"),
1337
+ displayName: row.model_display_name === null ? "已删除的模型" : stringValue(row, "model_display_name"),
1338
+ modelId: row.external_model_id === null ? null : stringValue(row, "external_model_id"),
1339
+ deleted: row.model_display_name === null
1340
+ },
1341
+ contextScope: json(stringValue(row, "context_scope_json"), {}),
1342
+ parameters: json(stringValue(row, "parameters_json"), {}),
1343
+ status: stringValue(row, "status"),
1344
+ failure: row.failure === null ? null : stringValue(row, "failure"),
1345
+ inputChars: numberValue(row, "input_chars"),
1346
+ outputChars: numberValue(row, "output_chars"),
1347
+ createdAt: stringValue(row, "created_at"),
1348
+ completedAt: row.completed_at === null ? null : stringValue(row, "completed_at"),
1349
+ trace: hasTrace ? {
1350
+ initialMessages: json(stringValue(row, "initial_messages_json"), []),
1351
+ rounds: json(stringValue(row, "rounds_json"), []),
1352
+ createdAt: stringValue(row, "trace_created_at"),
1353
+ updatedAt: stringValue(row, "trace_updated_at")
1354
+ } : null
1355
+ };
1356
+ });
1357
+ return {
1358
+ taskId,
1359
+ captured: calls.some((call) => call.trace !== null),
1360
+ calls
1361
+ };
1362
+ }
1259
1363
  async runTask(taskId, modelId) {
1260
1364
  const task = this.store.getTask(taskId);
1261
1365
  const workId = String(task.workId);
@@ -1310,6 +1414,7 @@ export class AiManager {
1310
1414
  else {
1311
1415
  const generated = await this.generate({
1312
1416
  workId,
1417
+ taskId,
1313
1418
  taskType: taskType === "book-analysis" ? "book-analysis" : "chapter-analysis",
1314
1419
  instruction: "请基于上下文完成分析,给出有原文依据的中文结论。",
1315
1420
  scope,
@@ -1771,8 +1876,20 @@ export class AiManager {
1771
1876
  });
1772
1877
  const callId = id("call");
1773
1878
  const timestamp = now();
1774
- this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
1775
- 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);
1879
+ const traceRounds = [];
1880
+ this.store.db.transaction(() => {
1881
+ this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
1882
+ status, input_chars, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?)`, callId, input.workId, input.taskId ?? null, input.taskType, stringValue(provider, "id"), stringValue(model, "id"), JSON.stringify(input.scope), JSON.stringify(parameters), context.length + input.instruction.length, timestamp, currentRequestActor()?.userId ?? null);
1883
+ if (input.taskId) {
1884
+ this.store.db.run(`INSERT INTO ai_call_traces (call_id, task_id, initial_messages_json, rounds_json, created_at, updated_at)
1885
+ VALUES (?, ?, ?, '[]', ?, ?)`, callId, input.taskId, JSON.stringify(messages), timestamp, timestamp);
1886
+ }
1887
+ });
1888
+ const saveTrace = () => {
1889
+ if (!input.taskId)
1890
+ return;
1891
+ this.store.db.run("UPDATE ai_call_traces SET rounds_json = ?, updated_at = ? WHERE call_id = ?", JSON.stringify(traceRounds), now(), callId);
1892
+ };
1776
1893
  const callStartedAt = process.hrtime.bigint();
1777
1894
  logger.info("ai.call.started", {
1778
1895
  callId,
@@ -1785,8 +1902,10 @@ export class AiManager {
1785
1902
  instructionChars: input.instruction.length,
1786
1903
  toolCount: tools.length
1787
1904
  });
1905
+ let activeApiKey = "";
1788
1906
  try {
1789
1907
  const apiKey = this.decryptKey(provider);
1908
+ activeApiKey = apiKey;
1790
1909
  const endpoint = `${normalizeBaseUrl(stringValue(provider, "base_url"))}/chat/completions`;
1791
1910
  const timeoutMs = input.taskType === "book-analysis" || input.taskType === "relationship-analysis" ? 300_000 : 60_000;
1792
1911
  const maximumAttempts = Math.round(clamp(input.maxAttempts ?? 3, 1, 5));
@@ -1795,10 +1914,32 @@ export class AiManager {
1795
1914
  let totalInputTokens = 0;
1796
1915
  let totalCachedInputTokens = 0;
1797
1916
  const requestCompletion = async (toolChoice) => {
1917
+ const traceRound = {
1918
+ round: traceRounds.length + 1,
1919
+ requestedAt: now(),
1920
+ request: {
1921
+ model: stringValue(model, "model_id"),
1922
+ messages: structuredClone(completionMessages),
1923
+ parameters: structuredClone(parameters),
1924
+ tools: toolChoice === "auto" ? structuredClone(tools) : [],
1925
+ toolChoice
1926
+ },
1927
+ attempts: [],
1928
+ toolExecutions: []
1929
+ };
1930
+ traceRounds.push(traceRound);
1931
+ saveTrace();
1798
1932
  let lastFailure = null;
1799
1933
  for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
1800
1934
  let retryable = true;
1801
1935
  const attemptStartedAt = process.hrtime.bigint();
1936
+ const traceAttempt = {
1937
+ attempt,
1938
+ startedAt: now(),
1939
+ status: "running"
1940
+ };
1941
+ traceRound.attempts.push(traceAttempt);
1942
+ saveTrace();
1802
1943
  logger.info("ai.call.attempt_started", { callId, attempt, maximumAttempts, toolChoice });
1803
1944
  try {
1804
1945
  const candidate = await this.scheduleProviderRequest(provider, input.signal, async () => {
@@ -1837,7 +1978,12 @@ export class AiManager {
1837
1978
  });
1838
1979
  if (candidate.ok) {
1839
1980
  try {
1840
- const parsed = JSON.parse(candidate.body);
1981
+ const parsed = redactProviderSecrets(JSON.parse(candidate.body), apiKey);
1982
+ traceAttempt.completedAt = now();
1983
+ traceAttempt.status = "completed";
1984
+ traceAttempt.httpStatus = candidate.status;
1985
+ traceAttempt.response = sanitizeCompletionTraceResponse(parsed);
1986
+ saveTrace();
1841
1987
  completionRequestCount += 1;
1842
1988
  const cacheUsage = resolveInputCacheUsage(parsed.usage);
1843
1989
  if (!cacheUsage)
@@ -1853,6 +1999,11 @@ export class AiManager {
1853
1999
  }
1854
2000
  }
1855
2001
  lastFailure = new Error(`HTTP ${candidate.status}: ${candidate.body.slice(0, 500)}`);
2002
+ traceAttempt.completedAt = now();
2003
+ traceAttempt.status = "failed";
2004
+ traceAttempt.httpStatus = candidate.status;
2005
+ traceAttempt.failure = redactProviderSecret(`HTTP ${candidate.status}: ${candidate.body.slice(0, 2_000)}`, apiKey);
2006
+ saveTrace();
1856
2007
  if (candidate.status !== 429 && candidate.status < 500) {
1857
2008
  retryable = false;
1858
2009
  throw lastFailure;
@@ -1860,6 +2011,14 @@ export class AiManager {
1860
2011
  }
1861
2012
  catch (error) {
1862
2013
  lastFailure = error;
2014
+ if (traceAttempt.status === "running") {
2015
+ traceAttempt.completedAt = now();
2016
+ traceAttempt.status = "failed";
2017
+ traceAttempt.failure = error instanceof Error
2018
+ ? redactProviderSecret(error.message.slice(0, 2_000), apiKey)
2019
+ : "AI request failed";
2020
+ saveTrace();
2021
+ }
1863
2022
  logger.warn("ai.call.attempt_failed", {
1864
2023
  callId,
1865
2024
  attempt,
@@ -1921,6 +2080,8 @@ export class AiManager {
1921
2080
  const execution = this.executeAgentTool(input.workId, toolCall);
1922
2081
  logger.info("ai.tool_call.completed", { callId, toolName: execution.name, status: execution.status, round });
1923
2082
  executedToolCalls.push(execution);
2083
+ traceRounds.at(-1)?.toolExecutions.push(execution);
2084
+ saveTrace();
1924
2085
  processSteps.push({ id: id("process"), type: "tool", round, toolCall: execution, createdAt: execution.calledAt });
1925
2086
  input.onToolCall?.(execution, round);
1926
2087
  completionMessages.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(execution.result) });
@@ -1966,7 +2127,7 @@ export class AiManager {
1966
2127
  return { callId, content, outputTokens, ...(cacheHitPercent === undefined ? {} : { cacheHitPercent }), provider: this.mapProvider(provider), model: this.mapModel(model), context, toolCalls: executedToolCalls, processSteps };
1967
2128
  }
1968
2129
  catch (error) {
1969
- const message = error instanceof Error ? error.message : "AI 调用失败";
2130
+ const message = error instanceof Error ? redactProviderSecret(error.message, activeApiKey) : "AI 调用失败";
1970
2131
  this.store.db.run("UPDATE ai_calls SET status = 'failed', failure = ?, completed_at = ? WHERE id = ?", message, now(), callId);
1971
2132
  logger.error("ai.call.failed", {
1972
2133
  callId,
@@ -2170,6 +2331,7 @@ export class AiManager {
2170
2331
  const chapter = this.store.getChapter(scope.chapterId);
2171
2332
  const generated = await this.generateTaggedJson({
2172
2333
  workId,
2334
+ taskId,
2173
2335
  taskType: "chapter-analysis",
2174
2336
  signal: this.taskSignal(taskId),
2175
2337
  instruction: "分析本章并输出 JSON 对象,字段为 summary(1至3句)、events(数组)、characters(数组)、settings(数组)、evidence(数组,每项含 conclusion 和 quote)、uncertainties(数组)。",
@@ -2189,6 +2351,7 @@ export class AiManager {
2189
2351
  async runTimelineAnalysis(workId, scope, modelId, taskId) {
2190
2352
  const generated = await this.generateTaggedJson({
2191
2353
  workId,
2354
+ taskId,
2192
2355
  taskType: "timeline-analysis",
2193
2356
  signal: this.taskSignal(taskId),
2194
2357
  instruction: "抽取大事件候选并输出 JSON 数组。每项字段:name、description、eventType、timeLabel、timeSort(无法确定为 null)、location、impactScope、chapterIds、participantIds、evidence。必须区分发生时间与叙述时间;不确定时使用‘时间待定’。",
@@ -2228,6 +2391,7 @@ export class AiManager {
2228
2391
  throw new AppError(409, "CHAPTERS_REQUIRED", "世界观分析范围内没有章节");
2229
2392
  const generated = await this.generateTaggedJson({
2230
2393
  workId,
2394
+ taskId,
2231
2395
  taskType: "book-analysis",
2232
2396
  signal: this.taskSignal(taskId),
2233
2397
  instruction: [
@@ -2325,6 +2489,7 @@ export class AiManager {
2325
2489
  return { candidates: [], callId: null };
2326
2490
  const generated = await this.generateTaggedJson({
2327
2491
  workId,
2492
+ taskId,
2328
2493
  taskType: "book-analysis",
2329
2494
  signal: this.taskSignal(taskId),
2330
2495
  maxAttempts: 2,
@@ -2468,6 +2633,7 @@ export class AiManager {
2468
2633
  async runConsistencyCheck(workId, scope, modelId, taskId) {
2469
2634
  const generated = await this.generateTaggedJson({
2470
2635
  workId,
2636
+ taskId,
2471
2637
  taskType: "consistency-check",
2472
2638
  signal: this.taskSignal(taskId),
2473
2639
  instruction: "检查设定、人物状态、关系和时间是否冲突,输出 JSON 数组。每项字段:itemType、severity(low/medium/high)、title、description、entityRefs、evidence、suggestion。没有问题时输出 []。",
@@ -2523,6 +2689,7 @@ export class AiManager {
2523
2689
  }).join("\n");
2524
2690
  const generated = await this.generateTaggedJson({
2525
2691
  workId,
2692
+ taskId,
2526
2693
  taskType: "book-analysis",
2527
2694
  signal: this.taskSignal(taskId),
2528
2695
  scope: scope.type === "none" ? scope : { type: "none" },
@@ -2643,6 +2810,7 @@ export class AiManager {
2643
2810
  async verifyCharacterTitlePairs(workId, pairs, modelId, taskId) {
2644
2811
  const generated = await this.generateTaggedJson({
2645
2812
  workId,
2813
+ taskId,
2646
2814
  taskType: "book-analysis",
2647
2815
  signal: this.taskSignal(taskId),
2648
2816
  scope: { type: "none" },
@@ -2696,6 +2864,7 @@ export class AiManager {
2696
2864
  const extractChunk = async (text, maxAttempts = 3) => {
2697
2865
  const generated = await this.generateTaggedJson({
2698
2866
  workId,
2867
+ taskId,
2699
2868
  taskType: "book-analysis",
2700
2869
  signal: this.taskSignal(taskId),
2701
2870
  maxAttempts,
@@ -3073,6 +3242,7 @@ export class AiManager {
3073
3242
  const extractChunk = async (text, maxAttempts = 3) => {
3074
3243
  const generated = await this.generateTaggedJson({
3075
3244
  workId,
3245
+ taskId,
3076
3246
  taskType: "relationship-analysis",
3077
3247
  signal: this.taskSignal(taskId),
3078
3248
  maxAttempts,
@@ -3210,6 +3380,7 @@ export class AiManager {
3210
3380
  const aggregationResults = await this.processChunks(evidenceBatches, Math.min(concurrency, 4), async (evidenceBatch) => {
3211
3381
  const generated = await this.generateTaggedJson({
3212
3382
  workId,
3383
+ taskId,
3213
3384
  taskType: "relationship-analysis",
3214
3385
  signal: this.taskSignal(taskId),
3215
3386
  maxAttempts: 2,