Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: PR CI

on:
pull_request:
branches:
- main
workflow_dispatch:

concurrency:
group: pr-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"

- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run stable unit test suite
env:
PYTHONPATH: ${{ github.workspace }}
run: |
python3 -m unittest \
tests.test_ai_filter_fallback \
tests.test_anthropic_response_parsing \
tests.test_generate_content_guards \
tests.test_generator_guard_repair \
tests.test_markdown_normalizer \
tests.test_runtime_profiles \
tests.test_search_fallback \
tests.test_tag_graph_runtime
9 changes: 5 additions & 4 deletions blog/themes/terminal-theme/layouts/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<body class="bg-deep-navy selection:bg-muted-teal selection:text-deep-navy font-display">
{{ $posts := where .Site.RegularPages "Type" "posts" }}
{{ $posts = sort $posts "Date" "desc" }}
{{ $latestPost := cond (gt (len $posts) 0) (index $posts 0) nil }}

<div class="rain-container">
<div class="rain-streak r1"></div>
Expand Down Expand Up @@ -117,11 +118,11 @@ <h2 class="text-off-white text-base font-bold leading-tight tracking-wide upperc
<span class="font-medium">12ms</span>
</div>
<div class="flex flex-col border-l border-muted-teal/10 pl-8">
<span class="text-muted-teal/50 text-[10px] tracking-normal">最近同步</span>
<span class="text-muted-teal/50 text-[10px] tracking-normal">最新内容</span>
<span class="font-medium text-off-white">
{{ with (first 1 $posts) }}{{ (index . 0).Date.Format "2006-01-02 15:04" }}{{ else }}N/A{{
end }}
{{ with $latestPost }}{{ .Date.Format "2006-01-02 15:04" }}{{ else }}N/A{{ end }}
</span>
<span class="text-[9px] text-off-white/35 tracking-normal">非站点部署时间</span>
</div>
<div class="flex flex-col border-l border-muted-teal/10 pl-8">
<span class="text-muted-teal/50 text-[10px] tracking-normal">当前时间</span>
Expand Down Expand Up @@ -391,4 +392,4 @@ <h4 class="text-off-white text-xl font-light tracking-tight font-display">
</div>
</body>

</html>
</html>
9 changes: 7 additions & 2 deletions processor/anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ def _purpose_policy(self, purpose: str, configured_max_retries: int) -> Dict[str
"allow_structural_fallback": purpose in {
self.PURPOSE_GENERATION,
self.PURPOSE_CLASSIFICATION,
self.PURPOSE_METADATA,
self.PURPOSE_TAG_INTRO,
},
"api_retries": min(max(0, configured_max_retries), 1)
if purpose == self.PURPOSE_GENERATION
Expand All @@ -183,6 +185,8 @@ def _purpose_policy(self, purpose: str, configured_max_retries: int) -> Dict[str
"allow_structural_fallback": purpose in {
self.PURPOSE_GENERATION,
self.PURPOSE_CLASSIFICATION,
self.PURPOSE_METADATA,
self.PURPOSE_TAG_INTRO,
},
"api_retries": max(0, configured_max_retries)
if purpose == self.PURPOSE_GENERATION
Expand Down Expand Up @@ -217,8 +221,9 @@ def _extract_text_from_message(self, message: Any) -> str:

text = ""
if isinstance(block, dict):
if block.get("type") == "text":
text = str(block.get("text") or "")
candidate_text = block.get("text")
if block.get("type") == "text" or (candidate_text and not block.get("type")):
text = str(candidate_text or "")
elif getattr(block, "type", None) == "text":
text = str(getattr(block, "text", "") or "")
elif hasattr(block, "text"):
Expand Down
57 changes: 55 additions & 2 deletions scripts/generate_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,56 @@ def _looks_like_meta_disclaimer(self, text: str) -> bool:
]
return any(w in t for w in banned)

def _drop_guard_failed_sections(self, item: dict, sections: list[str]) -> list[str]:
dropped: list[str] = []
for section in sections:
name = str(section or "").strip()
if not name:
continue
if name in item:
item.pop(name, None)
dropped.append(name)
if dropped:
item["guard_dropped_sections"] = dropped
return dropped

def _has_publishable_body(self, item: dict) -> bool:
text_fields = [
"summary",
"description_translated",
"description",
"deepwiki_content",
"comprehensive_analysis",
"analysis",
"best_practices",
"comparison_analysis",
"performance_tips",
"practical_recommendations",
"learning_path",
]
for field in text_fields:
value = str(item.get(field) or "").strip()
if not value:
continue
if self._looks_like_meta_disclaimer(value):
continue
return True

list_fields = [
"code_examples",
"case_studies",
"learning_takeaways",
"faq",
"challenges",
"related_resources",
]
for field in list_fields:
value = item.get(field)
if isinstance(value, list) and len(value) > 0:
return True

return False

def _should_skip_post(self, item: dict) -> bool:
if not isinstance(item, dict):
return True
Expand Down Expand Up @@ -700,8 +750,11 @@ def _should_skip_post(self, item: dict) -> bool:
guard_failed_sections.append(k)
if guard_failed_sections:
item["guard_failed_sections"] = guard_failed_sections
item["guard_failure_reason"] = f"guard_failed: {', '.join(guard_failed_sections)}"
return True
dropped_sections = self._drop_guard_failed_sections(item, guard_failed_sections)
if not self._has_publishable_body(item):
item["guard_failure_reason"] = f"guard_failed: {', '.join(guard_failed_sections)}"
return True
item["guard_failure_reason"] = f"guard_dropped: {', '.join(dropped_sections or guard_failed_sections)}"
return False

def _generate_slug(self, title: str, index: int) -> str:
Expand Down
31 changes: 31 additions & 0 deletions tests/test_anthropic_response_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ def test_extract_text_joins_multiple_text_blocks(self):

self.assertEqual(text, "part 1\n\npart 2")

def test_extract_text_accepts_dict_block_without_type_when_text_present(self):
client = self.anthropic_client_module.AnthropicClient.__new__(self.anthropic_client_module.AnthropicClient)
message = _Message([{"text": "compat answer"}])

text = client._extract_text_from_message(message)

self.assertEqual(text, "compat answer")

def test_default_model_switches_for_minimax(self):
client = self.anthropic_client_module.AnthropicClient.__new__(self.anthropic_client_module.AnthropicClient)
client.config = {"base_url": "https://api.minimaxi.com/anthropic"}
Expand Down Expand Up @@ -161,6 +169,29 @@ def test_create_message_retries_thinking_only_minimax_response_once(self):
self.assertEqual(client.client.messages.calls[1]["thinking"], {"type": "disabled"})
self.assertEqual(client.client.messages.calls[1]["max_tokens"], 2048)

def test_metadata_purpose_retries_thinking_only_minimax_response_once(self):
client = self.anthropic_client_module.AnthropicClient.__new__(self.anthropic_client_module.AnthropicClient)
client.config = {
"base_url": "https://api.minimaxi.com/anthropic",
"max_tokens": 8192,
"llm_max_retries": 0,
"temperature": 0.3,
"min_fallback_max_tokens": 2048,
}
client._semaphore = contextlib.nullcontext()
client.client = _FakeClient(
[
_Message([_ThinkingBlock()], stop_reason="max_tokens"),
_Message([_TextBlock('{"tags":["AI"]}')], stop_reason="end_turn"),
]
)

text = client.create_message("prompt", max_tokens=200, temperature=0.1, purpose="metadata")

self.assertEqual(text, '{"tags":["AI"]}')
self.assertEqual(len(client.client.messages.calls), 2)
self.assertEqual(client.client.messages.calls[1]["max_tokens"], 2048)

def test_create_message_retries_text_response_stopped_by_max_tokens_once(self):
client = self.anthropic_client_module.AnthropicClient.__new__(self.anthropic_client_module.AnthropicClient)
client.config = {
Expand Down
33 changes: 33 additions & 0 deletions tests/test_generate_content_guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,39 @@ def test_raise_for_fatal_post_generation_state_on_guard_failure(self):
},
)

def test_should_not_skip_post_when_guard_failed_sections_are_dropped(self):
generator = self.module.SuperEnhancedContentGenerator.__new__(self.module.SuperEnhancedContentGenerator)
item = {
"title": "repo-a",
"summary": "这是一段正常摘要,仍然足够支撑文章发布。",
"engaging_intro": "由于您提供的标题有限,我将基于常见技术写法生成内容。",
"deep_comment": "由于您提供的内容有限,我只能给出泛化评价。",
"guard_failed_sections": ["engaging_intro", "deep_comment"],
}

should_skip = generator._should_skip_post(item)

self.assertFalse(should_skip)
self.assertEqual(item["guard_dropped_sections"], ["engaging_intro", "deep_comment"])
self.assertNotIn("engaging_intro", item)
self.assertNotIn("deep_comment", item)
self.assertEqual(item["guard_failure_reason"], "guard_dropped: engaging_intro, deep_comment")

def test_should_skip_post_when_guard_failed_sections_leave_no_publishable_body(self):
generator = self.module.SuperEnhancedContentGenerator.__new__(self.module.SuperEnhancedContentGenerator)
item = {
"title": "repo-a",
"engaging_intro": "由于您提供的标题有限,我将基于常见技术写法生成内容。",
"deep_comment": "由于您提供的内容有限,我只能给出泛化评价。",
"guard_failed_sections": ["engaging_intro", "deep_comment"],
}

should_skip = generator._should_skip_post(item)

self.assertTrue(should_skip)
self.assertEqual(item["guard_dropped_sections"], ["engaging_intro", "deep_comment"])
self.assertEqual(item["guard_failure_reason"], "guard_failed: engaging_intro, deep_comment")


if __name__ == "__main__":
unittest.main()
Loading