myagent-ai 1.13.3 → 1.13.5
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.
|
Binary file
|
package/memory/manager.py
CHANGED
|
@@ -633,6 +633,62 @@ class MemoryManager:
|
|
|
633
633
|
# 记忆搜索
|
|
634
634
|
# ==========================================================================
|
|
635
635
|
|
|
636
|
+
# ==========================================================================
|
|
637
|
+
# 时间衰减权重 (遗忘曲线)
|
|
638
|
+
# ==========================================================================
|
|
639
|
+
|
|
640
|
+
@staticmethod
|
|
641
|
+
def _compute_time_weight(created_at: str, half_life_days: float = 30.0) -> float:
|
|
642
|
+
"""
|
|
643
|
+
计算记忆的时间衰减权重(模拟遗忘曲线)。
|
|
644
|
+
|
|
645
|
+
使用指数衰减模型: weight = e^(-ln(2) * age_days / half_life_days)
|
|
646
|
+
- 刚创建的记忆权重为 1.0
|
|
647
|
+
- 经过 half_life_days 后权重降至 0.5
|
|
648
|
+
- 经过 2 * half_life_days 后权重降至 0.25
|
|
649
|
+
- 权重永远不会降到 0(长期记忆仍可被召回)
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
created_at: 记忆创建时间(ISO 8601 格式,如 "2025-01-15T08:30:00+00:00")
|
|
653
|
+
half_life_days: 半衰期天数(默认 30 天)
|
|
654
|
+
- 7 天: 适合短期会话记忆,快速遗忘
|
|
655
|
+
- 30 天: 默认值,平衡近期与长期记忆
|
|
656
|
+
- 90 天: 适合知识型场景,长期保留
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
时间权重值 (0.0 ~ 1.0)
|
|
660
|
+
|
|
661
|
+
Examples:
|
|
662
|
+
>>> _compute_time_weight("2025-04-12T00:00:00+00:00", half_life_days=30)
|
|
663
|
+
1.0 # 今天的记忆
|
|
664
|
+
>>> _compute_time_weight("2025-03-13T00:00:00+00:00", half_life_days=30)
|
|
665
|
+
0.5 # 30天前的记忆
|
|
666
|
+
>>> _compute_time_weight("2025-02-11T00:00:00+00:00", half_life_days=30)
|
|
667
|
+
0.25 # 60天前的记忆
|
|
668
|
+
"""
|
|
669
|
+
if not created_at:
|
|
670
|
+
return 0.5 # 无时间信息的记忆给中等权重
|
|
671
|
+
|
|
672
|
+
try:
|
|
673
|
+
# 解析 ISO 8601 时间戳
|
|
674
|
+
created_dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
|
675
|
+
now_dt = datetime.now(created_dt.tzinfo) if created_dt.tzinfo else datetime.now()
|
|
676
|
+
|
|
677
|
+
age_seconds = (now_dt - created_dt).total_seconds()
|
|
678
|
+
if age_seconds <= 0:
|
|
679
|
+
return 1.0 # 未来的时间戳或零延迟
|
|
680
|
+
|
|
681
|
+
age_days = age_seconds / 86400.0
|
|
682
|
+
|
|
683
|
+
# 指数衰减: e^(-ln(2) * age / half_life)
|
|
684
|
+
decay = math.exp(-0.693147 * age_days / half_life_days)
|
|
685
|
+
|
|
686
|
+
# 限制最低权重为 0.05,确保极老的记忆仍有一丝被召回的可能
|
|
687
|
+
return max(decay, 0.05)
|
|
688
|
+
except (ValueError, TypeError, OSError) as e:
|
|
689
|
+
logger.debug(f"时间衰减计算失败 ({created_at}): {e}")
|
|
690
|
+
return 0.5 # 解析失败时给中等权重
|
|
691
|
+
|
|
636
692
|
# ==========================================================================
|
|
637
693
|
# TF-IDF 语义搜索 (无外部依赖)
|
|
638
694
|
# ==========================================================================
|
|
@@ -753,21 +809,31 @@ class MemoryManager:
|
|
|
753
809
|
category: str = "",
|
|
754
810
|
limit: int = 10,
|
|
755
811
|
mode: str = "hybrid",
|
|
812
|
+
time_decay: bool = True,
|
|
813
|
+
half_life_days: float = 30.0,
|
|
756
814
|
) -> List[MemoryEntry]:
|
|
757
815
|
"""
|
|
758
|
-
|
|
816
|
+
搜索记忆(内置遗忘曲线时间衰减)。
|
|
759
817
|
|
|
760
818
|
支持三种搜索模式:
|
|
761
819
|
- "keyword": 传统 LIKE 关键词匹配(快速)
|
|
762
820
|
- "semantic": TF-IDF 语义搜索(理解语义相似性)
|
|
763
821
|
- "hybrid": 混合搜索(默认)= 0.4 * keyword_score + 0.6 * tfidf_score
|
|
764
822
|
|
|
823
|
+
所有模式均支持时间衰减权重(遗忘曲线),越久远的记忆权重越低。
|
|
824
|
+
最终得分 = 相关性得分 × 时间权重。
|
|
825
|
+
|
|
765
826
|
Args:
|
|
766
827
|
query: 搜索查询
|
|
767
828
|
session_id: 会话 ID(空=所有会话)
|
|
768
829
|
category: 记忆类别(空=所有类别)
|
|
769
830
|
limit: 返回数量
|
|
770
831
|
mode: 搜索模式 "keyword" | "semantic" | "hybrid"
|
|
832
|
+
time_decay: 是否启用时间衰减(默认 True)
|
|
833
|
+
half_life_days: 遗忘曲线半衰期天数(默认 30 天)
|
|
834
|
+
- 7 天: 短期记忆场景,快速遗忘
|
|
835
|
+
- 30 天: 默认,平衡近期与长期
|
|
836
|
+
- 90 天: 知识型场景,长期保留
|
|
771
837
|
"""
|
|
772
838
|
conn = self._get_conn()
|
|
773
839
|
conditions = ["1=1"]
|
|
@@ -787,26 +853,38 @@ class MemoryManager:
|
|
|
787
853
|
where = " AND ".join(conditions)
|
|
788
854
|
|
|
789
855
|
if mode == "keyword":
|
|
790
|
-
return self._search_keyword(conn, query, where, params, limit
|
|
856
|
+
return self._search_keyword(conn, query, where, params, limit,
|
|
857
|
+
time_decay=time_decay, half_life_days=half_life_days)
|
|
791
858
|
elif mode == "semantic":
|
|
792
|
-
return self._search_semantic(conn, query, where, params, limit
|
|
859
|
+
return self._search_semantic(conn, query, where, params, limit,
|
|
860
|
+
time_decay=time_decay, half_life_days=half_life_days)
|
|
793
861
|
else:
|
|
794
|
-
# 混合模式:取两种搜索结果的加权和
|
|
795
|
-
keyword_results = self._search_keyword(conn, query, where, params, limit * 2
|
|
796
|
-
|
|
862
|
+
# 混合模式:取两种搜索结果的加权和 + 时间衰减
|
|
863
|
+
keyword_results = self._search_keyword(conn, query, where, params, limit * 2,
|
|
864
|
+
time_decay=False, half_life_days=half_life_days)
|
|
865
|
+
semantic_results = self._search_semantic(conn, query, where, params, limit * 2,
|
|
866
|
+
time_decay=False, half_life_days=half_life_days)
|
|
797
867
|
|
|
798
|
-
#
|
|
868
|
+
# 合并评分(相关性 + 时间衰减)
|
|
799
869
|
combined: Dict[str, Tuple[MemoryEntry, float]] = {}
|
|
800
870
|
for i, entry in enumerate(keyword_results):
|
|
801
|
-
|
|
802
|
-
|
|
871
|
+
relevance = 1.0 - (i / max(len(keyword_results), 1))
|
|
872
|
+
score = relevance * 0.4
|
|
873
|
+
combined[entry.id] = (entry, score)
|
|
803
874
|
|
|
804
875
|
for i, entry in enumerate(semantic_results):
|
|
805
|
-
|
|
876
|
+
relevance = 1.0 - (i / max(len(semantic_results), 1))
|
|
877
|
+
sem_score = relevance * 0.6
|
|
806
878
|
if entry.id in combined:
|
|
807
|
-
combined[entry.id] = (entry, combined[entry.id][1] +
|
|
879
|
+
combined[entry.id] = (entry, combined[entry.id][1] + sem_score)
|
|
808
880
|
else:
|
|
809
|
-
combined[entry.id] = (entry,
|
|
881
|
+
combined[entry.id] = (entry, sem_score)
|
|
882
|
+
|
|
883
|
+
# 应用时间衰减权重: final_score = relevance_score × time_weight
|
|
884
|
+
if time_decay:
|
|
885
|
+
for mem_id, (entry, relevance_score) in combined.items():
|
|
886
|
+
tw = self._compute_time_weight(entry.created_at, half_life_days)
|
|
887
|
+
combined[mem_id] = (entry, relevance_score * tw)
|
|
810
888
|
|
|
811
889
|
# 按综合得分排序
|
|
812
890
|
sorted_results = sorted(
|
|
@@ -832,28 +910,45 @@ class MemoryManager:
|
|
|
832
910
|
where: str,
|
|
833
911
|
params: list,
|
|
834
912
|
limit: int,
|
|
913
|
+
time_decay: bool = False,
|
|
914
|
+
half_life_days: float = 30.0,
|
|
835
915
|
) -> List[MemoryEntry]:
|
|
836
|
-
"""关键词 LIKE
|
|
916
|
+
"""关键词 LIKE 搜索(支持时间衰减重排序)"""
|
|
837
917
|
like_pattern = f"%{query}%"
|
|
838
918
|
conditions = f"{where} AND (content LIKE ? OR summary LIKE ? OR key LIKE ?)"
|
|
839
919
|
search_params = params + [like_pattern, like_pattern, like_pattern]
|
|
840
920
|
|
|
921
|
+
# 取更多候选(后续按时间衰减重排序)
|
|
922
|
+
fetch_limit = limit * 3 if time_decay else limit
|
|
841
923
|
sql = f"""
|
|
842
924
|
SELECT * FROM memories WHERE {conditions}
|
|
843
925
|
ORDER BY importance DESC, access_count DESC
|
|
844
926
|
LIMIT ?
|
|
845
927
|
"""
|
|
846
|
-
search_params.append(
|
|
928
|
+
search_params.append(fetch_limit)
|
|
847
929
|
rows = conn.execute(sql, search_params).fetchall()
|
|
848
930
|
|
|
849
|
-
for row in rows
|
|
931
|
+
entries = [MemoryEntry.from_row(row) for row in rows]
|
|
932
|
+
|
|
933
|
+
# 应用时间衰减重排序
|
|
934
|
+
if time_decay and entries:
|
|
935
|
+
scored = []
|
|
936
|
+
for entry in entries:
|
|
937
|
+
base_score = entry.importance + entry.access_count * 0.01
|
|
938
|
+
tw = self._compute_time_weight(entry.created_at, half_life_days)
|
|
939
|
+
scored.append((entry, base_score * tw))
|
|
940
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
941
|
+
entries = [e for e, _ in scored[:limit]]
|
|
942
|
+
|
|
943
|
+
# 更新访问计数
|
|
944
|
+
for entry in entries:
|
|
850
945
|
conn.execute(
|
|
851
946
|
"UPDATE memories SET access_count = access_count + 1 WHERE id = ?",
|
|
852
|
-
(
|
|
947
|
+
(entry.id,),
|
|
853
948
|
)
|
|
854
949
|
conn.commit()
|
|
855
950
|
|
|
856
|
-
return
|
|
951
|
+
return entries
|
|
857
952
|
|
|
858
953
|
def _search_semantic(
|
|
859
954
|
self,
|
|
@@ -862,8 +957,10 @@ class MemoryManager:
|
|
|
862
957
|
where: str,
|
|
863
958
|
params: list,
|
|
864
959
|
limit: int,
|
|
960
|
+
time_decay: bool = False,
|
|
961
|
+
half_life_days: float = 30.0,
|
|
865
962
|
) -> List[MemoryEntry]:
|
|
866
|
-
"""TF-IDF
|
|
963
|
+
"""TF-IDF 语义搜索(支持时间衰减重排序)"""
|
|
867
964
|
# 先取一批候选文档
|
|
868
965
|
candidate_sql = f"SELECT * FROM memories WHERE {where} ORDER BY created_at DESC LIMIT 200"
|
|
869
966
|
rows = conn.execute(candidate_sql, params).fetchall()
|
|
@@ -883,6 +980,14 @@ class MemoryManager:
|
|
|
883
980
|
# 计算 TF-IDF 得分
|
|
884
981
|
scores = self._compute_tfidf(query, documents)
|
|
885
982
|
|
|
983
|
+
# 应用时间衰减重排序
|
|
984
|
+
if time_decay:
|
|
985
|
+
for doc_id, tfidf_score in scores.items():
|
|
986
|
+
row = row_map.get(doc_id)
|
|
987
|
+
if row:
|
|
988
|
+
tw = self._compute_time_weight(row["created_at"], half_life_days)
|
|
989
|
+
scores[doc_id] = tfidf_score * tw
|
|
990
|
+
|
|
886
991
|
# 按得分排序,取 top N
|
|
887
992
|
top_ids = list(scores.keys())[:limit]
|
|
888
993
|
result = [MemoryEntry.from_row(row_map[doc_id]) for doc_id in top_ids if doc_id in row_map]
|
|
@@ -903,9 +1008,12 @@ class MemoryManager:
|
|
|
903
1008
|
category: str = "",
|
|
904
1009
|
limit: int = 20,
|
|
905
1010
|
mode: str = "hybrid",
|
|
1011
|
+
time_decay: bool = True,
|
|
1012
|
+
half_life_days: float = 30.0,
|
|
906
1013
|
) -> List[MemoryEntry]:
|
|
907
1014
|
"""跨会话搜索"""
|
|
908
|
-
return self.search(query, session_id="", category=category, limit=limit, mode=mode
|
|
1015
|
+
return self.search(query, session_id="", category=category, limit=limit, mode=mode,
|
|
1016
|
+
time_decay=time_decay, half_life_days=half_life_days)
|
|
909
1017
|
|
|
910
1018
|
# ==========================================================================
|
|
911
1019
|
# 记忆总结与维护
|
package/package.json
CHANGED
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -2419,14 +2419,14 @@ function _renderMessagesInner() {
|
|
|
2419
2419
|
partsInner += '<div class="timeline-segment">' + renderMarkdown(msg._streamingText) + _cursor + '</div>';
|
|
2420
2420
|
}
|
|
2421
2421
|
if (partsInner) {
|
|
2422
|
-
// All parts (text segments + tool calls) wrapped in ONE message-bubble
|
|
2423
|
-
timelineHtml = '<div class="message-bubble"><div class="msg-timeline">' + partsInner + '</div></div>';
|
|
2422
|
+
// All parts (text segments + tool calls) wrapped in ONE message-bubble (full width during streaming)
|
|
2423
|
+
timelineHtml = '<div class="message-bubble msg-bubble-wrapper"><div class="msg-timeline">' + partsInner + '</div></div>';
|
|
2424
2424
|
}
|
|
2425
2425
|
}
|
|
2426
2426
|
|
|
2427
|
-
// Backward compat: single bubble for messages without parts
|
|
2427
|
+
// Backward compat: single bubble for messages without parts (full width)
|
|
2428
2428
|
const singleBubbleHtml = (!hasParts && !hasStreamingText)
|
|
2429
|
-
? (content ? `<div class="message-bubble">${content}${ttsIndicator}</div>` : '')
|
|
2429
|
+
? (content ? `<div class="message-bubble msg-bubble-wrapper">${content}${ttsIndicator}</div>` : '')
|
|
2430
2430
|
: '';
|
|
2431
2431
|
|
|
2432
2432
|
// Exec events panel: only for backward compat (messages without parts loaded from DB)
|
|
@@ -3459,6 +3459,9 @@ const ttsManager = {
|
|
|
3459
3459
|
_audioPlaying: false, // 队列是否正在播放
|
|
3460
3460
|
_stopRequested: false, // 是否已请求停止
|
|
3461
3461
|
_streamMsgIndex: -1, // 流式模式对应的消息索引
|
|
3462
|
+
_pendingFetches: 0, // 正在进行的 TTS 请求计数(防竞态)
|
|
3463
|
+
_flushed: false, // streamFlush 已调用(防竞态停止)
|
|
3464
|
+
_firstChunkSent: false, // 是否已发送第一个音频块(降低首句延迟)
|
|
3462
3465
|
|
|
3463
3466
|
init() {
|
|
3464
3467
|
// Load TTS enabled state from localStorage
|
|
@@ -3545,6 +3548,9 @@ const ttsManager = {
|
|
|
3545
3548
|
this._audioQueue = [];
|
|
3546
3549
|
this._audioPlaying = false;
|
|
3547
3550
|
this._stopRequested = false;
|
|
3551
|
+
this._pendingFetches = 0;
|
|
3552
|
+
this._flushed = false;
|
|
3553
|
+
this._firstChunkSent = false;
|
|
3548
3554
|
// 保持 _streamActive = true 和 isPlaying = true
|
|
3549
3555
|
// 这样后续的 text_delta 会继续往新的缓冲区添加文本
|
|
3550
3556
|
// 新的句子会被合成并入队播放
|
|
@@ -3574,6 +3580,9 @@ const ttsManager = {
|
|
|
3574
3580
|
this._streamMsgIndex = msgIndex;
|
|
3575
3581
|
this.currentMsgIndex = msgIndex;
|
|
3576
3582
|
this.isPlaying = true;
|
|
3583
|
+
this._pendingFetches = 0;
|
|
3584
|
+
this._flushed = false;
|
|
3585
|
+
this._firstChunkSent = false;
|
|
3577
3586
|
},
|
|
3578
3587
|
|
|
3579
3588
|
/**
|
|
@@ -3588,6 +3597,27 @@ const ttsManager = {
|
|
|
3588
3597
|
|
|
3589
3598
|
this._streamBuffer += delta;
|
|
3590
3599
|
|
|
3600
|
+
// 首句快速通道:第一个音频块降低延迟,缓冲区达到 25 字即触发
|
|
3601
|
+
if (!this._firstChunkSent && this._streamBuffer.length >= 25) {
|
|
3602
|
+
var lastSep = -1;
|
|
3603
|
+
for (var k = 0; k < this._streamBuffer.length; k++) {
|
|
3604
|
+
var c = this._streamBuffer[k];
|
|
3605
|
+
if (c === ',' || c === ',' || c === '。' || c === '!' || c === '?' || c === ';' || c === ' ' || c === '\n') {
|
|
3606
|
+
lastSep = k;
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
if (lastSep > 0) {
|
|
3610
|
+
var firstChunk = this._streamBuffer.substring(0, lastSep + 1).trim();
|
|
3611
|
+
this._streamBuffer = this._streamBuffer.substring(lastSep + 1);
|
|
3612
|
+
var cleanFirst = this._cleanForStreamTTS(firstChunk);
|
|
3613
|
+
if (cleanFirst) {
|
|
3614
|
+
this._firstChunkSent = true;
|
|
3615
|
+
this._enqueueTTS(cleanFirst);
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
return;
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3591
3621
|
// 检测句子边界:中文句号/感叹号/问号,英文句号+空格,或换行
|
|
3592
3622
|
var boundaryPattern = /[。!?]|\.(?:\s|$)|\n/;
|
|
3593
3623
|
var boundaryIdx = -1;
|
|
@@ -3643,8 +3673,15 @@ const ttsManager = {
|
|
|
3643
3673
|
this._enqueueTTS(cleanText);
|
|
3644
3674
|
}
|
|
3645
3675
|
}
|
|
3646
|
-
//
|
|
3647
|
-
this.
|
|
3676
|
+
// 标记已刷新,但不立即停止 —— 等队列和 pending fetch 全部完成后再停止
|
|
3677
|
+
this._flushed = true;
|
|
3678
|
+
// 如果没有 pending fetch 且队列为空,立即停止
|
|
3679
|
+
if (this._pendingFetches === 0 && this._audioQueue.length === 0 && !this._audioPlaying) {
|
|
3680
|
+
this.isPlaying = false;
|
|
3681
|
+
this._streamActive = false;
|
|
3682
|
+
this.currentMsgIndex = -1;
|
|
3683
|
+
this.updatePlayingIndicator();
|
|
3684
|
+
}
|
|
3648
3685
|
},
|
|
3649
3686
|
|
|
3650
3687
|
/**
|
|
@@ -3683,6 +3720,7 @@ const ttsManager = {
|
|
|
3683
3720
|
_enqueueTTS(text) {
|
|
3684
3721
|
if (this._stopRequested) return;
|
|
3685
3722
|
var self = this;
|
|
3723
|
+
self._pendingFetches++;
|
|
3686
3724
|
|
|
3687
3725
|
(async function() {
|
|
3688
3726
|
try {
|
|
@@ -3719,6 +3757,15 @@ const ttsManager = {
|
|
|
3719
3757
|
}
|
|
3720
3758
|
} catch (e) {
|
|
3721
3759
|
console.error('TTS stream chunk error:', e);
|
|
3760
|
+
} finally {
|
|
3761
|
+
self._pendingFetches--;
|
|
3762
|
+
// 如果已 flush 且没有更多 pending,检查是否该停止
|
|
3763
|
+
if (self._flushed && self._pendingFetches === 0 && self._audioQueue.length === 0 && !self._audioPlaying) {
|
|
3764
|
+
self.isPlaying = false;
|
|
3765
|
+
self._streamActive = false;
|
|
3766
|
+
self.currentMsgIndex = -1;
|
|
3767
|
+
self.updatePlayingIndicator();
|
|
3768
|
+
}
|
|
3722
3769
|
}
|
|
3723
3770
|
})();
|
|
3724
3771
|
},
|
|
@@ -3736,15 +3783,16 @@ const ttsManager = {
|
|
|
3736
3783
|
}
|
|
3737
3784
|
|
|
3738
3785
|
if (this._audioQueue.length === 0) {
|
|
3739
|
-
// 队列空了,检查流式是否已结束
|
|
3740
|
-
if (
|
|
3741
|
-
//
|
|
3786
|
+
// 队列空了,检查流式是否已结束 AND 没有 pending fetches
|
|
3787
|
+
if (this._flushed && this._pendingFetches === 0) {
|
|
3788
|
+
// 流已结束、无 pending 请求、队列空 → 播放完成
|
|
3742
3789
|
this.isPlaying = false;
|
|
3790
|
+
this._streamActive = false;
|
|
3743
3791
|
this._audioPlaying = false;
|
|
3744
3792
|
this.currentMsgIndex = -1;
|
|
3745
3793
|
this.updatePlayingIndicator();
|
|
3746
3794
|
}
|
|
3747
|
-
//
|
|
3795
|
+
// 如果流还在继续或还有 pending fetches,等待新的音频入队
|
|
3748
3796
|
return;
|
|
3749
3797
|
}
|
|
3750
3798
|
|
|
@@ -472,59 +472,7 @@ function updateStreamingMessage(msgIdx) {
|
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
// V2 Reasoning block
|
|
476
|
-
if (msg._v2Reasoning) {
|
|
477
|
-
let v2ReasoningBlock = null;
|
|
478
|
-
const allThoughts2 = contentArea.querySelectorAll('.thought-block');
|
|
479
|
-
for (const tb of allThoughts2) {
|
|
480
|
-
const label = tb.querySelector('.thought-label');
|
|
481
|
-
if (label && label.textContent.includes('V2 推理')) {
|
|
482
|
-
v2ReasoningBlock = tb;
|
|
483
|
-
break;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
if (!v2ReasoningBlock && msg.thought) {
|
|
487
|
-
// Skip if V1 thought already exists - don't duplicate
|
|
488
|
-
} else {
|
|
489
|
-
const v2Len = msg._v2Reasoning.length;
|
|
490
|
-
const v2WordCount = msg.streaming
|
|
491
|
-
? '<span class="thought-word-count">' + v2Len + ' 字</span>'
|
|
492
|
-
: '';
|
|
493
|
-
if (v2ReasoningBlock) {
|
|
494
|
-
// Incremental update for V2 reasoning block
|
|
495
|
-
const label = v2ReasoningBlock.querySelector('.thought-label');
|
|
496
|
-
if (label) label.innerHTML = 'V2 推理过程' + v2WordCount;
|
|
497
|
-
const badge = v2ReasoningBlock.querySelector('.thought-badge');
|
|
498
|
-
if (badge) badge.textContent = msg.streaming ? '推理中...' : '已完成';
|
|
499
|
-
const thoughtContent = v2ReasoningBlock.querySelector('.thought-content');
|
|
500
|
-
if (thoughtContent && msg.streaming) {
|
|
501
|
-
const prevLen = v2ReasoningBlock._lastV2Len || 0;
|
|
502
|
-
if (msg._v2Reasoning.length > prevLen) {
|
|
503
|
-
const newText = msg._v2Reasoning.substring(prevLen);
|
|
504
|
-
thoughtContent.insertAdjacentHTML('beforeend', renderMarkdown(newText));
|
|
505
|
-
v2ReasoningBlock._lastV2Len = msg._v2Reasoning.length;
|
|
506
|
-
// 自动滚动 V2 推理框内部内容到底部
|
|
507
|
-
thoughtContent.scrollTop = thoughtContent.scrollHeight;
|
|
508
|
-
}
|
|
509
|
-
} else if (thoughtContent && !msg.streaming) {
|
|
510
|
-
thoughtContent.innerHTML = renderMarkdown(msg._v2Reasoning);
|
|
511
|
-
v2ReasoningBlock._lastV2Len = msg._v2Reasoning.length;
|
|
512
|
-
}
|
|
513
|
-
} else if (!msg.thought) {
|
|
514
|
-
const v2Html = `<details class="thought-block ${msg.streaming ? 'streaming' : ''}" ${msg.streaming ? 'open' : ''}>
|
|
515
|
-
<summary>
|
|
516
|
-
<span class="thought-icon">🧠</span>
|
|
517
|
-
<span class="thought-label">V2 推理过程${v2WordCount}</span>
|
|
518
|
-
${msg.streaming ? '<span class="thought-badge">推理中...</span>' : '<span class="thought-badge">已完成</span>'}
|
|
519
|
-
</summary>
|
|
520
|
-
<div class="thought-content">${renderMarkdown(msg._v2Reasoning)}</div>
|
|
521
|
-
</details>`;
|
|
522
|
-
contentArea.insertAdjacentHTML('afterbegin', v2Html);
|
|
523
|
-
const newBlock = contentArea.querySelector(':scope > .thought-block');
|
|
524
|
-
if (newBlock) newBlock._lastV2Len = msg._v2Reasoning.length;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
475
|
+
// V2 Reasoning: content is already rendered in msg.parts timeline, skip separate block to avoid duplication
|
|
528
476
|
|
|
529
477
|
// Update content - timeline (interleaved text + exec events) or single bubble (backward compat)
|
|
530
478
|
const hasParts = Array.isArray(msg.parts);
|