@musnows/scriverse 0.7.5 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai.js CHANGED
@@ -50,6 +50,9 @@ const AUTO_RUN_MAX_ATTEMPTS = 3;
50
50
  const AUTO_RUN_RETRY_DELAYS_MS = [5_000, 30_000];
51
51
  const AI_INTERACTIVE_TIMEOUT_MS = 60_000;
52
52
  const AI_LONG_RUNNING_TIMEOUT_MS = 300_000;
53
+ const FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT = 95;
54
+ const MIN_OUTPUT_RESERVE_TOKENS = 1_024;
55
+ const MIN_CONTEXT_REMAINING_TOKENS = 5_000;
53
56
  const analysisTaskTypes = new Set(ANALYSIS_TASK_TYPES);
54
57
  const interactiveStreamErrorCodes = new Set([
55
58
  "AI_STREAM_IDLE_TIMEOUT",
@@ -172,6 +175,7 @@ export function autoRunFailureDisposition(error, attemptCount) {
172
175
  }
173
176
  const allowedParameters = new Set(["temperature", "top_p", "max_tokens", "presence_penalty", "frequency_penalty", "seed"]);
174
177
  const DEFAULT_MAX_TOKENS = 32_000;
178
+ const MAX_MODEL_OUTPUT_TOKENS = 2_000_000;
175
179
  const DEFAULT_CONTEXT_WINDOW = 128_000;
176
180
  const RELATIONSHIP_MAX_FUZZY_REFERENCES = 32;
177
181
  const RELATIONSHIP_MAX_FUZZY_SOURCES = 200;
@@ -404,7 +408,7 @@ function sanitizeCompletionTraceResponse(value) {
404
408
  }
405
409
  const MAX_AGENT_TOOL_CALLS = 12;
406
410
  const TOOL_CONTEXT_COMPACT_MAX_TOKENS = 1_024;
407
- const TOOL_CONTEXT_RESPONSE_RESERVE_TOKENS = 512;
411
+ const TOOL_CONTEXT_RESPONSE_RESERVE_TOKENS = MIN_OUTPUT_RESERVE_TOKENS;
408
412
  const IMAGE_TOOL_MAX_BYTES = 30 * 1024 * 1024;
409
413
  const IMAGE_TOOL_MAX_OUTPUT_TOKENS = 8_192;
410
414
  const agentToolCursor = z.number().int().min(0).max(100_000).default(0);
@@ -737,7 +741,7 @@ function completionPayloadOutputText(payload) {
737
741
  }
738
742
  function normalizeModelPreset(input, modelId = "") {
739
743
  const maxTokens = typeof input.max_tokens === "number" && Number.isFinite(input.max_tokens)
740
- ? Math.round(clamp(input.max_tokens, 1, 32_768))
744
+ ? Math.round(clamp(input.max_tokens, 1, MAX_MODEL_OUTPUT_TOKENS))
741
745
  : DEFAULT_MAX_TOKENS;
742
746
  const temperature = input.temperature;
743
747
  const defaultTemperature = isKimiModelId(modelId) && !(typeof temperature === "number" && Number.isFinite(temperature))
@@ -1950,6 +1954,23 @@ export class AiManager {
1950
1954
  runningCount: this.store.countRunningTasks(workId)
1951
1955
  };
1952
1956
  }
1957
+ deleteWork(workId, expectedVersionNo) {
1958
+ const taskIds = this.store.deleteWork(workId, expectedVersionNo);
1959
+ const autoRunTimer = this.autoRunTimers.get(workId);
1960
+ if (autoRunTimer)
1961
+ clearTimeout(autoRunTimer);
1962
+ this.autoRunTimers.delete(workId);
1963
+ this.autoRunStarting.delete(workId);
1964
+ const relationshipIndexTimer = this.relationshipIndexSyncTimers.get(workId);
1965
+ if (relationshipIndexTimer)
1966
+ clearTimeout(relationshipIndexTimer);
1967
+ this.relationshipIndexSyncTimers.delete(workId);
1968
+ for (const taskId of taskIds) {
1969
+ this.taskControllers.get(taskId)?.abort(new Error("作品已移入回收站"));
1970
+ }
1971
+ logger.info("ai.work_tasks.expired", { workId, taskCount: taskIds.length });
1972
+ return taskIds;
1973
+ }
1953
1974
  dispose() {
1954
1975
  logger.info("ai.manager.disposing", { scheduledWorks: this.autoRunTimers.size, activeTasks: this.taskControllers.size });
1955
1976
  if (this.autoRunStartupTimer)
@@ -3553,7 +3574,7 @@ export class AiManager {
3553
3574
  const contextWindow = numberValue(model, "context_window") || DEFAULT_CONTEXT_WINDOW;
3554
3575
  const preset = safeJsonObject(stringValue(model, "preset_json"));
3555
3576
  const configuredOutputTokens = typeof preset.max_tokens === "number" ? preset.max_tokens : DEFAULT_MAX_TOKENS;
3556
- const outputReserveTokens = Math.max(512, Math.min(configuredOutputTokens, Math.floor(contextWindow * 0.25), contextWindow - 512));
3577
+ const outputReserveTokens = Math.max(MIN_OUTPUT_RESERVE_TOKENS, Math.min(configuredOutputTokens, Math.floor(contextWindow * 0.25), contextWindow - MIN_OUTPUT_RESERVE_TOKENS));
3557
3578
  const availableInputTokens = Math.max(256, contextWindow - outputReserveTokens - 512);
3558
3579
  const conversation = input.conversationId
3559
3580
  ? this.store.getAiConversationContext(input.conversationId, input.workId, input.excludeConversationMessageId)
@@ -3572,6 +3593,7 @@ export class AiManager {
3572
3593
  - functionTokens);
3573
3594
  return {
3574
3595
  contextWindow,
3596
+ configuredOutputTokens,
3575
3597
  outputReserveTokens,
3576
3598
  availableInputTokens,
3577
3599
  conversation,
@@ -3601,7 +3623,10 @@ export class AiManager {
3601
3623
  const threshold = Math.min(90, Math.max(50, Number(this.store.getWorkAiSettings(input.workId).contextCompactThreshold) || 85));
3602
3624
  const conversation = budget.conversation;
3603
3625
  const conversationUsagePercent = Number(budget.conversationUsagePercent) || 0;
3626
+ const configuredOutputTokens = Number(budget.configuredOutputTokens) || DEFAULT_MAX_TOKENS;
3627
+ const maxOutputUsagePercent = Math.min(100, Math.round(configuredOutputTokens / contextWindow * 100));
3604
3628
  const compactableMessageCount = Math.max(0, (conversation?.messages.length ?? 0) - 2);
3629
+ const contextFallbackReached = remainingTokens <= MIN_CONTEXT_REMAINING_TOKENS;
3605
3630
  return {
3606
3631
  modelId: stringValue(model, "id"),
3607
3632
  contextWindow,
@@ -3610,8 +3635,12 @@ export class AiManager {
3610
3635
  conversationTokens: Number(budget.conversationTokens),
3611
3636
  conversationBudgetTokens: Number(budget.conversationBudgetTokens),
3612
3637
  conversationUsagePercent,
3638
+ maxOutputTokens: configuredOutputTokens,
3639
+ maxOutputUsagePercent,
3640
+ maxOutputThresholdReached: maxOutputUsagePercent >= threshold,
3613
3641
  outputReserveTokens: Number(budget.outputReserveTokens),
3614
3642
  remainingTokens,
3643
+ contextFallbackReached,
3615
3644
  usagePercent: Math.min(100, Math.round(inputTokens / contextWindow * 100)),
3616
3645
  tokenDistribution: {
3617
3646
  systemPromptTokens,
@@ -3621,7 +3650,8 @@ export class AiManager {
3621
3650
  leftTokens: remainingTokens
3622
3651
  },
3623
3652
  compactThreshold: threshold,
3624
- compactRecommended: compactableMessageCount > 0 && conversationUsagePercent >= threshold,
3653
+ compactableMessageCount,
3654
+ compactRecommended: compactableMessageCount > 0 && (conversationUsagePercent >= threshold || contextFallbackReached),
3625
3655
  contextWarningPending: conversation?.warningPending ?? false,
3626
3656
  compactedMessageCount: conversation?.compactedMessageCount ?? 0,
3627
3657
  includedContextBlocks: contextPlan.includedBlockIds.length,
@@ -3646,6 +3676,7 @@ export class AiManager {
3646
3676
  contextWindow,
3647
3677
  inputTokens,
3648
3678
  remainingTokens,
3679
+ contextFallbackReached: remainingTokens <= MIN_CONTEXT_REMAINING_TOKENS,
3649
3680
  usagePercent: Math.min(100, Math.round(inputTokens / contextWindow * 100)),
3650
3681
  tokenDistribution: {
3651
3682
  systemPromptTokens,
@@ -3658,14 +3689,21 @@ export class AiManager {
3658
3689
  }
3659
3690
  inspectConversationContext(input) {
3660
3691
  const usage = this.getContextUsage({ ...input, taskType: "chat" });
3661
- const conversation = this.store.getAiConversationContext(input.conversationId, input.workId);
3692
+ const usagePercent = Number(usage.usagePercent) || 0;
3693
+ const maxOutputThresholdReached = usage.maxOutputThresholdReached === true;
3694
+ const contextFallbackReached = usage.contextFallbackReached === true;
3695
+ const compactableMessageCount = Number(usage.compactableMessageCount) || 0;
3696
+ const outputThresholdNeedsCompaction = (maxOutputThresholdReached || contextFallbackReached) && compactableMessageCount > 0;
3697
+ if (usagePercent >= FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT || outputThresholdNeedsCompaction) {
3698
+ if (compactableMessageCount <= 0) {
3699
+ throw new AppError(409, "AI_CONTEXT_COMPACTION_UNAVAILABLE", "当前请求已占满模型上下文,但没有可压缩的较早对话;请缩短问题、减少引用或新开对话");
3700
+ }
3701
+ return { action: "compact", usage };
3702
+ }
3662
3703
  if (!usage.compactRecommended) {
3663
3704
  return { action: "ready", usage: { ...usage, contextWarningPending: false } };
3664
3705
  }
3665
- if (!conversation.warningPending) {
3666
- return { action: "warn", usage: { ...usage, contextWarningPending: true } };
3667
- }
3668
- return { action: "compact", usage };
3706
+ return { action: "warn", usage: { ...usage, contextWarningPending: true } };
3669
3707
  }
3670
3708
  async prepareConversationContext(input, options = {}) {
3671
3709
  const inspection = this.inspectConversationContext(input);
@@ -3675,13 +3713,41 @@ export class AiManager {
3675
3713
  this.store.setAiConversationContextWarning(input.conversationId, false);
3676
3714
  return inspection;
3677
3715
  }
3678
- if (inspection.action === "warn" && !options.skipWarning) {
3716
+ if (inspection.action === "warn" && !options.ignoreWarning) {
3679
3717
  this.store.setAiConversationContextWarning(input.conversationId, true);
3680
3718
  return inspection;
3681
3719
  }
3720
+ if (inspection.action === "warn") {
3721
+ this.store.setAiConversationContextWarning(input.conversationId, false);
3722
+ return {
3723
+ action: "ready",
3724
+ reason: "warning_ignored",
3725
+ usage: { ...inspection.usage, contextWarningPending: false }
3726
+ };
3727
+ }
3682
3728
  const compaction = await this.compactConversation(input);
3729
+ if (compaction.changed !== true) {
3730
+ const inputBelowForcedThreshold = (Number(inspection.usage.usagePercent) || 0) < FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT;
3731
+ if ((inspection.usage.maxOutputThresholdReached === true || inspection.usage.contextFallbackReached === true) && inputBelowForcedThreshold) {
3732
+ return {
3733
+ action: "ready",
3734
+ reason: "output_budget_already_fits",
3735
+ usage: { ...inspection.usage, contextWarningPending: false }
3736
+ };
3737
+ }
3738
+ throw new AppError(409, "AI_CONTEXT_COMPACTION_UNAVAILABLE", "当前请求已占满模型上下文,但没有可压缩的较早对话;请缩短问题、减少引用或新开对话");
3739
+ }
3683
3740
  const compactedUsage = this.getContextUsage({ ...input, taskType: "chat" });
3684
- return { action: "compacted", usage: compactedUsage, compaction };
3741
+ if ((Number(compactedUsage.usagePercent) || 0) >= FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT
3742
+ || compactedUsage.contextFallbackReached === true) {
3743
+ throw new AppError(413, "AI_CONTEXT_STILL_OVER_LIMIT", "自动压缩后当前请求仍占满模型上下文;请缩短问题、减少引用或新开对话");
3744
+ }
3745
+ return {
3746
+ action: "compacted",
3747
+ reason: "forced_usage_threshold",
3748
+ usage: compactedUsage,
3749
+ compaction
3750
+ };
3685
3751
  }
3686
3752
  /** 解析本轮消息中的自动角色提及;不使用会话累计排除集,也不改写累计注入状态。 */
3687
3753
  resolveInstructionMentions(input) {
@@ -4709,6 +4775,8 @@ export class AiManager {
4709
4775
  ...this.sanitizeParameters({ ...preset, ...(input.parameters ?? {}) }, stringValue(model, "model_id")),
4710
4776
  ...thinkingParameters(provider, model)
4711
4777
  };
4778
+ const configuredOutputTokens = Number(requestedParameters.max_tokens) || DEFAULT_MAX_TOKENS;
4779
+ const contextCompactThreshold = Math.min(90, Math.max(50, Number(this.store.getWorkAiSettings(input.workId).contextCompactThreshold) || 85));
4712
4780
  let effectiveInput = input;
4713
4781
  let context = this.buildContext(effectiveInput, model);
4714
4782
  let messages = this.buildMessages(effectiveInput, context);
@@ -5125,7 +5193,14 @@ export class AiManager {
5125
5193
  // 新工具结果可能附带 toolCallQuotaNotice,预估体积时一并计入,避免低估后触发上下文溢出。
5126
5194
  const noticeBudgetChars = Math.max(agentToolCallQuotaNoticeBudgetChars(1, agentToolCallLimit), agentToolCallQuotaNoticeBudgetChars(agentToolCallSoftWarningThreshold(agentToolCallLimit), agentToolCallLimit));
5127
5195
  const maximumNewToolTokens = Math.ceil((AGENT_TOOL_RESULT_MAX_CHARS + noticeBudgetChars) * 1.1) * Math.max(1, toolCallCount);
5128
- return currentTokens + maximumNewToolTokens + TOOL_CONTEXT_RESPONSE_RESERVE_TOKENS >= contextWindow;
5196
+ // 这里只按工具结果写入后的 context 剩余判断;输出 max_tokens 由下方独立判断。
5197
+ const projectedContextTokens = currentTokens + maximumNewToolTokens;
5198
+ const projectedUsagePercent = Math.round(projectedContextTokens / contextWindow * 100);
5199
+ const projectedContextRemainingTokens = Math.max(0, contextWindow - projectedContextTokens);
5200
+ const maxOutputThresholdReached = configuredOutputTokens >= contextWindow * contextCompactThreshold / 100;
5201
+ return projectedUsagePercent >= contextCompactThreshold
5202
+ || maxOutputThresholdReached
5203
+ || projectedContextRemainingTokens <= MIN_CONTEXT_REMAINING_TOKENS;
5129
5204
  };
5130
5205
  let payload = await requestCompletion("auto");
5131
5206
  let choice = payload.choices?.[0];
@@ -9145,7 +9220,7 @@ export class AiManager {
9145
9220
  if (typeof output.top_p === "number")
9146
9221
  output.top_p = clamp(output.top_p, 0, 1);
9147
9222
  output.max_tokens = typeof output.max_tokens === "number"
9148
- ? Math.round(clamp(output.max_tokens, 1, 32_768))
9223
+ ? Math.round(clamp(output.max_tokens, 1, MAX_MODEL_OUTPUT_TOKENS))
9149
9224
  : DEFAULT_MAX_TOKENS;
9150
9225
  return output;
9151
9226
  }