重构: 巨函数拆分 + 清理历史标记注释 + custom_prompt/停止按钮/tunnel 改进
This commit is contained in:
+20
-20
@@ -7,7 +7,7 @@ import type { AiChatEvent, AiConversationDetail, AiConversationSummary, AiProvid
|
||||
export const aiApi = {
|
||||
/**
|
||||
* 发送消息(非阻塞,通过 ai-chat-event 流式返回);skill 传技能名则注入其 SKILL.md。
|
||||
* F-260614-05 Phase 2c: parts 透传给后端 ai_chat_send → user_parts → provider vision 端点。
|
||||
* parts 透传给后端 ai_chat_send → user_parts → provider vision 端点。
|
||||
* parts 为空/undefined → 后端走 user() 纯文本(向后兼容)。
|
||||
* Input Augmentation: mentionSpans 透传给后端 ai_chat_send 的 mention_spans 参数
|
||||
* (后端 resolve_and_inject 按 kind 投影成 Augmentation 注入 system prompt)。
|
||||
@@ -20,14 +20,14 @@ export const aiApi = {
|
||||
skill: skill || null,
|
||||
modelOverride: modelOverride || null,
|
||||
parts: parts && parts.length > 0 ? parts : null,
|
||||
// F-260616-09 B 批4(决策 e):传 conv_id,操作指定 conv 的 per_conv(不依赖 active 单值)。
|
||||
// 传 conv_id,操作指定 conv 的 per_conv(不依赖 active 单值)。
|
||||
conversationId: conversationId || null,
|
||||
// Input Augmentation: 非空数组才传(undefined/空让 payload 不含该键,后端默认 None)。
|
||||
mentionSpans: mentionSpans && mentionSpans.length > 0 ? mentionSpans : null,
|
||||
})
|
||||
},
|
||||
|
||||
/** 强制发送(B-260616-02: 复位 generating 残留后走 send 同款流程);parts/mentionSpans 透传同 sendMessage。 */
|
||||
/** 强制发送(复位 generating 残留后走 send 同款流程);parts/mentionSpans 透传同 sendMessage。 */
|
||||
forceSend(message: string, language?: string, skill?: string, modelOverride?: string | null, parts?: ContentPart[], conversationId?: string | null, mentionSpans?: MentionSpan[]): Promise<string> {
|
||||
return invoke('ai_chat_force_send', {
|
||||
message,
|
||||
@@ -35,7 +35,7 @@ export const aiApi = {
|
||||
skill: skill || null,
|
||||
modelOverride: modelOverride || null,
|
||||
parts: parts && parts.length > 0 ? parts : null,
|
||||
// F-260616-09 B 批4(决策 e):传 conv_id,强制复位+发送仅作用于目标 conv。
|
||||
// 传 conv_id,强制复位+发送仅作用于目标 conv。
|
||||
conversationId: conversationId || null,
|
||||
// Input Augmentation: 同 sendMessage,非空数组才传(后端 mention_spans 参数)。
|
||||
mentionSpans: mentionSpans && mentionSpans.length > 0 ? mentionSpans : null,
|
||||
@@ -76,7 +76,7 @@ export const aiApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* F-260619-03 Phase B: 路径授权弹窗决策(消费 AiDirAuthRequired 挂起)。
|
||||
* 路径授权弹窗决策(消费 AiDirAuthRequired 挂起)。
|
||||
* decision: 'once'(本次单次,执行后清)/ 'session'(当前会话,切会话清)/ 'always'(始终,持久化)/ 'deny'(拒绝)。
|
||||
* 后端 remove pending → 写授权目录(once/session/persistent)→ 执行工具 → try_continue 恢复 loop。
|
||||
*/
|
||||
@@ -85,7 +85,7 @@ export const aiApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 续跑 agentic 循环(F-260616-03:达 max_iterations 暂停态点继续)。
|
||||
* 续跑 agentic 循环(达 max_iterations 暂停态点继续)。
|
||||
* 后端 ai_continue_loop:复位 stop_flag → try_continue_agent_loop 重新 spawn
|
||||
* run_agentic_loop(iteration 从 0 重计,再跑 max_iterations 轮)。
|
||||
*/
|
||||
@@ -94,7 +94,7 @@ export const aiApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 停止 agentic 循环并走完成流程(F-260616-03:达 max_iterations 暂停态点停止)。
|
||||
* 停止 agentic 循环并走完成流程(达 max_iterations 暂停态点停止)。
|
||||
* 后端 ai_stop_loop:复位 generating + emit AiCompleted(暂停前已 save)。
|
||||
*/
|
||||
stopLoop(conversationId: string): Promise<string> {
|
||||
@@ -102,7 +102,7 @@ export const aiApi = {
|
||||
},
|
||||
|
||||
/** 查询某对话积压的待审批工具(重启后恢复 toolCard pending_approval 态用)。
|
||||
* 阶段4:返 kind 字段(risk/path),前端按 kind 渲染不同决策按钮。 */
|
||||
* 返 kind 字段(risk/path),前端按 kind 渲染不同决策按钮。 */
|
||||
pendingToolCalls(convId: string): Promise<{ tool_call_id: string; conversation_id: string | null; kind: 'risk' | 'path' }[]> {
|
||||
return invoke('ai_pending_tool_calls', { convId })
|
||||
},
|
||||
@@ -114,7 +114,7 @@ export const aiApi = {
|
||||
|
||||
/**
|
||||
* 停止当前生成。
|
||||
* F-260616-09 B 批4(决策 e):传 convId,停止仅作用于目标 conv(不杀其他后台 conv 的 loop)。
|
||||
* 传 convId,停止仅作用于目标 conv(不杀其他后台 conv 的 loop)。
|
||||
* convId 省略时后端 fallback active conv。
|
||||
*/
|
||||
stopChat(convId?: string | null): Promise<void> {
|
||||
@@ -122,14 +122,14 @@ export const aiApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询后端真实 generating 状态(B-260615-22:发送前 IPC 查后端真值)。
|
||||
* F-260616-09 B 批4(决策 e):传 convId 精确查指定 conv;省略时后端 fallback active conv。
|
||||
* 查询后端真实 generating 状态(发送前 IPC 查后端真值)。
|
||||
* 传 convId 精确查指定 conv;省略时后端 fallback active conv。
|
||||
*/
|
||||
isGenerating(convId?: string | null): Promise<boolean> {
|
||||
return invoke('ai_is_generating', { conversationId: convId || null })
|
||||
},
|
||||
|
||||
// ── F-15 阶段2: 手动上下文管理(清空 / 压缩) ──
|
||||
// ── 手动上下文管理(清空 / 压缩) ──
|
||||
// 后端经 ai-chat-event emit 生命周期事件:
|
||||
// ai_context_cleared { conversation_id } — 清空完成
|
||||
// ai_compressing { conversation_id } — 压缩开始(loading)
|
||||
@@ -180,7 +180,7 @@ export const aiApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新 provider 负载均衡池配置(F-260614-04c):仅 UPDATE enabled/weight + 重建 per_provider caps。
|
||||
* 更新 provider 负载均衡池配置:仅 UPDATE enabled/weight + 重建 per_provider caps。
|
||||
* 走轻量分支,不经 ai_save_provider 全量 INSERT OR REPLACE(避免空 api_key 触发 R-PD-1 密钥迁移)。
|
||||
* 后端 weight clamp [0,100];落库后立即 reload_provider_caps,下条消息即按新配置 acquire。
|
||||
*/
|
||||
@@ -194,10 +194,10 @@ export const aiApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 测试连接并拉取厂商模型列表(F-01 阶段5)。
|
||||
* 后端:DB 取 provider → FR-S1 内存解析 api_key → fetch_and_probe
|
||||
* 测试连接并拉取厂商模型列表。
|
||||
* 后端:DB 取 provider → 内存解析 api_key → fetch_and_probe
|
||||
* (网络拉模型名 + 探测 4 维度)→ 写回 model_configs → 返回。
|
||||
* 返回值 ModelConfig 不含 api_key(FR-S1 闭环)。
|
||||
* 返回值 ModelConfig 不含 api_key。
|
||||
* 需已落库的 providerId(新建态先保存再拉取)。
|
||||
*/
|
||||
fetchModels(providerId: string): Promise<ModelConfig[]> {
|
||||
@@ -205,7 +205,7 @@ export const aiApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 单模型探测(F-01 阶段5):纯 CPU 启发式 + 预设表,无网络。
|
||||
* 单模型探测:纯 CPU 启发式 + 预设表,无网络。
|
||||
* 返回填充了 probe_source 的 ModelConfig。用途:手动补模型名后探测能力维度。
|
||||
*/
|
||||
probeModel(modelId: string): Promise<ModelConfig> {
|
||||
@@ -222,7 +222,7 @@ export const aiApi = {
|
||||
return invoke('ai_set_agent_max_iterations', { value })
|
||||
},
|
||||
|
||||
/** 设置流式对话失败自动重试次数(F-260616-07:只重试流前失败,0=不重试,默认3,上限10) */
|
||||
/** 设置流式对话失败自动重试次数(只重试流前失败,0=不重试,默认3,上限10) */
|
||||
setAgentMaxRetries(value: number): Promise<void> {
|
||||
return invoke('ai_set_agent_max_retries', { value })
|
||||
},
|
||||
@@ -243,12 +243,12 @@ export const aiApi = {
|
||||
return invoke('ai_reload_skills')
|
||||
},
|
||||
|
||||
/** F-260619-03 Phase A: 获取 AI 工具文件访问授权目录列表(含 workspace_root) */
|
||||
/** 获取 AI 工具文件访问授权目录列表(含 workspace_root) */
|
||||
getAllowedDirs(): Promise<string[]> {
|
||||
return invoke<string[]>('ai_get_allowed_dirs')
|
||||
},
|
||||
|
||||
/** F-260619-03 Phase A: 设置 AI 工具授权目录(持久化 + 同步内存白名单),返回规范化后的列表 */
|
||||
/** 设置 AI 工具授权目录(持久化 + 同步内存白名单),返回规范化后的列表 */
|
||||
setAllowedDirs(dirs: string[]): Promise<string[]> {
|
||||
return invoke<string[]>('ai_set_allowed_dirs', { dirs })
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
import { useProjectStore } from '../../stores/project'
|
||||
import { getConvState } from '../../composables/ai/useAiEvents'
|
||||
import { getConvState, textIdle } from '../../composables/ai/useAiEvents'
|
||||
import SkillMention from './SkillMention.vue'
|
||||
import ImageInput from './ImageInput.vue'
|
||||
import MentionPopover from './MentionPopover.vue'
|
||||
@@ -226,26 +226,17 @@ async function onDrop(e: DragEvent): Promise<void> {
|
||||
/** 发送可用:有文本或图片或已选技能(纯技能调用允许空文本) */
|
||||
const canSend = computed(() => inputText.value.trim().length > 0 || pendingImages.value.length > 0 || !!pendingSkill.value)
|
||||
|
||||
// L2 状态机停止按钮三态(批2 1c):从 conv_state 派生,旧 streaming/generating bool 双轨过渡兜底。
|
||||
//
|
||||
// 三态映射(对齐后端 ConvState 语义):
|
||||
// - 'stopping':ConvState=stopping,停止中 → 显"停止中" disabled(防重复点);
|
||||
// - 'stop': ConvState=generating(含审批挂起/压缩派生等活跃态)→ 显停止按钮可点;
|
||||
// - 'retry': ConvState=error,停止失败可重试 → 显重试按钮(再发 stop 信号);
|
||||
// - 'idle': 空闲(或 conv_state 未收到回退旧 bool=false)→ 显发送按钮。
|
||||
//
|
||||
// 兜底:conv_state 未追踪过(getConvState 返回 null,CONV_STATE_ENABLED=off 或老后端)时,
|
||||
// 回退旧 store.state.streaming 判断(generating→显停止,否则 idle)。不强制全替旧 bool,防回归。
|
||||
// 注:旧 streaming 是全局单值(非 per-conv),仅当前会话生成时为 true;F-09 多会话并发下 conv_state
|
||||
// 更精确,但回退路径用 streaming 对单会话场景语义等价。
|
||||
// 按钮状态:streaming=true 且 textIdle=false(最近 300ms 有新 delta) → 'stop'(红);
|
||||
// 否则 → 'idle'(白)。textIdle 是 reactive ref,在 AiTextDelta 中每 delta 复位 false,
|
||||
// 300ms 无新 delta 后 setTimeout 置 true。不与 streaming 绑定——文本显示完后 300ms 按钮白,
|
||||
// 不等 AiCompleted,streaming 持续 true 也不影响。
|
||||
const stopBtnState = computed<'idle' | 'stop' | 'stopping' | 'retry'>(() => {
|
||||
const convId = store.state.activeConversationId
|
||||
const cs = getConvState(convId)
|
||||
if (cs === 'stopping') return 'stopping'
|
||||
if (cs === 'error') return 'retry'
|
||||
if (cs === 'generating' || cs === 'compressed') return 'stop'
|
||||
// cs === null (conv_state 已 idle 收敛删项) 或 cs === 'idle' → 发送态
|
||||
return 'idle'
|
||||
const textActive = store.state.streaming && !textIdle.value
|
||||
return textActive ? 'stop' : 'idle'
|
||||
})
|
||||
|
||||
// ── 技能 `/` 联想 ──
|
||||
|
||||
@@ -38,6 +38,16 @@
|
||||
<option :value="3600000">{{ $t('settings.approvalTimeout60m') }}</option>
|
||||
</select>
|
||||
</SettingRow>
|
||||
<!-- 自定义提示词:注入到 AI 系统 prompt 末尾,用户可自定义 AI 行为偏好 -->
|
||||
<SettingRow ref="rowCustomPrompt" :label="$t('settings.labelCustomPrompt')" :desc="$t('settings.descCustomPrompt')">
|
||||
<textarea
|
||||
v-model="customPrompt"
|
||||
class="custom-prompt-input"
|
||||
:placeholder="$t('settings.placeholderCustomPrompt')"
|
||||
rows="4"
|
||||
@change="onCustomPromptChange"
|
||||
/>
|
||||
</SettingRow>
|
||||
</div>
|
||||
|
||||
<!-- all 二次确认弹层(独立,不与 Settings 壳层共用:壳层按钮写死「删除」不适合「确认接管」) -->
|
||||
@@ -108,9 +118,11 @@ const settings = reactive({
|
||||
// 审批超时(ms):0=不限时,缺省 900000(15min);仅固定合法选项,脏值回退默认
|
||||
approvalTimeout: clampApprovalTimeout(appSettings.get<number>('df-approval-timeout', 900000)),
|
||||
})
|
||||
const customPrompt = ref(appSettings.get<string>('custom_prompt', ''))
|
||||
|
||||
const rowAutoExecute = useTemplateRef<InstanceType<typeof SettingRow>>('rowAutoExecute')
|
||||
const rowLogLevel = useTemplateRef<InstanceType<typeof SettingRow>>('rowLogLevel')
|
||||
const rowCustomPrompt = useTemplateRef<InstanceType<typeof SettingRow>>('rowCustomPrompt')
|
||||
const rowApprovalTimeout = useTemplateRef<InstanceType<typeof SettingRow>>('rowApprovalTimeout')
|
||||
|
||||
// 上一次确认生效的档位:all 二次确认取消时恢复此值(非写死 low),避免 medium 用户误选 all
|
||||
@@ -152,6 +164,11 @@ function markSaved() {
|
||||
}
|
||||
|
||||
// 下一笔发起的审批即生效(aiShared.startApprovalTimer 每次实时读),已在跑的计时器沿用旧值。
|
||||
function onCustomPromptChange() {
|
||||
appSettings.set('custom_prompt', customPrompt.value)
|
||||
rowCustomPrompt.value?.markSaved()
|
||||
}
|
||||
|
||||
function onApprovalTimeoutChange() {
|
||||
appSettings.set('df-approval-timeout', settings.approvalTimeout)
|
||||
rowApprovalTimeout.value?.markSaved()
|
||||
@@ -192,6 +209,23 @@ function onApprovalTimeoutChange() {
|
||||
.confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.btn-danger { background: var(--df-danger); color: #fff; }
|
||||
.btn-danger:hover { filter: brightness(1.1); }
|
||||
.custom-prompt-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
background: var(--df-bg);
|
||||
color: var(--df-text);
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.custom-prompt-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--df-primary);
|
||||
}
|
||||
.confirm-enter-active, .confirm-leave-active { transition: opacity 0.15s; }
|
||||
.confirm-enter-from, .confirm-leave-to { opacity: 0; }
|
||||
</style>
|
||||
|
||||
@@ -45,6 +45,24 @@ let _startPromise: Promise<void> | null = null
|
||||
// 上一次 AiTextDelta 的内容(单窗口内 LLM 重复 delta 防御用,见 handleStreamingEvent)
|
||||
let _lastDelta = ''
|
||||
|
||||
// 文本空闲信号(reactive ref):streaming=true 且 300ms 无新 delta 时=true。
|
||||
// 按钮据此显「发送」(idle)而非「停止」。不动 streaming(streaming 是渲染态,
|
||||
// 控制 MessageList 流式 block 显隐,不能在文本中途翻转,否则闪掉)。
|
||||
export const textIdle = ref(true)
|
||||
let _textIdleTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function resetTextIdleTimer(): void {
|
||||
if (_textIdleTimer) clearTimeout(_textIdleTimer)
|
||||
textIdle.value = false // 有新 delta → 文本活跃
|
||||
_textIdleTimer = setTimeout(() => {
|
||||
textIdle.value = true // 300ms 无新 delta → 文本空闲
|
||||
_textIdleTimer = null
|
||||
}, 300)
|
||||
}
|
||||
function clearTextIdleTimer(): void {
|
||||
if (_textIdleTimer) { clearTimeout(_textIdleTimer); _textIdleTimer = null }
|
||||
textIdle.value = true // 整轮结束/新轮开始 → 默认空闲
|
||||
}
|
||||
|
||||
const appSettings = useAppSettingsStore()
|
||||
|
||||
// B-260616-17: 看门狗不重置的事件集合(审批等待/完成/错误由各自 case 内 clear)。
|
||||
@@ -298,6 +316,8 @@ function handleStreamingEvent(event: AiChatEvent): boolean {
|
||||
}
|
||||
_lastDelta = event.delta
|
||||
state.currentText += event.delta
|
||||
// 重置文本空闲定时器:每次 delta 重置 300ms。无新 delta 到 300ms → textIdle=true → 按钮白。
|
||||
resetTextIdleTimer()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -305,6 +325,10 @@ function handleStreamingEvent(event: AiChatEvent): boolean {
|
||||
// Agent 循环新一轮:保存当前文本到上一条 assistant 消息,新建空 assistant 消息
|
||||
flushCurrentText()
|
||||
state.currentText = ''
|
||||
// 文本已刷新完毕 → 清空闲定时器(按钮白,等下一轮 deltas 来再红)
|
||||
// 不设 streaming=false:streaming 管渲染,多轮间需持续 true 让 MessageList 渲染后续 deltas。
|
||||
// 按钮白由 textIdle 独立控制,无需翻转 streaming。
|
||||
clearTextIdleTimer()
|
||||
state.completedTools = 0 // 新轮重置工具计数器
|
||||
state.messages.push({
|
||||
id: `ai-${nextMsgId()}` as MessageId,
|
||||
@@ -603,6 +627,7 @@ function handleUserMessageEvent(event: AiChatEvent): boolean {
|
||||
function cleanupTerminatedConversation(convId: string, reason: 'AiCompleted' | 'AiError' | 'AiHelpRequired') {
|
||||
clearStreamWatchdog(convId || undefined)
|
||||
clearAllToolSlowTimers() // B-260616-12: 整轮/错误/求助结束清全部工具慢执行计时器与已提示集合
|
||||
clearTextIdleTimer() // 清文本空闲定时器 + 置 textIdle=true(整轮结束不该有活跃信号残留)
|
||||
flushCurrentText()
|
||||
state.currentText = ''
|
||||
setStreaming(false, { convId: convId || null, reason })
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { ContentPart, AiMessage, MentionSpan, MessageId } from '@/api/types
|
||||
import { t } from '@/i18n/i18n-helpers'
|
||||
import { resetStreamWatchdog, clearStreamWatchdog } from './useAiStream'
|
||||
import { setStreaming } from './streamingGuard'
|
||||
import { startListener } from './useAiEvents'
|
||||
import { startListener, flushCurrentText } from './useAiEvents'
|
||||
import { nextMsgId, startApprovalTimer, clearApprovalTimer, clearAllApprovalTimers, resolveAiLang, convStates } from './aiShared'
|
||||
|
||||
/// 待发送队列上限(超过抛错提示用户)
|
||||
@@ -71,6 +71,10 @@ const modelOverride = ref<string | null>(null)
|
||||
* undefined/空数组 → 本地 user 消息 mentionSpans 置 undefined(纯文本气泡零回归)+ 后端不传。
|
||||
*/
|
||||
async function doSend(text: string, skill?: string, force = false, parts?: ContentPart[], spans?: MentionSpan[]) {
|
||||
// 防御:先 flush 残留 currentText(正常情况已是空,flushCurrentText 直接 return;
|
||||
// 异常时序下可防止上一轮文本被 state.currentText='' 误清)
|
||||
flushCurrentText()
|
||||
|
||||
const userMsgId = `user-${nextMsgId()}`
|
||||
state.messages.push({
|
||||
id: userMsgId as MessageId,
|
||||
@@ -291,16 +295,16 @@ export function drainQueue(convId?: string | null) {
|
||||
* 发送消息 — 三级降级(B-260616-02 L2 发送韧性):
|
||||
*
|
||||
* L0 normal: 后端 idle → 直接 doSend()
|
||||
* L1 queued: 后端 busy → 入队等 AiCompleted 续发(≤30s 正常等待)
|
||||
* L1 queued: 后端 busy → 入队等 AiCompleted 续发
|
||||
* L2 force: 排队>30s → 返回特殊标记,由调用方(handleSend)弹 confirm;
|
||||
* 用户确认后重新进 sendMessage(forceMode=true)走 forceSend IPC
|
||||
*
|
||||
* forceMode=true 时跳过入队,直接走 ai_chat_force_send。
|
||||
*
|
||||
* Input Augmentation: spans 随消息透传(L0 直接 doSend / L2 force doSend);
|
||||
* L1 入队时 queue item 当前不挂 spans(队列续发属异步重试,mention 区间在首次发送时
|
||||
* 已与文本对齐,排队等待后用户可能改输入,续发用旧 spans 语义模糊;续发走无 mention 路径,
|
||||
* 与现有 parts 入队续发的设计取舍一致——队列为韧性保内容,非保全部上下文元数据)。
|
||||
* 注:入队条件仅用 backendGenerating(IPC 查后端真实态),不再检查 state.streaming。
|
||||
* streaming 是渲染态(文本是否在屏幕上输出),不能作为「后端能否接收消息」的代理。
|
||||
* 文本显示完但后端在落库时,streaming 仍 true——此时按钮已白(基于 delta 时间戳),
|
||||
* 用户点击发送,若后端还在忙则入队短暂等待(~1s),AiCompleted 后自动续发。
|
||||
*/
|
||||
async function sendMessage(text: string, skill?: string, forceMode = false, parts?: ContentPart[], spans?: MentionSpan[]) {
|
||||
if (!text.trim() && !(parts && parts.length)) return
|
||||
@@ -320,14 +324,12 @@ async function sendMessage(text: string, skill?: string, forceMode = false, part
|
||||
backendGenerating = false
|
||||
}
|
||||
|
||||
// ── L1:后端 busy → 入队 ──
|
||||
if (backendGenerating || state.streaming) {
|
||||
// ── L1:后端 busy → 入队(消息已在本地显示,待后端空闲后自动发送)──
|
||||
// 仅用 backendGenerating,不用 state.streaming。streaming 是渲染态,不是后端忙的代理。
|
||||
if (backendGenerating) {
|
||||
if (state.queue.length >= QUEUE_LIMIT) {
|
||||
throw new Error(t('ai.queueFull', { limit: QUEUE_LIMIT }))
|
||||
}
|
||||
setStreaming(true, { convId: state.activeConversationId, reason: 'L1-enqueue' })
|
||||
// F-260614-05 Phase 2b: 入队项挂 parts(供 drainQueue/续发时本地 user 消息渲染图)
|
||||
// 同时挂 conversationId,供 drainQueue 按会话精准续发(防跨会话串话)
|
||||
state.queue.push({
|
||||
text: text.trim(),
|
||||
skill: skill || undefined,
|
||||
|
||||
@@ -137,6 +137,10 @@ export default {
|
||||
labelShowTokenUsage: 'Show token usage',
|
||||
descShowTokenUsage: 'Show token consumption of each reply in conversations',
|
||||
|
||||
labelCustomPrompt: 'Custom prompt',
|
||||
descCustomPrompt: 'Appended to the AI system prompt to customize AI behavior',
|
||||
placeholderCustomPrompt: 'e.g.: You specialize in Rust development questions...',
|
||||
|
||||
labelStreamingMd: 'Streaming Markdown rendering',
|
||||
descStreamingMd: 'Render code blocks/lists/headings during streaming (unclosed code blocks degrade to plain text to avoid flicker); when off, only plain text is shown during streaming and full formatting applies after generation completes',
|
||||
|
||||
|
||||
@@ -137,6 +137,10 @@ export default {
|
||||
labelShowTokenUsage: '显示 Token 用量',
|
||||
descShowTokenUsage: '在对话中展示每次回复的 token 消耗',
|
||||
|
||||
labelCustomPrompt: '自定义提示词',
|
||||
descCustomPrompt: '追加到 AI 系统提示词末尾,可自定义 AI 行为偏好',
|
||||
placeholderCustomPrompt: '例如:你擅长回答 Rust 开发问题...',
|
||||
|
||||
labelStreamingMd: '流式 Markdown 渲染',
|
||||
descStreamingMd: '流式生成过程中渲染代码块/列表/标题格式(未闭合代码块降级纯文本防闪烁);关闭后流式期间仅显纯文本,生成完成后才渲染完整格式',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user