@musnows/scriverse 0.8.8 → 0.9.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-analysis-timeout.js +12 -0
- package/dist/ai-analysis-timeout.js.map +1 -0
- package/dist/ai.js +411 -91
- package/dist/ai.js.map +1 -1
- package/dist/app.js +269 -22
- package/dist/app.js.map +1 -1
- package/dist/database.js +128 -2
- package/dist/database.js.map +1 -1
- package/dist/desktop-protocol.js +21 -0
- package/dist/desktop-protocol.js.map +1 -0
- package/dist/offline-sync.js +436 -0
- package/dist/offline-sync.js.map +1 -0
- package/dist/public/app.js +22 -4
- package/dist/public/index.html +3 -3
- package/dist/public/styles.css +10 -2
- package/dist/security.js +3 -2
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +11 -2
- package/dist/server-runtime.js.map +1 -1
- package/dist/storage-manifest.js +116 -0
- package/dist/storage-manifest.js.map +1 -0
- package/dist/store.js +57 -12
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +158 -29
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/ai.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ANALYSIS_TASK_TYPES, HISTORICAL_ANALYSIS_TASK_TYPES } from "./domain.js";
|
|
2
2
|
import { buildCompletionRequestBody, AI_THINKING_TYPES, isAiProviderProtocol, normalizeProviderBaseUrl, parseCompletionPayload, parseProviderModelListPage, providerCompletionEndpoint, providerModelListPageEndpoint, providerModelEndpoints, providerProtocolLabelText, providerRequestHeaders } from "./ai-protocol.js";
|
|
3
3
|
import { estimateLiteLlmUsageCost } from "./ai-model-pricing.js";
|
|
4
|
+
import { DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS, isLongRunningAiAnalysisTaskType, normalizeAiAnalysisTimeoutSeconds } from "./ai-analysis-timeout.js";
|
|
4
5
|
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";
|
|
5
6
|
import { AiConnectivityTestGate, hashAiConnectivityConfiguration } from "./ai-connectivity-test.js";
|
|
6
7
|
import { aiHttpRetryCount, aiHttpRetryDelayMs, normalizeAiRetryPolicy } from "./ai-retry.js";
|
|
@@ -13,7 +14,7 @@ import { assertOfficialGoogleVertexBaseUrl, fetchGoogleOAuthAccessToken, GoogleV
|
|
|
13
14
|
import { HYBRID_SEARCH_TYPES, MAXIMUM_WORK_SEARCH_QUERY_LENGTH, buildHybridSearchSnippet, documentParagraphLineRangesFromLines, fuseHybridSearchChannels, normalizeWorkSearchQuery } from "./hybrid-search.js";
|
|
14
15
|
import { logger, sanitizeError } from "./logger.js";
|
|
15
16
|
import { paginated, paginationSql } from "./pagination.js";
|
|
16
|
-
import { currentRequestActor } from "./request-context.js";
|
|
17
|
+
import { currentRequestActor, runWithRequestActor } from "./request-context.js";
|
|
17
18
|
import { aiEndpointUsesPrivateNetwork, fetchSafeAiEndpoint } from "./security.js";
|
|
18
19
|
import { defaultAiConversationTitle, normalizeCharacterName } from "./store.js";
|
|
19
20
|
import { composeRoleplayCurrentUserTurn, formatRoleplayScenePinText, roleplayUserTurnTitleSource } from "./roleplay-turn.js";
|
|
@@ -53,7 +54,6 @@ function connectivityTestErrorForLog(error) {
|
|
|
53
54
|
const AUTO_RUN_MAX_ATTEMPTS = 3;
|
|
54
55
|
const AUTO_RUN_RETRY_DELAYS_MS = [5_000, 30_000];
|
|
55
56
|
const AI_INTERACTIVE_TIMEOUT_MS = 60_000;
|
|
56
|
-
const AI_LONG_RUNNING_TIMEOUT_MS = 300_000;
|
|
57
57
|
const FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT = 95;
|
|
58
58
|
const MIN_OUTPUT_RESERVE_TOKENS = 1_024;
|
|
59
59
|
const MIN_CONTEXT_REMAINING_TOKENS = 5_000;
|
|
@@ -204,6 +204,9 @@ export function autoRunFailureDisposition(error, attemptCount) {
|
|
|
204
204
|
pauseImmediately
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
|
+
const DESKTOP_LOCAL_AI_RUN_LIMIT = 20;
|
|
208
|
+
const DESKTOP_LOCAL_AI_RUN_RETENTION_MS = 10 * 60_000;
|
|
209
|
+
const DESKTOP_LOCAL_AI_RESPONSE_MAX_BYTES = 4 * 1024 * 1024;
|
|
207
210
|
const allowedParameters = new Set(["temperature", "top_p", "max_tokens", "presence_penalty", "frequency_penalty", "seed"]);
|
|
208
211
|
const DEFAULT_MAX_TOKENS = 32_000;
|
|
209
212
|
const MAX_MODEL_OUTPUT_TOKENS = 2_000_000;
|
|
@@ -255,6 +258,9 @@ function providerThinkingType(provider) {
|
|
|
255
258
|
const value = stringValue(provider, "thinking_type");
|
|
256
259
|
return AI_THINKING_TYPES.includes(value) ? value : "enabled";
|
|
257
260
|
}
|
|
261
|
+
function providerAnalysisTimeoutSeconds(provider) {
|
|
262
|
+
return normalizeAiAnalysisTimeoutSeconds(numberValue(provider, "analysis_timeout_seconds") || DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS);
|
|
263
|
+
}
|
|
258
264
|
function supportsMultimodalProviderProtocol(provider) {
|
|
259
265
|
return ["openai-chat-completions", "openai-responses", "anthropic-messages", "google-vertex"].includes(providerProtocol(provider));
|
|
260
266
|
}
|
|
@@ -1891,6 +1897,7 @@ export class AiManager {
|
|
|
1891
1897
|
relationshipIndexTimer = null;
|
|
1892
1898
|
relationshipIndexDisposed = false;
|
|
1893
1899
|
providerSchedules = new Map();
|
|
1900
|
+
desktopLocalAiRuns = new Map();
|
|
1894
1901
|
vertexTokenCache = new GoogleVertexTokenCache();
|
|
1895
1902
|
connectivityTestGate;
|
|
1896
1903
|
allowPrivateAiEndpoints;
|
|
@@ -2644,6 +2651,12 @@ export class AiManager {
|
|
|
2644
2651
|
}
|
|
2645
2652
|
dispose() {
|
|
2646
2653
|
logger.info("ai.manager.disposing", { scheduledWorks: this.autoRunTimers.size, activeTasks: this.taskControllers.size });
|
|
2654
|
+
for (const run of this.desktopLocalAiRuns.values()) {
|
|
2655
|
+
run.pending?.dispose();
|
|
2656
|
+
run.pending?.reject(new Error("AI manager disposed"));
|
|
2657
|
+
run.controller.abort(new Error("AI manager disposed"));
|
|
2658
|
+
}
|
|
2659
|
+
this.desktopLocalAiRuns.clear();
|
|
2647
2660
|
if (this.autoRunStartupTimer)
|
|
2648
2661
|
clearTimeout(this.autoRunStartupTimer);
|
|
2649
2662
|
this.autoRunStartupTimer = null;
|
|
@@ -2916,9 +2929,17 @@ export class AiManager {
|
|
|
2916
2929
|
if (protocol === "google-vertex")
|
|
2917
2930
|
assertOfficialGoogleVertexBaseUrl(baseUrl);
|
|
2918
2931
|
this.store.db.run(`INSERT INTO providers (id, work_id, name, base_url, protocol, encrypted_key, key_iv, key_tag, key_hint, status,
|
|
2919
|
-
connection_status, concurrency_limit, rpm_limit, daily_token_quota, monthly_token_quota,
|
|
2920
|
-
|
|
2921
|
-
|
|
2932
|
+
connection_status, concurrency_limit, rpm_limit, analysis_timeout_seconds, daily_token_quota, monthly_token_quota,
|
|
2933
|
+
max_tokens_parameter, thinking_type, note, created_at, updated_at)
|
|
2934
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unchecked', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, providerId, PLATFORM_AI_WORK_ID, input.name, baseUrl, protocol, encrypted.encrypted, encrypted.iv, encrypted.tag, providerCredentialHint(protocol, input.apiKey), input.status ?? "disabled", input.concurrencyLimit ?? 10, input.rpmLimit ?? 10, input.analysisTimeoutSeconds ?? DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS, input.dailyTokenQuota ?? null, input.monthlyTokenQuota ?? null, maxTokensParameter, input.thinkingType ?? "enabled", input.note ?? "", timestamp, timestamp);
|
|
2935
|
+
this.store.audit(PLATFORM_AI_WORK_ID, "provider.created", "provider", providerId, {
|
|
2936
|
+
name: input.name,
|
|
2937
|
+
baseUrl,
|
|
2938
|
+
protocol,
|
|
2939
|
+
maxTokensParameter,
|
|
2940
|
+
thinkingType: input.thinkingType ?? "enabled",
|
|
2941
|
+
analysisTimeoutSeconds: input.analysisTimeoutSeconds ?? DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS
|
|
2942
|
+
});
|
|
2922
2943
|
return this.getProvider(providerId);
|
|
2923
2944
|
}
|
|
2924
2945
|
listProviders() {
|
|
@@ -2978,8 +2999,9 @@ export class AiManager {
|
|
|
2978
2999
|
? nullableNumberValue(row, "monthly_token_quota")
|
|
2979
3000
|
: input.monthlyTokenQuota;
|
|
2980
3001
|
this.store.db.run(`UPDATE providers SET name = ?, base_url = ?, protocol = ?, encrypted_key = ?, key_iv = ?, key_tag = ?, key_hint = ?,
|
|
2981
|
-
status = ?, connection_status = ?, concurrency_limit = ?, rpm_limit = ?,
|
|
2982
|
-
|
|
3002
|
+
status = ?, connection_status = ?, concurrency_limit = ?, rpm_limit = ?, analysis_timeout_seconds = ?,
|
|
3003
|
+
daily_token_quota = ?, monthly_token_quota = ?,
|
|
3004
|
+
max_tokens_parameter = ?, thinking_type = ?, note = ?, updated_at = ? WHERE id = ?`, input.name ?? stringValue(row, "name"), nextBaseUrl, nextProtocol, encryptedKey, keyIv, keyTag, keyHint, input.status ?? stringValue(row, "status"), connectionStatus, input.concurrencyLimit ?? numberValue(row, "concurrency_limit"), input.rpmLimit ?? numberValue(row, "rpm_limit"), input.analysisTimeoutSeconds ?? providerAnalysisTimeoutSeconds(row), nextDailyTokenQuota, nextMonthlyTokenQuota, nextMaxTokensParameter, nextThinkingType, input.note ?? stringValue(row, "note"), now(), providerId);
|
|
2983
3005
|
this.store.audit(PLATFORM_AI_WORK_ID, "provider.updated", "provider", providerId, {
|
|
2984
3006
|
fields: Object.keys(input).filter((key) => key !== "apiKey"),
|
|
2985
3007
|
keyReplaced: Boolean(input.apiKey)
|
|
@@ -4228,7 +4250,7 @@ export class AiManager {
|
|
|
4228
4250
|
this.store.db.run(`INSERT INTO ai_suggestions (id, call_id, work_id, chapter_id, chapter_version, task_type, instruction,
|
|
4229
4251
|
source_text, content, action, status, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`, suggestionId, generated.callId, input.workId, chapter ? String(chapter.id) : null, chapter ? Number(chapter.versionNo) : null, input.taskType, input.instruction, effectiveInput.scope.selection ?? "", generated.content, action, now(), currentRequestActor()?.userId ?? null);
|
|
4230
4252
|
if (input.taskType === "continue")
|
|
4231
|
-
await this.
|
|
4253
|
+
await this.runSuggestionGuardWithRuntime(suggestionId, undefined, effectiveInput.runtime);
|
|
4232
4254
|
return {
|
|
4233
4255
|
...this.getSuggestion(suggestionId),
|
|
4234
4256
|
outputTokens: generated.outputTokens,
|
|
@@ -4356,6 +4378,9 @@ export class AiManager {
|
|
|
4356
4378
|
}
|
|
4357
4379
|
}
|
|
4358
4380
|
async runSuggestionGuard(suggestionId, candidateContent) {
|
|
4381
|
+
return this.runSuggestionGuardWithRuntime(suggestionId, candidateContent);
|
|
4382
|
+
}
|
|
4383
|
+
async runSuggestionGuardWithRuntime(suggestionId, candidateContent, runtime) {
|
|
4359
4384
|
const suggestion = this.getSuggestion(suggestionId);
|
|
4360
4385
|
if (suggestion.taskType !== "continue" || !suggestion.chapterId) {
|
|
4361
4386
|
throw new AppError(409, "GUARD_NOT_APPLICABLE", "只有续写建议可以运行一致性守卫");
|
|
@@ -4387,7 +4412,8 @@ export class AiManager {
|
|
|
4387
4412
|
"续写候选:",
|
|
4388
4413
|
content
|
|
4389
4414
|
].join("\n\n"),
|
|
4390
|
-
extraSystemPrompt: "你是续写一致性守卫。必须逐项对照人物状态、地点、时间、世界观硬约束、章节大纲和未回收伏笔。"
|
|
4415
|
+
extraSystemPrompt: "你是续写一致性守卫。必须逐项对照人物状态、地点、时间、世界观硬约束、章节大纲和未回收伏笔。",
|
|
4416
|
+
...(runtime ? { runtime } : {})
|
|
4391
4417
|
});
|
|
4392
4418
|
const issues = parseGuardIssues(generated.content);
|
|
4393
4419
|
return this.store.createContinuationGuard({
|
|
@@ -4501,43 +4527,14 @@ export class AiManager {
|
|
|
4501
4527
|
}
|
|
4502
4528
|
listCalls(workId) {
|
|
4503
4529
|
this.store.getWork(workId);
|
|
4504
|
-
return this.store.db.all("SELECT * FROM ai_calls WHERE work_id = ? ORDER BY created_at DESC LIMIT 200", workId)
|
|
4505
|
-
|
|
4506
|
-
workId: stringValue(row, "work_id"),
|
|
4507
|
-
taskId: row.task_id === null ? null : stringValue(row, "task_id"),
|
|
4508
|
-
taskType: stringValue(row, "task_type"),
|
|
4509
|
-
provider: this.getProvider(stringValue(row, "provider_id")),
|
|
4510
|
-
model: this.getModel(stringValue(row, "model_id")),
|
|
4511
|
-
contextScope: json(stringValue(row, "context_scope_json"), {}),
|
|
4512
|
-
parameters: json(stringValue(row, "parameters_json"), {}),
|
|
4513
|
-
status: stringValue(row, "status"),
|
|
4514
|
-
failure: row.failure === null ? null : stringValue(row, "failure"),
|
|
4515
|
-
inputChars: numberValue(row, "input_chars"),
|
|
4516
|
-
outputChars: numberValue(row, "output_chars"),
|
|
4517
|
-
createdAt: stringValue(row, "created_at"),
|
|
4518
|
-
completedAt: row.completed_at === null ? null : stringValue(row, "completed_at")
|
|
4519
|
-
}));
|
|
4530
|
+
return this.store.db.all("SELECT * FROM ai_calls WHERE work_id = ? ORDER BY created_at DESC LIMIT 200", workId)
|
|
4531
|
+
.map((row) => this.mapCall(row));
|
|
4520
4532
|
}
|
|
4521
4533
|
listCallsPage(workId, pagination) {
|
|
4522
4534
|
this.store.getWork(workId);
|
|
4523
4535
|
const page = paginationSql(pagination);
|
|
4524
4536
|
const rows = this.store.db.all(`SELECT * FROM ai_calls WHERE work_id = ? ORDER BY created_at DESC${page.sql}`, workId, ...page.params);
|
|
4525
|
-
return paginated(rows.map((row) => (
|
|
4526
|
-
id: stringValue(row, "id"),
|
|
4527
|
-
workId: stringValue(row, "work_id"),
|
|
4528
|
-
taskId: row.task_id === null ? null : stringValue(row, "task_id"),
|
|
4529
|
-
taskType: stringValue(row, "task_type"),
|
|
4530
|
-
provider: this.getProvider(stringValue(row, "provider_id")),
|
|
4531
|
-
model: this.getModel(stringValue(row, "model_id")),
|
|
4532
|
-
contextScope: json(stringValue(row, "context_scope_json"), {}),
|
|
4533
|
-
parameters: json(stringValue(row, "parameters_json"), {}),
|
|
4534
|
-
status: stringValue(row, "status"),
|
|
4535
|
-
failure: row.failure === null ? null : stringValue(row, "failure"),
|
|
4536
|
-
inputChars: numberValue(row, "input_chars"),
|
|
4537
|
-
outputChars: numberValue(row, "output_chars"),
|
|
4538
|
-
createdAt: stringValue(row, "created_at"),
|
|
4539
|
-
completedAt: row.completed_at === null ? null : stringValue(row, "completed_at")
|
|
4540
|
-
})), pagination);
|
|
4537
|
+
return paginated(rows.map((row) => this.mapCall(row)), pagination);
|
|
4541
4538
|
}
|
|
4542
4539
|
getTaskTrace(taskId) {
|
|
4543
4540
|
this.store.getTaskWorkId(taskId);
|
|
@@ -4782,6 +4779,9 @@ export class AiManager {
|
|
|
4782
4779
|
}
|
|
4783
4780
|
getContextUsage(input) {
|
|
4784
4781
|
const { model } = this.resolveModel(input.workId, input.taskType, input.modelId);
|
|
4782
|
+
return this.contextUsageForModel(input, model);
|
|
4783
|
+
}
|
|
4784
|
+
contextUsageForModel(input, model) {
|
|
4785
4785
|
const budget = this.contextBudget(input, model);
|
|
4786
4786
|
const conversation = budget.conversation;
|
|
4787
4787
|
const contextPlan = this.buildContextPlan(input, model, budget);
|
|
@@ -4835,8 +4835,235 @@ export class AiManager {
|
|
|
4835
4835
|
degradedContextBlocks: contextPlan.degradedBlockIds.length
|
|
4836
4836
|
};
|
|
4837
4837
|
}
|
|
4838
|
+
async startDesktopLocalAiRun(input, actorScope, actor, permissions) {
|
|
4839
|
+
this.pruneDesktopLocalAiRuns();
|
|
4840
|
+
if (this.desktopLocalAiRuns.size >= DESKTOP_LOCAL_AI_RUN_LIMIT) {
|
|
4841
|
+
throw new AppError(429, "DESKTOP_LOCAL_AI_RUN_LIMIT", "当前正在处理的 Desktop 本地 AI 请求过多,请稍后再试");
|
|
4842
|
+
}
|
|
4843
|
+
const { provider, model } = this.desktopLocalAiRuntimeRows(input.runtimeModel);
|
|
4844
|
+
const imageAttachments = await this.prepareChatImageAttachmentsForModel(input.workId, model, provider, input.imageAttachmentIds ?? [], permissions);
|
|
4845
|
+
const runId = id("desktop-local-ai-run");
|
|
4846
|
+
const timestamp = Date.now();
|
|
4847
|
+
const run = {
|
|
4848
|
+
id: runId,
|
|
4849
|
+
workId: input.workId,
|
|
4850
|
+
actorScope,
|
|
4851
|
+
actor,
|
|
4852
|
+
status: "running",
|
|
4853
|
+
createdAt: timestamp,
|
|
4854
|
+
updatedAt: timestamp,
|
|
4855
|
+
controller: new AbortController(),
|
|
4856
|
+
contextUsage: null,
|
|
4857
|
+
pending: null,
|
|
4858
|
+
result: null,
|
|
4859
|
+
error: null
|
|
4860
|
+
};
|
|
4861
|
+
const runtime = {
|
|
4862
|
+
provider,
|
|
4863
|
+
model,
|
|
4864
|
+
localModelId: input.runtimeModel.id,
|
|
4865
|
+
completionTransport: (request) => this.awaitDesktopLocalAiCompletion(run, request)
|
|
4866
|
+
};
|
|
4867
|
+
this.desktopLocalAiRuns.set(runId, run);
|
|
4868
|
+
const updateContextUsage = (contextUsage) => {
|
|
4869
|
+
run.contextUsage = contextUsage;
|
|
4870
|
+
run.updatedAt = Date.now();
|
|
4871
|
+
};
|
|
4872
|
+
void Promise.resolve().then(() => runWithRequestActor(actor, () => this.createSuggestion({
|
|
4873
|
+
workId: input.workId,
|
|
4874
|
+
taskType: input.taskType,
|
|
4875
|
+
instruction: input.instruction,
|
|
4876
|
+
scope: input.scope,
|
|
4877
|
+
modelId: input.runtimeModel.id,
|
|
4878
|
+
signal: run.controller.signal,
|
|
4879
|
+
runtime,
|
|
4880
|
+
onPrepared: updateContextUsage,
|
|
4881
|
+
onContextCompacted: (event) => updateContextUsage(event.contextUsage),
|
|
4882
|
+
...(input.conversationId ? { conversationId: input.conversationId } : {}),
|
|
4883
|
+
...(input.excludeConversationMessageId ? { excludeConversationMessageId: input.excludeConversationMessageId } : {}),
|
|
4884
|
+
...(imageAttachments.length > 0 ? { imageAttachments } : {}),
|
|
4885
|
+
...(input.sceneDirection ? { sceneDirection: input.sceneDirection } : {})
|
|
4886
|
+
}))).then((result) => {
|
|
4887
|
+
if (run.status === "cancelled")
|
|
4888
|
+
return;
|
|
4889
|
+
run.status = "completed";
|
|
4890
|
+
run.result = result;
|
|
4891
|
+
run.contextUsage = result.contextUsage && typeof result.contextUsage === "object" && !Array.isArray(result.contextUsage)
|
|
4892
|
+
? result.contextUsage
|
|
4893
|
+
: run.contextUsage;
|
|
4894
|
+
run.updatedAt = Date.now();
|
|
4895
|
+
}).catch((error) => {
|
|
4896
|
+
if (run.status === "cancelled")
|
|
4897
|
+
return;
|
|
4898
|
+
const appError = error instanceof AppError ? error : null;
|
|
4899
|
+
run.status = "failed";
|
|
4900
|
+
run.error = {
|
|
4901
|
+
status: appError?.status ?? 502,
|
|
4902
|
+
code: appError?.code ?? "AI_CALL_FAILED",
|
|
4903
|
+
message: appError?.message ?? "AI 调用失败"
|
|
4904
|
+
};
|
|
4905
|
+
run.updatedAt = Date.now();
|
|
4906
|
+
});
|
|
4907
|
+
return this.desktopLocalAiRunStatus(runId, input.workId, actorScope);
|
|
4908
|
+
}
|
|
4909
|
+
desktopLocalAiRunStatus(runId, workId, actorScope) {
|
|
4910
|
+
this.pruneDesktopLocalAiRuns();
|
|
4911
|
+
const run = this.desktopLocalAiRun(runId, workId, actorScope);
|
|
4912
|
+
return {
|
|
4913
|
+
id: run.id,
|
|
4914
|
+
status: run.status,
|
|
4915
|
+
...(run.contextUsage ? { contextUsage: run.contextUsage } : {}),
|
|
4916
|
+
...(run.pending ? { completion: run.pending.request } : {}),
|
|
4917
|
+
...(run.result ? { result: run.result } : {}),
|
|
4918
|
+
...(run.error ? { error: run.error } : {})
|
|
4919
|
+
};
|
|
4920
|
+
}
|
|
4921
|
+
submitDesktopLocalAiCompletion(runId, workId, actorScope, input) {
|
|
4922
|
+
const run = this.desktopLocalAiRun(runId, workId, actorScope);
|
|
4923
|
+
const pending = run.pending;
|
|
4924
|
+
if (!pending || run.status !== "awaiting-completion") {
|
|
4925
|
+
throw new AppError(409, "DESKTOP_LOCAL_AI_NOT_AWAITING", "当前 Desktop 本地 AI 请求不等待模型响应");
|
|
4926
|
+
}
|
|
4927
|
+
if (pending.request.requestId !== input.requestId) {
|
|
4928
|
+
throw new AppError(409, "DESKTOP_LOCAL_AI_REQUEST_MISMATCH", "Desktop 本地 AI 响应与当前请求不匹配");
|
|
4929
|
+
}
|
|
4930
|
+
if (Buffer.byteLength(input.body, "utf8") > DESKTOP_LOCAL_AI_RESPONSE_MAX_BYTES) {
|
|
4931
|
+
throw new AppError(413, "DESKTOP_LOCAL_AI_RESPONSE_TOO_LARGE", "Desktop 本地 AI 响应过大");
|
|
4932
|
+
}
|
|
4933
|
+
run.pending = null;
|
|
4934
|
+
run.status = "running";
|
|
4935
|
+
run.updatedAt = Date.now();
|
|
4936
|
+
pending.dispose();
|
|
4937
|
+
pending.resolve({
|
|
4938
|
+
status: input.status,
|
|
4939
|
+
body: input.body,
|
|
4940
|
+
retryAfter: input.retryAfter ?? null
|
|
4941
|
+
});
|
|
4942
|
+
return this.desktopLocalAiRunStatus(runId, workId, actorScope);
|
|
4943
|
+
}
|
|
4944
|
+
cancelDesktopLocalAiRun(runId, workId, actorScope) {
|
|
4945
|
+
const run = this.desktopLocalAiRun(runId, workId, actorScope);
|
|
4946
|
+
if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
|
|
4947
|
+
return this.desktopLocalAiRunStatus(runId, workId, actorScope);
|
|
4948
|
+
}
|
|
4949
|
+
run.status = "cancelled";
|
|
4950
|
+
run.updatedAt = Date.now();
|
|
4951
|
+
run.pending?.dispose();
|
|
4952
|
+
run.pending?.reject(new Error("Desktop local AI run cancelled"));
|
|
4953
|
+
run.pending = null;
|
|
4954
|
+
run.controller.abort(new Error("Desktop local AI run cancelled"));
|
|
4955
|
+
return this.desktopLocalAiRunStatus(runId, workId, actorScope);
|
|
4956
|
+
}
|
|
4957
|
+
desktopLocalAiRuntimeRows(input) {
|
|
4958
|
+
const timestamp = now();
|
|
4959
|
+
return {
|
|
4960
|
+
provider: {
|
|
4961
|
+
id: input.providerId,
|
|
4962
|
+
work_id: PLATFORM_AI_WORK_ID,
|
|
4963
|
+
name: input.providerName,
|
|
4964
|
+
base_url: "",
|
|
4965
|
+
protocol: input.protocol,
|
|
4966
|
+
encrypted_key: "",
|
|
4967
|
+
key_iv: "",
|
|
4968
|
+
key_tag: "",
|
|
4969
|
+
key_hint: "",
|
|
4970
|
+
status: "enabled",
|
|
4971
|
+
connection_status: "success",
|
|
4972
|
+
max_tokens_parameter: input.maxTokensParameter,
|
|
4973
|
+
thinking_type: input.thinkingType,
|
|
4974
|
+
concurrency_limit: input.concurrencyLimit,
|
|
4975
|
+
rpm_limit: input.rpmLimit,
|
|
4976
|
+
analysis_timeout_seconds: input.analysisTimeoutSeconds,
|
|
4977
|
+
daily_token_quota: null,
|
|
4978
|
+
monthly_token_quota: null,
|
|
4979
|
+
default_model_id: input.id,
|
|
4980
|
+
note: input.note,
|
|
4981
|
+
last_error: null,
|
|
4982
|
+
last_success_at: timestamp,
|
|
4983
|
+
created_at: timestamp,
|
|
4984
|
+
updated_at: timestamp,
|
|
4985
|
+
desktop_local: 1
|
|
4986
|
+
},
|
|
4987
|
+
model: {
|
|
4988
|
+
id: input.id,
|
|
4989
|
+
provider_id: input.providerId,
|
|
4990
|
+
display_name: input.displayName,
|
|
4991
|
+
model_id: input.modelId,
|
|
4992
|
+
enabled: 1,
|
|
4993
|
+
purposes_json: JSON.stringify(input.purposes),
|
|
4994
|
+
context_note: input.contextNote,
|
|
4995
|
+
context_window: input.contextWindow,
|
|
4996
|
+
output_note: input.outputNote,
|
|
4997
|
+
preset_json: JSON.stringify(input.preset),
|
|
4998
|
+
thinking_enabled: input.thinkingEnabled ? 1 : 0,
|
|
4999
|
+
thinking_effort: input.thinkingEffort,
|
|
5000
|
+
multimodal_enabled: input.multimodalEnabled ? 1 : 0,
|
|
5001
|
+
note: input.note,
|
|
5002
|
+
created_at: timestamp,
|
|
5003
|
+
updated_at: timestamp,
|
|
5004
|
+
desktop_local: 1
|
|
5005
|
+
}
|
|
5006
|
+
};
|
|
5007
|
+
}
|
|
5008
|
+
desktopLocalAiRun(runId, workId, actorScope) {
|
|
5009
|
+
const run = this.desktopLocalAiRuns.get(runId);
|
|
5010
|
+
if (!run || run.workId !== workId || run.actorScope !== actorScope)
|
|
5011
|
+
throw notFound("Desktop 本地 AI 请求");
|
|
5012
|
+
return run;
|
|
5013
|
+
}
|
|
5014
|
+
awaitDesktopLocalAiCompletion(run, request) {
|
|
5015
|
+
if (run.controller.signal.aborted)
|
|
5016
|
+
return Promise.reject(new Error("Desktop local AI run cancelled"));
|
|
5017
|
+
if (run.pending)
|
|
5018
|
+
return Promise.reject(new Error("Desktop local AI run already has a pending completion"));
|
|
5019
|
+
return new Promise((resolve, reject) => {
|
|
5020
|
+
const timeout = setTimeout(() => {
|
|
5021
|
+
if (run.pending?.request.requestId !== request.requestId)
|
|
5022
|
+
return;
|
|
5023
|
+
run.pending = null;
|
|
5024
|
+
run.status = "running";
|
|
5025
|
+
run.updatedAt = Date.now();
|
|
5026
|
+
run.controller.signal.removeEventListener("abort", onAbort);
|
|
5027
|
+
reject(new Error(`AI 请求超时(${Math.round(request.timeoutMs / 1_000)} 秒)`));
|
|
5028
|
+
}, request.timeoutMs);
|
|
5029
|
+
const onAbort = () => {
|
|
5030
|
+
if (run.pending?.request.requestId !== request.requestId)
|
|
5031
|
+
return;
|
|
5032
|
+
run.pending = null;
|
|
5033
|
+
clearTimeout(timeout);
|
|
5034
|
+
reject(new Error("Desktop local AI run cancelled"));
|
|
5035
|
+
};
|
|
5036
|
+
const dispose = () => {
|
|
5037
|
+
clearTimeout(timeout);
|
|
5038
|
+
run.controller.signal.removeEventListener("abort", onAbort);
|
|
5039
|
+
};
|
|
5040
|
+
run.controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
5041
|
+
run.pending = {
|
|
5042
|
+
request,
|
|
5043
|
+
resolve: (response) => {
|
|
5044
|
+
dispose();
|
|
5045
|
+
resolve(response);
|
|
5046
|
+
},
|
|
5047
|
+
reject: (error) => {
|
|
5048
|
+
dispose();
|
|
5049
|
+
reject(error);
|
|
5050
|
+
},
|
|
5051
|
+
dispose
|
|
5052
|
+
};
|
|
5053
|
+
run.status = "awaiting-completion";
|
|
5054
|
+
run.updatedAt = Date.now();
|
|
5055
|
+
});
|
|
5056
|
+
}
|
|
5057
|
+
pruneDesktopLocalAiRuns() {
|
|
5058
|
+
const cutoff = Date.now() - DESKTOP_LOCAL_AI_RUN_RETENTION_MS;
|
|
5059
|
+
for (const [runId, run] of this.desktopLocalAiRuns) {
|
|
5060
|
+
if (run.updatedAt >= cutoff || run.status === "running" || run.status === "awaiting-completion")
|
|
5061
|
+
continue;
|
|
5062
|
+
this.desktopLocalAiRuns.delete(runId);
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
4838
5065
|
completionContextUsage(input, model, messages, tools, reportedUsage) {
|
|
4839
|
-
const baseUsage = this.
|
|
5066
|
+
const baseUsage = this.contextUsageForModel(input, model);
|
|
4840
5067
|
const contextWindow = numberValue(model, "context_window") || DEFAULT_CONTEXT_WINDOW;
|
|
4841
5068
|
const serializedMessageTokens = estimateCompletionMessageTokens(messages);
|
|
4842
5069
|
const systemPromptTokens = messages
|
|
@@ -4958,12 +5185,15 @@ export class AiManager {
|
|
|
4958
5185
|
return this.mergeInstructionEntityMatches(input.scope, matches);
|
|
4959
5186
|
}
|
|
4960
5187
|
async prepareChatImageAttachments(workId, modelId, attachmentIds, permissions) {
|
|
5188
|
+
const { model, provider } = this.resolveModel(workId, "chat", modelId);
|
|
5189
|
+
return this.prepareChatImageAttachmentsForModel(workId, model, provider, attachmentIds, permissions);
|
|
5190
|
+
}
|
|
5191
|
+
async prepareChatImageAttachmentsForModel(workId, model, provider, attachmentIds, permissions) {
|
|
4961
5192
|
const ids = [...new Set(attachmentIds.map((attachmentId) => String(attachmentId).trim()).filter(Boolean))];
|
|
4962
5193
|
if (ids.length === 0)
|
|
4963
5194
|
return [];
|
|
4964
5195
|
if (ids.length > 4)
|
|
4965
5196
|
throw new AppError(400, "AI_CHAT_IMAGE_LIMIT", "一次最多添加 4 张图片附件");
|
|
4966
|
-
const { model, provider } = this.resolveModel(workId, "chat", modelId);
|
|
4967
5197
|
if (!boolValue(model, "multimodal_enabled")) {
|
|
4968
5198
|
throw new AppError(400, "MODEL_NOT_MULTIMODAL", "当前选择的模型不是多模态模型,无法处理图片附件");
|
|
4969
5199
|
}
|
|
@@ -5006,11 +5236,10 @@ export class AiManager {
|
|
|
5006
5236
|
}
|
|
5007
5237
|
return prepared;
|
|
5008
5238
|
}
|
|
5009
|
-
async prepareConversationImageAttachments(workId,
|
|
5239
|
+
async prepareConversationImageAttachments(workId, model, provider, conversation) {
|
|
5010
5240
|
const preparedByMessage = new Map();
|
|
5011
5241
|
if (!conversation)
|
|
5012
5242
|
return preparedByMessage;
|
|
5013
|
-
const { model, provider } = this.resolveModel(workId, "chat", modelId);
|
|
5014
5243
|
if (!boolValue(model, "multimodal_enabled") || !supportsMultimodalProviderProtocol(provider)) {
|
|
5015
5244
|
return preparedByMessage;
|
|
5016
5245
|
}
|
|
@@ -5023,7 +5252,7 @@ export class AiManager {
|
|
|
5023
5252
|
: [];
|
|
5024
5253
|
if (ids.length === 0)
|
|
5025
5254
|
continue;
|
|
5026
|
-
preparedByMessage.set(message.id, await this.
|
|
5255
|
+
preparedByMessage.set(message.id, await this.prepareChatImageAttachmentsForModel(workId, model, provider, ids, permissions));
|
|
5027
5256
|
}
|
|
5028
5257
|
return preparedByMessage;
|
|
5029
5258
|
}
|
|
@@ -5486,8 +5715,10 @@ export class AiManager {
|
|
|
5486
5715
|
? this.roleplayCharacterId(workId, conversationId)
|
|
5487
5716
|
: roleplayCharacterIdOverride;
|
|
5488
5717
|
const permissions = this.store.getWork(workId).modulePermissions;
|
|
5718
|
+
const requested = requestedToolIds ? new Set(requestedToolIds) : null;
|
|
5719
|
+
if (requested?.size === 0)
|
|
5720
|
+
return [];
|
|
5489
5721
|
if (roleplayCharacterId) {
|
|
5490
|
-
const requested = requestedToolIds ? new Set(requestedToolIds) : null;
|
|
5491
5722
|
if (!canReadWorkModule(permissions, "characters"))
|
|
5492
5723
|
return [];
|
|
5493
5724
|
const roleplayTools = [];
|
|
@@ -5518,7 +5749,6 @@ export class AiManager {
|
|
|
5518
5749
|
: this.store.getWorkAiSettings(workId).agentTools;
|
|
5519
5750
|
const enabled = new Set(sourceTools
|
|
5520
5751
|
.filter((item) => typeof item === "string" && CONFIGURED_AGENT_TOOL_IDS.includes(item)));
|
|
5521
|
-
const requested = requestedToolIds ? new Set(requestedToolIds) : null;
|
|
5522
5752
|
return CONFIGURED_AGENT_TOOL_IDS.filter((toolId) => enabled.has(toolId)
|
|
5523
5753
|
&& (!requested || requested.has(toolId))
|
|
5524
5754
|
&& this.canReadWithAgentTool(permissions, toolId));
|
|
@@ -6722,13 +6952,13 @@ export class AiManager {
|
|
|
6722
6952
|
max_tokens: Math.min(Number(parameters.max_tokens) || DEFAULT_MAX_TOKENS, contextWindow - inputTokens)
|
|
6723
6953
|
};
|
|
6724
6954
|
}
|
|
6725
|
-
constrainParametersForTokenQuota(workId, provider, messages, parameters, tools = [], additionalUsedTokens = 0) {
|
|
6955
|
+
constrainParametersForTokenQuota(workId, provider, messages, parameters, tools = [], additionalUsedTokens = 0, includeProviderQuota = true) {
|
|
6726
6956
|
const workStatus = this.getWorkTokenQuotaStatus(workId);
|
|
6727
|
-
const providerStatus = this.getProviderTokenQuotaStatus(stringValue(provider, "id"));
|
|
6957
|
+
const providerStatus = includeProviderQuota ? this.getProviderTokenQuotaStatus(stringValue(provider, "id")) : null;
|
|
6728
6958
|
const dailyTokenQuota = workStatus.dailyTokenQuota === null ? null : Number(workStatus.dailyTokenQuota);
|
|
6729
6959
|
const monthlyTokenQuota = workStatus.monthlyTokenQuota === null ? null : Number(workStatus.monthlyTokenQuota);
|
|
6730
|
-
const providerDailyTokenQuota = providerStatus
|
|
6731
|
-
const providerMonthlyTokenQuota = providerStatus
|
|
6960
|
+
const providerDailyTokenQuota = providerStatus?.dailyTokenQuota === null || !providerStatus ? null : Number(providerStatus.dailyTokenQuota);
|
|
6961
|
+
const providerMonthlyTokenQuota = providerStatus?.monthlyTokenQuota === null || !providerStatus ? null : Number(providerStatus.monthlyTokenQuota);
|
|
6732
6962
|
if (dailyTokenQuota === null && monthlyTokenQuota === null && providerDailyTokenQuota === null && providerMonthlyTokenQuota === null)
|
|
6733
6963
|
return parameters;
|
|
6734
6964
|
const additionalTokens = Math.max(0, additionalUsedTokens);
|
|
@@ -6753,8 +6983,10 @@ export class AiManager {
|
|
|
6753
6983
|
resetsAt: String(workStatus.monthlyResetsAt),
|
|
6754
6984
|
startedAt: String(workStatus.monthStartedAt),
|
|
6755
6985
|
timezone: String(workStatus.timezone)
|
|
6756
|
-
}
|
|
6757
|
-
|
|
6986
|
+
}
|
|
6987
|
+
];
|
|
6988
|
+
if (providerStatus) {
|
|
6989
|
+
quotas.push({
|
|
6758
6990
|
scope: "provider",
|
|
6759
6991
|
period: "daily",
|
|
6760
6992
|
quota: providerDailyTokenQuota,
|
|
@@ -6764,8 +6996,7 @@ export class AiManager {
|
|
|
6764
6996
|
timezone: String(providerStatus.timezone),
|
|
6765
6997
|
providerId: stringValue(provider, "id"),
|
|
6766
6998
|
providerName: stringValue(provider, "name")
|
|
6767
|
-
},
|
|
6768
|
-
{
|
|
6999
|
+
}, {
|
|
6769
7000
|
scope: "provider",
|
|
6770
7001
|
period: "monthly",
|
|
6771
7002
|
quota: providerMonthlyTokenQuota,
|
|
@@ -6775,8 +7006,8 @@ export class AiManager {
|
|
|
6775
7006
|
timezone: String(providerStatus.timezone),
|
|
6776
7007
|
providerId: stringValue(provider, "id"),
|
|
6777
7008
|
providerName: stringValue(provider, "name")
|
|
6778
|
-
}
|
|
6779
|
-
|
|
7009
|
+
});
|
|
7010
|
+
}
|
|
6780
7011
|
for (const item of quotas) {
|
|
6781
7012
|
if (item.quota === null)
|
|
6782
7013
|
continue;
|
|
@@ -6832,8 +7063,8 @@ export class AiManager {
|
|
|
6832
7063
|
? this.store.getAiConversationContext(input.conversationId, input.workId, input.excludeConversationMessageId)
|
|
6833
7064
|
: null;
|
|
6834
7065
|
const generationRoleplayCharacterId = this.roleplayCharacterIdFromConversation(input.workId, conversation);
|
|
6835
|
-
const { model, provider } = this.resolveModel(input.workId, input.taskType, input.modelId);
|
|
6836
|
-
const conversationImageAttachments = await this.prepareConversationImageAttachments(input.workId,
|
|
7066
|
+
const { model, provider } = input.runtime ?? this.resolveModel(input.workId, input.taskType, input.modelId);
|
|
7067
|
+
const conversationImageAttachments = await this.prepareConversationImageAttachments(input.workId, model, provider, conversation);
|
|
6837
7068
|
const preset = safeJsonObject(stringValue(model, "preset_json"));
|
|
6838
7069
|
const requestedParameters = {
|
|
6839
7070
|
...this.sanitizeParameters({ ...preset, ...(input.parameters ?? {}) }, stringValue(model, "model_id")),
|
|
@@ -6880,14 +7111,24 @@ export class AiManager {
|
|
|
6880
7111
|
modelId: stringValue(model, "id")
|
|
6881
7112
|
});
|
|
6882
7113
|
}
|
|
6883
|
-
parameters = this.constrainParametersForTokenQuota(input.workId, provider, messages, parameters, tools);
|
|
7114
|
+
parameters = this.constrainParametersForTokenQuota(input.workId, provider, messages, parameters, tools, 0, input.runtime === undefined);
|
|
7115
|
+
input.onPrepared?.(this.completionContextUsage(effectiveInput, model, messages, tools));
|
|
6884
7116
|
const completionMessages = [...messages];
|
|
6885
7117
|
const callId = id("call");
|
|
6886
7118
|
const timestamp = now();
|
|
6887
7119
|
const traceRounds = [];
|
|
7120
|
+
const storedParameters = input.runtime
|
|
7121
|
+
? {
|
|
7122
|
+
...parameters,
|
|
7123
|
+
__desktopLocalAi: {
|
|
7124
|
+
provider: this.mapProvider(provider),
|
|
7125
|
+
model: this.mapModel(model)
|
|
7126
|
+
}
|
|
7127
|
+
}
|
|
7128
|
+
: parameters;
|
|
6888
7129
|
this.store.db.transaction(() => {
|
|
6889
7130
|
this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
|
|
6890
|
-
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(
|
|
7131
|
+
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(storedParameters), context.length + input.instruction.length, timestamp, currentRequestActor()?.userId ?? null);
|
|
6891
7132
|
if (input.taskId) {
|
|
6892
7133
|
this.store.db.run(`INSERT INTO ai_call_traces (call_id, task_id, initial_messages_json, rounds_json, source_refs_json, created_at, updated_at)
|
|
6893
7134
|
VALUES (?, ?, ?, '[]', ?, ?, ?)`, callId, input.taskId, JSON.stringify(sanitizeCompletionTraceMessages(messages)), JSON.stringify(taskTraceSourceRefs(messages, [])), timestamp, timestamp);
|
|
@@ -6937,11 +7178,16 @@ export class AiManager {
|
|
|
6937
7178
|
return "mixed";
|
|
6938
7179
|
};
|
|
6939
7180
|
try {
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
7181
|
+
let accessToken = "";
|
|
7182
|
+
let endpoint = "";
|
|
7183
|
+
if (!input.runtime) {
|
|
7184
|
+
const credential = await this.resolveProviderAccessToken(provider);
|
|
7185
|
+
accessToken = credential.accessToken;
|
|
7186
|
+
activeSecrets = [credential.credentialSecret, credential.accessToken];
|
|
7187
|
+
endpoint = providerCompletionEndpoint(stringValue(provider, "base_url"), protocol);
|
|
7188
|
+
}
|
|
7189
|
+
const timeoutMs = isLongRunningAiAnalysisTaskType(input.taskType)
|
|
7190
|
+
? providerAnalysisTimeoutSeconds(provider) * 1_000
|
|
6945
7191
|
: AI_INTERACTIVE_TIMEOUT_MS;
|
|
6946
7192
|
const legacyMaximumAttempts = Math.round(clamp(input.maxAttempts ?? 3, 1, 5));
|
|
6947
7193
|
const maximumAttempts = Math.max(legacyMaximumAttempts, this.retryPolicy.retryCount + 1, this.retryPolicy.backoffRetryCount + 1);
|
|
@@ -6957,11 +7203,11 @@ export class AiManager {
|
|
|
6957
7203
|
const requestParameters = options.parameters ?? parameters;
|
|
6958
7204
|
const purpose = options.purpose ?? "generation";
|
|
6959
7205
|
const requestTools = toolChoice === "auto" ? tools : [];
|
|
6960
|
-
const streamResponse = Boolean(onDelta) && purpose === "generation";
|
|
7206
|
+
const streamResponse = !input.runtime && Boolean(onDelta) && purpose === "generation";
|
|
6961
7207
|
const processRound = streamResponse ? streamingGenerationRound + 1 : 0;
|
|
6962
7208
|
if (streamResponse)
|
|
6963
7209
|
streamingGenerationRound = processRound;
|
|
6964
|
-
const roundParameters = this.constrainParametersForTokenQuota(input.workId, provider, requestMessages, this.constrainParametersForContext(model, requestMessages, requestParameters, requestTools), requestTools, trackedInputTokens + trackedOutputTokens);
|
|
7210
|
+
const roundParameters = this.constrainParametersForTokenQuota(input.workId, provider, requestMessages, this.constrainParametersForContext(model, requestMessages, requestParameters, requestTools), requestTools, trackedInputTokens + trackedOutputTokens, input.runtime === undefined);
|
|
6965
7211
|
const traceRound = {
|
|
6966
7212
|
round: traceRounds.length + 1,
|
|
6967
7213
|
requestedAt: now(),
|
|
@@ -6976,6 +7222,16 @@ export class AiManager {
|
|
|
6976
7222
|
attempts: [],
|
|
6977
7223
|
toolExecutions: []
|
|
6978
7224
|
};
|
|
7225
|
+
const completionRequestBody = buildCompletionRequestBody({
|
|
7226
|
+
protocol,
|
|
7227
|
+
model: stringValue(model, "model_id"),
|
|
7228
|
+
messages: requestMessages,
|
|
7229
|
+
parameters: roundParameters,
|
|
7230
|
+
maxTokensParameter: providerMaxTokensParameter(provider),
|
|
7231
|
+
tools: requestTools,
|
|
7232
|
+
toolChoice,
|
|
7233
|
+
...(streamResponse ? { stream: true } : {})
|
|
7234
|
+
});
|
|
6979
7235
|
traceRounds.push(traceRound);
|
|
6980
7236
|
saveTrace();
|
|
6981
7237
|
let streamedThinkingStep = null;
|
|
@@ -6997,6 +7253,31 @@ export class AiManager {
|
|
|
6997
7253
|
let streamedRoundContent = "";
|
|
6998
7254
|
try {
|
|
6999
7255
|
const candidate = await this.scheduleProviderRequest(provider, input.signal, async () => {
|
|
7256
|
+
if (input.runtime) {
|
|
7257
|
+
const response = await input.runtime.completionTransport({
|
|
7258
|
+
requestId: id("desktop-local-ai-completion"),
|
|
7259
|
+
localModelId: input.runtime.localModelId,
|
|
7260
|
+
taskType: input.taskType,
|
|
7261
|
+
purpose,
|
|
7262
|
+
body: completionRequestBody,
|
|
7263
|
+
timeoutMs
|
|
7264
|
+
});
|
|
7265
|
+
if (response.status < 200 || response.status >= 300) {
|
|
7266
|
+
return {
|
|
7267
|
+
ok: false,
|
|
7268
|
+
status: response.status,
|
|
7269
|
+
body: response.body,
|
|
7270
|
+
retryAfter: response.retryAfter
|
|
7271
|
+
};
|
|
7272
|
+
}
|
|
7273
|
+
try {
|
|
7274
|
+
const payload = parseCompletionPayload(protocol, JSON.parse(response.body));
|
|
7275
|
+
return { ok: true, status: response.status, payload, delivery: "json" };
|
|
7276
|
+
}
|
|
7277
|
+
catch {
|
|
7278
|
+
throw new Error(`${providerProtocolLabelText(protocol)} returned invalid JSON: ${response.body.slice(0, 500)}`);
|
|
7279
|
+
}
|
|
7280
|
+
}
|
|
7000
7281
|
const controller = new AbortController();
|
|
7001
7282
|
const forwardAbort = () => controller.abort(input.signal?.reason);
|
|
7002
7283
|
if (input.signal?.aborted)
|
|
@@ -7015,16 +7296,7 @@ export class AiManager {
|
|
|
7015
7296
|
const response = await this.outboundFetch(endpoint, {
|
|
7016
7297
|
method: "POST",
|
|
7017
7298
|
headers: providerRequestHeaders(protocol, accessToken, streamResponse ? "text/event-stream" : "application/json"),
|
|
7018
|
-
body: JSON.stringify(
|
|
7019
|
-
protocol,
|
|
7020
|
-
model: stringValue(model, "model_id"),
|
|
7021
|
-
messages: requestMessages,
|
|
7022
|
-
parameters: roundParameters,
|
|
7023
|
-
maxTokensParameter: providerMaxTokensParameter(provider),
|
|
7024
|
-
tools: requestTools,
|
|
7025
|
-
toolChoice,
|
|
7026
|
-
...(streamResponse ? { stream: true } : {})
|
|
7027
|
-
})),
|
|
7299
|
+
body: JSON.stringify(completionRequestBody),
|
|
7028
7300
|
signal: controller.signal
|
|
7029
7301
|
});
|
|
7030
7302
|
responseReceived = true;
|
|
@@ -11677,19 +11949,22 @@ export class AiManager {
|
|
|
11677
11949
|
return row;
|
|
11678
11950
|
}
|
|
11679
11951
|
mapProvider(row) {
|
|
11952
|
+
const desktopLocal = boolValue(row, "desktop_local");
|
|
11680
11953
|
let apiKeyHint = stringValue(row, "key_hint");
|
|
11681
|
-
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11954
|
+
if (!desktopLocal) {
|
|
11955
|
+
try {
|
|
11956
|
+
const secret = this.decryptKey(row);
|
|
11957
|
+
apiKeyHint = providerCredentialHint(providerProtocol(row), secret);
|
|
11958
|
+
}
|
|
11959
|
+
catch {
|
|
11960
|
+
// 凭据无法解密时保留数据库中的旧掩码,避免影响供应商列表展示。
|
|
11961
|
+
}
|
|
11687
11962
|
}
|
|
11688
11963
|
return {
|
|
11689
11964
|
id: stringValue(row, "id"),
|
|
11690
|
-
scope: "platform",
|
|
11965
|
+
scope: desktopLocal ? "local" : "platform",
|
|
11691
11966
|
name: stringValue(row, "name"),
|
|
11692
|
-
baseUrl: stringValue(row, "base_url"),
|
|
11967
|
+
baseUrl: desktopLocal ? "" : stringValue(row, "base_url"),
|
|
11693
11968
|
protocol: providerProtocol(row),
|
|
11694
11969
|
maxTokensParameter: providerMaxTokensParameter(row),
|
|
11695
11970
|
thinkingType: providerThinkingType(row),
|
|
@@ -11698,6 +11973,7 @@ export class AiManager {
|
|
|
11698
11973
|
connectionStatus: stringValue(row, "connection_status"),
|
|
11699
11974
|
concurrencyLimit: numberValue(row, "concurrency_limit") || 10,
|
|
11700
11975
|
rpmLimit: numberValue(row, "rpm_limit") || 10,
|
|
11976
|
+
analysisTimeoutSeconds: providerAnalysisTimeoutSeconds(row),
|
|
11701
11977
|
dailyTokenQuota: nullableNumberValue(row, "daily_token_quota"),
|
|
11702
11978
|
monthlyTokenQuota: nullableNumberValue(row, "monthly_token_quota"),
|
|
11703
11979
|
defaultModelId: row.default_model_id === null ? null : stringValue(row, "default_model_id"),
|
|
@@ -11709,8 +11985,10 @@ export class AiManager {
|
|
|
11709
11985
|
};
|
|
11710
11986
|
}
|
|
11711
11987
|
mapModel(row) {
|
|
11988
|
+
const desktopLocal = boolValue(row, "desktop_local");
|
|
11712
11989
|
return {
|
|
11713
11990
|
id: stringValue(row, "id"),
|
|
11991
|
+
...(desktopLocal ? { scope: "local" } : {}),
|
|
11714
11992
|
providerId: stringValue(row, "provider_id"),
|
|
11715
11993
|
displayName: stringValue(row, "display_name"),
|
|
11716
11994
|
modelId: stringValue(row, "model_id"),
|
|
@@ -11722,15 +12000,57 @@ export class AiManager {
|
|
|
11722
12000
|
thinkingEnabled: boolValue(row, "thinking_enabled"),
|
|
11723
12001
|
thinkingEffort: stringValue(row, "thinking_effort") || "default",
|
|
11724
12002
|
multimodalEnabled: boolValue(row, "multimodal_enabled"),
|
|
11725
|
-
imageToolDefault: String(this.store.getPlatformAiSettings().imageToolModelId ?? "") === stringValue(row, "id"),
|
|
12003
|
+
imageToolDefault: !desktopLocal && String(this.store.getPlatformAiSettings().imageToolModelId ?? "") === stringValue(row, "id"),
|
|
11726
12004
|
enabled: boolValue(row, "enabled"),
|
|
11727
12005
|
note: stringValue(row, "note"),
|
|
11728
12006
|
createdAt: stringValue(row, "created_at"),
|
|
11729
12007
|
updatedAt: stringValue(row, "updated_at")
|
|
11730
12008
|
};
|
|
11731
12009
|
}
|
|
12010
|
+
aiCallTarget(row) {
|
|
12011
|
+
const parameters = safeJsonObject(stringValue(row, "parameters_json"));
|
|
12012
|
+
const desktopLocal = parameters.__desktopLocalAi;
|
|
12013
|
+
if (desktopLocal && typeof desktopLocal === "object" && !Array.isArray(desktopLocal)) {
|
|
12014
|
+
const snapshot = desktopLocal;
|
|
12015
|
+
if (snapshot.provider && typeof snapshot.provider === "object" && !Array.isArray(snapshot.provider)
|
|
12016
|
+
&& snapshot.model && typeof snapshot.model === "object" && !Array.isArray(snapshot.model)) {
|
|
12017
|
+
return {
|
|
12018
|
+
provider: structuredClone(snapshot.provider),
|
|
12019
|
+
model: structuredClone(snapshot.model)
|
|
12020
|
+
};
|
|
12021
|
+
}
|
|
12022
|
+
}
|
|
12023
|
+
return {
|
|
12024
|
+
provider: this.getProvider(stringValue(row, "provider_id")),
|
|
12025
|
+
model: this.getModel(stringValue(row, "model_id"))
|
|
12026
|
+
};
|
|
12027
|
+
}
|
|
12028
|
+
publicAiCallParameters(row) {
|
|
12029
|
+
const { __desktopLocalAi: _desktopLocalAi, ...parameters } = safeJsonObject(stringValue(row, "parameters_json"));
|
|
12030
|
+
return parameters;
|
|
12031
|
+
}
|
|
12032
|
+
mapCall(row) {
|
|
12033
|
+
const target = this.aiCallTarget(row);
|
|
12034
|
+
return {
|
|
12035
|
+
id: stringValue(row, "id"),
|
|
12036
|
+
workId: stringValue(row, "work_id"),
|
|
12037
|
+
taskId: row.task_id === null ? null : stringValue(row, "task_id"),
|
|
12038
|
+
taskType: stringValue(row, "task_type"),
|
|
12039
|
+
provider: target.provider,
|
|
12040
|
+
model: target.model,
|
|
12041
|
+
contextScope: json(stringValue(row, "context_scope_json"), {}),
|
|
12042
|
+
parameters: this.publicAiCallParameters(row),
|
|
12043
|
+
status: stringValue(row, "status"),
|
|
12044
|
+
failure: row.failure === null ? null : stringValue(row, "failure"),
|
|
12045
|
+
inputChars: numberValue(row, "input_chars"),
|
|
12046
|
+
outputChars: numberValue(row, "output_chars"),
|
|
12047
|
+
createdAt: stringValue(row, "created_at"),
|
|
12048
|
+
completedAt: row.completed_at === null ? null : stringValue(row, "completed_at")
|
|
12049
|
+
};
|
|
12050
|
+
}
|
|
11732
12051
|
mapSuggestion(row) {
|
|
11733
|
-
const call = this.store.db.get("SELECT provider_id, model_id FROM ai_calls WHERE id = ?", stringValue(row, "call_id"));
|
|
12052
|
+
const call = this.store.db.get("SELECT provider_id, model_id, parameters_json FROM ai_calls WHERE id = ?", stringValue(row, "call_id"));
|
|
12053
|
+
const target = call ? this.aiCallTarget(call) : null;
|
|
11734
12054
|
const guard = this.store.getLatestContinuationGuard(stringValue(row, "id"));
|
|
11735
12055
|
return {
|
|
11736
12056
|
id: stringValue(row, "id"),
|
|
@@ -11746,8 +12066,8 @@ export class AiManager {
|
|
|
11746
12066
|
status: stringValue(row, "status"),
|
|
11747
12067
|
outputTokens: estimateAiTokens(stringValue(row, "content")),
|
|
11748
12068
|
guard,
|
|
11749
|
-
provider:
|
|
11750
|
-
model:
|
|
12069
|
+
provider: target?.provider ?? null,
|
|
12070
|
+
model: target?.model ?? null,
|
|
11751
12071
|
createdAt: stringValue(row, "created_at"),
|
|
11752
12072
|
decidedAt: row.decided_at === null ? null : stringValue(row, "decided_at")
|
|
11753
12073
|
};
|