@musnows/scriverse 0.7.1 → 0.7.3
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.md +5 -0
- package/dist/ai.js +248 -250
- package/dist/ai.js.map +1 -1
- package/dist/app.js +194 -40
- package/dist/app.js.map +1 -1
- package/dist/attachment-storage.js +28 -12
- package/dist/attachment-storage.js.map +1 -1
- package/dist/backup-encryption.js +129 -0
- package/dist/backup-encryption.js.map +1 -0
- package/dist/collaboration-presence.js +200 -18
- package/dist/collaboration-presence.js.map +1 -1
- package/dist/database.js +215 -4
- package/dist/database.js.map +1 -1
- package/dist/docx-export.js +1 -1
- package/dist/docx-export.js.map +1 -1
- package/dist/domain.js +18 -0
- package/dist/domain.js.map +1 -1
- package/dist/image-metadata.js +17 -2
- package/dist/image-metadata.js.map +1 -1
- package/dist/presence-store.js +160 -0
- package/dist/presence-store.js.map +1 -0
- package/dist/public/ai-context-meter.js +27 -0
- package/dist/public/ai-mentions.js +14 -0
- package/dist/public/app.js +1018 -292
- package/dist/public/background-task-center.js +1 -1
- package/dist/public/chapter-editor-virtualization.js +52 -0
- package/dist/public/index.html +44 -22
- package/dist/public/presence-client-id.d.ts +10 -0
- package/dist/public/presence-client-id.js +31 -0
- package/dist/public/relationship-graph.js +24 -4
- package/dist/public/s3-backup-ui.d.ts +10 -0
- package/dist/public/s3-backup-ui.js +29 -0
- package/dist/public/setting-filters.d.ts +16 -0
- package/dist/public/setting-filters.js +19 -0
- package/dist/public/styles.css +39 -9
- package/dist/s3-backup.js +197 -12
- package/dist/s3-backup.js.map +1 -1
- package/dist/security.js +82 -20
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +36 -18
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +77 -18
- package/dist/store.js.map +1 -1
- package/dist/upload-limits.js +35 -0
- package/dist/upload-limits.js.map +1 -0
- package/dist/user-auth.js +11 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/dist/ai.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ANALYSIS_TASK_TYPES, HISTORICAL_ANALYSIS_TASK_TYPES } from "./domain.js";
|
|
1
2
|
import { buildCompletionRequestBody, isAiProviderProtocol, normalizeProviderBaseUrl, parseCompletionPayload, providerCompletionEndpoint, providerModelEndpoints, providerProtocolLabelText, providerRequestHeaders } from "./ai-protocol.js";
|
|
2
3
|
import { AGENT_TOOL_RESULT_MAX_CHARS, DEFAULT_AGENT_TOOL_CALL_GLOBAL_MULTIPLIER, MIN_AGENT_TOOL_CALL_LIMIT, agentToolCallGlobalLimit, agentToolCallQuotaNoticeBudgetChars, agentToolCallQuotaUsedAfterCompact, agentToolCallSoftWarningThreshold, clampAgentToolCallGlobalMultiplier, paginateToolResultRecords, resolveMaxAgentToolCallLimit, shouldRejectAgentToolCalls, shouldRejectGlobalToolCalls, structuralToolResultRecords, withAgentToolCallQuotaNotice } from "./ai-tool-results.js";
|
|
3
4
|
import { PLATFORM_AI_WORK_ID } from "./database.js";
|
|
@@ -28,6 +29,13 @@ const AUTO_RUN_MAX_ATTEMPTS = 3;
|
|
|
28
29
|
const AUTO_RUN_RETRY_DELAYS_MS = [5_000, 30_000];
|
|
29
30
|
const AI_INTERACTIVE_TIMEOUT_MS = 60_000;
|
|
30
31
|
const AI_LONG_RUNNING_TIMEOUT_MS = 300_000;
|
|
32
|
+
const analysisTaskTypes = new Set(ANALYSIS_TASK_TYPES);
|
|
33
|
+
function isAnalysisTaskType(value) {
|
|
34
|
+
return analysisTaskTypes.has(value);
|
|
35
|
+
}
|
|
36
|
+
function unsupportedTaskType(taskType) {
|
|
37
|
+
return new AppError(400, "UNSUPPORTED_TASK_TYPE", `不支持的任务类型:${taskType}`);
|
|
38
|
+
}
|
|
31
39
|
// A small but non-transparent 128x128 PNG. The model test must exercise an actual image_url
|
|
32
40
|
// payload, while keeping the request cheap and avoiding any user data in the probe.
|
|
33
41
|
const MULTIMODAL_TEST_IMAGE_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAPoAAAD6AG1e1JrAAACfklEQVR4nO2cwY3EQBACJ8LOglRJyw4DJOpR/xOUuF17Zp91H9xsBi/9B8AhABIcC4AEx78AJDg+AyDB8SEQCY5vAUhwfA1EguM5ABIcD4KQ4HgSiATHo2AkON4FIMHxMggJjreBSHC8DkaC4zwAEhwHQpDgOBGEBMeRMCQ4zgQiwXEoFAmOU8FIcBwLR4LjXgASHBdDkOC4GYQEx9UwJDjuBiLBcTkUCY7bwUhwXA8319P5fQCPS8APRChfAgIUBOFRWADlS0CAgiA8CgugfAkIUBCER2EBlC8BAQqC8CgsgPIlIEBBEB6FBVC+BAQoCMKjsADKl4AABUF4FBZA+RIQoCAIj8ICKF8CAhQE4VFYAOVLQICCIDwKC6B8CQhQEIRHYQGULwEBCoLwKCyA8iUgQEEQHoUFUL4EBCgIwqOwAMqXgAAFQXgUFkD5EhCgIAiPwgIoXwICFAThUVgA5UtAgIIgPAoLoHwJCFAQhEdhAZQvAQEKgvAoLIDyJSBAQRAehQVQvgQEKAjCo7AAypeAAAVBeBQWQPkSEKAgCI/CAihfAgIUBOFRWADlS0CAgiA8CgugfAkIUBCER2EBlC8BAQqC8CgsgPIlIEBBEB6FBVC+BAQoCMKjsADKl4AABUF4FBZA+RIQoCAIj8ICKF8CAhQE4VFYAOVLQICCIDwKC6B8CQhQEIRHYQGULwEBCoLwKCyA8iUgQEEQHoUFUL4EBCgIwqOwAMqXgAAFQXgUFkD5EhCgIAiPwgIoXwICFAThUVgA5UtAgIIgPAoLoHwJCFAQhEdhAZQvAQEKgvAoLIDyJSBAQRAehQVQvgQEKAjCo7AAypeAAAVBeJQfFY4JQ620WGEAAAAASUVORK5CYII=";
|
|
@@ -1929,7 +1937,7 @@ export class AiManager {
|
|
|
1929
1937
|
failures: [{ message, ...(error instanceof AppError ? { code: error.code } : {}) }]
|
|
1930
1938
|
});
|
|
1931
1939
|
}
|
|
1932
|
-
if (current.status !== "partial")
|
|
1940
|
+
if (current.status !== "partial" && current.status !== "failed")
|
|
1933
1941
|
return;
|
|
1934
1942
|
const disposition = autoRunFailureDisposition(error, Number(current.attemptCount));
|
|
1935
1943
|
const settings = this.store.recordAutoRunFailure(workId, message, disposition.pauseImmediately);
|
|
@@ -2549,7 +2557,11 @@ export class AiManager {
|
|
|
2549
2557
|
}
|
|
2550
2558
|
rerunTask(taskId, modelOverrideId) {
|
|
2551
2559
|
const original = this.store.getTask(taskId);
|
|
2552
|
-
const
|
|
2560
|
+
const originalTaskType = String(original.taskType);
|
|
2561
|
+
if (HISTORICAL_ANALYSIS_TASK_TYPES.some((taskType) => taskType === originalTaskType)) {
|
|
2562
|
+
throw new AppError(409, "TASK_NOT_RERUNNABLE", `任务类型“${originalTaskType}”已经不支持重跑`);
|
|
2563
|
+
}
|
|
2564
|
+
const rerunnableStatuses = new Set(["review", "completed", "partial", "failed", "expired", "cancelled"]);
|
|
2553
2565
|
if (!rerunnableStatuses.has(String(original.status))) {
|
|
2554
2566
|
throw new AppError(409, "TASK_NOT_RERUNNABLE", "只有已结束的分析任务可以按原配置重跑");
|
|
2555
2567
|
}
|
|
@@ -2565,7 +2577,7 @@ export class AiManager {
|
|
|
2565
2577
|
if (modelId)
|
|
2566
2578
|
this.resolveModel(String(original.workId), this.analysisTaskModelPurpose(String(original.taskType)), modelId);
|
|
2567
2579
|
const rerun = this.store.createTask(String(original.workId), {
|
|
2568
|
-
taskType:
|
|
2580
|
+
taskType: originalTaskType,
|
|
2569
2581
|
scope,
|
|
2570
2582
|
...(modelId ? { modelId } : {}),
|
|
2571
2583
|
rerunOfTaskId: taskId
|
|
@@ -2618,12 +2630,7 @@ export class AiManager {
|
|
|
2618
2630
|
&& firstUserContent
|
|
2619
2631
|
&& titleModelId
|
|
2620
2632
|
&& (conversationBefore?.title === "新对话" || conversationBefore?.title === defaultTitle));
|
|
2621
|
-
const
|
|
2622
|
-
const generated = chatTools.length
|
|
2623
|
-
? await this.generate({ ...input, taskType: "chat" })
|
|
2624
|
-
: await this.generateStream({ ...input, taskType: "chat" }, onDelta);
|
|
2625
|
-
if (chatTools.length)
|
|
2626
|
-
onDelta(generated.content);
|
|
2633
|
+
const generated = await this.generate({ ...input, taskType: "chat" }, onDelta);
|
|
2627
2634
|
const chapter = input.scope.chapterId ? this.store.getChapter(input.scope.chapterId) : null;
|
|
2628
2635
|
const suggestionId = id("suggestion");
|
|
2629
2636
|
this.store.db.run(`INSERT INTO ai_suggestions (id, call_id, work_id, chapter_id, chapter_version, task_type, instruction,
|
|
@@ -2984,7 +2991,10 @@ export class AiManager {
|
|
|
2984
2991
|
const taskController = new AbortController();
|
|
2985
2992
|
this.taskControllers.set(taskId, taskController);
|
|
2986
2993
|
try {
|
|
2987
|
-
const
|
|
2994
|
+
const taskTypeValue = String(task.taskType);
|
|
2995
|
+
if (!isAnalysisTaskType(taskTypeValue))
|
|
2996
|
+
throw unsupportedTaskType(taskTypeValue);
|
|
2997
|
+
const taskType = taskTypeValue;
|
|
2988
2998
|
const scope = task.scope;
|
|
2989
2999
|
let result;
|
|
2990
3000
|
if (taskType === "chapter-analysis") {
|
|
@@ -3011,11 +3021,11 @@ export class AiManager {
|
|
|
3011
3021
|
else if (taskType === "consistency-check") {
|
|
3012
3022
|
result = await this.runConsistencyCheck(workId, scope, selectedModelId, taskId);
|
|
3013
3023
|
}
|
|
3014
|
-
else {
|
|
3024
|
+
else if (taskType === "book-analysis") {
|
|
3015
3025
|
const generated = await this.generate({
|
|
3016
3026
|
workId,
|
|
3017
3027
|
taskId,
|
|
3018
|
-
taskType:
|
|
3028
|
+
taskType: "book-analysis",
|
|
3019
3029
|
instruction: "请基于上下文完成分析,给出有原文依据的中文结论。",
|
|
3020
3030
|
scope,
|
|
3021
3031
|
signal: taskController.signal,
|
|
@@ -3023,6 +3033,9 @@ export class AiManager {
|
|
|
3023
3033
|
});
|
|
3024
3034
|
result = { content: generated.content, callId: generated.callId };
|
|
3025
3035
|
}
|
|
3036
|
+
else {
|
|
3037
|
+
throw unsupportedTaskType(taskType);
|
|
3038
|
+
}
|
|
3026
3039
|
if (!this.taskCanCommit(taskId)) {
|
|
3027
3040
|
logger.warn("ai.task.result_discarded", { taskId, workId, durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000 });
|
|
3028
3041
|
return this.store.getTask(taskId);
|
|
@@ -3054,7 +3067,8 @@ export class AiManager {
|
|
|
3054
3067
|
throw error;
|
|
3055
3068
|
}
|
|
3056
3069
|
}
|
|
3057
|
-
|
|
3070
|
+
const failedStatus = error instanceof AppError && error.code === "UNSUPPORTED_TASK_TYPE" ? "failed" : "partial";
|
|
3071
|
+
this.store.updateTask(taskId, { status: failedStatus, progress: 100, failures: [failure] });
|
|
3058
3072
|
logger.error("ai.task.failed", { taskId, workId, durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000, error: aiErrorForLog(error) });
|
|
3059
3073
|
throw error;
|
|
3060
3074
|
}
|
|
@@ -3194,6 +3208,13 @@ export class AiManager {
|
|
|
3194
3208
|
const compactedUsage = this.getContextUsage({ ...input, taskType: "chat" });
|
|
3195
3209
|
return { action: "compacted", usage: compactedUsage, compaction };
|
|
3196
3210
|
}
|
|
3211
|
+
/** 解析本轮消息中的自动角色提及;不使用会话累计排除集,也不改写累计注入状态。 */
|
|
3212
|
+
resolveInstructionMentions(input) {
|
|
3213
|
+
if (input.taskType !== "chat" || this.roleplayCharacterId(input.workId, input.conversationId))
|
|
3214
|
+
return input.scope;
|
|
3215
|
+
const matches = this.matchInstructionEntities(input.workId, input.instruction, input.scope, { characters: [], races: [], organizations: [] });
|
|
3216
|
+
return this.mergeInstructionEntityMatches(input.scope, matches);
|
|
3217
|
+
}
|
|
3197
3218
|
async compactConversation(input) {
|
|
3198
3219
|
const conversation = this.store.getAiConversationContext(input.conversationId, input.workId);
|
|
3199
3220
|
const { model } = this.resolveModel(input.workId, "chat", input.modelId);
|
|
@@ -3404,13 +3425,10 @@ export class AiManager {
|
|
|
3404
3425
|
: this.applyKeywordEntityMentions(input.workId, input.instruction, baseScope, input.conversationId, persistKeywordInjections);
|
|
3405
3426
|
return this.contextBuilder.buildPlan(input.workId, scope, workContextBudgetTokens, bookSummaryMaximumTokens, input.instruction);
|
|
3406
3427
|
}
|
|
3407
|
-
|
|
3408
|
-
const injected = conversationId
|
|
3409
|
-
? this.store.getAiConversationInjectedEntities(conversationId, workId)
|
|
3410
|
-
: { characters: [], races: [], organizations: [] };
|
|
3428
|
+
matchInstructionEntities(workId, instruction, scope, injected) {
|
|
3411
3429
|
const proseSettingInfoOn = scope.suppressAutomaticContext !== true && (scope.includeSettingInfo === true || (PROSE_CONTEXT_SCOPE_TYPES.has(scope.type)
|
|
3412
3430
|
&& scope.includeSettingInfo !== false));
|
|
3413
|
-
|
|
3431
|
+
return matchKeywordEntities(this.store, workId, instruction, {
|
|
3414
3432
|
excludeCharacterIds: [
|
|
3415
3433
|
...(scope.characterIds ?? []),
|
|
3416
3434
|
...(scope.mentionCharacterIds ?? []),
|
|
@@ -3421,9 +3439,23 @@ export class AiManager {
|
|
|
3421
3439
|
// 正文范围已整表注入组织/种族时,关键词不再重复塞提及卡
|
|
3422
3440
|
skipRacesAndOrganizations: proseSettingInfoOn
|
|
3423
3441
|
});
|
|
3442
|
+
}
|
|
3443
|
+
mergeInstructionEntityMatches(scope, matches) {
|
|
3424
3444
|
const mentionCharacterIds = [...new Set([...(scope.mentionCharacterIds ?? []), ...matches.characterIds])];
|
|
3425
3445
|
const raceIds = [...new Set([...(scope.raceIds ?? []), ...matches.raceIds])];
|
|
3426
3446
|
const organizationIds = [...new Set([...(scope.organizationIds ?? []), ...matches.organizationIds])];
|
|
3447
|
+
return {
|
|
3448
|
+
...scope,
|
|
3449
|
+
...(mentionCharacterIds.length ? { mentionCharacterIds } : {}),
|
|
3450
|
+
...(raceIds.length ? { raceIds } : {}),
|
|
3451
|
+
...(organizationIds.length ? { organizationIds } : {})
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
applyKeywordEntityMentions(workId, instruction, scope, conversationId, persist) {
|
|
3455
|
+
const injected = conversationId
|
|
3456
|
+
? this.store.getAiConversationInjectedEntities(conversationId, workId)
|
|
3457
|
+
: { characters: [], races: [], organizations: [] };
|
|
3458
|
+
const matches = this.matchInstructionEntities(workId, instruction, scope, injected);
|
|
3427
3459
|
if (persist && conversationId && (matches.characterIds.length || matches.raceIds.length || matches.organizationIds.length)) {
|
|
3428
3460
|
this.store.mergeAiConversationInjectedEntities(conversationId, workId, {
|
|
3429
3461
|
characters: matches.characterIds,
|
|
@@ -3431,12 +3463,7 @@ export class AiManager {
|
|
|
3431
3463
|
organizations: matches.organizationIds
|
|
3432
3464
|
});
|
|
3433
3465
|
}
|
|
3434
|
-
return
|
|
3435
|
-
...scope,
|
|
3436
|
-
...(mentionCharacterIds.length ? { mentionCharacterIds } : {}),
|
|
3437
|
-
...(raceIds.length ? { raceIds } : {}),
|
|
3438
|
-
...(organizationIds.length ? { organizationIds } : {})
|
|
3439
|
-
};
|
|
3466
|
+
return this.mergeInstructionEntityMatches(scope, matches);
|
|
3440
3467
|
}
|
|
3441
3468
|
buildContext(input, model) {
|
|
3442
3469
|
return collapseAiBlankLines(this.buildContextPlan(input, model, undefined, true).context);
|
|
@@ -4199,7 +4226,7 @@ export class AiManager {
|
|
|
4199
4226
|
extraSystemPrompt: [input.extraSystemPrompt, systemRequirement].filter(Boolean).join("\n")
|
|
4200
4227
|
});
|
|
4201
4228
|
}
|
|
4202
|
-
async generate(input) {
|
|
4229
|
+
async generate(input, onDelta) {
|
|
4203
4230
|
const generationRoleplayCharacterId = this.roleplayCharacterId(input.workId, input.conversationId);
|
|
4204
4231
|
const { model, provider } = this.resolveModel(input.workId, input.taskType, input.modelId);
|
|
4205
4232
|
const preset = safeJsonObject(stringValue(model, "preset_json"));
|
|
@@ -4271,7 +4298,7 @@ export class AiManager {
|
|
|
4271
4298
|
providerId: stringValue(provider, "id"),
|
|
4272
4299
|
modelId: stringValue(model, "id"),
|
|
4273
4300
|
protocol,
|
|
4274
|
-
streaming:
|
|
4301
|
+
streaming: Boolean(onDelta),
|
|
4275
4302
|
contextChars: context.length,
|
|
4276
4303
|
instructionChars: input.instruction.length,
|
|
4277
4304
|
toolCount: tools.length
|
|
@@ -4308,11 +4335,19 @@ export class AiManager {
|
|
|
4308
4335
|
let cacheUsageComplete = true;
|
|
4309
4336
|
let totalInputTokens = 0;
|
|
4310
4337
|
let totalCachedInputTokens = 0;
|
|
4338
|
+
const processSteps = [];
|
|
4339
|
+
const completionDelivery = new WeakMap();
|
|
4340
|
+
let streamedContent = "";
|
|
4341
|
+
let streamingGenerationRound = 0;
|
|
4311
4342
|
const requestCompletion = async (toolChoice, options = {}) => {
|
|
4312
4343
|
const requestMessages = options.messages ?? completionMessages;
|
|
4313
4344
|
const requestParameters = options.parameters ?? parameters;
|
|
4314
4345
|
const purpose = options.purpose ?? "generation";
|
|
4315
4346
|
const requestTools = toolChoice === "auto" ? tools : [];
|
|
4347
|
+
const streamResponse = Boolean(onDelta) && purpose === "generation";
|
|
4348
|
+
const processRound = streamResponse ? streamingGenerationRound + 1 : 0;
|
|
4349
|
+
if (streamResponse)
|
|
4350
|
+
streamingGenerationRound = processRound;
|
|
4316
4351
|
const roundParameters = this.constrainParametersForDailyTokenQuota(input.workId, requestMessages, this.constrainParametersForContext(model, requestMessages, requestParameters, requestTools), requestTools, trackedInputTokens + trackedOutputTokens);
|
|
4317
4352
|
const traceRound = {
|
|
4318
4353
|
round: traceRounds.length + 1,
|
|
@@ -4330,9 +4365,11 @@ export class AiManager {
|
|
|
4330
4365
|
};
|
|
4331
4366
|
traceRounds.push(traceRound);
|
|
4332
4367
|
saveTrace();
|
|
4368
|
+
let streamedThinkingStep = null;
|
|
4333
4369
|
let lastFailure = null;
|
|
4334
4370
|
for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
|
|
4335
4371
|
let retryable = true;
|
|
4372
|
+
let attemptEmitted = false;
|
|
4336
4373
|
const attemptStartedAt = process.hrtime.bigint();
|
|
4337
4374
|
const traceAttempt = {
|
|
4338
4375
|
attempt,
|
|
@@ -4354,18 +4391,57 @@ export class AiManager {
|
|
|
4354
4391
|
try {
|
|
4355
4392
|
const response = await this.outboundFetch(endpoint, {
|
|
4356
4393
|
method: "POST",
|
|
4357
|
-
headers: providerRequestHeaders(protocol, accessToken, "application/json"),
|
|
4394
|
+
headers: providerRequestHeaders(protocol, accessToken, streamResponse ? "text/event-stream" : "application/json"),
|
|
4358
4395
|
body: JSON.stringify(buildCompletionRequestBody({
|
|
4359
4396
|
protocol,
|
|
4360
4397
|
model: stringValue(model, "model_id"),
|
|
4361
4398
|
messages: requestMessages,
|
|
4362
4399
|
parameters: roundParameters,
|
|
4363
4400
|
tools: requestTools,
|
|
4364
|
-
toolChoice
|
|
4401
|
+
toolChoice,
|
|
4402
|
+
...(streamResponse ? { stream: true } : {})
|
|
4365
4403
|
})),
|
|
4366
4404
|
signal: controller.signal
|
|
4367
4405
|
});
|
|
4368
|
-
|
|
4406
|
+
if (!response.ok) {
|
|
4407
|
+
return { ok: false, status: response.status, body: await readResponseTextLimited(response) };
|
|
4408
|
+
}
|
|
4409
|
+
const isEventStream = response.headers.get("content-type")?.toLowerCase().includes("text/event-stream") ?? false;
|
|
4410
|
+
if (!streamResponse || !isEventStream) {
|
|
4411
|
+
const body = await readResponseTextLimited(response);
|
|
4412
|
+
try {
|
|
4413
|
+
const payload = parseCompletionPayload(protocol, redactProviderSecrets(JSON.parse(body), activeSecrets));
|
|
4414
|
+
return { ok: true, status: response.status, payload, delivery: "json" };
|
|
4415
|
+
}
|
|
4416
|
+
catch {
|
|
4417
|
+
throw new Error(`${providerProtocolLabelText(protocol)} returned invalid JSON: ${body.slice(0, 500)}`);
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
const payload = await this.readCompletionStream(response, protocol, activeSecrets, (delta) => {
|
|
4421
|
+
attemptEmitted = true;
|
|
4422
|
+
streamedContent += delta;
|
|
4423
|
+
onDelta?.(delta);
|
|
4424
|
+
}, (delta) => {
|
|
4425
|
+
attemptEmitted = true;
|
|
4426
|
+
if (!streamedThinkingStep) {
|
|
4427
|
+
streamedThinkingStep = {
|
|
4428
|
+
id: id("process"),
|
|
4429
|
+
type: "thinking",
|
|
4430
|
+
round: processRound,
|
|
4431
|
+
content: "",
|
|
4432
|
+
createdAt: now()
|
|
4433
|
+
};
|
|
4434
|
+
processSteps.push(streamedThinkingStep);
|
|
4435
|
+
}
|
|
4436
|
+
streamedThinkingStep.content += delta;
|
|
4437
|
+
input.onProcessStep?.({ ...streamedThinkingStep, content: delta, append: true });
|
|
4438
|
+
});
|
|
4439
|
+
return {
|
|
4440
|
+
ok: true,
|
|
4441
|
+
status: response.status,
|
|
4442
|
+
payload: redactProviderSecrets(payload, activeSecrets),
|
|
4443
|
+
delivery: "sse"
|
|
4444
|
+
};
|
|
4369
4445
|
}
|
|
4370
4446
|
finally {
|
|
4371
4447
|
clearTimeout(timeout);
|
|
@@ -4377,31 +4453,28 @@ export class AiManager {
|
|
|
4377
4453
|
attempt,
|
|
4378
4454
|
status: candidate.status,
|
|
4379
4455
|
ok: candidate.ok,
|
|
4380
|
-
durationMs: Number(process.hrtime.bigint() - attemptStartedAt) / 1_000_000
|
|
4456
|
+
durationMs: Number(process.hrtime.bigint() - attemptStartedAt) / 1_000_000,
|
|
4457
|
+
streaming: streamResponse
|
|
4381
4458
|
});
|
|
4382
4459
|
if (candidate.ok) {
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
}
|
|
4398
|
-
const outputText = completionPayloadOutputText(parsed);
|
|
4399
|
-
trackUsage(resolveAiTokenUsage(parsed.usage, estimateAiTokens(JSON.stringify(requestMessages)), outputText ? estimateAiTokens(outputText) : 0));
|
|
4400
|
-
return parsed;
|
|
4401
|
-
}
|
|
4402
|
-
catch {
|
|
4403
|
-
throw new Error(`${providerProtocolLabelText(protocol)} returned invalid JSON: ${candidate.body.slice(0, 500)}`);
|
|
4460
|
+
const parsed = candidate.payload;
|
|
4461
|
+
completionDelivery.set(parsed, candidate.delivery);
|
|
4462
|
+
traceAttempt.completedAt = now();
|
|
4463
|
+
traceAttempt.status = "completed";
|
|
4464
|
+
traceAttempt.httpStatus = candidate.status;
|
|
4465
|
+
traceAttempt.response = sanitizeCompletionTraceResponse(parsed);
|
|
4466
|
+
saveTrace();
|
|
4467
|
+
completionRequestCount += 1;
|
|
4468
|
+
const cacheUsage = resolveInputCacheUsage(parsed.usage);
|
|
4469
|
+
if (!cacheUsage)
|
|
4470
|
+
cacheUsageComplete = false;
|
|
4471
|
+
else {
|
|
4472
|
+
totalInputTokens += cacheUsage.inputTokens;
|
|
4473
|
+
totalCachedInputTokens += cacheUsage.cachedInputTokens;
|
|
4404
4474
|
}
|
|
4475
|
+
const outputText = completionPayloadOutputText(parsed);
|
|
4476
|
+
trackUsage(resolveAiTokenUsage(parsed.usage, estimateAiTokens(JSON.stringify(requestMessages)), outputText ? estimateAiTokens(outputText) : 0));
|
|
4477
|
+
return parsed;
|
|
4405
4478
|
}
|
|
4406
4479
|
lastFailure = new Error(`HTTP ${candidate.status}: ${candidate.body.slice(0, 500)}`);
|
|
4407
4480
|
traceAttempt.completedAt = now();
|
|
@@ -4427,11 +4500,12 @@ export class AiManager {
|
|
|
4427
4500
|
logger.warn("ai.call.attempt_failed", {
|
|
4428
4501
|
callId,
|
|
4429
4502
|
attempt,
|
|
4430
|
-
retryable: retryable && attempt < maximumAttempts && !input.signal?.aborted,
|
|
4503
|
+
retryable: retryable && !attemptEmitted && attempt < maximumAttempts && !input.signal?.aborted,
|
|
4431
4504
|
durationMs: Number(process.hrtime.bigint() - attemptStartedAt) / 1_000_000,
|
|
4505
|
+
streaming: streamResponse,
|
|
4432
4506
|
error: aiErrorForLog(error)
|
|
4433
4507
|
});
|
|
4434
|
-
if (input.signal?.aborted)
|
|
4508
|
+
if (input.signal?.aborted || attemptEmitted)
|
|
4435
4509
|
throw error;
|
|
4436
4510
|
if (!retryable || attempt >= maximumAttempts)
|
|
4437
4511
|
throw error;
|
|
@@ -4441,7 +4515,6 @@ export class AiManager {
|
|
|
4441
4515
|
}
|
|
4442
4516
|
throw lastFailure instanceof Error ? lastFailure : new Error("AI request failed after all retries.");
|
|
4443
4517
|
};
|
|
4444
|
-
const processSteps = [];
|
|
4445
4518
|
const baseMessageCount = messages.length;
|
|
4446
4519
|
const firstUserMessageIndex = messages.findIndex((message) => message.role !== "system");
|
|
4447
4520
|
const compactedMessageIndex = firstUserMessageIndex < 0 ? messages.length : firstUserMessageIndex;
|
|
@@ -4559,15 +4632,17 @@ export class AiManager {
|
|
|
4559
4632
|
let payload = await requestCompletion("auto");
|
|
4560
4633
|
let choice = payload.choices?.[0];
|
|
4561
4634
|
const executedToolCalls = [];
|
|
4562
|
-
const recordChoiceProcess = (
|
|
4635
|
+
const recordChoiceProcess = (currentPayload, round, includeIntermediate) => {
|
|
4636
|
+
const currentChoice = currentPayload.choices?.[0];
|
|
4637
|
+
const deliveredAsSse = completionDelivery.get(currentPayload) === "sse";
|
|
4563
4638
|
const reasoning = currentChoice?.message?.reasoning_content;
|
|
4564
|
-
if (reasoning?.trim()) {
|
|
4639
|
+
if (!deliveredAsSse && reasoning?.trim()) {
|
|
4565
4640
|
const step = { id: id("process"), type: "thinking", round, content: reasoning, createdAt: now() };
|
|
4566
4641
|
processSteps.push(step);
|
|
4567
4642
|
input.onProcessStep?.(step);
|
|
4568
4643
|
}
|
|
4569
4644
|
const intermediate = currentChoice?.message?.content;
|
|
4570
|
-
if (includeIntermediate && intermediate?.trim()) {
|
|
4645
|
+
if (!deliveredAsSse && includeIntermediate && intermediate?.trim()) {
|
|
4571
4646
|
const step = { id: id("process"), type: "intermediate", round, content: intermediate, createdAt: now() };
|
|
4572
4647
|
processSteps.push(step);
|
|
4573
4648
|
input.onProcessStep?.(step);
|
|
@@ -4576,7 +4651,7 @@ export class AiManager {
|
|
|
4576
4651
|
let toolRound = 0;
|
|
4577
4652
|
while (choice?.message?.tool_calls?.length) {
|
|
4578
4653
|
const round = toolRound + 1;
|
|
4579
|
-
recordChoiceProcess(
|
|
4654
|
+
recordChoiceProcess(payload, round, true);
|
|
4580
4655
|
const toolCalls = choice.message.tool_calls;
|
|
4581
4656
|
if (shouldRejectGlobalToolCalls(globalToolCallUsed, toolCalls.length, globalToolCallLimit)) {
|
|
4582
4657
|
logger.warn("ai.tool_call.global_limit_reached", {
|
|
@@ -4649,16 +4724,21 @@ export class AiManager {
|
|
|
4649
4724
|
payload = await requestCompletion("auto");
|
|
4650
4725
|
choice = payload.choices?.[0];
|
|
4651
4726
|
}
|
|
4652
|
-
recordChoiceProcess(
|
|
4653
|
-
const
|
|
4654
|
-
if (!
|
|
4727
|
+
recordChoiceProcess(payload, toolRound + 1, false);
|
|
4728
|
+
const finalContent = choice?.message?.content;
|
|
4729
|
+
if (!finalContent?.trim()) {
|
|
4655
4730
|
const reasoningLength = choice?.message?.reasoning_content?.length ?? 0;
|
|
4656
4731
|
const suffix = choice?.finish_reason === "length" || reasoningLength > 0
|
|
4657
4732
|
? `;模型已生成 ${reasoningLength} 个推理字符,请提高 max_tokens 输出预算`
|
|
4658
4733
|
: "";
|
|
4659
4734
|
throw new Error(`${providerProtocolLabelText(protocol)} 响应缺少可用正文,finish_reason=${choice?.finish_reason ?? "unknown"}${suffix}`);
|
|
4660
4735
|
}
|
|
4661
|
-
|
|
4736
|
+
if (onDelta && completionDelivery.get(payload) !== "sse") {
|
|
4737
|
+
streamedContent += finalContent;
|
|
4738
|
+
onDelta(finalContent);
|
|
4739
|
+
}
|
|
4740
|
+
const content = onDelta ? streamedContent : finalContent;
|
|
4741
|
+
const outputTokens = resolveOutputTokens(payload.usage, finalContent);
|
|
4662
4742
|
const cacheHitPercent = cacheUsageComplete && completionRequestCount > 0 && totalInputTokens > 0
|
|
4663
4743
|
? Math.round(totalCachedInputTokens / totalInputTokens * 1_000) / 10
|
|
4664
4744
|
: undefined;
|
|
@@ -4671,12 +4751,19 @@ export class AiManager {
|
|
|
4671
4751
|
callId,
|
|
4672
4752
|
workId: input.workId,
|
|
4673
4753
|
taskType: input.taskType,
|
|
4674
|
-
streaming:
|
|
4754
|
+
streaming: Boolean(onDelta),
|
|
4675
4755
|
durationMs: Number(process.hrtime.bigint() - callStartedAt) / 1_000_000,
|
|
4676
4756
|
outputChars: content.length,
|
|
4677
4757
|
outputTokens,
|
|
4678
4758
|
toolCallCount: executedToolCalls.length
|
|
4679
4759
|
});
|
|
4760
|
+
const finalAnthropicContent = choice?.message?.anthropic_content;
|
|
4761
|
+
const replayAnthropicContent = onDelta && finalAnthropicContent?.length && content !== finalContent
|
|
4762
|
+
? [
|
|
4763
|
+
...finalAnthropicContent.filter((block) => block.type !== "text" && block.type !== "tool_use"),
|
|
4764
|
+
{ type: "text", text: content }
|
|
4765
|
+
]
|
|
4766
|
+
: finalAnthropicContent;
|
|
4680
4767
|
return {
|
|
4681
4768
|
callId,
|
|
4682
4769
|
content,
|
|
@@ -4685,7 +4772,7 @@ export class AiManager {
|
|
|
4685
4772
|
? { reasoningContent: choice.message.reasoning_content }
|
|
4686
4773
|
: {}),
|
|
4687
4774
|
...(cacheHitPercent === undefined ? {} : { cacheHitPercent }),
|
|
4688
|
-
...(
|
|
4775
|
+
...(replayAnthropicContent?.length ? { anthropicContent: replayAnthropicContent } : {}),
|
|
4689
4776
|
provider: this.mapProvider(provider),
|
|
4690
4777
|
model: this.mapModel(model),
|
|
4691
4778
|
context,
|
|
@@ -4706,7 +4793,7 @@ export class AiManager {
|
|
|
4706
4793
|
callId,
|
|
4707
4794
|
workId: input.workId,
|
|
4708
4795
|
taskType: input.taskType,
|
|
4709
|
-
streaming:
|
|
4796
|
+
streaming: Boolean(onDelta),
|
|
4710
4797
|
durationMs: Number(process.hrtime.bigint() - callStartedAt) / 1_000_000,
|
|
4711
4798
|
error: aiErrorForLog(error)
|
|
4712
4799
|
});
|
|
@@ -4720,175 +4807,7 @@ export class AiManager {
|
|
|
4720
4807
|
throw new AppError(502, "AI_CALL_FAILED", "AI 调用失败", { callId, failure: message, ...failureTarget });
|
|
4721
4808
|
}
|
|
4722
4809
|
}
|
|
4723
|
-
async
|
|
4724
|
-
const { model, provider } = this.resolveModel(input.workId, input.taskType, input.modelId);
|
|
4725
|
-
const context = this.buildContext(input, model);
|
|
4726
|
-
const preset = safeJsonObject(stringValue(model, "preset_json"));
|
|
4727
|
-
const messages = this.buildMessages(input, context);
|
|
4728
|
-
let parameters;
|
|
4729
|
-
try {
|
|
4730
|
-
parameters = this.constrainParametersForContext(model, messages, {
|
|
4731
|
-
...this.sanitizeParameters({ ...preset, ...(input.parameters ?? {}) }, stringValue(model, "model_id")),
|
|
4732
|
-
...thinkingParameters(provider, model)
|
|
4733
|
-
});
|
|
4734
|
-
}
|
|
4735
|
-
catch (error) {
|
|
4736
|
-
if (!(error instanceof AppError) || error.code !== "CONTEXT_WINDOW_EXCEEDED")
|
|
4737
|
-
throw error;
|
|
4738
|
-
throw initialContextWindowError(error, provider, model);
|
|
4739
|
-
}
|
|
4740
|
-
parameters = this.constrainParametersForDailyTokenQuota(input.workId, messages, parameters);
|
|
4741
|
-
const callId = id("call");
|
|
4742
|
-
this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
|
|
4743
|
-
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);
|
|
4744
|
-
const callStartedAt = process.hrtime.bigint();
|
|
4745
|
-
const protocol = providerProtocol(provider);
|
|
4746
|
-
logger.info("ai.call.started", {
|
|
4747
|
-
callId,
|
|
4748
|
-
workId: input.workId,
|
|
4749
|
-
taskType: input.taskType,
|
|
4750
|
-
providerId: stringValue(provider, "id"),
|
|
4751
|
-
modelId: stringValue(model, "id"),
|
|
4752
|
-
protocol,
|
|
4753
|
-
streaming: true,
|
|
4754
|
-
contextChars: context.length,
|
|
4755
|
-
instructionChars: input.instruction.length
|
|
4756
|
-
});
|
|
4757
|
-
let activeSecrets = [];
|
|
4758
|
-
try {
|
|
4759
|
-
const { accessToken, credentialSecret } = await this.resolveProviderAccessToken(provider);
|
|
4760
|
-
activeSecrets = [credentialSecret, accessToken];
|
|
4761
|
-
const endpoint = providerCompletionEndpoint(stringValue(provider, "base_url"), protocol);
|
|
4762
|
-
const maximumAttempts = Math.round(clamp(input.maxAttempts ?? 3, 1, 5));
|
|
4763
|
-
let streamedResult = null;
|
|
4764
|
-
let lastFailure = null;
|
|
4765
|
-
let emitted = false;
|
|
4766
|
-
const thinkingStepId = id("process");
|
|
4767
|
-
const thinkingCreatedAt = now();
|
|
4768
|
-
for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
|
|
4769
|
-
const attemptStartedAt = process.hrtime.bigint();
|
|
4770
|
-
logger.info("ai.call.attempt_started", { callId, attempt, maximumAttempts, streaming: true });
|
|
4771
|
-
try {
|
|
4772
|
-
const candidate = await this.scheduleProviderRequest(provider, input.signal, async () => {
|
|
4773
|
-
const controller = new AbortController();
|
|
4774
|
-
const forwardAbort = () => controller.abort(input.signal?.reason);
|
|
4775
|
-
if (input.signal?.aborted)
|
|
4776
|
-
forwardAbort();
|
|
4777
|
-
else
|
|
4778
|
-
input.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
4779
|
-
const timeout = setTimeout(() => controller.abort(new Error(`AI 请求超时(${Math.round(AI_INTERACTIVE_TIMEOUT_MS / 1_000)} 秒)`)), AI_INTERACTIVE_TIMEOUT_MS);
|
|
4780
|
-
try {
|
|
4781
|
-
const response = await this.outboundFetch(endpoint, {
|
|
4782
|
-
method: "POST",
|
|
4783
|
-
headers: providerRequestHeaders(protocol, accessToken, "text/event-stream"),
|
|
4784
|
-
body: JSON.stringify(buildCompletionRequestBody({
|
|
4785
|
-
protocol,
|
|
4786
|
-
model: stringValue(model, "model_id"),
|
|
4787
|
-
messages,
|
|
4788
|
-
parameters,
|
|
4789
|
-
stream: true
|
|
4790
|
-
})),
|
|
4791
|
-
signal: controller.signal
|
|
4792
|
-
});
|
|
4793
|
-
if (!response.ok)
|
|
4794
|
-
return { ok: false, status: response.status, body: await readResponseTextLimited(response) };
|
|
4795
|
-
const streamed = await this.readCompletionStream(response, protocol, estimateAiTokens(JSON.stringify(messages)), activeSecrets, (delta) => {
|
|
4796
|
-
emitted = true;
|
|
4797
|
-
onDelta(delta);
|
|
4798
|
-
}, (delta) => {
|
|
4799
|
-
emitted = true;
|
|
4800
|
-
input.onProcessStep?.({ id: thinkingStepId, type: "thinking", round: 1, content: delta, createdAt: thinkingCreatedAt, append: true });
|
|
4801
|
-
});
|
|
4802
|
-
return { ok: true, status: response.status, result: streamed };
|
|
4803
|
-
}
|
|
4804
|
-
finally {
|
|
4805
|
-
clearTimeout(timeout);
|
|
4806
|
-
input.signal?.removeEventListener("abort", forwardAbort);
|
|
4807
|
-
}
|
|
4808
|
-
});
|
|
4809
|
-
logger.info("ai.call.attempt_completed", {
|
|
4810
|
-
callId,
|
|
4811
|
-
attempt,
|
|
4812
|
-
status: candidate.status,
|
|
4813
|
-
ok: candidate.ok,
|
|
4814
|
-
durationMs: Number(process.hrtime.bigint() - attemptStartedAt) / 1_000_000,
|
|
4815
|
-
streaming: true
|
|
4816
|
-
});
|
|
4817
|
-
if (candidate.ok) {
|
|
4818
|
-
streamedResult = candidate.result;
|
|
4819
|
-
break;
|
|
4820
|
-
}
|
|
4821
|
-
lastFailure = new Error(`HTTP ${candidate.status}: ${candidate.body.slice(0, 500)}`);
|
|
4822
|
-
if (candidate.status !== 429 && candidate.status < 500)
|
|
4823
|
-
attempt = maximumAttempts;
|
|
4824
|
-
}
|
|
4825
|
-
catch (error) {
|
|
4826
|
-
lastFailure = error;
|
|
4827
|
-
logger.warn("ai.call.attempt_failed", {
|
|
4828
|
-
callId,
|
|
4829
|
-
attempt,
|
|
4830
|
-
retryable: !input.signal?.aborted && !emitted && attempt < maximumAttempts,
|
|
4831
|
-
durationMs: Number(process.hrtime.bigint() - attemptStartedAt) / 1_000_000,
|
|
4832
|
-
streaming: true,
|
|
4833
|
-
error: aiErrorForLog(error)
|
|
4834
|
-
});
|
|
4835
|
-
if (input.signal?.aborted || emitted || attempt >= maximumAttempts)
|
|
4836
|
-
throw error;
|
|
4837
|
-
}
|
|
4838
|
-
if (attempt < maximumAttempts)
|
|
4839
|
-
await new Promise((resolve) => setTimeout(resolve, attempt * 1200));
|
|
4840
|
-
}
|
|
4841
|
-
if (streamedResult === null)
|
|
4842
|
-
throw lastFailure instanceof Error ? lastFailure : new Error("AI 流式请求重试后仍未返回响应");
|
|
4843
|
-
const { content, reasoning, outputTokens, cacheHitPercent, anthropicContent, tokenUsage } = streamedResult;
|
|
4844
|
-
const processSteps = reasoning.trim()
|
|
4845
|
-
? [{ id: thinkingStepId, type: "thinking", round: 1, content: reasoning, createdAt: thinkingCreatedAt }]
|
|
4846
|
-
: [];
|
|
4847
|
-
this.store.db.run(`UPDATE ai_calls
|
|
4848
|
-
SET status = 'completed', output_chars = ?, input_tokens = ?, output_tokens = ?,
|
|
4849
|
-
cached_input_tokens = ?, cache_eligible_input_tokens = ?, cache_usage_available = ?,
|
|
4850
|
-
token_usage_source = ?, completed_at = ?
|
|
4851
|
-
WHERE id = ?`, content.length, tokenUsage.inputTokens, tokenUsage.outputTokens, tokenUsage.cachedInputTokens, tokenUsage.cacheEligibleInputTokens, tokenUsage.cacheEligibleInputTokens > 0 ? 1 : 0, tokenUsage.source, now(), callId);
|
|
4852
|
-
logger.info("ai.call.completed", {
|
|
4853
|
-
callId,
|
|
4854
|
-
workId: input.workId,
|
|
4855
|
-
taskType: input.taskType,
|
|
4856
|
-
streaming: true,
|
|
4857
|
-
durationMs: Number(process.hrtime.bigint() - callStartedAt) / 1_000_000,
|
|
4858
|
-
outputChars: content.length,
|
|
4859
|
-
outputTokens
|
|
4860
|
-
});
|
|
4861
|
-
return {
|
|
4862
|
-
callId,
|
|
4863
|
-
content,
|
|
4864
|
-
outputTokens,
|
|
4865
|
-
...(reasoning.length > 0 ? { reasoningContent: reasoning } : {}),
|
|
4866
|
-
...(cacheHitPercent === undefined ? {} : { cacheHitPercent }),
|
|
4867
|
-
...(anthropicContent?.length ? { anthropicContent } : {}),
|
|
4868
|
-
provider: this.mapProvider(provider),
|
|
4869
|
-
model: this.mapModel(model),
|
|
4870
|
-
context,
|
|
4871
|
-
toolCalls: [],
|
|
4872
|
-
processSteps,
|
|
4873
|
-
contextUsage: this.completionContextUsage(input, model, messages, [])
|
|
4874
|
-
};
|
|
4875
|
-
}
|
|
4876
|
-
catch (error) {
|
|
4877
|
-
const message = error instanceof Error ? redactProviderSecretsText(error.message, ...activeSecrets) : "AI 流式调用失败";
|
|
4878
|
-
const failureTarget = aiFailureTargetDetails(provider, model);
|
|
4879
|
-
this.store.db.run("UPDATE ai_calls SET status = 'failed', failure = ?, completed_at = ? WHERE id = ?", message, now(), callId);
|
|
4880
|
-
logger.error("ai.call.failed", {
|
|
4881
|
-
callId,
|
|
4882
|
-
workId: input.workId,
|
|
4883
|
-
taskType: input.taskType,
|
|
4884
|
-
streaming: true,
|
|
4885
|
-
durationMs: Number(process.hrtime.bigint() - callStartedAt) / 1_000_000,
|
|
4886
|
-
error: aiErrorForLog(error)
|
|
4887
|
-
});
|
|
4888
|
-
throw new AppError(502, "AI_CALL_FAILED", "AI 调用失败", { callId, failure: message, ...failureTarget });
|
|
4889
|
-
}
|
|
4890
|
-
}
|
|
4891
|
-
async readCompletionStream(response, protocol, estimatedInputTokens, apiKey, onDelta, onThinkingDelta) {
|
|
4810
|
+
async readCompletionStream(response, protocol, apiKey, onDelta, onThinkingDelta) {
|
|
4892
4811
|
const protocolLabel = providerProtocolLabelText(protocol);
|
|
4893
4812
|
if (!response.body)
|
|
4894
4813
|
throw new Error(`${protocolLabel} 流式响应缺少正文`);
|
|
@@ -4918,6 +4837,9 @@ export class AiManager {
|
|
|
4918
4837
|
};
|
|
4919
4838
|
const anthropicBlocks = new Map();
|
|
4920
4839
|
const anthropicToolInputJson = new Map();
|
|
4840
|
+
const finalizedAnthropicToolInputs = new Map();
|
|
4841
|
+
const openAiToolCalls = new Map();
|
|
4842
|
+
let openAiToolCallsFinalized = false;
|
|
4921
4843
|
const eventIndex = (payload) => {
|
|
4922
4844
|
const index = payload.index;
|
|
4923
4845
|
return typeof index === "number" && Number.isInteger(index) && index >= 0 ? index : null;
|
|
@@ -4937,16 +4859,18 @@ export class AiManager {
|
|
|
4937
4859
|
const finalizeAnthropicToolInput = (index) => {
|
|
4938
4860
|
const block = anthropicBlocks.get(index);
|
|
4939
4861
|
const inputJson = anthropicToolInputJson.get(index);
|
|
4940
|
-
if (!block || block.type !== "tool_use"
|
|
4862
|
+
if (!block || block.type !== "tool_use")
|
|
4941
4863
|
return;
|
|
4864
|
+
const completeInputJson = inputJson ?? JSON.stringify(block.input ?? {});
|
|
4942
4865
|
try {
|
|
4943
|
-
const parsed = JSON.parse(
|
|
4866
|
+
const parsed = JSON.parse(completeInputJson);
|
|
4944
4867
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
4945
4868
|
block.input = parsed;
|
|
4946
4869
|
}
|
|
4947
4870
|
catch {
|
|
4948
4871
|
block.input = {};
|
|
4949
4872
|
}
|
|
4873
|
+
finalizedAnthropicToolInputs.set(index, completeInputJson);
|
|
4950
4874
|
anthropicToolInputJson.delete(index);
|
|
4951
4875
|
};
|
|
4952
4876
|
const consumeEvent = (eventText) => {
|
|
@@ -4982,6 +4906,12 @@ export class AiManager {
|
|
|
4982
4906
|
if (contentBlock.type === "tool_use" && !contentBlock.input)
|
|
4983
4907
|
contentBlock.input = {};
|
|
4984
4908
|
anthropicBlocks.set(index, contentBlock);
|
|
4909
|
+
if (contentBlock.type === "text" && typeof contentBlock.text === "string" && contentBlock.text.length > 0) {
|
|
4910
|
+
appendContent(contentBlock.text);
|
|
4911
|
+
}
|
|
4912
|
+
if (contentBlock.type === "thinking" && typeof contentBlock.thinking === "string" && contentBlock.thinking.length > 0) {
|
|
4913
|
+
appendReasoning(contentBlock.thinking);
|
|
4914
|
+
}
|
|
4985
4915
|
}
|
|
4986
4916
|
}
|
|
4987
4917
|
const eventUsage = payload.usage && typeof payload.usage === "object" && !Array.isArray(payload.usage)
|
|
@@ -5028,6 +4958,8 @@ export class AiManager {
|
|
|
5028
4958
|
if (eventDelta.type === "text_delta" && typeof eventDelta.text === "string" && eventDelta.text.length > 0) {
|
|
5029
4959
|
appendContent(eventDelta.text);
|
|
5030
4960
|
}
|
|
4961
|
+
if (type === "message_stop")
|
|
4962
|
+
upstreamDone = true;
|
|
5031
4963
|
return;
|
|
5032
4964
|
}
|
|
5033
4965
|
const streamUsage = payload.usage && typeof payload.usage === "object" && !Array.isArray(payload.usage)
|
|
@@ -5039,11 +4971,38 @@ export class AiManager {
|
|
|
5039
4971
|
const choice = choices[0] && typeof choices[0] === "object" && !Array.isArray(choices[0])
|
|
5040
4972
|
? choices[0]
|
|
5041
4973
|
: null;
|
|
5042
|
-
if (typeof choice?.finish_reason === "string")
|
|
4974
|
+
if (typeof choice?.finish_reason === "string") {
|
|
5043
4975
|
finishReason = choice.finish_reason;
|
|
4976
|
+
if (finishReason === "tool_calls")
|
|
4977
|
+
openAiToolCallsFinalized = true;
|
|
4978
|
+
}
|
|
5044
4979
|
const deltaRecord = choice?.delta && typeof choice.delta === "object" && !Array.isArray(choice.delta)
|
|
5045
4980
|
? choice.delta
|
|
5046
4981
|
: {};
|
|
4982
|
+
const toolCallDeltas = Array.isArray(deltaRecord.tool_calls) ? deltaRecord.tool_calls : [];
|
|
4983
|
+
for (const [position, value] of toolCallDeltas.entries()) {
|
|
4984
|
+
const toolCallDelta = value && typeof value === "object" && !Array.isArray(value)
|
|
4985
|
+
? value
|
|
4986
|
+
: {};
|
|
4987
|
+
const index = typeof toolCallDelta.index === "number" && Number.isInteger(toolCallDelta.index) && toolCallDelta.index >= 0
|
|
4988
|
+
? toolCallDelta.index
|
|
4989
|
+
: position;
|
|
4990
|
+
const current = openAiToolCalls.get(index) ?? {
|
|
4991
|
+
id: "",
|
|
4992
|
+
type: "function",
|
|
4993
|
+
function: { name: "", arguments: "" }
|
|
4994
|
+
};
|
|
4995
|
+
if (!current.id && typeof toolCallDelta.id === "string")
|
|
4996
|
+
current.id = toolCallDelta.id;
|
|
4997
|
+
const fn = toolCallDelta.function && typeof toolCallDelta.function === "object" && !Array.isArray(toolCallDelta.function)
|
|
4998
|
+
? toolCallDelta.function
|
|
4999
|
+
: {};
|
|
5000
|
+
if (typeof fn.name === "string")
|
|
5001
|
+
current.function.name += fn.name;
|
|
5002
|
+
if (typeof fn.arguments === "string")
|
|
5003
|
+
current.function.arguments = `${String(current.function.arguments)}${fn.arguments}`;
|
|
5004
|
+
openAiToolCalls.set(index, current);
|
|
5005
|
+
}
|
|
5047
5006
|
const thinkingDelta = deltaRecord.reasoning_content;
|
|
5048
5007
|
if (typeof thinkingDelta === "string" && thinkingDelta.length > 0) {
|
|
5049
5008
|
appendReasoning(thinkingDelta);
|
|
@@ -5091,25 +5050,64 @@ export class AiManager {
|
|
|
5091
5050
|
reasoning += finalReasoning;
|
|
5092
5051
|
onThinkingDelta(finalReasoning);
|
|
5093
5052
|
}
|
|
5094
|
-
|
|
5053
|
+
const sortedOpenAiToolCalls = [...openAiToolCalls.entries()].sort(([left], [right]) => left - right);
|
|
5054
|
+
const openAiToolCallsComplete = openAiToolCallsFinalized
|
|
5055
|
+
&& sortedOpenAiToolCalls.length > 0
|
|
5056
|
+
&& sortedOpenAiToolCalls.every(([, toolCall]) => Boolean(toolCall.id && toolCall.function.name));
|
|
5057
|
+
const anthropicToolBlocks = [...anthropicBlocks.entries()]
|
|
5058
|
+
.filter(([, block]) => block.type === "tool_use")
|
|
5059
|
+
.sort(([left], [right]) => left - right);
|
|
5060
|
+
const anthropicToolCallsComplete = finishReason === "tool_use"
|
|
5061
|
+
&& anthropicToolBlocks.length > 0
|
|
5062
|
+
&& anthropicToolBlocks.every(([index, block]) => (finalizedAnthropicToolInputs.has(index)
|
|
5063
|
+
&& typeof block.id === "string"
|
|
5064
|
+
&& block.id.length > 0
|
|
5065
|
+
&& typeof block.name === "string"
|
|
5066
|
+
&& block.name.length > 0));
|
|
5067
|
+
const toolCalls = protocol === "anthropic-messages"
|
|
5068
|
+
? anthropicToolCallsComplete
|
|
5069
|
+
? anthropicToolBlocks.map(([index, block]) => ({
|
|
5070
|
+
id: String(block.id),
|
|
5071
|
+
type: "function",
|
|
5072
|
+
function: {
|
|
5073
|
+
name: String(block.name),
|
|
5074
|
+
arguments: finalizedAnthropicToolInputs.get(index) ?? "{}"
|
|
5075
|
+
}
|
|
5076
|
+
}))
|
|
5077
|
+
: []
|
|
5078
|
+
: openAiToolCallsComplete
|
|
5079
|
+
? sortedOpenAiToolCalls.map(([, toolCall]) => toolCall)
|
|
5080
|
+
: [];
|
|
5081
|
+
if ((finishReason === "tool_calls" || finishReason === "tool_use") && toolCalls.length === 0) {
|
|
5082
|
+
throw new Error(`${protocolLabel} 流式工具调用不完整,finish_reason=${finishReason}`);
|
|
5083
|
+
}
|
|
5084
|
+
if (!content.trim() && toolCalls.length === 0) {
|
|
5095
5085
|
throw new Error(`${protocolLabel} 流式响应缺少可用正文,finish_reason=${finishReason}`);
|
|
5096
|
-
|
|
5097
|
-
const outputTokens = resolveOutputTokens(usage, content);
|
|
5086
|
+
}
|
|
5098
5087
|
const anthropicContent = protocol === "anthropic-messages"
|
|
5099
5088
|
? [...anthropicBlocks.entries()]
|
|
5100
5089
|
.sort(([left], [right]) => left - right)
|
|
5101
|
-
.map(([
|
|
5102
|
-
finalizeAnthropicToolInput(index);
|
|
5103
|
-
return redactProviderSecrets(block, apiKey);
|
|
5104
|
-
})
|
|
5090
|
+
.map(([, block]) => redactProviderSecrets(block, apiKey))
|
|
5105
5091
|
: undefined;
|
|
5092
|
+
const usageRecord = usage && typeof usage === "object" && !Array.isArray(usage)
|
|
5093
|
+
? usage
|
|
5094
|
+
: undefined;
|
|
5095
|
+
const normalizedFinishReason = finishReason === "unknown"
|
|
5096
|
+
? null
|
|
5097
|
+
: protocol === "anthropic-messages" && finishReason === "max_tokens"
|
|
5098
|
+
? "length"
|
|
5099
|
+
: finishReason;
|
|
5106
5100
|
return {
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5101
|
+
...(usageRecord ? { usage: usageRecord } : {}),
|
|
5102
|
+
choices: [{
|
|
5103
|
+
finish_reason: normalizedFinishReason,
|
|
5104
|
+
message: {
|
|
5105
|
+
content: content || null,
|
|
5106
|
+
reasoning_content: reasoning || null,
|
|
5107
|
+
...(toolCalls.length ? { tool_calls: toolCalls } : {}),
|
|
5108
|
+
...(anthropicContent?.length ? { anthropic_content: anthropicContent } : {})
|
|
5109
|
+
}
|
|
5110
|
+
}]
|
|
5113
5111
|
};
|
|
5114
5112
|
}
|
|
5115
5113
|
async runChapterAnalysis(workId, scope, modelId, taskId) {
|