mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix: split level-one headings without recursion (#2176)
* fix: split level-one headings without recursion * fix: avoid duplicate content after chunk flush
This commit is contained in:
@@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- [文档] FAQ 补充 macOS 桌面应用被 Gatekeeper quarantine 阻止启动时的受信任安装包临时放行步骤(refs #2113)。
|
||||
- [新功能] LLM 渠道新增显式 Chat Completions / Responses API Surface,支持 Anspire GPT-5.6 系列等 Responses-only 模型,并统一连接测试、主分析、筛选、图片识别与状态诊断路由;所有运行路径先按同一规则解析协议再校验 Surface,混合 Surface 的同名路由按未知能力保守处理;显式 Anspire 渠道独占共享 Key,非法 Surface 或协议不匹配时不会把该 Key 回退为旧版 Chat 部署,同时保留无关的 Gemini/OpenAI 等 legacy provider;本地 loopback 渠道可在图片识别路径继续无 Key 调用,远端渠道仍要求凭据;禁用渠道不会因残留 Surface 配置阻断其他兼容 fallback;Web 编辑器不会静默改写非法历史值,并允许将 Hermes 非法 Surface 修复为 Chat Completions。
|
||||
- [修复] 将 Responses 渠道的协议、模型 provider、公开 route alias 与 wire-model 构造收敛为统一路由契约,保存校验、运行时加载、状态诊断、选股入口和 Web 编辑器共同使用当前安装的 LiteLLM provider registry,拒绝 `openai` 协议下显式非 OpenAI provider 的模型、拒绝同一 alias 混用 Chat/Responses,并保留 OpenAI-compatible 网关自有的带斜杠模型 ID。
|
||||
- [修复] 长通知包含一级 Markdown 标题时按对应标题边界正确分片,避免错误递归耗尽长度预算并中断发送。
|
||||
|
||||
## [3.29.0] - 2026-08-02
|
||||
|
||||
|
||||
@@ -947,8 +947,8 @@ def _chunk_by_separators(content: str) -> tuple[list[str], str]:
|
||||
separator = "\n---\n"
|
||||
elif "\n# " in content:
|
||||
# 按 # 分割 (兼容一级标题)
|
||||
parts = content.split("\n## ")
|
||||
sections = [parts[0]] + [f"## {p}" for p in parts[1:]]
|
||||
parts = content.split("\n# ")
|
||||
sections = [parts[0]] + [f"# {p}" for p in parts[1:]]
|
||||
separator = "\n"
|
||||
elif "\n## " in content:
|
||||
# 按 ## 分割 (兼容二级标题)
|
||||
@@ -1059,6 +1059,8 @@ def chunk_content_by_max_words(
|
||||
# 先保存当前积累的内容
|
||||
if current_chunk:
|
||||
chunks.append("".join(current_chunk))
|
||||
current_chunk = []
|
||||
current_word_len = 0
|
||||
|
||||
# 强制截断这个超长 section
|
||||
section_chunks = _chunk(
|
||||
|
||||
@@ -54,6 +54,25 @@ class TestChunkContentByMaxWords(unittest.TestCase):
|
||||
self.assertGreaterEqual(len(result), 2)
|
||||
self.assertEqual("".join(result), text)
|
||||
|
||||
def test_level_one_heading_content_splits_without_recursive_failure(self):
|
||||
part_a = "A" * 40
|
||||
part_b = "B" * 40
|
||||
text = f"{part_a}\n# Section\n{part_b}"
|
||||
|
||||
result = chunk_content_by_max_words(text, 60)
|
||||
|
||||
self.assertGreaterEqual(len(result), 2)
|
||||
joined = "".join(result).replace(TRUNCATION_SUFFIX, "")
|
||||
self.assertEqual(joined, text)
|
||||
|
||||
def test_long_level_one_section_does_not_duplicate_buffered_preamble(self):
|
||||
text = "Intro\n# Section\n" + "B" * 2500
|
||||
|
||||
result = chunk_content_by_max_words(text, 2000)
|
||||
|
||||
joined = "".join(result).replace(TRUNCATION_SUFFIX, "")
|
||||
self.assertEqual(joined, text)
|
||||
|
||||
def test_long_content_without_separators_gets_force_split_with_suffix(self):
|
||||
long_text = "X" * 200
|
||||
result = chunk_content_by_max_words(long_text, 50)
|
||||
@@ -134,6 +153,17 @@ class TestChunkContentByMaxBytes(unittest.TestCase):
|
||||
joined = "".join(result).replace(TRUNCATION_SUFFIX, "")
|
||||
self.assertEqual(joined, text)
|
||||
|
||||
def test_level_one_heading_content_splits_without_recursive_failure(self):
|
||||
part_a = "A" * 80
|
||||
part_b = "B" * 80
|
||||
text = f"{part_a}\n# Section\n{part_b}"
|
||||
|
||||
result = chunk_content_by_max_bytes(text, 100)
|
||||
|
||||
self.assertGreaterEqual(len(result), 2)
|
||||
joined = "".join(result).replace(TRUNCATION_SUFFIX, "")
|
||||
self.assertEqual(joined, text)
|
||||
|
||||
def test_multiple_sections_in_one_chunk_no_double_separator(self):
|
||||
# When multiple sections fit in one chunk, they must be concatenated without
|
||||
# inserting an extra separator (sections already have separator appended).
|
||||
|
||||
Reference in New Issue
Block a user