@musnows/scriverse 0.4.12 → 0.5.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/dist/ai.js +279 -4
- package/dist/ai.js.map +1 -1
- package/dist/app.js +34 -11
- package/dist/app.js.map +1 -1
- package/dist/database.js +55 -0
- package/dist/database.js.map +1 -1
- package/dist/pagination.js +1 -1
- package/dist/public/app.js +470 -75
- package/dist/public/global-search.d.ts +12 -0
- package/dist/public/global-search.js +23 -0
- package/dist/public/index.html +17 -4
- package/dist/public/relationship-filters.d.ts +10 -0
- package/dist/public/relationship-filters.js +11 -0
- package/dist/public/relationship-graph.js +59 -3
- package/dist/public/styles.css +244 -5
- package/dist/store.js +110 -32
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +12 -0
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/ai.js
CHANGED
|
@@ -33,6 +33,119 @@ 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
|
+
const TASK_TRACE_PREVIEW_CHARACTER_LIMIT = 3_000;
|
|
37
|
+
function traceRecord(value) {
|
|
38
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
39
|
+
}
|
|
40
|
+
function previewTaskTraceMessages(messages) {
|
|
41
|
+
const records = messages.map(traceRecord);
|
|
42
|
+
const contents = records.map((message) => message.content === null ? "" : String(message.content ?? ""));
|
|
43
|
+
const allocations = contents.map(() => 0);
|
|
44
|
+
let remaining = TASK_TRACE_PREVIEW_CHARACTER_LIMIT;
|
|
45
|
+
let active = contents.map((_, index) => index).filter((index) => contents[index].length > 0);
|
|
46
|
+
while (remaining > 0 && active.length > 0) {
|
|
47
|
+
const share = Math.max(1, Math.floor(remaining / active.length));
|
|
48
|
+
const next = [];
|
|
49
|
+
for (const index of active) {
|
|
50
|
+
if (remaining <= 0)
|
|
51
|
+
break;
|
|
52
|
+
const available = contents[index].length - allocations[index];
|
|
53
|
+
const granted = Math.min(available, share, remaining);
|
|
54
|
+
allocations[index] = allocations[index] + granted;
|
|
55
|
+
remaining -= granted;
|
|
56
|
+
if (allocations[index] < contents[index].length)
|
|
57
|
+
next.push(index);
|
|
58
|
+
}
|
|
59
|
+
active = next;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
messages: records.map((message, index) => {
|
|
63
|
+
const content = contents[index];
|
|
64
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
65
|
+
return {
|
|
66
|
+
role: typeof message.role === "string" ? message.role : "user",
|
|
67
|
+
content: content.slice(0, allocations[index]),
|
|
68
|
+
contentChars: content.length,
|
|
69
|
+
contentTruncated: allocations[index] < content.length,
|
|
70
|
+
toolCallCount: toolCalls.length,
|
|
71
|
+
...(typeof message.tool_call_id === "string" ? { tool_call_id: message.tool_call_id } : {})
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
totalChars: contents.reduce((total, content) => total + content.length, 0),
|
|
75
|
+
truncated: contents.some((content, index) => allocations[index] < content.length)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function summarizeTaskTraceRound(value) {
|
|
79
|
+
const round = traceRecord(value);
|
|
80
|
+
const request = traceRecord(round.request);
|
|
81
|
+
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
82
|
+
const attempts = Array.isArray(round.attempts) ? round.attempts : [];
|
|
83
|
+
const toolExecutions = Array.isArray(round.toolExecutions) ? round.toolExecutions : [];
|
|
84
|
+
return {
|
|
85
|
+
round: typeof round.round === "number" ? round.round : 1,
|
|
86
|
+
requestedAt: typeof round.requestedAt === "string" ? round.requestedAt : "",
|
|
87
|
+
messageCount: messages.length,
|
|
88
|
+
promptChars: messages.reduce((total, message) => {
|
|
89
|
+
const content = traceRecord(message).content;
|
|
90
|
+
return total + (content === null ? 0 : String(content ?? "").length);
|
|
91
|
+
}, 0),
|
|
92
|
+
attemptCount: attempts.length,
|
|
93
|
+
toolExecutionCount: toolExecutions.length
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function redactProviderSecret(value, apiKey) {
|
|
97
|
+
if (!apiKey)
|
|
98
|
+
return value;
|
|
99
|
+
return value.split(apiKey).join("[REDACTED]");
|
|
100
|
+
}
|
|
101
|
+
function redactProviderSecrets(value, apiKey, depth = 0) {
|
|
102
|
+
if (typeof value === "string")
|
|
103
|
+
return redactProviderSecret(value, apiKey);
|
|
104
|
+
if (value === null || typeof value === "number" || typeof value === "boolean")
|
|
105
|
+
return value;
|
|
106
|
+
if (depth >= 32)
|
|
107
|
+
return "[REDACTED_DEPTH_LIMIT]";
|
|
108
|
+
if (Array.isArray(value))
|
|
109
|
+
return value.map((item) => redactProviderSecrets(item, apiKey, depth + 1));
|
|
110
|
+
if (!value || typeof value !== "object")
|
|
111
|
+
return null;
|
|
112
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactProviderSecrets(item, apiKey, depth + 1)]));
|
|
113
|
+
}
|
|
114
|
+
function sanitizeCompletionTraceResponse(value) {
|
|
115
|
+
const response = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
116
|
+
const choices = Array.isArray(response.choices) ? response.choices : [];
|
|
117
|
+
return {
|
|
118
|
+
choices: choices.map((choice) => {
|
|
119
|
+
const choiceRecord = choice && typeof choice === "object" && !Array.isArray(choice) ? choice : {};
|
|
120
|
+
const message = choiceRecord.message && typeof choiceRecord.message === "object" && !Array.isArray(choiceRecord.message)
|
|
121
|
+
? choiceRecord.message
|
|
122
|
+
: {};
|
|
123
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
124
|
+
return {
|
|
125
|
+
finish_reason: typeof choiceRecord.finish_reason === "string" || choiceRecord.finish_reason === null ? choiceRecord.finish_reason : null,
|
|
126
|
+
message: {
|
|
127
|
+
content: typeof message.content === "string" || message.content === null ? message.content : null,
|
|
128
|
+
reasoning_content: typeof message.reasoning_content === "string" || message.reasoning_content === null ? message.reasoning_content : null,
|
|
129
|
+
tool_calls: toolCalls.map((toolCall) => {
|
|
130
|
+
const toolCallRecord = toolCall && typeof toolCall === "object" && !Array.isArray(toolCall) ? toolCall : {};
|
|
131
|
+
const fn = toolCallRecord.function && typeof toolCallRecord.function === "object" && !Array.isArray(toolCallRecord.function)
|
|
132
|
+
? toolCallRecord.function
|
|
133
|
+
: {};
|
|
134
|
+
return {
|
|
135
|
+
id: typeof toolCallRecord.id === "string" ? toolCallRecord.id : "",
|
|
136
|
+
type: "function",
|
|
137
|
+
function: {
|
|
138
|
+
name: typeof fn.name === "string" ? fn.name : "",
|
|
139
|
+
arguments: fn.arguments ?? ""
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}),
|
|
146
|
+
...(response.usage && typeof response.usage === "object" && !Array.isArray(response.usage) ? { usage: response.usage } : {})
|
|
147
|
+
};
|
|
148
|
+
}
|
|
36
149
|
const MAX_AGENT_TOOL_ROUNDS = 6;
|
|
37
150
|
const MAX_AGENT_TOOL_CALLS = 12;
|
|
38
151
|
const MAX_CONFIGURED_AGENT_TOOL_CALLS = 48;
|
|
@@ -1223,6 +1336,7 @@ export class AiManager {
|
|
|
1223
1336
|
return this.store.db.all("SELECT * FROM ai_calls WHERE work_id = ? ORDER BY created_at DESC LIMIT 200", workId).map((row) => ({
|
|
1224
1337
|
id: stringValue(row, "id"),
|
|
1225
1338
|
workId: stringValue(row, "work_id"),
|
|
1339
|
+
taskId: row.task_id === null ? null : stringValue(row, "task_id"),
|
|
1226
1340
|
taskType: stringValue(row, "task_type"),
|
|
1227
1341
|
provider: this.getProvider(stringValue(row, "provider_id")),
|
|
1228
1342
|
model: this.getModel(stringValue(row, "model_id")),
|
|
@@ -1243,6 +1357,7 @@ export class AiManager {
|
|
|
1243
1357
|
return paginated(rows.map((row) => ({
|
|
1244
1358
|
id: stringValue(row, "id"),
|
|
1245
1359
|
workId: stringValue(row, "work_id"),
|
|
1360
|
+
taskId: row.task_id === null ? null : stringValue(row, "task_id"),
|
|
1246
1361
|
taskType: stringValue(row, "task_type"),
|
|
1247
1362
|
provider: this.getProvider(stringValue(row, "provider_id")),
|
|
1248
1363
|
model: this.getModel(stringValue(row, "model_id")),
|
|
@@ -1256,6 +1371,99 @@ export class AiManager {
|
|
|
1256
1371
|
completedAt: row.completed_at === null ? null : stringValue(row, "completed_at")
|
|
1257
1372
|
})), pagination);
|
|
1258
1373
|
}
|
|
1374
|
+
getTaskTrace(taskId) {
|
|
1375
|
+
this.store.getTask(taskId);
|
|
1376
|
+
const rows = this.store.db.all(`SELECT call.id, call.task_type, call.provider_id, call.model_id, call.status, call.failure,
|
|
1377
|
+
call.input_chars, call.output_chars, call.created_at, call.completed_at, trace.call_id AS trace_call_id,
|
|
1378
|
+
CASE WHEN trace.call_id IS NULL THEN 0 ELSE json_array_length(trace.initial_messages_json) END AS initial_message_count,
|
|
1379
|
+
CASE WHEN trace.call_id IS NULL THEN 0 ELSE json_array_length(trace.rounds_json) END AS round_count,
|
|
1380
|
+
CASE WHEN trace.call_id IS NULL THEN 0 ELSE length(trace.initial_messages_json) + length(trace.rounds_json) END AS trace_chars,
|
|
1381
|
+
trace.created_at AS trace_created_at, trace.updated_at AS trace_updated_at, provider.name AS provider_name,
|
|
1382
|
+
model.display_name AS model_display_name, model.model_id AS external_model_id
|
|
1383
|
+
FROM ai_calls call
|
|
1384
|
+
LEFT JOIN ai_call_traces trace ON trace.call_id = call.id
|
|
1385
|
+
LEFT JOIN providers provider ON provider.id = call.provider_id
|
|
1386
|
+
LEFT JOIN models model ON model.id = call.model_id
|
|
1387
|
+
WHERE call.task_id = ?
|
|
1388
|
+
ORDER BY call.created_at ASC, call.id ASC`, taskId);
|
|
1389
|
+
const calls = rows.map((row) => {
|
|
1390
|
+
const hasTrace = row.trace_call_id !== null && row.trace_call_id !== undefined;
|
|
1391
|
+
const failure = row.failure === null ? null : stringValue(row, "failure");
|
|
1392
|
+
return {
|
|
1393
|
+
id: stringValue(row, "id"),
|
|
1394
|
+
taskType: stringValue(row, "task_type"),
|
|
1395
|
+
provider: {
|
|
1396
|
+
id: stringValue(row, "provider_id"),
|
|
1397
|
+
name: row.provider_name === null ? "已删除的供应商" : stringValue(row, "provider_name"),
|
|
1398
|
+
deleted: row.provider_name === null
|
|
1399
|
+
},
|
|
1400
|
+
model: {
|
|
1401
|
+
id: stringValue(row, "model_id"),
|
|
1402
|
+
displayName: row.model_display_name === null ? "已删除的模型" : stringValue(row, "model_display_name"),
|
|
1403
|
+
modelId: row.external_model_id === null ? null : stringValue(row, "external_model_id"),
|
|
1404
|
+
deleted: row.model_display_name === null
|
|
1405
|
+
},
|
|
1406
|
+
status: stringValue(row, "status"),
|
|
1407
|
+
failure: failure === null ? null : failure.slice(0, 1_000),
|
|
1408
|
+
failureTruncated: failure !== null && failure.length > 1_000,
|
|
1409
|
+
inputChars: numberValue(row, "input_chars"),
|
|
1410
|
+
outputChars: numberValue(row, "output_chars"),
|
|
1411
|
+
createdAt: stringValue(row, "created_at"),
|
|
1412
|
+
completedAt: row.completed_at === null ? null : stringValue(row, "completed_at"),
|
|
1413
|
+
trace: hasTrace ? {
|
|
1414
|
+
available: true,
|
|
1415
|
+
initialMessageCount: numberValue(row, "initial_message_count"),
|
|
1416
|
+
roundCount: numberValue(row, "round_count"),
|
|
1417
|
+
serializedChars: numberValue(row, "trace_chars"),
|
|
1418
|
+
createdAt: stringValue(row, "trace_created_at"),
|
|
1419
|
+
updatedAt: stringValue(row, "trace_updated_at")
|
|
1420
|
+
} : null
|
|
1421
|
+
};
|
|
1422
|
+
});
|
|
1423
|
+
return {
|
|
1424
|
+
taskId,
|
|
1425
|
+
captured: calls.some((call) => call.trace !== null),
|
|
1426
|
+
calls
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
getTaskTraceCall(taskId, callId, full = false) {
|
|
1430
|
+
this.store.getTask(taskId);
|
|
1431
|
+
const row = this.store.db.get(`SELECT trace.initial_messages_json, trace.rounds_json, trace.created_at, trace.updated_at
|
|
1432
|
+
FROM ai_calls call JOIN ai_call_traces trace ON trace.call_id = call.id AND trace.task_id = call.task_id
|
|
1433
|
+
WHERE call.id = ? AND call.task_id = ?`, callId, taskId);
|
|
1434
|
+
if (!row)
|
|
1435
|
+
throw notFound("AI 调用追踪");
|
|
1436
|
+
const initialMessages = json(stringValue(row, "initial_messages_json"), []);
|
|
1437
|
+
const rounds = json(stringValue(row, "rounds_json"), []);
|
|
1438
|
+
if (full) {
|
|
1439
|
+
return {
|
|
1440
|
+
taskId,
|
|
1441
|
+
callId,
|
|
1442
|
+
mode: "full",
|
|
1443
|
+
trace: {
|
|
1444
|
+
initialMessages,
|
|
1445
|
+
rounds,
|
|
1446
|
+
createdAt: stringValue(row, "created_at"),
|
|
1447
|
+
updatedAt: stringValue(row, "updated_at")
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
const preview = previewTaskTraceMessages(initialMessages);
|
|
1452
|
+
return {
|
|
1453
|
+
taskId,
|
|
1454
|
+
callId,
|
|
1455
|
+
mode: "preview",
|
|
1456
|
+
previewLimit: TASK_TRACE_PREVIEW_CHARACTER_LIMIT,
|
|
1457
|
+
truncated: preview.truncated,
|
|
1458
|
+
totalPromptChars: preview.totalChars,
|
|
1459
|
+
trace: {
|
|
1460
|
+
initialMessages: preview.messages,
|
|
1461
|
+
rounds: rounds.map(summarizeTaskTraceRound),
|
|
1462
|
+
createdAt: stringValue(row, "created_at"),
|
|
1463
|
+
updatedAt: stringValue(row, "updated_at")
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1259
1467
|
async runTask(taskId, modelId) {
|
|
1260
1468
|
const task = this.store.getTask(taskId);
|
|
1261
1469
|
const workId = String(task.workId);
|
|
@@ -1310,6 +1518,7 @@ export class AiManager {
|
|
|
1310
1518
|
else {
|
|
1311
1519
|
const generated = await this.generate({
|
|
1312
1520
|
workId,
|
|
1521
|
+
taskId,
|
|
1313
1522
|
taskType: taskType === "book-analysis" ? "book-analysis" : "chapter-analysis",
|
|
1314
1523
|
instruction: "请基于上下文完成分析,给出有原文依据的中文结论。",
|
|
1315
1524
|
scope,
|
|
@@ -1771,8 +1980,20 @@ export class AiManager {
|
|
|
1771
1980
|
});
|
|
1772
1981
|
const callId = id("call");
|
|
1773
1982
|
const timestamp = now();
|
|
1774
|
-
|
|
1775
|
-
|
|
1983
|
+
const traceRounds = [];
|
|
1984
|
+
this.store.db.transaction(() => {
|
|
1985
|
+
this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
|
|
1986
|
+
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);
|
|
1987
|
+
if (input.taskId) {
|
|
1988
|
+
this.store.db.run(`INSERT INTO ai_call_traces (call_id, task_id, initial_messages_json, rounds_json, created_at, updated_at)
|
|
1989
|
+
VALUES (?, ?, ?, '[]', ?, ?)`, callId, input.taskId, JSON.stringify(messages), timestamp, timestamp);
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
const saveTrace = () => {
|
|
1993
|
+
if (!input.taskId)
|
|
1994
|
+
return;
|
|
1995
|
+
this.store.db.run("UPDATE ai_call_traces SET rounds_json = ?, updated_at = ? WHERE call_id = ?", JSON.stringify(traceRounds), now(), callId);
|
|
1996
|
+
};
|
|
1776
1997
|
const callStartedAt = process.hrtime.bigint();
|
|
1777
1998
|
logger.info("ai.call.started", {
|
|
1778
1999
|
callId,
|
|
@@ -1785,8 +2006,10 @@ export class AiManager {
|
|
|
1785
2006
|
instructionChars: input.instruction.length,
|
|
1786
2007
|
toolCount: tools.length
|
|
1787
2008
|
});
|
|
2009
|
+
let activeApiKey = "";
|
|
1788
2010
|
try {
|
|
1789
2011
|
const apiKey = this.decryptKey(provider);
|
|
2012
|
+
activeApiKey = apiKey;
|
|
1790
2013
|
const endpoint = `${normalizeBaseUrl(stringValue(provider, "base_url"))}/chat/completions`;
|
|
1791
2014
|
const timeoutMs = input.taskType === "book-analysis" || input.taskType === "relationship-analysis" ? 300_000 : 60_000;
|
|
1792
2015
|
const maximumAttempts = Math.round(clamp(input.maxAttempts ?? 3, 1, 5));
|
|
@@ -1795,10 +2018,32 @@ export class AiManager {
|
|
|
1795
2018
|
let totalInputTokens = 0;
|
|
1796
2019
|
let totalCachedInputTokens = 0;
|
|
1797
2020
|
const requestCompletion = async (toolChoice) => {
|
|
2021
|
+
const traceRound = {
|
|
2022
|
+
round: traceRounds.length + 1,
|
|
2023
|
+
requestedAt: now(),
|
|
2024
|
+
request: {
|
|
2025
|
+
model: stringValue(model, "model_id"),
|
|
2026
|
+
messages: structuredClone(completionMessages),
|
|
2027
|
+
parameters: structuredClone(parameters),
|
|
2028
|
+
tools: toolChoice === "auto" ? structuredClone(tools) : [],
|
|
2029
|
+
toolChoice
|
|
2030
|
+
},
|
|
2031
|
+
attempts: [],
|
|
2032
|
+
toolExecutions: []
|
|
2033
|
+
};
|
|
2034
|
+
traceRounds.push(traceRound);
|
|
2035
|
+
saveTrace();
|
|
1798
2036
|
let lastFailure = null;
|
|
1799
2037
|
for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
|
|
1800
2038
|
let retryable = true;
|
|
1801
2039
|
const attemptStartedAt = process.hrtime.bigint();
|
|
2040
|
+
const traceAttempt = {
|
|
2041
|
+
attempt,
|
|
2042
|
+
startedAt: now(),
|
|
2043
|
+
status: "running"
|
|
2044
|
+
};
|
|
2045
|
+
traceRound.attempts.push(traceAttempt);
|
|
2046
|
+
saveTrace();
|
|
1802
2047
|
logger.info("ai.call.attempt_started", { callId, attempt, maximumAttempts, toolChoice });
|
|
1803
2048
|
try {
|
|
1804
2049
|
const candidate = await this.scheduleProviderRequest(provider, input.signal, async () => {
|
|
@@ -1837,7 +2082,12 @@ export class AiManager {
|
|
|
1837
2082
|
});
|
|
1838
2083
|
if (candidate.ok) {
|
|
1839
2084
|
try {
|
|
1840
|
-
const parsed = JSON.parse(candidate.body);
|
|
2085
|
+
const parsed = redactProviderSecrets(JSON.parse(candidate.body), apiKey);
|
|
2086
|
+
traceAttempt.completedAt = now();
|
|
2087
|
+
traceAttempt.status = "completed";
|
|
2088
|
+
traceAttempt.httpStatus = candidate.status;
|
|
2089
|
+
traceAttempt.response = sanitizeCompletionTraceResponse(parsed);
|
|
2090
|
+
saveTrace();
|
|
1841
2091
|
completionRequestCount += 1;
|
|
1842
2092
|
const cacheUsage = resolveInputCacheUsage(parsed.usage);
|
|
1843
2093
|
if (!cacheUsage)
|
|
@@ -1853,6 +2103,11 @@ export class AiManager {
|
|
|
1853
2103
|
}
|
|
1854
2104
|
}
|
|
1855
2105
|
lastFailure = new Error(`HTTP ${candidate.status}: ${candidate.body.slice(0, 500)}`);
|
|
2106
|
+
traceAttempt.completedAt = now();
|
|
2107
|
+
traceAttempt.status = "failed";
|
|
2108
|
+
traceAttempt.httpStatus = candidate.status;
|
|
2109
|
+
traceAttempt.failure = redactProviderSecret(`HTTP ${candidate.status}: ${candidate.body.slice(0, 2_000)}`, apiKey);
|
|
2110
|
+
saveTrace();
|
|
1856
2111
|
if (candidate.status !== 429 && candidate.status < 500) {
|
|
1857
2112
|
retryable = false;
|
|
1858
2113
|
throw lastFailure;
|
|
@@ -1860,6 +2115,14 @@ export class AiManager {
|
|
|
1860
2115
|
}
|
|
1861
2116
|
catch (error) {
|
|
1862
2117
|
lastFailure = error;
|
|
2118
|
+
if (traceAttempt.status === "running") {
|
|
2119
|
+
traceAttempt.completedAt = now();
|
|
2120
|
+
traceAttempt.status = "failed";
|
|
2121
|
+
traceAttempt.failure = error instanceof Error
|
|
2122
|
+
? redactProviderSecret(error.message.slice(0, 2_000), apiKey)
|
|
2123
|
+
: "AI request failed";
|
|
2124
|
+
saveTrace();
|
|
2125
|
+
}
|
|
1863
2126
|
logger.warn("ai.call.attempt_failed", {
|
|
1864
2127
|
callId,
|
|
1865
2128
|
attempt,
|
|
@@ -1921,6 +2184,8 @@ export class AiManager {
|
|
|
1921
2184
|
const execution = this.executeAgentTool(input.workId, toolCall);
|
|
1922
2185
|
logger.info("ai.tool_call.completed", { callId, toolName: execution.name, status: execution.status, round });
|
|
1923
2186
|
executedToolCalls.push(execution);
|
|
2187
|
+
traceRounds.at(-1)?.toolExecutions.push(execution);
|
|
2188
|
+
saveTrace();
|
|
1924
2189
|
processSteps.push({ id: id("process"), type: "tool", round, toolCall: execution, createdAt: execution.calledAt });
|
|
1925
2190
|
input.onToolCall?.(execution, round);
|
|
1926
2191
|
completionMessages.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(execution.result) });
|
|
@@ -1966,7 +2231,7 @@ export class AiManager {
|
|
|
1966
2231
|
return { callId, content, outputTokens, ...(cacheHitPercent === undefined ? {} : { cacheHitPercent }), provider: this.mapProvider(provider), model: this.mapModel(model), context, toolCalls: executedToolCalls, processSteps };
|
|
1967
2232
|
}
|
|
1968
2233
|
catch (error) {
|
|
1969
|
-
const message = error instanceof Error ? error.message : "AI 调用失败";
|
|
2234
|
+
const message = error instanceof Error ? redactProviderSecret(error.message, activeApiKey) : "AI 调用失败";
|
|
1970
2235
|
this.store.db.run("UPDATE ai_calls SET status = 'failed', failure = ?, completed_at = ? WHERE id = ?", message, now(), callId);
|
|
1971
2236
|
logger.error("ai.call.failed", {
|
|
1972
2237
|
callId,
|
|
@@ -2170,6 +2435,7 @@ export class AiManager {
|
|
|
2170
2435
|
const chapter = this.store.getChapter(scope.chapterId);
|
|
2171
2436
|
const generated = await this.generateTaggedJson({
|
|
2172
2437
|
workId,
|
|
2438
|
+
taskId,
|
|
2173
2439
|
taskType: "chapter-analysis",
|
|
2174
2440
|
signal: this.taskSignal(taskId),
|
|
2175
2441
|
instruction: "分析本章并输出 JSON 对象,字段为 summary(1至3句)、events(数组)、characters(数组)、settings(数组)、evidence(数组,每项含 conclusion 和 quote)、uncertainties(数组)。",
|
|
@@ -2189,6 +2455,7 @@ export class AiManager {
|
|
|
2189
2455
|
async runTimelineAnalysis(workId, scope, modelId, taskId) {
|
|
2190
2456
|
const generated = await this.generateTaggedJson({
|
|
2191
2457
|
workId,
|
|
2458
|
+
taskId,
|
|
2192
2459
|
taskType: "timeline-analysis",
|
|
2193
2460
|
signal: this.taskSignal(taskId),
|
|
2194
2461
|
instruction: "抽取大事件候选并输出 JSON 数组。每项字段:name、description、eventType、timeLabel、timeSort(无法确定为 null)、location、impactScope、chapterIds、participantIds、evidence。必须区分发生时间与叙述时间;不确定时使用‘时间待定’。",
|
|
@@ -2228,6 +2495,7 @@ export class AiManager {
|
|
|
2228
2495
|
throw new AppError(409, "CHAPTERS_REQUIRED", "世界观分析范围内没有章节");
|
|
2229
2496
|
const generated = await this.generateTaggedJson({
|
|
2230
2497
|
workId,
|
|
2498
|
+
taskId,
|
|
2231
2499
|
taskType: "book-analysis",
|
|
2232
2500
|
signal: this.taskSignal(taskId),
|
|
2233
2501
|
instruction: [
|
|
@@ -2325,6 +2593,7 @@ export class AiManager {
|
|
|
2325
2593
|
return { candidates: [], callId: null };
|
|
2326
2594
|
const generated = await this.generateTaggedJson({
|
|
2327
2595
|
workId,
|
|
2596
|
+
taskId,
|
|
2328
2597
|
taskType: "book-analysis",
|
|
2329
2598
|
signal: this.taskSignal(taskId),
|
|
2330
2599
|
maxAttempts: 2,
|
|
@@ -2468,6 +2737,7 @@ export class AiManager {
|
|
|
2468
2737
|
async runConsistencyCheck(workId, scope, modelId, taskId) {
|
|
2469
2738
|
const generated = await this.generateTaggedJson({
|
|
2470
2739
|
workId,
|
|
2740
|
+
taskId,
|
|
2471
2741
|
taskType: "consistency-check",
|
|
2472
2742
|
signal: this.taskSignal(taskId),
|
|
2473
2743
|
instruction: "检查设定、人物状态、关系和时间是否冲突,输出 JSON 数组。每项字段:itemType、severity(low/medium/high)、title、description、entityRefs、evidence、suggestion。没有问题时输出 []。",
|
|
@@ -2523,6 +2793,7 @@ export class AiManager {
|
|
|
2523
2793
|
}).join("\n");
|
|
2524
2794
|
const generated = await this.generateTaggedJson({
|
|
2525
2795
|
workId,
|
|
2796
|
+
taskId,
|
|
2526
2797
|
taskType: "book-analysis",
|
|
2527
2798
|
signal: this.taskSignal(taskId),
|
|
2528
2799
|
scope: scope.type === "none" ? scope : { type: "none" },
|
|
@@ -2643,6 +2914,7 @@ export class AiManager {
|
|
|
2643
2914
|
async verifyCharacterTitlePairs(workId, pairs, modelId, taskId) {
|
|
2644
2915
|
const generated = await this.generateTaggedJson({
|
|
2645
2916
|
workId,
|
|
2917
|
+
taskId,
|
|
2646
2918
|
taskType: "book-analysis",
|
|
2647
2919
|
signal: this.taskSignal(taskId),
|
|
2648
2920
|
scope: { type: "none" },
|
|
@@ -2696,6 +2968,7 @@ export class AiManager {
|
|
|
2696
2968
|
const extractChunk = async (text, maxAttempts = 3) => {
|
|
2697
2969
|
const generated = await this.generateTaggedJson({
|
|
2698
2970
|
workId,
|
|
2971
|
+
taskId,
|
|
2699
2972
|
taskType: "book-analysis",
|
|
2700
2973
|
signal: this.taskSignal(taskId),
|
|
2701
2974
|
maxAttempts,
|
|
@@ -3073,6 +3346,7 @@ export class AiManager {
|
|
|
3073
3346
|
const extractChunk = async (text, maxAttempts = 3) => {
|
|
3074
3347
|
const generated = await this.generateTaggedJson({
|
|
3075
3348
|
workId,
|
|
3349
|
+
taskId,
|
|
3076
3350
|
taskType: "relationship-analysis",
|
|
3077
3351
|
signal: this.taskSignal(taskId),
|
|
3078
3352
|
maxAttempts,
|
|
@@ -3210,6 +3484,7 @@ export class AiManager {
|
|
|
3210
3484
|
const aggregationResults = await this.processChunks(evidenceBatches, Math.min(concurrency, 4), async (evidenceBatch) => {
|
|
3211
3485
|
const generated = await this.generateTaggedJson({
|
|
3212
3486
|
workId,
|
|
3487
|
+
taskId,
|
|
3213
3488
|
taskType: "relationship-analysis",
|
|
3214
3489
|
signal: this.taskSignal(taskId),
|
|
3215
3490
|
maxAttempts: 2,
|