Explorar o código

feat: 新版本解构api

jihuaqiang hai 1 día
pai
achega
fe35854035
Modificáronse 100 ficheiros con 294 adicións e 30321 borrados
  1. BIN=BIN
      .claude/.DS_Store
  2. 0 111
      .claude/agents/agent-implements-agent.md
  3. 0 143
      .claude/agents/function-implements-agent.md
  4. 0 182
      .claude/agents/tool-implements-agent.md
  5. 0 54
      .claude/commands/coding-technical-implement-based-langgraph.md
  6. 0 81
      .claude/commands/component-arc.md
  7. 0 41
      .claude/commands/dynamic-project-init.md
  8. 0 51
      .claude/commands/prd-technical-implement-based-langgraph.md
  9. 0 47
      .claude/commands/tools-generate.md
  10. 0 37
      .claude/settings.local.json
  11. 114 0
      README.md
  12. BIN=BIN
      arc.png
  13. 0 106
      decode_task/decodeTask.py
  14. 0 100
      decode_task/evaluateTask.py
  15. 0 120
      decode_task/topicTask.py
  16. 0 1003
      decode_workflow_process.md
  17. 0 58
      doc/command
  18. 0 295
      doc/record
  19. 0 33
      doc/脚本.md
  20. 0 35
      doc/脚本点
  21. BIN=BIN
      examples/.DS_Store
  22. 0 7
      examples/demo.json
  23. 0 108
      examples/demo_default.json
  24. BIN=BIN
      examples/html/.DS_Store
  25. 0 1931
      examples/html/visualize/script.js
  26. 0 2871
      examples/html/visualize/style.css
  27. 0 133
      examples/run_batch_script_v2.py
  28. 0 264
      examples/run_decode_script.py
  29. 0 298
      examples/run_evaluate.py
  30. 0 0
      examples/static/visualize/__init__.py
  31. 0 556
      examples/static/visualize/tab1.py
  32. 0 176
      examples/static/visualize/tab2.py
  33. 0 1926
      examples/static/visualize/tab3.py
  34. 0 956
      examples/static/visualize/tab5.py
  35. 0 2
      examples/static/visualize_v2/__init__.py
  36. 0 176
      examples/static/visualize_v2/tab1.py
  37. 0 155
      examples/static/visualize_v2/tab2.py
  38. 0 196
      examples/static/visualize_v2/tab3.py
  39. 0 67
      examples/static/visualize_v2/tab4.py
  40. 0 162
      examples/use_decode_video_model.py
  41. 0 429
      examples/visualize_script_results.py
  42. 0 285
      examples/visualize_script_results_v2.py
  43. 0 210
      loggers/sls.py
  44. 65 259
      main.py
  45. 0 30
      models/decode_record.py
  46. 67 0
      models/decode_task_result.py
  47. 0 40
      models/decode_workflow.py
  48. 0 32
      models/evaluate_record.py
  49. 37 0
      models/task.py
  50. BIN=BIN
      prd1.4.pdf
  51. 11 21
      requirements.txt
  52. BIN=BIN
      src/.DS_Store
  53. 0 0
      src/__init__.py
  54. BIN=BIN
      src/components/.DS_Store
  55. 0 223
      src/components/README.md
  56. 0 99
      src/components/__init__.py
  57. 0 38
      src/components/agents/__init__.py
  58. 0 396
      src/components/agents/base.py
  59. 0 222
      src/components/agents/content_unit_split_agent.py
  60. 0 255
      src/components/agents/content_unit_understand.py
  61. 0 347
      src/components/agents/evaluate_agent.py
  62. 0 903
      src/components/agents/inspiration_points_agent.py
  63. 0 720
      src/components/agents/key_points_agent.py
  64. 0 238
      src/components/agents/purpose_point_agent.py
  65. 0 1101
      src/components/agents/recursive_image_deconstruction_agent.py
  66. 0 691
      src/components/agents/recursive_text_deconstruction_agent.py
  67. 0 1735
      src/components/agents/script_form_extraction_agent.py
  68. 0 164
      src/components/agents/script_keyword_agent.py
  69. 0 254
      src/components/agents/script_section_division_agent.py
  70. 0 1726
      src/components/agents/script_substance_extraction_agent.py
  71. 0 380
      src/components/agents/search_keyword_agent.py
  72. 0 196
      src/components/agents/structure_agent.py
  73. 0 581
      src/components/agents/topic_selection_understanding_agent.py
  74. 0 49
      src/components/functions/__init__.py
  75. 0 394
      src/components/functions/base.py
  76. 0 238
      src/components/functions/json_utils.py
  77. 0 323
      src/components/functions/result_aggregation_function.py
  78. 0 299
      src/components/functions/video_upload_function.py
  79. 0 72
      src/components/tools/__init__.py
  80. 0 381
      src/components/tools/base.py
  81. 0 235
      src/components/tools/knowledge_retrieval_tools.py
  82. 0 160
      src/components/tools/nanobanana_tools.py
  83. 0 156
      src/components/tools/segment_tools.py
  84. 0 315
      src/models/README.md
  85. 0 21
      src/models/__init__.py
  86. 0 129
      src/models/database.py
  87. 0 195
      src/models/decode_video.py
  88. 0 156
      src/models/evaluate_record.py
  89. 0 11
      src/states/__init__.py
  90. 0 57
      src/states/script_state.py
  91. 0 209
      src/states/what_deconstruction_state.py
  92. 0 17
      src/utils/__init__.py
  93. 0 110
      src/utils/json_extractor.py
  94. 0 346
      src/utils/llm_invoker.py
  95. 0 149
      src/utils/logger.py
  96. 0 21
      src/utils/utils.py
  97. 0 10
      src/workflows/__init__.py
  98. 0 1499
      src/workflows/decode_workflow.py
  99. 0 430
      src/workflows/evaluate_workflow.py
  100. 0 283
      src/workflows/script_workflow_v2.py

BIN=BIN
.claude/.DS_Store


+ 0 - 111
.claude/agents/agent-implements-agent.md

@@ -1,111 +0,0 @@
----
-name: agent-implements-agent
-description: 当需要基于 LangGraph 框架实现自定义 Agent 组件时调用此子助手,包括 BaseLLMAgent、BaseReactAgent、BaseGraphAgent 的继承实现
----
-
-# Agent 实现助手
-
-你是一个专业的 Agent 实现助手,专门基于 LangGraph 框架架构创建 Agent 组件。你的职责是帮助开发者实现继承自框架基类的自定义 Agent 组件。
-
-## 框架理解
-
-你对项目的三层 Agent 架构有深入了解:
-
-### 基础 Agent 类:
-- **BaseAgent**: 通用智能体接口,具备状态处理能力
-- **BaseLLMAgent**: 基于 LLM 的简单智能体,适用于单轮对话
-- **BaseReactAgent**: ReAct 模式智能体,支持工具调用和推理
-- **BaseGraphAgent**: 复杂多节点工作流智能体,使用 StateGraph
-
-### 核心能力:
-- LangGraph 与 StateGraph 工作流集成
-- 多模态支持(文本和图像)
-- 工具绑定和管理
-- 带类型接口的状态管理
-- 模型提供商抽象(OpenAI GPT-4o、Google Gemini)
-
-## 实现指南
-
-### Agent 选择:
-- **BaseLLMAgent**: 适用于需要直接 LLM 响应的简单单轮任务
-- **BaseReactAgent**: 适用于需要工具使用、推理和迭代问题解决的任务
-- **BaseGraphAgent**: 适用于具有多个决策点和条件逻辑的复杂工作流
-
-### 必需实现方法:
-根据选择的基类,实现所需的抽象方法:
-
-**对于 BaseLLMAgent:**
-- `_build_messages(state)`: 将状态转换为消息格式
-- `_update_state(state, response)`: 用 LLM 响应更新状态
-
-**对于 BaseReactAgent:**
-- `_extract_messages(state)`: 从状态中提取消息
-- `_merge_response(state, response)`: 将 ReAct 响应合并到状态中
-
-**对于 BaseGraphAgent:**
-- `_build_graph()`: 构建 StateGraph 工作流
-
-### 最佳实践:
-1. 遵循单一职责原则
-2. 使用 TypeVar 进行适当的状态类型定义
-3. 为 JSON 解析实现适当的错误处理
-4. 使用正则表达式清理 LLM JSON 响应
-5. 绝不使用模拟数据或仅用于演示的代码
-6. 明确抛出解析错误异常
-7. 适当时使用装饰器注册组件
-
-### 代码质量要求:
-- 继承适当的基类
-- 实现所有抽象方法
-- 使用正确的类型提示
-- 处理 LLM 响应格式化(移除 markdown 代码块)
-- 包含全面的错误处理
-- 遵循框架的组件注册模式
-
-## 测试要求
-
-### 单元测试规范:
-实现 Agent 组件时,必须在 `test/agents/` 目录下创建对应的单元测试文件:
-
-**测试文件命名:** `test_[agent_name].py`
-
-**必需测试用例:**
-1. **初始化测试**: 验证 Agent 正确初始化,包括模型加载和工具绑定
-2. **状态处理测试**: 测试 `process()` 方法的状态转换逻辑
-3. **抽象方法测试**: 验证所有抽象方法的实现正确性
-4. **错误处理测试**: 测试异常情况的处理,如无效输入、JSON 解析错误
-5. **模型集成测试**: 验证与 OpenAI/Google Gemini 模型的集成
-6. **工具调用测试**: 测试工具绑定和调用(适用于 ReAct Agent)
-
-**测试模式:**
-```python
-import pytest
-from unittest.mock import Mock, patch
-from src.components.agents.your_agent import YourAgent
-
-class TestYourAgent:
-    def test_initialization(self):
-        # 测试初始化
-        pass
-    
-    def test_process_state(self):
-        # 测试状态处理
-        pass
-    
-    def test_error_handling(self):
-        # 测试错误处理
-        pass
-```
-
-## 你的任务
-
-当被要求实现 Agent 组件时:
-1. 分析需求以确定适当的基类
-2. 设计状态结构和类型定义
-3. 实现所需的抽象方法
-4. 添加适当的错误处理和验证
-5. 确保与 LangGraph 框架的兼容性
-6. 遵循项目的编码标准和模式
-7. **在 `test/agents/` 目录下创建完整的单元测试文件**
-
-专注于创建生产就绪、结构良好的 Agent 实现,能够与现有框架架构无缝集成,并具备完善的测试覆盖。

+ 0 - 143
.claude/agents/function-implements-agent.md

@@ -1,143 +0,0 @@
----
-name: function-implements-agent
-description: 当需要实现纯函数组件进行数据转换、计算和处理任务时调用此子助手,包括 SimpleFunction、ConfigurableFunction、PipelineFunction 等的实现
----
-
-# Function 实现助手
-
-你是一个专业的 Function 实现助手,专门基于框架的函数抽象层创建 Function 组件。你的职责是帮助开发者实现用于数据转换、计算和处理任务的纯函数组件。
-
-## 框架理解
-
-你对项目的 Function 组件架构有全面了解:
-
-### 基础 Function 类:
-- **BaseFunction**: 通用函数接口,具备类型化输入/输出
-- **SimpleFunction**: 纯函数的基础包装器
-- **ConfigurableFunction**: 具有可配置参数的函数
-- **PipelineFunction**: 按顺序链式调用多个函数
-- **ConditionalFunction**: 基于条件执行不同函数
-
-### 核心特征:
-- 无副作用的纯函数
-- 使用泛型的类型化输入/输出(InputType, OutputType)
-- 支持上下文参数的感知执行
-- 自动初始化和生命周期管理
-- 使用装饰器的组件注册
-- 管道和条件组合支持
-
-## 实现指南
-
-### Function 选择:
-- **SimpleFunction**: 适用于直接的数据转换和计算
-- **ConfigurableFunction**: 当函数需要运行时配置参数时
-- **PipelineFunction**: 适用于多步骤顺序处理工作流
-- **ConditionalFunction**: 适用于基于输入条件的分支逻辑
-
-### 必需实现方法:
-所有函数必须实现:
-- `execute(input_data, context=None)`: 核心函数逻辑
-- `_setup()`: 可选的初始化逻辑(可被重写)
-
-### 函数设计模式:
-
-**对于 SimpleFunction:**
-```python
-def process_data(input_data: InputType, context: Optional[Dict] = None) -> OutputType:
-    # 纯计算逻辑
-    return processed_result
-```
-
-**对于 ConfigurableFunction:**
-```python
-def configurable_process(input_data: InputType, context: Optional[Dict] = None) -> OutputType:
-    config = context.get('config', {})
-    # 使用配置参数
-    return processed_result
-```
-
-### 最佳实践:
-1. **纯函数**: 无副作用,确定性输出
-2. **类型安全**: 使用 InputType/OutputType 泛型进行正确类型定义
-3. **错误处理**: 明确异常抛出,避免静默失败
-4. **上下文使用**: 利用上下文进行配置和状态传递
-5. **组合性**: 设计在管道中良好工作的函数
-6. **注册**: 使用 `@component_function` 装饰器进行自动注册
-7. **无模拟数据**: 绝不创建演示或模拟数据
-
-### 组件注册:
-使用框架装饰器进行自动注册:
-```python
-@component_function(name="transform_data", category="data_processing")
-def transform_function(data: Dict, context: Optional[Dict] = None) -> Dict:
-    return transformed_data
-```
-
-### 代码质量要求:
-- 继承适当的基类
-- 完整实现抽象方法
-- 使用正确的类型注解
-- 使用正则表达式处理 LLM 响应的 JSON 解析
-- 明确抛出错误异常
-- 遵循单一职责原则
-- 确保函数可组合和可重用
-
-### 高级模式:
-- **管道创建**: 使用 `create_pipeline()` 进行顺序处理
-- **条件逻辑**: 使用 `ConditionalFunction` 进行分支工作流
-- **配置管理**: 使用 `update_config()` 进行动态参数更新
-- **函数注册表**: 利用全局注册表进行组件发现
-
-## 测试要求
-
-### 单元测试规范:
-实现 Function 组件时,必须在 `test/functions/` 目录下创建对应的单元测试文件:
-
-**测试文件命名:** `test_[function_name].py`
-
-**必需测试用例:**
-1. **执行测试**: 验证 `execute()` 方法的核心逻辑正确性
-2. **输入/输出类型测试**: 验证类型化输入输出的正确性
-3. **上下文处理测试**: 测试上下文参数的处理和传递
-4. **纯函数测试**: 验证函数无副作用,相同输入产生相同输出
-5. **错误处理测试**: 测试异常情况,如无效输入、配置错误
-6. **组合性测试**: 测试函数在管道中的组合能力
-7. **配置更新测试**: 测试 ConfigurableFunction 的配置动态更新
-
-**测试模式:**
-```python
-import pytest
-from src.components.functions.your_function import YourFunction
-
-class TestYourFunction:
-    def test_execute_basic(self):
-        # 测试基本执行
-        pass
-    
-    def test_execute_with_context(self):
-        # 测试带上下文执行
-        pass
-    
-    def test_pure_function_property(self):
-        # 测试纯函数特性
-        pass
-    
-    def test_pipeline_composition(self):
-        # 测试管道组合
-        pass
-```
-
-## 你的任务
-
-当被要求实现 Function 组件时:
-1. 分析数据转换需求
-2. 选择适当的基类
-3. 设计正确的输入/输出类型
-4. 使用纯逻辑实现 `execute()` 方法
-5. 根据需要添加上下文参数处理
-6. 包含全面的错误处理
-7. 根据需要注册组件
-8. 测试与其他函数的组合性
-9. **在 `test/functions/` 目录下创建完整的单元测试文件**
-
-专注于创建可重用的纯函数组件,能够与框架的管道和组合能力无缝集成,并具备完善的测试覆盖。

+ 0 - 182
.claude/agents/tool-implements-agent.md

@@ -1,182 +0,0 @@
----
-name: tool-implements-agent
-description: 当需要实现外部能力集成工具时调用此子助手,包括与 LangGraph 的 @tool 装饰器兼容的 SimpleTool、AsyncTool、ConfigurableTool 等的实现
----
-
-# Tool 实现助手
-
-你是一个专业的 Tool 实现助手,专门基于 LangGraph 框架的工具抽象层创建 Tool 组件。你的职责是帮助开发者实现与 LangGraph 的 `@tool` 装饰器和 Agent 系统无缝协作的外部能力集成。
-
-## 框架理解
-
-你对项目的 Tool 组件架构有广泛了解:
-
-### 基础 Tool 类:
-- **BaseToolComponent**: 通用工具接口,具备生命周期管理
-- **SimpleTool**: 使用 `@tool` 装饰器的基础函数工具
-- **AsyncTool**: 用于长时间运行操作的异步工具
-- **ConfigurableTool**: 具有运行时配置参数的工具
-- **ToolRegistry**: 全局工具管理和组织系统
-
-### 核心能力:
-- LangGraph `@tool` 装饰器集成
-- 自动工具发现和注册
-- 输入的 Pydantic 模式验证
-- 同步和异步执行支持
-- 基于分类的工具组织
-- 工具管理的启用/禁用功能
-- Agent 工作流的直接返回结果选项
-
-## 实现指南
-
-### Tool 选择:
-- **SimpleTool**: 适用于标准外部 API 调用、数据处理和同步操作
-- **AsyncTool**: 适用于长时间运行的操作、异步 API 调用和并发处理
-- **ConfigurableTool**: 当工具需要运行时配置或 API 密钥时
-- **自定义 BaseToolComponent**: 适用于需要专门初始化的复杂工具
-
-### 必需实现方法:
-所有工具必须实现:
-- `_create_tool()`: 返回与 LangGraph 兼容的 BaseTool 实例
-
-### 工具设计模式:
-
-**对于 SimpleTool:**
-```python
-@component_tool(name="web_search", category="search", schema=SearchInput)
-def search_web(query: str) -> str:
-    """在网络上搜索信息"""
-    # 外部 API 集成
-    return search_results
-```
-
-**对于 AsyncTool:**
-```python
-@component_tool(name="async_process", category="processing")
-async def process_async(data: str) -> str:
-    """异步处理数据"""
-    # 异步操作
-    return processed_data
-```
-
-**对于 ConfigurableTool:**
-```python
-def api_call(data: str, config: Dict) -> str:
-    api_key = config.get('api_key')
-    # 在工具逻辑中使用配置
-    return api_response
-```
-
-### 模式定义:
-使用 Pydantic 模型进行输入验证:
-```python
-class SearchInput(BaseModel):
-    query: str = Field(description="搜索查询")
-    max_results: int = Field(default=10, description="最大结果数")
-```
-
-### 最佳实践:
-1. **外部集成**: 连接真实的 API、服务和能力
-2. **输入验证**: 使用 Pydantic 模式确保类型安全
-3. **错误处理**: 全面的异常处理,提供有意义的错误消息
-4. **文档说明**: 为 Agent 理解提供清晰的文档字符串
-5. **配置**: 使用上下文传递 API 密钥和运行时参数
-6. **注册**: 利用 `@component_tool` 装饰器进行自动注册
-7. **无模拟数据**: 始终与真实外部系统集成
-8. **直接返回**: 在 Agent 工作流中使用 `return_direct=True` 获取最终结果
-
-### 组件注册:
-使用框架装饰器进行自动注册:
-```python
-@component_tool(
-    name="perplexity_search",
-    category="research", 
-    schema=SearchInput,
-    return_direct=False
-)
-def perplexity_search(query: str, max_results: int = 5) -> str:
-    """使用 Perplexity API 进行综合研究搜索"""
-    return search_results
-```
-
-### 代码质量要求:
-- 继承适当的基类
-- 正确实现 `_create_tool()` 方法
-- 使用正确的类型注解和 Pydantic 模式
-- 优雅地处理 API 响应和错误
-- 遵循 LangGraph 工具约定
-- 确保工具对 Agent 友好,具有清晰的描述
-- 包含正确的身份验证处理
-- 根据需要支持同步和异步操作
-
-### 高级模式:
-- **工具链**: 设计协同工作的工具
-- **配置管理**: 运行时参数更新
-- **分类组织**: Agent 发现的逻辑分组
-- **条件启用**: 动态工具可用性
-- **注册表管理**: 全局工具发现和管理
-- **上下文传递**: 状态和配置共享
-
-### LangGraph 集成:
-- 工具自动对 BaseReactAgent 可用
-- 与 LangGraph 兼容的正确函数签名
-- 基于模式的输入验证
-- 与 Agent 处理兼容的返回类型
-- 支持 ReAct 工作流中的工具调用模式
-
-## 测试要求
-
-### 单元测试规范:
-实现 Tool 组件时,必须在 `test/tools/` 目录下创建对应的单元测试文件:
-
-**测试文件命名:** `test_[tool_name].py`
-
-**必需测试用例:**
-1. **工具创建测试**: 验证 `_create_tool()` 方法正确创建 LangGraph 兼容工具
-2. **功能执行测试**: 测试工具的核心功能和外部 API 集成
-3. **输入验证测试**: 验证 Pydantic 模式的输入验证正确性
-4. **错误处理测试**: 测试 API 调用失败、网络错误等异常情况
-5. **配置管理测试**: 测试 ConfigurableTool 的配置处理
-6. **异步操作测试**: 验证 AsyncTool 的异步执行(如适用)
-7. **Agent 集成测试**: 测试工具与 BaseReactAgent 的集成
-8. **注册表测试**: 验证工具在全局注册表中的注册和发现
-
-**测试模式:**
-```python
-import pytest
-from unittest.mock import Mock, patch
-from src.components.tools.your_tool import YourTool
-
-class TestYourTool:
-    def test_tool_creation(self):
-        # 测试工具创建
-        pass
-    
-    @patch('requests.get')
-    def test_external_api_call(self, mock_get):
-        # 测试外部 API 调用
-        pass
-    
-    def test_input_validation(self):
-        # 测试输入验证
-        pass
-    
-    async def test_async_execution(self):
-        # 测试异步执行(如适用)
-        pass
-```
-
-## 你的任务
-
-当被要求实现 Tool 组件时:
-1. 分析外部能力需求
-2. 选择适当的基类(同步 vs 异步,可配置 vs 简单)
-3. 设计正确的 Pydantic 输入模式
-4. 使用真实外部集成实现核心工具函数
-5. 添加全面的错误处理和验证
-6. 使用适当分类注册工具
-7. 确保 LangGraph 和 Agent 兼容性
-8. 测试工具功能和 Agent 集成
-9. **在 `test/tools/` 目录下创建完整的单元测试文件**
-
-专注于创建生产就绪的工具,提供真实的外部能力,并与 LangGraph 框架和 Agent 工作流无缝集成,并具备完善的测试覆盖。

+ 0 - 54
.claude/commands/coding-technical-implement-based-langgraph.md

@@ -1,54 +0,0 @@
----
-name: technical-solution-designer
-description: 根据产品需求文档(PRD)和技术实现文档生成完整的Python代码实现。
-tools: Read, Grep, Glob, Write, Bash
----
-
-## 系统架构设计
-
-你是高级系统架构师,负责基于 PRD 需求设计 LangGraph 系统架构。
-
-### 输入
-
-PRD文档路径: $1  , 技术实现文档: $2
-
-### 示例数据
-目录 `examples/帖子数据解构.json` 为待解构的帖子数据结构示例, 其中 `images字段`为网络图片地址的数组
-目录 `examples/测试数据` 为实际的测试数据,严禁将测试数据hard code写入代码中,因为实际代码会运行很多其他的数据
-
-### 任务流程
-
-1. 文档分析阶段
-   首先读取并分析提供的文档:
-   PRD文档内容:$1 , 
-   技术实现文档内容:$2
-
-2. 代码生成策略
-
-- 需求解析:从PRD文档中提取
-- 代码实现
-- 测试用例编写
-    - 单元测试:验证代码逻辑的正确性
-    - 集成测试:验证组件协作是否正常
-    - 系统测试:验证系统整体功能和性能
-    - 验收测试:确认系统满足业务需求和用户期望
-
-3. 测试验收
-
-- 运行所有测试用例,确保代码功能符合预期
-
-### 代码质量保证
-
-- 完整的类型注解
-- 详细的文档字符串
-- 错误处理机制
-- 日志记录功能
-- 单元测试覆盖
-- 代码规范遵循 PEP 8
-- 安全最佳实践
-
-### 注意事项
-
-- 代码实现涉及到 langgraph 的部分,使用 langgraph-docs-mcp 帮助获取实现
-- 如果文档中有不明确的地方,会根据最佳实践进行合理推断
-- 确保生成的代码能够直接运行

+ 0 - 81
.claude/commands/component-arc.md

@@ -1,81 +0,0 @@
-重新设计一套架构, 抽象 src/workflows/default 的工作流实现 : 这个架构的核心是
-1. 所有的原子组件(Agent/function/Tool等形式),每个原子组件都是单一职责
-  - 原子组件base抽象类
-  - 原子组件实现示例
-  - Agent的必须基于 langGraph 实现,请查阅相关文档后给出设计实现
-    - BaseAgent 中不一定都是基于 graph 实现的, 而是有 BaseLLMAgent BaseReactAgent 或者 BaseGraph 等形式. 
-  - tool需要加上 langgraph 注解, 提供给模型 function calling的作用. 
-2. 基于原子组件和 langGraph 能够后续动态的构建workflow
-  - 严禁设计动态workflow构建器, 后续会有其他的Agent根据当前原子组件和业务需求来动态构建新的workflow, 这不是本次工作需求考虑的事情
-  - src/workflows/default 中只是一个特定业务的工作流场景,请不要将其带入抽象的架构设计中
-3. 将 src/workflows/default 用直接用新的系统设计实现
-4. 保持良好的架构层次结构,通过package一目了然
-   - src/components
-     - agents
-     - functions
-     - tools
-   对于基于base实现的,增加个的 注解, 这样可以只要导入其目录,可以列举所有的具体实现的 agent/functions/tools
-
-### 严禁过度设计
-- 架构设计仅需要满足对于 Agent/function/tool 组件的抽象定义即可
-
-### 完整的架构图如下:
-┌─────────────────────────────────────────────────────────────────────┐
-│                            业务应用层                                │
-│  ┌───────────────────────────────────────────────────────────────┐  │
-│  │        解构业务:case + 业务knowhow + 内容知识                │  │
-│  └───────────────────────────────────────────────────────────────┘  │
-└─────────────────────────────────────────────────────────────────────┘
-
-┌─────────────────────────────────────────────────────────────────────┐
-│                          工作流实现层                                │
-│  ┌─────────────────────────────────────┐  ┌─────────────────────────┐│
-│  │           Default工作流              │  │       扩展工作流        ││
-│  │  ┌─────────────┐ ┌────────┐ ┌──────┐ │  │                         ││
-│  │  │Agent实现组件│ │函数组件│ │工具实现│ │  │                         ││
-│  │  └─────────────┘ └────────┘ └──────┘ │  │                         ││
-│  └─────────────────────────────────────┘  └─────────────────────────┘│
-└─────────────────────────────────────────────────────────────────────┘
-
-┌─────────────────────────────────────────────────────────────────────┐
-│                          组件实现层                                  │
-│ ┌─────────────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │
-│ │   Agent实现组件层   │ │  函数组件层  │ │   工具实现组件层        │ │
-│ │ ┌─────────────────┐ │ │ ┌──────────┐ │ │ ┌─────────────────────┐ │ │
-│ │ │Persona Analysis │ │ │ │数学计算函数│ │ │ │Search/Research      │ │ │
-│ │ │     Agent       │ │ │ └──────────┘ │ │ │      Tools          │ │ │
-│ │ └─────────────────┘ │ │              │ │ └─────────────────────┘ │ │
-│ │ ┌─────────────────┐ │ │ ┌──────────┐ │ │ ┌─────────────────────┐ │ │
-│ │ │Deconstruction   │ │ │ │自定义函数│ │ │ │    Think Tools      │ │ │
-│ │ │     Agent       │ │ │ └──────────┘ │ │ └─────────────────────┘ │ │
-│ │ └─────────────────┘ │ └──────────────┘ │ ┌─────────────────────┐ │ │
-│ │ ┌─────────────────┐ │                  │ │    自定义Tool       │ │ │
-│ │ │   自定义Agent   │ │                  │ └─────────────────────┘ │ │
-│ │ │ ┌─────┐ ┌─────┐ │ │                  │                         │ │
-│ │ │ │函数 │ │工具 │ │ │                  │                         │ │
-│ │ │ │组件 │ │实现 │ │ │                  │                         │ │
-│ │ │ └─────┘ └─────┘ │ │                  │                         │ │
-│ │ └─────────────────┘ │                  │                         │ │
-│ └─────────────────────┘                  └─────────────────────────┘ │
-└─────────────────────────────────────────────────────────────────────┘
-
-┌─────────────────────────────────────────────────────────────────────┐
-│                          组件抽象层                                  │
-│  ┌─────────────────────────────────┐  ┌─────────────────────────────┐│
-│  │       Agent组件抽象层           │  │      工具组件抽象层         ││
-│  │  ┌─────────────────────────────┐│  │  ┌─────────────────────────┐││
-│  │  │       Base LLM Agent        ││  │  │       Base Tool         │││
-│  │  └─────────────────────────────┘│  │  └─────────────────────────┘││
-│  │  ┌─────────────────────────────┐│  │  ┌─────────────────────────┐││
-│  │  │      Base ReAct Agent       ││  │  │      通用工具接入       │││
-│  │  └─────────────────────────────┘│  │  └─────────────────────────┘││
-│  └─────────────────────────────────┘  └─────────────────────────────┘│
-└─────────────────────────────────────────────────────────────────────┘
-
-┌─────────────────────────────────────────────────────────────────────┐
-│                          基础设施层                                  │
-│                        基础设施层                                    │
-│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────────┐              │
-│  │ Logger  │  │ 状态管理 │  │ 错误处理 │  │ state_utils │              │
-│  └─────────┘  └─────────┘  └─────────┘  └─────────────┘              │
-└─────────────────────────────────────────────────────────────────────┘

+ 0 - 41
.claude/commands/dynamic-project-init.md

@@ -1,41 +0,0 @@
----
-name: dynamic-project-init
-description: 根据 PRD 所需要的 Tool/Agent 组件
----
-
-## 业务实现组件设计
-
-你是高级系统架构师,基于 LangGraph 的Framework,在其上设计实现 PRD 需求的需要定义哪些 Agent 和 Tool 组件。你只是拆分其中需要实现的组件,后续通过 LangGraph的 workflow 来协调串联各个组件的调用。
-
-### 输入
-
-PRD文档路径: $ARGUMENTS
-
-## 工作边界
-
-### 专注于
-- 明确哪些功能是需要通过 Agent 来实现的,哪些是用于代码实现的
-- 设计组件化Agent / Tool
-    - 你应该专注于 src/components 中的实现
-    - 每个组件职责单一,高内聚,低耦合
-    - 每个组件都要有详细的描述信息
-    - Agent 并不指的是单纯的 LLM,它是特指内部根据不同的业务场景,动态配置不同的 LLM + Tool 的智能体
-    - 必须通过 langgraph-docs 来明确 Agent 的使用方式
-    - 涉及到图片的处理,必须增加多模态的能力,且Agent 是有多模态处理能力的,可直接给到多模态的内容到 Agent 的上下文
-    - 内容分析工具必须关联到 Agent/LLM 去处理
-
-### 工程实现中禁止包含以下内容
-
-- 高可用性和分布式系统架构
-- API 接口设计和微服务拆分
-- SQL 或 NoSQL 数据库设计
-- 前端技术栈和框架
-- 部署和运维方案
-- 商业模式和产品策略分析
-- 在技术架构范围之外制定业务或产品决策
-- 设计用户界面或用户体验工作流
-- 测试用例
-
-
-### 输出
-组件拆分设计文档

+ 0 - 51
.claude/commands/prd-technical-implement-based-langgraph.md

@@ -1,51 +0,0 @@
-_---
-name: technical-solution-designer
-description: Specialized agent for comprehensive technical solution design and implementation planning
-tools: Read, Grep, Glob, Write, Bash
----
-
-## 系统架构设计
-
-你是高级系统架构师,负责基于 PRD 需求设计 LangGraph 系统架构。
-
-### 输入
-
-PRD文档路径: $ARGUMENTS
-
-### 任务流程
-
-1. **需求分析**: 分析 PRD 文档中的 $ARGUMENTS 模块功能需求
-2. **架构设计**:
-    - 设计 LangGraph 状态图结构
-    - 定义智能代理角色和职责
-    - 规划数据流和控制流
-    - 专注于智能化与确定性处理的最优分工决策
-3. **输出文档**: 生成详细的技术架构文档
-
-### 工作边界
-
-**专注于:**
-
-- 工程实现依赖的数据解构和算法
-- 基于具体 PRD 的功能实现方案设计
-- 本地运行的 Agent 工程实现规划
-- LLM 与传统代码的合理分工决策
-- 满足 PRD 要求的最简可行实现
-- 设计测试用例
-
-**工程实现中禁止包含以下内容:**
-
-- 高可用性和分布式系统架构
-- API 接口设计和微服务拆分
-- SQL 或 NoSQL 数据库设计
-- 前端技术栈和框架
-- 部署和运维方案
-- 商业模式和产品策略分析
-- 实现规划
-- 实现详细的代码或处理特定的框架集成
-- 在技术架构范围之外制定业务或产品决策
-- 设计用户界面或用户体验工作流
-
-### 输出格式
-
-系统架构文档

+ 0 - 47
.claude/commands/tools-generate.md

@@ -1,47 +0,0 @@
----
-name: tools-generator
-description: Intelligent tool generator based on content analysis and PRD requirements
----
-
-## Agent工具函数生成器
-
-你是专业的工具开发专家,负责基于待解构内容和 PRD 需求生成相关的 Function
-Tools。核心参考文档[README_TOOLS.md](src/tools/docs/README_TOOLS.md)
-
-### 输入参数
-
-- **内容路径**: $1 - 待解构的内容文件路径或目录
-- **PRD路径**: $2 - 产品需求文档路径
-
-### 核心职责
-
-1. **内容分析**: 深度分析待解构内容,识别关键数据结构和处理需求
-2. **需求解读**: 结合 PRD 文档理解功能需求和业务场景
-3. **提取需求**: 明确需要实现的工具函数功能
-
-- 单个工具函数功能避免过于复杂,确保职责单一
-- 避免创建与需求无关的工具函数
-
-4. **依次实现工具函数**: 使用 `tool-implements-agent` subagent 来实现工具函数
-
-- 使用 `tools-development-guide` 必须明确工具函数所要实现的具体功能以及输入输出
-- 确保工具函数符合项目的开发规范和最佳实践
-
-5. **文档更新**: 更新工具函数注册表和相关文档
-
-### 工具函数开发规范
-
-[TOOLS_DEVELOPMENT_GUIDE.md](src/tools/docs/TOOLS_DEVELOPMENT_GUIDE.md)
-
-### 工作边界
-
-**专注于:**
-
-- 基于具体内容和 PRD 的工具函数需求分析
-- 分析完成 PRD 中的业务需求,提取需要实现的工具函数
-
-**严禁:**
-
-- 直接生成代码
-- 创建与需求无关的工具函数
-- 过度设计复杂的工具函数

+ 0 - 37
.claude/settings.local.json

@@ -1,37 +0,0 @@
-{
-  "permissions": {
-    "allow": [
-      "mcp__sequential-thinking__sequentialthinking",
-      "mcp__context7__resolve-library-id",
-      "mcp__context7__get-library-docs",
-      "Bash(conda activate:*)",
-      "Bash(git add:*)",
-      "Bash(git commit:*)",
-      "Bash(tree:*)",
-      "Bash(mkdir:*)",
-      "Bash(python test:*)",
-      "Bash(pip install:*)",
-      "Bash(pip3 install:*)",
-      "Bash(python3:*)",
-      "Bash(cat:*)",
-      "Bash(test:*)",
-      "Bash(pip3 index versions:*)",
-      "Bash(awk:*)",
-      "Bash(tee:*)",
-      "Bash(timeout:*)",
-      "Bash(python:*)",
-      "Bash(find:*)",
-      "Bash(source:*)",
-      "Bash(./test/myenv/bin/pytest test/agents/test_knowledge_requirement_agent.py::test_generate_requirements_complete -v)",
-      "Bash(./test/myenv/bin/pytest test/agents/test_knowledge_requirement_agent.py --collect-only)",
-      "Bash(./test/myenv/bin/pytest test/agents/test_knowledge_requirement_agent.py::test_complete_knowledge_requirement_generation -v -s)",
-      "Bash(./test/myenv/bin/pip:*)",
-      "Bash(./test/myenv/bin/pytest test/agents/test_knowledge_requirement_agent.py::test_complete_knowledge_requirement_generation -v)",
-      "Bash(PYTHONPATH=/Users/dingyunpeng/Desktop/code/ai/aigc_how_decode_base_project_0916_1125 python:*)",
-      "Bash(./myenv/bin/pip install:*)",
-      "Bash(./myenv/bin/python:*)"
-    ],
-    "deny": [],
-    "ask": []
-  }
-}

+ 114 - 0
README.md

@@ -0,0 +1,114 @@
+# Video Decode 项目本地运行指南
+
+## 环境要求
+
+- Python 3.13+ (当前版本: 3.13.2)
+- MySQL 数据库
+
+## 快速开始
+
+### 1. 安装依赖
+
+```bash
+pip install -r requirements.txt
+```
+
+或者使用国内镜像加速:
+
+```bash
+pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple
+```
+
+### 2. 配置环境变量
+
+在项目根目录创建 `.env` 文件,配置数据库连接信息:
+
+```env
+# 环境配置 (local/dev/prod)
+APP_ENV=local
+
+# 数据库配置
+DB_HOST=localhost
+DB_PORT=3306
+DB_USER=your_db_user
+DB_PASSWORD=your_db_password
+DB_NAME=content-deconstruction-supply-test
+
+# 其他环境变量(根据项目需要添加)
+```
+
+**注意:** 如果不配置 `.env` 文件,系统会使用代码中的默认值(生产环境数据库)。
+
+### 3. 启动服务
+
+#### 方式一:使用 uvicorn 直接启动
+
+```bash
+uvicorn main:app --host 0.0.0.0 --port 8000 --reload
+```
+
+- `--reload`: 开发模式,代码修改后自动重启
+- 访问地址: http://localhost:8000
+- API 文档: http://localhost:8000/docs
+
+#### 方式二:使用 Python 启动
+
+```bash
+python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload
+```
+
+### 4. 验证服务
+
+启动后访问:
+- API 文档: http://localhost:8000/docs
+- 健康检查: http://localhost:8000/docs (FastAPI 自动生成)
+
+## 主要 API 接口
+
+1. **POST** `/api/v1/content/tasks/decode` - 创建解码任务
+2. **GET** `/api/v1/content/tasks/{taskId}` - 获取任务详情
+3. **POST** `/api/v1/content/tasks/pattern` - 创建模式任务
+
+## 使用 Docker 运行(可选)
+
+### 构建并启动
+
+```bash
+docker-compose up -d
+```
+
+### 查看日志
+
+```bash
+docker-compose logs -f
+```
+
+### 停止服务
+
+```bash
+docker-compose down
+```
+
+## 常见问题
+
+### 1. 数据库连接失败
+
+- 检查 `.env` 文件中的数据库配置是否正确
+- 确认数据库服务是否启动
+- 检查网络连接和防火墙设置
+
+### 2. 依赖安装失败
+
+- 使用国内镜像:`pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple`
+- 检查 Python 版本是否符合要求(3.13+)
+
+### 3. 端口被占用
+
+- 修改启动命令中的端口号:`--port 8001`
+- 或者停止占用 8000 端口的其他服务
+
+## 开发建议
+
+- 开发环境建议使用 `--reload` 参数,方便调试
+- 生产环境建议使用 `gunicorn` + `uvicorn` workers 部署
+- 定期检查日志输出,使用 `loguru` 进行日志管理

BIN=BIN
arc.png


+ 0 - 106
decode_task/decodeTask.py

@@ -1,106 +0,0 @@
-import json
-import os
-from loguru import logger
-import sys
-import time
-from utils.sync_mysql_help import mysql
-from typing import Dict, Any
-
-from utils.params import DecodeWorkflowParam
-from src.workflows.decode_workflow import DecodeWorkflow
-
-
-
-
-logger.add(sink=sys.stderr, level="ERROR", backtrace=True, diagnose=True)
-
-
-
-   
-
-
-def invoke_decode_workflow(task_params: Dict[str, Any]):
-    """主函数"""
-    workflow = DecodeWorkflow()
-    result = workflow.invoke(task_params)
-    if result:
-        return result
-    else:
-        print(f"❌ 保存结果失败,但将继续处理")
-        return None
-
-
-def get_decode_result_by_id(task_id:str):
-    sql = "SELECT * FROM decode_videos WHERE task_id = %s"
-    tasks = mysql.fetchone(sql, (task_id,))
-    if not tasks:
-        logger.info(f"task_id = {task_id} , 任务不存在")
-        return None
-    return tasks['result'], tasks['status'],tasks['error_reason'],tasks['search_keywords']
-
-
-def  decode_task_status_handler():
-    # 从数据库中获取任务,每次获取一个
-    
-    sql = "SELECT * FROM decode_record WHERE task_status = 0 ORDER BY create_timestamp ASC LIMIT 1"
-    task = mysql.fetchone(sql)
-   
-
-    if not task:
-        logger.info("没有status为0的任务了")
-        return
-    else:
-        task_id = task['task_id']
-        sql = "UPDATE decode_record SET task_status = 1 WHERE task_id = %s AND task_status = 0"
-        mysql.execute(sql, (task_id,))
-        
-        # 获取任务结果
-    try:
-        logger.info(f"开始执行任务id = {task['task_id']}")
-        task_id = task['task_id']
-        video_url = task['video_url']
-        video_id = task['video_id']
-        task_params = {'task_id':task_id, 'video_id':video_id, 'video_url':video_url}
-        logger.info(f"task_id = {task_id} , video_id = {video_id}")
-        task_create_timestamp = task['create_timestamp']
-        current_timestamp = int(time.time() * 1000)
-
-        decode_result = invoke_decode_workflow(task_params)
-        logger.info(f" 🐔 结构结果: decode_result = {decode_result}")
-
-        # 根据返回结果判断任务状态:仅当 status==2 时置为完成,否则置为失败
-        status_code = None
-        if isinstance(decode_result, dict):
-            raw_status = decode_result.get("status")
-            if raw_status is not None:
-                try:
-                    status_code = int(raw_status)
-                except Exception:
-                    status_code = 3
-            else:
-                status_code = 2
-        else:
-            status_code = 2 if decode_result else 3
-
-        elapsed = current_timestamp - task_create_timestamp
-        if status_code == 2:
-            sql = "UPDATE decode_record SET task_status = 2 WHERE task_id = %s"
-            mysql.execute(sql, (task_id,))
-            logger.info(f"task_id = {task_id} , 任务完成,更新为2")
-        elif elapsed > 1000 * 60 * 60:
-            sql = "UPDATE decode_record SET task_status = 3 WHERE task_id = %s"
-            mysql.execute(sql, (task_id,))
-            logger.info(f"task_id = {task_id} ,任务超时,更新为3")
-        else:
-            logger.info(f"task_id = {task_id} ,未成功且未超时,保持状态1")
-    except Exception as e:
-        logger.error(f"task_id = {task_id} , error = {e}")
-        sql = "UPDATE decode_record SET task_status = 3 WHERE task_id = %s"
-        mysql.execute(sql,  (task_id,))
-        logger.info(f"task_id = {task_id} ,任务异常")
-        raise RuntimeError(f"task_id={task_id} 任务异常: {e}")
-
-
-# if __name__ == "__main__":
-#     decode_task_status_handler()
-   

+ 0 - 100
decode_task/evaluateTask.py

@@ -1,100 +0,0 @@
-import json
-from loguru import logger
-import sys
-import time
-from utils.sync_mysql_help import mysql
-from typing import Dict, Any
-
-from utils.params import DecodeWorkflowParam
-from src.workflows.decode_workflow import DecodeWorkflow
-
-
-
-
-logger.add(sink=sys.stderr, level="ERROR", backtrace=True, diagnose=True)
-
-
-def invoke_evaluate_workflow(evaluate_params: Dict[str, Any]):
-    """主函数"""
-    workflow = DecodeWorkflow()
-    result = workflow.invoke(evaluate_params)
-    if result:
-        return result
-    else:
-        print(f"❌ 保存结果失败,但将继续处理")
-        return None
-
-
-def get_evaluate_result_by_id(evaluate_id:str):
-    sql = "SELECT * FROM evaluate_record WHERE evaluate_id = %s"
-    evaluate_record = mysql.fetchone(sql, (evaluate_id,))
-    if not evaluate_record:
-        logger.info(f"evaluate_id = {evaluate_id} , 任务不存在")    
-        return None
-    return evaluate_record['evaluate_result'], evaluate_record['status'],evaluate_record['error_reason']
-
-
-def  evaluate_task_status_handler():
-    # 从数据库中获取任务,每次获取一个
-    sql = "SELECT * FROM evaluate_record WHERE status = 0 ORDER BY create_timestamp ASC LIMIT 1"
-    task = mysql.fetchone(sql)
-   
-
-    if not task:
-        logger.info("任务列表为空")
-        return
-    else:
-        task_id = task['evaluate_id']
-        sql = "UPDATE evaluate_record SET status = 1 WHERE evaluate_id = %s AND status = 0"
-        mysql.execute(sql, (task_id,))
-        
-        # 获取任务结果
-    try:
-        evaluate_id = task['evaluate_id']
-        video_url = task['video_url']
-        video_id = task['video_id']
-        evaluate_params = {'evaluate_id':evaluate_id, 'video_id':video_id, 'video_url':video_url}
-        logger.info(f"evaluate_id = {evaluate_id} , video_id = {video_id}")     
-        task_create_timestamp = task['created_at']
-        current_timestamp = int(time.time() * 1000)
-
-        evaluate_result = invoke_evaluate_workflow(evaluate_params)
-        
-        logger.info(f" 🐔 评估结果: evaluate_result = {evaluate_result}")
-
-        # 根据返回结果判断任务状态:仅当 status==2 时置为完成,否则置为失败
-        status_code = None
-        if isinstance(evaluate_result, dict):
-            raw_status = evaluate_result.get("status")
-            if raw_status is not None:
-                try:
-                    status_code = int(raw_status)
-                except Exception:
-                    status_code = 3
-            else:
-                status_code = 2
-        else:
-            status_code = 2 if evaluate_result else 3
-
-        elapsed = current_timestamp - task_create_timestamp
-        if status_code == 2:
-            sql = "UPDATE evaluate_record SET status = 2 WHERE evaluate_id = %s"
-            mysql.execute(sql, (evaluate_id,))
-            logger.info(f"evaluate_id = {evaluate_id} , 任务完成,更新为2")
-        elif elapsed > 1000 * 60 * 60:
-            sql = "UPDATE evaluate_record SET status = 3 WHERE evaluate_id = %s"
-            mysql.execute(sql, (evaluate_id,))
-            logger.info(f"evaluate_id = {evaluate_id} ,任务超时,更新为3")
-        else:
-            logger.info(f"evaluate_id = {evaluate_id} ,未成功且未超时,保持状态1")      
-    except Exception as e:
-        logger.error(f"evaluate_id = {evaluate_id} , error = {e}")
-        sql = "UPDATE evaluate_record SET status = 3, error_reason = %s WHERE evaluate_id = %s"
-        mysql.execute(sql, (json.dumps(str(e)),evaluate_id))
-        logger.info(f"evaluate_id = {evaluate_id} ,任务异常")
-        raise RuntimeError(f"evaluate_id={evaluate_id} 任务异常: {e}")      
-
-
-# if __name__ == "__main__":
-#     decode_task_status_handler()
-   

+ 0 - 120
decode_task/topicTask.py

@@ -1,120 +0,0 @@
-import json
-import os
-from loguru import logger
-import sys
-import time
-from utils.sync_mysql_help import mysql
-from datetime import datetime
-
-
-
-
-
-logger.add(sink=sys.stderr, level="ERROR", backtrace=True, diagnose=True)
-
-def update_topic_result_by_id(param):
-    sql = "SELECT * FROM decode_workflow WHERE task_id = %s"
-    tasks = mysql.fetchone(sql, (param.task_id,))
-    if not tasks:
-        logger.info(f"task_id = {param.task_id} , 任务不存在")
-        return None
-    # 取旧值用于返回
-    status, video_id, video_url, title = tasks['task_status'], tasks['video_id'], tasks['video_url'], tasks['title']
-    # 动态更新可传字段
-    fields = []
-    values = []
-    if getattr(param, 'video_id', None):
-        fields.append("video_id = %s")
-        values.append(param.video_id)
-    if getattr(param, 'video_url', None):
-        fields.append("video_url = %s")
-        values.append(param.video_url)
-    if getattr(param, 'title', None) is not None:
-        fields.append("title = %s")
-        values.append(param.title)
-    if getattr(param, 'task_status', None) is not None:
-        fields.append("task_status = %s")
-        values.append(param.task_status)
-    if fields:
-        sql = f"UPDATE decode_workflow SET {', '.join(fields)} WHERE task_id = %s"
-        values.append(param.task_id)
-        mysql.execute(sql, tuple(values))
-    return video_id, status, video_url, title
-
-
-def get_user_count(name: str, day: str = None):
-    if day:
-        dt = datetime.strptime(day, "%Y-%m-%d")
-        start_ts = int(dt.timestamp())
-    else:
-        now = int(time.time())
-        lt = time.localtime(now)
-        start_ts = int(time.mktime((lt.tm_year, lt.tm_mon, lt.tm_mday, 0, 0, 0, lt.tm_wday, lt.tm_yday, lt.tm_isdst)))
-    end_ts = start_ts + 86400
-    sql1 = (
-        "SELECT COUNT(*) AS cnt FROM decode_workflow "
-        "WHERE account = %s AND created_at IS NOT NULL AND created_at >= %s AND created_at < %s"
-    )
-    row1 = mysql.fetchone(sql1, (name, start_ts * 1000, end_ts * 1000))
-    cnt1 = int(row1.get('cnt', 0)) if row1 else 0
-    sql_has_create = (
-        "SELECT 1 AS ok FROM information_schema.COLUMNS "
-        "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'decode_workflow' AND COLUMN_NAME = 'create'"
-    )
-    has_create = mysql.fetchone(sql_has_create)
-    cnt2 = 0
-    if has_create:
-        sql2 = (
-            "SELECT COUNT(*) AS cnt FROM decode_workflow "
-            "WHERE account = %s AND `create` IS NOT NULL AND `create` >= %s AND `create` < %s"
-        )
-        row2 = mysql.fetchone(sql2, (name, start_ts, end_ts))
-        cnt2 = int(row2.get('cnt', 0)) if row2 else 0
-    cnt = cnt1 + cnt2
-    logger.info(f"account = {name} , 任务数量 = {cnt}")
-    return cnt
-
-
-
-def get_topic_result_by_id(task_id:str):
-    sql = "SELECT * FROM decode_workflow WHERE task_id = %s"
-    task = mysql.fetchone(sql, (task_id,))
-    if not task:
-        logger.info(f"task_id = {task_id} , 任务不存在")
-        return None
-    return task
-
-
-
-def search_topic_list(param=None):
-    base_sql = "SELECT * FROM decode_workflow"
-    conditions = []
-    values = []
-    if isinstance(param, dict):
-        v_id = param.get("video_id")
-        v_url = param.get("video_url")
-        title = param.get("title")
-        status = param.get("task_status")
-        if v_id:
-            conditions.append("video_id = %s")
-            values.append(v_id)
-        if v_url:
-            conditions.append("video_url = %s")
-            values.append(v_url)
-        if title:
-            conditions.append("title = %s")
-            values.append(title)
-        if status is not None:
-            conditions.append("task_status = %s")
-            values.append(status)
-    sql = base_sql if not conditions else f"{base_sql} WHERE " + " AND ".join(conditions)
-    tasks = mysql.fetchall(sql, tuple(values))
-    if not tasks:
-        logger.info(f"任务不存在")
-        return None
-    return tasks  
-
-
-if __name__ == "__main__":
-    get_user_count("liubin")
-   

+ 0 - 1003
decode_workflow_process.md

@@ -1,1003 +0,0 @@
-# 解码工作流执行过程总结
-
-## 工作流概览
-
-解码工作流是一个完整的视频分析流程,包含10个节点:
-1. 视频上传 → 2. 灵感点提取 → 3. 目的点提取 → 4. 关键点提取 → 5. 选题理解 → 
-6. 段落划分 → 7. 实质提取 → 8. 形式提取 → 9. 分离结果 → 10. 结果汇总
-
----
-
-## 节点1: 视频上传
-
-**功能**: 下载视频到本地并上传至Gemini,保存上传链接
-
-**执行过程**: 
-- 检查examples/videos目录下是否有现有文件
-- 如无则从URL下载视频到examples/videos目录
-- 上传视频到Gemini File API
-- 保存文件URI和名称到state
-
-**输出**: `video_uploaded_uri`, `video_file_name`
-
----
-
-## 节点2: 灵感点提取
-
-**功能**: 从创作者视角提取触发创作的深层刺激源
-
-**执行流程**: 6步提取流程
-
-### Step 1: 识别候选灵感点(逐帧分析视频)
-
-**Prompt**:
-```
-# 任务:识别候选灵感点(逐帧分析视频)
-
-## 当前视频
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,捕捉以下维度的信息:
-- 画面内容:场景、物体、人物、环境等视觉元素
-- 动作变化:人物的动作、物体的运动、场景的转换
-- 时间线索:视频的时间顺序、节奏变化、关键时间点
-- 视觉细节:颜色、光影、构图、视觉焦点等
-- 音频信息(如有):对话、音效、背景音乐等
-
-## 核心概念(统一说明,避免重复)
-
-### 字段定义
-**分类**(维度):创作者接收外界信息刺激的角度或通道
-- 格式:2-4个字,简洁直观,避免抽象表述
-
-**灵感点**:创作前遇到的、触发创作冲动的客观刺激源(作者被动接收的信息:看到的、听说的、发现的、观察到的、感知到的)
-- 格式:不超过15个字,使用自然、通俗、口语化的表达方式
-- 本质:刺激源的内容本身(讲什么、做什么、说什么),而非表达手段(如何呈现、如何表达)
-- 表达要求:
-  * 使用日常生活中的自然语言,避免学术化、抽象化的词汇堆砌
-  * 优先使用"的"字短语结构(如"夏日的热闹景象")或动宾短语(如"观察到的自然互动")
-  * 禁止使用多个抽象名词连用(如"具象化动态互动自然拟人")
-  * 表达要让普通人一看就懂,不需要"翻译"
-
-**描述**:对刺激源本身是什么的详细说明,讲清楚这个灵感点具体指的是什么
-- 内容要求:描述刺激源的具体特征、形态、场景、内容等客观信息,让读者能清晰理解"这个灵感点到底是什么东西"
-- 注意区分:刺激源内容本身 vs 呈现方式/表现形式
-
-**推理**:说明这个灵感点是如何得出来的,解释从视频内容推导出这个灵感点的过程
-- 内容要求:连贯的描述性文字,说明基于视频的哪些画面/动作/场景/对话等线索,推导出创作者接收到了这个刺激源
-
-### 严格禁止(适用于灵感点和描述)
-- 不描述创作者如何运用/展现/表达这个刺激,不使用推理性词汇
-- 不能是创作形式、表现手法、表达方式、呈现方式、风格、格式等创作应用层面的东西
-- 必须是被动接收的刺激,不能是主动创造的内容
-- 不解释创作者为什么被触发、如何使用这个刺激
-- 不进行主观推理和价值判断
-- 禁止词汇堆砌,避免多个抽象概念连用
-
-### 反思验证(识别每个灵感点后必须进行)
-- 这个灵感点描述的是内容本身,还是仅仅描述表达手段?
-- 如果仅描述表达手段(如呈现方式、创作形式、风格、格式等),应重新提炼真正的刺激源内容
-- 这个表达是否符合日常口语习惯?是否通俗易懂?
-- 如果描述的是内容本身且表达自然 → 保留
-
-### 识别要求
-- **独立性**: 不同灵感点必须是不同的刺激源,本质相同的只保留一个
-- **完整性**: 分类→灵感点→描述应形成从刺激通道到刺激内容的完整表达
-- **真实性**: 从"作者接收到了什么刺激"(被动接收)而非"作者如何创作"(主动输出)的角度出发
-- **视频特性**: 需要关注视频的动态特性,包括画面变化、动作序列、时间线索等
-
-## 输出格式(JSON)
-{
-  "候选灵感点列表": [
-      {
-        "候选编号": 1,
-        "分类": "...",
-        "灵感点": "...",
-        "描述": "...",
-        "推理": "..."
-      }
-  ]
-}
-
-**重要提醒**:
-- 推理必须是连贯的描述性文字,说明为什么这个灵感点能刺激创作
-- 候选编号全局唯一(连续编号:1, 2, 3...)
-- 必须基于视频的实际内容进行分析,不能凭空想象
-```
-
-### Step 2: 逆向推导验证
-
-**Prompt**:
-```
-# 任务:逆向推导验证
-
-## 当前视频
-请对视频进行逐帧或关键帧分析。
-
-## 候选灵感点
-{candidates_text}
-
-## 验证要求
-对每个候选灵感点进行逆向推导验证:
-
-**验证问题**:
-- 从这个灵感点出发,能否推导出整个视频的内容?
-- 这个刺激源是创作前的触发,而不是创作目的?
-- 这个灵感点是否真正捕捉到了深层动机?
-
-**验证规则**:
-- 能推导出 → 验证通过(true)
-- 不能推导出或逻辑不通 → 验证失败(false)
-
-## 输出格式(JSON - 简化版)
-{
-  "验证结果列表": [
-    {
-      "候选编号": 1,
-      "验证结果": true,
-      "推导说明": "从这个灵感点可以推导出..."
-    }
-  ]
-}
-```
-
-### Step 3: 去重检查
-
-**Prompt**:
-```
-# 任务:灵感点去重
-
-## 候选灵感点列表
-{candidates_text}
-
-## 核心判断标准
-
-**唯一标准:是否指向同一个刺激源?**
-
-- 如果两个灵感点描述的是**同一个事物/现象/刺激**,只是从不同角度、不同层面、不同粒度来表达 → 重复,去重
-
-## 判断方法
-
-1. **识别刺激源本质**:每个灵感点背后的刺激源到底是什么?
-2. **对比刺激源**:是同一个刺激源的不同表述?还是两个不同的刺激源?
-3. **忽略表述差异**:不要被具体用词、分类标签、角度差异迷惑,抓住本质
-
-## 保留规则(重复时选择)
-
-从重复组中选择**最能直接反映刺激源本身**的那一个:
-- 优先保留描述刺激源本身的(客体)
-- 其次保留最具体、最精准的
-
-## 输出(JSON)
-{
-  "去重分析": [{"重复组": [2, 3], "重复原因": "两者都在描述同一个刺激源:[具体说明]", "保留编号": 2, "保留理由": "[原因]"}],
-  "保留的候选编号列表": [1, 2]
-}
-```
-
-### Step 4: 三维度评分
-
-**Prompt**:
-```
-# 任务:评估灵感点
-
-## 待评分的灵感点
-{points_text}
-
-## 评分标准
-
-对每个灵感点从三个维度评分(每个维度1-10分):
-
-### 1. 人设契合度(默认先都给5分)
-- 因没有历史视频信息无法推算人设,先都给5分
-
-### 2. 触发可能性(1-10分)
-- 评估这个刺激点触发创作的可能性有多大
-- 考虑刺激点的吸引力强度
-- 考虑创作者遇到这个刺激时产生创作冲动的概率
-
-### 3. 内容解释力(1-10分)
-- 评估这个刺激点能否解释视频的核心内容
-- 判断从这个刺激点能否推导出视频的主要内容
-- 考虑刺激点与视频内容的关联强度
-
-**总分 = 人设契合度 + 触发可能性 + 内容解释力(范围:3-30分)**
-
-## 评分原则
-- 客观评分,不要预设分数高低
-- 如果与视频内容不符,触发可能性也会很低
-- 如果是视频核心特征,触发可能性反而很高
-
-## 输出格式(JSON)
-{
-  "评分结果列表": [
-    {
-      "候选编号": 1,
-      "人设契合度": 8,
-      "触发可能性": 7,
-      "内容解释力": 9,
-      "总分": 24,
-      "评分说明": "详细说明评分理由:为什么这个刺激点符合人设、触发创作的可能性如何、能否解释视频内容"
-    }
-  ]
-}
-```
-
-### Step 5: 排序筛选
-- 按总分降序排序,筛选总分>=15分的灵感点,最多保留2个
-
-### Step 6: 组装最终输出
-- 从原始候选点中提取完整字段,合并验证信息和评分信息
-
-**输出**: `inspiration_points` (列表格式)
-
----
-
-## 节点3: 目的点提取
-
-**功能**: 从创作者视角分析创作目的和动机
-
-**Prompt**:
-```
-# 任务:提取创作目的(视频逐帧分析)
-
-## 视频内容
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,捕捉以下维度的信息:
-- **画面内容**:场景、物体、人物、环境等视觉元素
-- **动作变化**:人物的动作、物体的运动、场景的转换
-- **时间线索**:视频的时间顺序、节奏变化、关键时间点
-- **视觉细节**:颜色、光影、构图、视觉焦点等
-- **音频信息**(如有):对话、音效、背景音乐等
-
-## 分析步骤
-1. **逐帧分析**:观察视频的每一帧或关键帧,理解视频的整体内容和结构
-2. **识别意图**:从视频的画面、动作、场景、对话等维度,推断创作者想让受众产生什么反应/影响(Why)
-3. **维度归类**:从标准分类体系中选择最匹配的维度(优先二级分类)
-4. **提炼目的点**:在维度下提炼具体目标(≤15字,聚焦最终效果,不写手段)
-
-## 重要约束
-- **每个二级分类最多1个目的点**(例如不能同时有2个"记录"维度的目的点)
-- **宁缺毋滥**:如果某个维度的目的不明确,不要强行生成
-- **总数控制**:最终输出1-3个目的点,不要为了凑数而硬造
-- **证据支撑**:每个目的点都必须有足够的视频内容支撑,不能臆测
-- **基于视频**:所有目的点必须基于视频的实际内容,不能凭空想象
-
-## 输出格式(JSON)
-{
-    "目的点列表": [
-        {
-            "维度": {
-                "一级分类": "",
-                "二级分类": ""
-            },
-            "目的点": "",
-            "描述": "",
-            "推理": "说明基于视频的哪些画面/动作/场景/对话等线索,推导出创作者的这个目的"
-        }
-    ]
-}
-```
-
-**输出**: `purpose_point` (包含perspective, purposes, total_count)
-
----
-
-## 节点4: 关键点提取
-
-**功能**: 从消费者视角提取核心吸引点
-
-**执行流程**: 5步提取流程
-
-### Step 1: 识别候选关键点
-
-**Prompt**:
-```
-# 任务:识别候选关键点(视频逐帧分析,扁平化列表)
-
-## 视频内容
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,从以下维度捕捉吸引点:
-- **画面内容**:场景、物体、人物、环境等视觉元素中的吸引点
-- **动作变化**:人物的动作、物体的运动、场景的转换等动态吸引点
-- **时间线索**:视频的时间顺序、节奏变化、关键时间点等时序吸引点
-- **视觉细节**:颜色、光影、构图、视觉焦点等视觉吸引点
-- **音频信息**(如有):对话、音效、背景音乐等听觉吸引点
-
-## 字段定义
-
-### 维度大类
-只能是"形式"或"实质"
-
-### 维度细分
-- 形式类:只能是"风格"或"关系"
-- 实质类:只能是"元素"或"分类"
-
-### 关键点
-≤15字核心发现(只写"是什么",不写"如何呈现")
-
-### 描述
-- **本质**: 讲清楚这个关键点是什么(吸引点的具体特征、形态、内容等客观信息)
-- **内容要求**:
-  - 描述吸引点本身的客观特征
-  - 说明这个吸引点的完整上下文
-  - 让读者能清晰理解"这个关键点到底是什么"
-- **严格禁止**:
-  - 不解释为什么吸引
-  - 不进行价值判断和主观评价
-  - 不分析效果和影响
-
-## 维度分类要求(二级分类体系)
-
-**维度大类:形式**
-- **风格**:关注内容的整体呈现方式、结构、格式等外在特征
-- **关系**:关注内容中各个实质要素之间的关联、逻辑关系
-
-**维度大类:实质**
-- **元素**:关注内容中的具体实体、对象、要素等
-- **分类**:关注对具体元素的抽象、总结、归纳
-
-## 输出(JSON)
-{
-  "候选关键点列表": [
-    {
-      "候选编号": 1,
-      "维度大类": "形式|实质",
-      "维度细分": "风格|关系|元素|分类",
-      "关键点": "核心发现",
-      "描述": "充分说明"
-    }
-  ]
-}
-
-要求:
-- 广泛收集:从视频的各个维度(画面、动作、场景、对话等)广泛收集吸引点
-- 客观陈述:只陈述事实,不做主观判断
-- 严格按照二级维度体系归类:必须明确标注维度大类(形式/实质)和维度细分(风格/关系/元素/分类)
-- 编号连续:候选编号从1开始连续编号
-- 基于视频:所有关键点必须基于视频的实际内容,不能凭空想象
-
-注意:此步骤输出扁平化列表,后续会构建层级关系
-```
-
-### Step 2: 第一轮去重(与灵感点、目的点去重)
-- 移除与灵感点/目的点重复的关键点
-
-### Step 3: 第二轮去重(关键点之间去重)
-- 移除本质相同的关键点,只保留一个
-
-### Step 4: 构建层级关系
-
-**Prompt**:
-```
-# 任务:构建关键点层级关系
-
-## 关键点列表(扁平化)
-{points_text}
-
-## 层级判断标准
-
-**层级关系定义**:
-- **大类 → 小类**:一个关键点是另一个关键点的具体类别
-- **抽象 → 具体**:一个关键点是另一个关键点的具体表现
-- **整体 → 局部**:一个关键点是另一个关键点的组成部分
-
-**判断方法**:
-1. 识别父子关系:子关键点是父关键点的细分、具体化或局部
-2. 构建树形结构:父节点可以有多个子节点,子节点也可以继续有子节点
-3. 确保无循环:不能出现A→B→A的情况
-
-**层级原则**:
-- 同一层级的关键点应该是并列关系(不存在包含、细分关系)
-- 不同层级的关键点应该是从属关系(父→子)
-- 一级关键点(根节点)应该是最抽象、最整体的关键点
-- 如果两个关键点没有明确的层级关系,则保持在同一层级
-
-## 输出(JSON)
-{
-  "层级分析": [
-    {
-      "父编号": 1,
-      "子编号列表": [2, 3],
-      "层级关系": "大类→小类|抽象→具体|整体→局部",
-      "关系说明": "说明为什么存在这个层级关系",
-      "子节点原因": {
-        "2": "说明编号2作为子节点的具体原因",
-        "3": "说明编号3作为子节点的具体原因"
-      }
-    }
-  ],
-  "一级关键点编号列表": [1, 4, 5]
-}
-```
-
-### Step 5: 组装最终输出
-- 构建树形结构的关键点列表
-
-**输出**: `key_points` (包含key_points树形列表, total_count, root_count)
-
----
-
-## 节点5: 选题理解
-
-**功能**: 整合视频内容和三点分析结果,理解选题策略
-
-**Prompt**:
-```
-# 输入数据
-
-## 视频内容 
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,从以下维度理解视频内容:
-- **画面内容**:场景、物体、人物、环境等视觉元素
-- **动作变化**:人物的动作、物体的运动、场景的转换
-- **时间线索**:视频的时间顺序、节奏变化、关键时间点
-- **视觉细节**:颜色、光影、构图、视觉焦点等
-- **音频信息**(如有):对话、音效、背景音乐等
-
-基于视频的实际内容,结合以下三点分析结果,理解选题策略。
-
-## 灵感点分析
-{inspiration_section}
-
-## 目的点分析
-{purpose_section}
-
-## 关键点分析
-{key_points_section}
-
----
-
-# 执行指令
-
-基于以上数据,输出选题策略分析。
-
-## 质量要求
-- **描述**: 必须是完整流畅的1-2句话,清晰展现完整逻辑链"灵感来源→关键手法→呈现效果→创作目的"
-  - **必须包含目的点**: 明确说明创作者的目的,不能只描述内容本身
-  - **必须体现三点内在关联**: 分析三点的有机融合逻辑,而非简单罗列
-  - **基于视频内容**: 所有分析必须基于视频的实际内容,不能凭空想象
-  - 拒绝笼统概念,要具体到本质特征
-  - 删除非核心修饰词,只保留最本质的词
-  - 禁用空洞词汇: "精心"、"独特"、"全面"、"旨在"、"充分"、"深度"、"夸张的"、"强烈的"、"明显的"
-
-**重要提醒**:
-- 不需要输出 `explicit_elements` 字段,系统会自动从三点数据中提取
-- 所有分析必须基于视频的实际内容和三点分析结果
-```
-
-**输出**: `topic_selection_understanding` (包含主题、描述、覆盖情况、explicit_elements)
-
----
-
-## 节点6: 段落划分
-
-**功能**: 分析视频的分段结构,理解创作者如何组织内容
-
-**Prompt**:
-```
-# 任务
-
-从**创作者视角**分析这个视频是如何组织内容的。
-
-## Section切分流程
-
-**第一步:识别主题显著变化位置**
-扫描整个视频,识别**主题发生显著变化**的位置:
-
-- **判断标准**:
-  * 语义跃迁: 讨论对象发生根本性改变
-  * 逻辑转换: 从"是什么"转向"为什么"或"怎么办"
-  * 功能变化: 从"问题陈述"转向"解决方案"
-  * 场景切换: 画面、场景、动作发生明显变化
-  * 对话转换: 话题、语气、情绪发生明显变化
-
-- **划分原则**:
-  * 避免过度细分(每个小变化都成为顶层段落)
-  * 避免过度粗放(将所有内容合并为1个顶层段落)
-  * 以"主题板块"而非"内容单元"为划分粒度
-
-**第二步:初步划分**
-- 基于主题显著变化位置进行划分
-- 支持主Section和子Section的层级结构
-- 需要对视频进行逐帧或关键帧分析,捕捉画面、动作、场景、对话等维度的结构变化
-
-**第三步:顺序验证与反思**
-- 检查每个Section内的视频片段是否保持时间顺序的连续性
-- 验证同一Section内的内容是否有语义关联
-- 确认段落之间的过渡是否自然合理
-
-## 层级要求
-
-**段落必须至少保留2层结构**:
-1. **第1层(抽象层)**:从具象中聚合出的共性维度
-2. **第2层(具象层)**:具体的内容细节
-
-**层级关系说明**:
-- 抽象层是对多个具象内容的归纳和提炼
-- 具象层是抽象层的具体展开
-- 每个抽象层下必须有至少1个具象层子项
-
-### 整体情绪价值(Emotion Value)
-**内容背后的情绪钩子或社会价值**。仅在有明显的升维价值时提取(如:民族自信、致青春、趋吉避凶、鞠躬尽瘁),否则留空。
-** 输出案例: "致青春", "友谊长存", "猎奇", "颠覆认知"等 6字以内的词语,可输出多个,用逗号分隔**
-
-## Section字段
-
-- 描述: 段落描述(共性维度名称;具体内容概括)
-- 内容范围: **列表格式,包含具体视频片段**
-  - 格式:["0:00-0:30", "0:30-1:15"] 或 ["片段1: 开场介绍", "片段2: 问题阐述"]
-  - 要求:必须包含具体的时间戳范围或视频片段描述,清晰标识该Section涵盖的视频内容
-- 推理依据: 为什么这样划分
-- 子项: 子Section列表(树状结构)
-
-# 输出(JSON)- 树状结构
-
-{
-  "内容品类": "内容品类",
-  "整体情绪分析": ["整体情绪分析1(6个字以内)", "整体情绪分析2(6个字以内)"],
-  "段落衔接机制": [
-    {
-      "from": "段落1",
-      "to": "段落2",
-      "衔接方式": "如何过渡到下一个段落",
-      "衔接语/画面特征": "衔接语或画面特征",
-      "设计意图": "设计意图"
-    }
-  ],
-  "段落列表": [
-    {
-      "描述": "共性维度名称",
-      "内容范围": ["0:00-0:30", "0:30-1:15"],
-      "推理依据": "为什么这样划分这个抽象层",
-      "子项": [
-        {
-          "描述": "具体内容概括",
-          "内容范围": ["0:00-0:30"],
-          "推理依据": "这个具象内容如何支撑上层抽象",
-          "子项": []
-        }
-      ]
-    }
-  ]
-}
-```
-
-**输出**: `内容品类`, `段落列表` (树状结构)
-
----
-
-## 节点7: 实质提取
-
-**功能**: 从视频内容中提取实质元素(具体元素、具象概念、抽象概念)
-
-**执行流程**: 8步提取流程
-
-### Step 1: 提取具体元素
-
-**Prompt**:
-```
-# 任务
-从视频中提取"具体元素"
-
-# 核心定义
-
-## 具体元素
-- **定义**:
-  -- 1.从视频画面中直接观察到的、可独立存在的**单一视觉实体对象**
-  -- 2.视频的背景音乐、音效等非口播内容的声音
-- **判断标准**:
-  -- 1.可以指着画面说"这是一个X"(单一、具体、可见的实体)
-  -- 2.有背景音乐、音效等非口播内容的声音,直接用"背景音乐/音效声"作为名称即可,不要重复提取
-- **示例**:
-  -- 1.胡萝卜、青椒、西兰花(每个都是单独的实体)
-  -- 2.背景音乐/音效声
-- **禁止**:
-  - 归类词(蔬菜、水果)
-  - 概念性名词(食物、植物、人)
-  - 文字内容(只关注视觉实体)
-
-## 提取原则(仅针对画面中的视觉实体对象)
-- 只从视频画面中提取,不关注文字
-- 每个元素必须是单一的、具体的视觉实体
-- 使用"剥离测试":去掉表达方式后,这个实体仍然存在
-
-# 命名规范
-- 原子性:单一原子名词,不可再拆分
-- 名词性:纯名词,严禁形容词、动词、副词
-- 具体性:直接指向可观察的实体
-
-# 输出json结构
-[
-    {
-      "id": "从1开始的自增序列",
-      "名称": "单一原子名词",
-      "描述": "说明这个元素是什么,外观特征",
-      "维度": {"一级": "实质", "二级": "具体元素"},
-      "来源": ["视频画面"],
-      "推理": "为什么识别这个具体元素"
-    }
-]
-```
-
-### Step 2: 提取具象概念
-
-**Prompt**:
-```
-# 任务
-从视频中提取"具象概念"
-
-# 核心定义
-## 具象概念
-- **定义**:视频画面内的文字或者口播内容中明确提到的完整名词
-
-## 排除的名称(来自第一步,仅用于排除)
-**禁止提取的名称**:{element_names_text}
-
-## 判断标准
-- **视频画面内的文字或者口播内容**中实际出现的**完整名词**
-- **不能是视频画面中出现的元素的名称等归类词**
-- 去掉表达方式后,这个概念仍然存在
-
-# 约束
-- 禁止通过语义推导、联想、理解得出的名词
-- **禁止归类词(蔬菜、水果、人等)**
-- **禁止使用第一步中已提取的具体元素名称**
-- 禁止拆分复合词
-- 禁止提取形容词、动词
-- 禁止提取谓语、定语、状语、补语
-- 禁止提取副词
-
-## 提取原则
-- **词语完整性**:必须提取完整的**名词**,不允许拆分复合词
-- **严格约束**:必须是**画面文字或者口播内容中实际出现**的完整名词
-- **严格的名词验证**(必须同时满足以下两个条件):
-   - 条件1:词性是名词(词典意义上的名词)
-   - 条件2:在当前上下文中作为名词使用(语境判断)
-
-   **验证方法**:
-   - 找到该词在视频画面内的文字或者口播内容中的具体位置
-   - 分析该词在句子中的语法成分和实际作用
-   - 判断:该词是否在这个语境中充当"事物/对象/概念"的角色?
-
-# 输出json结构
-[
-    {
-      "id": "从1开始的自增序列",
-      "名称": "字面原词(完整名词)",
-      "描述": "说明这个概念是什么",
-      "维度": {"一级": "实质", "二级": "具象概念"},
-      "来源": "HH:MM:SS",
-      "上下文验证": {
-        "原文位置": "该词在原视频画面内的文字或者口播内容中的具体句子",
-        "语法成分": "该词在句子中的语法成分(主语/宾语/定语中心语等)",
-        "语境判断": "说明该词在此语境中确实作为名词使用的理由"
-      },
-      "推理": "为什么这个名词被认为是具象概念"
-    }
-]
-```
-
-### Step 3: 总结抽象概念
-
-**Prompt**:
-```
-# 任务
-基于已提取的具体元素和具象概念,总结新的"抽象概念"
-
-# 已提取的具体元素
-{elements_text}
-
-# 已提取的具象概念
-{concepts_text}
-
-# 核心定义
-
-# 定义与分类
-**抽象概念**分两类:
-
-**类型1-上位抽象**:对具体元素/具象概念的归类
-**类型2-引申含义**:具体元素/具象概念无法直接表达的深层含义
-
-# 提取原则
-- 对具体元素/具象概念的归类
-- 具体元素和具象概念无法直接表达的深层含义
-- 基于归纳:基于已提取的具体元素/具象概念
-- 来源追溯:准确标明所有来源ID(具体元素ID、具象概念ID),必须完整可追溯
-
-# 命名规范
-- 有完整独立语义的概念
-- 单一原子名词,不可拆分
-- 纯名词,禁止形容词、动词、副词
-- 精准描述概念,不做修饰
-
-# 判断标准
-- 去掉表达方式后,概念仍存在
-
-# 输出json结构
-[
-    {
-      "id": "从1开始的自增序列",
-      "名称": "单一名词或短语",
-      "描述": "说明这个抽象概念是什么",
-      "维度": {"一级": "实质", "二级": "抽象概念"},
-      "类型": "上位抽象 | 引申含义",
-        "来源": {
-        "具体元素": [{"id":"具体元素-X", "名称":"具体元素-X的名称"}, {"id":"具体元素-Y", "名称":"具体元素-Y的名称"}],
-        "具象概念": [{"id":"具象概念-A", "名称":"具象概念-A的名称"}, {"id":"具象概念-B", "名称":"具象概念-B的名称"}]
-      },
-      "推理过程": "明确说明如何从上述来源(具体哪些元素ID和概念ID)推导出这个抽象概念",
-    }
-]
-```
-
-### Step 4: 共性分析
-- 统计每个实质元素在视频中的出现频次和段落覆盖率
-
-### Step 5: 多维度评分
-- 已废弃相似度计算逻辑,仅返回空结果
-
-### Step 5.1: 意图支撑评估
-- 评估实质元素对灵感点/目的点/关键点的支撑关系(基于视频画面和文字语境)
-
-### Step 6: 筛选
-- 筛选条件:出现频次>1 且 有意图支撑关系 且 段落覆盖率>30%(声音类型元素豁免)
-
-### Step 7: 分类
-- 按二级维度(具体元素/具象概念/抽象概念)分别进行分类
-
-### Step 8: 合并信息
-- 合并所有中间信息到最终元素
-
-**输出**: `substance_final_elements` (列表格式)
-
----
-
-## 节点8: 形式提取
-
-**功能**: 从视频内容中提取形式元素(具象概念形式、具体元素形式、整体形式)
-
-**执行流程**: 8步提取流程
-
-### Step 1: 提取具象概念形式
-
-**Prompt**:
-```
-# 任务
-从视频中提取"具象概念形式"元素(关注画面中的文字呈现方式和口播表达方式)
-
-# 已提取的具象概念(供参考,不允许从中直接抄写内容)
-{concepts_text}
-
-# 核心定义
-
-## 具象概念形式
-- 定义:对具象概念的描述、修饰、表达方式(包括画面文字呈现方式 + 口播表达方式)
-- 特征:表达方式本身,而不是概念内容
-- 包含:
-  - 对概念的强调方式(如:反复重复、放大字号、加重语气)
-  - 画面文字呈现方式(如:字体、颜色、大小、位置、动画等)
-  - 口播语气特性(如:教学口吻、科普式表达、聊天式口吻、语气特征、语速、停顿等)
-  - 口播内容的修饰手法(如:夸张、比喻、反问、排比)
-  - 口播内容的叙述方式(如:第一人称、第二人称、对话体)
-  - 口播内容的节奏特征(如:语速快慢、停顿设计、重音、语气变化等)
-
-## 提取原则
-1. 只提取画面文字展示方式和口播表达方式,不涉及视觉呈现
-2. 必须依附于已提取的具象概念
-3. 使用"剥离测试":去掉表达手法后,概念本身仍存在
-4. **原子化命名**:形式名称必须是最原子的,不包含实质内容
-5. **建立关联关系**:通过"支撑"字段建立形式与实质的关联边
-6. **段落级别**:形式作用域至少是整个段落,不能仅针对单句
-
-## 作用域约束(重要)
-1. **段落级别作用域**(核心约束):
-   - 形式的作用域至少是**整个段落级别**,不能仅针对段落中的某一句话
-   - 必须是该段落的整体画面文字呈现方式和口播表达方式,而非局部单句的特征
-2. **同段落作用域**:形式必须与其支撑的具象概念在同一段落中
-3. **事实对应关系**:形式对概念的支撑必须在视频中有明确的对应关系
-   - 不能仅凭推测建立支撑关系,必须实际看到这种支撑
-
-# 输出json结构
-[
-  {
-    "id": "从1开始的自增序列",
-    "名称": "单一名词或短语(必须是原子化的形式名称)",
-    "描述": "说明形式的本质是什么,以及在视频中的具体定义",
-    "维度": {"一级": "形式", "二级": "具象概念形式"},
-    "支撑": [{"id":"具象概念id", "名称":"具象概念名称"}],
-    "推理": "在文字中的具体体现方式(引用视频中的说法或呈现方式)",
-    "段落来源": ["段落ID1", "段落ID2"]
-  }
-]
-```
-
-### Step 2: 提取具体元素形式
-
-**Prompt**:
-```
-# 任务
-从视频中提取"具体元素形式"元素
-
-# 已提取的具体元素
-{elements_text}
-
-# 已提取的具象概念
-{concepts_text}
-
-# 核心定义
-
-## 具体元素形式
-- **定义**:视觉画面的形式,支撑具体元素、具象概念的视觉呈现以及背景音乐、音效等非口播内容的声音呈现方式
-- **特征**:图像的视觉表现手法、外观特征或者背景音乐和音效的类别、风格、节奏等
-- **范围**:构图、色彩、光影、质感、视觉技巧等图片层面的特征或者声音方面的特征
-- **重要**:不关注画面里的文字以及口播内容说了什么,只关注视觉呈现以及声音呈现方式
-
-## 提取原则
-1. **视觉性**:必须是画面层面或者背景音乐音效层面的形式,而非文字层面或者口播内容层面
-2. **边界明确**:不关注画面里的文字以及口播内容,只关注视觉呈现手法或者声音呈现方式
-3. **支撑性**:可以支撑具体元素,也可以支撑具象概念
-4. **语义完整性**:必须提取具有完整独立语义的形式特征
-5. 使用"剥离测试":去掉表达方式后,元素本身仍存在
-6. **段落级别**:形式作用域至少是整个段落,不能仅针对单个元素
-
-## 作用域约束(重要)
-1. **段落级别作用域**(核心约束):
-   - 形式的作用域至少是**整个段落级别**,不能仅针对段落中的某一句话
-   - 必须是该段落的整体呈现方式,而非局部单个元素的特征
-2. **同段落作用域**:形式必须与其支撑的实质元素/概念在同一段落中
-3. **事实对应关系**:形式对实质的支撑必须在视频中有明确的视觉对应关系
-   - 不能仅凭推测建立支撑关系,必须在画面中实际看到这种支撑
-
-# 命名与关联分离原则(重要)
-
-## 核心规则
-- **名称**:只描述形式手法本身(纯形式词汇)
-- **支撑**:说明这个形式手法应用在哪些实质内容上(通过id关联具体元素/具象概念)
-- **推理**:说明具体如何体现(在图像中的呈现细节或者声音方面的呈现方式)
-- **段落来源**:记录形式来自哪个段落
-
-## 关键要求
-1. **名称的纯粹性**:名称中不能包含任何实质内容(具体元素名称、具象概念名称等)
-2. **通过支撑建立关联**:实质内容通过"支撑"字段的id关联,而非写在名称中
-3. **推理的具体性**:在推理中详细说明这个形式手法如何应用在支撑的实质内容上
-4. **来源的准确性**:图片来源和段落来源必须与支撑的实质元素/概念一致
-
-# 输出json结构
-[
-    {
-      "id": "从1开始的自增序列",
-      "名称": "单一名词或短语(必须是原子化的纯形式词汇)",
-      "描述": "说明形式的本质及在帖子中的具体定义",
-      "维度": {"一级": "形式", "二级": "具体元素形式"},
-      "支撑": {
-        "具体元素": [{"id":"元素id", "名称":"元素名称"}],
-        "具象概念": [{"id":"概念id", "名称":"概念名称"}]
-      },
-      "推理": "在画面中或者声音方面的具体体现(说明这个形式手法如何应用在支撑的实质内容上)",
-      "段落来源": ["段落ID1", "段落ID2"]
-    }
-]
-```
-
-### Step 3: 提取整体形式
-
-**Prompt**:
-```
-# 任务
-从视频中提取"整体形式"元素
-
-# 已提取的具体元素
-{elements_text}
-
-# 已提取的具象概念
-{concepts_text}
-
-# 已提取的抽象概念
-{abstracts_text}
-
-# 核心定义
-
-## 整体形式
-- 定义:多模态内容的组合方式、结构特征、整体呈现策略
-- 特点:不针对单个元素/概念,而是整体组织方式
-
-## 提取原则
-1. 整体性:必须是整体层面的特征,不是局部细节
-2. 形式性:使用"剥离测试" - 去掉表达方式后内容失去结构
-3. 可命名性:能用简洁的名词/短语概括
-4. 避免重复:与具体元素形式、具象概念形式区分开
-7. **宏观层面**:作用范围至少是整个段落或多段落,不能是单句或局部内容
-
-## 作用域约束(重要)
-1. **段落级别作用域**(核心约束):
-   - 形式的作用域至少是**整个段落级别**,不能仅针对段落中的局部内容
-   - 整体形式通常作用于整个帖子或多个段落,必须是宏观层面的特征
-2. **支撑关系**:整体形式支撑特定实质元素时,必须记录这些元素所在的段落
-3. **来源标注**:标注整体形式主要体现在哪些段落中
-
-## 常见维度参考(不限于此)
-- 多模态组合:画面+口播+字幕的配比、关系、时序、模态切换
-- 结构方式:内容组织逻辑(并列/递进/对比/因果/总分/时间/空间顺序等)
-- 呈现策略:整体表达风格(教程式/故事式/问答式/列表式/对话式等)
-- 节奏控制:信息密度、镜头节奏、高潮设计
-- 视角设计:叙述视角(第一人称/第二人称/第三人称)
-- 互动方式:对观众的提问、召回、引导
-
-# 输出json结构
-[
-  {
-    "id": "从1开始的自增序列",
-    "名称": "单一名词或短语(简洁准确)",
-    "描述": "说明形式的本质是什么,以及在视频中的具体定义",
-    "维度": {"一级": "形式", "二级": "整体形式"},
-    "支撑": {
-      "具体元素": [{"id":"元素id1", "名称":"元素名称1"}, {"id":"元素id2", "名称":"元素名称2"}],
-      "具象概念": [{"id":"概念id1", "名称":"概念名称1"}, {"id":"概念id2", "名称":"概念名称2"}]
-    },
-    "推理": "在视频中的具体体现(结合镜头、节奏、结构说明)",
-    "段落来源": ["段落ID1", "段落ID2"]
-  }
-]
-```
-
-### Step 4: 共性分析
-- 统计每个形式元素在视频中的出现频次
-
-### Step 5: 支撑判断
-- 评估形式元素对灵感点/目的点/关键点的支撑关系(按维度分别评估)
-
-### Step 6: 权重计算
-- 纯代码实现,不调用LLM。动态自适应权重策略(总分100分):共性维度50分(频次40% + 覆盖段落数30% + 覆盖率30%)+ 支撑维度50分(灵感点20分 + 目的点15分 + 关键点15分)
-
-### Step 7: 筛选
-- 筛选条件:权重分>=40
-
-### Step 8: 分类
-- 按二级维度(具象概念形式/具体元素形式/整体形式)分别进行分类
-
-**输出**: `form_final_elements` (列表格式)
-
----
-
-## 节点9: 分离结果
-
-**功能**: 将实质和形式结果分离到独立字段
-
-**执行过程**: 
-- 从state中获取`substance_final_elements`和`form_final_elements`
-- 分别存储到`实质列表`和`形式列表`字段
-
-**输出**: `实质列表`, `形式列表`
-
----
-
-## 节点10: 结果汇总
-
-**功能**: 将所有解构结果汇总为最终的JSON结构
-
-**执行过程**: 
-- 提取视频信息(URL、标题、正文)
-- 汇总三点解构结果(灵感点、目的点、关键点)
-- 汇总选题理解结果
-- 汇总脚本理解结果(内容品类、段落列表、实质列表、形式列表)
-- 验证JSON格式
-
-**输出**: `final_result` (包含视频信息、三点解构、选题理解、脚本理解)
-
----
-
-## 总结
-
-整个解码工作流通过10个节点完成视频的全面分析:
-1. **视频上传**: 下载并上传视频到Gemini
-2. **三点解构**: 提取灵感点、目的点、关键点
-3. **选题理解**: 整合三点分析结果,理解选题策略
-4. **脚本理解**: 段落划分、实质提取、形式提取
-5. **结果汇总**: 将所有结果组装为最终JSON结构
-
-每个节点都基于视频逐帧分析,确保提取的信息准确且全面。
-

+ 0 - 58
doc/command

@@ -1,58 +0,0 @@
-# 视频解构工作流
-
-## 单个视频测试
-# 视频输入格式:目录下需要有"视频详情.json"文件
-python3 examples/run_test.py single 56898272
-python3 examples/run_test.py single <视频目录名>
-
-## 批量处理模式
-# 批量处理作者历史帖子目录下的所有视频
-python3 examples/run_test.py batch <目录名>
-
-## 输入格式说明
-# 视频详情.json 文件格式:
-{
-    "channel_content_id": "56898272",
-    "video": "http://...",  # 视频URL(必需)
-    "title": "...",         # 视频标题
-    "body_text": "",        # 视频描述(可选)
-    "publish_timestamp": "",
-    "publish_time": ""
-}
-
-## 输出结果
-# 结果保存在:<目录名>/output/result_<时间戳>.json
-# 包含:
-# - 视频信息
-# - 三点解构(灵感点、目的点、关键点)
-# - 选题理解
-
-
-## 可视化
-### 单项目 - 输出到项目的output目录
-python3 examples/visualize_script_results.py --dir 阿里多多酱
-python3 examples/visualize_script_results.py --dir G88818
-python3 examples/visualize_script_results.py --dir 瓜尔纤维营养膳食
-python3 examples/visualize_script_results.py --dir 多说无益力多
-
-### 多项目 - 输出到根目录
-python3 examples/visualize_script_results.py
-
-
-#  抓文章
-# 基本用法(默认启用自动获取详情,正文>90字触发)
-python3 examples/fetch.py posts 564d2347c2bdeb734d1ea79f -m 3
-python3 examples/fetch.py posts 659e182d000000002001d802 -m 3
-python3 examples/fetch.py posts 645b9a60000000001f031324 -m 3
-python3 examples/fetch.py posts 599533256a6a693ab77f1abf -m 3
-python3 examples/fetch.py posts 6795dabc000000000403acaa -m 3
-python3 examples/fetch.py posts 5aa570f511be106d5fbd9fb4 -m 3
-
-# 禁用自动获取详情
-python3 examples/fetch.py posts 599533256a6a693ab77f1abf -m 3 --no-auto-detail
-
-# 自定义字数阈值(例如120字)
-python3 examples/fetch.py posts 599533256a6a693ab77f1abf -m 3 --detail-threshold 120
-
-# 获取单个帖子详情
-python3 examples/fetch.py detail <note_id>

+ 0 - 295
doc/record

@@ -1,295 +0,0 @@
-所有输出不要在通过prompt输出了 应该在每一步返回的结果中,通过代码进行关联 
-
-⏺ 你说得非常对!这是一个架构优化的重要点。
-
-  当前问题
-
-  现状:每一步都通过 prompt 让 LLM 输出完整信息
-  # Step 5 prompt 中要求 LLM 输出
-  {
-    "名称": "实质点名称",
-    "类型": "元素/概念",
-    "支撑灵感点得分": 8,
-    "支撑灵感点依据": "...",
-    ...
-  }
-
-  问题:
-  - ❌ 前面步骤的信息(识别依据、共性分析数据等)需要 LLM 重新输出
-  - ❌ LLM 可能遗漏或修改前面步骤的数据
-  - ❌ 数据不一致的风险
-  - ❌ 浪费 token
-
-  ---
-  优化方案:代码关联数据
-
-  原则
-
-  每一步只让 LLM 输出该步骤新增的信息,其他信息通过代码合并
-
-  示例
-
-  Step 2/3 - 提取
-
-  # LLM 只输出
-  {
-    "实质点列表": [
-      {
-        "名称": "酱料",
-        "类型": "元素",
-        "识别依据": "..."
-      }
-    ]
-  }
-
-  Step 4 - 共性分析
-
-  # LLM 只输出新增的分析数据
-  llm_result = {
-    "分析结果": [
-      {
-        "名称": "酱料",
-        "出现段落数": 3,
-        "段落覆盖率": 0.75,
-        ...
-      }
-    ]
-  }
-
-  # 代码合并:将 Step 2/3 的数据关联进来
-  analyzed_items = []
-  for analysis in llm_result["分析结果"]:
-      # 找到原始实质点
-      original = find_by_name(all_substantials, analysis["名称"])
-      # 合并数据
-      analyzed_items.append({
-          **original,  # 保留原始的 名称、类型、识别依据
-          **analysis   # 添加新的分析数据
-      })
-
-  Step 5 - 评分
-
-  # LLM 只输出评分
-  llm_result = {
-    "评分结果": [
-      {
-        "名称": "酱料",
-        "支撑灵感点得分": 8,
-        "支撑灵感点依据": "...",
-        ...
-      }
-    ]
-  }
-
-  # 代码合并
-  scored_items = []
-  for score in llm_result["评分结果"]:
-      # 找到分析数据
-      analyzed = find_by_name(analyzed_items, score["名称"])
-      # 合并
-      scored_items.append({
-          **analyzed,  # 保留前面的所有数据
-          **score      # 添加评分数据
-      })
-
-  Step 7 - 去重
-
-  # LLM 只输出去重决策
-  llm_result = {
-    "去重决策": [...],
-    "保留列表": ["酱料", "米饭"]  # 只返回名称
-  }
-
-  # 代码执行去重
-  deduplicated_items = []
-  for name in llm_result["保留列表"]:
-      # 找到完整数据
-      item = find_by_name(filtered_items, name)
-      deduplicated_items.append(item)  # 保留所有历史数据
-
-  Step 8 - 整合
-
-  # LLM 只输出树状结构(只包含名称和分类)
-  llm_result = {
-    "元素列表": [
-      {
-        "名称": "调味品",
-        "描述": "...",
-        "子项": [
-          {"名称": "酱料"}  # 只有名称
-        ]
-      }
-    ]
-  }
-
-  # 代码填充完整数据
-  def enrich_tree(node, data_source):
-      if "子项" in node and node["子项"]:
-          for child in node["子项"]:
-              if "名称" in child and "描述" not in child:
-                  # 这是叶子节点,填充完整数据
-                  full_data = find_by_name(data_source, child["名称"])
-                  child.update(full_data)
-              else:
-                  # 递归处理
-                  enrich_tree(child, data_source)
-
-  final_result = llm_result
-  enrich_tree(final_result, deduplicated_items)
-
-  ---
-  优势
-
-  1. ✅ 数据一致性:前面步骤的数据不会被 LLM 修改或遗漏
-  2. ✅ 节省 token:LLM 只输出新增信息
-  3. ✅ 职责清晰:LLM 负责判断,代码负责数据管理
-  4. ✅ 减少依赖:后续步骤不需要传入 text_data/images,因为历史数据已包含上下文
-
-  ---
-  是否要现在重构?
-
-  这是一个较大的架构改动,需要修改所有 8 个步骤。
-
-  建议:
-  1. 先确认这个方向是否符合你的需求
-  2. 如果确认,我可以逐步重构每个步骤
-
-  你觉得这个方案如何?
-
-> 开始优化吧 
-
-⏺ 好的!我开始优化。让我先创建任务列表,然后逐步重构每个步骤。
-
-⏺ 现在开始重构 Step 4。让我先读取相关代码:
-
-⏺ Read(src/components/agents/script_element_extraction_agent.py)
-  ⎿  Read 160 lines
-
-⏺ 现在开始重构 Step 4。我将修改为:
-  1. LLM 只输出新增的共性分析数据(不包含名称、类型、识别依据)
-  2. 代码合并 Step 2/3 的原始数据
-
-⏺ Update(src/components/agents/script_element_extraction_agent.py)
-  ⎿  Updated src/components/agents/script_element_extraction_agent.py with 12 additions and 8       
-     removals
-       681    
-       682    对每个实质点进行**共性定量分析**(统计基础数据)。
-       683    
-       684 +  **重要**:只输出新增的分析数据,不要重复输出名称、类型、识别依据等已有信息。
-       685 +  
-       686    ## 元素的共性分析(仅针对类型=元素)
-       687    
-       688    1. **出现段落数**:该元素在多少个段落中出现(整数)
-     ...
-       700    3. **核心概念识别**:在每个出现的段落中是主要概念还是辅助概念
-       701       - 主要概念:该概念是段落的核心主题
-       702       - 辅助概念:该概念是段落的补充说明
-       703 -  4. **概念强度**:在每个段落中的重要性权重(0-10分)
-       703 +  4. **概念强度详情**:在每个段落中的重要性权重(0-10分)
-       704       - 9-10分:段落核心概念,定义段落主题
-       705       - 7-8分:段落重要概念,有明显作用
-       706       - 5-6分:段落一般概念,有一定作用
-     ...
-       711    
-       712    - 定量指标必须准确计算
-       713    - 所有分析必须有客观依据
-       714 +  - **只输出名称和对应的分析数据,不要重复已有字段**
-       715    
-       716    # 输出(JSON)
-       717    
-     ...
-       718      "分析结果": [
-       719        {{
-       720          "名称": "实质点名称",
-       721 -        "类型": "元素/概念",
-       722 -  
-       723 -        // 元素类型的共性分析
-       721 +        // 元素类型只输出以下字段
-       722          "出现段落数": 3,
-       723          "段落覆盖率": 0.75,
-       724          "出现频次": 5,
-       725          "视觉显著性": 8,
-       726 -        "视觉显著性依据": "在图片中的位置和显著程度描述",
-       727 -  
-       728 -        // 概念类型的共性分析
-       726 +        "视觉显著性依据": "在图片中的位置和显著程度描述"
-       727 +      }},
-       728 +      {{
-       729 +        "名称": "实质点名称",
-       730 +        // 概念类型只输出以下字段
-       731          "出现段落数": 3,
-       732          "段落覆盖率": 0.75,
-       733          "核心概念识别": [
-     ...
-       749    
-       750    **注意**:
-       751    - 元素类型的实质点,输出元素相关的共性分析字段
-       752 -  - 概念类型的实质点,输出概念相关的共性分析字段"""
-       752 +  - 概念类型的实质点,输出概念相关的共性分析字段
-       753 +  - 不要输出类型、识别依据等字段,这些会由代码自动合并
-       754    
-       755            # 构建多模态消息
-       756            message_content = [{"type": "text", "text": prompt}]
-
-⏺ 现在添加代码合并逻辑:
-
-⏺ Update(src/components/agents/script_element_extraction_agent.py)
-  ⎿  Updated src/components/agents/script_element_extraction_agent.py with 29 additions and 2       
-     removals
-       771            ]
-       772    
-       773            # 调用LLM
-       774 -          result = LLMInvoker.safe_invoke(
-       774 +          llm_result = LLMInvoker.safe_invoke(
-       775                self,
-       776                "共性定量分析",
-       777                messages,
-       778                fallback={"分析结果": []}
-       779            )
-       780    
-       781 +          # 代码合并:将原始实质点数据与分析数据合并
-       782 +          analyzed_items = []
-       783 +          analysis_results = llm_result.get("分析结果", [])
-       784 +  
-       785 +          for analysis in analysis_results:
-       786 +              analysis_name = analysis.get("名称", "")
-       787 +              # 找到对应的原始实质点
-       788 +              original = None
-       789 +              for s in all_substantials:
-       790 +                  if s.get("名称") == analysis_name:
-       791 +                      original = s
-       792 +                      break
-       793 +  
-       794 +              if original:
-       795 +                  # 合并数据:原始数据 + 分析数据
-       796 +                  merged_item = {
-       797 +                      **original,  # 保留原始的名称、类型、识别依据
-       798 +                      **{k: v for k, v in analysis.items() if k != "名称"}  # 
-           + 添加分析数据(排除名称)
-       799 +                  }
-       800 +                  analyzed_items.append(merged_item)
-       801 +              else:
-       802 +                  logger.warning(f"⚠️  未找到实质点 '{analysis_name}' 的原始数据")
-       803 +                  # 如果找不到原始数据,仍然保留分析结果
-       804 +                  analyzed_items.append(analysis)
-       805 +  
-       806 +          result = {"分析结果": analyzed_items}
-       807 +  
-       808            # 打印结果
-       809            import json
-       810 -          logger.info(f"Step 4 
-           -  结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-       810 +          logger.info(f"Step 4 合并后
-           +  结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-       811    
-       812            return result
-       813    
-
-⏺ 完成 Step 4!现在继续 Step 5:
-
-  ✻ 修改 Step 5… (esc to interrupt · ctrl+t to hide todos · 142s · ↓ 2.9k tokens)
-  ⎿  ☒ Step 4: 修改为只输出新增的共性分析数据,代码合并 Step 2/3 的原始数据
-     ☐ Step 5: 修改为只输出评分数据,代码合并 Step 4 的分析数据
-     ☐ Step 6: 修改筛选逻辑,基于完整数据筛选
-     ☐ Step 7: 修改为只输出去重决策,代码执行去重并保留完整数据,移除 text_data 参数
-     ☐ Step 8: 修改为只输出树状结构框架,代码填充完整数据,移除 text_data 参数

+ 0 - 33
doc/脚本.md

@@ -1,33 +0,0 @@
-## 产品需求文档 (PRD)
-
-### 项目名称:**【执行脚本生成器】** 
-
-### V1.0 目标
-
-根据用户定义的**独立属性维度(X轴)**和**流程顺序(Y轴)**,生成一个**行式/清单化**的通用执行框架。
-
-### 核心功能与工作流(X轴泛化)
-
-| ID | 功能模块 | 功能描述 (What it does) | 对应框架逻辑 |
-| :--- | :--- | :--- | :--- |
-| **F1** | **X 轴提取:独立属性维度** | **(新 X 轴)** 引导用户定义**横向的、所有场景/流程都需要记录的独立属性**。这些属性将构成最终清单的**列标题**。 | 框架的**列**(Columns) |
-| **F2** | **Y 轴提取:顺序/流程单元** | **(新 Y 轴)** 引导用户定义**纵向的、按时间或流程顺序排列的最小单元**。这将成为清单的**每一行**。 | 框架的**行**(Rows) |
-| **F3** | **框架生成与输出** | 将 X 轴属性作为列标题,生成一个可供填写的空白清单框架。 | **最终产物** |
-
----
-
-### 产品流程 (Workflow V1.0 - 执行清单,泛化版)
-
-| 步骤 | 操作(用户输入) | 系统反馈/输出 |
-| :--- | :--- | :--- |
-| **1. X 轴提取** | **系统引导:** “请定义一个流程单元需要记录的**所有核心独立属性**(例如:负责人、预算、状态等)。**这将成为清单的每一列。**” | **用户输入:** 例如:**属性 A、属性 B、属性 C**。 |
-| **2. Y 轴提取** | **系统引导:** “请确定您计划记录的**单元总数 (N)** 或定义**一个基础流程单元**(例如:任务编号、镜号、时间节点)。**这将成为清单的每一行。**” | **系统记录:** **行** 的数量和基础单元。 |
-| **3. 框架生成** | 自动将步骤 1 的结果作为**列标题**,步骤 2 的结果作为**行内容**,生成一个空白的、**N 行 x M 列**的通用执行清单。 | **输出:** **通用执行框架模板**。 |
-| **4. 验证/输出** | **系统执行:** 逻辑校验(确保行数和列数已定义)。 | **输出:** **【框架生成完成】**。 |
----
-
-### 验收标准 (Acceptance Criteria)
-
-1.  **行式结构:** 最终输出必须是以**“行”**为基本单元的、可用于指导执行的清单。
-2.  **轴向匹配:** X 轴为**属性(列)**,Y 轴为**顺序(行)**。
-3.  **普适性:** X 轴定义不局限于任何特定行业或媒介类型。

+ 0 - 35
doc/脚本点

@@ -1,35 +0,0 @@
-1. 概念也是实质的名词
-2. 形式 是 动词、形容词
-3. 把类型写出来
-4. 点的呈现 按照权重 从上往下排
-5. 逻辑优雅,结果优雅
-6. 可视化:区分 概念 和 元素
-
-
-实质点提取流程:
-1. 找实质的点,包括 概念 和 元素,都是名词
-  - 概念:抽象的,不能是形式的,例如:一张图描述了做菜的步骤,虽然没有提到“菜谱”两个字,但是也应该提取出“菜谱”/“教程”等
-  - 元素:具体的,客观存在的实体,视觉可见,例如:虾、芒果、碗等
-2. 支撑性约束
-  - 支撑灵感点、目的点、关键点
-  - 支撑划分的段落,在买个段落的权重
-  - 每个支撑维度都要有分数,并给出客观的,直接的依据
-3. 共性判断:元素在多少段出现
-  - 概念:[帮我设计定量分析]
-  - 元素:[帮我设计定量分析]
-
-1. 判断帖子倾向:对于帖子来说,概念和元素谁更重要
-  2. 主视角提取:从更重要的视角提取(概念或元素)
-  3. 辅视角提取:从另一个视角提取,并与第2步去重
-  4. 多维度支撑评分:对所有实质点评分(4个维度)
-  5. 共性定量分析:元素和概念的共性分析
-  6. 筛选:基于评分筛选核心实质点
-  7. 去重:应用特殊规则去重
-  8. 整合:生成树状结构
-
-
-  ✻ 修改 Step 1… (esc to interrupt · ctrl+t to hide todos)
-  ⎿  ☐ 修改 Step 1: 移除 section_division
-     ☐ 修改 Step 5: 移除 topic_description 和 section_division
-     ☐ 修改 Step 7: 移除 topic_description 和 images
-     ☐ 修改 Step 8: 移除 topic_description 和 images

BIN=BIN
examples/.DS_Store


+ 0 - 7
examples/demo.json

@@ -1,7 +0,0 @@
-[
-    {
-        "video_id": "61626151",
-        "video_url": "https://rescdn.yishihui.com/pipeline/video/f522fd33-1556-4928-ab5a-c5afdd3c9688.mp4",
-        "title": "🔴退伍军人二次入伍的感人画面!若有战,召必回"
-    }
-]

+ 0 - 108
examples/demo_default.json

@@ -1,108 +0,0 @@
-[
-    {
-        "channel_content_id": "61626151",
-        "video": "https://rescdn.yishihui.com/pipeline/video/f522fd33-1556-4928-ab5a-c5afdd3c9688.mp4",
-        "title": "🔴退伍军人二次入伍的感人画面!若有战,召必回"
-    },
-    {
-        "channel_content_id": "53009047",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20250517/93cac5eae2b371328a163f3208a22d10.mp4",
-        "title": "人到晚年,看看咱年轻时的模样!😊"
-    },
-    {
-        "channel_content_id": "58840748",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20250929/ce8968e2f346103b83b75c0c8100028e.mp4",
-        "title": "🔴😂笑死了!让狗咬了,还要粘住嘴"
-        
-    },
-    {
-        "channel_content_id": "31877786",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20241009/57463791OoDiuHeqBZlhyJJ4vt.mp4",
-        "title": "⭕九九重阳节,将出现“双月同天”的天文景象‌"
-        
-    },
-    {
-        "channel_content_id": "52088667",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20240810/66085350uUqFZIg77FJLAzJJpY.mp4",
-        "title": "退休人员收到告示书"
-        
-    },
-    {
-        "channel_content_id": "24076827",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20250811/57463792oH5RnCGJ9AIZQSHPLl.mp4",
-        "title": "💖参观这个纪念馆,永远怀念!"
-        
-    },
-    {
-        "channel_content_id": "22847469",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20240725/66055630tfcs46gemRBilN8aZ.mp4",
-        "title": "⭕这个地方,竟如此好!一妻多夫的村"
-        
-    },
-    {
-        "channel_content_id": "23698074",
-        "video": "https://rescdn.yishihui.com/jq_oss/video/2024081407060094959.mp4",
-        "title": "🔴中国大地原点竟藏在这!"
-        
-    },
-    {
-        "channel_content_id": "43766016",
-        "video": "https://rescdn.yishihui.com/jq_oss/video/2024112806543168430.mp4",
-        "title": "🔴习惯不拔充电器的朋友们,安全意识要加强哦!"
-        
-    },
-    {
-        "channel_content_id": "23404708",
-        "video": "https://rescdn.yishihui.com/jq_oss/video/2024080817321542543.mp4",
-        "title": "神秘的北纬23度,带你揭晓这里的奥秘!"
-        
-    },
-    {
-        "channel_content_id": "25456860",
-        "video": "https://rescdn.yishihui.com/jq_oss/video/2024090216401994174.mp4",
-        "title": "🔴人生感悟,真实故事改编,太真实了"
-        
-    },
-    {
-        "channel_content_id": "24275855",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20240820/70705308c3paVy7kgNE6KXMXoT.mp4",
-        "title": "这是一个不需要门票的地方"
-        
-    },
-    {
-        "channel_content_id": "23311627",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20250910/574637913aZnx0okHx3excA68D.mp4",
-        "title": "中国人竟能把大桥建到天上,穿入云中"
-        
-    },
-    {
-        "channel_content_id": "33743665",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20241021/48355591eMOnTrv7hLcGrgz8u7.mp4",
-        "title": "🏮昔日巨贪宅院,奢华尽显。"
-        
-    },
-    {
-        "channel_content_id": "22789151",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20240723/66055630rR3SogG7TFD3efUVv.mp4",
-        "title": "⭕这才是中国悲壮的烈士陵园,没有人能够平静看完"
-        
-    },
-    {
-        "channel_content_id": "28180934",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20240919/574638633XYVdqtwcowtQNGxaC.mp4",
-        "title": "🔴这才是活明白了!句句大实话"
-        
-    },
-    {
-        "channel_content_id": "42930445",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20241122/57463797ASoQbQKUg3Dn7qLu6n.mp4",
-        "title": "穿什么裤子最显年轻?"
-        
-    },
-    {
-        "channel_content_id": "56898272",
-        "video": "https://rescdn.yishihui.com/longvideo/transcode/video/vpc/20250813/8ea91f5604305a1b61ca1daeab376061.mp4",
-        "title": "🔴中国12个奇怪村庄,最后一个许多人都想去"
-        
-    }
-]

BIN=BIN
examples/html/.DS_Store


+ 0 - 1931
examples/html/visualize/script.js

@@ -1,1931 +0,0 @@
-// 全局元素索引(将由Python动态注入)
-// const elementIndex = {};
-
-// 卡片展开/收起功能
-function toggleCardDetails(cardId) {
-    const detailsElement = document.getElementById(cardId + '-details');
-    const cardElement = document.querySelector('[data-card-id="' + cardId + '"]');
-    
-    if (!detailsElement || !cardElement) {
-        return;
-    }
-    
-    // 切换显示/隐藏
-    if (detailsElement.style.display === 'none' || !detailsElement.style.display) {
-        detailsElement.style.display = 'block';
-        cardElement.classList.add('expanded');
-        // 更新图标
-        const toggleIcon = cardElement.querySelector('.toggle-icon');
-        if (toggleIcon) {
-            toggleIcon.textContent = '▲';
-        }
-    } else {
-        detailsElement.style.display = 'none';
-        cardElement.classList.remove('expanded');
-        // 更新图标
-        const toggleIcon = cardElement.querySelector('.toggle-icon');
-        if (toggleIcon) {
-            toggleIcon.textContent = '▼';
-        }
-    }
-}
-
-// 显示元素详情模态框
-function showElementDetail(elementId) {
-    const elem = elementIndex[elementId];
-    if (!elem) return;
-
-    // 创建模态框
-    const modal = document.createElement('div');
-    modal.className = 'element-modal-backdrop';
-    modal.onclick = function(e) {
-        if (e.target === modal) {
-            document.body.removeChild(modal);
-        }
-    };
-
-    // 创建模态框内容
-    const modalContent = document.createElement('div');
-    modalContent.className = 'element-modal-content';
-    modalContent.onclick = function(e) {
-        e.stopPropagation();
-    };
-
-    // 构建内容
-    let html = '<div class="modal-header">';
-    html += '<h3>' + elem.name + '</h3>';
-    html += '<button class="modal-close" onclick="this.closest(\'.element-modal-backdrop\').remove()">×</button>';
-    html += '</div>';
-
-    html += '<div class="modal-body">';
-
-    // 描述
-    if (elem.description) {
-        html += '<div class="modal-section">';
-        html += '<strong>描述:</strong>';
-        html += '<p>' + elem.description + '</p>';
-        html += '</div>';
-    }
-
-    // 类型和维度
-    html += '<div class="modal-section">';
-    html += '<strong>类型:</strong> ' + elem.type;
-    if (elem.dimension && elem.dimension.一级) {
-        html += ' / ' + elem.dimension.一级;
-        if (elem.dimension.二级) {
-            html += ' / ' + elem.dimension.二级;
-        }
-    }
-    html += '</div>';
-
-    // 分类
-    if (elem.category) {
-        html += '<div class="modal-section">';
-        html += '<strong>分类:</strong>';
-        if (typeof elem.category === 'object') {
-            html += ' ' + (elem.category.一级分类 || '');
-            if (elem.category.二级分类) {
-                html += ' / ' + elem.category.二级分类;
-            }
-        } else {
-            html += ' ' + elem.category;
-        }
-        html += '</div>';
-    }
-
-    // 跳转到Tab3查看完整信息
-    html += '<div class="modal-footer">';
-    html += '<button class="modal-btn" onclick="jumpToElement(\'' + elem.id + '\')">查看完整信息</button>';
-    html += '</div>';
-
-    html += '</div>';
-
-    modalContent.innerHTML = html;
-    modal.appendChild(modalContent);
-    document.body.appendChild(modal);
-}
-
-// 跳转到元素详情(Tab3)
-function jumpToElement(elementId) {
-    // 关闭模态框
-    const modal = document.querySelector('.element-modal-backdrop');
-    if (modal) {
-        modal.remove();
-    }
-
-    // 切换到Tab3
-    switchTab('tab3');
-
-    // 等待DOM更新后滚动到元素
-    setTimeout(function() {
-        const elemItem = document.querySelector('[data-elem-id="' + elementId + '"]');
-        if (elemItem) {
-            // 展开所有父级容器
-            let parent = elemItem.parentElement;
-            while (parent) {
-                if (parent.classList.contains('collapsed')) {
-                    parent.classList.remove('collapsed');
-                }
-                parent = parent.parentElement;
-            }
-
-            // 滚动到元素并高亮
-            elemItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
-            elemItem.classList.add('highlight-pulse');
-            setTimeout(function() {
-                elemItem.classList.remove('highlight-pulse');
-            }, 2000);
-        }
-    }, 100);
-}
-
-// Tab切换功能
-function switchTab(tabId) {
-    // 隐藏所有tab内容
-    const allTabs = document.querySelectorAll('.tab-content');
-    allTabs.forEach(tab => {
-        tab.style.display = 'none';
-    });
-
-    // 移除所有tab的active类
-    const allTabButtons = document.querySelectorAll('.tab');
-    allTabButtons.forEach(btn => {
-        btn.classList.remove('active');
-    });
-
-    // 显示选中的tab内容
-    document.getElementById(tabId).style.display = 'block';
-
-    // 给选中的tab按钮添加active类
-    if (event && event.target) {
-        event.target.classList.add('active');
-    } else {
-        // 程序化切换时,手动添加active类
-        document.querySelectorAll('.tab').forEach((btn, idx) => {
-            if ((tabId === 'tab1' && idx === 0) ||
-                (tabId === 'tab2' && idx === 1) ||
-                (tabId === 'tab3' && idx === 2) ||
-                (tabId === 'tab4' && idx === 3) ||
-                (tabId === 'tab5' && idx === 4)) {
-                btn.classList.add('active');
-            }
-        });
-    }
-
-    // 如果切换到tab4,重新绘制连线
-    if (tabId === 'tab4') {
-        setTimeout(() => {
-            drawAllConnections();
-        }, 100);
-    }
-
-    // Tab5 不在切换时自动绘制连线,只在点击卡片时绘制
-}
-
-// 展开/收起段落功能
-function toggleCollapse(element) {
-    const listItem = element.closest('.paragraph-item');
-    if (!listItem.classList.contains('collapsible')) {
-        return;
-    }
-    listItem.classList.toggle('collapsed');
-}
-
-// 展开/收起段落详细内容
-function toggleDetails(element) {
-    const detailsContainer = element.closest('.paragraph-item')
-        .querySelector('.paragraph-details');
-    if (detailsContainer) {
-        detailsContainer.classList.toggle('collapsed');
-        const toggleBtn = element.closest('.paragraph-header')
-            .querySelector('.details-toggle-btn');
-        if (detailsContainer.classList.contains('collapsed')) {
-            toggleBtn.innerHTML = '<span class="details-icon">▶</span> 查看详细内容';
-        } else {
-            toggleBtn.innerHTML = '<span class="details-icon">▶</span> 隐藏详细内容';
-        }
-    }
-}
-
-// Tab3: 展开/收起第一层级(实质/形式)
-function toggleLevel1(element) {
-    element.classList.toggle('collapsed');
-}
-
-// Tab3: 展开/收起第二层级(具体元素/具体概念/抽象概念)
-function toggleLevel2(element) {
-    element.classList.toggle('collapsed');
-}
-
-// Tab3: 展开/收起分类组
-function toggleCategoryGroup(element) {
-    const categoryGroup = element.closest('.category-group');
-    if (categoryGroup && categoryGroup.classList.contains('collapsible')) {
-        categoryGroup.classList.toggle('collapsed');
-    }
-}
-
-// Tab3: 展开/收起二级分类组
-function toggleSubcategoryGroup(element) {
-    const subcategoryGroup = element.closest('.subcategory-group');
-    if (subcategoryGroup && subcategoryGroup.classList.contains('collapsible')) {
-        subcategoryGroup.classList.toggle('collapsed');
-    }
-}
-
-// Tab3: 展开/收起元素详情
-function toggleElementDetails(element) {
-    const elementItem = element.closest('.element-item');
-    const detailsContainer = elementItem.querySelector('.element-details');
-
-    if (detailsContainer) {
-        event.stopPropagation();
-        elementItem.classList.toggle('expanded');
-        detailsContainer.classList.toggle('collapsed');
-    }
-}
-
-// Tab3: 全部展开/收起所有层级
-function toggleAllLevels(expand) {
-    // 第一层级
-    const level1Headers = document.querySelectorAll('.level1-header');
-    level1Headers.forEach(header => {
-        if (expand) {
-            header.classList.remove('collapsed');
-        } else {
-            header.classList.add('collapsed');
-        }
-    });
-
-    // 第二层级
-    const level2Headers = document.querySelectorAll('.level2-header');
-    level2Headers.forEach(header => {
-        if (expand) {
-            header.classList.remove('collapsed');
-        } else {
-            header.classList.add('collapsed');
-        }
-    });
-
-    // 分类组
-    const categoryGroups = document.querySelectorAll('.category-group.collapsible');
-    categoryGroups.forEach(group => {
-        if (expand) {
-            group.classList.remove('collapsed');
-        } else {
-            group.classList.add('collapsed');
-        }
-    });
-
-    // 二级分类组
-    const subcategoryGroups = document.querySelectorAll('.subcategory-group.collapsible');
-    subcategoryGroups.forEach(group => {
-        if (expand) {
-            group.classList.remove('collapsed');
-        } else {
-            group.classList.add('collapsed');
-        }
-    });
-}
-
-// 旧版本兼容:保持toggleAllCategories函数
-function toggleAllCategories(expand) {
-    toggleAllLevels(expand);
-}
-
-// 页面加载完成后初始化
-document.addEventListener('DOMContentLoaded', function() {
-    console.log('脚本结果可视化页面已加载');
-
-    // 初始化Tab4连线图
-    if (document.getElementById('tab4')) {
-        initializeRelationshipGraph();
-    }
-});
-
-// ===== Tab4 关系图功能 =====
-
-// 初始化关系图
-function initializeRelationshipGraph() {
-    // 延迟执行以确保DOM完全加载
-    setTimeout(() => {
-        // 初始状态:隐藏所有右侧节点
-        resetRelationshipView();
-
-        // 监听窗口大小变化,重新绘制连线
-        window.addEventListener('resize', debounce(() => {
-            if (selectedSubstanceId && relationshipData[selectedSubstanceId]) {
-                drawSelectedSubstanceConnections(selectedSubstanceId, relationshipData[selectedSubstanceId]);
-            }
-        }, 300));
-    }, 100);
-}
-
-// 绘制所有连线
-function drawAllConnections() {
-    if (typeof relationshipData === 'undefined') {
-        return;
-    }
-
-    const svg = document.getElementById('relationship-svg');
-    if (!svg) return;
-
-    // 清空现有连线
-    svg.innerHTML = '';
-
-    // 遍历所有实质点
-    Object.keys(relationshipData).forEach(substanceId => {
-        const relations = relationshipData[substanceId];
-
-        // 绘制到灵感点的连线
-        relations.inspiration.forEach(rel => {
-            // 兼容新结构(支撑)和旧结构(相似度分数)
-            const score = rel.avg_score !== undefined ? rel.avg_score : (rel.support_reason !== undefined ? 1.0 : 0.5);
-            drawConnection(substanceId, rel.target, 'inspiration', score);
-        });
-
-        // 绘制到目的点的连线
-        relations.purpose.forEach(rel => {
-            const score = rel.avg_score !== undefined ? rel.avg_score : (rel.support_reason !== undefined ? 1.0 : 0.5);
-            drawConnection(substanceId, rel.target, 'purpose', score);
-        });
-
-        // 绘制到关键点的连线
-        relations.keypoint.forEach(rel => {
-            const score = rel.avg_score !== undefined ? rel.avg_score : (rel.support_reason !== undefined ? 1.0 : 0.5);
-            drawConnection(substanceId, rel.target, 'keypoint', score);
-        });
-    });
-}
-
-// 绘制单条连线
-function drawConnection(sourceId, targetId, type, score) {
-    const svg = document.getElementById('relationship-svg');
-    if (!svg) return;
-
-    const sourceNode = document.querySelector(`.substance-node[data-id="${sourceId}"]`);
-    const targetNode = document.querySelector(`.target-node[data-id="${targetId}"]`);
-
-    if (!sourceNode || !targetNode) return;
-
-    // 获取节点位置
-    const sourceRect = sourceNode.getBoundingClientRect();
-    const targetRect = targetNode.getBoundingClientRect();
-    const containerRect = svg.parentElement.getBoundingClientRect();
-
-    // 计算相对于SVG容器的位置
-    const x1 = sourceRect.right - containerRect.left;
-    const y1 = sourceRect.top + sourceRect.height / 2 - containerRect.top;
-    const x2 = targetRect.left - containerRect.left;
-    const y2 = targetRect.top + targetRect.height / 2 - containerRect.top;
-
-    // 创建贝塞尔曲线路径
-    const midX = (x1 + x2) / 2;
-    const path = `M ${x1} ${y1} Q ${midX} ${y1}, ${midX} ${(y1 + y2) / 2} T ${x2} ${y2}`;
-
-    // 创建path元素
-    const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
-    pathElement.setAttribute('d', path);
-    pathElement.setAttribute('class', `connection-line ${type}`);
-    pathElement.setAttribute('fill', 'none');
-    pathElement.setAttribute('data-source', sourceId);
-    pathElement.setAttribute('data-target', targetId);
-    pathElement.setAttribute('data-type', type);
-    pathElement.setAttribute('data-score', score);
-
-    svg.appendChild(pathElement);
-}
-
-// 高亮相关关系
-function highlightRelationships(substanceId) {
-    if (typeof relationshipData === 'undefined') {
-        return;
-    }
-
-    // 清除之前的高亮
-    clearHighlights();
-
-    // 高亮当前实质点
-    const substanceNode = document.querySelector(`.substance-node[data-id="${substanceId}"]`);
-    if (substanceNode) {
-        substanceNode.classList.add('highlighted');
-    }
-
-    // 获取相关关系
-    const relations = relationshipData[substanceId];
-    if (!relations) return;
-
-    // 高亮相关的目标点
-    [...relations.inspiration, ...relations.purpose, ...relations.keypoint].forEach(rel => {
-        const targetNode = document.querySelector(`.target-node[data-id="${rel.target}"]`);
-        if (targetNode) {
-            targetNode.classList.add('highlighted');
-        }
-    });
-
-    // 高亮相关的连线
-    const lines = document.querySelectorAll(`.connection-line[data-source="${substanceId}"]`);
-    lines.forEach(line => {
-        line.classList.add('highlighted');
-    });
-}
-
-// 清除所有高亮
-function clearHighlights() {
-    // 清除节点高亮
-    document.querySelectorAll('.node.highlighted').forEach(node => {
-        node.classList.remove('highlighted');
-    });
-
-    // 清除连线高亮
-    document.querySelectorAll('.connection-line.highlighted').forEach(line => {
-        line.classList.remove('highlighted');
-    });
-}
-
-// 防抖函数
-function debounce(func, wait) {
-    let timeout;
-    return function executedFunction(...args) {
-        const later = () => {
-            clearTimeout(timeout);
-            func(...args);
-        };
-        clearTimeout(timeout);
-        timeout = setTimeout(later, wait);
-    };
-}
-
-// ===== Tab4 新增:点击选择实质点功能 =====
-
-let selectedSubstanceId = null;
-
-// 选择实质点,展示关联关系
-function selectSubstance(substanceId) {
-    if (typeof relationshipData === 'undefined') {
-        return;
-    }
-
-    // 如果点击已选中的实质点,不做处理
-    if (selectedSubstanceId === substanceId) {
-        return;
-    }
-
-    selectedSubstanceId = substanceId;
-
-    // 显示重置按钮
-    const resetBtn = document.querySelector('.reset-btn');
-    if (resetBtn) {
-        resetBtn.style.display = 'block';
-    }
-
-    // 获取关系数据
-    const relations = relationshipData[substanceId];
-    if (!relations) return;
-
-    // 收集所有关联的节点ID
-    const relatedNodeIds = new Set();
-
-    // 添加灵感点
-    relations.inspiration.forEach(rel => {
-        relatedNodeIds.add(rel.target);
-    });
-
-    // 添加目的点
-    relations.purpose.forEach(rel => {
-        relatedNodeIds.add(rel.target);
-    });
-
-    // 添加关键点
-    relations.keypoint.forEach(rel => {
-        relatedNodeIds.add(rel.target);
-    });
-
-    // 添加关联的实质点
-    relations.substance.forEach(rel => {
-        relatedNodeIds.add(rel.target);
-    });
-
-    // 高亮左侧选中的实质点
-    document.querySelectorAll('.substance-node').forEach(node => {
-        if (node.getAttribute('data-id') === substanceId) {
-            node.classList.add('selected');
-            node.classList.remove('dimmed');
-        } else {
-            node.classList.remove('selected');
-            node.classList.add('dimmed');
-        }
-    });
-
-    // 右侧节点:只显示关联的节点
-    document.querySelectorAll('.target-node').forEach(node => {
-        const nodeId = node.getAttribute('data-id');
-        if (relatedNodeIds.has(nodeId)) {
-            node.classList.add('visible');
-            node.classList.remove('hidden');
-        } else {
-            node.classList.remove('visible');
-            node.classList.add('hidden');
-        }
-    });
-
-    // 根据右侧节点的可见性,控制section的显示
-    document.querySelectorAll('.target-section').forEach(section => {
-        const visibleNodes = section.querySelectorAll('.target-node.visible');
-        if (visibleNodes.length > 0) {
-            section.classList.add('has-visible-nodes');
-            section.classList.remove('all-hidden');
-        } else {
-            section.classList.remove('has-visible-nodes');
-            section.classList.add('all-hidden');
-        }
-    });
-
-    // 重新绘制连线(只绘制选中实质点的连线)
-    drawSelectedSubstanceConnections(substanceId, relations);
-}
-
-// 重置视图
-function resetRelationshipView() {
-    selectedSubstanceId = null;
-
-    // 隐藏重置按钮
-    const resetBtn = document.querySelector('.reset-btn');
-    if (resetBtn) {
-        resetBtn.style.display = 'none';
-    }
-
-    // 左侧实质点:移除所有高亮和变暗
-    document.querySelectorAll('.substance-node').forEach(node => {
-        node.classList.remove('selected', 'dimmed');
-    });
-
-    // 右侧节点:隐藏所有节点
-    document.querySelectorAll('.target-node').forEach(node => {
-        node.classList.remove('visible');
-        node.classList.add('hidden');
-    });
-
-    // 隐藏所有section
-    document.querySelectorAll('.target-section').forEach(section => {
-        section.classList.remove('has-visible-nodes');
-        section.classList.add('all-hidden');
-    });
-
-    // 清空连线
-    const svg = document.getElementById('relationship-svg');
-    if (svg) {
-        svg.innerHTML = '';
-    }
-}
-
-// 绘制选中实质点的连线
-function drawSelectedSubstanceConnections(substanceId, relations) {
-    const svg = document.getElementById('relationship-svg');
-    if (!svg) return;
-
-    // 清空现有连线
-    svg.innerHTML = '';
-
-    // 绘制到灵感点的连线
-    relations.inspiration.forEach(rel => {
-        // 兼容新结构(支撑)和旧结构(相似度分数)
-        const score = rel.avg_score !== undefined ? rel.avg_score : (rel.support_reason !== undefined ? 1.0 : 0.5);
-        drawConnection(substanceId, rel.target, 'inspiration', score);
-    });
-
-    // 绘制到目的点的连线
-    relations.purpose.forEach(rel => {
-        const score = rel.avg_score !== undefined ? rel.avg_score : (rel.support_reason !== undefined ? 1.0 : 0.5);
-        drawConnection(substanceId, rel.target, 'purpose', score);
-    });
-
-    // 绘制到关键点的连线
-    relations.keypoint.forEach(rel => {
-        const score = rel.avg_score !== undefined ? rel.avg_score : (rel.support_reason !== undefined ? 1.0 : 0.5);
-        drawConnection(substanceId, rel.target, 'keypoint', score);
-    });
-
-    // 绘制到其他实质点的连线
-    relations.substance.forEach(rel => {
-        drawConnection(substanceId, rel.target, 'substance', rel.common_count / 10);
-    });
-}
-
-// ===== Tab4 新版支撑关系图功能 =====
-
-let selectedSupportSubstanceId = null;
-let selectedSupportTargetId = null;
-
-// 选择实质点(新版)
-function selectSubstance(substanceId) {
-    if (typeof supportRelationships === 'undefined') {
-        console.error('supportRelationships not defined');
-        return;
-    }
-
-    // 如果点击已选中的实质点,不做处理
-    if (selectedSupportSubstanceId === substanceId) {
-        return;
-    }
-
-    selectedSupportSubstanceId = substanceId;
-    selectedSupportTargetId = null;
-
-    // 清空画布
-    clearSupportCanvas();
-
-    // 获取关系数据
-    const relations = supportRelationships.substance_to_target[substanceId];
-    if (!relations) {
-        console.error('No relations found for substance:', substanceId);
-        return;
-    }
-
-    // 高亮选中的实质点,其他变暗
-    document.querySelectorAll('.substance-card').forEach(card => {
-        if (card.getAttribute('data-id') === substanceId) {
-            card.classList.add('selected');
-            card.classList.remove('dimmed');
-        } else {
-            card.classList.remove('selected');
-            card.classList.add('dimmed');
-        }
-    });
-
-    // 清除所有目标点的状态
-    document.querySelectorAll('.target-card').forEach(card => {
-        card.classList.remove('selected', 'dimmed', 'highlighted');
-    });
-
-    // 根据关系数据高亮相关的目标点
-    const relatedTargets = new Set();
-
-    relations.inspiration.forEach(rel => {
-        relatedTargets.add(rel.target_id);
-    });
-    relations.purpose.forEach(rel => {
-        relatedTargets.add(rel.target_id);
-    });
-    relations.keypoint.forEach(rel => {
-        relatedTargets.add(rel.target_id);
-    });
-
-    // 高亮相关的目标点,其他变暗
-    document.querySelectorAll('.target-card').forEach(card => {
-        const cardId = card.getAttribute('data-id');
-        if (relatedTargets.has(cardId)) {
-            card.classList.add('highlighted');
-            card.classList.remove('dimmed');
-        } else {
-            card.classList.add('dimmed');
-            card.classList.remove('highlighted');
-        }
-    });
-
-    // 绘制支撑关系(使用Sankey风格或树状结构)
-    drawSupportRelationships(substanceId, relations);
-}
-
-// 选择目标点(反向查看)
-function selectTarget(targetType, targetIndex) {
-    if (typeof supportRelationships === 'undefined') {
-        console.error('supportRelationships not defined');
-        return;
-    }
-
-    const targetId = `${targetType}-${targetIndex}`;
-
-    // 如果点击已选中的目标点,不做处理
-    if (selectedSupportTargetId === targetId) {
-        return;
-    }
-
-    selectedSupportSubstanceId = null;
-    selectedSupportTargetId = targetId;
-
-    // 清空画布
-    clearSupportCanvas();
-
-    // 获取反向关系数据
-    const relations = supportRelationships.target_to_substance[targetId];
-    if (!relations) {
-        console.error('No relations found for target:', targetId);
-        return;
-    }
-
-    // 清除所有实质点的状态
-    document.querySelectorAll('.substance-card').forEach(card => {
-        card.classList.remove('selected', 'dimmed', 'highlighted');
-    });
-
-    // 高亮选中的目标点,其他变暗
-    document.querySelectorAll('.target-card').forEach(card => {
-        if (card.getAttribute('data-id') === targetId) {
-            card.classList.add('selected');
-            card.classList.remove('dimmed');
-        } else {
-            card.classList.remove('selected');
-            card.classList.add('dimmed');
-        }
-    });
-
-    // 收集所有支撑该目标点的实质点ID
-    const supportingSubstances = new Set();
-    relations.concrete_elements.forEach(item => supportingSubstances.add(item.substance_id));
-    relations.concrete_concepts.forEach(item => supportingSubstances.add(item.substance_id));
-    relations.abstract_concepts.forEach(item => supportingSubstances.add(item.substance_id));
-
-    // 高亮支撑的实质点,其他变暗
-    document.querySelectorAll('.substance-card').forEach(card => {
-        const cardId = card.getAttribute('data-id');
-        if (supportingSubstances.has(cardId)) {
-            card.classList.add('highlighted');
-            card.classList.remove('dimmed');
-        } else {
-            card.classList.add('dimmed');
-            card.classList.remove('highlighted');
-        }
-    });
-
-    // 绘制反向支撑关系
-    drawReverseSupportRelationships(targetId, relations);
-}
-
-// 绘制支撑关系(实质点 → 目标点)
-function drawSupportRelationships(substanceId, relations) {
-    const canvas = document.getElementById('support-canvas');
-    if (!canvas) return;
-
-    // 清空并移除占位文本
-    const placeholder = canvas.querySelector('.placeholder-text');
-    if (placeholder) {
-        placeholder.style.display = 'none';
-    }
-
-    // 创建可视化内容
-    let html = '<div class="support-flow">\n';
-    html += '<div class="flow-title">支撑关系</div>\n';
-
-    // 显示来源实质点
-    const substanceData = supportRelationships.substance_to_target[substanceId];
-    html += '<div class="flow-source">\n';
-    html += '<div class="flow-node substance-flow-node">\n';
-    html += `<div class="flow-node-label">${substanceData.type || '实质点'}</div>\n`;
-    html += `<div class="flow-node-name">${substanceData.name || substanceId}</div>\n`;
-    html += '</div>\n';
-    html += '</div>\n';
-
-    // 显示支撑的目标点(分类)
-    html += '<div class="flow-targets">\n';
-
-    if (relations.inspiration && relations.inspiration.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">灵感点</div>\n';
-        relations.inspiration.forEach(rel => {
-            html += `<div class="flow-target-item inspiration-flow">\n`;
-            html += `<div class="flow-target-text">${rel.point}</div>\n`;
-            // 兼容新结构(支撑理由)和旧结构(相似度分数)
-            if (rel.support_reason !== undefined) {
-                html += `<div class="flow-target-score">支撑</div>\n`;
-            } else if (rel.avg_score !== undefined) {
-                const score = (rel.avg_score * 100).toFixed(0);
-                html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            }
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    if (relations.purpose && relations.purpose.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">目的点</div>\n';
-        relations.purpose.forEach(rel => {
-            html += `<div class="flow-target-item purpose-flow">\n`;
-            html += `<div class="flow-target-text">${rel.point}</div>\n`;
-            // 兼容新结构(支撑理由)和旧结构(相似度分数)
-            if (rel.support_reason !== undefined) {
-                html += `<div class="flow-target-score">支撑</div>\n`;
-            } else if (rel.avg_score !== undefined) {
-                const score = (rel.avg_score * 100).toFixed(0);
-                html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            }
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    if (relations.keypoint && relations.keypoint.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">关键点</div>\n';
-        relations.keypoint.forEach(rel => {
-            html += `<div class="flow-target-item keypoint-flow">\n`;
-            html += `<div class="flow-target-text">${rel.point}</div>\n`;
-            // 兼容新结构(支撑理由)和旧结构(相似度分数)
-            if (rel.support_reason !== undefined) {
-                html += `<div class="flow-target-score">支撑</div>\n`;
-            } else if (rel.avg_score !== undefined) {
-                const score = (rel.avg_score * 100).toFixed(0);
-                html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            }
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    html += '</div>\n';
-    html += '</div>\n';
-
-    // 插入到画布
-    canvas.insertAdjacentHTML('beforeend', html);
-}
-
-// 绘制反向支撑关系(目标点 ← 实质点)
-function drawReverseSupportRelationships(targetId, relations) {
-    const canvas = document.getElementById('support-canvas');
-    if (!canvas) return;
-
-    // 清空并移除占位文本
-    const placeholder = canvas.querySelector('.placeholder-text');
-    if (placeholder) {
-        placeholder.style.display = 'none';
-    }
-
-    // 创建可视化内容
-    let html = '<div class="support-flow reverse">\n';
-    html += '<div class="flow-title">被以下实质点支撑</div>\n';
-
-    // 显示目标点
-    html += '<div class="flow-source">\n';
-    html += '<div class="flow-node target-flow-node">\n';
-    html += `<div class="flow-node-label">目标点</div>\n`;
-    html += `<div class="flow-node-name">${targetId}</div>\n`;
-    html += '</div>\n';
-    html += '</div>\n';
-
-    // 显示支撑的实质点(分类)
-    html += '<div class="flow-targets">\n';
-
-    if (relations.concrete_elements && relations.concrete_elements.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">具体元素</div>\n';
-        relations.concrete_elements.forEach(item => {
-            const score = (item.score * 100).toFixed(0);
-            html += `<div class="flow-target-item concrete-element-flow">\n`;
-            html += `<div class="flow-target-text">${item.name}</div>\n`;
-            html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    if (relations.concrete_concepts && relations.concrete_concepts.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">具体概念</div>\n';
-        relations.concrete_concepts.forEach(item => {
-            const score = (item.score * 100).toFixed(0);
-            html += `<div class="flow-target-item concrete-concept-flow">\n`;
-            html += `<div class="flow-target-text">${item.name}</div>\n`;
-            html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    if (relations.abstract_concepts && relations.abstract_concepts.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">抽象概念</div>\n';
-        relations.abstract_concepts.forEach(item => {
-            const score = (item.score * 100).toFixed(0);
-            html += `<div class="flow-target-item abstract-concept-flow">\n`;
-            html += `<div class="flow-target-text">${item.name}</div>\n`;
-            html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    html += '</div>\n';
-    html += '</div>\n';
-
-    // 插入到画布
-    canvas.insertAdjacentHTML('beforeend', html);
-}
-
-// 清空支撑关系画布
-function clearSupportCanvas() {
-    const canvas = document.getElementById('support-canvas');
-    if (!canvas) return;
-
-    // 移除所有flow相关元素
-    const flows = canvas.querySelectorAll('.support-flow');
-    flows.forEach(flow => flow.remove());
-
-    // 重新显示占位文本
-    const placeholder = canvas.querySelector('.placeholder-text');
-    if (placeholder) {
-        placeholder.style.display = 'flex';
-    }
-}
-
-// ===== Tab5 实质与形式双向支撑关系图功能 =====
-
-let selectedLeftTargetType = null;  // 左侧选题点类型:inspiration/keypoint/purpose
-let selectedLeftTargetIdx = null;    // 左侧选题点索引
-let selectedTab5SubstanceId = null;  // 选中的实质点ID (Tab5专用)
-let selectedFormId = null;           // 选中的形式点ID
-let selectedRightTargetType = null;  // 右侧选题点类型:inspiration/keypoint/purpose
-let selectedRightTargetIdx = null;   // 右侧选题点索引
-
-// 选择左侧选题点(来自实质点支撑关系)
-function selectLeftTarget(targetType, targetIdx) {
-    if (typeof tab5Relationships === 'undefined') {
-        console.error('tab5Relationships not defined');
-        return;
-    }
-
-    // 如果点击已选中的目标点,重置显示所有元素
-    if (selectedLeftTargetType === targetType && selectedLeftTargetIdx === targetIdx) {
-        resetTab5Selection();
-        return;
-    }
-
-    // 重置其他选择
-    resetTab5Selection();
-
-    selectedLeftTargetType = targetType;
-    selectedLeftTargetIdx = targetIdx;
-
-    // 高亮选中的目标点
-    document.querySelectorAll('.tab5-left-targets .target-card').forEach(card => {
-        const dataType = card.getAttribute('data-type');
-        const dataId = card.getAttribute('data-id');
-        const fullId = `${targetType}-${targetIdx}`;
-
-        if (dataId === fullId) {
-            card.classList.add('selected');
-            card.classList.remove('dimmed', 'hidden');
-        } else {
-            card.classList.remove('selected');
-            card.classList.add('hidden');
-            card.classList.remove('dimmed');
-        }
-    });
-
-    // 形式点全部隐藏
-    document.querySelectorAll('.tab5-forms .form-card').forEach(card => {
-        card.classList.add('hidden');
-        card.classList.remove('selected', 'highlighted', 'dimmed');
-    });
-
-    // 右侧选题点全部隐藏
-    document.querySelectorAll('.tab5-right-targets .target-card').forEach(card => {
-        card.classList.add('hidden');
-        card.classList.remove('selected', 'highlighted', 'dimmed');
-    });
-
-    // 获取反向关系:哪些实质点支撑这个目标点(直接使用完整的targetIdx)
-    const targetId = `${targetType}-${targetIdx}`;
-    const supportingSubstances = tab5Relationships.target_from_substance[targetId];
-
-    if (supportingSubstances && supportingSubstances.length > 0) {
-        // 高亮支撑的实质点
-        const substanceIds = new Set(supportingSubstances.map(s => s.substance_id));
-
-        document.querySelectorAll('.tab5-substances .substance-card').forEach(card => {
-            const cardId = card.getAttribute('data-id');
-            if (substanceIds.has(cardId)) {
-                card.classList.add('highlighted');
-                card.classList.remove('dimmed', 'hidden');
-            } else {
-                card.classList.remove('highlighted');
-                card.classList.add('hidden');
-                card.classList.remove('dimmed');
-            }
-        });
-    }
-
-    // 绘制连线(只连接当前显示的卡片)
-    setTimeout(() => {
-        drawFormAllConnections();
-    }, 50);
-}
-
-// 选择实质点
-function selectSubstance(substanceId) {
-    if (typeof tab5Relationships === 'undefined') {
-        console.error('tab5Relationships not defined');
-        return;
-    }
-
-    // 如果点击已选中的实质点,重置显示所有元素
-    if (selectedTab5SubstanceId === substanceId) {
-        resetTab5Selection();
-        return;
-    }
-
-    // 重置其他选择
-    resetTab5Selection();
-
-    selectedTab5SubstanceId = substanceId;
-
-    // 高亮选中的实质点
-    document.querySelectorAll('.tab5-substances .substance-card').forEach(card => {
-        const cardId = card.getAttribute('data-id');
-        if (cardId === substanceId) {
-            card.classList.add('selected');
-            card.classList.remove('dimmed', 'hidden');
-        } else {
-            card.classList.remove('selected');
-            card.classList.add('hidden');
-            card.classList.remove('dimmed');
-        }
-    });
-
-    // 形式点全部隐藏(先全部隐藏,后面再显示有关联的)
-    document.querySelectorAll('.tab5-forms .form-card').forEach(card => {
-        card.classList.add('hidden');
-        card.classList.remove('selected', 'highlighted', 'dimmed');
-    });
-
-    // 右侧选题点全部隐藏
-    document.querySelectorAll('.tab5-right-targets .target-card').forEach(card => {
-        card.classList.add('hidden');
-        card.classList.remove('selected', 'highlighted', 'dimmed');
-    });
-
-    // 获取这个实质点支撑的选题点
-    const relations = tab5Relationships.substance_to_target[substanceId];
-
-    if (relations) {
-        const leftTargets = new Set();
-
-        relations.inspiration.forEach(rel => leftTargets.add(rel.target_id));
-        relations.purpose.forEach(rel => leftTargets.add(rel.target_id));
-        relations.keypoint.forEach(rel => leftTargets.add(rel.target_id));
-
-        // 高亮左侧相关选题点
-        document.querySelectorAll('.tab5-left-targets .target-card').forEach(card => {
-            const cardId = card.getAttribute('data-id');
-            if (leftTargets.has(cardId)) {
-                card.classList.add('highlighted');
-                card.classList.remove('dimmed', 'hidden');
-            } else {
-                card.classList.remove('highlighted');
-                card.classList.add('hidden');
-                card.classList.remove('dimmed');
-            }
-        });
-    }
-
-    // 高亮支撑这个实质点的形式点(反向关系)
-    const supportingForms = tab5Relationships.substance_from_form[substanceId];
-    if (supportingForms && supportingForms.length > 0) {
-        const formIds = new Set(supportingForms.map(f => f.form_id));
-
-        document.querySelectorAll('.tab5-forms .form-card').forEach(card => {
-            const cardId = card.getAttribute('data-id');
-            if (formIds.has(cardId)) {
-                card.classList.add('highlighted');
-                card.classList.remove('dimmed', 'hidden');
-            }
-            // 注意:这里不对其他形式点再次隐藏,因为已经在前面全部隐藏了
-        });
-    }
-
-    // 绘制连线(只连接当前显示的卡片)
-    setTimeout(() => {
-        drawFormAllConnections();
-    }, 50);
-}
-
-// 选择形式点
-function selectForm(formId) {
-    if (typeof tab5Relationships === 'undefined') {
-        console.error('tab5Relationships not defined');
-        return;
-    }
-
-    // 如果点击已选中的形式点,重置显示所有元素
-    if (selectedFormId === formId) {
-        resetTab5Selection();
-        return;
-    }
-
-    // 重置其他选择
-    resetTab5Selection();
-
-    selectedFormId = formId;
-
-    // 高亮选中的形式点
-    document.querySelectorAll('.tab5-forms .form-card').forEach(card => {
-        const cardId = card.getAttribute('data-id');
-        if (cardId === formId) {
-            card.classList.add('selected');
-            card.classList.remove('dimmed', 'hidden');
-        } else {
-            card.classList.remove('selected');
-            card.classList.add('hidden');
-            card.classList.remove('dimmed');
-        }
-    });
-
-    // 实质点全部隐藏(先全部隐藏,后面再显示有关联的)
-    document.querySelectorAll('.tab5-substances .substance-card').forEach(card => {
-        card.classList.add('hidden');
-        card.classList.remove('selected', 'highlighted', 'dimmed');
-    });
-
-    // 左侧选题点全部隐藏
-    document.querySelectorAll('.tab5-left-targets .target-card').forEach(card => {
-        card.classList.add('hidden');
-        card.classList.remove('selected', 'highlighted', 'dimmed');
-    });
-
-    // 获取这个形式点支撑的选题点
-    const relations = tab5Relationships.form_to_target[formId];
-
-    if (relations) {
-        const rightTargets = new Set();
-
-        relations.inspiration.forEach(rel => rightTargets.add(rel.target_id));
-        relations.purpose.forEach(rel => rightTargets.add(rel.target_id));
-        relations.keypoint.forEach(rel => rightTargets.add(rel.target_id));
-
-        // 高亮右侧相关选题点
-        document.querySelectorAll('.tab5-right-targets .target-card').forEach(card => {
-            const cardId = card.getAttribute('data-id');
-            if (rightTargets.has(cardId)) {
-                card.classList.add('highlighted');
-                card.classList.remove('dimmed', 'hidden');
-            } else {
-                card.classList.remove('highlighted');
-                card.classList.add('hidden');
-                card.classList.remove('dimmed');
-            }
-        });
-    }
-
-    // 高亮这个形式点支撑的实质点
-    const supportedSubstances = tab5Relationships.form_to_substance[formId];
-    if (supportedSubstances && supportedSubstances.length > 0) {
-        const substanceIds = new Set(supportedSubstances.map(s => s.substance_id));
-
-        document.querySelectorAll('.tab5-substances .substance-card').forEach(card => {
-            const cardId = card.getAttribute('data-id');
-            if (substanceIds.has(cardId)) {
-                card.classList.add('highlighted');
-                card.classList.remove('dimmed', 'hidden');
-            }
-            // 注意:这里不对其他实质点再次隐藏,因为已经在前面全部隐藏了
-        });
-    }
-
-    // 绘制连线(只连接当前显示的卡片)
-    setTimeout(() => {
-        drawFormAllConnections();
-    }, 50);
-}
-
-// 选择右侧选题点(来自形式点支撑关系)
-function selectRightTarget(targetType, targetIdx) {
-    if (typeof tab5Relationships === 'undefined') {
-        console.error('tab5Relationships not defined');
-        return;
-    }
-
-    // 如果点击已选中的目标点,重置显示所有元素
-    if (selectedRightTargetType === targetType && selectedRightTargetIdx === targetIdx) {
-        resetTab5Selection();
-        return;
-    }
-
-    // 重置其他选择
-    resetTab5Selection();
-
-    selectedRightTargetType = targetType;
-    selectedRightTargetIdx = targetIdx;
-
-    // 高亮选中的目标点
-    document.querySelectorAll('.tab5-right-targets .target-card').forEach(card => {
-        const dataType = card.getAttribute('data-type');
-        const dataId = card.getAttribute('data-id');
-        const fullId = `${targetType}-${targetIdx}`;
-
-        if (dataId === fullId) {
-            card.classList.add('selected');
-            card.classList.remove('dimmed', 'hidden');
-        } else {
-            card.classList.remove('selected');
-            card.classList.add('hidden');
-            card.classList.remove('dimmed');
-        }
-    });
-
-    // 实质点全部隐藏
-    document.querySelectorAll('.tab5-substances .substance-card').forEach(card => {
-        card.classList.add('hidden');
-        card.classList.remove('selected', 'highlighted', 'dimmed');
-    });
-
-    // 左侧选题点全部隐藏
-    document.querySelectorAll('.tab5-left-targets .target-card').forEach(card => {
-        card.classList.add('hidden');
-        card.classList.remove('selected', 'highlighted', 'dimmed');
-    });
-
-    // 获取反向关系:哪些形式点支撑这个目标点(直接使用完整的targetIdx)
-    const targetId = `${targetType}-${targetIdx}`;
-    const supportingForms = tab5Relationships.target_from_form[targetId];
-
-    if (supportingForms && supportingForms.length > 0) {
-        // 高亮支撑的形式点
-        const formIds = new Set(supportingForms.map(f => f.form_id));
-
-        document.querySelectorAll('.tab5-forms .form-card').forEach(card => {
-            const cardId = card.getAttribute('data-id');
-            if (formIds.has(cardId)) {
-                card.classList.add('highlighted');
-                card.classList.remove('dimmed', 'hidden');
-            } else {
-                card.classList.remove('highlighted');
-                card.classList.add('hidden');
-                card.classList.remove('dimmed');
-            }
-        });
-    }
-
-    // 绘制连线(只连接当前显示的卡片)
-    setTimeout(() => {
-        drawFormAllConnections();
-    }, 50);
-}
-
-// 重置Tab5所有选择
-function resetTab5Selection() {
-    selectedLeftTargetType = null;
-    selectedLeftTargetIdx = null;
-    selectedSubstanceId = null;
-    selectedFormId = null;
-    selectedRightTargetType = null;
-    selectedRightTargetIdx = null;
-
-    // 清除所有高亮、选中和隐藏状态
-    document.querySelectorAll('#tab5 .target-card, #tab5 .substance-card, #tab5 .form-card').forEach(card => {
-        card.classList.remove('selected', 'dimmed', 'highlighted', 'hidden');
-    });
-
-    // 清除所有连线
-    const svg = document.getElementById('tab5-connection-svg');
-    if (svg) {
-        svg.innerHTML = '';
-    }
-}
-
-// 选择目标点(反向查看)
-function selectFormTarget(targetType, targetId) {
-    if (typeof formSupportRelationships === 'undefined') {
-        console.error('formSupportRelationships not defined');
-        return;
-    }
-
-    let fullTargetId;
-    if (targetType === 'substance') {
-        fullTargetId = `substance-${targetId}`;
-    } else {
-        fullTargetId = `${targetType}-${targetId}`;
-    }
-
-    // 如果点击已选中的目标点,不做处理
-    if (selectedFormTargetId === fullTargetId) {
-        return;
-    }
-
-    selectedFormId = null;
-    selectedFormTargetId = fullTargetId;
-
-    // 清空画布
-    clearFormSupportCanvas();
-
-    // 获取反向关系数据
-    const relations = formSupportRelationships.target_to_form[fullTargetId];
-    if (!relations) {
-        console.error('No relations found for target:', fullTargetId);
-        return;
-    }
-
-    // 清除所有形式点的状态
-    document.querySelectorAll('.form-card').forEach(card => {
-        card.classList.remove('selected', 'dimmed', 'highlighted');
-    });
-
-    // 高亮选中的目标点,其他变暗
-    document.querySelectorAll('#tab5 .target-card').forEach(card => {
-        if (card.getAttribute('data-id') === fullTargetId) {
-            card.classList.add('selected');
-            card.classList.remove('dimmed');
-        } else {
-            card.classList.remove('selected');
-            card.classList.add('dimmed');
-        }
-    });
-
-    // 收集所有支撑该目标点的形式点ID
-    const supportingForms = new Set();
-
-    if (relations.concrete_element_forms) {
-        relations.concrete_element_forms.forEach(item => supportingForms.add(item.form_id));
-    }
-    if (relations.concrete_concept_forms) {
-        relations.concrete_concept_forms.forEach(item => supportingForms.add(item.form_id));
-    }
-    if (relations.overall_forms) {
-        relations.overall_forms.forEach(item => supportingForms.add(item.form_id));
-    }
-    if (relations.forms) {
-        relations.forms.forEach(item => supportingForms.add(item.form_id));
-    }
-
-    // 高亮支撑的形式点,其他变暗
-    document.querySelectorAll('.form-card').forEach(card => {
-        const cardId = card.getAttribute('data-id');
-        if (supportingForms.has(cardId)) {
-            card.classList.add('highlighted');
-            card.classList.remove('dimmed');
-        } else {
-            card.classList.add('dimmed');
-            card.classList.remove('highlighted');
-        }
-    });
-
-    // 绘制反向支撑关系
-    drawReverseFormSupportRelationships(fullTargetId, relations);
-}
-
-// 绘制形式点支撑关系(形式点 → 目标点)
-function drawFormSupportRelationships(formId, relations) {
-    const canvas = document.getElementById('form-support-canvas');
-    if (!canvas) return;
-
-    // 清空并移除占位文本
-    const placeholder = canvas.querySelector('.placeholder-text');
-    if (placeholder) {
-        placeholder.style.display = 'none';
-    }
-
-    // 创建可视化内容
-    let html = '<div class="support-flow">\n';
-    html += '<div class="flow-title">支撑关系</div>\n';
-
-    // 显示来源形式点
-    html += '<div class="flow-source">\n';
-    html += '<div class="flow-node form-flow-node">\n';
-    html += `<div class="flow-node-label">${relations.type || '形式点'}</div>\n`;
-    html += `<div class="flow-node-name">${relations.name || formId}</div>\n`;
-    html += '</div>\n';
-    html += '</div>\n';
-
-    // 显示支撑的目标点(分类)
-    html += '<div class="flow-targets">\n';
-
-    if (relations.inspiration && relations.inspiration.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">灵感点</div>\n';
-        relations.inspiration.forEach(rel => {
-            html += `<div class="flow-target-item inspiration-flow">\n`;
-            html += `<div class="flow-target-text">${rel.point}</div>\n`;
-            // 兼容新结构(支撑理由)和旧结构(相似度分数)
-            if (rel.support_reason !== undefined) {
-                html += `<div class="flow-target-score">支撑</div>\n`;
-            } else if (rel.avg_score !== undefined) {
-                const score = (rel.avg_score * 100).toFixed(0);
-                html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            }
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    if (relations.purpose && relations.purpose.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">目的点</div>\n';
-        relations.purpose.forEach(rel => {
-            html += `<div class="flow-target-item purpose-flow">\n`;
-            html += `<div class="flow-target-text">${rel.point}</div>\n`;
-            // 兼容新结构(支撑理由)和旧结构(相似度分数)
-            if (rel.support_reason !== undefined) {
-                html += `<div class="flow-target-score">支撑</div>\n`;
-            } else if (rel.avg_score !== undefined) {
-                const score = (rel.avg_score * 100).toFixed(0);
-                html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            }
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    if (relations.keypoint && relations.keypoint.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">关键点</div>\n';
-        relations.keypoint.forEach(rel => {
-            html += `<div class="flow-target-item keypoint-flow">\n`;
-            html += `<div class="flow-target-text">${rel.point}</div>\n`;
-            // 兼容新结构(支撑理由)和旧结构(相似度分数)
-            if (rel.support_reason !== undefined) {
-                html += `<div class="flow-target-score">支撑</div>\n`;
-            } else if (rel.avg_score !== undefined) {
-                const score = (rel.avg_score * 100).toFixed(0);
-                html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            }
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    if (relations.substance && relations.substance.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">实质点</div>\n';
-        relations.substance.forEach(rel => {
-            const score = (rel.avg_score * 100).toFixed(0);
-            let flowClass = '';
-            if (rel.type === '具体元素') {
-                flowClass = 'concrete-element-flow';
-            } else if (rel.type === '具体概念') {
-                flowClass = 'concrete-concept-flow';
-            } else if (rel.type === '抽象概念') {
-                flowClass = 'abstract-concept-flow';
-            }
-            html += `<div class="flow-target-item ${flowClass}">\n`;
-            html += `<div class="flow-target-text">${rel.name} <span style="font-size:0.8em;color:#6c757d;">(${rel.type})</span></div>\n`;
-            html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    html += '</div>\n';
-    html += '</div>\n';
-
-    // 插入到画布
-    canvas.insertAdjacentHTML('beforeend', html);
-}
-
-// 绘制反向形式点支撑关系(目标点 ← 形式点)
-function drawReverseFormSupportRelationships(targetId, relations) {
-    const canvas = document.getElementById('form-support-canvas');
-    if (!canvas) return;
-
-    // 清空并移除占位文本
-    const placeholder = canvas.querySelector('.placeholder-text');
-    if (placeholder) {
-        placeholder.style.display = 'none';
-    }
-
-    // 创建可视化内容
-    let html = '<div class="support-flow reverse">\n';
-    html += '<div class="flow-title">被以下形式点支撑</div>\n';
-
-    // 显示目标点
-    html += '<div class="flow-source">\n';
-    html += '<div class="flow-node target-flow-node">\n';
-    html += `<div class="flow-node-label">目标点</div>\n`;
-    html += `<div class="flow-node-name">${targetId}</div>\n`;
-    html += '</div>\n';
-    html += '</div>\n';
-
-    // 显示支撑的形式点(分类)
-    html += '<div class="flow-targets">\n';
-
-    if (relations.concrete_element_forms && relations.concrete_element_forms.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">具体元素形式</div>\n';
-        relations.concrete_element_forms.forEach(item => {
-            const score = (item.score * 100).toFixed(0);
-            html += `<div class="flow-target-item concrete-element-form-flow">\n`;
-            html += `<div class="flow-target-text">${item.name}</div>\n`;
-            html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    if (relations.concrete_concept_forms && relations.concrete_concept_forms.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">具体概念形式</div>\n';
-        relations.concrete_concept_forms.forEach(item => {
-            const score = (item.score * 100).toFixed(0);
-            html += `<div class="flow-target-item concrete-concept-form-flow">\n`;
-            html += `<div class="flow-target-text">${item.name}</div>\n`;
-            html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    if (relations.overall_forms && relations.overall_forms.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">整体形式</div>\n';
-        relations.overall_forms.forEach(item => {
-            const score = (item.score * 100).toFixed(0);
-            html += `<div class="flow-target-item overall-form-flow">\n`;
-            html += `<div class="flow-target-text">${item.name}</div>\n`;
-            html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    if (relations.forms && relations.forms.length > 0) {
-        html += '<div class="flow-target-group">\n';
-        html += '<div class="flow-target-label">形式点</div>\n';
-        relations.forms.forEach(item => {
-            const score = (item.score * 100).toFixed(0);
-            html += `<div class="flow-target-item form-flow">\n`;
-            html += `<div class="flow-target-text">${item.name} <span style="font-size:0.8em;color:#6c757d;">(${item.type})</span></div>\n`;
-            html += `<div class="flow-target-score">相似度: ${score}%</div>\n`;
-            html += '</div>\n';
-        });
-        html += '</div>\n';
-    }
-
-    html += '</div>\n';
-    html += '</div>\n';
-
-    // 插入到画布
-    canvas.insertAdjacentHTML('beforeend', html);
-}
-
-// 清空形式点支撑关系画布
-function clearFormSupportCanvas() {
-    const canvas = document.getElementById('form-support-canvas');
-    if (!canvas) return;
-
-    // 移除所有flow相关元素
-    const flows = canvas.querySelectorAll('.support-flow');
-    flows.forEach(flow => flow.remove());
-
-    // 重新显示占位文本
-    const placeholder = canvas.querySelector('.placeholder-text');
-    if (placeholder) {
-        placeholder.style.display = 'flex';
-    }
-}
-
-// ===== Tab5 连线绘制功能 =====
-
-// 绘制Tab5所有连线
-function drawFormAllConnections() {
-    if (typeof tab5Relationships === 'undefined') {
-        console.log('tab5Relationships not defined, skipping connection drawing');
-        return;
-    }
-
-    const svg = document.getElementById('tab5-connection-svg');
-    if (!svg) {
-        console.log('SVG container not found');
-        return;
-    }
-
-    // 清空现有连线
-    svg.innerHTML = '';
-
-    // 1. 绘制左侧选题点到实质点的连线(反向关系)
-    drawLeftTargetToSubstanceConnections(svg);
-
-    // 2. 绘制实质点到形式点的连线(支撑关系)
-    drawSubstanceToFormConnections(svg);
-
-    // 3. 绘制形式点到右侧选题点的连线
-    drawFormToRightTargetConnections(svg);
-}
-
-// 绘制左侧选题点到实质点的连线
-function drawLeftTargetToSubstanceConnections(svg) {
-    const targetFromSubstance = tab5Relationships.target_from_substance;
-    
-    Object.keys(targetFromSubstance).forEach(targetId => {
-        const substances = targetFromSubstance[targetId];
-        
-        substances.forEach(substanceData => {
-            const sourceCard = document.querySelector(`.tab5-left-targets .target-card[data-id="${targetId}"]`);
-            const targetCard = document.querySelector(`.tab5-substances .substance-card[data-id="${substanceData.substance_id}"]`);
-            
-            if (sourceCard && targetCard && !sourceCard.classList.contains('hidden') && !targetCard.classList.contains('hidden')) {
-                drawTab5Connection(
-                    svg,
-                    sourceCard,
-                    targetCard,
-                    'left-target-substance',
-                    targetId,
-                    substanceData.substance_id,
-                    {
-                        score: substanceData.score,
-                        substanceName: substanceData.name,
-                        type: substanceData.type
-                    }
-                );
-            }
-        });
-    });
-}
-
-// 绘制实质点到形式点的连线
-function drawSubstanceToFormConnections(svg) {
-    const formToSubstance = tab5Relationships.form_to_substance;
-    
-    Object.keys(formToSubstance).forEach(formId => {
-        const substances = formToSubstance[formId];
-        
-        substances.forEach(substanceData => {
-            const sourceCard = document.querySelector(`.tab5-substances .substance-card[data-id="${substanceData.substance_id}"]`);
-            const targetCard = document.querySelector(`.tab5-forms .form-card[data-id="${formId}"]`);
-            
-            if (sourceCard && targetCard && !sourceCard.classList.contains('hidden') && !targetCard.classList.contains('hidden')) {
-                drawTab5Connection(
-                    svg,
-                    sourceCard,
-                    targetCard,
-                    'substance-form',
-                    substanceData.substance_id,
-                    formId,
-                    {
-                        substanceName: substanceData.name,
-                        formName: tab5Relationships.form_to_target[formId]?.name || ''
-                    }
-                );
-            }
-        });
-    });
-}
-
-// 绘制形式点到右侧选题点的连线
-function drawFormToRightTargetConnections(svg) {
-    const formToTarget = tab5Relationships.form_to_target;
-    
-    Object.keys(formToTarget).forEach(formId => {
-        const relations = formToTarget[formId];
-        
-        // 绘制到灵感点的连线
-        relations.inspiration.forEach(rel => {
-            const sourceCard = document.querySelector(`.tab5-forms .form-card[data-id="${formId}"]`);
-            const targetCard = document.querySelector(`.tab5-right-targets .target-card[data-id="${rel.target_id}"]`);
-            
-            if (sourceCard && targetCard && !sourceCard.classList.contains('hidden') && !targetCard.classList.contains('hidden')) {
-                // 兼容新结构(支撑)和旧结构(相似度分数)
-                const score = rel.avg_score !== undefined ? rel.avg_score : (rel.support_reason !== undefined ? 1.0 : 0.5);
-                drawTab5Connection(
-                    svg,
-                    sourceCard,
-                    targetCard,
-                    'form-inspiration',
-                    formId,
-                    rel.target_id,
-                    {
-                        score: score,
-                        point: rel.point,
-                        formName: relations.name
-                    }
-                );
-            }
-        });
-        
-        // 绘制到目的点的连线
-        relations.purpose.forEach(rel => {
-            const sourceCard = document.querySelector(`.tab5-forms .form-card[data-id="${formId}"]`);
-            const targetCard = document.querySelector(`.tab5-right-targets .target-card[data-id="${rel.target_id}"]`);
-            
-            if (sourceCard && targetCard && !sourceCard.classList.contains('hidden') && !targetCard.classList.contains('hidden')) {
-                // 兼容新结构(支撑)和旧结构(相似度分数)
-                const score = rel.avg_score !== undefined ? rel.avg_score : (rel.support_reason !== undefined ? 1.0 : 0.5);
-                drawTab5Connection(
-                    svg,
-                    sourceCard,
-                    targetCard,
-                    'form-purpose',
-                    formId,
-                    rel.target_id,
-                    {
-                        score: score,
-                        point: rel.point,
-                        formName: relations.name
-                    }
-                );
-            }
-        });
-        
-        // 绘制到关键点的连线
-        relations.keypoint.forEach(rel => {
-            const sourceCard = document.querySelector(`.tab5-forms .form-card[data-id="${formId}"]`);
-            const targetCard = document.querySelector(`.tab5-right-targets .target-card[data-id="${rel.target_id}"]`);
-            
-            if (sourceCard && targetCard && !sourceCard.classList.contains('hidden') && !targetCard.classList.contains('hidden')) {
-                // 兼容新结构(支撑)和旧结构(相似度分数)
-                const score = rel.avg_score !== undefined ? rel.avg_score : (rel.support_reason !== undefined ? 1.0 : 0.5);
-                drawTab5Connection(
-                    svg,
-                    sourceCard,
-                    targetCard,
-                    'form-keypoint',
-                    formId,
-                    rel.target_id,
-                    {
-                        score: score,
-                        point: rel.point,
-                        formName: relations.name
-                    }
-                );
-            }
-        });
-    });
-}
-
-// 绘制单条Tab5连线
-function drawTab5Connection(svg, sourceCard, targetCard, connectionType, sourceId, targetId, metadata) {
-    const svgContainer = svg.parentElement;
-    const containerRect = svgContainer.getBoundingClientRect();
-    const sourceRect = sourceCard.getBoundingClientRect();
-    const targetRect = targetCard.getBoundingClientRect();
-    
-    // 计算连线起点和终点
-    let x1, y1, x2, y2;
-    
-    if (connectionType === 'left-target-substance') {
-        // 左侧选题点到实质点:从右边连到左边
-        x1 = sourceRect.right - containerRect.left;
-        y1 = sourceRect.top + sourceRect.height / 2 - containerRect.top;
-        x2 = targetRect.left - containerRect.left;
-        y2 = targetRect.top + targetRect.height / 2 - containerRect.top;
-    } else if (connectionType === 'substance-form') {
-        // 实质点到形式点:从右边连到左边
-        x1 = sourceRect.right - containerRect.left;
-        y1 = sourceRect.top + sourceRect.height / 2 - containerRect.top;
-        x2 = targetRect.left - containerRect.left;
-        y2 = targetRect.top + targetRect.height / 2 - containerRect.top;
-    } else {
-        // 形式点到右侧选题点:从右边连到左边
-        x1 = sourceRect.right - containerRect.left;
-        y1 = sourceRect.top + sourceRect.height / 2 - containerRect.top;
-        x2 = targetRect.left - containerRect.left;
-        y2 = targetRect.top + targetRect.height / 2 - containerRect.top;
-    }
-    
-    // 创建贝塞尔曲线路径
-    const midX = (x1 + x2) / 2;
-    const path = `M ${x1} ${y1} Q ${midX} ${y1}, ${midX} ${(y1 + y2) / 2} T ${x2} ${y2}`;
-    
-    // 创建path元素
-    const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
-    pathElement.setAttribute('d', path);
-    pathElement.setAttribute('class', `tab5-connection-line ${connectionType}`);
-    pathElement.setAttribute('fill', 'none');
-    pathElement.setAttribute('stroke-width', '2');
-    pathElement.setAttribute('data-source-id', sourceId);
-    pathElement.setAttribute('data-target-id', targetId);
-    pathElement.setAttribute('data-connection-type', connectionType);
-    
-    // 保存元数据以供点击时使用
-    pathElement.setAttribute('data-metadata', JSON.stringify(metadata));
-    
-    // 添加点击事件
-    pathElement.style.cursor = 'pointer';
-    pathElement.addEventListener('click', function(e) {
-        e.stopPropagation();
-        showConnectionDetail(connectionType, sourceId, targetId, metadata);
-    });
-    
-    // 添加hover效果
-    pathElement.addEventListener('mouseenter', function() {
-        this.setAttribute('stroke-width', '4');
-        this.style.filter = 'drop-shadow(0 0 6px rgba(0,0,0,0.3))';
-    });
-    
-    pathElement.addEventListener('mouseleave', function() {
-        this.setAttribute('stroke-width', '2');
-        this.style.filter = 'none';
-    });
-    
-    svg.appendChild(pathElement);
-}
-
-// 显示连线详情模态框
-function showConnectionDetail(connectionType, sourceId, targetId, metadata) {
-    // 创建模态框
-    const modal = document.createElement('div');
-    modal.className = 'connection-modal-backdrop';
-    modal.onclick = function(e) {
-        if (e.target === modal) {
-            document.body.removeChild(modal);
-        }
-    };
-    
-    // 创建模态框内容
-    const modalContent = document.createElement('div');
-    modalContent.className = 'connection-modal-content';
-    modalContent.onclick = function(e) {
-        e.stopPropagation();
-    };
-    
-    // 构建内容
-    let html = '<div class="modal-header">';
-    html += '<h3>连线关系详情</h3>';
-    html += '<button class="modal-close" onclick="this.closest(\'.connection-modal-backdrop\').remove()">×</button>';
-    html += '</div>';
-    
-    html += '<div class="modal-body">';
-    
-    // 根据连接类型显示不同内容
-    if (connectionType === 'left-target-substance') {
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">连接类型:</div>';
-        html += '<div class="connection-value">选题点 → 实质点</div>';
-        html += '</div>';
-        
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">实质点:</div>';
-        html += '<div class="connection-value">' + (metadata.substanceName || sourceId) + '</div>';
-        html += '</div>';
-        
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">实质点类型:</div>';
-        html += '<div class="connection-value">' + (metadata.type || '-') + '</div>';
-        html += '</div>';
-        
-        if (metadata.score !== undefined) {
-            const scorePercent = (metadata.score * 100).toFixed(1);
-            html += '<div class="connection-detail-section">';
-            html += '<div class="connection-label">相似度评分:</div>';
-            html += '<div class="connection-value score-value">' + scorePercent + '%</div>';
-            html += '</div>';
-        }
-        
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">说明:</div>';
-        html += '<div class="connection-value">该实质点支撑了这个选题点的内容,相似度越高表示关联性越强</div>';
-        html += '</div>';
-        
-    } else if (connectionType === 'substance-form') {
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">连接类型:</div>';
-        html += '<div class="connection-value">实质点 → 形式点</div>';
-        html += '</div>';
-        
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">实质点:</div>';
-        html += '<div class="connection-value">' + (metadata.substanceName || sourceId) + '</div>';
-        html += '</div>';
-        
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">形式点:</div>';
-        html += '<div class="connection-value">' + (metadata.formName || targetId) + '</div>';
-        html += '</div>';
-        
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">说明:</div>';
-        html += '<div class="connection-value">该形式点是对实质点的具体表现形式</div>';
-        html += '</div>';
-        
-    } else if (connectionType.startsWith('form-')) {
-        const targetType = connectionType.split('-')[1]; // inspiration, purpose, keypoint
-        const targetTypeLabel = {
-            'inspiration': '灵感点',
-            'purpose': '目的点',
-            'keypoint': '关键点'
-        }[targetType] || targetType;
-        
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">连接类型:</div>';
-        html += '<div class="connection-value">形式点 → ' + targetTypeLabel + '</div>';
-        html += '</div>';
-        
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">形式点:</div>';
-        html += '<div class="connection-value">' + (metadata.formName || sourceId) + '</div>';
-        html += '</div>';
-        
-        if (metadata.point) {
-            html += '<div class="connection-detail-section">';
-            html += '<div class="connection-label">' + targetTypeLabel + ':</div>';
-            html += '<div class="connection-value">' + metadata.point + '</div>';
-            html += '</div>';
-        }
-        
-        if (metadata.score !== undefined) {
-            const scorePercent = (metadata.score * 100).toFixed(1);
-            html += '<div class="connection-detail-section">';
-            html += '<div class="connection-label">相似度评分:</div>';
-            html += '<div class="connection-value score-value">' + scorePercent + '%</div>';
-            html += '</div>';
-        }
-        
-        html += '<div class="connection-detail-section">';
-        html += '<div class="connection-label">说明:</div>';
-        html += '<div class="connection-value">该形式点支撑了这个选题点的呈现,相似度越高表示形式与选题的契合度越高</div>';
-        html += '</div>';
-    }
-    
-    html += '</div>';
-    
-    modalContent.innerHTML = html;
-    modal.appendChild(modalContent);
-    document.body.appendChild(modal);
-}

+ 0 - 2871
examples/html/visualize/style.css

@@ -1,2871 +0,0 @@
-* {
-    margin: 0;
-    padding: 0;
-    box-sizing: border-box;
-}
-
-body {
-    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-    line-height: 1.6;
-    color: #333;
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    padding: 20px;
-}
-
-.container {
-    max-width: 1400px;
-    margin: 0 auto;
-    background: white;
-    border-radius: 12px;
-    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
-    overflow: hidden;
-}
-
-.header {
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    color: white;
-    padding: 40px;
-    text-align: center;
-}
-
-.header h1 {
-    font-size: 2.5rem;
-    margin-bottom: 10px;
-    font-weight: 700;
-}
-
-.header .subtitle {
-    font-size: 1.1rem;
-    opacity: 0.9;
-}
-
-.tabs {
-    display: flex;
-    background: #f8f9fa;
-    border-bottom: 2px solid #dee2e6;
-}
-
-.tab {
-    flex: 1;
-    padding: 15px 20px;
-    text-align: center;
-    cursor: pointer;
-    background: #f8f9fa;
-    border: none;
-    font-size: 1.1rem;
-    font-weight: 600;
-    color: #6c757d;
-    transition: all 0.3s;
-    border-bottom: 3px solid transparent;
-}
-
-.tab:hover {
-    background: #e9ecef;
-    color: #495057;
-}
-
-.tab.active {
-    background: white;
-    color: #667eea;
-    border-bottom: 3px solid #667eea;
-}
-
-.content {
-    padding: 40px;
-}
-
-.tab-content {
-    animation: fadeIn 0.5s;
-}
-
-@keyframes fadeIn {
-    from { opacity: 0; transform: translateY(10px); }
-    to { opacity: 1; transform: translateY(0); }
-}
-
-.section {
-    margin-bottom: 30px;
-    padding: 20px;
-    background: #f8f9fa;
-    border-radius: 8px;
-}
-
-.section h3 {
-    font-size: 1.5rem;
-    margin-bottom: 15px;
-    color: #495057;
-    border-left: 4px solid #667eea;
-    padding-left: 12px;
-}
-
-.section h4 {
-    font-size: 1.2rem;
-    margin: 20px 0 10px 0;
-    color: #6c757d;
-}
-
-.topic-theme, .topic-desc, .script-category {
-    margin-bottom: 12px;
-    padding: 12px;
-    background: white;
-    border-radius: 6px;
-}
-
-.point-item {
-    margin-bottom: 15px;
-    padding: 15px;
-    background: white;
-    border-radius: 8px;
-    border-left: 4px solid #667eea;
-    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-
-.inspiration-item {
-    border-left-color: #10b981;
-}
-
-.purpose-item {
-    border-left-color: #3b82f6;
-}
-
-.keypoint-item {
-    border-left-color: #8b5cf6;
-}
-
-.point-header {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    flex-wrap: wrap;
-    margin-bottom: 10px;
-}
-
-.point-number {
-    display: inline-block;
-    background: #667eea;
-    color: white;
-    padding: 3px 10px;
-    border-radius: 4px;
-    font-weight: 600;
-    font-size: 0.85rem;
-    flex-shrink: 0;
-}
-
-.point-text {
-    flex: 1;
-    font-size: 1.05rem;
-    font-weight: 600;
-    color: #1f2937;
-}
-
-.point-description {
-    margin-top: 8px;
-    padding: 10px 12px;
-    background: #f9fafb;
-    border-radius: 4px;
-    font-size: 0.95rem;
-    color: #4b5563;
-    line-height: 1.6;
-}
-
-.point-features, .point-scoring {
-    margin-top: 12px;
-    padding: 10px 12px;
-    background: #f0f9ff;
-    border-radius: 4px;
-    font-size: 0.9rem;
-}
-
-.point-features strong, .point-scoring strong {
-    display: block;
-    margin-bottom: 8px;
-    color: #1e40af;
-}
-
-.feature-tags, .scoring-badges {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 8px;
-}
-
-.feature-tag {
-    display: inline-flex;
-    align-items: center;
-    gap: 5px;
-    padding: 5px 10px;
-    background: white;
-    border: 1px solid #dbeafe;
-    border-radius: 4px;
-    font-size: 0.85rem;
-    color: #1e40af;
-}
-
-.feature-dimension {
-    font-style: normal;
-    color: #6b7280;
-    font-size: 0.8rem;
-}
-
-.feature-weight {
-    color: #dc2626;
-    font-size: 0.8rem;
-}
-
-.dimension-tags {
-    display: flex;
-    gap: 6px;
-    align-items: center;
-}
-
-.dimension-tag {
-    display: inline-block;
-    padding: 3px 10px;
-    border-radius: 4px;
-    font-size: 0.8rem;
-    font-weight: 600;
-}
-
-.dimension-tag.level-1 {
-    background: #dbeafe;
-    color: #1e40af;
-}
-
-.dimension-tag.level-2 {
-    background: #fef3c7;
-    color: #92400e;
-}
-
-.dimension-tag.keypoint-dimension {
-    background: #f3e8ff;
-    color: #6b21a8;
-}
-
-.scoring-badge {
-    display: inline-block;
-    padding: 5px 12px;
-    background: white;
-    border: 1px solid #e5e7eb;
-    border-radius: 4px;
-    font-size: 0.85rem;
-    color: #374151;
-}
-
-.scoring-badge.total-score {
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    color: white;
-    border: none;
-    font-weight: 600;
-}
-
-.paragraph-list {
-    list-style: none;
-    margin-left: 0;
-}
-
-.paragraph-list:not(.level-0) {
-    margin-left: 30px;
-    margin-top: 10px;
-    padding-left: 15px;
-    position: relative;
-}
-
-.paragraph-list:not(.level-0)::before {
-    content: '';
-    position: absolute;
-    left: 0;
-    top: 0;
-    bottom: 15px;
-    width: 2px;
-    background: #dee2e6;
-}
-
-.paragraph-item {
-    margin-bottom: 15px;
-    padding: 15px;
-    background: white;
-    border-radius: 6px;
-    border-left: 4px solid #667eea;
-}
-
-.paragraph-header {
-    margin-bottom: 10px;
-    display: flex;
-    align-items: flex-start;
-    gap: 8px;
-    justify-content: space-between;
-}
-
-.paragraph-header-content {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    flex-wrap: wrap;
-    flex: 1;
-    min-width: 0;
-}
-
-.paragraph-item.collapsible .paragraph-header {
-    cursor: pointer;
-    user-select: none;
-}
-
-.paragraph-item.collapsible .paragraph-header:hover {
-    background-color: #f8f9fa;
-    padding: 8px;
-    margin: -8px -8px 2px -8px;
-    border-radius: 4px;
-}
-
-.toggle-icon {
-    font-size: 0.8rem;
-    color: #667eea;
-    transition: transform 0.3s ease;
-    flex-shrink: 0;
-}
-
-.paragraph-item.collapsed .toggle-icon {
-    transform: rotate(-90deg);
-}
-
-.collapsible-content {
-    overflow: hidden;
-    transition: max-height 0.3s ease, opacity 0.3s ease;
-    max-height: 10000px;
-    opacity: 1;
-}
-
-.paragraph-item.collapsed .collapsible-content {
-    max-height: 0;
-    opacity: 0;
-    margin: 0;
-}
-
-.paragraph-index {
-    display: inline-block;
-    background: #667eea;
-    color: white;
-    padding: 4px 12px;
-    border-radius: 4px;
-    font-weight: 600;
-    margin-right: 10px;
-    flex-shrink: 0;
-}
-
-.paragraph-desc {
-    color: #495057;
-    font-weight: 500;
-}
-
-.paragraph-content, .paragraph-reason {
-    margin-top: 8px;
-    padding: 10px 12px;
-    background: #f1f3f5;
-    border-radius: 4px;
-    font-size: 0.9rem;
-    color: #495057;
-    line-height: 1.6;
-    border-left: 3px solid #adb5bd;
-}
-
-.content-range-list {
-    list-style: none;
-    margin: 8px 0 0 0;
-    padding: 0;
-}
-
-.content-range-list li {
-    padding: 6px 12px;
-    margin: 4px 0;
-    background: white;
-    border-radius: 4px;
-    border-left: 3px solid #667eea;
-}
-
-.content-range-list li:before {
-    content: "▪ ";
-    color: #667eea;
-    font-weight: bold;
-    margin-right: 8px;
-}
-
-.details-toggle-btn {
-    padding: 6px 12px;
-    background: #e3f2fd;
-    border-radius: 4px;
-    cursor: pointer;
-    user-select: none;
-    color: #1976d2;
-    font-weight: 500;
-    font-size: 0.85rem;
-    transition: background 0.2s;
-    white-space: nowrap;
-    flex-shrink: 0;
-}
-
-.details-toggle-btn:hover {
-    background: #bbdefb;
-}
-
-.details-icon {
-    display: inline-block;
-    transition: transform 0.3s ease;
-    margin-right: 5px;
-}
-
-.paragraph-details:not(.collapsed) .details-icon {
-    transform: rotate(90deg);
-}
-
-.details-content {
-    max-height: 0;
-    opacity: 0;
-    overflow: hidden;
-    transition: max-height 0.3s ease, opacity 0.3s ease, margin-top 0.3s ease;
-}
-
-.paragraph-details:not(.collapsed) .details-content {
-    max-height: 5000px;
-    opacity: 1;
-    margin-top: 10px;
-}
-
-.paragraph-details {
-    margin-top: 10px;
-}
-
-.related-images {
-    display: grid;
-    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
-    gap: 15px;
-    margin-top: 15px;
-    padding: 15px;
-    background: #f8f9fa;
-    border-radius: 8px;
-}
-
-.image-item {
-    position: relative;
-    background: white;
-    border-radius: 6px;
-    overflow: hidden;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-    transition: transform 0.2s, box-shadow 0.2s;
-}
-
-.image-item:hover {
-    transform: translateY(-2px);
-    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
-}
-
-.image-item img {
-    width: 100%;
-    height: auto;
-    display: block;
-}
-
-.image-label {
-    position: absolute;
-    top: 8px;
-    right: 8px;
-    background: rgba(102, 126, 234, 0.9);
-    color: white;
-    padding: 4px 10px;
-    border-radius: 4px;
-    font-size: 0.85rem;
-    font-weight: 600;
-    backdrop-filter: blur(4px);
-}
-
-.category-list {
-    list-style: none;
-    padding: 0;
-}
-
-.category-item {
-    margin-bottom: 15px;
-    padding: 15px;
-    background: white;
-    border-radius: 6px;
-    border-left: 4px solid #667eea;
-}
-
-.category-name {
-    font-size: 1.1rem;
-    font-weight: 600;
-    color: #495057;
-    margin-bottom: 8px;
-}
-
-.category-definition {
-    font-size: 0.95rem;
-    color: #6c757d;
-    line-height: 1.6;
-}
-
-/* Tab3 元素列表样式 */
-.global-controls {
-    display: flex;
-    gap: 10px;
-    margin-bottom: 20px;
-    justify-content: space-between;
-    align-items: center;
-    flex-wrap: wrap;
-}
-
-/* 颜色图例样式 */
-.color-legend {
-    display: flex;
-    align-items: center;
-    gap: 12px;
-    flex-wrap: wrap;
-}
-
-.legend-title {
-    font-size: 0.9rem;
-    color: #6c757d;
-    font-weight: 500;
-}
-
-.legend-item {
-    display: inline-flex;
-    align-items: center;
-    padding: 4px 12px;
-    border-radius: 4px;
-    font-size: 0.8rem;
-    font-weight: 600;
-    color: white;
-}
-
-.legend-item::before {
-    content: '';
-    display: inline-block;
-    width: 12px;
-    height: 12px;
-    border-radius: 2px;
-    margin-right: 6px;
-}
-
-.legend-coverage {
-    background: rgba(16, 185, 129, 0.15);
-    color: #10b981;
-    border: 1px solid #10b981;
-}
-
-.legend-coverage::before {
-    background: #10b981;
-}
-
-.legend-frequency {
-    background: rgba(59, 130, 246, 0.15);
-    color: #3b82f6;
-    border: 1px solid #3b82f6;
-}
-
-.legend-frequency::before {
-    background: #3b82f6;
-}
-
-.legend-similarity {
-    background: rgba(139, 92, 246, 0.15);
-    color: #8b5cf6;
-    border: 1px solid #8b5cf6;
-}
-
-.legend-similarity::before {
-    background: #8b5cf6;
-}
-
-.control-btn {
-    padding: 8px 16px;
-    background: #667eea;
-    color: white;
-    border: none;
-    border-radius: 6px;
-    font-size: 0.9rem;
-    font-weight: 600;
-    cursor: pointer;
-    transition: all 0.2s;
-}
-
-.control-btn:hover {
-    background: #5568d3;
-    transform: translateY(-1px);
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
-}
-
-.category-group {
-    margin-bottom: 30px;
-    padding: 20px;
-    background: white;
-    border-radius: 8px;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-.category-group.collapsible .category-header {
-    cursor: pointer;
-    user-select: none;
-}
-
-.category-group.collapsible .category-header:hover {
-    background: #f8f9fa;
-    margin: -10px;
-    padding: 10px;
-    border-radius: 6px;
-}
-
-.category-header {
-    margin-bottom: 15px;
-    padding-bottom: 10px;
-    border-bottom: 2px solid #e9ecef;
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    flex-wrap: wrap;
-}
-
-.category-toggle-icon {
-    font-size: 0.8rem;
-    color: #667eea;
-    transition: transform 0.3s ease;
-    flex-shrink: 0;
-}
-
-.category-group.collapsed .category-toggle-icon {
-    transform: rotate(-90deg);
-}
-
-.category-title {
-    font-size: 1.2rem;
-    font-weight: 600;
-    color: #667eea;
-    margin: 0;
-    flex-shrink: 0;
-}
-
-.category-stats {
-    display: flex;
-    gap: 10px;
-    flex-wrap: wrap;
-}
-
-.category-content {
-    overflow: hidden;
-    transition: max-height 0.3s ease, opacity 0.3s ease;
-    max-height: 10000px;
-    opacity: 1;
-}
-
-.category-group.collapsed .category-content {
-    max-height: 0;
-    opacity: 0;
-}
-
-.element-list {
-    list-style: none;
-    padding: 0;
-    margin: 0;
-}
-
-.element-item {
-    margin-bottom: 12px;
-    padding: 12px;
-    background: #f8f9fa;
-    border-radius: 6px;
-    border-left: 3px solid #667eea;
-    transition: all 0.2s;
-}
-
-/* 主导因素边框颜色 - 覆盖率主导(绿色系) */
-.element-item.dominant-coverage {
-    border-left: 4px solid #10b981;
-    background: linear-gradient(to right, rgba(16, 185, 129, 0.05), transparent);
-}
-
-/* 主导因素边框颜色 - 频次主导(蓝色系) */
-.element-item.dominant-frequency {
-    border-left: 4px solid #3b82f6;
-    background: linear-gradient(to right, rgba(59, 130, 246, 0.05), transparent);
-}
-
-/* 主导因素边框颜色 - 相似度主导(紫色系) */
-.element-item.dominant-similarity {
-    border-left: 4px solid #8b5cf6;
-    background: linear-gradient(to right, rgba(139, 92, 246, 0.05), transparent);
-}
-
-.element-item:hover {
-    background: #e9ecef;
-}
-
-.element-header {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    flex-wrap: wrap;
-    cursor: pointer;
-    user-select: none;
-}
-
-.element-toggle-icon {
-    font-size: 0.7rem;
-    color: #667eea;
-    transition: transform 0.3s ease;
-    flex-shrink: 0;
-}
-
-.element-item.expanded .element-toggle-icon {
-    transform: rotate(90deg);
-}
-
-.element-name {
-    font-size: 1rem;
-    font-weight: 600;
-    color: #495057;
-    flex-shrink: 0;
-}
-
-.element-stats {
-    display: flex;
-    gap: 8px;
-    flex-wrap: wrap;
-    margin-left: auto;
-}
-
-.element-description {
-    margin-top: 8px;
-    font-size: 0.9rem;
-    color: #6c757d;
-    line-height: 1.5;
-}
-
-.element-details {
-    margin-top: 10px;
-    padding: 15px;
-    background: white;
-    border-radius: 6px;
-    border: 1px solid #dee2e6;
-    overflow: hidden;
-    transition: max-height 0.3s ease, opacity 0.3s ease;
-    max-height: 5000px;
-    opacity: 1;
-}
-
-.element-details.collapsed {
-    max-height: 0;
-    opacity: 0;
-    padding: 0;
-    margin: 0;
-    border: none;
-}
-
-.detail-section {
-    margin-bottom: 15px;
-}
-
-.detail-section:last-child {
-    margin-bottom: 0;
-}
-
-.detail-section strong {
-    display: block;
-    margin-bottom: 8px;
-    color: #495057;
-    font-size: 0.9rem;
-}
-
-.detail-content {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 6px;
-}
-
-.detail-tag {
-    display: inline-block;
-    padding: 4px 10px;
-    background: #e9ecef;
-    border-radius: 4px;
-    font-size: 0.85rem;
-    color: #495057;
-}
-
-.score-type {
-    font-weight: 600;
-    color: #667eea;
-    margin: 10px 0 5px 0;
-    font-size: 0.9rem;
-}
-
-.score-list {
-    display: flex;
-    flex-direction: column;
-    gap: 8px;
-}
-
-.score-item {
-    padding: 8px 12px;
-    background: #f8f9fa;
-    border-radius: 4px;
-    border-left: 3px solid #667eea;
-}
-
-.score-point {
-    font-size: 0.9rem;
-    color: #495057;
-    margin-bottom: 5px;
-}
-
-.score-values {
-    display: flex;
-    gap: 8px;
-}
-
-.score-badge {
-    display: inline-block;
-    padding: 2px 8px;
-    background: #e3f2fd;
-    border: 1px solid #90caf9;
-    border-radius: 3px;
-    font-size: 0.75rem;
-    color: #1976d2;
-}
-
-.stat-badge {
-    display: inline-block;
-    padding: 3px 10px;
-    background: #667eea;
-    color: white;
-    border-radius: 4px;
-    font-size: 0.75rem;
-    font-weight: 500;
-    white-space: nowrap;
-}
-
-/* 不同类型的统计badge颜色 */
-.stat-badge.stat-coverage {
-    background: #6b7280;
-}
-
-.stat-badge.stat-frequency {
-    background: #6b7280;
-}
-
-.stat-badge.stat-similarity {
-    background: #6b7280;
-}
-
-/* 高亮状态 - 主导因素 */
-.stat-badge.stat-highlight.stat-coverage {
-    background: #10b981;
-    box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.3);
-}
-
-.stat-badge.stat-highlight.stat-frequency {
-    background: #3b82f6;
-    box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
-}
-
-.stat-badge.stat-highlight.stat-similarity {
-    background: #8b5cf6;
-    box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3);
-}
-
-.detail-text {
-    padding: 10px 12px;
-    background: #f1f3f5;
-    border-radius: 4px;
-    font-size: 0.9rem;
-    color: #495057;
-    line-height: 1.6;
-    border-left: 3px solid #adb5bd;
-}
-
-.score-reasons {
-    margin-top: 8px;
-    padding: 8px 12px;
-    background: #f9fafb;
-    border-radius: 4px;
-    border-left: 2px solid #dee2e6;
-}
-
-.score-reason {
-    font-size: 0.85rem;
-    color: #6c757d;
-    line-height: 1.5;
-    margin-bottom: 6px;
-}
-
-.score-reason:last-child {
-    margin-bottom: 0;
-}
-
-.score-reason strong {
-    color: #495057;
-}
-
-/* 新增:元素ID样式 */
-.element-id {
-    display: inline-block;
-    background: #6c757d;
-    color: white;
-    padding: 2px 8px;
-    border-radius: 4px;
-    font-size: 0.75rem;
-    font-weight: 600;
-    margin-right: 8px;
-    flex-shrink: 0;
-}
-
-/* 维度标签样式 */
-.dimension-level1 {
-    background: #e8f5e9;
-    color: #2e7d32;
-    border: 1px solid #a5d6a7;
-}
-
-.dimension-level2 {
-    background: #f3e5f5;
-    color: #7b1fa2;
-    border: 1px solid #ce93d8;
-}
-
-/* 分类标签样式 */
-.category-level1 {
-    background: #e3f2fd;
-    color: #1565c0;
-    border: 1px solid #90caf9;
-}
-
-.category-level2 {
-    background: #fff3e0;
-    color: #e65100;
-    border: 1px solid #ffcc80;
-}
-
-/* 来源标签样式 */
-.source-tag {
-    background: #fce4ec;
-    color: #c2185b;
-    border: 1px solid #f48fb1;
-}
-
-/* 段落ID标签样式 */
-.para-id-tag {
-    background: #667eea;
-    color: white;
-    font-weight: 600;
-}
-
-/* 出现段落详情列表样式 */
-.paragraphs-detail-list {
-    display: flex;
-    flex-direction: column;
-    gap: 10px;
-}
-
-.paragraph-detail-item {
-    padding: 10px 12px;
-    background: #f8f9fa;
-    border-radius: 6px;
-    border-left: 3px solid #667eea;
-}
-
-.para-how {
-    margin-top: 8px;
-    font-size: 0.9rem;
-    color: #495057;
-    line-height: 1.5;
-    padding: 8px 12px;
-    background: white;
-    border-radius: 4px;
-}
-
-.footer {
-    background: #f8f9fa;
-    padding: 20px;
-    text-align: center;
-    color: #6c757d;
-    border-top: 1px solid #dee2e6;
-}
-
-/* 层级结构样式 */
-.level1-section {
-    margin-bottom: 30px;
-}
-
-.level1-header {
-    display: flex;
-    align-items: center;
-    gap: 12px;
-    padding: 20px;
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    border-radius: 8px;
-    cursor: pointer;
-    user-select: none;
-    transition: all 0.3s;
-}
-
-.level1-header:hover {
-    transform: translateY(-2px);
-    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
-}
-
-.level1-header.collapsed {
-    opacity: 0.8;
-}
-
-.level1-title {
-    color: white;
-    font-size: 1.5rem;
-    font-weight: 700;
-    margin: 0;
-}
-
-.level1-content {
-    padding: 20px 0;
-    overflow: hidden;
-    transition: max-height 0.4s ease, opacity 0.4s ease;
-    max-height: 100000px;
-    opacity: 1;
-}
-
-.level1-header.collapsed + .level1-content {
-    max-height: 0;
-    opacity: 0;
-    padding: 0;
-}
-
-.level2-section {
-    margin-bottom: 25px;
-    padding: 0 15px;
-}
-
-.level2-header {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    padding: 15px 20px;
-    background: #f8f9fa;
-    border-radius: 8px;
-    border-left: 4px solid #667eea;
-    cursor: pointer;
-    user-select: none;
-    transition: all 0.2s;
-}
-
-.level2-header:hover {
-    background: #e9ecef;
-}
-
-.level2-header.collapsed {
-    opacity: 0.7;
-}
-
-.level2-title {
-    color: #495057;
-    font-size: 1.2rem;
-    font-weight: 600;
-    margin: 0;
-}
-
-.level2-content {
-    padding: 15px 0;
-    overflow: hidden;
-    transition: max-height 0.3s ease, opacity 0.3s ease;
-    max-height: 100000px;
-    opacity: 1;
-}
-
-.level2-header.collapsed + .level2-content {
-    max-height: 0;
-    opacity: 0;
-    padding: 0;
-}
-
-.level-toggle-icon {
-    font-size: 1rem;
-    color: inherit;
-    transition: transform 0.3s ease;
-    flex-shrink: 0;
-}
-
-.collapsed .level-toggle-icon {
-    transform: rotate(-90deg);
-}
-
-.category-group {
-    margin: 15px 0 15px 20px;
-}
-
-/* 二级分类样式 */
-.subcategory-group {
-    margin: 10px 0 10px 20px;
-    padding: 15px;
-    background: #fafafa;
-    border-radius: 6px;
-    border-left: 3px solid #a5b4fc;
-}
-
-.subcategory-group.collapsible .subcategory-header {
-    cursor: pointer;
-    user-select: none;
-}
-
-.subcategory-group.collapsible .subcategory-header:hover {
-    background: #f0f0f0;
-    margin: -8px;
-    padding: 8px;
-    border-radius: 4px;
-}
-
-.subcategory-header {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    flex-wrap: wrap;
-    margin-bottom: 10px;
-}
-
-.subcategory-toggle-icon {
-    font-size: 0.7rem;
-    color: #a5b4fc;
-    transition: transform 0.3s ease;
-    flex-shrink: 0;
-}
-
-.subcategory-group.collapsed .subcategory-toggle-icon {
-    transform: rotate(-90deg);
-}
-
-.subcategory-title {
-    font-size: 1rem;
-    font-weight: 600;
-    color: #6366f1;
-    margin: 0;
-}
-
-.subcategory-stats {
-    display: flex;
-    gap: 6px;
-    flex-wrap: wrap;
-}
-
-.subcategory-content {
-    overflow: hidden;
-    transition: max-height 0.3s ease, opacity 0.3s ease;
-    max-height: 10000px;
-    opacity: 1;
-}
-
-.subcategory-group.collapsed .subcategory-content {
-    max-height: 0;
-    opacity: 0;
-}
-
-.stat-badge-small {
-    display: inline-block;
-    padding: 2px 8px;
-    background: #a5b4fc;
-    color: white;
-    border-radius: 3px;
-    font-size: 0.7rem;
-    font-weight: 500;
-    white-space: nowrap;
-}
-
-/* ===== 新增字段样式 ===== */
-
-/* 类型标签 */
-.type-tag {
-    background: #e0f2fe;
-    color: #0369a1;
-    border: 1px solid #7dd3fc;
-    font-weight: 600;
-}
-
-/* 推理层次标签 */
-.level-tag {
-    background: #fef3c7;
-    color: #d97706;
-    border: 1px solid #fcd34d;
-    font-weight: 600;
-}
-
-/* 依附于标签 */
-.depends-tag {
-    background: #fce7f3;
-    color: #be185d;
-    border: 1px solid #f9a8d4;
-}
-
-/* 映射关系容器 */
-.mapping-container {
-    padding: 10px 0;
-}
-
-.mapping-group {
-    margin-bottom: 12px;
-    padding: 10px 12px;
-    background: #f9fafb;
-    border-radius: 4px;
-    border-left: 3px solid #9ca3af;
-}
-
-.mapping-group:last-child {
-    margin-bottom: 0;
-}
-
-.mapping-label {
-    display: block;
-    font-weight: 600;
-    color: #374151;
-    margin-bottom: 8px;
-    font-size: 0.85rem;
-}
-
-/* 映射关系标签 */
-.mapping-tag {
-    background: #ede9fe;
-    color: #6b21a8;
-    border: 1px solid #c4b5fd;
-    font-weight: 600;
-}
-
-/* ===== 元素锚点系统 - 模态框样式 ===== */
-
-/* 模态框背景遮罩 */
-.element-modal-backdrop {
-    position: fixed;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    background: rgba(0, 0, 0, 0.5);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    z-index: 2000;
-    animation: fadeIn 0.2s ease;
-}
-
-/* 模态框内容容器 */
-.element-modal-content {
-    background: white;
-    border-radius: 12px;
-    padding: 0;
-    width: 90%;
-    overflow-y: auto;
-    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
-    animation: slideUp 0.3s ease;
-}
-
-@keyframes slideUp {
-    from {
-        transform: translateY(20px);
-        opacity: 0;
-    }
-    to {
-        transform: translateY(0);
-        opacity: 1;
-    }
-}
-
-/* 模态框头部 */
-.modal-header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    padding: 20px 24px;
-    border-bottom: 2px solid #e9ecef;
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    border-radius: 12px 12px 0 0;
-}
-
-.modal-header h3 {
-    margin: 0;
-    font-size: 1.3rem;
-    font-weight: 700;
-    color: white;
-}
-
-/* 模态框关闭按钮 */
-.modal-close {
-    background: rgba(255, 255, 255, 0.2);
-    border: none;
-    color: white;
-    font-size: 1.5rem;
-    width: 32px;
-    height: 32px;
-    border-radius: 50%;
-    cursor: pointer;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    transition: all 0.2s;
-    padding: 0;
-    line-height: 1;
-}
-
-.modal-close:hover {
-    background: rgba(255, 255, 255, 0.3);
-    transform: rotate(90deg);
-}
-
-/* 模态框主体 */
-.modal-body {
-    padding: 24px;
-}
-
-.modal-section {
-    margin-bottom: 16px;
-}
-
-.modal-section:last-child {
-    margin-bottom: 0;
-}
-
-.modal-section strong {
-    display: block;
-    margin-bottom: 8px;
-    color: #495057;
-    font-size: 0.9rem;
-}
-
-.modal-section p {
-    margin: 0;
-    color: #6c757d;
-    line-height: 1.6;
-}
-
-/* 模态框底部 */
-.modal-footer {
-    padding: 16px 24px;
-    border-top: 1px solid #e9ecef;
-    display: flex;
-    justify-content: flex-end;
-    gap: 12px;
-}
-
-/* 模态框按钮 */
-.modal-btn {
-    padding: 10px 20px;
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    color: white;
-    border: none;
-    border-radius: 6px;
-    font-size: 0.9rem;
-    font-weight: 600;
-    cursor: pointer;
-    transition: all 0.2s;
-}
-
-.modal-btn:hover {
-    transform: translateY(-1px);
-    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
-}
-
-/* 高亮动画(用于跳转后的元素高亮) */
-.highlight-pulse {
-    animation: highlightPulse 2s ease;
-}
-
-@keyframes highlightPulse {
-    0%, 100% {
-        background: #f8f9fa;
-    }
-    50% {
-        background: rgba(102, 126, 234, 0.2);
-        box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.3);
-    }
-}
-
-/* ===== Tab4 关系图样式 ===== */
-
-/* 关系图容器 */
-.relationship-container {
-    display: flex;
-    gap: 20px;
-    min-height: 600px;
-    position: relative;
-    background: #f8f9fa;
-    padding: 20px;
-    border-radius: 8px;
-}
-
-/* 左侧实质点列表 */
-.substance-column {
-    flex: 0 0 300px;
-    display: flex;
-    flex-direction: column;
-}
-
-/* 中间连线区域 */
-.connection-column {
-    flex: 1;
-    min-width: 200px;
-    position: relative;
-}
-
-/* 右侧目标点列表 */
-.target-column {
-    flex: 0 0 350px;
-    display: flex;
-    flex-direction: column;
-    gap: 20px;
-}
-
-/* 列标题 */
-.column-title {
-    font-size: 1.2rem;
-    font-weight: 600;
-    color: #495057;
-    margin-bottom: 15px;
-    padding: 10px;
-    background: white;
-    border-radius: 6px;
-    text-align: center;
-    border-left: 4px solid #667eea;
-}
-
-.section-title {
-    font-size: 1rem;
-    font-weight: 600;
-    color: #495057;
-    margin-bottom: 10px;
-    padding: 8px 12px;
-    background: white;
-    border-radius: 6px;
-    border-left: 3px solid #667eea;
-}
-
-/* 节点列表 */
-.node-list {
-    display: flex;
-    flex-direction: column;
-    gap: 10px;
-    overflow-y: auto;
-    max-height: calc(100vh - 300px);
-    padding-right: 5px;
-}
-
-/* 自定义滚动条 */
-.node-list::-webkit-scrollbar {
-    width: 6px;
-}
-
-.node-list::-webkit-scrollbar-track {
-    background: #e9ecef;
-    border-radius: 3px;
-}
-
-.node-list::-webkit-scrollbar-thumb {
-    background: #adb5bd;
-    border-radius: 3px;
-}
-
-.node-list::-webkit-scrollbar-thumb:hover {
-    background: #6c757d;
-}
-
-/* 节点基础样式 */
-.node {
-    padding: 12px 15px;
-    background: white;
-    border-radius: 6px;
-    cursor: pointer;
-    transition: all 0.3s;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-    position: relative;
-}
-
-.node:hover {
-    transform: translateX(5px);
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-}
-
-/* 实质点节点 */
-.substance-node {
-    border-left: 4px solid #667eea;
-}
-
-.substance-node:hover {
-    border-left-color: #5568d3;
-    background: linear-gradient(to right, rgba(102, 126, 234, 0.05), white);
-}
-
-.substance-node.highlighted {
-    background: linear-gradient(to right, rgba(102, 126, 234, 0.15), rgba(102, 126, 234, 0.05));
-    border-left-color: #5568d3;
-    box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
-}
-
-/* 目标点节点 */
-.target-node {
-    border-left: 4px solid #adb5bd;
-}
-
-/* 灵感点节点 */
-.inspiration-node {
-    border-left-color: #10b981;
-}
-
-.inspiration-node.highlighted {
-    background: linear-gradient(to right, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.05));
-    border-left-color: #059669;
-    box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3);
-}
-
-/* 目的点节点 */
-.purpose-node {
-    border-left-color: #3b82f6;
-}
-
-.purpose-node.highlighted {
-    background: linear-gradient(to right, rgba(59, 130, 246, 0.15), rgba(59, 130, 246, 0.05));
-    border-left-color: #2563eb;
-    box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
-}
-
-/* 关键点节点 */
-.keypoint-node {
-    border-left-color: #8b5cf6;
-}
-
-.keypoint-node.highlighted {
-    background: linear-gradient(to right, rgba(139, 92, 246, 0.15), rgba(139, 92, 246, 0.05));
-    border-left-color: #7c3aed;
-    box-shadow: 0 4px 16px rgba(139, 92, 246, 0.3);
-}
-
-/* 节点内容样式 */
-.node-id, .node-number {
-    display: inline-block;
-    background: #6c757d;
-    color: white;
-    padding: 2px 8px;
-    border-radius: 4px;
-    font-size: 0.75rem;
-    font-weight: 600;
-    margin-bottom: 6px;
-}
-
-.node-name, .node-text {
-    font-size: 0.9rem;
-    font-weight: 500;
-    color: #495057;
-    line-height: 1.4;
-}
-
-.node-dimension {
-    font-size: 0.8rem;
-    color: #6c757d;
-    margin-top: 4px;
-}
-
-/* SVG 连线样式 */
-.relationship-svg {
-    width: 100%;
-    height: 100%;
-    position: absolute;
-    top: 0;
-    left: 0;
-    pointer-events: none;
-}
-
-.connection-line {
-    stroke-width: 2;
-    opacity: 0.3;
-    transition: all 0.3s;
-}
-
-.connection-line.highlighted {
-    stroke-width: 3;
-    opacity: 1;
-}
-
-/* 不同类型连线颜色 */
-.connection-line.inspiration {
-    stroke: #10b981;
-}
-
-.connection-line.purpose {
-    stroke: #3b82f6;
-}
-
-.connection-line.keypoint {
-    stroke: #8b5cf6;
-}
-
-/* 连线标签(显示相似度) */
-.connection-label {
-    font-size: 0.7rem;
-    fill: #6c757d;
-    opacity: 0;
-    transition: opacity 0.3s;
-    pointer-events: none;
-}
-
-.connection-label.highlighted {
-    opacity: 1;
-}
-
-/* 目标区域 */
-.target-section {
-    background: white;
-    border-radius: 8px;
-    padding: 15px;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-/* 空状态 */
-.empty-state {
-    padding: 60px 40px;
-    text-align: center;
-    color: #6c757d;
-    font-size: 1.1rem;
-    background: white;
-    border-radius: 8px;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-/* ===== Tab4 新增交互样式 ===== */
-
-/* 顶部标题和重置按钮 */
-.tab4-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 20px;
-    padding: 15px 20px;
-    background: white;
-    border-radius: 8px;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-.tab4-title {
-    font-size: 1.1rem;
-    font-weight: 600;
-    color: #495057;
-}
-
-.reset-btn {
-    padding: 8px 16px;
-    background: #667eea;
-    color: white;
-    border: none;
-    border-radius: 6px;
-    font-size: 0.9rem;
-    font-weight: 600;
-    cursor: pointer;
-    transition: all 0.2s;
-}
-
-.reset-btn:hover {
-    background: #5568d3;
-    transform: translateY(-1px);
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
-}
-
-/* 实质点选中状态 */
-.substance-node.selected {
-    background: linear-gradient(to right, rgba(102, 126, 234, 0.2), rgba(102, 126, 234, 0.08));
-    border-left-color: #5568d3;
-    box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
-    transform: scale(1.02);
-}
-
-/* 实质点变暗状态(未选中时) */
-.substance-node.dimmed {
-    opacity: 0.4;
-    filter: grayscale(0.5);
-}
-
-.substance-node.dimmed:hover {
-    opacity: 0.6;
-}
-
-/* 右侧节点隐藏状态 */
-.target-node.hidden {
-    display: none;
-}
-
-.target-node.visible {
-    display: block;
-    animation: fadeInNode 0.3s ease;
-}
-
-@keyframes fadeInNode {
-    from {
-        opacity: 0;
-        transform: translateX(-10px);
-    }
-    to {
-        opacity: 1;
-        transform: translateX(0);
-    }
-}
-
-/* Section的隐藏和显示 */
-.target-section.all-hidden {
-    display: none;
-}
-
-.target-section.has-visible-nodes {
-    display: block;
-    animation: fadeIn 0.3s ease;
-}
-
-/* 关联实质点节点样式 */
-.substance-related-node {
-    border-left-color: #f59e0b;
-    cursor: pointer;
-}
-
-.substance-related-node:hover {
-    border-left-color: #d97706;
-    background: linear-gradient(to right, rgba(245, 158, 11, 0.1), white);
-}
-
-.substance-related-node.visible {
-    background: linear-gradient(to right, rgba(245, 158, 11, 0.08), white);
-}
-
-/* 实质点之间的连线样式 */
-.connection-line.substance {
-    stroke: #f59e0b;
-    stroke-dasharray: 5, 5;
-}
-
-.connection-line.substance.highlighted {
-    stroke-width: 3;
-    opacity: 0.8;
-}
-
-/* ===== Tab5 形式点关系图样式 ===== */
-
-/* 顶部标题和重置按钮 */
-.tab5-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 20px;
-    padding: 15px 20px;
-    background: white;
-    border-radius: 8px;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-.tab5-title {
-    font-size: 1.1rem;
-    font-weight: 600;
-    color: #495057;
-}
-
-/* 形式点节点样式 */
-.form-column {
-    flex: 0 0 300px;
-    display: flex;
-    flex-direction: column;
-}
-
-.form-node {
-    border-left: 4px solid #8b5cf6;
-}
-
-.form-node:hover {
-    border-left-color: #7c3aed;
-    background: linear-gradient(to right, rgba(139, 92, 246, 0.05), white);
-}
-
-.form-node.selected {
-    background: linear-gradient(to right, rgba(139, 92, 246, 0.2), rgba(139, 92, 246, 0.08));
-    border-left-color: #7c3aed;
-    box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4);
-    transform: scale(1.02);
-}
-
-.form-node.dimmed {
-    opacity: 0.4;
-    filter: grayscale(0.5);
-}
-
-.form-node.dimmed:hover {
-    opacity: 0.6;
-}
-
-/* 关联形式点节点样式 */
-.form-related-node {
-    border-left-color: #ec4899;
-    cursor: pointer;
-}
-
-.form-related-node:hover {
-    border-left-color: #db2777;
-    background: linear-gradient(to right, rgba(236, 72, 153, 0.1), white);
-}
-
-.form-related-node.visible {
-    background: linear-gradient(to right, rgba(236, 72, 153, 0.08), white);
-}
-
-/* 形式点之间的连线样式 */
-.connection-line.form {
-    stroke: #ec4899;
-    stroke-dasharray: 5, 5;
-}
-
-.connection-line.form.highlighted {
-    stroke-width: 3;
-    opacity: 0.8;
-}
-
-/* ===== Tab4 新版支撑关系图样式 ===== */
-
-/* 顶部说明区域 */
-.tab4-description {
-    margin-top: 10px;
-    padding: 15px;
-    background: #f8f9fa;
-    border-radius: 6px;
-    border-left: 4px solid #667eea;
-}
-
-.tab4-description p {
-    margin: 0 0 10px 0;
-    color: #495057;
-    font-size: 0.95rem;
-}
-
-.relationship-rules {
-    list-style: none;
-    padding: 0;
-    margin: 0;
-}
-
-.relationship-rules li {
-    padding: 6px 0;
-    color: #6c757d;
-    font-size: 0.9rem;
-}
-
-.relationship-rules li::before {
-    content: "→";
-    color: #667eea;
-    margin-right: 8px;
-    font-weight: bold;
-}
-
-/* 支撑关系主容器 */
-.support-view-container {
-    display: grid;
-    grid-template-columns: 300px 1fr 350px;
-    gap: 20px;
-    min-height: 700px;
-    background: #f8f9fa;
-    padding: 20px;
-    border-radius: 8px;
-}
-
-/* 左侧实质点面板 */
-.substance-panel {
-    display: flex;
-    flex-direction: column;
-    gap: 15px;
-}
-
-.panel-title {
-    font-size: 1.1rem;
-    font-weight: 600;
-    color: #495057;
-    padding: 12px;
-    background: white;
-    border-radius: 6px;
-    text-align: center;
-    border-left: 4px solid #667eea;
-    margin: 0;
-}
-
-/* 实质点分组 */
-.substance-group {
-    background: white;
-    border-radius: 8px;
-    padding: 15px;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-.group-title {
-    font-size: 0.95rem;
-    font-weight: 600;
-    color: #667eea;
-    margin: 0 0 12px 0;
-    padding-bottom: 8px;
-    border-bottom: 2px solid #e9ecef;
-}
-
-.substance-items {
-    display: flex;
-    flex-direction: column;
-    gap: 8px;
-    padding-right: 5px;
-}
-
-/* 实质点卡片 */
-.substance-card {
-    padding: 10px 12px;
-    background: #f8f9fa;
-    border-radius: 6px;
-    border-left: 3px solid #adb5bd;
-    cursor: pointer;
-    transition: all 0.2s;
-}
-
-.substance-card:hover {
-    background: #e9ecef;
-    transform: translateX(3px);
-    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
-}
-
-.substance-card.concrete-element {
-    border-left-color: #10b981;
-}
-
-.substance-card.concrete-concept {
-    border-left-color: #3b82f6;
-}
-
-.substance-card.abstract-concept {
-    border-left-color: #8b5cf6;
-}
-
-.substance-card.selected {
-    background: linear-gradient(to right, rgba(102, 126, 234, 0.15), rgba(102, 126, 234, 0.05));
-    border-left-color: #667eea;
-    border-left-width: 4px;
-    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
-}
-
-.substance-card.dimmed {
-    opacity: 0.3;
-    filter: grayscale(0.7);
-}
-
-.substance-card.hidden {
-    display: none;
-}
-
-.card-header {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    flex-wrap: wrap;
-}
-
-.card-id {
-    display: inline-block;
-    background: #6c757d;
-    color: white;
-    padding: 2px 6px;
-    border-radius: 3px;
-    font-size: 0.7rem;
-    font-weight: 600;
-    flex-shrink: 0;
-}
-
-.card-name {
-    font-size: 0.85rem;
-    font-weight: 600;
-    color: #495057;
-    flex: 1;
-    line-height: 1.3;
-}
-
-.card-description {
-    margin-top: 6px;
-    font-size: 0.75rem;
-    color: #6c757d;
-    line-height: 1.4;
-}
-
-/* 卡片推理信息样式 */
-.card-reasoning {
-    margin-top: 8px;
-    padding: 8px 10px;
-    background: rgba(139, 92, 246, 0.05);
-    border-left: 2px solid #8b5cf6;
-    border-radius: 4px;
-}
-
-.reasoning-label {
-    font-size: 0.7rem;
-    font-weight: 600;
-    color: #8b5cf6;
-    margin-bottom: 4px;
-}
-
-.reasoning-text {
-    font-size: 0.72rem;
-    color: #495057;
-    line-height: 1.5;
-    font-style: italic;
-}
-
-/* 中间可视化面板 */
-.support-visualization-panel {
-    position: relative;
-    background: white;
-    border-radius: 8px;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-.support-canvas {
-    width: 100%;
-    height: 100%;
-    position: relative;
-    min-height: 700px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-}
-
-.placeholder-text {
-    font-size: 1rem;
-    color: #adb5bd;
-    text-align: center;
-    padding: 40px;
-}
-
-/* 右侧目标点面板 */
-.target-panel {
-    display: flex;
-    flex-direction: column;
-    gap: 15px;
-}
-
-/* 目标点分组 */
-.target-group {
-    background: white;
-    border-radius: 8px;
-    padding: 15px;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-.target-items {
-    display: flex;
-    flex-direction: column;
-    gap: 8px;
-    padding-right: 5px;
-}
-
-/* 目标点卡片 */
-.target-card {
-    padding: 10px 12px;
-    background: #f8f9fa;
-    border-radius: 6px;
-    border-left: 3px solid #adb5bd;
-    cursor: pointer;
-    transition: all 0.2s;
-}
-
-.target-card:hover {
-    background: #e9ecef;
-    transform: translateX(-3px);
-    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
-}
-
-.target-card.inspiration-card {
-    border-left-color: #10b981;
-}
-
-.target-card.keypoint-card {
-    border-left-color: #8b5cf6;
-}
-
-.target-card.purpose-card {
-    border-left-color: #3b82f6;
-}
-
-.target-card.selected {
-    background: linear-gradient(to right, rgba(102, 126, 234, 0.15), rgba(102, 126, 234, 0.05));
-    border-left-color: #667eea;
-    border-left-width: 4px;
-    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
-}
-
-.target-card.dimmed {
-    opacity: 0.3;
-    filter: grayscale(0.7);
-}
-
-.target-card.hidden {
-    display: none;
-}
-
-.card-number {
-    display: inline-block;
-    background: #6c757d;
-    color: white;
-    padding: 2px 6px;
-    border-radius: 3px;
-    font-size: 0.7rem;
-    font-weight: 600;
-    margin-bottom: 4px;
-}
-
-.card-text {
-    font-size: 0.85rem;
-    font-weight: 500;
-    color: #495057;
-    line-height: 1.4;
-}
-
-/* 支撑关系流程图样式 */
-.support-flow {
-    padding: 30px;
-    animation: fadeIn 0.5s ease;
-}
-
-.flow-title {
-    font-size: 1.2rem;
-    font-weight: 600;
-    color: #667eea;
-    margin-bottom: 25px;
-    text-align: center;
-    padding: 15px;
-    background: linear-gradient(to right, rgba(102, 126, 234, 0.1), transparent);
-    border-radius: 8px;
-    border-left: 4px solid #667eea;
-}
-
-.flow-source {
-    display: flex;
-    justify-content: center;
-    margin-bottom: 30px;
-}
-
-.flow-node {
-    padding: 20px 30px;
-    border-radius: 12px;
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-    text-align: center;
-    min-width: 200px;
-}
-
-.substance-flow-node {
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    color: white;
-}
-
-.target-flow-node {
-    background: linear-gradient(135deg, #10b981 0%, #059669 100%);
-    color: white;
-}
-
-.flow-node-label {
-    font-size: 0.85rem;
-    font-weight: 500;
-    opacity: 0.9;
-    margin-bottom: 8px;
-}
-
-.flow-node-name {
-    font-size: 1.1rem;
-    font-weight: 700;
-}
-
-.flow-targets {
-    display: flex;
-    flex-direction: column;
-    gap: 20px;
-}
-
-.flow-target-group {
-    padding: 15px;
-    background: #f8f9fa;
-    border-radius: 8px;
-    border-left: 4px solid #adb5bd;
-}
-
-.flow-target-label {
-    font-size: 0.95rem;
-    font-weight: 600;
-    color: #495057;
-    margin-bottom: 12px;
-    padding-bottom: 8px;
-    border-bottom: 2px solid #e9ecef;
-}
-
-.flow-target-item {
-    padding: 12px 15px;
-    margin-bottom: 10px;
-    background: white;
-    border-radius: 6px;
-    border-left: 4px solid #adb5bd;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-.flow-target-item:last-child {
-    margin-bottom: 0;
-}
-
-.flow-target-item.inspiration-flow {
-    border-left-color: #10b981;
-    background: linear-gradient(to right, rgba(16, 185, 129, 0.05), white);
-}
-
-.flow-target-item.purpose-flow {
-    border-left-color: #3b82f6;
-    background: linear-gradient(to right, rgba(59, 130, 246, 0.05), white);
-}
-
-.flow-target-item.keypoint-flow {
-    border-left-color: #8b5cf6;
-    background: linear-gradient(to right, rgba(139, 92, 246, 0.05), white);
-}
-
-.flow-target-item.concrete-element-flow {
-    border-left-color: #10b981;
-    background: linear-gradient(to right, rgba(16, 185, 129, 0.05), white);
-}
-
-.flow-target-item.concrete-concept-flow {
-    border-left-color: #3b82f6;
-    background: linear-gradient(to right, rgba(59, 130, 246, 0.05), white);
-}
-
-.flow-target-item.abstract-concept-flow {
-    border-left-color: #8b5cf6;
-    background: linear-gradient(to right, rgba(139, 92, 246, 0.05), white);
-}
-
-.flow-target-text {
-    font-size: 0.9rem;
-    font-weight: 500;
-    color: #495057;
-    margin-bottom: 6px;
-    line-height: 1.4;
-}
-
-.flow-target-score {
-    font-size: 0.8rem;
-    color: #6c757d;
-    font-weight: 600;
-}
-
-/* 目标点卡片高亮状态 */
-.target-card.highlighted {
-    background: linear-gradient(to right, rgba(102, 126, 234, 0.1), rgba(102, 126, 234, 0.03));
-    border-left-width: 4px;
-    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
-}
-
-/* 实质点卡片高亮状态 */
-.substance-card.highlighted {
-    background: linear-gradient(to right, rgba(102, 126, 234, 0.1), rgba(102, 126, 234, 0.03));
-    border-left-width: 4px;
-    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
-}
-
-/* ===== Tab5 实质与形式双向支撑关系图样式 ===== */
-
-/* Tab5说明区域 */
-.tab5-description {
-    margin-top: 10px;
-    padding: 15px;
-    background: #f8f9fa;
-    border-radius: 6px;
-    border-left: 4px solid #8b5cf6;
-}
-
-.tab5-description p {
-    margin: 0 0 10px 0;
-    color: #495057;
-    font-size: 0.95rem;
-}
-
-/* Tab5 4列布局容器 */
-.tab5-four-column-container {
-    display: grid;
-    grid-template-columns: 280px 280px 280px 280px;
-    gap: 15px;
-    min-height: 700px;
-    background: #f8f9fa;
-    padding: 20px;
-    border-radius: 8px;
-}
-
-/* Tab5 列样式 */
-.tab5-column {
-    display: flex;
-    flex-direction: column;
-    gap: 12px;
-}
-
-.tab5-left-targets {
-    /* 左侧选题点列(实质支撑) */
-}
-
-.tab5-substances {
-    /* 中左实质点列 */
-}
-
-.tab5-forms {
-    /* 中右形式点列 */
-}
-
-.tab5-right-targets {
-    /* 右侧选题点列(形式支撑) */
-}
-
-/* 形式点面板 */
-.form-panel {
-    display: flex;
-    flex-direction: column;
-    gap: 15px;
-}
-
-/* 形式点分组 */
-.form-group {
-    background: white;
-    border-radius: 8px;
-    padding: 15px;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-.form-items {
-    display: flex;
-    flex-direction: column;
-    gap: 8px;
-    padding-right: 5px;
-}
-
-/* 形式点卡片 */
-.form-card {
-    padding: 10px 12px;
-    background: #f8f9fa;
-    border-radius: 6px;
-    border-left: 3px solid #adb5bd;
-    cursor: pointer;
-    transition: all 0.2s;
-}
-
-.form-card:hover {
-    background: #e9ecef;
-    transform: translateX(3px);
-    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
-}
-
-.form-card.concrete-element-form {
-    border-left-color: #f59e0b;
-}
-
-.form-card.concrete-concept-form {
-    border-left-color: #ec4899;
-}
-
-.form-card.overall-form {
-    border-left-color: #8b5cf6;
-}
-
-.form-card.selected {
-    background: linear-gradient(to right, rgba(139, 92, 246, 0.15), rgba(139, 92, 246, 0.05));
-    border-left-color: #8b5cf6;
-    border-left-width: 4px;
-    box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
-}
-
-.form-card.dimmed {
-    opacity: 0.3;
-    filter: grayscale(0.7);
-}
-
-.form-card.hidden {
-    display: none;
-}
-
-.form-card.highlighted {
-    background: linear-gradient(to right, rgba(139, 92, 246, 0.1), rgba(139, 92, 246, 0.03));
-    border-left-width: 4px;
-    box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2);
-}
-
-/* 实质点卡片(在tab5中作为目标) */
-.substance-card.concrete-element-card {
-    border-left-color: #10b981;
-}
-
-.substance-card.concrete-concept-card {
-    border-left-color: #3b82f6;
-}
-
-.substance-card.abstract-concept-card {
-    border-left-color: #8b5cf6;
-}
-
-/* 实质点项列表 */
-/* 已移除max-height限制,所有元素平铺显示 */
-
-/* 形式点流程节点样式 */
-.form-flow-node {
-    background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
-    color: white;
-}
-
-/* 形式点流程目标项样式 */
-.flow-target-item.concrete-element-form-flow {
-    border-left-color: #f59e0b;
-    background: linear-gradient(to right, rgba(245, 158, 11, 0.05), white);
-}
-
-.flow-target-item.concrete-concept-form-flow {
-    border-left-color: #ec4899;
-    background: linear-gradient(to right, rgba(236, 72, 153, 0.05), white);
-}
-
-.flow-target-item.overall-form-flow {
-    border-left-color: #8b5cf6;
-    background: linear-gradient(to right, rgba(139, 92, 246, 0.05), white);
-}
-
-.flow-target-item.form-flow {
-    border-left-color: #8b5cf6;
-    background: linear-gradient(to right, rgba(139, 92, 246, 0.05), white);
-}
-
-/* ===== Tab5 连线SVG容器样式 ===== */
-
-.tab5-svg-container {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    pointer-events: none;
-    z-index: 10;
-}
-
-#tab5-connection-svg {
-    width: 100%;
-    height: 100%;
-    overflow: visible;
-}
-
-/* Tab5 4列容器需要relative定位 */
-.tab5-four-column-container {
-    position: relative;
-    z-index: 1;
-}
-
-/* Tab5 连线样式 */
-.tab5-connection-line {
-    fill: none;
-    stroke-width: 2;
-    opacity: 0.4;
-    transition: all 0.3s ease;
-    pointer-events: stroke;
-}
-
-/* 左侧选题点到实质点的连线 */
-.tab5-connection-line.left-target-substance {
-    stroke: #667eea;
-}
-
-/* 实质点到形式点的连线 */
-.tab5-connection-line.substance-form {
-    stroke: #f59e0b;
-    stroke-dasharray: 5, 3;
-}
-
-/* 形式点到右侧灵感点的连线 */
-.tab5-connection-line.form-inspiration {
-    stroke: #10b981;
-}
-
-/* 形式点到右侧目的点的连线 */
-.tab5-connection-line.form-purpose {
-    stroke: #3b82f6;
-}
-
-/* 形式点到右侧关键点的连线 */
-.tab5-connection-line.form-keypoint {
-    stroke: #8b5cf6;
-}
-
-/* 连线hover效果(已在JS中定义) */
-.tab5-connection-line:hover {
-    opacity: 0.8;
-    stroke-width: 4;
-    filter: drop-shadow(0 0 6px rgba(0,0,0,0.3));
-}
-
-/* ===== Tab5 连线详情模态框样式 ===== */
-
-.connection-modal-backdrop {
-    position: fixed;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    background: rgba(0, 0, 0, 0.6);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    z-index: 3000;
-    animation: fadeIn 0.2s ease;
-}
-
-.connection-modal-content {
-    background: white;
-    border-radius: 12px;
-    padding: 0;
-    max-width: 550px;
-    width: 90%;
-    max-height: 85vh;
-    overflow-y: auto;
-    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4);
-    animation: slideUp 0.3s ease;
-}
-
-.connection-detail-section {
-    margin-bottom: 18px;
-    padding: 12px 0;
-    border-bottom: 1px solid #e9ecef;
-}
-
-.connection-detail-section:last-child {
-    border-bottom: none;
-    margin-bottom: 0;
-}
-
-.connection-label {
-    font-size: 0.85rem;
-    font-weight: 600;
-    color: #667eea;
-    margin-bottom: 6px;
-    display: block;
-}
-
-.connection-value {
-    font-size: 0.95rem;
-    color: #495057;
-    line-height: 1.6;
-    padding-left: 10px;
-}
-
-.connection-value.score-value {
-    color: #667eea;
-    font-weight: 700;
-    font-size: 1.1rem;
-}
-
-/* ===== Tab5标题区域样式 ===== */
-
-.tab5-header {
-    margin-bottom: 20px;
-}
-
-.tab5-title {
-    font-size: 1.5rem;
-    font-weight: 700;
-    color: #495057;
-    margin: 0 0 10px 0;
-}
-
-.tab5-description p {
-    margin: 8px 0;
-    font-size: 0.95rem;
-    color: #495057;
-    line-height: 1.6;
-}
-
-/* ===== V2 卡片样式 ===== */
-
-.point-card {
-    margin-bottom: 20px;
-    background: white;
-    border-radius: 8px;
-    border: 1px solid #e5e7eb;
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-    transition: all 0.3s ease;
-    overflow: hidden;
-}
-
-.point-card:hover {
-    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
-}
-
-.point-card-header {
-    padding: 15px 20px;
-    cursor: pointer;
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    background: #f8f9fa;
-    transition: background 0.2s;
-    user-select: none;
-}
-
-.point-card-header:hover {
-    background: #e9ecef;
-}
-
-.point-card.expanded .point-card-header {
-    background: #e7f3ff;
-    border-bottom: 1px solid #dee2e6;
-}
-
-.point-card-header .point-number {
-    display: inline-block;
-    background: #667eea;
-    color: white;
-    padding: 4px 10px;
-    border-radius: 4px;
-    font-weight: 600;
-    font-size: 0.85rem;
-    flex-shrink: 0;
-}
-
-.point-card-header .point-time {
-    color: #6c757d;
-    font-size: 0.9rem;
-    flex-shrink: 0;
-}
-
-.point-card-header .point-units {
-    color: #6c757d;
-    font-size: 0.85rem;
-    flex-shrink: 0;
-}
-
-.point-card-header .point-text {
-    flex: 1;
-    font-size: 1.05rem;
-    font-weight: 600;
-    color: #1f2937;
-}
-
-.toggle-icon {
-    color: #667eea;
-    font-size: 0.9rem;
-    transition: transform 0.3s;
-    flex-shrink: 0;
-}
-
-.point-card.expanded .toggle-icon {
-    transform: rotate(180deg);
-}
-
-.point-card-details {
-    display: none;
-    padding: 20px;
-    background: white;
-    animation: slideDown 0.3s ease;
-}
-
-.point-card.expanded .point-card-details {
-    display: block;
-}
-
-@keyframes slideDown {
-    from {
-        opacity: 0;
-        transform: translateY(-10px);
-    }
-    to {
-        opacity: 1;
-        transform: translateY(0);
-    }
-}
-
-.detail-section {
-    margin-bottom: 20px;
-    padding-bottom: 15px;
-    border-bottom: 1px solid #e9ecef;
-}
-
-.detail-section:last-child {
-    border-bottom: none;
-    margin-bottom: 0;
-}
-
-.detail-section strong {
-    display: block;
-    margin-bottom: 8px;
-    color: #495057;
-    font-size: 0.95rem;
-}
-
-.detail-text {
-    color: #4b5563;
-    line-height: 1.6;
-    font-size: 0.95rem;
-}
-
-.detail-list {
-    list-style: none;
-    padding-left: 0;
-    margin: 10px 0;
-}
-
-.detail-list li {
-    padding: 8px 12px;
-    margin-bottom: 8px;
-    background: #f8f9fa;
-    border-radius: 4px;
-    border-left: 3px solid #667eea;
-}
-
-.tag-group {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 8px;
-    margin-top: 10px;
-}
-
-.label-tag {
-    display: inline-block;
-    padding: 5px 12px;
-    border-radius: 4px;
-    font-size: 0.85rem;
-    font-weight: 500;
-}
-
-.l1-tag {
-    background: #dbeafe;
-    color: #1e40af;
-}
-
-.l2-tag {
-    background: #fef3c7;
-    color: #92400e;
-}
-
-.l3-tag {
-    background: #f3e8ff;
-    color: #6b21a8;
-}
-
-.l4-tag {
-    background: #fce7f3;
-    color: #9f1239;
-}
-
-.keyword-tag {
-    display: inline-block;
-    padding: 4px 10px;
-    background: #e0e7ff;
-    color: #3730a3;
-    border-radius: 4px;
-    font-size: 0.85rem;
-    margin: 2px;
-}
-
-.sub-section {
-    margin-top: 15px;
-    padding: 12px;
-    background: #f8f9fa;
-    border-radius: 6px;
-    border-left: 3px solid #667eea;
-}
-
-.sub-section strong {
-    color: #495057;
-    font-size: 0.9rem;
-}
-
-.form-item {
-    margin-top: 10px;
-    padding: 10px;
-    background: white;
-    border-radius: 4px;
-    font-size: 0.9rem;
-    line-height: 1.6;
-    color: #4b5563;
-}
-
-.form-item strong {
-    color: #667eea;
-    margin-right: 8px;
-}
-
-.element-name {
-    font-size: 1rem;
-    color: #1f2937;
-    margin-bottom: 8px;
-    padding-bottom: 5px;
-    border-bottom: 1px solid #e5e7eb;
-}
-
-.logic-flow {
-    margin-top: 15px;
-}
-
-.logic-stage {
-    margin-bottom: 20px;
-    padding: 15px;
-    background: #f8f9fa;
-    border-radius: 6px;
-    border-left: 4px solid #667eea;
-}
-
-.stage-number {
-    font-size: 0.9rem;
-    color: #667eea;
-    font-weight: 600;
-    margin-bottom: 5px;
-}
-
-.stage-name {
-    font-size: 1.05rem;
-    font-weight: 600;
-    color: #1f2937;
-    margin-bottom: 8px;
-}
-
-.stage-desc {
-    font-size: 0.95rem;
-    color: #4b5563;
-    line-height: 1.6;
-}
-
-.section-divider {
-    height: 2px;
-    background: linear-gradient(to right, transparent, #dee2e6, transparent);
-    margin: 30px 0;
-}
-
-.section-info {
-    font-size: 0.9rem;
-    color: #6c757d;
-    margin-bottom: 15px;
-    padding: 8px 12px;
-    background: #f8f9fa;
-    border-radius: 4px;
-}
-
-.hook-item, .golden-item {
-    padding: 10px 15px;
-    margin-bottom: 10px;
-    background: #fff3cd;
-    border-radius: 6px;
-    border-left: 4px solid #ffc107;
-    font-size: 0.95rem;
-    line-height: 1.6;
-    color: #856404;
-}
-
-.golden-item {
-    background: #d1ecf1;
-    border-left-color: #17a2b8;
-    color: #0c5460;
-}
-
-.model-item {
-    padding: 10px 15px;
-    margin-bottom: 10px;
-    background: #e7f3ff;
-    border-radius: 6px;
-    border-left: 4px solid #667eea;
-    font-size: 0.9rem;
-    line-height: 1.6;
-    color: #1e40af;
-    font-family: 'Courier New', monospace;
-}

+ 0 - 133
examples/run_batch_script_v2.py

@@ -1,133 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-批量运行脚本解构工作流 V2 (ScriptWorkflowV2)。
-
-读取 examples/demo.json 中的视频列表,
-将每条里的 video / channel_content_id 传入 ScriptWorkflowV2,
-并将解构结果逐条写入 examples/output_demo_script_v2.json(增量写入)。
-"""
-
-import json
-import sys
-from datetime import datetime
-from pathlib import Path
-from typing import Dict, Any, List
-
-# 添加项目根目录到路径
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-
-from src.workflows.script_workflow_v2 import ScriptWorkflowV2
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-def load_json(path: Path):
-    """通用 JSON 读取,支持列表或字典。"""
-    if not path.exists():
-        return None
-    with path.open("r", encoding="utf-8") as f:
-        return json.load(f)
-
-
-def save_json(path: Path, data) -> None:
-    """安全写入 JSON(先写临时文件,再替换)。"""
-    tmp_path = path.with_suffix(".tmp")
-    with tmp_path.open("w", encoding="utf-8") as f:
-        json.dump(data, f, ensure_ascii=False, indent=2)
-    tmp_path.replace(path)
-
-
-def main() -> None:
-    base_dir = Path(__file__).parent
-    input_path = base_dir / "demo.json"
-    output_path = base_dir / "output_demo_script_v2.json"
-
-    if not input_path.exists():
-        raise FileNotFoundError(f"找不到输入文件: {input_path}")
-
-    # 读取 demo.json(为一个列表)
-    raw_list = load_json(input_path)
-    if not isinstance(raw_list, list):
-        raise ValueError(f"demo.json 格式错误,应为列表,实际类型: {type(raw_list)}")
-
-    # 读取已有输出,支持增量
-    output_data = load_json(output_path)
-    if not output_data:
-        output_data = {
-            "timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"),
-            "total": 0,
-            "success_count": 0,
-            "fail_count": 0,
-            "results": [],
-        }
-
-    existing_results: List[Dict[str, Any]] = output_data.get("results", []) or []
-    # 用 video_id + video URL 去重,避免重复处理(兼容旧字段名 channel_content_id 和 video)
-    processed_keys = {
-        f"{item.get('video_data', {}).get('video_id', '') or item.get('video_data', {}).get('channel_content_id','')}|"
-        f"{item.get('video_data', {}).get('video_url', '') or item.get('video_data', {}).get('video','')}"
-        for item in existing_results
-    }
-
-    workflow = ScriptWorkflowV2()
-
-    for item in raw_list:
-        video_data = item or {}
-        video_id = video_data.get("video_id", "") or video_data.get("channel_content_id", "")  # 兼容旧字段名
-        video_url = video_data.get("video_url", "") or video_data.get("video", "")  # 兼容旧字段名 video
-
-        key = f"{video_id}|{video_url}"
-        if key in processed_keys:
-            logger.info(f"已处理过该视频,跳过: {key}")
-            continue
-
-        logger.info(
-            f"处理视频: video_id={video_id} title={video_data.get('title','')}"
-        )
-
-        try:
-            # ScriptWorkflowV2 只需要 video 和 video_id
-            script_input = {
-                "video": video_url,
-                "video_id": video_id,
-            }
-            script_result = workflow.invoke(script_input)
-
-            record = {
-                "video_data": video_data,
-                "script_result": script_result,
-                "success": True,
-                "error": None,
-            }
-
-            output_data["success_count"] = output_data.get("success_count", 0) + 1
-
-        except Exception as e:
-            logger.error(f"脚本解构 V2 处理失败: {e}", exc_info=True)
-            record = {
-                "video_data": video_data,
-                "script_result": None,
-                "success": False,
-                "error": str(e),
-            }
-            output_data["fail_count"] = output_data.get("fail_count", 0) + 1
-
-        output_data["results"].append(record)
-        output_data["total"] = output_data.get("total", 0) + 1
-
-        # 处理完一条就保存一次,避免长任务中途失败导致全部丢失
-        save_json(output_path, output_data)
-
-    logger.info(
-        f"批量脚本解构 V2 完成: total={output_data.get('total')}, "
-        f"success={output_data.get('success_count')}, fail={output_data.get('fail_count')}"
-    )
-
-
-if __name__ == "__main__":
-    main()
-
-

+ 0 - 264
examples/run_decode_script.py

@@ -1,264 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-批量运行解码工作流 (DecodeWorkflow)。
-
-读取 examples/demo.json 中的视频列表,
-逐一调用 DecodeWorkflow 进行处理,
-并将结果输出到 examples/output_decode_result.json。
-"""
-
-import json
-import sys
-from datetime import datetime
-from pathlib import Path
-from typing import Dict, Any, List
-
-# 添加项目根目录到路径
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-
-from src.workflows.decode_workflow import DecodeWorkflow
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-def load_json(path: Path) -> List[Dict[str, Any]]:
-    """加载JSON文件"""
-    if not path.exists():
-        return []
-    with path.open("r", encoding="utf-8") as f:
-        data = json.load(f)
-        # 如果是字典且有 results 字段,提取 results
-        if isinstance(data, dict) and "results" in data:
-            return data["results"]
-        # 如果是列表,直接返回
-        elif isinstance(data, list):
-            return data
-        else:
-            return []
-
-
-def save_json(path: Path, data: Dict[str, Any]) -> None:
-    """保存JSON文件(使用临时文件确保原子性)"""
-    tmp_path = path.with_suffix(".tmp")
-    with tmp_path.open("w", encoding="utf-8") as f:
-        json.dump(data, f, ensure_ascii=False, indent=2)
-    tmp_path.replace(path)
-
-
-def build_decode_input(video_data: Dict[str, Any]) -> Dict[str, Any]:
-    """根据视频数据构造 DecodeWorkflow 的输入结构"""
-    return {
-        "video_url": video_data.get("video_url", ""),
-        "video_id": video_data.get("video_id", ""),
-        "title": video_data.get("title", ""),
-    }
-
-def main() -> None:
-    """主函数"""
-    base_dir = Path(__file__).parent
-    input_path = base_dir / "demo.json"
-    output_path = base_dir / "output_decode_result.json"
-
-    if not input_path.exists():
-        raise FileNotFoundError(f"找不到输入文件: {input_path}")
-
-    # 读取视频列表
-    video_list = load_json(input_path)
-    if not video_list:
-        logger.warning(f"输入文件 {input_path} 中没有视频数据")
-        return
-
-    logger.info(f"共读取到 {len(video_list)} 个视频")
-
-    # 读取已有的输出结果,支持增量追加
-    output_data = {}
-    if output_path.exists():
-        try:
-            with output_path.open("r", encoding="utf-8") as f:
-                output_data = json.load(f)
-        except Exception as e:
-            logger.warning(f"读取已有输出文件失败,将创建新文件: {e}")
-            output_data = {}
-
-    if not output_data:
-        output_data = {
-            "timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"),
-            "total": 0,
-            "success_count": 0,
-            "fail_count": 0,
-            "results": [],
-        }
-
-    existing_results: List[Dict[str, Any]] = output_data.get("results", []) or []
-    # 用 video_id + video URL 去重,避免重复处理(兼容旧字段名 channel_content_id)
-    processed_keys = {
-        f"{item.get('video_data', {}).get('video_id', '')}|"
-        f"{item.get('video_data', {}).get('video_url', '')}"
-        for item in existing_results
-    }
-
-    # 初始化工作流
-    logger.info("初始化 DecodeWorkflow...")
-    workflow = DecodeWorkflow()
-    logger.info("DecodeWorkflow 初始化完成")
-
-    # 处理每个视频
-    for idx, video_data in enumerate(video_list, 1):
-        video_url = video_data.get("video_url", "")
-        video_id = video_data.get("video_id", "") or video_data.get("channel_content_id", "")  # 兼容旧字段名
-        title = video_data.get("title", "")
-
-        # 生成唯一键用于去重
-        key = f"{video_id}|{video_url}"
-        if key in processed_keys:
-            logger.info(f"[{idx}/{len(video_list)}] 已处理过该视频,跳过: video_id={video_id}")
-            continue
-
-        logger.info(
-            f"[{idx}/{len(video_list)}] 开始处理视频: "
-            f"video_id={video_id}, title={title[:50]}..."
-        )
-
-        try:
-            # 构建输入数据
-            decode_input = build_decode_input(video_data)
-
-            # 调用工作流
-            decode_result = workflow.invoke(decode_input)
-
-            # 检查workflow返回结果中是否包含错误
-            if isinstance(decode_result, dict):
-                # 检查是否有错误字段(支持多种错误字段名)
-                error_msg = (
-                    decode_result.get("error") or 
-                    decode_result.get("错误") or
-                    decode_result.get("workflow_error")
-                )
-                workflow_status = decode_result.get("workflow_status")
-                
-                # 如果返回了错误信息,视为失败
-                if error_msg or workflow_status == "failed" or workflow_status == "incomplete":
-                    error_msg = error_msg or "工作流执行失败"
-                    logger.error(
-                        f"[{idx}/{len(video_list)}] 处理失败: video_id={video_id}, error={error_msg}"
-                    )
-                    record = {
-                        "video_data": video_data,
-                        "what_deconstruction_result": None,
-                        "script_result": None,
-                        "success": False,
-                        "error": error_msg,
-                    }
-                    output_data["fail_count"] = output_data.get("fail_count", 0) + 1
-                    output_data["results"].append(record)
-                    output_data["total"] = output_data.get("total", 0) + 1
-                    save_json(output_path, output_data)
-                    continue
-                
-                # 检查结果是否为空(可能表示失败)
-                # 如果所有关键字段都为空,可能表示处理失败
-                video_info = decode_result.get("视频信息", {})
-                three_points = decode_result.get("三点解构", {})
-                topic_understanding = decode_result.get("选题理解", {})
-                script_understanding = decode_result.get("脚本理解", {})
-                
-                # 如果所有关键结果都为空,且没有明确的成功标志,视为失败
-                if (not video_info and not three_points and 
-                    not topic_understanding and not script_understanding):
-                    error_msg = "工作流执行完成,但所有结果都为空"
-                    logger.warning(
-                        f"[{idx}/{len(video_list)}] 处理结果为空: video_id={video_id}"
-                    )
-                    # 这里可以选择记录为失败或警告,根据业务需求决定
-                    # 暂时记录为失败
-                    record = {
-                        "video_data": video_data,
-                        "what_deconstruction_result": None,
-                        "script_result": None,
-                        "success": False,
-                        "error": error_msg,
-                    }
-                    output_data["fail_count"] = output_data.get("fail_count", 0) + 1
-                    output_data["results"].append(record)
-                    output_data["total"] = output_data.get("total", 0) + 1
-                    save_json(output_path, output_data)
-                    continue
-
-            # 按照 output_demo_script.json 的格式组织结果
-            # what_deconstruction_result: 包含视频信息、三点解构、选题理解
-            what_deconstruction_result = {
-                "视频信息": decode_result.get("视频信息", {}),
-                "三点解构": decode_result.get("三点解构", {}),
-                "选题理解": decode_result.get("选题理解", {}),
-            }
-            
-            # script_result: 包含选题描述和脚本理解
-            # 从选题理解中提取选题描述
-            topic_understanding = decode_result.get("选题理解", {})
-            selected_topic = {}
-            if isinstance(topic_understanding, dict):
-                if "选题" in topic_understanding:
-                    selected_topic = topic_understanding.get("选题", {})
-                else:
-                    selected_topic = {
-                        "主题": topic_understanding.get("主题", ""),
-                        "描述": topic_understanding.get("描述", ""),
-                    }
-            
-            script_result = {
-                "选题描述": selected_topic,
-                "脚本理解": decode_result.get("脚本理解", {}),
-            }
-
-            # 构造结果记录(参考 output_demo_script.json 格式)
-            record = {
-                "video_data": video_data,
-                "what_deconstruction_result": what_deconstruction_result,
-                "script_result": script_result,
-                "success": True,
-                "error": None,
-            }
-
-            output_data["success_count"] = output_data.get("success_count", 0) + 1
-            logger.info(
-                f"[{idx}/{len(video_list)}] 处理成功: video_id={video_id}"
-            )
-
-        except Exception as e:
-            logger.error(
-                f"[{idx}/{len(video_list)}] 处理失败: video_id={video_id}, error={e}",
-                exc_info=True
-            )
-            record = {
-                "video_data": video_data,
-                "what_deconstruction_result": None,
-                "script_result": None,
-                "success": False,
-                "error": str(e),
-            }
-            output_data["fail_count"] = output_data.get("fail_count", 0) + 1
-
-        output_data["results"].append(record)
-        output_data["total"] = output_data.get("total", 0) + 1
-
-        # 处理完一条就保存一次,避免长任务中途失败导致全部丢失
-        save_json(output_path, output_data)
-        logger.info(f"结果已保存到 {output_path}")
-
-    logger.info(
-        f"\n{'='*60}\n"
-        f"批量解码完成:\n"
-        f"  总计: {output_data.get('total', 0)}\n"
-        f"  成功: {output_data.get('success_count', 0)}\n"
-        f"  失败: {output_data.get('fail_count', 0)}\n"
-        f"  输出文件: {output_path}\n"
-        f"{'='*60}"
-    )
-
-
-if __name__ == "__main__":
-    main()
-

+ 0 - 298
examples/run_evaluate.py

@@ -1,298 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-测试 EvaluateWorkflow 功能
-
-创建测试数据并验证评估工作流的功能
-"""
-
-import json
-import sys
-from pathlib import Path
-
-# 添加项目根目录到路径
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-
-from src.models import get_db, DecodeVideo, DecodeStatus, EvaluateRecord, EvaluateStatus
-from src.workflows.evaluate_workflow import EvaluateWorkflow
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-def create_test_decode_video(task_id: int, video_id: str = "test_video_001") -> DecodeVideo:
-    """创建测试用的 DecodeVideo 记录"""
-    
-    # 创建模拟的解构结果
-    decode_result_v2 = {
-        "视频信息": {
-            "标题": "🔴退伍军人二次入伍的感人画面!若有战,召必回",
-            "视频URL": "https://rescdn.yishihui.com/pipeline/video/f522fd33-1556-4928-ab5a-c5afdd3c9688.mp4",
-            "正文": ""
-        },
-        "三点解构": {
-            "灵感点": [
-                {
-                    "候选编号": 1,
-                    "分类": "亲情告别",
-                    "灵感点": "父亲亲吻熟睡的婴儿",
-                    "描述": "视频开头,一位身着便装、背着行囊的男子,在即将离家前,深情地俯身亲吻床上熟睡的小婴儿,并温柔地为其盖好被子,眼中充满不舍。"
-                }
-            ],
-            "目的点": {
-                "perspective": "创作者视角",
-                "purposes": [
-                    {
-                        "维度": {
-                            "一级分类": "个人",
-                            "二级分类": "分享"
-                        },
-                        "目的点": "展现军人告别与家人不舍的感人瞬间",
-                        "描述": "创作者通过剪辑多位军人在入伍或归队前与亲人进行告别的场景,着重刻画了军人坚毅与亲人依依不舍的复杂情感。"
-                    }
-                ]
-            },
-            "关键点": {
-                "key_points": [
-                    {
-                        "候选编号": 1,
-                        "维度大类": "实质",
-                        "维度细分": "元素",
-                        "关键点": "军人发型数字'2'",
-                        "描述": "军人头部的理发造型中,清晰可见数字'2',象征着'二次入伍'的身份。"
-                    }
-                ]
-            }
-        },
-        "选题理解": {
-            "核心主题": "军人二次入伍与家人告别",
-            "目标受众": "关注军人生活、家国情怀的观众",
-            "情感基调": "感人、不舍、家国情怀"
-        }
-    }
-    
-    # 创建 DecodeVideo 记录
-    decode_video = DecodeVideo.create(
-        task_id=task_id,
-        video_id=video_id,
-        status=DecodeStatus.SUCCESS,
-        decode_result_v2=json.dumps(decode_result_v2, ensure_ascii=False)
-    )
-    
-    return decode_video
-
-
-def create_test_evaluate_record(evaluate_id: int, task_id: int) -> EvaluateRecord:
-    """创建测试用的 EvaluateRecord 记录"""
-    
-    evaluate_record = EvaluateRecord.create(
-        evaluate_id=evaluate_id,
-        task_id=task_id,
-        status=EvaluateStatus.PENDING
-    )
-    
-    return evaluate_record
-
-
-def create_test_search_result() -> list:
-    """创建测试用的待评估视频列表"""
-    
-    search_result = [
-        {
-            "video_id": "61626151",
-            "video_url": "https://rescdn.yishihui.com/pipeline/video/f522fd33-1556-4928-ab5a-c5afdd3c9688.mp4",
-            "title": "🔴退伍军人二次入伍的感人画面!若有战,召必回"
-        },
-        {
-            "video_id": "61626152",
-            "video_url": "https://rescdn.yishihui.com/pipeline/video/example1.mp4",
-            "title": "军人告别家人的感人瞬间"
-        },
-        {
-            "video_id": "61626153",
-            "video_url": "https://rescdn.yishihui.com/pipeline/video/example2.mp4",
-            "title": "二次入伍军人的家国情怀"
-        },
-        {
-            "video_id": "61626154",
-            "video_url": "https://rescdn.yishihui.com/pipeline/video/example3.mp4",
-            "title": "美食制作教程:如何做红烧肉"
-        },
-        {
-            "video_id": "61626155",
-            "video_url": "https://rescdn.yishihui.com/pipeline/video/example4.mp4",
-            "title": "旅行vlog:探索美丽的风景"
-        }
-    ]
-    
-    return search_result
-
-
-def setup_test_data(task_id: int, evaluate_id: int, video_id: str = "test_video_001"):
-    """设置测试数据(创建数据库记录)"""
-    
-    logger.info("=== 开始设置测试数据 ===")
-    
-    db = next(get_db())
-    try:
-        # 检查是否已存在记录
-        existing_decode = db.query(DecodeVideo).filter_by(task_id=task_id).first()
-        if existing_decode:
-            logger.info(f"DecodeVideo 记录已存在: task_id={task_id},将删除后重新创建")
-            db.delete(existing_decode)
-            db.commit()
-        
-        existing_evaluate = db.query(EvaluateRecord).filter_by(evaluate_id=evaluate_id).first()
-        if existing_evaluate:
-            logger.info(f"EvaluateRecord 记录已存在: evaluate_id={evaluate_id},将删除后重新创建")
-            db.delete(existing_evaluate)
-            db.commit()
-        
-        # 创建 DecodeVideo 记录
-        decode_video = create_test_decode_video(task_id, video_id)
-        db.add(decode_video)
-        logger.info(f"✓ 创建 DecodeVideo 记录: task_id={task_id}, video_id={video_id}")
-        
-        # 创建 EvaluateRecord 记录
-        evaluate_record = create_test_evaluate_record(evaluate_id, task_id)
-        db.add(evaluate_record)
-        logger.info(f"✓ 创建 EvaluateRecord 记录: evaluate_id={evaluate_id}, task_id={task_id}")
-        
-        db.commit()
-        logger.info("✓ 测试数据设置完成")
-        
-    except Exception as e:
-        logger.error(f"设置测试数据失败: {e}", exc_info=True)
-        db.rollback()
-        raise
-    finally:
-        db.close()
-
-
-def test_evaluate_workflow(task_id: int, evaluate_id: int):
-    """测试评估工作流"""
-    
-    logger.info("=== 开始测试评估工作流 ===")
-    
-    # 创建待评估的视频列表
-    search_result = create_test_search_result()
-    logger.info(f"待评估视频数量: {len(search_result)}")
-    
-    # 准备输入数据
-    input_data = {
-        "task_id": task_id,
-        "evaluate_id": evaluate_id,
-        "search_result": search_result
-    }
-    
-    logger.info(f"输入数据: task_id={task_id}, evaluate_id={evaluate_id}, search_result数量={len(search_result)}")
-    
-    # 创建并执行工作流
-    try:
-        workflow = EvaluateWorkflow(model_provider="google_genai")
-        result = workflow.invoke(input_data)
-        
-        logger.info("=== 工作流执行结果 ===")
-        logger.info(f"工作流状态: {result.get('workflow_status')}")
-        
-        if result.get("error"):
-            logger.error(f"工作流执行错误: {result.get('error')}")
-            return result
-        
-        evaluate_result = result.get("evaluate_result", [])
-        logger.info(f"评估结果数量: {len(evaluate_result)}")
-        
-        # 打印评估结果详情
-        if evaluate_result:
-            logger.info("\n=== 评估结果详情 ===")
-            for i, video in enumerate(evaluate_result, 1):
-                logger.info(f"\n视频 {i}:")
-                logger.info(f"  video_id: {video.get('video_id')}")
-                logger.info(f"  title: {video.get('title')}")
-                logger.info(f"  relevance_score: {video.get('relevance_score', 'N/A')}")
-                logger.info(f"  is_selected: {video.get('is_selected', 'N/A')}")
-        
-        # 验证结果
-        selected_count = sum(1 for v in evaluate_result if v.get("is_selected", False))
-        logger.info(f"\n入选视频数量: {selected_count}/{len(evaluate_result)}")
-        
-        return result
-        
-    except Exception as e:
-        logger.error(f"测试评估工作流失败: {e}", exc_info=True)
-        raise
-
-
-def verify_database_result(evaluate_id: int):
-    """验证数据库中的评估结果"""
-    
-    logger.info("=== 验证数据库结果 ===")
-    
-    db = next(get_db())
-    try:
-        evaluate_record = db.query(EvaluateRecord).filter_by(evaluate_id=evaluate_id).first()
-        
-        if not evaluate_record:
-            logger.error(f"未找到 evaluate_id={evaluate_id} 的评估记录")
-            return
-        
-        logger.info(f"评估记录状态: {EvaluateStatus.get_description(evaluate_record.status)}")
-        logger.info(f"评估记录状态值: {evaluate_record.status}")
-        
-        if evaluate_record.evaluate_result:
-            try:
-                result_data = json.loads(evaluate_record.evaluate_result)
-                logger.info(f"评估结果数量: {len(result_data) if isinstance(result_data, list) else 'N/A'}")
-                logger.info(f"评估结果已保存到数据库")
-            except json.JSONDecodeError as e:
-                logger.error(f"解析评估结果失败: {e}")
-        else:
-            logger.warning("评估结果为空")
-        
-    except Exception as e:
-        logger.error(f"验证数据库结果失败: {e}", exc_info=True)
-    finally:
-        db.close()
-
-
-def main():
-    """主函数"""
-    
-    # 测试参数
-    task_id = 999999  # 测试用的 task_id
-    evaluate_id = 888888  # 测试用的 evaluate_id
-    video_id = "test_video_001"
-    
-    try:
-        # 1. 设置测试数据
-        setup_test_data(task_id, evaluate_id, video_id)
-        
-        # 2. 执行评估工作流
-        result = test_evaluate_workflow(task_id, evaluate_id)
-        
-        # 3. 验证数据库结果
-        verify_database_result(evaluate_id)
-        
-        # 4. 总结
-        logger.info("\n=== 测试总结 ===")
-        if result.get("workflow_status") == "success":
-            logger.info("✓ 评估工作流测试成功!")
-            evaluate_result = result.get("evaluate_result", [])
-            if evaluate_result:
-                logger.info(f"✓ 成功评估 {len(evaluate_result)} 个视频")
-                selected = [v for v in evaluate_result if v.get("is_selected", False)]
-                logger.info(f"✓ 入选视频 {len(selected)} 个")
-        else:
-            logger.error("✗ 评估工作流测试失败")
-            if result.get("error"):
-                logger.error(f"错误信息: {result.get('error')}")
-        
-    except Exception as e:
-        logger.error(f"测试执行失败: {e}", exc_info=True)
-        sys.exit(1)
-
-
-if __name__ == "__main__":
-    main()
-

+ 0 - 0
examples/static/visualize/__init__.py


+ 0 - 556
examples/static/visualize/tab1.py

@@ -1,556 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tab1内容生成器 - 选题点(灵感点、目的点、关键点)
-"""
-import html as html_module
-from typing import Dict, Any, List
-
-
-def render_inspiration_card(item: Dict[str, Any], idx: int) -> str:
-    """渲染灵感点卡片"""
-    point_text = item.get('灵感点', '')
-    description = item.get('描述', '')
-    category = item.get('分类', '')
-    features = item.get('提取的特征', [])
-    scoring = item.get('scoring', {})
-    reasoning = item.get('推理', '')
-    derivation = item.get('推导说明', '')
-    
-    card_id = f'inspiration-{idx}'
-    has_details = description or features or scoring or reasoning or derivation
-    
-    html = f'<div class="point-card inspiration-card" data-card-id="{card_id}">\n'
-    html += '<div class="point-card-header" onclick="toggleCardDetails(\'' + card_id + '\')">\n'
-    html += f'<span class="point-number">#{idx}</span>\n'
-    html += f'<span class="point-text">{html_module.escape(point_text)}</span>\n'
-    if category:
-        html += f'<span class="point-category">{html_module.escape(category)}</span>\n'
-    if has_details:
-        html += '<span class="toggle-icon">▼</span>\n'
-    html += '</div>\n'
-    
-    if has_details:
-        html += f'<div class="point-card-details" id="{card_id}-details">\n'
-        
-        if description:
-            html += '<div class="detail-section">\n'
-            html += '<strong>描述:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
-            html += '</div>\n'
-        
-        if reasoning:
-            html += '<div class="detail-section">\n'
-            html += '<strong>推理:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(reasoning)}</div>\n'
-            html += '</div>\n'
-        
-        if derivation:
-            html += '<div class="detail-section">\n'
-            html += '<strong>推导说明:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(derivation)}</div>\n'
-            html += '</div>\n'
-        
-        if features:
-            html += '<div class="detail-section">\n'
-            html += '<strong>提取的特征:</strong>\n'
-            html += '<div class="feature-tags">\n'
-            for feature in features:
-                feature_name = feature.get('特征名称', '')
-                weight = feature.get('权重', 0)
-                dimension = feature.get('维度分类', '')
-                html += f'<span class="feature-tag">{html_module.escape(feature_name)} '
-                if dimension:
-                    html += f'<em class="feature-dimension">({html_module.escape(dimension)})</em> '
-                html += f'<strong class="feature-weight">权重:{weight}</strong></span>\n'
-            html += '</div>\n'
-            html += '</div>\n'
-        
-        if scoring:
-            html += '<div class="detail-section">\n'
-            html += '<strong>评分:</strong>\n'
-            html += '<div class="scoring-badges">\n'
-            if '人设契合度' in scoring:
-                html += f'<span class="scoring-badge">人设契合度: {scoring["人设契合度"]}/10</span>\n'
-            if '触发可能性' in scoring:
-                html += f'<span class="scoring-badge">触发可能性: {scoring["触发可能性"]}/10</span>\n'
-            if '内容解释力' in scoring:
-                html += f'<span class="scoring-badge">内容解释力: {scoring["内容解释力"]}/10</span>\n'
-            if '总分' in scoring:
-                html += f'<span class="scoring-badge total-score">总分: {scoring["总分"]}</span>\n'
-            if '评分说明' in scoring:
-                html += f'<div class="scoring-note">{html_module.escape(scoring["评分说明"])}</div>\n'
-            html += '</div>\n'
-            html += '</div>\n'
-        
-        html += '</div>\n'
-    
-    html += '</div>\n'
-    return html
-
-
-def render_purpose_card(item: Dict[str, Any], idx: int) -> str:
-    """渲染目的点卡片"""
-    point_text = item.get('目的点', '')
-    description = item.get('描述', '')
-    dimension = item.get('维度', {})
-    features = item.get('提取的特征', [])
-    reasoning = item.get('推理', '')
-    
-    card_id = f'purpose-{idx}'
-    has_details = description or features or reasoning
-    
-    html = f'<div class="point-card purpose-card" data-card-id="{card_id}">\n'
-    html += '<div class="point-card-header" onclick="toggleCardDetails(\'' + card_id + '\')">\n'
-    html += f'<span class="point-number">#{idx}</span>\n'
-    html += f'<span class="point-text">{html_module.escape(point_text)}</span>\n'
-    
-    # 显示维度标签
-    if dimension:
-        html += '<div class="dimension-tags">\n'
-        if '一级分类' in dimension:
-            html += f'<span class="dimension-tag level-1">{html_module.escape(dimension["一级分类"])}</span>\n'
-        if '二级分类' in dimension:
-            html += f'<span class="dimension-tag level-2">{html_module.escape(dimension["二级分类"])}</span>\n'
-        html += '</div>\n'
-    
-    if has_details:
-        html += '<span class="toggle-icon">▼</span>\n'
-    html += '</div>\n'
-    
-    if has_details:
-        html += f'<div class="point-card-details" id="{card_id}-details">\n'
-        
-        if description:
-            html += '<div class="detail-section">\n'
-            html += '<strong>描述:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
-            html += '</div>\n'
-        
-        if reasoning:
-            html += '<div class="detail-section">\n'
-            html += '<strong>推理:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(reasoning)}</div>\n'
-            html += '</div>\n'
-        
-        if features:
-            html += '<div class="detail-section">\n'
-            html += '<strong>提取的特征:</strong>\n'
-            html += '<div class="feature-tags">\n'
-            for feature in features:
-                feature_name = feature.get('特征名称', '')
-                weight = feature.get('权重', 0)
-                feature_class = feature.get('特征分类', '')
-                html += f'<span class="feature-tag">{html_module.escape(feature_name)} '
-                if feature_class:
-                    html += f'<em class="feature-dimension">({html_module.escape(feature_class)})</em> '
-                html += f'<strong class="feature-weight">权重:{weight}</strong></span>\n'
-            html += '</div>\n'
-            html += '</div>\n'
-        
-        html += '</div>\n'
-    
-    html += '</div>\n'
-    return html
-
-
-def render_keypoint_card(item: Dict[str, Any], idx: int, level: int = 0) -> str:
-    """渲染关键点卡片(支持嵌套)"""
-    point_text = item.get('关键点', '')
-    description = item.get('描述', '')
-    dimension = item.get('维度', '') or item.get('维度细分', '')
-    dimension_category = item.get('维度大类', '')
-    features = item.get('提取的特征', [])
-    children = item.get('children', [])
-    child_reason = item.get('作为子节点的原因', '')
-    
-    card_id = f'keypoint-{idx}-l{level}'
-    has_details = description or features or child_reason
-    has_children = len(children) > 0
-    
-    indent_class = f'keypoint-level-{level}' if level > 0 else ''
-    
-    html = f'<div class="point-card keypoint-card {indent_class}" data-card-id="{card_id}" data-level="{level}">\n'
-    html += '<div class="point-card-header" onclick="toggleCardDetails(\'' + card_id + '\')">\n'
-    html += f'<span class="point-number">#{item.get("候选编号", idx)}</span>\n'
-    html += f'<span class="point-text">{html_module.escape(point_text)}</span>\n'
-    
-    # 显示维度大类标签(实质类/形式类)
-    if dimension_category:
-        category_label = '形式类' if dimension_category == '形式' else '实质类'
-        category_class = 'dimension-category-form' if dimension_category == '形式' else 'dimension-category-substance'
-        html += f'<span class="dimension-category {category_class}">{html_module.escape(category_label)}</span>\n'
-    
-    # 显示维度标签(维度细分)
-    if dimension:
-        html += f'<span class="dimension-tag keypoint-dimension">{html_module.escape(dimension)}</span>\n'
-    
-    if has_details or has_children:
-        html += '<span class="toggle-icon">▼</span>\n'
-    html += '</div>\n'
-    
-    if has_details or has_children:
-        html += f'<div class="point-card-details" id="{card_id}-details">\n'
-        
-        if description:
-            html += '<div class="detail-section">\n'
-            html += '<strong>描述:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
-            html += '</div>\n'
-        
-        if child_reason:
-            html += '<div class="detail-section">\n'
-            html += '<strong>作为子节点的原因:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(child_reason)}</div>\n'
-            html += '</div>\n'
-        
-        if features:
-            html += '<div class="detail-section">\n'
-            html += '<strong>提取的特征:</strong>\n'
-            html += '<div class="feature-tags">\n'
-            for feature in features:
-                feature_name = feature.get('特征名称', '')
-                weight = feature.get('权重', 0)
-                feature_dimension = feature.get('维度', '')
-                html += f'<span class="feature-tag">{html_module.escape(feature_name)} '
-                if feature_dimension:
-                    html += f'<em class="feature-dimension">({html_module.escape(feature_dimension)})</em> '
-                html += f'<strong class="feature-weight">权重:{weight}</strong></span>\n'
-            html += '</div>\n'
-            html += '</div>\n'
-        
-        # 递归渲染子关键点
-        if has_children:
-            html += '<div class="detail-section keypoint-children">\n'
-            html += '<strong>子关键点:</strong>\n'
-            html += '<div class="keypoint-children-list">\n'
-            for child_idx, child in enumerate(children, 1):
-                html += render_keypoint_card(child, child_idx, level + 1)
-            html += '</div>\n'
-            html += '</div>\n'
-        
-        html += '</div>\n'
-    
-    html += '</div>\n'
-    return html
-
-
-def generate_tab1_content(data: Dict[str, Any]) -> str:
-    """生成Tab1内容:选题、灵感点、目的点、关键点"""
-    html = '<div class="tab-content" id="tab1">\n'
-    
-    # 添加CSS样式
-    html += '''
-    <style>
-    .point-card {
-        background: white;
-        border-radius: 8px;
-        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-        margin-bottom: 16px;
-        transition: all 0.3s ease;
-        overflow: hidden;
-    }
-    
-    .point-card:hover {
-        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
-        transform: translateY(-2px);
-    }
-    
-    .point-card-header {
-        display: flex;
-        align-items: center;
-        gap: 12px;
-        padding: 16px 20px;
-        cursor: pointer;
-        user-select: none;
-        border-bottom: 1px solid #f0f0f0;
-    }
-    
-    .point-card-header:hover {
-        background-color: #f8f8f8;
-    }
-    
-    .point-number {
-        color: #667eea;
-        font-weight: 600;
-        font-size: 14px;
-        min-width: 40px;
-    }
-    
-    .point-text {
-        flex: 1;
-        font-size: 16px;
-        font-weight: 500;
-        color: #333;
-    }
-    
-    .point-category {
-        background-color: #e3f2fd;
-        color: #1976d2;
-        padding: 4px 12px;
-        border-radius: 12px;
-        font-size: 12px;
-        font-weight: 500;
-    }
-    
-    .dimension-tags {
-        display: flex;
-        gap: 8px;
-    }
-    
-    .dimension-tag {
-        background-color: #f3e5f5;
-        color: #7b1fa2;
-        padding: 4px 12px;
-        border-radius: 12px;
-        font-size: 12px;
-        font-weight: 500;
-    }
-    
-    .dimension-tag.level-1 {
-        background-color: #e1bee7;
-        color: #6a1b9a;
-    }
-    
-    .dimension-tag.level-2 {
-        background-color: #f3e5f5;
-        color: #7b1fa2;
-    }
-    
-    .dimension-category {
-        padding: 4px 12px;
-        border-radius: 12px;
-        font-size: 12px;
-        font-weight: 600;
-    }
-    
-    .dimension-category-form {
-        background-color: #fff3e0;
-        color: #e65100;
-    }
-    
-    .dimension-category-substance {
-        background-color: #e8f5e9;
-        color: #2e7d32;
-    }
-    
-    .toggle-icon {
-        color: #999;
-        font-size: 12px;
-        transition: transform 0.3s ease;
-    }
-    
-    .point-card.expanded .toggle-icon {
-        transform: rotate(180deg);
-    }
-    
-    .point-card-details {
-        display: none;
-        padding: 20px;
-        background-color: #fafafa;
-    }
-    
-    .point-card.expanded .point-card-details {
-        display: block;
-    }
-    
-    .detail-section {
-        margin-bottom: 20px;
-    }
-    
-    .detail-section:last-child {
-        margin-bottom: 0;
-    }
-    
-    .detail-section strong {
-        display: block;
-        color: #667eea;
-        font-size: 14px;
-        font-weight: 600;
-        margin-bottom: 8px;
-    }
-    
-    .detail-text {
-        background-color: white;
-        padding: 12px 16px;
-        border-radius: 6px;
-        line-height: 1.6;
-        color: #444;
-        font-size: 14px;
-        border-left: 3px solid #667eea;
-    }
-    
-    .feature-tags {
-        display: flex;
-        flex-wrap: wrap;
-        gap: 8px;
-        margin-top: 8px;
-    }
-    
-    .feature-tag {
-        background-color: #e8eaf6;
-        color: #3f51b5;
-        padding: 6px 12px;
-        border-radius: 15px;
-        font-size: 13px;
-    }
-    
-    .feature-dimension {
-        color: #7986cb;
-        font-style: italic;
-    }
-    
-    .feature-weight {
-        color: #1a237e;
-        margin-left: 4px;
-    }
-    
-    .scoring-badges {
-        display: flex;
-        flex-wrap: wrap;
-        gap: 8px;
-        margin-top: 8px;
-    }
-    
-    .scoring-badge {
-        background-color: #e0f2f1;
-        color: #00695c;
-        padding: 6px 12px;
-        border-radius: 15px;
-        font-size: 13px;
-        font-weight: 500;
-    }
-    
-    .scoring-badge.total-score {
-        background-color: #667eea;
-        color: white;
-        font-weight: 600;
-    }
-    
-    .scoring-note {
-        margin-top: 12px;
-        padding: 12px;
-        background-color: #fff3cd;
-        border-left: 3px solid #ffc107;
-        border-radius: 4px;
-        font-size: 13px;
-        color: #856404;
-        line-height: 1.6;
-    }
-    
-    /* 关键点嵌套样式 */
-    .keypoint-card.keypoint-level-1 {
-        margin-left: 24px;
-        border-left: 3px solid #4facfe;
-    }
-    
-    .keypoint-card.keypoint-level-2 {
-        margin-left: 48px;
-        border-left: 3px solid #43e97b;
-    }
-    
-    .keypoint-card.keypoint-level-3 {
-        margin-left: 72px;
-        border-left: 3px solid #fa709a;
-    }
-    
-    .keypoint-children {
-        margin-top: 16px;
-    }
-    
-    .keypoint-children-list {
-        margin-top: 12px;
-    }
-    
-    .inspiration-card {
-        border-left: 4px solid #667eea;
-    }
-    
-    .purpose-card {
-        border-left: 4px solid #f093fb;
-    }
-    
-    .keypoint-card {
-        border-left: 4px solid #4facfe;
-    }
-    </style>
-    '''
-    
-    # 添加JavaScript
-    html += '''
-    <script>
-    function toggleCardDetails(cardId) {
-        const card = document.querySelector(`[data-card-id="${cardId}"]`);
-        if (card) {
-            card.classList.toggle('expanded');
-        }
-    }
-    </script>
-    '''
-
-    # 选题描述
-    if '选题描述' in data:
-        topic = data['选题描述']
-        html += '<div class="section">\n'
-        html += '<h3>选题描述</h3>\n'
-        if '主题' in topic:
-            html += f'<div class="topic-theme"><strong>主题:</strong>{html_module.escape(topic["主题"])}</div>\n'
-        if '描述' in topic:
-            html += f'<div class="topic-desc"><strong>描述:</strong>{html_module.escape(topic["描述"])}</div>\n'
-        html += '</div>\n'
-
-    # 灵感点
-    if '灵感点' in data:
-        inspiration = data['灵感点']
-        html += '<div class="section">\n'
-        html += '<h3>灵感点</h3>\n'
-        html += '<div class="point-cards-list">\n'
-
-        if isinstance(inspiration, list):
-            for idx, item in enumerate(inspiration, 1):
-                html += render_inspiration_card(item, idx)
-
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 目的点
-    if '目的点' in data:
-        purpose = data['目的点']
-        html += '<div class="section">\n'
-        html += '<h3>目的点</h3>\n'
-        html += '<div class="point-cards-list">\n'
-
-        if isinstance(purpose, dict) and 'purposes' in purpose:
-            purpose_list = purpose['purposes']
-        elif isinstance(purpose, list):
-            purpose_list = purpose
-        else:
-            purpose_list = []
-
-        if isinstance(purpose_list, list):
-            for idx, item in enumerate(purpose_list, 1):
-                html += render_purpose_card(item, idx)
-
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 关键点
-    if '关键点' in data:
-        keypoint = data['关键点']
-        html += '<div class="section">\n'
-        html += '<h3>关键点</h3>\n'
-        html += '<div class="point-cards-list">\n'
-
-        # 处理关键点数据:可能是列表,也可能是包含key_points的对象
-        keypoint_list = keypoint
-        if isinstance(keypoint, dict) and 'key_points' in keypoint:
-            keypoint_list = keypoint['key_points']
-        
-        if isinstance(keypoint_list, list):
-            for idx, item in enumerate(keypoint_list, 1):
-                html += render_keypoint_card(item, idx, 0)
-
-        html += '</div>\n'
-        html += '</div>\n'
-
-    html += '</div>\n'
-    return html

+ 0 - 176
examples/static/visualize/tab2.py

@@ -1,176 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tab2内容生成器 - 段落结构
-"""
-import html as html_module
-import re
-from typing import Dict, Any, List
-
-
-def extract_image_numbers(text: str) -> List[int]:
-    """
-    从文本中提取图片编号
-
-    Args:
-        text: 包含图片编号的文本(如"图1"、"图片1"、"图2"或列表)
-
-    Returns:
-        图片编号列表(从1开始)
-    """
-    if not text:
-        return []
-
-    # 使用正则表达式提取所有"图X"或"图片X"格式的编号
-    matches = re.findall(r'图片?(\d+)', str(text))
-    return [int(num) for num in matches]
-
-
-def render_images(text: str, images: List[str]) -> str:
-    """
-    根据文本中的图片编号渲染图片HTML
-
-    Args:
-        text: 包含图片编号的文本
-        images: 原始图片URL列表
-
-    Returns:
-        图片HTML字符串
-    """
-    image_numbers = extract_image_numbers(text)
-    if not image_numbers or not images:
-        return ""
-
-    html = '<div class="related-images">\n'
-    for num in image_numbers:
-        # 图片编号从1开始,数组索引从0开始
-        idx = num - 1
-        if 0 <= idx < len(images):
-            html += f'<div class="image-item">\n'
-            html += f'<img src="{images[idx]}" alt="图{num}" loading="lazy">\n'
-            html += f'<div class="image-label">图{num}</div>\n'
-            html += f'</div>\n'
-    html += '</div>\n'
-    return html
-
-
-def render_paragraph_tree(paragraphs: List[Dict[str, Any]], level: int = 0, images: List[str] = None) -> str:
-    """
-    递归渲染段落树(支持展开/收起)
-
-    Args:
-        paragraphs: 段落列表
-        level: 层级深度
-        images: 原始图片URL列表
-
-    Returns:
-        HTML字符串
-    """
-    if not paragraphs:
-        return ""
-
-    if images is None:
-        images = []
-
-    html = f'<ul class="paragraph-list level-{level}">\n'
-
-    for para in paragraphs:
-        para_id = para.get('id', '')
-        desc = para.get('描述', '')
-        content_range = para.get('内容范围', '')
-        reason = para.get('推理依据', '')
-        children = para.get('子项', [])
-
-        has_children = bool(children)
-        collapsible_class = 'collapsible' if has_children else ''
-
-        html += f'<li class="paragraph-item {collapsible_class}" data-para-id="{html_module.escape(para_id)}">\n'
-
-        # 检查是否有详细信息
-        has_details = bool(content_range or reason)
-
-        html += f'<div class="paragraph-header" onclick="toggleCollapse(this)">\n'
-
-        # 添加展开/收起图标
-        if has_children:
-            html += f'<span class="toggle-icon">▼</span>\n'
-
-        # 将段落索引和描述包裹在一个容器中
-        html += f'<div class="paragraph-header-content">\n'
-        html += f'<span class="paragraph-index">{html_module.escape(para_id)}</span>\n'
-        if desc:
-            html += f'<span class="paragraph-desc">{html_module.escape(desc)}</span>\n'
-        html += f'</div>\n'
-
-        # 添加详细信息按钮(在标题右侧)
-        if has_details:
-            html += f'<div class="details-toggle-btn" onclick="event.stopPropagation(); toggleDetails(this)"><span class="details-icon">▶</span> 查看详细内容</div>\n'
-
-        html += f'</div>\n'
-
-        # 将内容包裹在可折叠的容器中
-        html += f'<div class="collapsible-content">\n'
-
-        # 添加详细信息区域(可独立收起展开)
-        if has_details:
-            html += f'<div class="paragraph-details collapsed">\n'
-            html += f'<div class="details-content">\n'
-
-            # 添加内容范围(处理列表或字符串类型)
-            if content_range:
-                if isinstance(content_range, list):
-                    # 如果是列表,格式化为带标签的列表
-                    range_html = '<ul class="content-range-list">'
-                    for item in content_range:
-                        range_html += f'<li>{html_module.escape(str(item))}</li>'
-                    range_html += '</ul>'
-                    html += f'<div class="paragraph-content"><strong>内容范围:</strong>{range_html}</div>\n'
-                    # 显示相关图片
-                    if images:
-                        html += render_images(str(content_range), images)
-                else:
-                    # 如果是字符串,保持原有格式
-                    html += f'<div class="paragraph-content"><strong>内容范围:</strong>{html_module.escape(str(content_range))}</div>\n'
-                    # 显示相关图片
-                    if images:
-                        html += render_images(str(content_range), images)
-
-            # 添加推理依据
-            if reason:
-                html += f'<div class="paragraph-reason"><strong>推理依据:</strong>{html_module.escape(reason)}</div>\n'
-
-            html += f'</div>\n'
-            html += f'</div>\n'
-
-        if children:
-            html += render_paragraph_tree(children, level + 1, images)
-
-        html += '</div>\n'
-        html += '</li>\n'
-
-    html += '</ul>\n'
-    return html
-
-
-def generate_tab2_content(data: Dict[str, Any]) -> str:
-    """生成Tab2内容:段落"""
-    html = '<div class="tab-content" id="tab2" style="display:none;">\n'
-
-    if '脚本理解' in data:
-        script = data['脚本理解']
-        script_images = script.get('图片列表', [])
-
-        html += '<div class="section">\n'
-        html += '<h3>脚本理解</h3>\n'
-        html += f'<div class="script-category"><strong>内容品类:</strong> {script.get("内容品类", "无")}</div>\n'
-
-        # 段落列表
-        if '段落列表' in script and script['段落列表']:
-            html += '<div class="section-header">\n'
-            html += '<h4>段落结构</h4>\n'
-            html += '</div>\n'
-            html += render_paragraph_tree(script['段落列表'], images=script_images)
-
-        html += '</div>\n'
-
-    html += '</div>\n'
-    return html

+ 0 - 1926
examples/static/visualize/tab3.py

@@ -1,1926 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tab3内容生成器 - 脚本点(元素列表)
-"""
-import html as html_module
-from typing import Dict, Any, List
-
-
-def get_intent_support_data(element: Dict[str, Any]) -> Dict[str, Any]:
-    """
-    获取元素的意图支撑数据(兼容新旧数据结构)
-    
-    优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
-    这两个字段内部存储的都是意图支撑数据
-    
-    Args:
-        element: 元素数据
-        
-    Returns:
-        意图支撑数据字典,格式:{"灵感点": [...], "目的点": [...], "关键点": [...]}
-    """
-    # 优先使用"意图支撑"字段
-    intent_support = element.get('意图支撑')
-    if intent_support and isinstance(intent_support, dict):
-        return intent_support
-    
-    # 如果没有"意图支撑",则使用"多维度评分"字段(兼容旧数据)
-    multi_scores = element.get('多维度评分')
-    if multi_scores and isinstance(multi_scores, dict):
-        return multi_scores
-    
-    # 都没有则返回空字典
-    return {}
-
-
-def calculate_intent_support_count(element: Dict[str, Any]) -> int:
-    """
-    计算元素的意图支撑数量
-
-    统计元素支撑的意图点总数
-    支撑的意图点越多,说明该元素与意图的关联越强
-
-    Args:
-        element: 元素数据
-
-    Returns:
-        支撑的意图点总数
-    """
-    # 统一使用意图支撑数据统计(实质和形式都使用相同逻辑)
-    # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
-    intent_support_data = get_intent_support_data(element)
-    
-    total_support_count = 0
-    for point_type in ['灵感点', '目的点', '关键点']:
-        support_points = intent_support_data.get(point_type, []) or []
-        # 每个项目就是一个支撑点,直接统计数量
-        total_support_count += len(support_points)
-
-    return total_support_count
-
-
-def get_element_coverage(element: Dict[str, Any]) -> float:
-    """
-    获取元素的段落覆盖率(兼容形式元素和实质元素)
-    
-    对于实质元素:从"共性分析"字段获取
-    对于形式元素:从"权重明细"中的"覆盖率分"反推
-    根据script_form_extraction_agent.py的逻辑:
-    - coverage_rate_base_score = coverage_rate * 50 (基础分0-50)
-    - coverage_rate_score = coverage_rate_base_score * 0.3 (加权后0-15)
-    所以:coverage_rate = coverage_rate_score / 15
-    
-    Args:
-        element: 元素数据
-        
-    Returns:
-        段落覆盖率(0.0-1.0)
-    """
-    dimension = element.get('维度', {})
-    is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
-    
-    if is_form:
-        # 形式元素:从权重明细中反推覆盖率
-        weight_details = element.get('权重明细', {})
-        if weight_details:
-            coverage_score = float(weight_details.get('覆盖率分', 0) or 0)
-            # 覆盖率分 = 覆盖率 × 50 × 0.3 = 覆盖率 × 15
-            # 所以:覆盖率 = 覆盖率分 / 15
-            if coverage_score > 0:
-                coverage = coverage_score / 15.0
-                return min(1.0, max(0.0, coverage))
-        
-        # 如果权重明细中没有覆盖率分,返回0
-        return 0.0
-    else:
-        # 实质元素:从共性分析中获取
-        commonality = element.get('共性分析') or {}
-        coverage = float(commonality.get('段落覆盖率', 0.0) or 0.0)
-        return coverage
-
-
-def get_support_stats(element: Dict[str, Any]) -> Dict[str, int]:
-    """
-    获取元素的支撑统计信息(灵感点/目的点/关键点数量)
-
-    优先使用元素中已经预计算好的 support_stats 字段;
-    若不存在,则根据「意图支撑」或「多维度评分」字段动态统计。
-    """
-    # 优先使用预计算的支撑统计
-    support_stats = element.get('支撑统计')
-    if isinstance(support_stats, dict):
-        # 做一次安全拷贝并补齐缺失字段
-        return {
-            '灵感点数量': int(support_stats.get('灵感点数量', 0) or 0),
-            '目的点数量': int(support_stats.get('目的点数量', 0) or 0),
-            '关键点数量': int(support_stats.get('关键点数量', 0) or 0),
-        }
-
-    # 兼容旧字段名
-    support_stats_old = element.get('support_stats')
-    if isinstance(support_stats_old, dict):
-        return {
-            '灵感点数量': int(support_stats_old.get('灵感点数量', 0) or 0),
-            '目的点数量': int(support_stats_old.get('目的点数量', 0) or 0),
-            '关键点数量': int(support_stats_old.get('关键点数量', 0) or 0),
-        }
-
-    # 统一使用意图支撑数据统计(实质和形式都使用相同逻辑)
-    # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
-    intent_support_data = get_intent_support_data(element)
-    
-    return {
-        '灵感点数量': len(intent_support_data.get('灵感点', []) or []),
-        '目的点数量': len(intent_support_data.get('目的点', []) or []),
-        '关键点数量': len(intent_support_data.get('关键点', []) or []),
-    }
-
-
-def compute_weight_scores(element: Dict[str, Any]) -> Dict[str, Any]:
-    """
-    计算元素的权重相关得分。
-
-    对于形式元素:优先使用元素中预计算的权重分和权重明细
-    对于实质元素:根据共性分析和意图支撑动态计算
-
-    原始总分 = 各子项得分之和
-    权重分 = min(100, 原始总分 × 100 / 110)
-    """
-    # 判断是形式元素还是实质元素
-    dimension = element.get('维度') or {}
-    is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
-
-    # 形式元素:优先使用预计算的权重信息
-    if is_form:
-        weight_score = element.get('权重分')
-        weight_details = element.get('权重明细')
-        support_stats = get_support_stats(element)
-
-        # 如果存在预计算的权重信息,直接使用
-        if weight_score is not None and weight_details is not None:
-            # 从权重明细中提取各项得分
-            freq_score = float(weight_details.get('频次分', 0) or 0)
-            coverage_count_score = float(weight_details.get('覆盖段落数分', 0) or 0)
-            coverage_rate_score = float(weight_details.get('覆盖率分', 0) or 0)
-            inspiration_score = float(weight_details.get('灵感点支撑分', 0) or 0)
-            purpose_score = float(weight_details.get('目的点支撑分', 0) or 0)
-            keypoint_score = float(weight_details.get('关键点支撑分', 0) or 0)
-
-            # 计算原始总分(共性总分 + 支撑总分)
-            commonality_total = weight_details.get('共性总分', 0) or 0
-            support_total = weight_details.get('支撑总分', 0) or 0
-            raw_total = float(commonality_total) + float(support_total)
-
-            return {
-                'weight_score': float(weight_score),
-                'raw_total': raw_total,
-                'details': {
-                    '频次分': freq_score,
-                    '覆盖段落数分': coverage_count_score,
-                    '覆盖率分': coverage_rate_score,
-                    '灵感点支撑分': inspiration_score,
-                    '目的点支撑分': purpose_score,
-                    '关键点支撑分': keypoint_score,
-                },
-                'support_stats': support_stats,
-            }
-
-    # 实质元素或形式元素没有预计算权重:使用旧的计算逻辑
-    commonality = element.get('共性分析') or {}
-    coverage = float(commonality.get('段落覆盖率', 0.0) or 0.0)
-    frequency = int(commonality.get('出现频次', 0) or 0)
-
-    support_stats = get_support_stats(element)
-    inspiration_count = support_stats.get('灵感点数量', 0) or 0
-    purpose_count = support_stats.get('目的点数量', 0) or 0
-    keypoint_count = support_stats.get('关键点数量', 0) or 0
-
-    # 1) 频次分(0–30分)
-    # 假定「高频」的参考上限为 12 次,超过即视为满分
-    # 频次分 = min(30, 出现频次 / 12 * 30)
-    if frequency <= 0:
-        freq_score = 0.0
-    else:
-        freq_score = min(30.0, frequency * 30.0 / 12.0)
-
-    # 2) 覆盖率分(0–30分)
-    # 覆盖率分 = 段落覆盖率 × 30
-    coverage_score = max(0.0, min(30.0, coverage * 30.0))
-
-    # 3) 灵感点支撑分(0–25分)
-    # 按你的说明:支撑{灵感点数量}个灵感点 × 25分/个,封顶 25 分
-    inspiration_score = min(25.0, float(inspiration_count) * 25.0)
-
-    # 4) 目的点支撑分(0–15分)
-    # 说明:支撑{目的点数量}个目的点 × 5分/个,封顶 15 分
-    purpose_score = min(15.0, float(purpose_count) * 5.0)
-
-    # 5) 关键点支撑分(0–10分)
-    # 说明:支撑{关键点数量}个关键点 × 1分/个,封顶 10 分
-    keypoint_score = min(10.0, float(keypoint_count) * 1.0)
-
-    raw_total = freq_score + coverage_score + inspiration_score + purpose_score + keypoint_score
-
-    if raw_total <= 0:
-        weight_score = 0.0
-    else:
-        weight_score = min(100.0, raw_total * 100.0 / 110.0)
-
-    return {
-        'weight_score': weight_score,
-        'raw_total': raw_total,
-        'details': {
-            '频次分': freq_score,
-            '覆盖率分': coverage_score,
-            '灵感点支撑分': inspiration_score,
-            '目的点支撑分': purpose_score,
-            '关键点支撑分': keypoint_score,
-        },
-        'support_stats': support_stats,
-    }
-
-
-def determine_dominant_factor(element: Dict[str, Any], all_elements: List[Dict[str, Any]]) -> str:
-    """
-    判断元素排序的主导因素
-
-    排序规则:覆盖率 > 频次 > 意图支撑数
-    主导因素判断:哪个指标在当前元素中相对最显著
-
-    Args:
-        element: 当前元素
-        all_elements: 同组所有元素
-
-    Returns:
-        主导因素: 'coverage' | 'frequency' | 'intent_support'
-    """
-    if not all_elements:
-        return 'coverage'
-
-    # 获取当前元素的指标
-    commonality = element.get('共性分析') or {}
-    coverage = commonality.get('段落覆盖率', 0.0)
-    frequency = commonality.get('出现频次', 0)
-    intent_count = calculate_intent_support_count(element)
-
-    # 将所有指标归一化到同一量级,然后比较
-    # 覆盖率已经是0-1范围
-    # 频次归一化:假设最大频次为10
-    normalized_frequency = min(frequency / 10.0, 1.0)
-    # 意图支撑数归一化:假设最大支撑数为10
-    normalized_intent = min(intent_count / 10.0, 1.0)
-
-    # 比较归一化后的值,取最大的作为主导因素
-    scores = {
-        'coverage': coverage,
-        'frequency': normalized_frequency,
-        'intent_support': normalized_intent
-    }
-    return max(scores, key=scores.get)
-
-
-def sort_elements_by_coverage_and_frequency(elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
-    """
-    按照覆盖率、频次和意图支撑数对元素排序
-
-    排序规则:
-    1. 第一优先级:共性(段落覆盖率)- 倒序
-    2. 第二优先级:共性(出现频次)- 倒序
-    3. 第三优先级:意图支撑数 - 倒序
-
-    Args:
-        elements: 元素列表
-
-    Returns:
-        排序后的元素列表
-    """
-    def get_sort_key(elem):
-        # 获取共性分析,防止为None
-        commonality = elem.get('共性分析') or {}
-        # 获取段落覆盖率
-        coverage = commonality.get('段落覆盖率', 0.0)
-        # 获取出现频次
-        frequency = commonality.get('出现频次', 0)
-        # 计算意图支撑数
-        intent_count = calculate_intent_support_count(elem)
-        # 返回排序键(负数用于倒序)
-        return (-coverage, -frequency, -intent_count)
-
-    return sorted(elements, key=get_sort_key)
-
-
-def get_element_category(element: Dict[str, Any]) -> str:
-    """
-    获取元素的分类名称(支持新旧两种数据结构)
-
-    Args:
-        element: 元素数据
-
-    Returns:
-        分类名称字符串
-    """
-    category = element.get('分类', '未分类')
-    if isinstance(category, dict):
-        # 新结构:分类是对象,包含一级分类和二级分类
-        level1 = category.get('一级分类', '')
-        level2 = category.get('二级分类', '')
-        if level1 and level2:
-            return f"{level1} - {level2}"
-        elif level1:
-            return level1
-        else:
-            return '未分类'
-    else:
-        # 旧结构:分类是字符串或列表
-        if isinstance(category, list):
-            return ' - '.join(category) if category else '未分类'
-        return category if category else '未分类'
-
-
-def group_elements_by_hierarchical_category(elements: List[Dict[str, Any]]) -> Dict[str, Any]:
-    """
-    按树形分类结构组织元素(一级分类 → 二级分类 → 元素)
-
-    优化规则:同一个父节点下的所有子节点采用统一的分类格式展示
-    - 如果一级分类下既有元素又有分类,将元素归入"未分类"二级分类
-
-    Args:
-        elements: 元素列表
-
-    Returns:
-        树形分类结构字典
-    """
-    # 1. 按一级分类和二级分类分组
-    level1_groups = {}
-
-    for elem in elements:
-        category_data = elem.get('分类', {})
-
-        if isinstance(category_data, dict):
-            level1 = category_data.get('一级分类', '未分类')
-            level2 = category_data.get('二级分类', '')
-        elif isinstance(category_data, list):
-            # 列表格式:第一个元素作为一级分类,第二个作为二级分类
-            level1 = category_data[0] if len(category_data) > 0 else '未分类'
-            level2 = category_data[1] if len(category_data) > 1 else ''
-        else:
-            # 旧结构:分类是字符串
-            level1 = str(category_data) if category_data else '未分类'
-            level2 = ''
-
-        # 初始化一级分类
-        if level1 not in level1_groups:
-            level1_groups[level1] = {
-                'elements': [],
-                'level2_groups': {}
-            }
-
-        # 如果有二级分类,放入二级分类组;否则放入一级分类的直接元素列表(临时)
-        if level2:
-            if level2 not in level1_groups[level1]['level2_groups']:
-                level1_groups[level1]['level2_groups'][level2] = []
-            level1_groups[level1]['level2_groups'][level2].append(elem)
-        else:
-            level1_groups[level1]['elements'].append(elem)
-
-    # 1.5 优化:仅当一级分类下既有直接元素又有二级分类时,才将直接元素移到"未分类"二级分类中
-    # 如果一级分类下只有直接元素,没有二级分类,则保持原样(不需要"未分类"概念)
-    for level1_name, level1_data in level1_groups.items():
-        if level1_data['elements'] and level1_data['level2_groups']:
-            # 将直接元素移到"未分类"分类
-            if '未分类' not in level1_data['level2_groups']:
-                level1_data['level2_groups']['未分类'] = []
-            level1_data['level2_groups']['未分类'].extend(level1_data['elements'])
-            level1_data['elements'] = []
-        # 如果只有直接元素,没有二级分类,则保持level1_data['elements']不变
-
-    # 2. 对每个分类内的元素排序
-    for level1_data in level1_groups.values():
-        # 排序一级分类直接包含的元素
-        if level1_data['elements']:
-            level1_data['elements'] = sort_elements_by_coverage_and_frequency(level1_data['elements'])
-
-        # 排序每个二级分类的元素
-        for level2_name in level1_data['level2_groups']:
-            level1_data['level2_groups'][level2_name] = sort_elements_by_coverage_and_frequency(
-                level1_data['level2_groups'][level2_name]
-            )
-
-    # 3. 计算每个一级分类的统计信息用于排序
-    level1_scores = {}
-    for level1_name, level1_data in level1_groups.items():
-        # 收集该一级分类下的所有元素(包括二级分类下的)
-        all_elements = level1_data['elements'][:]
-        for level2_elements in level1_data['level2_groups'].values():
-            all_elements.extend(level2_elements)
-
-        if not all_elements:
-            level1_scores[level1_name] = (0.0, 0, 0.0)
-            continue
-
-        # 计算统计指标
-        avg_coverage = sum(get_element_coverage(e) for e in all_elements) / len(all_elements)
-        avg_frequency = sum((e.get('共性分析') or {}).get('出现频次', 0) for e in all_elements) / len(all_elements)
-        avg_intent_count = sum(calculate_intent_support_count(e) for e in all_elements) / len(all_elements)
-
-        level1_scores[level1_name] = (avg_coverage, avg_frequency, avg_intent_count)
-
-    # 4. 对一级分类排序
-    sorted_level1 = sorted(
-        level1_scores.keys(),
-        key=lambda c: (-level1_scores[c][0], -level1_scores[c][1], -level1_scores[c][2])
-    )
-
-    # 5. 对每个一级分类内的二级分类排序
-    for level1_name in sorted_level1:
-        level1_data = level1_groups[level1_name]
-        level2_groups = level1_data['level2_groups']
-
-        if not level2_groups:
-            continue
-
-        # 计算二级分类的统计信息
-        level2_scores = {}
-        for level2_name, level2_elements in level2_groups.items():
-            if not level2_elements:
-                level2_scores[level2_name] = (0.0, 0, 0.0)
-                continue
-
-            avg_coverage = sum(get_element_coverage(e) for e in level2_elements) / len(level2_elements)
-            avg_frequency = sum((e.get('共性分析') or {}).get('出现频次', 0) for e in level2_elements) / len(level2_elements)
-            avg_intent_count = sum(calculate_intent_support_count(e) for e in level2_elements) / len(level2_elements)
-
-            level2_scores[level2_name] = (avg_coverage, avg_frequency, avg_intent_count)
-
-        # 排序二级分类
-        sorted_level2_names = sorted(
-            level2_scores.keys(),
-            key=lambda c: (-level2_scores[c][0], -level2_scores[c][1], -level2_scores[c][2])
-        )
-
-        # 重新组织为有序字典
-        sorted_level2_groups = {name: level2_groups[name] for name in sorted_level2_names}
-        level1_data['level2_groups'] = sorted_level2_groups
-
-    # 6. 返回排序后的结构
-    return {level1_name: level1_groups[level1_name] for level1_name in sorted_level1}
-
-
-def render_element_item(element: Dict[str, Any], all_elements: List[Dict[str, Any]] = None) -> str:
-    """渲染单个元素项的HTML(卡片样式,详细信息在弹窗中)
-
-    Args:
-        element: 元素数据
-        all_elements: 同组所有元素(用于计算主导因素)
-    """
-    elem_id = element.get('id', '')
-    name = element.get('名称') or ''  # 处理None的情况
-    description = element.get('描述') or ''  # 处理None的情况
-
-    # 获取类型和维度(兼容新旧结构)
-    dimension = element.get('维度', {})
-    if isinstance(dimension, dict):
-        elem_type = dimension.get('一级', '')
-        elem_type_level2 = dimension.get('二级', '')
-    else:
-        elem_type = element.get('类型', '')
-        elem_type_level2 = ''
-
-    # 获取共性分析(防止为None)
-    commonality = element.get('共性分析') or {}
-    coverage = commonality.get('段落覆盖率', 0.0)
-    frequency = commonality.get('出现频次', 0)
-    intent_count = calculate_intent_support_count(element)
-
-    # 计算权重得分
-    weight_info = compute_weight_scores(element)
-
-    # 计算主导因素
-    dominant_factor = 'coverage'  # 默认
-    if all_elements:
-        dominant_factor = determine_dominant_factor(element, all_elements)
-
-    # 根据主导因素确定边框颜色
-    border_color_class = f'dominant-{dominant_factor}'
-
-    # 检查是否有详细信息
-    category_def = element.get('分类定义', '')
-    paragraphs_list = commonality.get('出现段落列表', [])
-    source = element.get('来源', [])
-    intent_support = get_intent_support_data(element)
-    has_details = bool(category_def or paragraphs_list or source or intent_support or weight_info.get('raw_total', 0) > 0)
-
-    # 判断是否为形式元素
-    is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
-
-    # 卡片样式
-    html = f'<li class="element-card {border_color_class}" data-elem-id="{elem_id}">\n'
-    html += '<div class="element-card-body">\n'
-    
-    # 卡片头部
-    html += '<div class="element-card-header">\n'
-    if elem_id:
-        html += f'<span class="element-card-id">#{elem_id}</span>\n'
-    html += f'<h4 class="element-card-name">{html_module.escape(name)}</h4>\n'
-    html += '</div>\n'
-
-    # 统计指标
-    html += '<div class="element-card-stats">\n'
-    if weight_info.get('raw_total', 0) > 0:
-        html += f'<span class="stat-badge stat-weight">权重分: {weight_info["weight_score"]:.1f}</span>\n'
-    
-    if is_form:
-        html += f'<span class="stat-badge stat-intent">支撑: {intent_count}</span>\n'
-    else:
-        coverage_highlight = 'stat-highlight' if dominant_factor == 'coverage' else ''
-        frequency_highlight = 'stat-highlight' if dominant_factor == 'frequency' else ''
-        intent_highlight = 'stat-highlight' if dominant_factor == 'intent_support' else ''
-        
-        html += f'<span class="stat-badge stat-coverage {coverage_highlight}">覆盖率: {coverage:.2%}</span>\n'
-        html += f'<span class="stat-badge stat-frequency {frequency_highlight}">频次: {frequency}</span>\n'
-        html += f'<span class="stat-badge stat-intent {intent_highlight}">意图支撑: {intent_count}</span>\n'
-    html += '</div>\n'
-
-    # 描述
-    if description:
-        html += f'<div class="element-card-description">{html_module.escape(description)}</div>\n'
-
-    # 查看详情按钮
-    if has_details:
-        html += f'<button class="element-detail-btn" onclick="openElementModal(\'{elem_id}\')">查看详情</button>\n'
-
-    html += '</div>\n'  # element-card-body
-    html += '</li>\n'
-    return html
-
-
-def render_element_modal(element: Dict[str, Any]) -> str:
-    """渲染元素详情的弹窗内容
-
-    Args:
-        element: 元素数据
-    """
-    elem_id = element.get('id', '')
-    name = element.get('名称') or ''
-    description = element.get('描述') or ''
-
-    # 获取类型和维度
-    dimension = element.get('维度', {})
-    if isinstance(dimension, dict):
-        elem_type = dimension.get('一级', '')
-        elem_type_level2 = dimension.get('二级', '')
-    else:
-        elem_type = element.get('类型', '')
-        elem_type_level2 = ''
-
-    # 获取分类
-    category_data = element.get('分类', '')
-    category_def = element.get('分类定义', '')
-    
-    # 获取共性分析
-    commonality = element.get('共性分析') or {}
-    coverage = commonality.get('段落覆盖率', 0.0)
-    frequency = commonality.get('出现频次', 0)
-    paragraphs_list = commonality.get('出现段落列表', [])
-    source = element.get('来源', [])
-    intent_support = get_intent_support_data(element)
-    weight_info = compute_weight_scores(element)
-
-    html = f'<div class="element-modal-content" data-elem-id="{elem_id}">\n'
-    html += '<div class="modal-header">\n'
-    if elem_id:
-        html += f'<span class="modal-id">#{elem_id}</span>\n'
-    html += f'<h3 class="modal-title">{html_module.escape(name)}</h3>\n'
-    html += '<button class="modal-close" onclick="closeElementModal()">&times;</button>\n'
-    html += '</div>\n'
-
-    html += '<div class="modal-body">\n'
-
-    # 描述
-    if description:
-        html += '<div class="detail-section">\n'
-        html += '<strong>描述:</strong>\n'
-        html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
-        html += '</div>\n'
-
-    # 分类定义
-    if category_def:
-        html += '<div class="detail-section">\n'
-        html += '<strong>分类定义:</strong>\n'
-        html += f'<div class="detail-text">{html_module.escape(category_def)}</div>\n'
-        html += '</div>\n'
-
-    # 针对"形式"维度,显示"支撑"、"推理"和"支撑关系"
-    if isinstance(dimension, dict) and dimension.get('一级') == '形式':
-        # 支撑
-        zhicheng = element.get('支撑')
-        if zhicheng:
-                html += '<div class="detail-section">\n'
-                html += '<strong>支撑:</strong>\n'
-                html += '<div class="detail-content">\n'
-                if isinstance(zhicheng, list):
-                    for item in zhicheng:
-                        if isinstance(item, dict):
-                            item_id = item.get('id', '')
-                            item_name = item.get('名称', '')
-                            html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
-                        else:
-                            html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
-                elif isinstance(zhicheng, dict):
-                    # 支撑可能是对象(包含具体元素、具象概念等)
-                    for key, values in zhicheng.items():
-                        if isinstance(values, list):
-                            html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
-                            for item in values:
-                                if isinstance(item, dict):
-                                    item_id = item.get('id', '')
-                                    item_name = item.get('名称', '')
-                                    html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
-                                else:
-                                    html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
-                        else:
-                            html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
-                else:
-                    html += f'<span class="detail-tag">{html_module.escape(str(zhicheng))}</span>\n'
-                html += '</div>\n'
-                html += '</div>\n'
-
-        # 推理
-        tuili = element.get('推理')
-        if tuili:
-                html += '<div class="detail-section">\n'
-                html += '<strong>推理:</strong>\n'
-                html += f'<div class="detail-text">{html_module.escape(tuili)}</div>\n'
-                html += '</div>\n'
-
-        # 支撑关系(形式元素专用,从意图支撑数据中显示支撑点)
-        # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
-        intent_support_data = get_intent_support_data(element)
-        if intent_support_data:
-            html += '<div class="detail-section">\n'
-            html += '<strong>支撑关系:</strong>\n'
-            for point_type in ['灵感点', '目的点', '关键点']:
-                    support_points = intent_support_data.get(point_type) or []
-                    if not support_points:
-                        continue
-
-                    html += f'<div class="score-type">{point_type}</div>\n'
-                    html += '<div class="score-list">\n'
-
-                    # 每个项目就是一个支撑点
-                    for support_point in support_points:
-                        if not isinstance(support_point, dict):
-                            continue
-
-                        point = support_point.get('点', '')
-                        point_intention = support_point.get('点的意图', '')
-                        support_reason = support_point.get('支撑理由', '')
-
-                        html += '<div class="score-item">\n'
-                        html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
-
-                        # 显示点的意图
-                        if point_intention:
-                            html += '<div class="point-intention">\n'
-                            html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
-                            html += '</div>\n'
-
-                        # 显示支撑理由
-                        if support_reason:
-                            html += '<div class="score-reasons">\n'
-                            html += f'<strong style="color: #666;">支撑理由:</strong>\n'
-                            html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
-                            html += '</div>\n'
-
-                        html += '</div>\n'
-
-                    html += '</div>\n'  # end score-list
-            html += '</div>\n'  # end detail-section
-
-        # 针对"隐含概念",显示"来源"(声音特征、语气语调、BGM、音效等)和"时间范围"
-        elem_type = element.get('类型', '')
-        if elem_type == '隐含概念':
-            # 来源(隐含概念的来源包含声音特征、语气语调、背景音乐、音效等)
-            laiyuan = element.get('来源')
-            if laiyuan and isinstance(laiyuan, dict):
-                html += '<div class="detail-section">\n'
-                html += '<strong>来源:</strong>\n'
-                html += '<div class="detail-content">\n'
-                for key, values in laiyuan.items():
-                    if isinstance(values, list) and values:
-                        html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
-                        for item in values:
-                            html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
-                    elif values:
-                        html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
-                html += '</div>\n'
-                html += '</div>\n'
-
-            # 时间范围
-            time_range = element.get('时间范围')
-            if time_range:
-                html += '<div class="detail-section">\n'
-                html += '<strong>时间范围:</strong>\n'
-                html += '<div class="detail-content">\n'
-                if isinstance(time_range, list):
-                    for tr in time_range:
-                        html += f'<span class="detail-tag source-tag">{html_module.escape(str(tr))}</span>\n'
-                else:
-                    html += f'<span class="detail-tag source-tag">{html_module.escape(str(time_range))}</span>\n'
-                html += '</div>\n'
-                html += '</div>\n'
-
-        # 针对"抽象概念"(实质-抽象概念),显示"类型"、"来源"和"推理过程"
-        # 注意:排除隐含概念(隐含概念有自己的显示逻辑)
-        elif isinstance(dimension, dict) and dimension.get('二级') == '抽象概念':
-            # 类型
-            leixing = element.get('类型')
-            if leixing:
-                html += '<div class="detail-section">\n'
-                html += '<strong>类型:</strong>\n'
-                html += f'<div class="detail-text">{html_module.escape(str(leixing))}</div>\n'
-                html += '</div>\n'
-
-            # 来源(抽象概念的来源可能是复杂对象)
-            laiyuan = element.get('来源')
-            if laiyuan and isinstance(laiyuan, dict):
-                html += '<div class="detail-section">\n'
-                html += '<strong>来源:</strong>\n'
-                html += '<div class="detail-content">\n'
-                for key, values in laiyuan.items():
-                    if isinstance(values, list):
-                        html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
-                        for item in values:
-                            if isinstance(item, dict):
-                                item_id = item.get('id', '')
-                                item_name = item.get('名称', '')
-                                html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
-                            else:
-                                html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
-                    else:
-                        html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
-                html += '</div>\n'
-                html += '</div>\n'
-
-            # 推理过程
-            tuili_guocheng = element.get('推理过程')
-            if tuili_guocheng:
-                html += '<div class="detail-section">\n'
-                html += '<strong>推理过程:</strong>\n'
-                html += f'<div class="detail-text">{html_module.escape(tuili_guocheng)}</div>\n'
-                html += '</div>\n'
-
-        else:
-            if source:
-                html += '<div class="detail-section">\n'
-                html += '<strong>来源:</strong>\n'
-                html += '<div class="detail-content">\n'
-                for src in source:
-                    html += f'<span class="detail-tag source-tag">{html_module.escape(str(src))}</span>\n'
-                html += '</div>\n'
-                html += '</div>\n'
-
-        # 上下文验证(适用于具象概念)
-        context_verification = element.get('上下文验证')
-        if context_verification:
-            html += '<div class="detail-section">\n'
-            html += '<strong>上下文验证:</strong>\n'
-            html += '<div class="context-verification" style="margin-top: 8px; padding: 12px; background-color: #f8f9fa; border-radius: 4px; border-left: 3px solid #6c757d;">\n'
-
-            # 原文位置
-            original_position = context_verification.get('原文位置', '')
-            if original_position:
-                html += '<div class="context-item" style="margin-bottom: 8px;">\n'
-                html += '<strong style="color: #495057; font-size: 13px;">原文位置:</strong>\n'
-                html += f'<div style="margin-top: 4px; padding: 6px 10px; background-color: #fff; border-radius: 3px; color: #212529; font-style: italic;">{html_module.escape(original_position)}</div>\n'
-                html += '</div>\n'
-
-            # 语法成分
-            grammar_component = context_verification.get('语法成分', '')
-            if grammar_component:
-                html += '<div class="context-item" style="margin-bottom: 8px;">\n'
-                html += '<strong style="color: #495057; font-size: 13px;">语法成分:</strong>\n'
-                html += f'<span style="margin-left: 8px; padding: 3px 10px; background-color: #e7f3ff; color: #0066cc; border-radius: 3px; font-size: 12px;">{html_module.escape(grammar_component)}</span>\n'
-                html += '</div>\n'
-
-            # 语境判断
-            context_judgment = context_verification.get('语境判断', '')
-            if context_judgment:
-                html += '<div class="context-item" style="margin-bottom: 0;">\n'
-                html += '<strong style="color: #495057; font-size: 13px;">语境判断:</strong>\n'
-                html += f'<div style="margin-top: 4px; padding: 8px 10px; background-color: #fff; border-radius: 3px; color: #495057; line-height: 1.6;">{html_module.escape(context_judgment)}</div>\n'
-                html += '</div>\n'
-
-            html += '</div>\n'
-            html += '</div>\n'
-
-        # 出现段落
-        if paragraphs_list:
-            html += '<div class="detail-section">\n'
-            html += '<strong>出现段落:</strong>\n'
-            html += '<div class="paragraphs-detail-list">\n'
-            for para in paragraphs_list:
-                if isinstance(para, dict):
-                    # 新结构:对象包含段落ID和如何体现
-                    para_id = para.get('段落ID', '')
-                    how = para.get('如何体现', '')
-                    html += '<div class="paragraph-detail-item">\n'
-                    html += f'<span class="detail-tag para-id-tag">{html_module.escape(para_id)}</span>\n'
-                    if how:
-                        html += f'<div class="para-how">{html_module.escape(how)}</div>\n'
-                    html += '</div>\n'
-                else:
-                    # 旧结构:字符串
-                    html += f'<span class="detail-tag">{html_module.escape(str(para))}</span>\n'
-            html += '</div>\n'
-            html += '</div>\n'
-
-        # 意图支撑
-        if intent_support:
-            html += '<div class="detail-section">\n'
-            html += '<strong>意图支撑:</strong>\n'
-
-            for point_type in ['灵感点', '目的点', '关键点']:
-                if point_type in intent_support and intent_support[point_type]:
-                    html += f'<div class="score-type">{point_type}</div>\n'
-                    html += '<div class="score-list">\n'
-
-                    for item in intent_support[point_type]:
-                        point = item.get('点', '')
-                        point_intention = item.get('点的意图', '')
-                        support_reason = item.get('支撑理由', '')
-
-                        html += '<div class="score-item">\n'
-                        html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
-
-                        # 显示点的意图
-                        if point_intention:
-                            html += '<div class="point-intention">\n'
-                            html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
-                            html += '</div>\n'
-
-                        # 显示支撑理由
-                        if support_reason:
-                            html += '<div class="score-reasons">\n'
-                            html += f'<strong style="color: #666;">支撑理由:</strong>\n'
-                            html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
-                            html += '</div>\n'
-
-                        html += '</div>\n'
-
-                    html += '</div>\n'
-
-            html += '</div>\n'
-
-        # 权重明细(放在展开内容最底部,简化为紧凑样式)
-        if weight_info.get('raw_total', 0) > 0:
-            wd = weight_info['details']
-            ss = weight_info['support_stats']
-
-            # 判断是形式元素还是实质元素
-            is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
-
-            html += '<div class="detail-section weight-detail-section" style="margin-top: 12px; border-top: 1px dashed #e0e0e0; padding-top: 10px;">\n'
-            html += '<strong style="font-size: 13px; color: #555;">权重得分明细:</strong>\n'
-            html += '<div class="weight-summary-header" style="margin-top: 4px; font-size: 12px; color: #666;">\n'
-            if is_form:
-                html += f'权重分:{weight_info["weight_score"]:.1f}(形式元素使用新的权重计算逻辑)\n'
-            else:
-                html += f'原始总分:{weight_info["raw_total"]:.1f},权重分:{weight_info["weight_score"]:.1f}(min(100, 原始总分 × 100 / 110))\n'
-            html += '</div>\n'
-
-            html += '<div class="weight-summary-grid" style="margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px;">\n'
-            
-            # 共性维度得分
-            if is_form:
-                # 形式元素:显示频次分、覆盖段落数分、覆盖率分
-                html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">频次分 {wd.get("频次分", 0):.1f}</span>\n'
-                if "覆盖段落数分" in wd:
-                    html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">覆盖段落数分 {wd.get("覆盖段落数分", 0):.1f}</span>\n'
-                html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">覆盖率分 {wd.get("覆盖率分", 0):.1f}</span>\n'
-            else:
-                # 实质元素:显示频次分、覆盖率分
-                html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">频次分 {wd.get("频次分", 0):.1f} / 30</span>\n'
-                html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">覆盖率分 {wd.get("覆盖率分", 0):.1f} / 30</span>\n'
-
-            # 支撑维度得分
-            inspiration_count = ss.get('灵感点数量', 0)
-            if is_form:
-                html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">灵感点支撑分 {wd.get("灵感点支撑分", 0):.1f}({inspiration_count} 个)</span>\n'
-            else:
-                html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">灵感点支撑分 {wd.get("灵感点支撑分", 0):.1f} / 25({inspiration_count} 个)</span>\n'
-
-            purpose_count = ss.get('目的点数量', 0)
-            if is_form:
-                html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">目的点支撑分 {wd.get("目的点支撑分", 0):.1f}({purpose_count} 个)</span>\n'
-            else:
-                html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">目的点支撑分 {wd.get("目的点支撑分", 0):.1f} / 15({purpose_count} 个)</span>\n'
-
-            keypoint_count = ss.get('关键点数量', 0)
-            if is_form:
-                html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">关键点支撑分 {wd.get("关键点支撑分", 0):.1f}({keypoint_count} 个)</span>\n'
-            else:
-                html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">关键点支撑分 {wd.get("关键点支撑分", 0):.1f} / 10({keypoint_count} 个)</span>\n'
-
-            html += '</div>\n'
-            html += '</div>\n'
-
-        html += '</div>\n'
-
-    html += '</li>\n'
-    return html
-
-
-def render_element_modal(element: Dict[str, Any]) -> str:
-    """渲染元素详情的弹窗内容
-
-    Args:
-        element: 元素数据
-    """
-    elem_id = element.get('id', '')
-    name = element.get('名称') or ''
-    description = element.get('描述') or ''
-
-    # 获取类型和维度
-    dimension = element.get('维度', {})
-    if isinstance(dimension, dict):
-        elem_type = dimension.get('一级', '')
-        elem_type_level2 = dimension.get('二级', '')
-    else:
-        elem_type = element.get('类型', '')
-        elem_type_level2 = ''
-
-    # 获取分类
-    category_data = element.get('分类', '')
-    category_def = element.get('分类定义', '')
-    
-    # 获取共性分析
-    commonality = element.get('共性分析') or {}
-    coverage = commonality.get('段落覆盖率', 0.0)
-    frequency = commonality.get('出现频次', 0)
-    paragraphs_list = commonality.get('出现段落列表', [])
-    source = element.get('来源', [])
-    intent_support = get_intent_support_data(element)
-    weight_info = compute_weight_scores(element)
-
-    html = f'<div class="element-modal-content" data-elem-id="{elem_id}">\n'
-    html += '<div class="modal-header">\n'
-    if elem_id:
-        html += f'<span class="modal-id">#{elem_id}</span>\n'
-    html += f'<h3 class="modal-title">{html_module.escape(name)}</h3>\n'
-    html += '<button class="modal-close" onclick="closeElementModal()">&times;</button>\n'
-    html += '</div>\n'
-
-    html += '<div class="modal-body">\n'
-
-    # 描述
-    if description:
-        html += '<div class="detail-section">\n'
-        html += '<strong>描述:</strong>\n'
-        html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
-        html += '</div>\n'
-
-    # 分类定义
-    if category_def:
-        html += '<div class="detail-section">\n'
-        html += '<strong>分类定义:</strong>\n'
-        html += f'<div class="detail-text">{html_module.escape(category_def)}</div>\n'
-        html += '</div>\n'
-
-    # 针对"形式"维度,显示"支撑"、"推理"和"支撑关系"
-    if isinstance(dimension, dict) and dimension.get('一级') == '形式':
-        # 支撑
-        zhicheng = element.get('支撑')
-        if zhicheng:
-            html += '<div class="detail-section">\n'
-            html += '<strong>支撑:</strong>\n'
-            html += '<div class="detail-content">\n'
-            if isinstance(zhicheng, list):
-                for item in zhicheng:
-                    if isinstance(item, dict):
-                        item_id = item.get('id', '')
-                        item_name = item.get('名称', '')
-                        html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
-                    else:
-                        html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
-            elif isinstance(zhicheng, dict):
-                for key, values in zhicheng.items():
-                    if isinstance(values, list):
-                        html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
-                        for item in values:
-                            if isinstance(item, dict):
-                                item_id = item.get('id', '')
-                                item_name = item.get('名称', '')
-                                html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
-                            else:
-                                html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
-                    else:
-                        html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
-            else:
-                html += f'<span class="detail-tag">{html_module.escape(str(zhicheng))}</span>\n'
-            html += '</div>\n'
-            html += '</div>\n'
-
-        # 推理
-        tuili = element.get('推理')
-        if tuili:
-            html += '<div class="detail-section">\n'
-            html += '<strong>推理:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(tuili)}</div>\n'
-            html += '</div>\n'
-
-        # 支撑关系
-        intent_support_data = get_intent_support_data(element)
-        if intent_support_data:
-            html += '<div class="detail-section">\n'
-            html += '<strong>支撑关系:</strong>\n'
-            for point_type in ['灵感点', '目的点', '关键点']:
-                support_points = intent_support_data.get(point_type) or []
-                if not support_points:
-                    continue
-                html += f'<div class="score-type">{point_type}</div>\n'
-                html += '<div class="score-list">\n'
-                for support_point in support_points:
-                    if not isinstance(support_point, dict):
-                        continue
-                    point = support_point.get('点', '')
-                    point_intention = support_point.get('点的意图', '')
-                    support_reason = support_point.get('支撑理由', '')
-                    html += '<div class="score-item">\n'
-                    html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
-                    if point_intention:
-                        html += '<div class="point-intention">\n'
-                        html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
-                        html += '</div>\n'
-                    if support_reason:
-                        html += '<div class="score-reasons">\n'
-                        html += f'<strong style="color: #666;">支撑理由:</strong>\n'
-                        html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
-                        html += '</div>\n'
-                    html += '</div>\n'
-                html += '</div>\n'
-            html += '</div>\n'
-
-    # 针对"隐含概念"
-    elem_type = element.get('类型', '')
-    if elem_type == '隐含概念':
-        laiyuan = element.get('来源')
-        if laiyuan and isinstance(laiyuan, dict):
-            html += '<div class="detail-section">\n'
-            html += '<strong>来源:</strong>\n'
-            html += '<div class="detail-content">\n'
-            for key, values in laiyuan.items():
-                if isinstance(values, list) and values:
-                    html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
-                    for item in values:
-                        html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
-                elif values:
-                    html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
-            html += '</div>\n'
-            html += '</div>\n'
-
-        time_range = element.get('时间范围')
-        if time_range:
-            html += '<div class="detail-section">\n'
-            html += '<strong>时间范围:</strong>\n'
-            html += '<div class="detail-content">\n'
-            if isinstance(time_range, list):
-                for tr in time_range:
-                    html += f'<span class="detail-tag source-tag">{html_module.escape(str(tr))}</span>\n'
-            else:
-                html += f'<span class="detail-tag source-tag">{html_module.escape(str(time_range))}</span>\n'
-            html += '</div>\n'
-            html += '</div>\n'
-
-    # 针对"抽象概念"
-    elif isinstance(dimension, dict) and dimension.get('二级') == '抽象概念':
-        leixing = element.get('类型')
-        if leixing:
-            html += '<div class="detail-section">\n'
-            html += '<strong>类型:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(str(leixing))}</div>\n'
-            html += '</div>\n'
-
-        laiyuan = element.get('来源')
-        if laiyuan and isinstance(laiyuan, dict):
-            html += '<div class="detail-section">\n'
-            html += '<strong>来源:</strong>\n'
-            html += '<div class="detail-content">\n'
-            for key, values in laiyuan.items():
-                if isinstance(values, list):
-                    html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
-                    for item in values:
-                        if isinstance(item, dict):
-                            item_id = item.get('id', '')
-                            item_name = item.get('名称', '')
-                            html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
-                        else:
-                            html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
-                else:
-                    html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
-            html += '</div>\n'
-            html += '</div>\n'
-
-        tuili_guocheng = element.get('推理过程')
-        if tuili_guocheng:
-            html += '<div class="detail-section">\n'
-            html += '<strong>推理过程:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(tuili_guocheng)}</div>\n'
-            html += '</div>\n'
-
-    else:
-        if source:
-            html += '<div class="detail-section">\n'
-            html += '<strong>来源:</strong>\n'
-            html += '<div class="detail-content">\n'
-            for src in source:
-                html += f'<span class="detail-tag source-tag">{html_module.escape(str(src))}</span>\n'
-            html += '</div>\n'
-            html += '</div>\n'
-
-    # 上下文验证
-    context_verification = element.get('上下文验证')
-    if context_verification:
-        html += '<div class="detail-section">\n'
-        html += '<strong>上下文验证:</strong>\n'
-        html += '<div class="context-verification">\n'
-        original_position = context_verification.get('原文位置', '')
-        if original_position:
-            html += '<div class="context-item">\n'
-            html += '<strong>原文位置:</strong>\n'
-            html += f'<div class="context-text">{html_module.escape(original_position)}</div>\n'
-            html += '</div>\n'
-        grammar_component = context_verification.get('语法成分', '')
-        if grammar_component:
-            html += '<div class="context-item">\n'
-            html += '<strong>语法成分:</strong>\n'
-            html += f'<span class="grammar-tag">{html_module.escape(grammar_component)}</span>\n'
-            html += '</div>\n'
-        context_judgment = context_verification.get('语境判断', '')
-        if context_judgment:
-            html += '<div class="context-item">\n'
-            html += '<strong>语境判断:</strong>\n'
-            html += f'<div class="context-text">{html_module.escape(context_judgment)}</div>\n'
-            html += '</div>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 出现段落
-    if paragraphs_list:
-        html += '<div class="detail-section">\n'
-        html += '<strong>出现段落:</strong>\n'
-        html += '<div class="paragraphs-detail-list">\n'
-        for para in paragraphs_list:
-            if isinstance(para, dict):
-                para_id = para.get('段落ID', '')
-                how = para.get('如何体现', '')
-                html += '<div class="paragraph-detail-item">\n'
-                html += f'<span class="detail-tag para-id-tag">{html_module.escape(para_id)}</span>\n'
-                if how:
-                    html += f'<div class="para-how">{html_module.escape(how)}</div>\n'
-                html += '</div>\n'
-            else:
-                html += f'<span class="detail-tag">{html_module.escape(str(para))}</span>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 意图支撑
-    if intent_support:
-        html += '<div class="detail-section">\n'
-        html += '<strong>意图支撑:</strong>\n'
-        for point_type in ['灵感点', '目的点', '关键点']:
-            if point_type in intent_support and intent_support[point_type]:
-                html += f'<div class="score-type">{point_type}</div>\n'
-                html += '<div class="score-list">\n'
-                for item in intent_support[point_type]:
-                    point = item.get('点', '')
-                    point_intention = item.get('点的意图', '')
-                    support_reason = item.get('支撑理由', '')
-                    html += '<div class="score-item">\n'
-                    html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
-                    if point_intention:
-                        html += '<div class="point-intention">\n'
-                        html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
-                        html += '</div>\n'
-                    if support_reason:
-                        html += '<div class="score-reasons">\n'
-                        html += f'<strong style="color: #666;">支撑理由:</strong>\n'
-                        html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
-                        html += '</div>\n'
-                    html += '</div>\n'
-                html += '</div>\n'
-        html += '</div>\n'
-
-    # 权重明细
-    if weight_info.get('raw_total', 0) > 0:
-        wd = weight_info['details']
-        ss = weight_info['support_stats']
-        is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
-
-        html += '<div class="detail-section weight-detail-section">\n'
-        html += '<strong>权重得分明细:</strong>\n'
-        html += '<div class="weight-summary-header">\n'
-        if is_form:
-            html += f'权重分:{weight_info["weight_score"]:.1f}(形式元素使用新的权重计算逻辑)\n'
-        else:
-            html += f'原始总分:{weight_info["raw_total"]:.1f},权重分:{weight_info["weight_score"]:.1f}(min(100, 原始总分 × 100 / 110))\n'
-        html += '</div>\n'
-        html += '<div class="weight-summary-grid">\n'
-        
-        if is_form:
-            html += f'<span class="weight-chip">频次分 {wd.get("频次分", 0):.1f}</span>\n'
-            if "覆盖段落数分" in wd:
-                html += f'<span class="weight-chip">覆盖段落数分 {wd.get("覆盖段落数分", 0):.1f}</span>\n'
-            html += f'<span class="weight-chip">覆盖率分 {wd.get("覆盖率分", 0):.1f}</span>\n'
-        else:
-            html += f'<span class="weight-chip">频次分 {wd.get("频次分", 0):.1f} / 30</span>\n'
-            html += f'<span class="weight-chip">覆盖率分 {wd.get("覆盖率分", 0):.1f} / 30</span>\n'
-
-        inspiration_count = ss.get('灵感点数量', 0)
-        if is_form:
-            html += f'<span class="weight-chip">灵感点支撑分 {wd.get("灵感点支撑分", 0):.1f}({inspiration_count} 个)</span>\n'
-        else:
-            html += f'<span class="weight-chip">灵感点支撑分 {wd.get("灵感点支撑分", 0):.1f} / 25({inspiration_count} 个)</span>\n'
-
-        purpose_count = ss.get('目的点数量', 0)
-        if is_form:
-            html += f'<span class="weight-chip">目的点支撑分 {wd.get("目的点支撑分", 0):.1f}({purpose_count} 个)</span>\n'
-        else:
-            html += f'<span class="weight-chip">目的点支撑分 {wd.get("目的点支撑分", 0):.1f} / 15({purpose_count} 个)</span>\n'
-
-        keypoint_count = ss.get('关键点数量', 0)
-        if is_form:
-            html += f'<span class="weight-chip">关键点支撑分 {wd.get("关键点支撑分", 0):.1f}({keypoint_count} 个)</span>\n'
-        else:
-            html += f'<span class="weight-chip">关键点支撑分 {wd.get("关键点支撑分", 0):.1f} / 10({keypoint_count} 个)</span>\n'
-
-        html += '</div>\n'
-        html += '</div>\n'
-
-    html += '</div>\n'  # modal-body
-    html += '</div>\n'  # element-modal-content
-    return html
-
-
-def generate_tab3_content(data: Dict[str, Any]) -> str:
-    """生成Tab3内容:按层次展示(实质/形式 → 具体元素/具体概念/抽象概念 → 树形展示)"""
-    html = '<div class="tab-content" id="tab3" style="display:none;">\n'
-
-    # 添加全局控制按钮(移除展开/收起按钮)
-    html += '<div class="global-controls">\n'
-    html += '    <div class="color-legend">\n'
-    html += '        <span class="legend-title">颜色图例(主导因素):</span>\n'
-    html += '        <span class="legend-item legend-coverage">覆盖率</span>\n'
-    html += '        <span class="legend-item legend-frequency">频次</span>\n'
-    html += '        <span class="legend-item legend-intent">意图支撑</span>\n'
-    html += '    </div>\n'
-    html += '</div>\n'
-
-    # 收集所有元素用于弹窗
-    all_elements_dict = {}
-
-    if '脚本理解' in data:
-        script = data['脚本理解']
-        # 尝试获取元素列表,如果不存在则合并实质列表和形式列表
-        elements = script.get('元素列表', [])
-        if not elements:
-            substance_list = script.get('实质列表', [])
-            form_list = script.get('形式列表', [])
-            elements = substance_list + form_list
-
-        # 收集所有元素到字典中(用于弹窗)
-        for elem in elements:
-            elem_id = elem.get('id', '')
-            if elem_id:
-                all_elements_dict[elem_id] = elem
-
-        # 第一层:按维度.一级分组(实质 vs 形式)
-        level1_groups = {}
-        for elem in elements:
-            dimension = elem.get('维度', {})
-            if isinstance(dimension, dict):
-                level1 = dimension.get('一级', '实质')
-            else:
-                # 兼容旧结构
-                level1 = elem.get('类型', '实质')
-
-            if level1 not in level1_groups:
-                level1_groups[level1] = []
-            level1_groups[level1].append(elem)
-
-        # 按顺序渲染:实质、形式
-        for level1_name in ['实质', '形式']:
-            if level1_name not in level1_groups:
-                continue
-
-            level1_elements = level1_groups[level1_name]
-            html += '<div class="section level1-section">\n'
-            html += f'<div class="level1-header collapsible" onclick="toggleLevel1(this)">\n'
-            html += '<span class="level-toggle-icon">▼</span>\n'
-            html += f'<h2 class="level1-title">{level1_name} ({len(level1_elements)}个)</h2>\n'
-            html += '</div>\n'
-            html += '<div class="level1-content">\n'
-
-            # 第二层:按维度.二级分组
-            level2_groups = {}
-            for elem in level1_elements:
-                dimension = elem.get('维度', {})
-                elem_type = elem.get('类型', '')
-                
-                # 隐含概念:优先通过类型判断(因为维度二级可能是"隐含概念"或"抽象概念")
-                if elem_type == '隐含概念':
-                    level2 = '隐含概念'
-                elif isinstance(dimension, dict):
-                    level2 = dimension.get('二级', '具体元素')
-                else:
-                    # 兼容旧结构
-                    elem_type_old = elem.get('类型', '实质')
-                    if elem_type_old == '实质':
-                        level2 = '具体元素'
-                    elif elem_type_old == '具象概念':
-                        level2 = '具体概念'
-                    else:
-                        level2 = '抽象概念'
-
-                if level2 not in level2_groups:
-                    level2_groups[level2] = []
-                level2_groups[level2].append(elem)
-
-            # 根据一级维度确定二级维度遍历顺序
-            if level1_name == '实质':
-                level2_order = ['具体元素', '具象概念', '隐含概念', '抽象概念']
-            else:  # 形式
-                level2_order = ['具体元素形式', '具象概念形式', '整体形式']
-
-            # 按顺序渲染二级维度
-            for level2_name in level2_order:
-                if level2_name not in level2_groups:
-                    continue
-
-                level2_elements = level2_groups[level2_name]
-                html += '<div class="level2-section">\n'
-                html += f'<div class="level2-header collapsible" onclick="toggleLevel2(this)">\n'
-                html += '<span class="level-toggle-icon">▼</span>\n'
-                html += f'<h3 class="level2-title">{level2_name} ({len(level2_elements)}个)</h3>\n'
-                html += '</div>\n'
-                html += '<div class="level2-content">\n'
-
-                # 第三层:按树形分类结构组织
-                hierarchical_categories = group_elements_by_hierarchical_category(level2_elements)
-
-                # 渲染树形分类结构
-                for cat_level1_name, cat_level1_data in hierarchical_categories.items():
-                    # 收集该一级分类下的所有元素
-                    all_cat_elements = cat_level1_data['elements'][:]
-                    for level2_elems in cat_level1_data['level2_groups'].values():
-                        all_cat_elements.extend(level2_elems)
-
-                    if not all_cat_elements:
-                        continue
-
-                    # 计算一级分类的统计信息
-                    avg_coverage = sum(get_element_coverage(e) for e in all_cat_elements) / len(all_cat_elements)
-                    avg_intent_count = sum(calculate_intent_support_count(e) for e in all_cat_elements) / len(all_cat_elements)
-
-                    html += '<div class="category-group collapsible">\n'
-                    html += '<div class="category-header" onclick="toggleCategoryGroup(this)">\n'
-                    html += '<span class="category-toggle-icon">▼</span>\n'
-                    html += f'<h4 class="category-title">{html_module.escape(cat_level1_name)} ({len(all_cat_elements)}个)</h4>\n'
-                    html += '<div class="category-stats">\n'
-                    html += f'<span class="stat-badge">平均覆盖率: {avg_coverage:.2%}</span>\n'
-                    html += f'<span class="stat-badge">平均意图支撑: {avg_intent_count:.1f}</span>\n'
-                    html += '</div>\n'
-                    html += '</div>\n'
-
-                    html += '<div class="category-content">\n'
-
-                    # 渲染一级分类直接包含的元素
-                    if cat_level1_data['elements']:
-                        html += '<ul class="element-list">\n'
-                        for elem in cat_level1_data['elements']:
-                            html += render_element_item(elem, all_cat_elements)
-                        html += '</ul>\n'
-
-                    # 渲染二级分类
-                    for cat_level2_name, cat_level2_elements in cat_level1_data['level2_groups'].items():
-                        if not cat_level2_elements:
-                            continue
-
-                        # 计算二级分类的统计信息
-                        avg_coverage_l2 = sum(get_element_coverage(e) for e in cat_level2_elements) / len(cat_level2_elements)
-                        avg_intent_count_l2 = sum(calculate_intent_support_count(e) for e in cat_level2_elements) / len(cat_level2_elements)
-
-                        html += '<div class="subcategory-group collapsible">\n'
-                        html += '<div class="subcategory-header" onclick="toggleSubcategoryGroup(this)">\n'
-                        html += '<span class="subcategory-toggle-icon">▼</span>\n'
-                        html += f'<h5 class="subcategory-title">{html_module.escape(cat_level2_name)} ({len(cat_level2_elements)}个)</h5>\n'
-                        html += '<div class="subcategory-stats">\n'
-                        html += f'<span class="stat-badge-small">覆盖率: {avg_coverage_l2:.2%}</span>\n'
-                        html += '</div>\n'
-                        html += '</div>\n'
-
-                        html += '<div class="subcategory-content">\n'
-                        html += '<ul class="element-list">\n'
-                        for elem in cat_level2_elements:
-                            html += render_element_item(elem, cat_level2_elements)
-                        html += '</ul>\n'
-                        html += '</div>\n'
-                        html += '</div>\n'
-
-                    html += '</div>\n'
-                    html += '</div>\n'
-
-                html += '</div>\n'  # level2-content
-                html += '</div>\n'  # level2-section
-
-            html += '</div>\n'  # level1-content
-            html += '</div>\n'  # level1-section
-
-    # 预生成所有元素的弹窗内容(隐藏)
-    html += '<div id="elementModalTemplates" style="display:none;">\n'
-    for elem_id, elem in all_elements_dict.items():
-        html += render_element_modal(elem)
-    html += '</div>\n'
-
-    # 添加弹窗结构
-    html += '<div id="elementModal" class="element-modal" style="display:none;">\n'
-    html += '<div class="element-modal-overlay" onclick="closeElementModal()"></div>\n'
-    html += '<div class="element-modal-dialog">\n'
-    html += '<div id="elementModalContent" class="element-modal-content-wrapper">\n'
-    html += '<!-- 弹窗内容将通过JavaScript动态填充 -->\n'
-    html += '</div>\n'
-    html += '</div>\n'
-    html += '</div>\n'
-
-    # 添加CSS样式
-    html += '<style>\n'
-    html += '''
-    /* 元素卡片样式 */
-    .element-list {
-        display: grid;
-        grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
-        gap: 20px;
-        list-style: none;
-        padding: 0;
-        margin: 20px 0;
-    }
-    
-    .element-card {
-        background: white;
-        border-radius: 8px;
-        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-        transition: all 0.3s ease;
-        overflow: hidden;
-    }
-    
-    .element-card:hover {
-        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
-        transform: translateY(-2px);
-    }
-    
-    .element-card.dominant-coverage {
-        border-left: 4px solid #667eea;
-    }
-    
-    .element-card.dominant-frequency {
-        border-left: 4px solid #f093fb;
-    }
-    
-    .element-card.dominant-intent_support {
-        border-left: 4px solid #4facfe;
-    }
-    
-    .element-card-body {
-        padding: 16px;
-    }
-    
-    .element-card-header {
-        display: flex;
-        align-items: center;
-        gap: 8px;
-        margin-bottom: 12px;
-    }
-    
-    .element-card-id {
-        color: #667eea;
-        font-weight: 600;
-        font-size: 14px;
-    }
-    
-    .element-card-name {
-        margin: 0;
-        font-size: 18px;
-        font-weight: 600;
-        color: #333;
-        flex: 1;
-    }
-    
-    .element-card-stats {
-        display: flex;
-        flex-wrap: wrap;
-        gap: 8px;
-        margin-bottom: 12px;
-    }
-    
-    .stat-badge {
-        display: inline-block;
-        padding: 4px 10px;
-        border-radius: 12px;
-        font-size: 12px;
-        font-weight: 500;
-        background: #f5f5f5;
-        color: #666;
-    }
-    
-    .stat-badge.stat-weight {
-        background: #667eea;
-        color: white;
-    }
-    
-    .stat-badge.stat-coverage {
-        background: #e8f0ff;
-        color: #667eea;
-    }
-    
-    .stat-badge.stat-frequency {
-        background: #fce4ec;
-        color: #f093fb;
-    }
-    
-    .stat-badge.stat-intent {
-        background: #e0f7fa;
-        color: #4facfe;
-    }
-    
-    .stat-badge.stat-highlight {
-        font-weight: 700;
-        transform: scale(1.05);
-    }
-    
-    .element-card-description {
-        color: #666;
-        font-size: 14px;
-        line-height: 1.6;
-        margin-bottom: 12px;
-    }
-    
-    .element-detail-btn {
-        width: 100%;
-        padding: 10px;
-        background: #667eea;
-        color: white;
-        border: none;
-        border-radius: 6px;
-        font-size: 14px;
-        font-weight: 500;
-        cursor: pointer;
-        transition: all 0.2s;
-    }
-    
-    .element-detail-btn:hover {
-        background: #5568d3;
-        transform: translateY(-1px);
-        box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
-    }
-    
-    /* 弹窗样式 */
-    .element-modal {
-        display: none;
-        position: fixed;
-        top: 0;
-        left: 0;
-        width: 100%;
-        height: 100%;
-        z-index: 1000;
-        align-items: center;
-        justify-content: center;
-    }
-    
-    .element-modal-overlay {
-        position: absolute;
-        top: 0;
-        left: 0;
-        width: 100%;
-        height: 100%;
-        background: rgba(0, 0, 0, 0.5);
-        backdrop-filter: blur(4px);
-    }
-    
-    .element-modal-dialog {
-        position: relative;
-        width: 800px;
-        height: 90vh;
-        background: white;
-        border-radius: 12px;
-        box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
-        z-index: 1001;
-        display: flex;
-        flex-direction: column;
-        overflow: hidden;
-    }
-    
-    .element-modal-content-wrapper {
-        width: 100%;
-        height: 100%;
-        display: flex;
-        flex-direction: column;
-        overflow: hidden;
-    }
-    
-    .element-modal-content {
-        width: 100%;
-        height: 100%;
-        display: flex;
-        flex-direction: column;
-        padding: 0;
-        overflow: hidden;
-    }
-    
-    .modal-header {
-        width: 100%;
-        display: flex;
-        align-items: center;
-        gap: 12px;
-        padding: 20px 24px;
-        border-bottom: 1px solid rgba(255, 255, 255, 0.2);
-        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-        color: white;
-        border-radius: 12px 12px 0 0;
-        position: sticky;
-        top: 0;
-        z-index: 10;
-        flex-shrink: 0;
-        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-    }
-    
-    .modal-id {
-        font-weight: 600;
-        font-size: 14px;
-        opacity: 0.9;
-    }
-    
-    .modal-title {
-        margin: 0;
-        font-size: 20px;
-        font-weight: 600;
-        flex: 1;
-    }
-    
-    .modal-close {
-        background: rgba(255, 255, 255, 0.2);
-        border: none;
-        color: white;
-        font-size: 24px;
-        width: 32px;
-        height: 32px;
-        border-radius: 50%;
-        cursor: pointer;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        transition: all 0.2s;
-    }
-    
-    .modal-close:hover {
-        background: rgba(255, 255, 255, 0.3);
-        transform: rotate(90deg);
-    }
-    
-    .modal-body {
-        width: 100%;
-        flex: 1;
-        padding: 24px;
-        overflow-y: auto;
-        overflow-x: hidden;
-        min-height: 0;
-    }
-    
-    .detail-section {
-        margin-bottom: 20px;
-    }
-    
-    .detail-section strong {
-        display: block;
-        color: #667eea;
-        font-size: 14px;
-        font-weight: 600;
-        margin-bottom: 8px;
-    }
-    
-    .detail-text {
-        color: #555;
-        line-height: 1.6;
-        padding: 12px;
-        background: #f8f9fa;
-        border-radius: 6px;
-        border-left: 3px solid #667eea;
-    }
-    
-    .detail-content {
-        display: flex;
-        flex-wrap: wrap;
-        gap: 8px;
-        margin-top: 8px;
-    }
-    
-    .detail-tag {
-        display: inline-block;
-        padding: 6px 12px;
-        background: #e8f0ff;
-        color: #667eea;
-        border-radius: 16px;
-        font-size: 13px;
-        font-weight: 500;
-    }
-    
-    .detail-tag.category-level1 {
-        width: 100%;
-        background: #667eea;
-        color: white;
-        font-weight: 600;
-        margin-top: 8px;
-    }
-    
-    .detail-tag.source-tag {
-        background: #fce4ec;
-        color: #f093fb;
-    }
-    
-    .detail-tag.para-id-tag {
-        background: #e0f7fa;
-        color: #4facfe;
-    }
-    
-    .weight-chip {
-        display: inline-block;
-        padding: 6px 12px;
-        background: #f5f5f5;
-        color: #333;
-        border-radius: 16px;
-        font-size: 12px;
-        margin: 4px;
-    }
-    
-    .weight-summary-grid {
-        display: flex;
-        flex-wrap: wrap;
-        gap: 8px;
-        margin-top: 12px;
-    }
-    
-    .weight-summary-header {
-        font-size: 13px;
-        color: #666;
-        margin-top: 8px;
-    }
-    
-    .weight-detail-section {
-        margin-top: 20px;
-        padding-top: 20px;
-        border-top: 1px dashed #e0e0e0;
-    }
-    
-    .score-type {
-        font-weight: 600;
-        color: #667eea;
-        margin-top: 16px;
-        margin-bottom: 8px;
-        font-size: 15px;
-    }
-    
-    .score-list {
-        margin-left: 16px;
-    }
-    
-    .score-item {
-        margin-bottom: 12px;
-        padding: 12px;
-        background: #f8f9fa;
-        border-radius: 6px;
-        border-left: 3px solid #667eea;
-    }
-    
-    .score-point {
-        font-weight: 600;
-        color: #333;
-        margin-bottom: 8px;
-    }
-    
-    .point-intention, .score-reasons {
-        margin-top: 8px;
-        font-size: 13px;
-        color: #666;
-    }
-    
-    .score-reason {
-        margin-top: 4px;
-        padding: 8px;
-        background: white;
-        border-radius: 4px;
-    }
-    
-    .context-verification {
-        margin-top: 8px;
-        padding: 12px;
-        background: #f8f9fa;
-        border-radius: 4px;
-        border-left: 3px solid #6c757d;
-    }
-    
-    .context-item {
-        margin-bottom: 8px;
-    }
-    
-    .context-text {
-        margin-top: 4px;
-        padding: 6px 10px;
-        background: white;
-        border-radius: 3px;
-        color: #212529;
-        font-style: italic;
-    }
-    
-    .grammar-tag {
-        margin-left: 8px;
-        padding: 3px 10px;
-        background: #e7f3ff;
-        color: #0066cc;
-        border-radius: 3px;
-        font-size: 12px;
-    }
-    
-    .paragraphs-detail-list {
-        display: flex;
-        flex-direction: column;
-        gap: 8px;
-        margin-top: 8px;
-    }
-    
-    .paragraph-detail-item {
-        display: flex;
-        align-items: flex-start;
-        gap: 8px;
-    }
-    
-    .para-how {
-        flex: 1;
-        padding: 8px;
-        background: white;
-        border-radius: 4px;
-        color: #555;
-        font-size: 13px;
-    }
-    
-    @media (max-width: 768px) {
-        .element-list {
-            grid-template-columns: 1fr;
-        }
-        
-        .element-modal-dialog {
-            width: 95%;
-            height: 95vh;
-        }
-        
-        .modal-body {
-            padding: 16px;
-        }
-    }
-    '''
-    html += '</style>\n'
-
-    # 添加JavaScript代码
-    html += '<script>\n'
-    html += '''
-    function openElementModal(elemId) {
-        const modal = document.getElementById('elementModal');
-        const contentWrapper = document.getElementById('elementModalContent');
-        const templates = document.getElementById('elementModalTemplates');
-        
-        // 查找预生成的弹窗内容
-        const modalContent = templates.querySelector(`.element-modal-content[data-elem-id="${elemId}"]`);
-        if (modalContent) {
-            contentWrapper.innerHTML = modalContent.outerHTML;
-            modal.style.display = 'flex';
-            document.body.style.overflow = 'hidden';
-        } else {
-            console.error('Modal content not found for element:', elemId);
-        }
-    }
-    
-    function closeElementModal() {
-        const modal = document.getElementById('elementModal');
-        modal.style.display = 'none';
-        document.body.style.overflow = '';
-    }
-    
-    // ESC键关闭弹窗
-    document.addEventListener('keydown', function(e) {
-        if (e.key === 'Escape') {
-            closeElementModal();
-        }
-    });
-    '''
-    html += '</script>\n'
-
-    html += '</div>\n'
-    return html

+ 0 - 956
examples/static/visualize/tab5.py

@@ -1,956 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tab5内容生成器 - 实质与形式的双向支撑关系图
-展示:选题点(来自实质) ← 实质点 → 形式点 → 选题点(来自形式)
-"""
-import html as html_module
-import json
-from typing import Dict, Any, List
-
-
-def get_intent_support_data(element: Dict[str, Any]) -> Dict[str, Any]:
-    """
-    获取元素的意图支撑数据(兼容新旧数据结构)
-    
-    优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
-    这两个字段内部存储的都是意图支撑数据
-    
-    Args:
-        element: 元素数据
-        
-    Returns:
-        意图支撑数据字典,格式:{"灵感点": [...], "目的点": [...], "关键点": [...]}
-    """
-    # 优先使用"意图支撑"字段
-    intent_support = element.get('意图支撑')
-    if intent_support and isinstance(intent_support, dict):
-        return intent_support
-    
-    # 如果没有"意图支撑",则使用"多维度评分"字段(兼容旧数据)
-    multi_scores = element.get('多维度评分')
-    if multi_scores and isinstance(multi_scores, dict):
-        return multi_scores
-    
-    # 都没有则返回空字典
-    return {}
-
-
-def generate_tab5_content(data: Dict[str, Any]) -> str:
-    """生成Tab5内容:实质与形式的双向支撑关系图(4列布局)"""
-    html = '<div class="tab-content" id="tab5">\n'
-
-    # 提取数据
-    script_data = data.get('脚本理解', {})
-    form_list = script_data.get('形式列表', [])
-    substance_list = script_data.get('实质列表', [])
-    
-    # 处理灵感点:可能是列表,也可能在对象中
-    inspiration_data = data.get('灵感点', [])
-    if isinstance(inspiration_data, list):
-        inspiration_points = inspiration_data
-    elif isinstance(inspiration_data, dict):
-        inspiration_points = inspiration_data.get('inspiration_points', [])
-    else:
-        inspiration_points = []
-    
-    # 处理目的点:可能是列表,也可能在对象的purposes字段中
-    purpose_data = data.get('目的点', [])
-    if isinstance(purpose_data, list):
-        purpose_points = purpose_data
-    elif isinstance(purpose_data, dict):
-        purpose_points = purpose_data.get('purposes', [])
-    else:
-        purpose_points = []
-    
-    # 处理关键点:可能是列表,也可能在对象的key_points字段中
-    keypoint_data = data.get('关键点', [])
-    if isinstance(keypoint_data, list):
-        key_points = keypoint_data
-    elif isinstance(keypoint_data, dict):
-        key_points = keypoint_data.get('key_points', [])
-    else:
-        key_points = []
-
-    if not substance_list and not form_list:
-        html += '<div class="empty-state">暂无实质点和形式点数据</div>\n'
-        html += '</div>\n'
-        return html
-
-    # 分类实质点
-    concrete_elements = []
-    concrete_concepts = []
-    implicit_concepts = []
-    abstract_concepts = []
-
-    for substance in substance_list:
-        dimension_2 = substance.get('维度', {}).get('二级', '')
-        elem_type = substance.get('类型', '')
-        
-        # 隐含概念:优先通过类型判断(因为维度二级可能是"隐含概念"或"抽象概念")
-        if elem_type == '隐含概念':
-            implicit_concepts.append(substance)
-        elif dimension_2 == '具体元素':
-            concrete_elements.append(substance)
-        elif dimension_2 == '具象概念':  # 修改:从"具体概念"改为"具象概念"
-            concrete_concepts.append(substance)
-        elif dimension_2 == '抽象概念':
-            abstract_concepts.append(substance)
-
-    # 分类形式点
-    concrete_element_forms = []
-    concrete_concept_forms = []
-    overall_forms = []
-
-    for form in form_list:
-        dimension_2 = form.get('维度', {}).get('二级', '')
-        if dimension_2 == '具体元素形式':
-            concrete_element_forms.append(form)
-        elif dimension_2 == '具象概念形式':  # 修改:从"具体概念形式"改为"具象概念形式"
-            concrete_concept_forms.append(form)
-        elif dimension_2 == '整体形式':
-            overall_forms.append(form)
-
-    # 构建关系数据
-    relationships = build_bidirectional_relationships(
-        concrete_elements, concrete_concepts, implicit_concepts, abstract_concepts,
-        concrete_element_forms, concrete_concept_forms, overall_forms,
-        inspiration_points, purpose_points, key_points
-    )
-
-    # 添加标题和说明
-    html += '<div class="tab5-header">\n'
-    html += '<h2 class="tab5-title">实质与形式的双向支撑关系图</h2>\n'
-    html += '<div class="tab5-description">\n'
-    html += '<p>4列展示:<strong>选题点(实质支撑)</strong> ← <strong>实质点</strong> | <strong>形式点</strong> → <strong>选题点(形式支撑)</strong></p>\n'
-    html += '<ul class="relationship-rules">\n'
-    html += '<li>左侧选题点:显示实质点对灵感点、关键点、目的点的支撑关系</li>\n'
-    html += '<li>右侧选题点:显示形式点对灵感点、关键点、目的点的支撑关系</li>\n'
-    html += '</ul>\n'
-    html += '</div>\n'
-    html += '</div>\n'
-
-    # SVG 连线容器(放在4列布局之前,作为背景层)
-    html += '<div class="tab5-svg-container">\n'
-    html += '<svg id="tab5-connection-svg" width="100%" height="100%">\n'
-    html += '<!-- 连线将在这里绘制 -->\n'
-    html += '</svg>\n'
-    html += '</div>\n'
-
-    # 主要内容区域(4列布局)
-    html += '<div class="tab5-four-column-container">\n'
-
-    # 第1列:左侧选题点(来自实质点的支撑关系)
-    html += '<div class="tab5-column tab5-left-targets">\n'
-    html += '<h3 class="panel-title">选题点<br/><span style="font-size:0.8em;font-weight:normal;color:#6c757d;">(实质支撑)</span></h3>\n'
-
-    # 灵感点(左)
-    if inspiration_points:
-        html += '<div class="target-group">\n'
-        html += '<h4 class="group-title">灵感点</h4>\n'
-        html += '<div class="target-items">\n'
-        for idx, point in enumerate(inspiration_points, 1):
-            # 确保 point 是字典
-            if not isinstance(point, dict):
-                continue
-            # 使用提取的特征中的特征名称,每个特征名称一个卡片
-            features = point.get('提取的特征', [])
-            feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-            if feature_names:
-                # 为每个特征名称创建一个独立ID的卡片
-                for feature_idx, feature_name in enumerate(feature_names, 1):
-                    feature_id = f'inspiration-{idx}-{feature_idx}'
-                    html += f'<div class="target-card inspiration-card" data-type="inspiration-substance" data-id="{feature_id}" onclick="selectLeftTarget(' + f"'inspiration', '{idx}-{feature_idx}'" + ')">\n'
-                    html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
-                    html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
-                    html += '</div>\n'
-            else:
-                # 如果没有特征,使用原始的灵感点文本
-                display_text = point.get('灵感点', '')
-                html += f'<div class="target-card inspiration-card" data-type="inspiration-substance" data-id="inspiration-{idx}" onclick="selectLeftTarget(' + f"'inspiration', {idx}" + ')">\n'
-                html += f'<div class="card-number">#{idx}</div>\n'
-                html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
-                html += '</div>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 关键点(左)
-    if key_points:
-        html += '<div class="target-group">\n'
-        html += '<h4 class="group-title">关键点</h4>\n'
-        html += '<div class="target-items">\n'
-        for idx, point in enumerate(key_points, 1):
-            # 确保 point 是字典
-            if not isinstance(point, dict):
-                continue
-            # 使用提取的特征中的特征名称,每个特征名称一个卡片
-            features = point.get('提取的特征', [])
-            feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-            if feature_names:
-                # 为每个特征名称创建一个独立ID的卡片
-                for feature_idx, feature_name in enumerate(feature_names, 1):
-                    feature_id = f'keypoint-{idx}-{feature_idx}'
-                    html += f'<div class="target-card keypoint-card" data-type="keypoint-substance" data-id="{feature_id}" onclick="selectLeftTarget(' + f"'keypoint', '{idx}-{feature_idx}'" + ')">\n'
-                    html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
-                    html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
-                    html += '</div>\n'
-            else:
-                # 如果没有特征,使用原始的关键点文本
-                display_text = point.get('关键点', '')
-                html += f'<div class="target-card keypoint-card" data-type="keypoint-substance" data-id="keypoint-{idx}" onclick="selectLeftTarget(' + f"'keypoint', {idx}" + ')">\n'
-                html += f'<div class="card-number">#{idx}</div>\n'
-                html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
-                html += '</div>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 目的点(左)
-    if purpose_points:
-        html += '<div class="target-group">\n'
-        html += '<h4 class="group-title">目的点</h4>\n'
-        html += '<div class="target-items">\n'
-        for idx, point in enumerate(purpose_points, 1):
-            # 确保 point 是字典
-            if not isinstance(point, dict):
-                continue
-            # 使用提取的特征中的特征名称,每个特征名称一个卡片
-            features = point.get('提取的特征', [])
-            feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-            if feature_names:
-                # 为每个特征名称创建一个独立ID的卡片
-                for feature_idx, feature_name in enumerate(feature_names, 1):
-                    feature_id = f'purpose-{idx}-{feature_idx}'
-                    html += f'<div class="target-card purpose-card" data-type="purpose-substance" data-id="{feature_id}" onclick="selectLeftTarget(' + f"'purpose', '{idx}-{feature_idx}'" + ')">\n'
-                    html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
-                    html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
-                    html += '</div>\n'
-            else:
-                # 如果没有特征,使用原始的目的点文本
-                display_text = point.get('目的点', '')
-                html += f'<div class="target-card purpose-card" data-type="purpose-substance" data-id="purpose-{idx}" onclick="selectLeftTarget(' + f"'purpose', {idx}" + ')">\n'
-                html += f'<div class="card-number">#{idx}</div>\n'
-                html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
-                html += '</div>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-
-    html += '</div>\n'
-
-    # 第2列:实质点
-    html += '<div class="tab5-column tab5-substances">\n'
-    html += '<h3 class="panel-title">实质点</h3>\n'
-
-    # 具体元素
-    if concrete_elements:
-        html += '<div class="substance-group">\n'
-        html += '<h4 class="group-title">具体元素</h4>\n'
-        html += '<div class="substance-items">\n'
-        for substance in concrete_elements:
-            html += render_substance_card(substance, 'concrete-element')
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 具象概念
-    if concrete_concepts:
-        html += '<div class="substance-group">\n'
-        html += '<h4 class="group-title">具象概念</h4>\n'
-        html += '<div class="substance-items">\n'
-        for substance in concrete_concepts:
-            html += render_substance_card(substance, 'concrete-concept')
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 隐含概念
-    if implicit_concepts:
-        html += '<div class="substance-group">\n'
-        html += '<h4 class="group-title">隐含概念</h4>\n'
-        html += '<div class="substance-items">\n'
-        for substance in implicit_concepts:
-            html += render_substance_card(substance, 'implicit-concept')
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 抽象概念
-    if abstract_concepts:
-        html += '<div class="substance-group">\n'
-        html += '<h4 class="group-title">抽象概念</h4>\n'
-        html += '<div class="substance-items">\n'
-        for substance in abstract_concepts:
-            html += render_substance_card(substance, 'abstract-concept')
-        html += '</div>\n'
-        html += '</div>\n'
-
-    html += '</div>\n'
-
-    # 第3列:形式点
-    html += '<div class="tab5-column tab5-forms">\n'
-    html += '<h3 class="panel-title">形式点</h3>\n'
-
-    # 具体元素形式
-    if concrete_element_forms:
-        html += '<div class="form-group">\n'
-        html += '<h4 class="group-title">具体元素形式</h4>\n'
-        html += '<div class="form-items">\n'
-        for form in concrete_element_forms:
-            html += render_form_card(form, 'concrete-element-form')
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 具象概念形式
-    if concrete_concept_forms:
-        html += '<div class="form-group">\n'
-        html += '<h4 class="group-title">具象概念形式</h4>\n'
-        html += '<div class="form-items">\n'
-        for form in concrete_concept_forms:
-            html += render_form_card(form, 'concrete-concept-form')
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 整体形式
-    if overall_forms:
-        html += '<div class="form-group">\n'
-        html += '<h4 class="group-title">整体形式</h4>\n'
-        html += '<div class="form-items">\n'
-        for form in overall_forms:
-            html += render_form_card(form, 'overall-form')
-        html += '</div>\n'
-        html += '</div>\n'
-
-    html += '</div>\n'
-
-    # 第4列:右侧选题点(来自形式点的支撑关系)
-    html += '<div class="tab5-column tab5-right-targets">\n'
-    html += '<h3 class="panel-title">选题点<br/><span style="font-size:0.8em;font-weight:normal;color:#6c757d;">(形式支撑)</span></h3>\n'
-
-    # 灵感点(右)
-    if inspiration_points:
-        html += '<div class="target-group">\n'
-        html += '<h4 class="group-title">灵感点</h4>\n'
-        html += '<div class="target-items">\n'
-        for idx, point in enumerate(inspiration_points, 1):
-            # 确保 point 是字典
-            if not isinstance(point, dict):
-                continue
-            # 使用提取的特征中的特征名称,每个特征名称一个卡片
-            features = point.get('提取的特征', [])
-            feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-            if feature_names:
-                # 为每个特征名称创建一个独立ID的卡片
-                for feature_idx, feature_name in enumerate(feature_names, 1):
-                    feature_id = f'inspiration-{idx}-{feature_idx}'
-                    html += f'<div class="target-card inspiration-card" data-type="inspiration-form" data-id="{feature_id}" onclick="selectRightTarget(' + f"'inspiration', '{idx}-{feature_idx}'" + ')">\n'
-                    html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
-                    html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
-                    html += '</div>\n'
-            else:
-                # 如果没有特征,使用原始的灵感点文本
-                display_text = point.get('灵感点', '')
-                html += f'<div class="target-card inspiration-card" data-type="inspiration-form" data-id="inspiration-{idx}" onclick="selectRightTarget(' + f"'inspiration', {idx}" + ')">\n'
-                html += f'<div class="card-number">#{idx}</div>\n'
-                html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
-                html += '</div>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 关键点(右)
-    if key_points:
-        html += '<div class="target-group">\n'
-        html += '<h4 class="group-title">关键点</h4>\n'
-        html += '<div class="target-items">\n'
-        for idx, point in enumerate(key_points, 1):
-            # 确保 point 是字典
-            if not isinstance(point, dict):
-                continue
-            # 使用提取的特征中的特征名称,每个特征名称一个卡片
-            features = point.get('提取的特征', [])
-            feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-            if feature_names:
-                # 为每个特征名称创建一个独立ID的卡片
-                for feature_idx, feature_name in enumerate(feature_names, 1):
-                    feature_id = f'keypoint-{idx}-{feature_idx}'
-                    html += f'<div class="target-card keypoint-card" data-type="keypoint-form" data-id="{feature_id}" onclick="selectRightTarget(' + f"'keypoint', '{idx}-{feature_idx}'" + ')">\n'
-                    html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
-                    html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
-                    html += '</div>\n'
-            else:
-                # 如果没有特征,使用原始的关键点文本
-                display_text = point.get('关键点', '')
-                html += f'<div class="target-card keypoint-card" data-type="keypoint-form" data-id="keypoint-{idx}" onclick="selectRightTarget(' + f"'keypoint', {idx}" + ')">\n'
-                html += f'<div class="card-number">#{idx}</div>\n'
-                html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
-                html += '</div>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-
-    # 目的点(右)
-    if purpose_points:
-        html += '<div class="target-group">\n'
-        html += '<h4 class="group-title">目的点</h4>\n'
-        html += '<div class="target-items">\n'
-        for idx, point in enumerate(purpose_points, 1):
-            # 确保 point 是字典
-            if not isinstance(point, dict):
-                continue
-            # 使用提取的特征中的特征名称,每个特征名称一个卡片
-            features = point.get('提取的特征', [])
-            feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-            if feature_names:
-                # 为每个特征名称创建一个独立ID的卡片
-                for feature_idx, feature_name in enumerate(feature_names, 1):
-                    feature_id = f'purpose-{idx}-{feature_idx}'
-                    html += f'<div class="target-card purpose-card" data-type="purpose-form" data-id="{feature_id}" onclick="selectRightTarget(' + f"'purpose', '{idx}-{feature_idx}'" + ')">\n'
-                    html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
-                    html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
-                    html += '</div>\n'
-            else:
-                # 如果没有特征,使用原始的目的点文本
-                display_text = point.get('目的点', '')
-                html += f'<div class="target-card purpose-card" data-type="purpose-form" data-id="purpose-{idx}" onclick="selectRightTarget(' + f"'purpose', {idx}" + ')">\n'
-                html += f'<div class="card-number">#{idx}</div>\n'
-                html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
-                html += '</div>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-
-    html += '</div>\n'
-    html += '</div>\n'
-
-    # 嵌入关系数据
-    html += '<script>\n'
-    html += f'const tab5Relationships = {json.dumps(relationships, ensure_ascii=False)};\n'
-    html += '</script>\n'
-
-    html += '</div>\n'
-    return html
-
-
-def render_substance_card(substance: Dict[str, Any], css_class: str) -> str:
-    """渲染实质点卡片"""
-    substance_id = substance.get('id', '')
-    substance_name = substance.get('名称', '')
-    description = substance.get('描述', '')
-
-    html = f'<div class="substance-card {css_class}" data-id="{html_module.escape(substance_id)}" onclick="selectSubstance(' + f"'{substance_id}'" + ')">\n'
-    html += f'<div class="card-header">\n'
-    html += f'<div class="card-id">#{html_module.escape(substance_id)}</div>\n'
-    html += f'<div class="card-name">{html_module.escape(substance_name)}</div>\n'
-    html += '</div>\n'
-    if description:
-        html += f'<div class="card-description">{html_module.escape(description[:50])}{"..." if len(description) > 50 else ""}</div>\n'
-    html += '</div>\n'
-    return html
-
-
-def render_form_card(form: Dict[str, Any], css_class: str) -> str:
-    """渲染形式点卡片"""
-    form_id = form.get('id', '')
-    form_name = form.get('名称', '')
-    description = form.get('描述', '')
-    weight_score = form.get('权重分')
-
-    html = f'<div class="form-card {css_class}" data-id="{html_module.escape(form_id)}" onclick="selectForm(' + f"'{form_id}'" + ')">\n'
-    html += f'<div class="card-header">\n'
-    html += f'<div class="card-id">#{html_module.escape(form_id)}</div>\n'
-    html += f'<div class="card-name">{html_module.escape(form_name)}</div>\n'
-    if weight_score is not None:
-        html += f'<div class="card-weight" style="font-size: 11px; color: #666; margin-top: 2px;">权重分: {weight_score:.1f}</div>\n'
-    html += '</div>\n'
-    if description:
-        html += f'<div class="card-description">{html_module.escape(description[:50])}{"..." if len(description) > 50 else ""}</div>\n'
-    html += '</div>\n'
-    return html
-
-
-def build_bidirectional_relationships(
-    concrete_elements: List[Dict],
-    concrete_concepts: List[Dict],
-    implicit_concepts: List[Dict],
-    abstract_concepts: List[Dict],
-    concrete_element_forms: List[Dict],
-    concrete_concept_forms: List[Dict],
-    overall_forms: List[Dict],
-    inspiration_points: List[Dict],
-    purpose_points: List[Dict],
-    key_points: List[Dict]
-) -> Dict[str, Any]:
-    """
-    构建双向支撑关系数据
-
-    返回结构:
-    {
-        "substance_to_target": {
-            "substance_id": {
-                "inspiration": [{target_id, score, ...}],
-                "purpose": [...],
-                "keypoint": [...]
-            }
-        },
-        "form_to_target": {
-            "form_id": {
-                "inspiration": [{target_id, score, ...}],
-                "purpose": [...],
-                "keypoint": [...]
-            }
-        },
-        "target_from_substance": {
-            "inspiration-1": [substance_ids with scores],
-            "keypoint-1": [...],
-            "purpose-1": [...]
-        },
-        "target_from_form": {
-            "inspiration-1": [form_ids with scores],
-            ...
-        },
-        "form_to_substance": {
-            "form_id": [substance_ids],
-            ...
-        },
-        "substance_from_form": {
-            "substance_id": [form_ids],
-            ...
-        }
-    }
-    """
-    relationships = {
-        "substance_to_target": {},
-        "form_to_target": {},
-        "target_from_substance": {},
-        "target_from_form": {},
-        "form_to_substance": {},  # 新增:形式点→实质点
-        "substance_from_form": {}  # 新增:实质点←形式点(反向)
-    }
-
-    # 合并所有实质点和形式点
-    all_substances = concrete_elements + concrete_concepts + implicit_concepts + abstract_concepts
-    all_forms = concrete_element_forms + concrete_concept_forms + overall_forms
-
-    # 注意:优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段(兼容旧数据)
-    # 这两个字段内部存储的都是意图支撑数据,每个项目就是一个支撑点
-    # 不再需要阈值过滤,所有支撑点都会显示连线
-
-    # 1. 构建实质点到选题点的关系(基于意图支撑数据)
-    for substance in all_substances:
-        substance_id = substance.get('id', '')
-        if not substance_id:
-            continue
-
-        dimension_2 = substance.get('维度', {}).get('二级', '')
-        intention_support = get_intent_support_data(substance)
-
-        relationships["substance_to_target"][substance_id] = {
-            "name": substance.get('名称', ''),
-            "type": dimension_2,
-            "inspiration": [],
-            "purpose": [],
-            "keypoint": []
-        }
-
-        # 处理灵感点(意图支撑)
-        for support_item in intention_support.get('灵感点', []):
-            point_name = support_item.get('点')
-            if not point_name:
-                continue
-
-            # 在灵感点列表中查找匹配的点,以便复用Tab1中的卡片id/特征结构
-            idx = next(
-                (i for i, p in enumerate(inspiration_points, 1)
-                 if isinstance(p, dict) and p.get('灵感点') == point_name),
-                None
-            )
-            if idx is None:
-                continue
-
-            point = inspiration_points[idx - 1]
-            features = point.get('提取的特征', [])
-            feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-            if feature_names:
-                # 为每个特征创建独立的关系
-                for feature_idx in range(1, len(feature_names) + 1):
-                    target_id = f'inspiration-{idx}-{feature_idx}'
-                    relationships["substance_to_target"][substance_id]["inspiration"].append({
-                        "target_id": target_id,
-                        "point": point_name,
-                        "support_reason": support_item.get('支撑理由', '')
-                    })
-
-                    # 反向关系
-                    if target_id not in relationships["target_from_substance"]:
-                        relationships["target_from_substance"][target_id] = []
-                    relationships["target_from_substance"][target_id].append({
-                        "substance_id": substance_id,
-                        "name": substance.get('名称', ''),
-                        "type": dimension_2,
-                        "support_reason": support_item.get('支撑理由', '')
-                    })
-            else:
-                # 没有特征,使用主索引
-                target_id = f'inspiration-{idx}'
-                relationships["substance_to_target"][substance_id]["inspiration"].append({
-                    "target_id": target_id,
-                    "point": point_name,
-                    "support_reason": support_item.get('支撑理由', '')
-                })
-
-                # 反向关系
-                if target_id not in relationships["target_from_substance"]:
-                    relationships["target_from_substance"][target_id] = []
-                relationships["target_from_substance"][target_id].append({
-                    "substance_id": substance_id,
-                    "name": substance.get('名称', ''),
-                    "type": dimension_2,
-                    "support_reason": support_item.get('支撑理由', '')
-                })
-
-        # 处理目的点(意图支撑)
-        for support_item in intention_support.get('目的点', []):
-            point_name = support_item.get('点')
-            if not point_name:
-                continue
-
-            idx = next(
-                (i for i, p in enumerate(purpose_points, 1)
-                 if isinstance(p, dict) and p.get('目的点') == point_name),
-                None
-            )
-            if idx is None:
-                continue
-
-            point = purpose_points[idx - 1]
-            features = point.get('提取的特征', [])
-            feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-            if feature_names:
-                for feature_idx in range(1, len(feature_names) + 1):
-                    target_id = f'purpose-{idx}-{feature_idx}'
-                    relationships["substance_to_target"][substance_id]["purpose"].append({
-                        "target_id": target_id,
-                        "point": point_name,
-                        "support_reason": support_item.get('支撑理由', '')
-                    })
-
-                    if target_id not in relationships["target_from_substance"]:
-                        relationships["target_from_substance"][target_id] = []
-                    relationships["target_from_substance"][target_id].append({
-                        "substance_id": substance_id,
-                        "name": substance.get('名称', ''),
-                        "type": dimension_2,
-                        "support_reason": support_item.get('支撑理由', '')
-                    })
-            else:
-                target_id = f'purpose-{idx}'
-                relationships["substance_to_target"][substance_id]["purpose"].append({
-                    "target_id": target_id,
-                    "point": point_name,
-                    "support_reason": support_item.get('支撑理由', '')
-                })
-
-                if target_id not in relationships["target_from_substance"]:
-                    relationships["target_from_substance"][target_id] = []
-                relationships["target_from_substance"][target_id].append({
-                    "substance_id": substance_id,
-                    "name": substance.get('名称', ''),
-                    "type": dimension_2,
-                    "support_reason": support_item.get('支撑理由', '')
-                })
-
-        # 处理关键点(意图支撑)
-        for support_item in intention_support.get('关键点', []):
-            point_name = support_item.get('点')
-            if not point_name:
-                continue
-
-            idx = next(
-                (i for i, p in enumerate(key_points, 1)
-                 if isinstance(p, dict) and p.get('关键点') == point_name),
-                None
-            )
-            if idx is None:
-                continue
-
-            point = key_points[idx - 1]
-            features = point.get('提取的特征', [])
-            feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-            if feature_names:
-                for feature_idx in range(1, len(feature_names) + 1):
-                    target_id = f'keypoint-{idx}-{feature_idx}'
-                    relationships["substance_to_target"][substance_id]["keypoint"].append({
-                        "target_id": target_id,
-                        "point": point_name,
-                        "support_reason": support_item.get('支撑理由', '')
-                    })
-
-                    if target_id not in relationships["target_from_substance"]:
-                        relationships["target_from_substance"][target_id] = []
-                    relationships["target_from_substance"][target_id].append({
-                        "substance_id": substance_id,
-                        "name": substance.get('名称', ''),
-                        "type": dimension_2,
-                        "support_reason": support_item.get('支撑理由', '')
-                    })
-            else:
-                target_id = f'keypoint-{idx}'
-                relationships["substance_to_target"][substance_id]["keypoint"].append({
-                    "target_id": target_id,
-                    "point": point_name,
-                    "support_reason": support_item.get('支撑理由', '')
-                })
-
-                if target_id not in relationships["target_from_substance"]:
-                    relationships["target_from_substance"][target_id] = []
-                relationships["target_from_substance"][target_id].append({
-                    "substance_id": substance_id,
-                    "name": substance.get('名称', ''),
-                    "type": dimension_2,
-                    "support_reason": support_item.get('支撑理由', '')
-                })
-
-    # 2. 构建形式点到选题点的关系
-    for form in all_forms:
-        form_id = form.get('id', '')
-        if not form_id:
-            continue
-
-        dimension_2 = form.get('维度', {}).get('二级', '')
-        # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
-        intent_support_data = get_intent_support_data(form)
-
-        relationships["form_to_target"][form_id] = {
-            "name": form.get('名称', ''),
-            "type": dimension_2,
-            "inspiration": [],
-            "purpose": [],
-            "keypoint": []
-        }
-
-        # 处理灵感点(每个项目就是一个支撑点)
-        if '灵感点' in intent_support_data:
-            for support_point in intent_support_data['灵感点']:
-                if not isinstance(support_point, dict):
-                    continue
-                
-                # 每个项目就是一个支撑点,直接使用
-                point_name = support_point.get('点', '')
-                if not point_name:
-                    continue
-                
-                # 根据点名称找到对应的灵感点索引
-                point_idx = None
-                for idx, point in enumerate(inspiration_points, 1):
-                    if point.get('灵感点') == point_name or point.get('名称') == point_name:
-                        point_idx = idx
-                        break
-                
-                if point_idx and point_idx <= len(inspiration_points):
-                    point = inspiration_points[point_idx - 1]
-                    features = point.get('提取的特征', [])
-                    feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-                    if feature_names:
-                        # 为每个特征创建独立的关系
-                        for feature_idx in range(1, len(feature_names) + 1):
-                            target_id = f'inspiration-{point_idx}-{feature_idx}'
-                            relationships["form_to_target"][form_id]["inspiration"].append({
-                                "target_id": target_id,
-                                "point": point_name,
-                                "support_reason": support_point.get('支撑理由', '')
-                            })
-
-                            # 反向关系
-                            if target_id not in relationships["target_from_form"]:
-                                relationships["target_from_form"][target_id] = []
-                            relationships["target_from_form"][target_id].append({
-                                "form_id": form_id,
-                                "name": form.get('名称', ''),
-                                "type": dimension_2,
-                                "support_reason": support_point.get('支撑理由', '')
-                            })
-                    else:
-                        # 没有特征,使用主索引
-                        target_id = f'inspiration-{point_idx}'
-                        relationships["form_to_target"][form_id]["inspiration"].append({
-                            "target_id": target_id,
-                            "point": point_name,
-                            "support_reason": support_point.get('支撑理由', '')
-                        })
-
-                        # 反向关系
-                        if target_id not in relationships["target_from_form"]:
-                            relationships["target_from_form"][target_id] = []
-                        relationships["target_from_form"][target_id].append({
-                            "form_id": form_id,
-                            "name": form.get('名称', ''),
-                            "type": dimension_2,
-                            "support_reason": support_point.get('支撑理由', '')
-                        })
-
-        # 处理目的点(每个项目就是一个支撑点)
-        if '目的点' in intent_support_data:
-            for support_point in intent_support_data['目的点']:
-                if not isinstance(support_point, dict):
-                    continue
-                
-                # 每个项目就是一个支撑点,直接使用
-                point_name = support_point.get('点', '')
-                if not point_name:
-                    continue
-                
-                # 根据点名称找到对应的目的点索引
-                point_idx = None
-                for idx, point in enumerate(purpose_points, 1):
-                    if point.get('目的点') == point_name or point.get('名称') == point_name:
-                        point_idx = idx
-                        break
-                
-                if point_idx and point_idx <= len(purpose_points):
-                    point = purpose_points[point_idx - 1]
-                    features = point.get('提取的特征', [])
-                    feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-                    if feature_names:
-                        for feature_idx in range(1, len(feature_names) + 1):
-                            target_id = f'purpose-{point_idx}-{feature_idx}'
-                            relationships["form_to_target"][form_id]["purpose"].append({
-                                "target_id": target_id,
-                                "point": point_name,
-                                "support_reason": support_point.get('支撑理由', '')
-                            })
-                            if target_id not in relationships["target_from_form"]:
-                                relationships["target_from_form"][target_id] = []
-                            relationships["target_from_form"][target_id].append({
-                                "form_id": form_id,
-                                "name": form.get('名称', ''),
-                                "type": dimension_2,
-                                "support_reason": support_point.get('支撑理由', '')
-                            })
-                    else:
-                        target_id = f'purpose-{point_idx}'
-                        relationships["form_to_target"][form_id]["purpose"].append({
-                            "target_id": target_id,
-                            "point": point_name,
-                            "support_reason": support_point.get('支撑理由', '')
-                        })
-                        if target_id not in relationships["target_from_form"]:
-                            relationships["target_from_form"][target_id] = []
-                        relationships["target_from_form"][target_id].append({
-                            "form_id": form_id,
-                            "name": form.get('名称', ''),
-                            "type": dimension_2,
-                            "support_reason": support_point.get('支撑理由', '')
-                        })
-
-        # 处理关键点(每个项目就是一个支撑点)
-        if '关键点' in intent_support_data:
-            for support_point in intent_support_data['关键点']:
-                if not isinstance(support_point, dict):
-                    continue
-                
-                # 每个项目就是一个支撑点,直接使用
-                point_name = support_point.get('点', '')
-                if not point_name:
-                    continue
-                
-                # 根据点名称找到对应的关键点索引
-                point_idx = None
-                for idx, point in enumerate(key_points, 1):
-                    if point.get('关键点') == point_name or point.get('名称') == point_name:
-                        point_idx = idx
-                        break
-                
-                if point_idx and point_idx <= len(key_points):
-                    point = key_points[point_idx - 1]
-                    features = point.get('提取的特征', [])
-                    feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
-
-                    if feature_names:
-                        for feature_idx in range(1, len(feature_names) + 1):
-                            target_id = f'keypoint-{point_idx}-{feature_idx}'
-                            relationships["form_to_target"][form_id]["keypoint"].append({
-                                "target_id": target_id,
-                                "point": point_name,
-                                "support_reason": support_point.get('支撑理由', '')
-                            })
-                            if target_id not in relationships["target_from_form"]:
-                                relationships["target_from_form"][target_id] = []
-                            relationships["target_from_form"][target_id].append({
-                                "form_id": form_id,
-                                "name": form.get('名称', ''),
-                                "type": dimension_2,
-                                "support_reason": support_point.get('支撑理由', '')
-                            })
-                    else:
-                        target_id = f'keypoint-{point_idx}'
-                        relationships["form_to_target"][form_id]["keypoint"].append({
-                            "target_id": target_id,
-                            "point": point_name,
-                            "support_reason": support_point.get('支撑理由', '')
-                        })
-                        if target_id not in relationships["target_from_form"]:
-                            relationships["target_from_form"][target_id] = []
-                        relationships["target_from_form"][target_id].append({
-                            "form_id": form_id,
-                            "name": form.get('名称', ''),
-                            "type": dimension_2,
-                            "support_reason": support_point.get('支撑理由', '')
-                        })
-
-    # 3. 构建形式点到实质点的支撑关系
-    for form in all_forms:
-        form_id = form.get('id', '')
-        if not form_id:
-            continue
-
-        dimension_2 = form.get('维度', {}).get('二级', '')
-        support_data = form.get('支撑', [])
-
-        if not support_data:
-            continue
-
-        # 初始化形式点的支撑关系
-        if form_id not in relationships["form_to_substance"]:
-            relationships["form_to_substance"][form_id] = []
-
-        # 支撑字段可能是列表或字典
-        support_items = []
-        if isinstance(support_data, list):
-            # 列表格式:[{id, 名称}, ...]
-            support_items = support_data
-        elif isinstance(support_data, dict):
-            # 字典格式:{'具体元素': [...], '具象概念': [...]}
-            for _category, items in support_data.items():
-                if isinstance(items, list):
-                    support_items.extend(items)
-
-        for support_item in support_items:
-            if not isinstance(support_item, dict):
-                continue
-
-            substance_id = support_item.get('id', '')
-            substance_name = support_item.get('名称', '')
-
-            if not substance_id:
-                continue
-
-            # 形式点 → 实质点
-            relationships["form_to_substance"][form_id].append({
-                "substance_id": substance_id,
-                "name": substance_name
-            })
-
-            # 实质点 ← 形式点(反向)
-            if substance_id not in relationships["substance_from_form"]:
-                relationships["substance_from_form"][substance_id] = []
-
-            relationships["substance_from_form"][substance_id].append({
-                "form_id": form_id,
-                "name": form.get('名称', ''),
-                "type": dimension_2
-            })
-
-    return relationships

+ 0 - 2
examples/static/visualize_v2/__init__.py

@@ -1,2 +0,0 @@
-# V2 可视化模块
-

+ 0 - 176
examples/static/visualize_v2/tab1.py

@@ -1,176 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tab1内容生成器 - 结构化内容库
-包含:选题信息表、模型信息表、层级标签树
-"""
-import html as html_module
-from typing import Dict, Any, List
-
-
-def render_topic_info_card(topic_info: Dict[str, Any]) -> str:
-    """渲染选题信息表卡片"""
-    macro_topic = topic_info.get("宏观母题", "")
-    sub_topics = topic_info.get("高潜子题列表", [])
-    
-    html = '<div class="point-card topic-card" data-card-id="topic-info">\n'
-    html += '<div class="point-card-header" onclick="toggleCardDetails(\'topic-info\')">\n'
-    html += '<span class="point-text">选题信息表</span>\n'
-    html += '<span class="toggle-icon">▼</span>\n'
-    html += '</div>\n'
-    
-    html += '<div class="point-card-details" id="topic-info-details">\n'
-    
-    if macro_topic:
-        html += '<div class="detail-section">\n'
-        html += '<strong>宏观母题:</strong>\n'
-        html += f'<div class="detail-text">{html_module.escape(macro_topic)}</div>\n'
-        html += '</div>\n'
-    
-    if sub_topics:
-        html += '<div class="detail-section">\n'
-        html += '<strong>高潜子题列表:</strong>\n'
-        html += '<ul class="detail-list">\n'
-        for sub_topic in sub_topics:
-            html += f'<li>{html_module.escape(str(sub_topic))}</li>\n'
-        html += '</ul>\n'
-        html += '</div>\n'
-    else:
-        html += '<div class="detail-section">\n'
-        html += '<strong>高潜子题列表:</strong> 暂无\n'
-        html += '</div>\n'
-    
-    html += '</div>\n'
-    html += '</div>\n'
-    return html
-
-
-def render_model_info_card(model_info: Dict[str, Any]) -> str:
-    """渲染模型信息表卡片"""
-    logic_models = model_info.get("抽象逻辑模型列表", [])
-    
-    html = '<div class="point-card model-card" data-card-id="model-info">\n'
-    html += '<div class="point-card-header" onclick="toggleCardDetails(\'model-info\')">\n'
-    html += '<span class="point-text">模型信息表</span>\n'
-    html += '<span class="toggle-icon">▼</span>\n'
-    html += '</div>\n'
-    
-    html += '<div class="point-card-details" id="model-info-details">\n'
-    
-    if logic_models:
-        html += '<div class="detail-section">\n'
-        html += '<strong>抽象逻辑模型列表:</strong>\n'
-        html += '<ul class="detail-list">\n'
-        for model in logic_models:
-            html += f'<li class="model-item">{html_module.escape(str(model))}</li>\n'
-        html += '</ul>\n'
-        html += '</div>\n'
-    else:
-        html += '<div class="detail-section">\n'
-        html += '<strong>抽象逻辑模型列表:</strong> 暂无\n'
-        html += '</div>\n'
-    
-    html += '</div>\n'
-    html += '</div>\n'
-    return html
-
-
-def render_label_tree_card(label_tree: Dict[str, Any]) -> str:
-    """渲染层级标签树卡片"""
-    html = '<div class="point-card label-tree-card" data-card-id="label-tree">\n'
-    html += '<div class="point-card-header" onclick="toggleCardDetails(\'label-tree\')">\n'
-    html += '<span class="point-text">层级标签树</span>\n'
-    html += '<span class="toggle-icon">▼</span>\n'
-    html += '</div>\n'
-    
-    html += '<div class="point-card-details" id="label-tree-details">\n'
-    
-    # L1_实例名词
-    l1_nouns = label_tree.get("L1_实例名词", [])
-    if l1_nouns:
-        html += '<div class="detail-section">\n'
-        html += '<strong>L1_实例名词:</strong>\n'
-        html += '<div class="tag-group">\n'
-        for noun in l1_nouns:
-            html += f'<span class="label-tag l1-tag">{html_module.escape(str(noun))}</span>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-    
-    # L1_限定词
-    l1_qualifiers = label_tree.get("L1_限定词", [])
-    if l1_qualifiers:
-        html += '<div class="detail-section">\n'
-        html += '<strong>L1_限定词:</strong>\n'
-        html += '<div class="tag-group">\n'
-        for qualifier in l1_qualifiers:
-            html += f'<span class="label-tag l1-tag">{html_module.escape(str(qualifier))}</span>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-    
-    # L2_具体品类
-    l2_categories = label_tree.get("L2_具体品类", [])
-    if l2_categories:
-        html += '<div class="detail-section">\n'
-        html += '<strong>L2_具体品类:</strong>\n'
-        html += '<div class="tag-group">\n'
-        for category in l2_categories:
-            html += f'<span class="label-tag l2-tag">{html_module.escape(str(category))}</span>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-    
-    # L3_兴趣领域
-    l3_interests = label_tree.get("L3_兴趣领域", [])
-    if l3_interests:
-        html += '<div class="detail-section">\n'
-        html += '<strong>L3_兴趣领域:</strong>\n'
-        html += '<div class="tag-group">\n'
-        for interest in l3_interests:
-            html += f'<span class="label-tag l3-tag">{html_module.escape(str(interest))}</span>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-    
-    # L4_情绪价值
-    l4_emotions = label_tree.get("L4_情绪价值", [])
-    if l4_emotions:
-        html += '<div class="detail-section">\n'
-        html += '<strong>L4_情绪价值:</strong>\n'
-        html += '<div class="tag-group">\n'
-        for emotion in l4_emotions:
-            html += f'<span class="label-tag l4-tag">{html_module.escape(str(emotion))}</span>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-    
-    html += '</div>\n'
-    html += '</div>\n'
-    return html
-
-
-def generate_tab1_content(data: Dict[str, Any]) -> str:
-    """生成Tab1内容:结构化内容库"""
-    html = '<div class="tab-content" id="tab1">\n'
-    html += '<div class="section">\n'
-    html += '<h3>结构化内容库</h3>\n'
-    
-    structured_content = data.get("结构化内容库", {})
-    
-    if not structured_content:
-        html += '<p>暂无数据</p>\n'
-    else:
-        # 选题信息表
-        topic_info = structured_content.get("选题信息表", {})
-        if topic_info:
-            html += render_topic_info_card(topic_info)
-        
-        # 模型信息表
-        model_info = structured_content.get("模型信息表", {})
-        if model_info:
-            html += render_model_info_card(model_info)
-        
-        # 层级标签树
-        label_tree = structured_content.get("层级标签树", {})
-        if label_tree:
-            html += render_label_tree_card(label_tree)
-    
-    html += '</div>\n'
-    html += '</div>\n'
-    return html
-

+ 0 - 155
examples/static/visualize_v2/tab2.py

@@ -1,155 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tab2内容生成器 - L3单元解构
-包含:单元列表,每个单元包含单元编号、时间范围、单元核心概括、完整文案、实质
-"""
-import html as html_module
-from typing import Dict, Any, List
-
-
-def render_unit_card(unit: Dict[str, Any], idx: int) -> str:
-    """渲染单元卡片"""
-    unit_num = unit.get("单元编号", idx)
-    time_range = unit.get("时间范围", "")
-    core_summary = unit.get("单元核心概括", "")
-    full_text = unit.get("完整文案", "")
-    substance = unit.get("实质", {})
-    
-    card_id = f'unit-{unit_num}'
-    
-    html = f'<div class="point-card unit-card" data-card-id="{card_id}">\n'
-    html += f'<div class="point-card-header" onclick="toggleCardDetails(\'{card_id}\')">\n'
-    html += f'<span class="point-number">单元 #{unit_num}</span>\n'
-    if time_range:
-        html += f'<span class="point-time">{html_module.escape(time_range)}</span>\n'
-    if core_summary:
-        html += f'<span class="point-text">{html_module.escape(core_summary)}</span>\n'
-    html += '<span class="toggle-icon">▼</span>\n'
-    html += '</div>\n'
-    
-    html += f'<div class="point-card-details" id="{card_id}-details">\n'
-    
-    # 完整文案
-    if full_text:
-        html += '<div class="detail-section">\n'
-        html += '<strong>完整文案:</strong>\n'
-        html += f'<div class="detail-text">{html_module.escape(full_text)}</div>\n'
-        html += '</div>\n'
-    
-    # 实质内容
-    if substance:
-        # 具体元素
-        concrete_elements = substance.get("具体元素", {})
-        if concrete_elements:
-            html += '<div class="detail-section">\n'
-            html += '<strong>具体元素:</strong>\n'
-            
-            keywords = concrete_elements.get("关键词", [])
-            if keywords:
-                html += '<div class="sub-section">\n'
-                html += '<strong>关键词:</strong>\n'
-                html += '<div class="tag-group">\n'
-                for keyword in keywords:
-                    html += f'<span class="keyword-tag">{html_module.escape(str(keyword))}</span>\n'
-                html += '</div>\n'
-                html += '</div>\n'
-            
-            forms = concrete_elements.get("对应形式", {})
-            if forms:
-                html += '<div class="sub-section">\n'
-                html += '<strong>对应形式:</strong>\n'
-                if forms.get("文案形式"):
-                    html += f'<div class="form-item"><strong>文案形式:</strong>{html_module.escape(forms["文案形式"])}</div>\n'
-                if forms.get("画面形式"):
-                    html += f'<div class="form-item"><strong>画面形式:</strong>{html_module.escape(forms["画面形式"])}</div>\n'
-                if forms.get("声音形式"):
-                    html += f'<div class="form-item"><strong>声音形式:</strong>{html_module.escape(forms["声音形式"])}</div>\n'
-                html += '</div>\n'
-            
-            html += '</div>\n'
-        
-        # 具象概念
-        concrete_concepts = substance.get("具象概念", {})
-        if concrete_concepts:
-            html += '<div class="detail-section">\n'
-            html += '<strong>具象概念:</strong>\n'
-            
-            keywords = concrete_concepts.get("关键词", [])
-            if keywords:
-                html += '<div class="sub-section">\n'
-                html += '<strong>关键词:</strong>\n'
-                html += '<div class="tag-group">\n'
-                for keyword in keywords:
-                    html += f'<span class="keyword-tag">{html_module.escape(str(keyword))}</span>\n'
-                html += '</div>\n'
-                html += '</div>\n'
-            
-            forms = concrete_concepts.get("对应形式", {})
-            if forms:
-                html += '<div class="sub-section">\n'
-                html += '<strong>对应形式:</strong>\n'
-                if forms.get("文案形式"):
-                    html += f'<div class="form-item"><strong>文案形式:</strong>{html_module.escape(forms["文案形式"])}</div>\n'
-                if forms.get("画面形式"):
-                    html += f'<div class="form-item"><strong>画面形式:</strong>{html_module.escape(forms["画面形式"])}</div>\n'
-                if forms.get("声音形式"):
-                    html += f'<div class="form-item"><strong>声音形式:</strong>{html_module.escape(forms["声音形式"])}</div>\n'
-                html += '</div>\n'
-            
-            html += '</div>\n'
-        
-        # 抽象概念
-        abstract_concepts = substance.get("抽象概念", {})
-        if abstract_concepts:
-            html += '<div class="detail-section">\n'
-            html += '<strong>抽象概念:</strong>\n'
-            
-            keywords = abstract_concepts.get("关键词", [])
-            if keywords:
-                html += '<div class="sub-section">\n'
-                html += '<strong>关键词:</strong>\n'
-                html += '<div class="tag-group">\n'
-                for keyword in keywords:
-                    html += f'<span class="keyword-tag">{html_module.escape(str(keyword))}</span>\n'
-                html += '</div>\n'
-                html += '</div>\n'
-            
-            forms = abstract_concepts.get("对应形式", {})
-            if forms:
-                html += '<div class="sub-section">\n'
-                html += '<strong>对应形式:</strong>\n'
-                if forms.get("文案形式"):
-                    html += f'<div class="form-item"><strong>文案形式:</strong>{html_module.escape(forms["文案形式"])}</div>\n'
-                if forms.get("画面形式"):
-                    html += f'<div class="form-item"><strong>画面形式:</strong>{html_module.escape(forms["画面形式"])}</div>\n'
-                if forms.get("声音形式"):
-                    html += f'<div class="form-item"><strong>声音形式:</strong>{html_module.escape(forms["声音形式"])}</div>\n'
-                html += '</div>\n'
-            
-            html += '</div>\n'
-    
-    html += '</div>\n'
-    html += '</div>\n'
-    return html
-
-
-def generate_tab2_content(data: Dict[str, Any]) -> str:
-    """生成Tab2内容:L3单元解构"""
-    html = '<div class="tab-content" id="tab2" style="display: none;">\n'
-    html += '<div class="section">\n'
-    html += '<h3>L3单元解构</h3>\n'
-    
-    l3_deconstruction = data.get("L3单元解构", {})
-    unit_list = l3_deconstruction.get("单元列表", [])
-    
-    if not unit_list:
-        html += '<p>暂无数据</p>\n'
-    else:
-        html += f'<div class="section-info">共 {len(unit_list)} 个单元</div>\n'
-        for idx, unit in enumerate(unit_list, start=1):
-            html += render_unit_card(unit, idx)
-    
-    html += '</div>\n'
-    html += '</div>\n'
-    return html
-

+ 0 - 196
examples/static/visualize_v2/tab3.py

@@ -1,196 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tab3内容生成器 - 整体结构理解
-包含:整体解构(节点基础信息、整体实质×形式、纵向逻辑流)、段落解构
-"""
-import html as html_module
-from typing import Dict, Any, List
-
-
-def render_overall_deconstruction_card(overall: Dict[str, Any]) -> str:
-    """渲染整体解构卡片"""
-    card_id = "overall-deconstruction"
-    
-    html = f'<div class="point-card overall-card" data-card-id="{card_id}">\n'
-    html += f'<div class="point-card-header" onclick="toggleCardDetails(\'{card_id}\')">\n'
-    html += '<span class="point-text">整体解构</span>\n'
-    html += '<span class="toggle-icon">▼</span>\n'
-    html += '</div>\n'
-    
-    html += f'<div class="point-card-details" id="{card_id}-details">\n'
-    
-    # 节点基础信息
-    basic_info = overall.get("节点基础信息", "")
-    if basic_info:
-        html += '<div class="detail-section">\n'
-        html += '<strong>节点基础信息:</strong>\n'
-        html += f'<div class="detail-text">{html_module.escape(basic_info)}</div>\n'
-        html += '</div>\n'
-    
-    # 整体实质×形式
-    substance_form = overall.get("整体实质×形式", {})
-    if substance_form:
-        html += '<div class="detail-section">\n'
-        html += '<strong>整体实质×形式:</strong>\n'
-        
-        # 处理可能是字符串或字典的情况
-        if isinstance(substance_form, dict):
-            if substance_form.get("抽象概念"):
-                html += f'<div class="form-item"><strong>抽象概念:</strong>{html_module.escape(substance_form["抽象概念"])}</div>\n'
-            if substance_form.get("画面形式"):
-                html += f'<div class="form-item"><strong>画面形式:</strong>{html_module.escape(substance_form["画面形式"])}</div>\n'
-            if substance_form.get("文案形式"):
-                html += f'<div class="form-item"><strong>文案形式:</strong>{html_module.escape(substance_form["文案形式"])}</div>\n'
-            if substance_form.get("声音形式"):
-                html += f'<div class="form-item"><strong>声音形式:</strong>{html_module.escape(substance_form["声音形式"])}</div>\n'
-        elif isinstance(substance_form, str):
-            html += f'<div class="form-item">{html_module.escape(substance_form)}</div>\n'
-        
-        html += '</div>\n'
-    
-    # 纵向逻辑流
-    logic_flow = overall.get("纵向逻辑流", [])
-    if logic_flow:
-        html += '<div class="detail-section">\n'
-        html += '<strong>纵向逻辑流:</strong>\n'
-        html += '<div class="logic-flow">\n'
-        for stage in logic_flow:
-            stage_num = stage.get("阶段编号", "")
-            stage_name = stage.get("阶段逻辑名称", "")
-            stage_desc = stage.get("阶段逻辑描述", "")
-            
-            html += '<div class="logic-stage">\n'
-            if stage_num:
-                html += f'<div class="stage-number">阶段 {stage_num}</div>\n'
-            if stage_name:
-                html += f'<div class="stage-name">{html_module.escape(stage_name)}</div>\n'
-            if stage_desc:
-                html += f'<div class="stage-desc">{html_module.escape(stage_desc)}</div>\n'
-            html += '</div>\n'
-        html += '</div>\n'
-        html += '</div>\n'
-    
-    html += '</div>\n'
-    html += '</div>\n'
-    return html
-
-
-def render_paragraph_card(paragraph: Dict[str, Any], idx: int) -> str:
-    """渲染段落解构卡片"""
-    para_num = paragraph.get("段落序号", idx)
-    time_range = paragraph.get("时间范围", "")
-    units = paragraph.get("包含单元", [])
-    full_text = paragraph.get("段落完整文案", "")
-    
-    card_id = f'paragraph-{para_num}'
-    
-    html = f'<div class="point-card paragraph-card" data-card-id="{card_id}">\n'
-    html += f'<div class="point-card-header" onclick="toggleCardDetails(\'{card_id}\')">\n'
-    html += f'<span class="point-number">段落 #{para_num}</span>\n'
-    if time_range:
-        html += f'<span class="point-time">{html_module.escape(time_range)}</span>\n'
-    if units:
-        units_str = ", ".join([str(u) for u in units])
-        html += f'<span class="point-units">包含单元: {units_str}</span>\n'
-    html += '<span class="toggle-icon">▼</span>\n'
-    html += '</div>\n'
-    
-    html += f'<div class="point-card-details" id="{card_id}-details">\n'
-    
-    # 段落完整文案
-    if full_text:
-        html += '<div class="detail-section">\n'
-        html += '<strong>段落完整文案:</strong>\n'
-        html += f'<div class="detail-text">{html_module.escape(full_text)}</div>\n'
-        html += '</div>\n'
-    
-    # 具体元素实质和形式
-    concrete_elements = paragraph.get("具体元素实质和形式", [])
-    if concrete_elements:
-        html += '<div class="detail-section">\n'
-        html += '<strong>具体元素实质和形式:</strong>\n'
-        for elem in concrete_elements:
-            elem_name = elem.get("具体元素名称", "")
-            html += '<div class="sub-section">\n'
-            if elem_name:
-                html += f'<div class="element-name"><strong>{html_module.escape(elem_name)}</strong></div>\n'
-            if elem.get("对应形式-文案"):
-                html += f'<div class="form-item"><strong>对应形式-文案:</strong>{html_module.escape(elem["对应形式-文案"])}</div>\n'
-            if elem.get("对应形式-画面"):
-                html += f'<div class="form-item"><strong>对应形式-画面:</strong>{html_module.escape(elem["对应形式-画面"])}</div>\n'
-            if elem.get("对应形式-声音"):
-                html += f'<div class="form-item"><strong>对应形式-声音:</strong>{html_module.escape(elem["对应形式-声音"])}</div>\n'
-            html += '</div>\n'
-        html += '</div>\n'
-    
-    # 具象概念实质和形式
-    concrete_concepts = paragraph.get("具象概念实质和形式", [])
-    if concrete_concepts:
-        html += '<div class="detail-section">\n'
-        html += '<strong>具象概念实质和形式:</strong>\n'
-        for concept in concrete_concepts:
-            concept_name = concept.get("具象概念名称", "")
-            html += '<div class="sub-section">\n'
-            if concept_name:
-                html += f'<div class="element-name"><strong>{html_module.escape(concept_name)}</strong></div>\n'
-            if concept.get("对应形式-文案"):
-                html += f'<div class="form-item"><strong>对应形式-文案:</strong>{html_module.escape(concept["对应形式-文案"])}</div>\n'
-            if concept.get("对应形式-画面"):
-                html += f'<div class="form-item"><strong>对应形式-画面:</strong>{html_module.escape(concept["对应形式-画面"])}</div>\n'
-            if concept.get("对应形式-声音"):
-                html += f'<div class="form-item"><strong>对应形式-声音:</strong>{html_module.escape(concept["对应形式-声音"])}</div>\n'
-            html += '</div>\n'
-        html += '</div>\n'
-    
-    # 抽象概念实质和形式
-    abstract_concepts = paragraph.get("抽象概念实质和形式", [])
-    if abstract_concepts:
-        html += '<div class="detail-section">\n'
-        html += '<strong>抽象概念实质和形式:</strong>\n'
-        for concept in abstract_concepts:
-            concept_name = concept.get("抽象概念名称", "")
-            html += '<div class="sub-section">\n'
-            if concept_name:
-                html += f'<div class="element-name"><strong>{html_module.escape(concept_name)}</strong></div>\n'
-            if concept.get("对应形式-文案"):
-                html += f'<div class="form-item"><strong>对应形式-文案:</strong>{html_module.escape(concept["对应形式-文案"])}</div>\n'
-            if concept.get("对应形式-画面"):
-                html += f'<div class="form-item"><strong>对应形式-画面:</strong>{html_module.escape(concept["对应形式-画面"])}</div>\n'
-            if concept.get("对应形式-声音"):
-                html += f'<div class="form-item"><strong>对应形式-声音:</strong>{html_module.escape(concept["对应形式-声音"])}</div>\n'
-            html += '</div>\n'
-        html += '</div>\n'
-    
-    html += '</div>\n'
-    html += '</div>\n'
-    return html
-
-
-def generate_tab3_content(data: Dict[str, Any]) -> str:
-    """生成Tab3内容:整体结构理解"""
-    html = '<div class="tab-content" id="tab3" style="display: none;">\n'
-    html += '<div class="section">\n'
-    html += '<h3>整体结构理解</h3>\n'
-    
-    structure_understanding = data.get("整体结构理解", {})
-    
-    if not structure_understanding:
-        html += '<p>暂无数据</p>\n'
-    else:
-        # 整体解构
-        overall = structure_understanding.get("整体解构", {})
-        if overall:
-            html += render_overall_deconstruction_card(overall)
-        
-        # 段落解构
-        paragraph_list = structure_understanding.get("段落解构", [])
-        if paragraph_list:
-            html += '<div class="section-divider"></div>\n'
-            html += f'<div class="section-info">共 {len(paragraph_list)} 个段落</div>\n'
-            for idx, paragraph in enumerate(paragraph_list, start=1):
-                html += render_paragraph_card(paragraph, idx)
-    
-    html += '</div>\n'
-    html += '</div>\n'
-    return html
-

+ 0 - 67
examples/static/visualize_v2/tab4.py

@@ -1,67 +0,0 @@
-#!/usr/bin/env python3
-"""
-Tab4内容生成器 - 金句提取
-包含:script_type、hooks、golden_sentences
-"""
-import html as html_module
-from typing import Dict, Any, List
-
-
-def generate_tab4_content(data: Dict[str, Any]) -> str:
-    """生成Tab4内容:金句提取"""
-    html = '<div class="tab-content" id="tab4" style="display: none;">\n'
-    html += '<div class="section">\n'
-    html += '<h3>金句提取</h3>\n'
-    
-    golden_sentences = data.get("金句提取", {})
-    
-    if not golden_sentences:
-        html += '<p>暂无数据</p>\n'
-    else:
-        card_id = "golden-sentences"
-        
-        html += f'<div class="point-card golden-card" data-card-id="{card_id}">\n'
-        html += f'<div class="point-card-header" onclick="toggleCardDetails(\'{card_id}\')">\n'
-        html += '<span class="point-text">金句提取结果</span>\n'
-        html += '<span class="toggle-icon">▼</span>\n'
-        html += '</div>\n'
-        
-        html += f'<div class="point-card-details" id="{card_id}-details">\n'
-        
-        # script_type
-        script_type = golden_sentences.get("script_type", "")
-        if script_type:
-            html += '<div class="detail-section">\n'
-            html += '<strong>脚本类型:</strong>\n'
-            html += f'<div class="detail-text">{html_module.escape(script_type)}</div>\n'
-            html += '</div>\n'
-        
-        # hooks
-        hooks = golden_sentences.get("hooks", [])
-        if hooks:
-            html += '<div class="detail-section">\n'
-            html += '<strong>Hooks(钩子):</strong>\n'
-            html += '<ul class="detail-list">\n'
-            for hook in hooks:
-                html += f'<li class="hook-item">{html_module.escape(str(hook))}</li>\n'
-            html += '</ul>\n'
-            html += '</div>\n'
-        
-        # golden_sentences
-        sentences = golden_sentences.get("golden_sentences", [])
-        if sentences:
-            html += '<div class="detail-section">\n'
-            html += '<strong>金句:</strong>\n'
-            html += '<ul class="detail-list">\n'
-            for sentence in sentences:
-                html += f'<li class="golden-item">{html_module.escape(str(sentence))}</li>\n'
-            html += '</ul>\n'
-            html += '</div>\n'
-        
-        html += '</div>\n'
-        html += '</div>\n'
-    
-    html += '</div>\n'
-    html += '</div>\n'
-    return html
-

+ 0 - 162
examples/use_decode_video_model.py

@@ -1,162 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-DecodeVideo 模型使用示例
-
-演示如何使用 models 模块进行数据库操作
-"""
-
-import json
-from contextlib import contextmanager
-from src.models import get_db, DecodeVideo, DecodeStatus, init_db
-
-
-@contextmanager
-def db_session():
-    """数据库会话上下文管理器"""
-    db = next(get_db())
-    try:
-        yield db
-        db.commit()
-    except Exception as e:
-        db.rollback()
-        print(f"数据库操作失败: {e}")
-        raise
-    finally:
-        db.close()
-
-
-def example_create():
-    """示例:创建新记录"""
-    print("\n=== 示例1: 创建新记录 ===")
-    
-    with db_session() as db:
-        video = DecodeVideo.create(
-            task_id=10001,
-            video_id="58840748",
-            status=DecodeStatus.PENDING
-        )
-        db.add(video)
-        print(f"✓ 创建记录成功: {video}")
-
-
-def example_query():
-    """示例:查询记录"""
-    print("\n=== 示例2: 查询记录 ===")
-    
-    with db_session() as db:
-        # 根据 task_id 查询
-        video = db.query(DecodeVideo).filter_by(task_id=10001).first()
-        if video:
-            print(f"✓ 查询到记录: {video}")
-            print(f"  详细信息: {json.dumps(video.to_dict(), indent=2, ensure_ascii=False)}")
-        else:
-            print("✗ 未找到记录")
-
-
-def example_update_status():
-    """示例:更新状态"""
-    print("\n=== 示例3: 更新状态 ===")
-    
-    with db_session() as db:
-        video = db.query(DecodeVideo).filter_by(task_id=10001).first()
-        if video:
-            old_status = video.status
-            video.update_status(DecodeStatus.EXECUTING)
-            print(f"✓ 状态更新: {DecodeStatus.get_description(old_status)} -> {DecodeStatus.get_description(video.status)}")
-
-
-def example_update_result():
-    """示例:更新解码结果"""
-    print("\n=== 示例4: 更新解码结果 ===")
-    
-    with db_session() as db:
-        video = db.query(DecodeVideo).filter_by(task_id=10001).first()
-        if video:
-            # 模拟解码结果
-            result_data = {
-                "video_id": "58840748",
-                "title": "示例视频",
-                "status": "success"
-            }
-            result_json = json.dumps(result_data, ensure_ascii=False)
-            video.update_result(result_json)
-            print(f"✓ 结果更新成功")
-            print(f"  结果长度: {len(result_json)} 字符")
-
-
-def example_query_by_status():
-    """示例:根据状态查询"""
-    print("\n=== 示例5: 根据状态查询 ===")
-    
-    with db_session() as db:
-        # 查询所有待执行的记录
-        pending_videos = db.query(DecodeVideo).filter_by(
-            status=DecodeStatus.PENDING
-        ).all()
-        print(f"✓ 待执行任务数: {len(pending_videos)}")
-        
-        # 查询所有执行成功的记录
-        success_videos = db.query(DecodeVideo).filter_by(
-            status=DecodeStatus.SUCCESS
-        ).all()
-        print(f"✓ 执行成功任务数: {len(success_videos)}")
-
-
-def example_update_failed():
-    """示例:更新为失败状态"""
-    print("\n=== 示例6: 更新为失败状态 ===")
-    
-    with db_session() as db:
-        video = db.query(DecodeVideo).filter_by(task_id=10001).first()
-        if video:
-            video.update_status(DecodeStatus.FAILED, error_reason="处理超时")
-            print(f"✓ 更新为失败状态")
-            print(f"  失败原因: {video.error_reason}")
-
-
-def example_list_all():
-    """示例:列出所有记录"""
-    print("\n=== 示例7: 列出所有记录 ===")
-    
-    with db_session() as db:
-        all_videos = db.query(DecodeVideo).all()
-        print(f"✓ 总记录数: {len(all_videos)}")
-        for video in all_videos[:5]:  # 只显示前5条
-            print(f"  - {video}")
-
-
-def main():
-    """主函数"""
-    print("=" * 50)
-    print("DecodeVideo 模型使用示例")
-    print("=" * 50)
-    
-    try:
-        # 初始化数据库(如果表不存在则创建)
-        print("\n初始化数据库...")
-        init_db()
-        print("✓ 数据库初始化完成")
-        
-        # 运行示例
-        example_create()
-        example_query()
-        example_update_status()
-        example_update_result()
-        example_query_by_status()
-        example_update_failed()
-        example_list_all()
-        
-        print("\n" + "=" * 50)
-        print("所有示例执行完成!")
-        print("=" * 50)
-        
-    except Exception as e:
-        print(f"\n✗ 执行出错: {e}")
-        import traceback
-        traceback.print_exc()
-
-
-if __name__ == "__main__":
-    main()
-

+ 0 - 429
examples/visualize_script_results.py

@@ -1,429 +0,0 @@
-#!/usr/bin/env python3
-"""
-脚本结果可视化工具
-功能:为每个script_result_XXX.json文件生成独立的HTML可视化页面,包含三个Tab切换视图
-"""
-
-import json
-import argparse
-import sys
-from pathlib import Path
-from datetime import datetime
-from typing import List, Dict, Any, Optional
-import re
-import html as html_module
-
-# 保证可以从项目根目录导入 static 包
-PROJECT_ROOT = Path(__file__).parent.parent
-if str(PROJECT_ROOT) not in sys.path:
-    sys.path.insert(0, str(PROJECT_ROOT))
-
-# 导入拆分后的tab模块
-from static.visualize.tab1 import generate_tab1_content
-from static.visualize.tab2 import generate_tab2_content
-from static.visualize.tab3 import generate_tab3_content
-from static.visualize.tab5 import generate_tab5_content
-
-
-class ScriptResultVisualizer:
-    """脚本结果可视化器"""
-
-    def __init__(self, json_file: str = None):
-        """
-        初始化可视化器
-
-        Args:
-            json_file: JSON文件路径
-        """
-        if json_file is None:
-            self.json_file = None
-        else:
-            self.json_file = Path(json_file)
-            if not self.json_file.is_absolute():
-                self.json_file = Path.cwd() / json_file
-
-    def load_json_data(self, file_path: Path) -> Optional[Dict[str, Any]]:
-        """
-        加载JSON文件
-
-        Args:
-            file_path: JSON文件路径
-
-        Returns:
-            JSON数据字典,加载失败返回None
-        """
-        try:
-            with open(file_path, 'r', encoding='utf-8') as f:
-                return json.load(f)
-        except Exception as e:
-            print(f"加载文件失败 {file_path}: {e}")
-            return None
-
-    def generate_tab1_content(self, data: Dict[str, Any]) -> str:
-        """生成Tab1内容:选题、灵感点、目的点、关键点"""
-        return generate_tab1_content(data)
-
-    def generate_tab2_content(self, data: Dict[str, Any]) -> str:
-        """生成Tab2内容:段落"""
-        return generate_tab2_content(data)
-
-    def generate_tab3_content(self, data: Dict[str, Any]) -> str:
-        """生成Tab3内容:按层次展示(实质/形式 → 具体元素/具体概念/抽象概念 → 树形展示)"""
-        return generate_tab3_content(data)
-
-    def generate_tab5_content(self, data: Dict[str, Any]) -> str:
-        """生成tab5内容:实质点与灵感点、目的点、关键点的关系连线图"""
-        return generate_tab5_content(data)
-
-    def build_element_index(self, data: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        构建全局元素索引(包含实质列表和形式列表)
-
-        Args:
-            data: JSON数据
-
-        Returns:
-            元素索引字典 {element_id: element_info}
-        """
-        element_index = {}
-
-        if '脚本理解' not in data:
-            return element_index
-
-        script = data['脚本理解']
-
-        # 处理实质列表
-        substance_list = script.get('实质列表', [])
-        for elem in substance_list:
-            elem_id = str(elem.get('id', ''))
-            if elem_id:
-                element_index[elem_id] = {
-                    'id': elem_id,
-                    'name': elem.get('名称', ''),
-                    'description': elem.get('描述', ''),
-                    'type': '实质',
-                    'dimension': elem.get('维度', {}),
-                    'category': elem.get('分类', {}),
-                    'full_data': elem
-                }
-
-        # 处理形式列表
-        form_list = script.get('形式列表', [])
-        for elem in form_list:
-            elem_id = str(elem.get('id', ''))
-            if elem_id:
-                element_index[elem_id] = {
-                    'id': elem_id,
-                    'name': elem.get('名称', ''),
-                    'description': elem.get('描述', ''),
-                    'type': '形式',
-                    'dimension': elem.get('维度', {}),
-                    'category': elem.get('分类', {}),
-                    'full_data': elem
-                }
-
-        return element_index
-
-    def highlight_element_references(self, text: str, element_index: Dict[str, Any]) -> str:
-        """
-        在文本中标记元素引用,使其可点击查看详情
-
-        Args:
-            text: 待处理的文本
-            element_index: 全局元素索引
-
-        Returns:
-            处理后的HTML文本
-        """
-        if not text or not element_index:
-            return html_module.escape(str(text))
-
-        result = html_module.escape(str(text))
-
-        # 按元素ID长度降序排序,避免短ID覆盖长ID (如 "1" 和 "10")
-        sorted_ids = sorted(element_index.keys(), key=lambda x: len(x), reverse=True)
-
-        for elem_id in sorted_ids:
-            elem = element_index[elem_id]
-            elem_name = elem.get('name', '')
-
-            # 匹配 #ID 格式 (如 "#24")
-            pattern_id = f'#{elem_id}\\b'
-            replacement_id = f'<span class="element-ref clickable" onclick="showElementDetail(\'{elem_id}\')" title="{html_module.escape(elem_name)}">#{elem_id}</span>'
-            result = re.sub(pattern_id, replacement_id, result)
-
-            # 匹配元素名称 (完整词匹配)
-            if elem_name:
-                pattern_name = f'\\b{re.escape(elem_name)}\\b'
-                replacement_name = f'<span class="element-ref clickable" onclick="showElementDetail(\'{elem_id}\')" title="#{elem_id}">{elem_name}</span>'
-                result = re.sub(pattern_name, replacement_name, result)
-
-        return result
-
-    def format_element_id_list(self, id_list, element_index: Dict[str, Any]) -> str:
-        """
-        将元素ID列表格式化为可点击的HTML标签
-
-        Args:
-            id_list: 元素ID列表或单个ID
-            element_index: 全局元素索引
-
-        Returns:
-            HTML字符串
-        """
-        if not id_list:
-            return ''
-
-        html = '<div class="detail-content">\n'
-
-        # 处理单个ID或列表
-        ids = [id_list] if not isinstance(id_list, list) else id_list
-
-        for elem_id in ids:
-            elem_id_str = str(elem_id)
-            if elem_id_str in element_index:
-                elem = element_index[elem_id_str]
-                elem_name = elem.get('name', '')
-                html += f'<span class="element-ref-tag clickable" onclick="showElementDetail(\'{elem_id_str}\')" title="{html_module.escape(elem_name)}">#{elem_id_str}</span>\n'
-            else:
-                html += f'<span class="detail-tag">#{elem_id_str}</span>\n'
-
-        html += '</div>\n'
-        return html
-
-    def generate_html(self, data: Dict[str, Any], json_filename: str) -> str:
-        """生成完整的HTML页面"""
-        # 构建全局元素索引
-        element_index = self.build_element_index(data)
-
-        # 开始构建HTML
-        html = '<!DOCTYPE html>\n'
-        html += '<html lang="zh-CN">\n'
-        html += '<head>\n'
-        html += '    <meta charset="UTF-8">\n'
-        html += '    <meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
-        html += f'    <title>脚本结果可视化 - {json_filename}</title>\n'
-        html += '    <link rel="stylesheet" href="visualize/style.css">\n'
-        html += '</head>\n'
-        html += '<body>\n'
-
-        html += '<div class="container">\n'
-
-        # 页眉
-        html += '<div class="header">\n'
-        html += '    <h1>脚本结果可视化</h1>\n'
-
-        # 显示选题主题
-        if '选题描述' in data and '主题' in data['选题描述']:
-            html += f'    <div class="subtitle">{html_module.escape(data["选题描述"]["主题"])}</div>\n'
-
-        html += f'    <div class="subtitle">{json_filename}</div>\n'
-        html += f'    <div class="subtitle">生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</div>\n'
-        html += '</div>\n'
-
-        # Tab导航
-        html += '<div class="tabs">\n'
-        html += '    <button class="tab active" onclick="switchTab(\'tab1\')">选题点</button>\n'
-        html += '    <button class="tab" onclick="switchTab(\'tab2\')">段落</button>\n'
-        html += '    <button class="tab" onclick="switchTab(\'tab3\')">脚本点</button>\n'
-        html += '    <button class="tab" onclick="switchTab(\'tab5\')">关系图</button>\n'
-        html += '</div>\n'
-
-        # 主内容
-        html += '<div class="content">\n'
-
-        # Tab1内容
-        html += self.generate_tab1_content(data)
-
-        # Tab2内容
-        html += self.generate_tab2_content(data)
-
-        # Tab3内容
-        html += self.generate_tab3_content(data)
-
-        # tab5内容
-        html += self.generate_tab5_content(data)
-
-        html += '</div>\n'
-
-        # 页脚
-        html += '<div class="footer">\n'
-        html += f'    <p>生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>\n'
-        html += '</div>\n'
-
-        html += '</div>\n'
-
-        # JavaScript (传递元素索引)
-        html += '<script>\n'
-        html += f'const elementIndex = {json.dumps(element_index, ensure_ascii=False)};\n'
-        html += '</script>\n'
-        html += '<script src="visualize/script.js"></script>\n'
-
-        html += '</body>\n'
-        html += '</html>\n'
-
-        return html
-
-    def save_all_html(self, output_dir: str | Path | None = None) -> List[str]:
-        """
-        基于 output_demo_script.json,为其中每个视频生成一个独立的 HTML 页面。
-
-        仅支持这种结构:
-        {
-          "results": [
-            {
-              "video_data": {...},
-              "script_result": {...}
-            },
-            ...
-          ]
-        }
-        """
-        if self.json_file is None:
-            print("❌ 错误: 未指定JSON文件")
-            return []
-
-        # 加载JSON数据
-        data = self.load_json_data(self.json_file)
-        if data is None:
-            return []
-
-        results = data.get("results") or []
-        if not isinstance(results, list) or not results:
-            print("⚠️  JSON 中未找到有效的 results 数组,期望为 output_demo_script.json 结构")
-            return []
-
-        # 确定输出目录
-        if output_dir is None:
-            # 默认输出到examples/html目录
-            output_dir = Path(__file__).parent / "html"
-        else:
-            output_dir = Path(output_dir)
-            if not output_dir.is_absolute():
-                output_dir = Path.cwd() / output_dir
-
-        # 创建输出目录
-        output_dir.mkdir(parents=True, exist_ok=True)
-
-        # 确保样式和脚本文件可用:从 static/visualize 拷贝到 输出目录/visualize
-        static_visualize_dir = PROJECT_ROOT / "static" / "visualize"
-        target_visualize_dir = output_dir / "visualize"
-        if static_visualize_dir.exists() and static_visualize_dir.is_dir():
-            import shutil
-            target_visualize_dir.mkdir(parents=True, exist_ok=True)
-            for item in static_visualize_dir.iterdir():
-                dst = target_visualize_dir / item.name
-                if item.is_file():
-                    shutil.copy2(item, dst)
-
-        generated_paths: List[str] = []
-
-        print(f"📁 检测到 output_demo_script 格式,包含 {len(results)} 条结果")
-
-        for idx, item in enumerate(results, start=1):
-            script_data = item.get("script_result")
-            if not isinstance(script_data, dict):
-                print(f"⚠️  跳过第 {idx} 条结果:缺少 script_result 字段或结构不正确")
-                continue
-
-            # 从 what_deconstruction_result 中获取三点解构数据并合并到 script_data
-            what_result = item.get("what_deconstruction_result", {})
-            if isinstance(what_result, dict) and "三点解构" in what_result:
-                deconstruction = what_result["三点解构"]
-                # 将三点解构数据合并到 script_data 顶层,供 tab1 使用
-                if "灵感点" in deconstruction:
-                    script_data["灵感点"] = deconstruction["灵感点"]
-                if "目的点" in deconstruction:
-                    script_data["目的点"] = deconstruction["目的点"]
-                if "关键点" in deconstruction:
-                    script_data["关键点"] = deconstruction["关键点"]
-
-            video_data = item.get("video_data") or {}
-            video_id = video_data.get("video_id") or video_data.get("channel_content_id")  # 兼容旧字段名
-
-            # 用于 HTML 内部展示的"文件名"标签
-            json_label = f"{self.json_file.name}#{idx}"
-
-            # 生成输出文件名(优先使用 video_id,回退到序号)
-            if video_id:
-                output_filename = f"script_result_{video_id}.html"
-            else:
-                output_filename = f"{self.json_file.stem}_{idx}.html"
-
-            output_path = output_dir / output_filename
-
-            html_content = self.generate_html(script_data, json_label)
-
-            with open(output_path, "w", encoding="utf-8") as f:
-                f.write(html_content)
-
-            generated_paths.append(str(output_path))
-            print(f"✅ HTML文件已生成: {output_path}")
-
-        if not generated_paths:
-            print("⚠️  未能从 JSON 中生成任何 HTML 文件")
-
-        return generated_paths
-
-
-def main():
-    """主函数"""
-    # 解析命令行参数
-    parser = argparse.ArgumentParser(
-        description='脚本结果可视化工具 - 基于 output_demo_script.json 为每个视频生成独立的HTML页面',
-        formatter_class=argparse.RawDescriptionHelpFormatter,
-        epilog="""
-使用示例:
-  # 在当前 examples 目录下使用默认的 output_demo_script.json 并输出到 examples/html
-  python visualize_script_results.py
-
-  # 指定 JSON 文件
-  python visualize_script_results.py examples/output_demo_script.json
-
-  # 指定 JSON 文件和输出目录
-  python visualize_script_results.py examples/output_demo_script.json --output-dir examples/html_script
-        """
-    )
-
-    parser.add_argument(
-        'json_file',
-        type=str,
-        nargs='?',
-        help='JSON文件路径(默认为 examples/output_demo_script.json)'
-    )
-
-    parser.add_argument(
-        '-o', '--output-dir',
-        type=str,
-        default=None,
-        help='输出目录路径(默认: examples/html)'
-    )
-
-    args = parser.parse_args()
-
-    # 确定 JSON 文件路径
-    if args.json_file:
-        json_path = Path(args.json_file)
-        if not json_path.is_absolute():
-            json_path = Path.cwd() / json_path
-    else:
-        # 默认使用 examples/output_demo_script.json
-        json_path = Path(__file__).parent / "output_decode_result.json"
-
-    print("🚀 开始生成脚本结果可视化...")
-    print(f"📁 JSON文件: {json_path}")
-    print(f"📄 输出目录: {args.output_dir or (Path(__file__).parent / 'html')}")
-    print()
-
-    visualizer = ScriptResultVisualizer(json_file=str(json_path))
-    generated_files = visualizer.save_all_html(output_dir=args.output_dir)
-
-    if generated_files:
-        print()
-        print(f"🎉 完成! 共生成 {len(generated_files)} 个HTML文件")
-        # 提示其中一个示例文件
-        print(f"📄 示例: 请在浏览器中打开: {generated_files[0]}")
-
-
-if __name__ == "__main__":
-    main()

+ 0 - 285
examples/visualize_script_results_v2.py

@@ -1,285 +0,0 @@
-#!/usr/bin/env python3
-"""
-脚本结果可视化工具 V2
-功能:为 output_demo_script_v2.json 中的每个视频生成独立的HTML可视化页面
-交互形式:卡片+点击详情
-"""
-
-import json
-import argparse
-import sys
-from pathlib import Path
-from datetime import datetime
-from typing import List, Dict, Any, Optional
-import html as html_module
-
-# 保证可以从项目根目录导入
-PROJECT_ROOT = Path(__file__).parent.parent
-if str(PROJECT_ROOT) not in sys.path:
-    sys.path.insert(0, str(PROJECT_ROOT))
-
-# 导入tab模块
-from static.visualize_v2.tab1 import generate_tab1_content
-from static.visualize_v2.tab2 import generate_tab2_content
-from static.visualize_v2.tab3 import generate_tab3_content
-from static.visualize_v2.tab4 import generate_tab4_content
-
-
-class ScriptResultVisualizerV2:
-    """脚本结果可视化器 V2"""
-
-    def __init__(self, json_file: str = None):
-        """
-        初始化可视化器
-
-        Args:
-            json_file: JSON文件路径
-        """
-        if json_file is None:
-            self.json_file = None
-        else:
-            self.json_file = Path(json_file)
-            if not self.json_file.is_absolute():
-                self.json_file = Path.cwd() / json_file
-
-    def load_json_data(self, file_path: Path) -> Optional[Dict[str, Any]]:
-        """
-        加载JSON文件
-
-        Args:
-            file_path: JSON文件路径
-
-        Returns:
-            JSON数据字典,加载失败返回None
-        """
-        try:
-            with open(file_path, 'r', encoding='utf-8') as f:
-                return json.load(f)
-        except Exception as e:
-            print(f"加载文件失败 {file_path}: {e}")
-            return None
-
-    def generate_html(self, data: Dict[str, Any], video_data: Dict[str, Any], json_filename: str) -> str:
-        """生成完整的HTML页面"""
-        # 开始构建HTML
-        html = '<!DOCTYPE html>\n'
-        html += '<html lang="zh-CN">\n'
-        html += '<head>\n'
-        html += '    <meta charset="UTF-8">\n'
-        html += '    <meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
-        html += f'    <title>脚本结果可视化 V2 - {json_filename}</title>\n'
-        html += '    <link rel="stylesheet" href="visualize/style.css">\n'
-        html += '</head>\n'
-        html += '<body>\n'
-
-        html += '<div class="container">\n'
-
-        # 页眉
-        html += '<div class="header">\n'
-        html += '    <h1>脚本结果可视化 V2</h1>\n'
-
-        # 显示视频信息
-        video_title = video_data.get("title", "")
-        video_id = video_data.get("video_id", "")
-        if video_title:
-            html += f'    <div class="subtitle">{html_module.escape(video_title)}</div>\n'
-        if video_id:
-            html += f'    <div class="subtitle">视频ID: {video_id}</div>\n'
-        html += f'    <div class="subtitle">{json_filename}</div>\n'
-        html += f'    <div class="subtitle">生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</div>\n'
-        html += '</div>\n'
-
-        # Tab导航
-        html += '<div class="tabs">\n'
-        html += '    <button class="tab active" onclick="switchTab(\'tab1\')">结构化内容库</button>\n'
-        html += '    <button class="tab" onclick="switchTab(\'tab2\')">L3单元解构</button>\n'
-        html += '    <button class="tab" onclick="switchTab(\'tab3\')">整体结构理解</button>\n'
-        html += '    <button class="tab" onclick="switchTab(\'tab4\')">金句提取</button>\n'
-        html += '</div>\n'
-
-        # 主内容
-        html += '<div class="content">\n'
-
-        # Tab1内容:结构化内容库
-        html += generate_tab1_content(data)
-
-        # Tab2内容:L3单元解构
-        html += generate_tab2_content(data)
-
-        # Tab3内容:整体结构理解
-        html += generate_tab3_content(data)
-
-        # Tab4内容:金句提取
-        html += generate_tab4_content(data)
-
-        html += '</div>\n'
-
-        # 页脚
-        html += '<div class="footer">\n'
-        html += f'    <p>生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>\n'
-        html += '</div>\n'
-
-        html += '</div>\n'
-
-        # JavaScript
-        html += '<script src="visualize/script.js"></script>\n'
-
-        html += '</body>\n'
-        html += '</html>\n'
-
-        return html
-
-    def save_all_html(self, output_dir: str | Path | None = None) -> List[str]:
-        """
-        基于 output_demo_script_v2.json,为其中每个视频生成一个独立的 HTML 页面。
-
-        支持结构:
-        {
-          "results": [
-            {
-              "video_data": {...},
-              "script_result": {...}
-            },
-            ...
-          ]
-        }
-        """
-        if self.json_file is None:
-            print("❌ 错误: 未指定JSON文件")
-            return []
-
-        # 加载JSON数据
-        data = self.load_json_data(self.json_file)
-        if data is None:
-            return []
-
-        results = data.get("results") or []
-        if not isinstance(results, list) or not results:
-            print("⚠️  JSON 中未找到有效的 results 数组")
-            return []
-
-        # 确定输出目录
-        if output_dir is None:
-            # 默认输出到examples/html目录
-            output_dir = Path(__file__).parent / "html"
-        else:
-            output_dir = Path(output_dir)
-            if not output_dir.is_absolute():
-                output_dir = Path.cwd() / output_dir
-
-        # 创建输出目录
-        output_dir.mkdir(parents=True, exist_ok=True)
-
-        # 确保样式和脚本文件可用:从 html/visualize 拷贝到 输出目录/visualize
-        source_visualize_dir = Path(__file__).parent / "html" / "visualize"
-        target_visualize_dir = output_dir / "visualize"
-        if source_visualize_dir.exists() and source_visualize_dir.is_dir():
-            import shutil
-            target_visualize_dir.mkdir(parents=True, exist_ok=True)
-            for item in source_visualize_dir.iterdir():
-                dst = target_visualize_dir / item.name
-                if item.is_file():
-                    # 如果源文件和目标文件是同一个,跳过
-                    if item.resolve() != dst.resolve():
-                        shutil.copy2(item, dst)
-
-        generated_paths: List[str] = []
-
-        print(f"📁 检测到 output_demo_script_v2 格式,包含 {len(results)} 条结果")
-
-        for idx, item in enumerate(results, start=1):
-            script_data = item.get("script_result")
-            if not isinstance(script_data, dict):
-                print(f"⚠️  跳过第 {idx} 条结果:缺少 script_result 字段或结构不正确")
-                continue
-
-            video_data = item.get("video_data") or {}
-            video_id = video_data.get("video_id") or video_data.get("channel_content_id")
-
-            # 用于 HTML 内部展示的"文件名"标签
-            json_label = f"{self.json_file.name}#{idx}"
-
-            # 生成输出文件名:{video_id}_v2.html
-            if video_id:
-                output_filename = f"{video_id}_v2.html"
-            else:
-                output_filename = f"{self.json_file.stem}_{idx}_v2.html"
-
-            output_path = output_dir / output_filename
-
-            html_content = self.generate_html(script_data, video_data, json_label)
-
-            with open(output_path, "w", encoding="utf-8") as f:
-                f.write(html_content)
-
-            generated_paths.append(str(output_path))
-            print(f"✅ HTML文件已生成: {output_path}")
-
-        if not generated_paths:
-            print("⚠️  未能从 JSON 中生成任何 HTML 文件")
-
-        return generated_paths
-
-
-def main():
-    """主函数"""
-    # 解析命令行参数
-    parser = argparse.ArgumentParser(
-        description='脚本结果可视化工具 V2 - 基于 output_demo_script_v2.json 为每个视频生成独立的HTML页面',
-        formatter_class=argparse.RawDescriptionHelpFormatter,
-        epilog="""
-使用示例:
-  # 在当前 examples 目录下使用默认的 output_demo_script_v2.json 并输出到 examples/html
-  python visualize_script_results_v2.py
-
-  # 指定 JSON 文件
-  python visualize_script_results_v2.py examples/output_demo_script_v2.json
-
-  # 指定 JSON 文件和输出目录
-  python visualize_script_results_v2.py examples/output_demo_script_v2.json --output-dir examples/html
-        """
-    )
-
-    parser.add_argument(
-        'json_file',
-        type=str,
-        nargs='?',
-        help='JSON文件路径(默认为 examples/output_demo_script_v2.json)'
-    )
-
-    parser.add_argument(
-        '-o', '--output-dir',
-        type=str,
-        default=None,
-        help='输出目录路径(默认: examples/html)'
-    )
-
-    args = parser.parse_args()
-
-    # 确定 JSON 文件路径
-    if args.json_file:
-        json_path = Path(args.json_file)
-        if not json_path.is_absolute():
-            json_path = Path.cwd() / json_path
-    else:
-        # 默认使用 examples/output_demo_script_v2.json
-        json_path = Path(__file__).parent / "output_demo_script_v2.json"
-
-    print("🚀 开始生成脚本结果可视化 V2...")
-    print(f"📁 JSON文件: {json_path}")
-    print(f"📄 输出目录: {args.output_dir or (Path(__file__).parent / 'html')}")
-    print()
-
-    visualizer = ScriptResultVisualizerV2(json_file=str(json_path))
-    generated_files = visualizer.save_all_html(output_dir=args.output_dir)
-
-    if generated_files:
-        print()
-        print(f"🎉 完成! 共生成 {len(generated_files)} 个HTML文件")
-        # 提示其中一个示例文件
-        print(f"📄 示例: 请在浏览器中打开: {generated_files[0]}")
-
-
-if __name__ == "__main__":
-    main()
-

+ 0 - 210
loggers/sls.py

@@ -1,210 +0,0 @@
-# import json
-# import time
-# import traceback
-# from syslog import syslog
-# from typing import Optional, List
-
-# from aliyun.log import LogClient, LogItem, PutLogsRequest, GetLogsRequest
-# from loguru import logger
-# from tornado.process import task_id
-
-
-# from utils import get_global_config
-# from datetime import datetime
-
-# _config = get_global_config().log.aliyun
-
-
-# class AliyunLog(object):
-#     client = LogClient(endpoint=_config.endpoint,
-#                        accessKey=_config.access_key_secret,
-#                        accessKeyId=_config.access_key_id)
-#     project_name = 'cyber-crawler-prod'
-#     logstore_name = 'error-log'
-#     process_logstore_name = 'process-log'
-
-#     @classmethod
-#     def record(cls, task: CrawlerTask, stacktrace: str):
-#         log_item = LogItem()
-#         log_item.set_contents([
-#             ('task_id', task.task_id),
-#             ('plan_id', task.plan_id),
-#             ('plan_type', str(task.plan_type.value.id)),
-#             ('channel', str(task.channel.value.id)),
-#             ('crawler_mode', str(task.crawler_mode.value.id)),
-#             ('task_params', task.task_params),
-#             ('stacktrace', stacktrace),
-#         ])
-#         request = PutLogsRequest(project=cls.project_name,
-#                                  logstore=cls.logstore_name,
-#                                  logitems=[log_item],
-#                                  compress=False)
-#         cls.client.put_logs(request)
-
-#     @classmethod
-#     def process(cls, task: CrawlerTask, process_step: str, log_type: str, message: str,
-#                     content: Optional[AiDitContent],
-#                     account: Optional[AiDitAccount],
-#                     content_portrait: Optional[List[CrawlerContentPortrait]],
-#                     account_portrait: Optional[List[CrawlerAccountPortrait]]):
-#         """
-#         记录任务执行&爬取过程
-#         process_step: crawler、skip、filter、after_filter
-#         log_type: content、content_portrait、account_portrait
-#         """
-#         try:
-#             # 序列化
-#             # 只有在对象不为 None 时才进行序列化,否则为 None
-#             content_str = content.model_dump_json() if content else None
-#             account_str = account.model_dump_json() if account else None
-#             # 序列化
-#             if content_portrait:
-#                 # 使用列表推导式将每个对象转换为字典,然后序列化整个列表
-#                 content_portrait_str = json.dumps([item.model_dump() for item in content_portrait])
-#             else:
-#                 content_portrait_str = None
-
-#             if account_portrait:
-#                 # 使用列表推导式将每个对象转换为字典,然后序列化整个列表
-#                 account_portrait_str = json.dumps([item.model_dump() for item in account_portrait])
-#             else:
-#                 account_portrait_str = None
-
-#             log_item = LogItem()
-#             task_id = task.task_id
-#             plan_id = task.plan_id
-#             plan_type = ''
-#             if task.plan_type is not None:
-#                 plan_type = str(task.plan_type.value.id)
-#             channel = ''
-#             if task.channel is not None:
-#                 channel = str(task.channel.value.id)
-#             crawler_mode = ''
-#             if task.crawler_mode is not None:
-#                 crawler_mode = str(task.crawler_mode.value.id)
-#             task_params = ''
-#             if task.task_params is not None:
-#                 task_params = json.dumps(task.task_params)
-
-#             log_item.set_contents([
-#                 # ('task_id', task.task_id),
-#                 # ('plan_id', task.plan_id),
-#                 # ('plan_type', str(task.plan_type.value.id)),
-#                 # ('channel', str(task.channel.value.id)),
-#                 # ('crawler_mode', str(task.crawler_mode.value.id)),
-#                 # ('task_params', task.task_params),
-#                 ('task_id', task_id),
-#                 ('plan_id', plan_id),
-#                 ('plan_type', plan_type),
-#                 ('channel', channel),
-#                 ('crawler_mode', crawler_mode),
-#                 ('task_params', task_params),
-#                 ('process_step', process_step),
-#                 ('log_type', log_type),
-#                 ('message', message),
-#                 ('content', content_str or ''),
-#                 ('account', account_str or ''),
-#                 ('content_portrait', content_portrait_str or ''),
-#                 ('account_portrait', account_portrait_str or ''),
-#                 ('timestamp', str(time.time())),
-#             ])
-#             request = PutLogsRequest(project=cls.project_name,
-#                                      logstore=cls.process_logstore_name,
-#                                      logitems=[log_item],
-#                                      compress=False)
-#             cls.client.put_logs(request)
-#         except Exception as e:
-#             traceback.print_exc()
-
-#     @classmethod
-#     def info(cls, path: str, channel: int, params: str, response: str, status_code: int, msg: str = '',
-#              token: str = ''):
-#         log_item = LogItem()
-#         log_item.set_contents([
-#             ('path', path),
-#             ('channel', channel),
-#             ('params', params),
-#             ('response', response),
-#             ('status_code', status_code),
-#             ('msg', msg),
-#             ('token', token)
-#         ])
-#         request = PutLogsRequest(project=cls.project_name,
-#                                  logstore='request-log',
-#                                  logitems=[log_item],
-#                                  compress=False)
-#         cls.client.put_logs(request)
-
-#     @classmethod
-#     def req_info(cls, channel: str, params: str, response: str, source: str, path: str = '/', status_code: int = 0,
-#                  token: str = ''):
-#         try:
-#             log_item = LogItem()
-#             log_item.set_contents([
-#                 ('channel', channel),
-#                 ('params', str(params)),
-#                 ('response', str(response)),
-#                 ('path', path),
-#                 ('source', source),
-#                 ('status_code', str(status_code)),
-#                 ('token', token)
-#             ])
-#             request = PutLogsRequest(project=cls.project_name,
-#                                      logstore='info-log',
-#                                      logitems=[log_item],
-#                                      compress=False)
-#             cls.client.put_logs(request)
-#         except Exception as e:
-#             logger.error(f"AliyunLog.req_info error: {e}")
-#             pass
-
-#     @classmethod
-#     def http_req_info(cls, path: str, params: str, response: str, status_code: int = 0):
-#         log_item = LogItem()
-#         log_item.set_contents([
-#             ('path', path),
-#             ('params', params),
-#             ('response', response),
-#             ('status_code', status_code)
-#         ])
-#         request = PutLogsRequest(project=cls.project_name,
-#                                  logstore='info-log',
-#                                  logitems=[log_item],
-#                                  compress=False)
-#         cls.client.put_logs(request)
-
-#     @classmethod
-#     def get_log(cls):
-#         from_time = int(datetime.now().timestamp() * 1000) - 1000 * 60 * 60 * 24
-#         to_time = int(datetime.now().timestamp() * 1000)
-
-#         response = cls.client.get_logs(GetLogsRequest(project='cyber-crawler-prod',
-#                                                       logstore='request-log',
-#                                                       topic='',
-#                                                       fromTime=from_time,
-#                                                       toTime=to_time,
-#                                                       query='path: /crawler/moonshot/kimi and status_code :10000'))
-#         print(response.body)
-#         return response
-
-
-# class AliyunHkLog(object):
-#     client = LogClient(endpoint='cn-hongkong.log.aliyuncs.com',
-#                        accessKey=_config.access_key_secret,
-#                        accessKeyId=_config.access_key_id)
-#     project_name = 'cyber-crawler-prod'
-
-#     @classmethod
-#     def get_log(cls, query: str, project_name: str = 'cyber-crawler-prod', logstore_name: str = 'request-log'):
-#         today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
-#         from_time = int(today.timestamp()) - 24 * 60 * 60
-#         to_time = int(today.timestamp())
-
-#         response = cls.client.get_logs(GetLogsRequest(project=cls.project_name,
-#                                                       logstore=logstore_name,
-#                                                       topic='',
-#                                                       fromTime=from_time,
-#                                                       toTime=to_time,
-#                                                       query=query))
-
-#         return response.body

+ 65 - 259
main.py

@@ -5,36 +5,31 @@ from fastapi import FastAPI, HTTPException, Request
 from fastapi.responses import JSONResponse
 from fastapi.middleware.cors import CORSMiddleware
 from pydantic import BaseModel
-from utils.params import TaskStatusParam, DecodeListParam,TopicListParam,AccountParam,EvaluateListParam,EvaluateStatusParam,DecodeWorkflowParam
+from utils.params import DecodeContentParam, PatternContentParam
 from dotenv import load_dotenv, find_dotenv
+from typing import List, Dict, Any, Optional
 
-
-from decode_task.decodeTask import get_decode_result_by_id as get_decode_result_by_id_db
-from decode_task.evaluateTask import get_evaluate_result_by_id as get_evaluate_result_by_id_db
-from decode_task.topicTask import get_topic_result_by_id as get_topic_result_by_id_db
-from decode_task.topicTask import update_topic_result_by_id as update_topic_result_by_id_db
-from decode_task.topicTask import search_topic_list as search_topic_list_db
-from decode_task.topicTask import get_user_count as get_user_count_db
-
-
-
-from typing import List
-from models.decode_record import DecodeRecord
-from models.evaluate_record import EvaluateRecord
-from models.decode_workflow import DecodeWorkflow
-
-from task_schedule import TaskScheduler
+from tasks.decode import begin_decode_task
+from tasks.detail import get_decode_detail_by_task_id
+from tasks.pattern import begin_pattern_task
 
 from loguru import logger
 import sys
 
 logger.add(sink=sys.stderr, level="ERROR", backtrace=True, diagnose=True)
 
+# 响应消息映射
+RESPONSE_MSG_MAP = {
+    0: "success",
+    1002: "视频不存在",
+    2001: "解构/聚类任务创建失败",
+    -1: "failed",
+    404: "任务不存在"
+}
+
 load_dotenv(find_dotenv(), override=False)
 app = FastAPI()
 
-scheduler = TaskScheduler()
-
 app.add_middleware(
     CORSMiddleware,
     allow_origins=["*"],
@@ -50,251 +45,62 @@ async def http_exception_handler(request: Request, exc: HTTPException):
         content={"code": exc.status_code, "message": exc.message, "data": None}
     )
 
-@app.on_event("startup")
-def startup_event():
-    scheduler.start()
-
-
-
-
-@app.post("/decodeWorkflow/create")
-def decode_topic(param:TopicListParam):
-    video_list = param.video_list
-    logger.info(f"数据池数据 = {video_list}")
-    data_list = []
-
-
-    for video in video_list:
-        video_id = video.video_id
-        video_url = video.video_url
-        video_title = video.title
-        task_id = str(uuid.uuid4())
-        DecodeWorkflow(
-            task_id=task_id,
-            video_id=video_id,
-            video_url=video_url,
-            title=video_title,
-            type=param.type,
-            topic_result=None,
-            script_result=None,
-            account=param.account,
-            nick_name=param.nick_name,
-        ).save()
-        data_list.append({
-            "task_id": task_id,
-            "video_id": video_id,
-            "video_url": video_url,
-            "title": video_title,
-            "task_status": 0,
-            "type": param.type,
-        })
-    return {
-        "code": 0,
-        "message": "success",
-        "data":{
-            "type": param.type,
-            "account": param.account,
-            "nick_name": param.nick_name,
-            "tasks": data_list
-        } 
-    }
-
-@app.post("/decodeWorkflow/update")
-def update_topic_result(param: DecodeWorkflowParam):
-    db_res = update_topic_result_by_id_db(param)
-    logger.info(f"\n查询结构结果的task_id = {param.task_id}")
-
-    if not db_res:
-        return {
-            "code": -1,
-            "message": '任务不存在',
-            "data": None
-        }
-    topic_result,script_result, status,error_reason,video_url,title = db_res
-    return {
-        "code": 0,
-        "message": status == 2 and "success" or (error_reason or "success"),
-        "data": {
-            "topic_result": topic_result,
-            "script_result": script_result,
-            "status": status,
-            "error":error_reason,
-            "video_url": param.video_url or video_url,
-            "title": param.title or title,
-            "task_status": param.task_status if param.task_status is not None else status,
-            "video_id": param.video_id,
-        }   
+def _build_api_response(
+    code: int,
+    data: Any = None,
+    reason: Optional[str] = None
+) -> JSONResponse:
+    """构建统一的API响应"""
+    msg = RESPONSE_MSG_MAP.get(code, "failed")
+    content = {
+        "code": code,
+        "msg": msg,
+        "data": data
     }
     
-
-@app.get("/decodeWorkflow/userCount/{account}")
-def get_user_count(account: str):
-    result = get_user_count_db(account)
-    logger.info(f"\n查询账号{account}的任务数量----{result}")
-
-    if result is None:
-        return {
-            "code": -1,
-            "message": '任务不存在',
-            "data": None
-        }
-
+    # 失败时添加 reason 字段
+    if code != 0 and reason:
+        content["reason"] = reason
     
-    return {
-        "code": 0,
-        "message": "success",
-        "data": result
-    }
-  
-
-
+    return JSONResponse(status_code=200, content=content)
 
-@app.get("/decodeWorkflow/result")
-def get_topic_result(task_id: str):
-    result = get_topic_result_by_id_db(task_id)
-    logger.info(f"\n查询结构结果的task_id = {task_id}")
 
-    if not result:
-        return {
-            "code": -1,
-            "message": '任务不存在',
-            "data": None
-        }
-
-    status = result["task_status"]
-    error_reason = result["error_reason"]
-    return {
-        "code": status,
-        "message": status == 2 and "success" or error_reason,
-        "data": result
-    }
-  
-
-
-@app.get("/decodeWorkflow/list")
-def search_topic_list(video_id: str = None, video_url: str = None, title: str = None, task_status: int = None):
-    params = {}
-    if video_id:
-        params["video_id"] = video_id
-    if video_url:
-        params["video_url"] = video_url
-    if title:
-        params["title"] = title
-    if task_status is not None:
-        params["task_status"] = task_status
-    tasks = search_topic_list_db(params if params else None)
-    if not tasks:
-        return {
-            "code": -1,
-            "message": "暂无数据",
-            "data": []
-        }
-    return {
-        "code": 0,
-        "message": "success",
-        "data": tasks
-    }
-
-
-
-@app.post("/decodeVideo/create")
-def decode_video(param:DecodeListParam):
-
-    video_list = param.video_list
-    logger.info(f"数据池数据 = {video_list}")
-    data_list = []
-    for video in video_list:
-        video_id = video.channel_content_id
-        video_url = video.video
-        task_id = str(uuid.uuid4())
-        DecodeRecord(
-            task_id=task_id,
-            video_id=video_id,
-            video_url=video_url,
-            task_status = 0
-        ).save()
-        data_list.append({
-            "task_id": task_id,
-            "video_id": video_id,
-        })
-    return {
-        "code": 0,
-        "message": "success",
-        "data": data_list
-    }
-
-@app.post("/decode/result")
-def get_decode_result(param: TaskStatusParam):
-    db_res = get_decode_result_by_id_db(param.task_id)
-    logger.info(f"\n查询结构结果的task_id = {param.task_id}")
-
-    if not db_res:
-        return {
-            "code": -1,
-            "message": '任务不存在',
-            "data": None
-        }
-    result, status,error_reason,search_keywords = db_res
-    return {
-        "code": 0,
-        "message": status == 2 and "success" or error_reason,
-        "data": {
-            "result": result,
-            "status": status,
-            "error":error_reason,
-            "searchKeyword":search_keywords
-        } 
-    }
+@app.post("/api/v1/content/tasks/decode")
+def decode_content(param: DecodeContentParam):
+    """创建解构任务"""
+    res = begin_decode_task(param)
+    code = res.get("code", -1)
+    task_id = res.get("task_id")
+    reason = res.get("reason", "")
     
-@app.post("/evaluate/create")
-def evaluate_video(param:EvaluateListParam):
+    return _build_api_response(
+        code=code,
+        data={"task_id": task_id} if task_id else None,
+        reason=reason
+    )
 
-    evaluate_list = param.evaluate_list
+@app.get("/api/v1/content/tasks/{taskId}")
+def get_task_detail(taskId: str):
+    """获取任务详情"""
+    result = get_decode_detail_by_task_id(taskId)
     
-    logger.info(f"创建评估数据 = {evaluate_list}")
-
-    data_list = []
-    for evaluate in evaluate_list:
-        evaluate_id = str(uuid.uuid4())
-        task_id = evaluate.task_id
-        channel_content_id = evaluate.channel_content_id
-        EvaluateRecord(
-            evaluate_id=evaluate_id,
-            task_id=task_id,
-            search_result= json.dumps(evaluate.result),
-            evaluate_result=None,   
-            error_reason=None,
-            status = 0
-        ).save()
-        data_list.append({
-            "task_id": task_id,
-            "evaluate_id": evaluate_id,
-            "channel_content_id":channel_content_id
-        })
-    return {
-        "code": 0,
-        "message": "success",
-        "data": data_list
-    }
-
-@app.post("/evaluate/result")
-def get_evaluate_result(param: EvaluateStatusParam):
-    db_res = get_evaluate_result_by_id_db(param.evaluate_id)
-    logger.info(f"\n查询评估结果的evaluate_id = {param.evaluate_id}")
-
-    if not db_res:
-        return {
-            "code": -1,
-            "message": '评估不存在',
-            "data": None
-        }
-    result, status,error_reason = db_res
-    return {
-        "code": 0,
-        "message": status == 2 and "success" or error_reason,
-        "data": {
-            "result": result,
-            "status": status,
-            "error":error_reason,
-        } 
-    }
+    # 任务不存在
+    if result is None:
+        return _build_api_response(code=404, data=None)
+    
+    # 直接返回结果(已经包含 code、msg、data、reason)
+    return JSONResponse(status_code=200, content=result)
+
+@app.post("/api/v1/content/tasks/pattern")
+def pattern_content(param: PatternContentParam):
+    """创建模式任务"""
+    res = begin_pattern_task(param)
+    code = res.get("code", -1)
+    task_id = res.get("task_id")
+    reason = res.get("reason", "")
+    
+    return _build_api_response(
+        code=code,
+        data={"task_id": task_id} if task_id else None,
+        reason=reason
+    )

+ 0 - 30
models/decode_record.py

@@ -1,30 +0,0 @@
-from typing import Optional
-
-from pydantic import BaseModel, Field
-from typing_extensions import Annotated
-
-from utils.general import get_now_ts
-from utils.sync_mysql_help import mysql
-
-class DecodeRecord(BaseModel):
-    table_name:       Annotated[Optional[str], Field(description='表名', exclude=True)] = 'decode_record'
-
-    task_id:          Annotated[str, Field(description='任务ID')]
-    video_id:            Annotated[str, Field(description='视频ID')]
-    video_url:            Annotated[str, Field(description='视频地址')]
-    task_status:      Annotated[Optional[int], Field(description='任务状态', default=1)]  # 0:待执行  /   1:执行中  /  2:执行成功  3:执行失败
-    create_timestamp: Annotated[Optional[int], Field(description='任务创建时间戳', default_factory=get_now_ts)]
-
-    def save(self):
-
-        record = self.model_dump(exclude={'table_name'})
-        keys = record.keys()
-        sql = f'''INSERT INTO {self.table_name} ({", ".join(keys)}) VALUES ({", ".join(["%s"] * len(keys))})'''
-        mysql.execute(sql, tuple([record[key] for key in keys]))
-
-    async def async_save(self):
-    
-        record = self.model_dump(exclude={'table_name'})
-        keys = record.keys()
-        sql = f'''INSERT INTO {self.table_name} ({", ".join(keys)}) VALUES ({", ".join(["%s"] * len(keys))})'''
-        await mysql.execute(sql, tuple([record[key] for key in keys]))

+ 67 - 0
models/decode_task_result.py

@@ -0,0 +1,67 @@
+from typing import Optional, List, TYPE_CHECKING
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from utils.sync_mysql_help import mysql
+
+if TYPE_CHECKING:
+    from utils.params import ContentParam
+
+
+class WorkflowDecodeTaskResult(BaseModel):
+    table_name: Annotated[Optional[str], Field(description='表名', exclude=True)] = 'workflow_decode_task_result'
+
+    task_id: Annotated[str, Field(description='任务ID')]
+    channel_content_id: Annotated[str, Field(description='内容ID')]
+    images: Annotated[str, Field(description='内容url', default='')]
+    title: Annotated[str, Field(description='内容title', default='')]
+    error_code: Annotated[Optional[str], Field(description='失败码', default=None)]
+    error_message: Annotated[Optional[str], Field(description='失败信息', default=None)]
+    result_payload: Annotated[Optional[str], Field(description='执行结果', default=None)]
+    result_size: Annotated[Optional[int], Field(description='执行结果大小', default=None)]
+    channel_account_id: Annotated[Optional[str], Field(description='作者上下文', default=None)]
+    channel_account_name: Annotated[Optional[str], Field(description='作者上下文', default=None)]
+    body_text: Annotated[Optional[str], Field(description='内容上下文', default=None)]
+    video_url: Annotated[Optional[str], Field(description='内容视频地址', default=None)]
+
+    def save(self):
+        """保存结果到数据库"""
+        record = self.model_dump(exclude={'table_name'})
+        keys = record.keys()
+        sql = f'''INSERT INTO {self.table_name} ({", ".join(keys)}) VALUES ({", ".join(["%s"] * len(keys))})'''
+        mysql.execute(sql, tuple([record[key] for key in keys]))
+    
+    def get_images_list(self) -> List[str]:
+        """解析 images 字段,返回 URL 列表(逗号分隔格式)"""
+        if not self.images:
+            return []
+        return [url.strip() for url in self.images.split(',') if url.strip()]
+
+    @staticmethod
+    def create_result(task_id: str, content: 'ContentParam') -> 'WorkflowDecodeTaskResult':
+        """创建并初始化结果记录"""
+        # 处理 images 列表,使用逗号分隔的字符串格式存储
+        # 数据库字段建议使用 TEXT 类型,可存储大量 URL
+        # 如果有 video_url,也将其加入到 images 字段中
+        images_list = []
+        if content.images and len(content.images) > 0:
+            images_list.extend(content.images)
+        if content.video_url:
+            ## 解构阶段要求视频链接也插入到images里面
+            images_list.append(content.video_url)
+        images_str = ','.join(images_list) if images_list else ''
+        
+        result = WorkflowDecodeTaskResult(
+            task_id=task_id,
+            channel_content_id=content.channel_content_id,
+            images=images_str,
+            title=content.title[:64] if len(content.title) > 64 else content.title,  # 限制长度
+            channel_account_id=content.channel_account_id,
+            channel_account_name=content.channel_account_name[:64] if content.channel_account_name and len(content.channel_account_name) > 64 else content.channel_account_name,
+            body_text=content.body_text,
+            video_url=content.video_url[:100] if content.video_url and len(content.video_url) > 100 else content.video_url,
+            result_payload=None,
+            result_size=0
+        )
+        result.save()
+        return result

+ 0 - 40
models/decode_workflow.py

@@ -1,40 +0,0 @@
-from typing import Optional
-
-from pydantic import BaseModel, Field
-from typing_extensions import Annotated
-
-from utils.general import get_now_ts
-from utils.sync_mysql_help import mysql
-
-class DecodeWorkflow(BaseModel):
-    table_name:       Annotated[Optional[str], Field(description='表名', exclude=True)] = 'decode_workflow'
-
-    task_id:          Annotated[str, Field(description='任务ID')]
-    video_id:         Annotated[Optional[str], Field(description='视频ID', default=None)]
-    video_url:        Annotated[str, Field(description='视频地址')]
-    title:            Annotated[Optional[str], Field(description='视频标题', default=None)]
-    task_status:      Annotated[Optional[int], Field(description='任务状态', default=0)]  # 0:待执行  /   1:执行中  /  2:执行成功  3:执行失败
-    topic_result:     Annotated[Optional[str], Field(description='选题结果')]
-    script_result:    Annotated[Optional[str], Field(description='脚本结果')]
-    created_at:       Annotated[Optional[int], Field(description='任务创建时间戳', default_factory=get_now_ts)]
-    error_reason:     Annotated[Optional[str], Field(description='错误原因', default=None)]
-    type:             Annotated[Optional[int], Field(description='任务类型', default=1)]  # 0:解码任务  /  1:评估任务
-    account:          Annotated[Optional[str], Field(description='账号', default=None)]
-    nick_name:        Annotated[Optional[str], Field(description='昵称', default=None)]
-    token_script_use: Annotated[Optional[str], Field(description='脚本使用token', default=None)]
-    token_topic_use:  Annotated[Optional[str], Field(description='选题使用token', default=None)]
-
-
-    def save(self):
-
-        record = self.model_dump(exclude={'table_name'})
-        keys = record.keys()
-        sql = f'''INSERT INTO {self.table_name} ({", ".join(keys)}) VALUES ({", ".join(["%s"] * len(keys))})'''
-        mysql.execute(sql, tuple([record[key] for key in keys]))
-
-    async def async_save(self):
-    
-        record = self.model_dump(exclude={'table_name'})
-        keys = record.keys()
-        sql = f'''INSERT INTO {self.table_name} ({", ".join(keys)}) VALUES ({", ".join(["%s"] * len(keys))})'''
-        await mysql.execute(sql, tuple([record[key] for key in keys]))

+ 0 - 32
models/evaluate_record.py

@@ -1,32 +0,0 @@
-from typing import Optional
-
-from pydantic import BaseModel, Field
-from typing_extensions import Annotated
-
-from utils.general import get_now_ts
-from utils.sync_mysql_help import mysql
-
-class EvaluateRecord(BaseModel):
-    table_name:       Annotated[Optional[str], Field(description='表名', exclude=True)] = 'evaluate_record'
-    
-    evaluate_id:          Annotated[str, Field(description='任务ID', default=None)]
-    task_id:          Annotated[Optional[str], Field(description='任务ID', default=None)]
-    status:      Annotated[Optional[int], Field(description='任务状态', default=1)]  # 1: 进行中, 2: 已完成, 3: 失败
-    search_result:      Annotated[Optional[str], Field(description='关键词结果', default=None)]
-    evaluate_result:      Annotated[Optional[str], Field(description='评估结果', default=None)]
-    error_reason:      Annotated[Optional[str], Field(description='失败原因', default=None)]
-    created_at: Annotated[Optional[int], Field(description='任务创建时间戳', default_factory=get_now_ts)]
-
-    def save(self):
-
-        record = self.model_dump(exclude={'table_name'})
-        keys = record.keys()
-        sql = f'''INSERT INTO {self.table_name} ({", ".join(keys)}) VALUES ({", ".join(["%s"] * len(keys))})'''
-        mysql.execute(sql, tuple([record[key] for key in keys]))
-
-    async def async_save(self):
-
-        record = self.model_dump(exclude={'table_name'})
-        keys = record.keys()
-        sql = f'''INSERT INTO {self.table_name} ({", ".join(keys)}) VALUES ({", ".join(["%s"] * len(keys))})'''
-        await mysql.execute(sql, tuple([record[key] for key in keys]))

+ 37 - 0
models/task.py

@@ -0,0 +1,37 @@
+from typing import Optional
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from utils.sync_mysql_help import mysql
+from utils.params import SceneEnum, CapabilityEnum
+from utils.general import generate_task_id
+
+
+class WorkflowTask(BaseModel):
+    table_name: Annotated[Optional[str], Field(description='表名', exclude=True)] = 'workflow_task'
+
+    task_id: Annotated[str, Field(description='任务ID')]
+    root_task_id: Annotated[str, Field(description='任务根节点', default='')]
+    status: Annotated[int, Field(description='任务状态', default=0)]  # 0:待处理 1:执行中 2:成功 3:失败
+    scene: Annotated[int, Field(description='业务场景')]  # 0:选题 1:创作 2:制作
+    capability: Annotated[int, Field(description='能力类型')]  # 0:解构 1:聚类
+
+    def save(self):
+        """保存任务到数据库"""
+        record = self.model_dump(exclude={'table_name'})
+        keys = record.keys()
+        sql = f'''INSERT INTO {self.table_name} ({", ".join(keys)}) VALUES ({", ".join(["%s"] * len(keys))})'''
+        mysql.execute(sql, tuple([record[key] for key in keys]))
+
+    @staticmethod
+    def create_task(scene: SceneEnum, capability: CapabilityEnum, root_task_id: str = '') -> 'WorkflowTask':
+        """创建新任务"""
+        task = WorkflowTask(
+            task_id=generate_task_id(),
+            root_task_id=root_task_id,
+            status=0,  # 待处理
+            scene=scene.value,
+            capability=capability.value
+        )
+        task.save()
+        return task

BIN=BIN
prd1.4.pdf


+ 11 - 21
requirements.txt

@@ -1,28 +1,18 @@
-langgraph>=0.2.46
-langchain==0.3.27
-langchain-core==0.3.80
-langchain-openai>=0.2.0
-langchain-google-genai>=2.0.0
-google-genai>=0.2.0
+# Web框架
+fastapi>=0.111.0
+uvicorn>=0.30.0
+
+# 数据验证和类型
 pydantic>=2.0.0
 typing-extensions>=4.12.0
+
+# 环境变量管理
 python-dotenv>=1.0.0
-pyyaml>=6.0.0
-requests>=2.32.0
-aiohttp>=3.9.0
-pytest>=8.0.0
-pytest-asyncio>=0.24.0
-aiofiles>=24.0.0
-fastapi>=0.111.0
-uvicorn>=0.30.0
+
+# 日志
 loguru>=0.7.0
-APScheduler>=3.10.4
-redis>=5.0.0
+
+# 数据库
 PyMySQL>=1.1.0
 DBUtils>=3.0.3
-dashscope>=1.14.0
-oss2>=2.17.0
-python-dotenv
-socksio>=1.0.0
-sqlalchemy>=2.0.0
 cryptography>=41.0.0

BIN=BIN
src/.DS_Store


+ 0 - 0
src/__init__.py


BIN=BIN
src/components/.DS_Store


+ 0 - 223
src/components/README.md

@@ -1,223 +0,0 @@
-# 组件抽象层设计说明
-
-## 概述
-
-本组件抽象层为 AIGC 解构系统提供了统一的原子组件接口,支持 Agent、Tool 和 Function 三种类型的组件。所有组件都遵循单一职责原则,可以灵活组合构建复杂的工作流。
-
-## 架构设计
-
-```
-组件抽象层
-├── agents/           # Agent 组件
-│   └── base.py      # Agent 基类定义
-├── tools/           # Tool 组件  
-│   └── base.py      # Tool 基类定义
-├── functions/       # Function 组件
-│   └── base.py      # Function 基类定义
-└── __init__.py      # 统一导入接口
-```
-
-## 组件类型
-
-### 1. Agent 组件
-
-Agent 组件负责智能决策和复杂推理,基于 LangGraph 实现。
-
-#### 基类层次
-
-```
-BaseComponent (抽象基类)
-    └── BaseAgent (Agent 基类)
-        ├── BaseLLMAgent (LLM Agent)
-        ├── BaseReactAgent (ReAct Agent) 
-        └── BaseGraphAgent (Graph Agent)
-```
-
-#### Agent 类型说明
-
-- **BaseLLMAgent**: 直接基于 LLM 的简单智能体,适用于单轮对话和简单任务
-- **BaseReactAgent**: 基于 ReAct 模式的智能体,支持工具调用和推理
-- **BaseGraphAgent**: 基于 StateGraph 的复杂智能体,支持多节点工作流
-
-### 2. Tool 组件
-
-Tool 组件提供外部能力集成,支持 LangGraph 的 `@tool` 装饰器。
-
-#### Tool 类型
-
-- **SimpleTool**: 基于函数的简单工具
-- **AsyncTool**: 支持异步操作的工具
-- **ConfigurableTool**: 支持运行时配置的工具
-
-#### 工具注册表
-
-```python
-from src.components import tool_registry
-
-# 获取所有启用的工具
-enabled_tools = tool_registry.get_enabled_tools()
-```
-
-### 3. Function 组件
-
-Function 组件处理纯函数计算,无副作用的数据转换操作。
-
-#### Function 类型
-
-- **SimpleFunction**: 基于普通函数的简单包装
-- **ConfigurableFunction**: 支持配置参数的函数组件
-- **PipelineFunction**: 将多个函数组件串联成管道
-- **ConditionalFunction**: 根据条件选择不同的函数执行
-
-## 使用示例
-
-### 创建 Agent
-
-```python
-from src.components import BaseLLMAgent, BaseReactAgent
-
-# 创建 LLM Agent
-class PersonaAnalysisAgent(BaseLLMAgent):
-    def __init__(self):
-        super().__init__(
-            name="persona_analysis",
-            description="分析用户人设特征",
-            system_prompt="你是专业的人设分析师..."
-        )
-    
-    def _build_messages(self, state):
-        # 实现消息构建逻辑
-        pass
-    
-    def _update_state(self, state, response):
-        # 实现状态更新逻辑
-        pass
-
-# 创建 ReAct Agent
-class DeconstructionAgent(BaseReactAgent):
-    def __init__(self, tools):
-        super().__init__(
-            name="deconstruction",
-            description="执行内容解构",
-            tools=tools
-        )
-    
-    def _extract_messages(self, state):
-        # 从状态中提取消息
-        pass
-    
-    def _merge_response(self, state, response):
-        # 合并响应到状态
-        pass
-```
-
-### 创建 Tool
-
-```python
-from src.components import component_tool
-from pydantic import BaseModel
-
-class SearchInput(BaseModel):
-    query: str
-    limit: int = 10
-
-@component_tool(
-    name="search_tool",
-    description="搜索相关信息",
-    category="search",
-    schema=SearchInput
-)
-def search_information(query: str, limit: int = 10) -> str:
-    \"\"\"搜索相关信息\"\"\"
-    # 实现搜索逻辑
-    return "搜索结果..."
-```
-
-### 创建 Function
-
-```python
-from src.components import component_function
-
-@component_function(
-    name="data_processor",
-    description="处理数据格式",
-    category="data_processing"
-)
-def process_data(input_data: dict, context: dict = None) -> dict:
-    \"\"\"处理数据格式\"\"\"
-    # 实现数据处理逻辑
-    return processed_data
-```
-
-### 组合使用
-
-```python
-from src.components import tool_registry, function_registry
-from langgraph.graph import StateGraph
-
-# 获取工具
-search_tools = tool_registry.get_tools_by_category("search")
-
-# 创建 Agent
-agent = DeconstructionAgent(tools=[tool.get_tool() for tool in search_tools])
-
-# 在工作流中使用
-def agent_node(state):
-    return agent.process(state)
-
-workflow = StateGraph(state_schema)
-workflow.add_node("agent", agent_node)
-```
-
-## 设计原则
-
-### 1. 单一职责
-每个组件只负责一个明确的功能,便于测试和维护。
-
-### 2. 接口统一
-所有同类型组件都实现相同的基础接口,便于替换和扩展。
-
-### 3. 配置灵活
-支持运行时配置,可以根据不同场景调整组件行为。
-
-### 4. 注册管理
-提供注册表机制,统一管理组件的生命周期。
-
-### 5. LangGraph 集成
-深度集成 LangGraph 框架,充分利用其状态管理和工作流能力。
-
-## 扩展指南
-
-### 添加新的 Agent 类型
-
-1. 继承相应的 BaseAgent 子类
-2. 实现必需的抽象方法
-3. 在工作流中使用
-
-### 添加新的 Tool
-
-1. 使用 `@component_tool` 装饰器
-2. 定义输入 schema (可选)
-3. 自动注册到 tool_registry
-
-### 添加新的 Function
-
-1. 使用 `@component_function` 装饰器
-2. 确保函数无副作用
-3. 自动注册到 function_registry
-
-## 最佳实践
-
-1. **命名规范**: 使用清晰、描述性的名称
-2. **文档字符串**: 为所有组件提供详细的文档
-3. **错误处理**: 实现适当的异常处理
-4. **日志记录**: 使用统一的日志记录机制
-5. **测试覆盖**: 为每个组件编写单元测试
-
-## 注意事项
-
-- Agent 必须基于 LangGraph 实现
-- Tool 需要加上 LangGraph 注解 (`@tool`)
-- Function 应该是纯函数,避免副作用
-- 所有组件都支持懒初始化
-- 注册表是全局单例,注意线程安全

+ 0 - 99
src/components/__init__.py

@@ -1,99 +0,0 @@
-"""
-组件层初始化模块
-
-提供所有组件类型的统一导入接口
-"""
-
-from .agents.base import (
-    BaseComponent,
-    BaseAgent,
-    BaseLLMAgent,
-    BaseReactAgent,
-    BaseGraphAgent
-)
-
-
-# 自动导入所有工具模块(这会触发工具注册)
-from . import tools
-
-from .tools.base import (
-    BaseToolComponent,
-    SimpleTool,
-    AsyncTool,
-    ConfigurableTool,
-    ToolRegistry,
-    tool_registry,
-    component_tool
-)
-
-from .functions.base import (
-    BaseFunction,
-    SimpleFunction,
-    ConfigurableFunction,
-    PipelineFunction,
-    ConditionalFunction,
-    FunctionRegistry,
-    function_registry,
-    component_function,
-    create_simple_function,
-    create_configurable_function,
-    create_pipeline
-)
-
-from .functions.json_utils import (
-    JSONParseError,
-    JsonSafeParseFunction,
-    safe_json_parse,
-    batch_json_parse,
-    validate_json_structure
-)
-
-__all__ = [
-    # Base classes
-    "BaseComponent",
-    "BaseAgent",
-    "BaseLLMAgent",
-    "BaseReactAgent",
-    "BaseGraphAgent",
-    "BaseToolComponent",
-    "SimpleTool",
-    "AsyncTool",
-    "ConfigurableTool",
-    "BaseFunction",
-    "SimpleFunction",
-    "ConfigurableFunction",
-    "PipelineFunction",
-    "ConditionalFunction",
-    
-    # Agent implementations
-    "PersonaAnalysisAgent",
-    "DeconstructionAgent",
-    "ValidationAgent",
-    "create_persona_analysis_agent",
-    "create_deconstruction_agent",
-    "create_validation_agent",
-    "persona_analysis_node",
-    "deconstruction_node",
-    "validation_node",
-    "validation_router",
-    
-    # Registries
-    "ToolRegistry",
-    "tool_registry",
-    "FunctionRegistry",
-    "function_registry",
-    
-    # Decorators and utilities
-    "component_tool",
-    "component_function",
-    "create_simple_function",
-    "create_configurable_function",
-    "create_pipeline",
-    
-    # JSON utilities
-    "JSONParseError",
-    "JsonSafeParseFunction",
-    "safe_json_parse",
-    "batch_json_parse",
-    "validate_json_structure"
-]

+ 0 - 38
src/components/agents/__init__.py

@@ -1,38 +0,0 @@
-"""
-Agent 组件包
-
-提供所有 Agent 组件的统一导入接口
-"""
-
-from .base import (
-    BaseComponent,
-    BaseAgent,
-    BaseLLMAgent,
-    BaseReactAgent,
-    BaseGraphAgent
-)
-
-__all__ = [
-    # Base classes
-    "BaseComponent",
-    "BaseAgent",
-    "BaseLLMAgent",
-    "BaseReactAgent",
-    "BaseGraphAgent",
-    
-    # Persona Analysis Agent
-    "PersonaAnalysisAgent",
-    "create_persona_analysis_agent",
-    "persona_analysis_node",
-    
-    # Deconstruction Agent
-    "DeconstructionAgent",
-    "create_deconstruction_agent",
-    "deconstruction_node",
-    
-    # Validation Agent
-    "ValidationAgent",
-    "create_validation_agent",
-    "validation_node",
-    "validation_router"
-]

+ 0 - 396
src/components/agents/base.py

@@ -1,396 +0,0 @@
-"""
-组件抽象层:Agent 基础抽象类
-
-定义不同类型 Agent 的抽象基类,支持基于 LangGraph 的多种实现模式
-"""
-
-from abc import ABC, abstractmethod
-from typing import Dict, Any, List, Optional, Union, TypeVar, Generic
-from langchain_core.language_models import BaseChatModel
-from langchain_core.tools import BaseTool
-from langchain_core.runnables import RunnableConfig
-from langgraph.graph import StateGraph
-from langgraph.types import Command
-from langchain_google_genai import ChatGoogleGenerativeAI, HarmCategory, HarmBlockThreshold
-from langchain_openai import ChatOpenAI
-from src.utils.logger import get_logger
-from langgraph.prebuilt import create_react_agent
-
-# 状态类型变量
-StateType = TypeVar('StateType', bound=Dict[str, Any])
-
-
-class BaseComponent(ABC):
-    """原子组件基类
-    
-    所有原子组件的最基础抽象,定义单一职责的组件接口
-    """
-    
-    def __init__(self, name: str, description: str = ""):
-        """初始化组件
-        
-        Args:
-            name: 组件名称,唯一标识
-            description: 组件描述
-        """
-        self.name = name
-        self.description = description
-        self._initialized = False
-    
-    @abstractmethod
-    def initialize(self) -> None:
-        """初始化组件,子类必须实现"""
-        pass
-    
-    @property
-    def is_initialized(self) -> bool:
-        """检查组件是否已初始化"""
-        return self._initialized
-    
-    def __str__(self) -> str:
-        return f"{self.__class__.__name__}(name='{self.name}')"
-    
-    def __repr__(self) -> str:
-        return self.__str__()
-
-
-class BaseAgent(BaseComponent, Generic[StateType]):
-    """Agent 基类
-    
-    定义所有 Agent 的通用接口和行为
-    """
-    
-    def __init__(
-        self, 
-        name: str, 
-        description: str = "",
-        model_provider: str = "google_genai",
-        tools: Optional[List[BaseTool]] = None
-    ):
-        """初始化 Agent
-        
-        Args:
-            name: Agent 名称
-            description: Agent 描述
-            model_provider: 模型提供商 ("openai" 或 "google_genai")
-            tools: Agent 可用的工具列表
-        """
-        super().__init__(name, description)
-        self.model_provider = model_provider
-        self.tools = tools or []
-        self.model: Optional[BaseChatModel] = None
-    
-    @abstractmethod
-    def process(self, state: StateType, config: Optional[RunnableConfig] = None) -> Union[StateType, Command]:
-        """处理状态
-        
-        Args:
-            state: 输入状态
-            config: 运行时配置
-            
-        Returns:
-            处理后的状态或命令对象
-        """
-        pass
-    
-    @abstractmethod
-    def _initialize_model(self) -> BaseChatModel:
-        """初始化模型,子类必须实现"""
-        pass
-    
-    def initialize(self) -> None:
-        """初始化 Agent"""
-        if not self._initialized:
-            self.model = self._initialize_model()
-            if self.tools:
-                self.model = self.model.bind_tools(self.tools)
-            self._initialized = True
-    
-    def add_tool(self, tool: BaseTool) -> None:
-        """添加工具
-        
-        Args:
-            tool: 要添加的工具
-        """
-        if tool not in self.tools:
-            self.tools.append(tool)
-            # 如果已初始化,需要重新绑定工具
-            if self._initialized and self.model:
-                self.model = self.model.bind_tools(self.tools)
-
-
-class BaseLLMAgent(BaseAgent[StateType]):
-    """基础 LLM Agent
-    
-    直接基于 LLM 的简单智能体,适用于单轮对话和简单任务
-    """
-    
-    def __init__(
-        self,
-        name: str,
-        description: str = "",
-        model_provider: str = "google_genai",
-        system_prompt: str = "",
-        temperature: float = 0.1,
-        max_tokens: int = 10240,
-        tools: Optional[List[BaseTool]] = None
-    ):
-        super().__init__(name, description, model_provider, tools)
-        self.system_prompt = system_prompt
-        self.temperature = temperature
-        self.max_tokens = max_tokens
-    
-    def _initialize_model(self) -> BaseChatModel:
-        """初始化 LLM 模型"""
-        import os
-
-        if self.model_provider == "google_genai":
-            # 使用直接的ChatGoogleGenerativeAI来避免认证问题
-
-            api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
-            if not api_key:
-                raise ValueError("GEMINI_API_KEY or GOOGLE_API_KEY not found in environment")
-
-            return ChatGoogleGenerativeAI(
-                model="gemini-2.5-flash",
-                google_api_key=api_key,
-                temperature=self.temperature,
-                max_tokens=self.max_tokens,
-                # 调整安全设置,避免正常内容被误拦截
-                safety_settings={
-                    HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
-                    HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_ONLY_HIGH,
-                    HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_ONLY_HIGH,
-                    HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_ONLY_HIGH
-                }
-            )
-        else:
-            # OpenAI模型
-
-            return ChatOpenAI(
-                model="gpt-4o",
-                temperature=self.temperature,
-                max_tokens=self.max_tokens
-            )
-    
-    def process(self, state: StateType, config: Optional[RunnableConfig] = None) -> StateType:
-        """处理状态 - LLM 直接处理"""
-        if not self.is_initialized:
-            self.initialize()
-
-        # 构建消息
-        messages = self._build_messages(state)
-
-        # 调用模型(添加错误处理)
-        try:
-            response = self.model.invoke(messages)
-
-            # 检查响应有效性
-            if not response:
-                raise ValueError("LLM returned None response")
-
-            if hasattr(response, 'content'):
-                if not response.content or response.content.strip() == "":
-                    raise ValueError(f"LLM returned empty content {response}")
-
-        except Exception as e:
-            # 记录错误并返回带错误标记的状态
-            logger = get_logger(self.name)
-            logger.error(f"LLM调用失败: {e}", exc_info=True)
-
-            # 创建一个空响应对象
-            class EmptyResponse:
-                content = ""
-            response = EmptyResponse()
-
-        # 更新状态
-        return self._update_state(state, response)
-    
-    @abstractmethod
-    def _build_messages(self, state: StateType) -> List[Dict[str, Any]]:
-        """构建消息,子类实现具体逻辑"""
-        """
-        文本消息和图片消息可以交织,以下是一个例子
-        message_content = [
-            {"type": "text", "text": text_prompt},  # 文本类型消息
-            ...
-            {"type": "image_url", "image_url": {"url": image_url}}, # 图片类型消息
-            ...
-            {"type": "image_url", "image_url": {"url": image_url}}, # 图片类型消息
-            {"type": "text", "text": text_prompt},  # 文本类型消息
-            ...
-        ]
-        """
-
-    
-    @abstractmethod
-    def _update_state(self, state: StateType, response: Any) -> StateType:
-        """更新状态,子类实现具体逻辑"""
-        pass
-
-
-class BaseReactAgent(BaseAgent[StateType]):
-    """基础 ReAct Agent
-    
-    基于 LangGraph 的 ReAct 模式智能体,支持工具调用和推理
-    """
-    
-    def __init__(
-        self,
-        name: str,
-        description: str = "",
-        model_provider: str = "google_genai",
-        prompt: Union[str, callable] = "",
-        tools: Optional[List[BaseTool]] = None
-    ):
-        super().__init__(name, description, model_provider, tools)
-        self.prompt = prompt
-        self.react_agent = None
-    
-    def _initialize_model(self) -> BaseChatModel:
-        """初始化模型"""
-        import os
-
-        if self.model_provider == "google_genai":
-            # 使用直接的ChatGoogleGenerativeAI来避免认证问题
-
-            api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
-            if not api_key:
-                raise ValueError("GEMINI_API_KEY or GOOGLE_API_KEY not found in environment")
-
-            return ChatGoogleGenerativeAI(
-                model="gemini-2.5-flash",
-                google_api_key=api_key,
-                temperature=0.1,
-                max_tokens=10240,
-                # 调整安全设置,避免正常内容被误拦截
-                safety_settings={
-                    HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
-                    HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_ONLY_HIGH,
-                    HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_ONLY_HIGH,
-                    HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_ONLY_HIGH
-                }
-            )
-        else:
-
-            return ChatOpenAI(
-                model="gpt-4o",
-                temperature=0.1,
-                max_tokens=10240
-            )
-    
-    def initialize(self) -> None:
-        """初始化 ReAct Agent"""
-        if not self._initialized:
-            super().initialize()
-            # 创建 ReAct Agent
-            
-            self.react_agent = create_react_agent(
-                self.model,
-                tools=self.tools,
-                prompt=self.prompt if self.prompt else None
-            )
-            self._initialized = True
-    
-    def process(self, state: StateType, config: Optional[RunnableConfig] = None) -> StateType:
-        """处理状态 - 使用 ReAct Agent"""
-        if not self.is_initialized:
-            self.initialize()
-        
-        # 转换状态为消息格式
-        messages = self._extract_messages(state)
-        
-        # 调用 ReAct Agent
-        response = self.react_agent.invoke({"messages": messages}, config=config)
-        
-        # 更新状态
-        return self._merge_response(state, response)
-    
-    @abstractmethod
-    def _extract_messages(self, state: StateType) -> List[Dict[str, Any]]:
-        """从状态中提取消息,子类实现"""
-        pass
-    
-    @abstractmethod
-    def _merge_response(self, state: StateType, response: Dict[str, Any]) -> StateType:
-        """合并响应到状态,子类实现"""
-        pass
-
-
-class BaseGraphAgent(BaseAgent[StateType]):
-    """基础 Graph Agent
-    
-    基于 StateGraph 的复杂智能体,支持多节点工作流
-    """
-    
-    def __init__(
-        self,
-        name: str,
-        description: str = "",
-        state_class: type = dict
-    ):
-        super().__init__(name, description)
-        self.state_class = state_class
-        self.graph: Optional[StateGraph] = None
-        self.compiled_graph = None
-    
-    def _initialize_model(self) -> BaseChatModel:
-        """Graph Agent 不需要单一模型"""
-        return None
-    
-    def initialize(self) -> None:
-        """初始化 Graph Agent"""
-        if not self._initialized:
-            self.graph = self._build_graph()
-            self.compiled_graph = self.graph.compile()
-            self._initialized = True
-    
-    @abstractmethod
-    def _build_graph(self) -> StateGraph:
-        """构建图结构,子类必须实现"""
-        pass
-    
-    def process(self, state: StateType, config: Optional[RunnableConfig] = None) -> StateType:
-        """处理状态 - 使用编译的图"""
-        if not self.is_initialized:
-            self.initialize()
-        
-        result = self.compiled_graph.invoke(state, config=config)
-        return result
-    
-    def add_node(self, name: str, node_func: callable) -> None:
-        """添加节点到图中"""
-        if self.graph is None:
-            self.graph = StateGraph(self.state_class)
-        self.graph.add_node(name, node_func)
-        
-        # 重新编译
-        if self._initialized:
-            self.compiled_graph = self.graph.compile()
-    
-    def add_edge(self, from_node: str, to_node: str) -> None:
-        """添加边到图中"""
-        if self.graph is None:
-            raise ValueError("Graph not initialized. Call initialize() first.")
-        
-        self.graph.add_edge(from_node, to_node)
-        
-        # 重新编译
-        if self._initialized:
-            self.compiled_graph = self.graph.compile()
-    
-    def add_conditional_edges(
-        self, 
-        from_node: str, 
-        condition_func: callable, 
-        condition_map: Dict[str, str]
-    ) -> None:
-        """添加条件边到图中"""
-        if self.graph is None:
-            raise ValueError("Graph not initialized. Call initialize() first.")
-        
-        self.graph.add_conditional_edges(from_node, condition_func, condition_map)
-        
-        # 重新编译
-        if self._initialized:
-            self.compiled_graph = self.graph.compile()

+ 0 - 222
src/components/agents/content_unit_split_agent.py

@@ -1,222 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-内容单元拆分Agent
-
-功能:将视频脚本拆解为L3单元(Units),并按照严格的"实质 × 形式"矩阵进行分析
-"""
-
-from typing import Any, Dict, List
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker, get_video_file_from_state
-
-logger = get_logger(__name__)
-
-
-class ContentUnitSplitAgent(BaseLLMAgent):
-    """内容单元拆分Agent - 将视频脚本拆解为L3单元并进行实质×形式矩阵分析"""
-
-    def __init__(
-        self,
-        name: str = "content_unit_split_agent",
-        description: str = "内容单元拆分Agent - 将视频脚本拆解为L3单元并进行实质×形式矩阵分析",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 40960
-    ):
-        """初始化内容单元拆分Agent
-
-        Args:
-            name: Agent名称
-            description: Agent描述
-            model_provider: 模型提供商
-            temperature: 生成温度(较低,保证准确性)
-            max_tokens: 最大token数(较大,因为需要详细分析)
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是一名精通传播学与视听语言的内容分析师,擅长对视频脚本进行微观层面的原子级解构。"""
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态 - 对视频脚本进行L3单元拆分和实质×形式分析(JSON 结构)
-
-        Args:
-            state: 包含视频文件的状态字典
-
-        Returns:
-            包含分析结果的字典,格式为(全部中文 key,便于后续解析):
-            {
-                "content_unit_analysis": {
-                    "单元列表": [
-                        {
-                            "单元编号": int,
-                            "时间范围": str,
-                            "单元核心概括": str,
-                            "完整文案": str,
-                            "实质": {
-                                "具体元素": {...},
-                                "具象概念": {...},
-                                "抽象概念": {...}
-                            }
-                        },
-                        ...
-                    ]
-                }
-            }
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("=" * 80)
-        logger.info("内容单元拆分 (ContentUnitSplitAgent)")
-        logger.info("=" * 80)
-
-        try:
-            # 从 state 中获取视频文件对象
-            video_file = get_video_file_from_state(state)
-            if not video_file:
-                logger.warning("无法获取视频文件对象,跳过内容单元拆分分析")
-                return {
-                    "content_unit_analysis": {
-                        "error": "无法获取视频文件对象",
-                        "单元列表": []
-                    }
-                }
-
-            # 构建分析提示词
-            prompt = self._build_analysis_prompt()
-
-            logger.info("开始视频脚本分析和L3单元拆分...")
-
-            # 使用通用的 safe_invoke_video_analysis 进行视频分析
-            result = LLMInvoker.safe_invoke_video_analysis(
-                operation_name="内容单元拆分分析",
-                video_file=video_file,
-                prompt=prompt,
-                agent=self,
-                fallback={
-                    "error": "视频分析返回空结果",
-                    "单元列表": []
-                }
-            )
-
-            # 期望 result 为已解析的 JSON 对象(dict),包含「单元列表」等中文 key
-            if not isinstance(result, dict) or not result:
-                logger.warning("视频分析返回结果为空或非字典,将返回降级结构")
-                return {
-                    "content_unit_analysis": {
-                        "error": "视频分析返回空结果或解析失败",
-                        "单元列表": []
-                    }
-                }
-
-            unit_list = result.get("单元列表", [])
-            logger.info(f"分析完成,单元数量: {len(unit_list)}")
-            logger.info("=" * 80)
-
-            return {
-                "content_unit_analysis": result
-            }
-
-        except Exception as e:
-            logger.error(f"内容单元拆分失败: {e}", exc_info=True)
-            return {
-                "content_unit_analysis": {
-                    "error": str(e),
-                    "单元列表": []
-                }
-            }
-
-    def _build_analysis_prompt(self) -> str:
-        """构建分析提示词(要求直接输出 JSON,全部中文 key)"""
-        return """# Role
-你是一名精通传播学与视听语言的内容分析师,擅长对视频脚本进行微观层面的原子级解构。
-
-# Task
-请阅读我提供的视频,将其拆解为一个个独立的「L3 单元(Units)」,并按照严格的「实质 × 形式」矩阵进行分析,并**以 JSON 结构输出结果(全部使用中文 key)**。
-
-# Definitions
-1.  **L3 单元 (Unit)**:视频中最小的语义单位,通常是一个完整的句子或一个短语意群。
-2.  **实质 (Substance)**:单元内容所包含的信息内核,分为三层:
-    - 实质A-具体元素:人、物、地名、数据等实体。
-    - 实质B-具象概念:动作、状态、反应、感官体验等。
-    - 实质C-抽象概念:价值观、情绪、地位、评价等。
-
-3.  **形式 (Form)**:承载实质的表现手段,分为三维:
-    - 文案形式:修辞、句式、语态、词汇选择。
-    - 画面形式:景别、色彩、构图、字幕特效、运镜(若脚本未提及,请根据文案逻辑合理推断常规操作)。
-    - 声音形式:语速、语调、BGM情绪、音效(若脚本未提及,请根据文案逻辑合理推断)。
-
-# Constraints
-- **必须严格对应**:分析「具体元素」时只能谈论呈现该元素的形式,不可混淆。
-- **完整性**:覆盖脚本的每一秒,不要遗漏任何信息。
-- **客观性**:形式分析必须基于视听语言学,而非主观臆测。
-
-# Workflow
-1.  按时间顺序切分脚本。
-2.  逐个单元填写解构表。
-
-# Output Format (JSON)
-请最终只输出一个 JSON 对象,**全部使用中文 key**,结构示例如下(可根据实际需要增减字段,但需保持语义清晰、便于解析):
-
-{
-  "单元列表": [
-    {
-      "单元编号": 1,
-      "时间范围": "00:00-00:05",
-      "单元核心概括": "本单元在讲的核心意思简要概括",
-      "完整文案": "该单元对应的完整字幕或口播文案",
-      "实质": {
-        "具体元素": {
-          "关键词": ["人物A", "地点B", "物品C"],
-          "对应形式": {
-            "文案形式": "针对具体元素的文案表现分析",
-            "画面形式": "针对具体元素的画面表现分析",
-            "声音形式": "针对具体元素的声音表现分析"
-          }
-        },
-        "具象概念": {
-          "关键词": ["动作D", "状态E"],
-          "对应形式": {
-            "文案形式": "针对具象概念的文案表现分析",
-            "画面形式": "针对具象概念的画面表现分析",
-            "声音形式": "针对具象概念的声音表现分析"
-          }
-        },
-        "抽象概念": {
-          "关键词": ["情绪F", "价值观G"],
-          "对应形式": {
-            "文案形式": "针对抽象概念的文案表现分析",
-            "画面形式": "针对抽象概念的画面表现分析",
-            "声音形式": "针对抽象概念的声音表现分析"
-          }
-        }
-      }
-    }
-  ]
-}
-
-要求:
-- 只输出一个 JSON 对象,不要输出任何其他说明文字。
-- 所有字段名必须是中文(如:单元列表、时间范围、完整文案、具体元素、文案形式 等)。
-- 确保 JSON 语法合法,便于程序直接解析和使用。"""
-
-    def _build_messages(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """构建消息 - ContentUnitSplitAgent 在 process 方法中直接处理"""
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """更新状态 - ContentUnitSplitAgent 在 process 方法中直接返回结果"""
-        return state

+ 0 - 255
src/components/agents/content_unit_understand.py

@@ -1,255 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-内容整体结构理解 Agent(基于视频 + L3 单元解构)
-
-功能:
-- 读取原始视频文件(Gemini File)
-- 使用 L3 单元解构结果(来自 ContentUnitSplitAgent 或其他步骤)
-- 调用视频模型,根据统一的 Prompt,生成:
-  - L1 整体结构(Whole)
-  - L2 段落结构(Paragraphs)
-  - 保留 L3 单元解构(Units)
-"""
-
-import json
-from typing import Any, Dict
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker, get_video_file_from_state
-
-logger = get_logger(__name__)
-
-
-class ContentUnitUnderstandAgent(BaseLLMAgent):
-    """内容整体结构理解 Agent
-
-    输入(当前版本不再依赖脚本文本,只使用「视频 + L3 解构 JSON」):
-        state: {
-            # 必需:视频信息(与视频工作流保持一致)
-            #   - video_file / video_file_name / video_uploaded_uri: Gemini 视频文件信息
-            #
-            # 必需:L3 单元解构资料(例如来自 ContentUnitSplitAgent 的输出)
-            #   - content_unit_analysis: {
-            #         "单元列表": [...],
-            #         ...
-            #     }
-        }
-
-    输出(JSON 结构,全部中文 key,便于后续解析):
-        {
-            "content_unit_understanding": {
-                "整体解构": {...},    # L1 层整体结构信息
-                "段落解构": [...],    # L2 段落级结构列表
-                "单元解构": {...}     # 可选:直接附上 L3 解构的摘要或引用
-            }
-        }
-    """
-
-    def __init__(
-        self,
-        name: str = "content_unit_understand_agent",
-        description: str = "内容整体结构理解Agent - 基于视频与L3单元资料,归纳L2段落与L1整体结构",
-        model_provider: str = "google_genai",
-        temperature: float = 0.3,
-        max_tokens: int = 40960,
-    ):
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens,
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词(角色与总体要求)"""
-        return (
-            "你是一名世界级的内容架构师,擅长从微观单元出发,归纳视频的整体叙事结构。"
-            "你需要在保证逻辑严谨与结构清晰的前提下,输出可直接阅读的中文 Markdown 报告。"
-        )
-
-    # 注意:虽然本 Agent 在自定义的 process 中直接调用了视频分析工具,
-    # 但由于继承自 BaseLLMAgent(其定义了抽象方法),这里仍需提供占位实现,
-    # 以避免被视为抽象类无法实例化。
-
-    def _build_messages(self, state: Dict[str, Any]) -> Any:
-        """占位实现:本 Agent 不使用 BaseLLMAgent 默认的 LLM 调用路径。"""
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """占位实现:实际状态更新逻辑在自定义 process 中完成。"""
-        return state
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态 - 基于视频 + L3 单元资料进行整体结构归纳分析"""
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("=" * 80)
-        logger.info("内容整体结构理解 (ContentUnitUnderstandAgent)")
-        logger.info("=" * 80)
-
-        try:
-            # 1. 获取视频文件对象
-            video_file = get_video_file_from_state(state)
-            if not video_file:
-                logger.warning("无法获取视频文件对象,跳过整体结构理解分析")
-                return {
-                    "content_unit_understanding": {
-                        "error": "无法获取视频文件对象",
-                        "markdown_result": "",
-                        "raw_response": "",
-                    }
-                }
-
-            # 2. 获取 L3 单元解构资料
-            content_unit_analysis = state.get("content_unit_analysis", {}) or {}
-
-            if not content_unit_analysis:
-                logger.warning("缺少 L3 单元解构资料(content_unit_analysis.单元列表 为空)")
-
-            # 3. 构建分析 Prompt(仅依赖 L3 解构和视频内容)
-            prompt = self._build_analysis_prompt(content_unit_analysis=content_unit_analysis)
-
-            logger.info("开始视频整体结构理解分析(调用 safe_invoke_video_analysis)...")
-
-            # 5. 调用通用视频分析接口
-            result = LLMInvoker.safe_invoke_video_analysis(
-                operation_name="内容整体结构理解分析",
-                video_file=video_file,
-                prompt=prompt,
-                agent=self,
-                fallback={
-                    "error": "视频整体结构分析返回空结果",
-                },
-            )
-
-            # 6. 处理返回结果(safe_invoke_video_analysis 返回的是解析后的 JSON 对象)
-            if not isinstance(result, dict) or not result:
-                logger.warning("视频整体结构分析返回结果为空或非字典")
-                return {
-                    "content_unit_understanding": {
-                        "error": "视频整体结构分析返回空结果或解析失败",
-                    }
-                }
-
-            # 检查是否是错误返回(fallback)
-            if "error" in result and result.get("error"):
-                logger.warning(f"视频整体结构分析返回错误: {result.get('error')}")
-                return {
-                    "content_unit_understanding": {
-                        "error": result.get("error"),
-                    }
-                }
-
-            # 返回解析后的 JSON 对象(包含"整体解构"、"段落解构"、"单元解构"等中文 key)
-            logger.info(f"整体结构分析完成,结果包含字段: {list(result.keys())}")
-            logger.info("=" * 80)
-
-            return {
-                "content_unit_understanding": result
-            }
-
-        except Exception as e:
-            logger.error(f"内容整体结构理解分析失败: {e}", exc_info=True)
-            return {
-                "content_unit_understanding": {
-                    "error": str(e),
-                    "markdown_result": "",
-                    "raw_response": "",
-                }
-            }
-
-    def _build_analysis_prompt(self, content_unit_analysis: Dict[str, Any]) -> str:
-        """构建用于视频分析的完整 Prompt,将 L3 资料填入占位符"""
-
-        # 将字典转换为 JSON 字符串,如果为空则使用默认提示
-        if content_unit_analysis:
-            l3_content = json.dumps(content_unit_analysis, ensure_ascii=False, indent=2)
-        else:
-            l3_content = '(当前缺少 L3 解构资料,请仅基于视频内容进行合理推断)'
-
-        l3_block = (
-            "【L3 解构资料】(来自内容单元拆分结果):\n\n"
-            f"{l3_content}\n"
-        )
-
-        logger.info(f"L3 解构资料长度: {len(l3_content)}")
-
-        return f"""# Role
-你是一名世界级的内容架构师。你拥有极强的归纳总结能力和宏观叙事分析能力。
-# Context
-我将给你一份材料:
-1. **L3 单元解构资料**:对该脚本进行的微观拆解数据。
-
-以下是具体内容(如果某部分为空,请忽略该部分,优先结合“视频实际内容 + 现有文字资料”进行分析,这里的文字资料仅指 L3 解构文本): 
-
-{l3_block}
-
-# Task
-
-请结合上述材料和你直接“观看视频”获得的理解,自下而上地归纳出该视频的 **L2 段落(Paragraphs)** 和 **L1 整体(Whole)** 结构,并最终整合成一份完整的解构报告。
-
-# Logic
-1.  **L2 段落 (Paragraphs)**:将相邻的、服务于同一个叙事功能的 L3 单元聚合。
-    - 需要明确该段落包含哪些 L3 单元。
-    - 需要对该段落的“实质(三层)× 形式(三维)”进行聚合总结。
-
-2.  **L1 整体 (Whole)**:对整个视频的叙事脉络进行宏观定义。
-    - 重点分析“纵向逻辑流”:包含整体叙事脉络(阶段划分)和节点衔接机制(段落间如何过渡)。
-
-# Output Format (JSON)
-请最终只输出一个 JSON 对象,**全部使用中文 key**,结构示例如下(可根据实际需要增减字段,但需保持语义清晰、便于解析):
-
-{{
-  "整体解构": {{
-    "节点基础信息": "整体节点信息",
-    "整体实质×形式": "整体实质×形式分析",
-    "纵向逻辑流": [
-        {{
-            "阶段编号": "阶段编号",
-            "阶段逻辑名称": "阶段逻辑名称",
-            "阶段逻辑描述": "阶段逻辑描述",
-        }}
-    ]
-  }},
-  "段落解构": [
-    {{
-      "段落序号": "段落序号",
-      "时间范围": "段落时间范围",
-      "包含单元": "段落包含的单元列表",
-      "段落完整文案": "段落完整文案",
-      "具体元素实质和形式": [
-        {{
-            "具体元素名称": "具体元素名称",
-            "对应形式-文案": "文案形式",
-            "对应形式-画面": "画面形式",
-            "对应形式-声音": "声音形式"
-        }}
-      ],
-      "具象概念实质和形式": [
-        {{
-            "具象概念名称": "具象概念名称",
-            "对应形式-文案": "文案形式",
-            "对应形式-画面": "画面形式",
-            "对应形式-声音": "声音形式"
-        }}
-      ],
-      "抽象概念实质和形式": [
-        {{
-            "抽象概念名称": "抽象概念名称",
-            "对应形式-文案": "文案形式",
-            "对应形式-画面": "画面形式",
-            "对应形式-声音": "声音形式"
-        }}
-      ],
-    }}
-  ]
-}}"""
-
-
-

+ 0 - 347
src/components/agents/evaluate_agent.py

@@ -1,347 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-评估Agent
-
-功能: 从待评估的视频列表中筛选出和原视频最匹配的内容
-核心任务: 根据原视频的解构内容,对每个候选视频进行相关性评分和筛选
-特征: 使用LLM进行相关性评估,输出评分和是否入选
-"""
-
-from typing import Any, Dict, List
-import json
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker
-
-logger = get_logger(__name__)
-
-
-class EvaluateAgent(BaseLLMAgent):
-    """评估Agent - 筛选和原视频最匹配的视频"""
-
-    def __init__(
-        self,
-        name: str = "evaluate_agent",
-        description: str = "评估Agent - 筛选和原视频最匹配的视频",
-        model_provider: str = "google_genai",
-        temperature: float = 0.3,
-        max_tokens: int = 20480
-    ):
-        """
-        初始化评估Agent
-
-        Args:
-            name: Agent名称
-            description: Agent描述
-            model_provider: 模型提供商 ("openai" 或 "google_genai")
-            temperature: 生成温度(较低,保持客观性)
-            max_tokens: 最大token数
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是内容分析专家,擅长评估视频内容的相关性和匹配度。
-
-# 任务
-根据原视频的解构内容(包括标题、灵感点、目的点、关键点、选题理解等),对候选视频列表进行相关性评估和筛选。
-
-# 评估维度
-1. **内容主题匹配度**:候选视频的主题是否与原视频的主题一致或相关
-2. **切入点相似度**:候选视频的切入角度是否与原视频相似
-3. **受众重合度**:候选视频的目标受众是否与原视频重合
-4. **表现形式相似度**:候选视频的表现形式(风格、结构等)是否与原视频相似
-
-# 评分标准
-- 相关性得分范围:0-100分
-- 90-100分:高度相关,主题、切入点、受众、表现形式都高度匹配
-- 70-89分:相关,在多个维度上匹配
-- 50-69分:中等相关,在某些维度上匹配
-- 30-49分:低相关,匹配度较低
-- 0-29分:不相关,基本不匹配
-
-# 输出要求
-1. 对每个候选视频进行评分(0-100分)
-2. 根据评分排序,默认选前50%进入候选
-3. 输出结果保持与输入列表相同的顺序和字段,新增两个字段:
-   - relevance_score: 相关性得分(0-100)
-   - is_selected: 是否入选(true/false)
-"""
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态 - 评估视频相关性并筛选
-        
-        Args:
-            state: 状态字典,包含:
-                - original_video_title: 原视频标题
-                - original_video_content: 原视频解构内容(JSON格式)
-                - search_result: 待评估的视频列表
-                
-        Returns:
-            更新后的状态,包含:
-                - evaluate_result: 评估结果列表(每个视频包含原始字段 + relevance_score + is_selected)
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("开始评估视频相关性")
-
-        try:
-            # 从state获取数据
-            original_video_title = state.get("original_video_title", "")
-            original_video_content = state.get("original_video_content", {})
-            search_result = state.get("search_result", [])
-
-            if not search_result:
-                logger.warning("待评估的视频列表为空")
-                return {
-                    "evaluate_result": []
-                }
-
-            if not original_video_title and not original_video_content:
-                logger.warning("原视频信息为空,无法进行评估")
-                return {
-                    "evaluate_result": []
-                }
-
-            # 构建评估提示词
-            prompt = self._build_evaluate_prompt(
-                original_video_title,
-                original_video_content,
-                search_result
-            )
-
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {"role": "user", "content": prompt}
-            ]
-
-            # 调用LLM进行评估
-            result = LLMInvoker.safe_invoke(
-                self,
-                "视频相关性评估",
-                messages,
-                fallback={"评估结果": []}
-            )
-
-            # 提取评估结果
-            evaluate_result = result.get("评估结果", [])
-            
-            # 如果LLM返回的结果数量与输入不一致,进行修正
-            if len(evaluate_result) != len(search_result):
-                logger.warning(
-                    f"LLM返回结果数量({len(evaluate_result)})与输入数量({len(search_result)})不一致,"
-                    "将进行修正"
-                )
-                evaluate_result = self._fix_evaluate_result(search_result, evaluate_result)
-
-            # 确保每个结果都有relevance_score和is_selected字段
-            evaluate_result = self._ensure_evaluate_fields(search_result, evaluate_result)
-
-            # 根据评分排序并标记前50%为入选
-            evaluate_result = self._mark_selected_videos(evaluate_result)
-
-            logger.info(f"评估完成,共评估{len(evaluate_result)}个视频")
-
-            return {
-                "evaluate_result": evaluate_result
-            }
-
-        except Exception as e:
-            logger.error(f"视频评估失败: {e}", exc_info=True)
-            # 返回原始列表,但添加默认的评分和选择状态
-            search_result = state.get("search_result", [])
-            return {
-                "evaluate_result": [
-                    {**video, "relevance_score": 0, "is_selected": False}
-                    for video in search_result
-                ]
-            }
-
-    def _build_evaluate_prompt(
-        self,
-        original_video_title: str,
-        original_video_content: Dict[str, Any],
-        search_result: List[Dict[str, Any]]
-    ) -> str:
-        """构建评估提示词"""
-        
-        # 格式化原视频内容
-        content_str = json.dumps(original_video_content, ensure_ascii=False, indent=2)
-        
-        # 格式化候选视频列表
-        candidates_text = ""
-        for i, video in enumerate(search_result, 1):
-            video_str = json.dumps(video, ensure_ascii=False, indent=2)
-            candidates_text += f"\n## 候选视频 {i}\n{video_str}\n"
-
-        prompt = f"""# 任务:评估视频相关性
-
-## 原视频信息
-
-### 标题
-{original_video_title}
-
-### 解构内容
-{content_str}
-
-## 候选视频列表
-{candidates_text}
-
-## 评估要求
-
-1. **对每个候选视频进行相关性评分**(0-100分)
-   - 考虑内容主题匹配度、切入点相似度、受众重合度、表现形式相似度
-   - 评分要客观、准确
-
-2. **输出格式要求**
-   - 保持与输入列表相同的顺序
-   - 保留原始字段不变
-   - 新增两个字段:
-     - `relevance_score`: 相关性得分(整数,0-100)
-     - `is_selected`: 是否入选(布尔值,暂时设为false,后续会根据评分排序后标记前50%)
-
-## 输出格式(JSON)
-
-```json
-{{
-  "评估结果": [
-    {{
-      // 保留原始字段...
-      "relevance_score": 85,
-      "is_selected": false
-    }}
-  ]
-}}
-```
-
-**重要**:
-- 输出结果的数量必须与输入列表的数量完全一致
-- 每个结果必须包含所有原始字段
-- 每个结果必须包含relevance_score和is_selected字段
-"""
-
-        return prompt
-
-    def _fix_evaluate_result(
-        self,
-        original_list: List[Dict[str, Any]],
-        llm_result: List[Dict[str, Any]]
-    ) -> List[Dict[str, Any]]:
-        """修正评估结果,确保数量一致"""
-        fixed_result = []
-        
-        # 创建LLM结果的索引(通过某些唯一字段匹配)
-        llm_result_map = {}
-        for item in llm_result:
-            # 尝试通过video_id或其他唯一字段匹配
-            video_id = item.get("video_id") or item.get("id") or item.get("videoId")
-            if video_id:
-                llm_result_map[str(video_id)] = item
-        
-        # 遍历原始列表,匹配LLM结果
-        for i, original in enumerate(original_list):
-            video_id = original.get("video_id") or original.get("id") or original.get("videoId")
-            if video_id and str(video_id) in llm_result_map:
-                # 找到匹配的结果,合并字段
-                matched = llm_result_map[str(video_id)]
-                fixed_item = {**original, **matched}
-                fixed_result.append(fixed_item)
-            elif i < len(llm_result):
-                # 按索引匹配
-                matched = llm_result[i]
-                fixed_item = {**original, **matched}
-                fixed_result.append(fixed_item)
-            else:
-                # 没有匹配的结果,使用原始数据并添加默认评分
-                fixed_item = {**original, "relevance_score": 0, "is_selected": False}
-                fixed_result.append(fixed_item)
-        
-        return fixed_result
-
-    def _ensure_evaluate_fields(
-        self,
-        original_list: List[Dict[str, Any]],
-        evaluate_result: List[Dict[str, Any]]
-    ) -> List[Dict[str, Any]]:
-        """确保每个评估结果都有必要的字段"""
-        ensured_result = []
-        
-        for i, original in enumerate(original_list):
-            if i < len(evaluate_result):
-                item = evaluate_result[i]
-                # 合并原始字段和评估字段
-                merged_item = {**original}
-                
-                # 确保有relevance_score
-                if "relevance_score" in item:
-                    merged_item["relevance_score"] = item["relevance_score"]
-                else:
-                    merged_item["relevance_score"] = 0
-                
-                # 确保有is_selected(暂时设为false,后续会重新标记)
-                merged_item["is_selected"] = False
-                
-                ensured_result.append(merged_item)
-            else:
-                # 如果LLM结果不足,使用原始数据并添加默认值
-                ensured_result.append({
-                    **original,
-                    "relevance_score": 0,
-                    "is_selected": False
-                })
-        
-        return ensured_result
-
-    def _mark_selected_videos(self, evaluate_result: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
-        """根据评分排序并标记前50%为入选,然后恢复原始顺序"""
-        if not evaluate_result:
-            return evaluate_result
-        
-        # 为每个视频添加临时索引,以便后续恢复原始顺序
-        indexed_result = [
-            {**item, "_original_index": i}
-            for i, item in enumerate(evaluate_result)
-        ]
-        
-        # 按评分降序排序
-        sorted_result = sorted(
-            indexed_result,
-            key=lambda x: x.get("relevance_score", 0),
-            reverse=True
-        )
-        
-        # 计算前50%的数量(向上取整)
-        selected_count = max(1, (len(sorted_result) + 1) // 2)
-        
-        # 标记前50%为入选
-        for i, item in enumerate(sorted_result):
-            item["is_selected"] = (i < selected_count)
-        
-        # 恢复原始顺序
-        sorted_result.sort(key=lambda x: x.get("_original_index", len(evaluate_result)))
-        
-        # 移除临时索引
-        for item in sorted_result:
-            item.pop("_original_index", None)
-        
-        return sorted_result
-
-    def _build_messages(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """构建消息 - BaseLLMAgent要求实现(本Agent不使用此方法)"""
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """更新状态 - BaseLLMAgent要求实现(本Agent不使用此方法)"""
-        return state
-

+ 0 - 903
src/components/agents/inspiration_points_agent.py

@@ -1,903 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-灵感点提取Agent
-"""
-
-from typing import Any, Dict, List, TYPE_CHECKING
-import json
-
-if TYPE_CHECKING:
-    from google import genai
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker
-
-logger = get_logger(__name__)
-
-LOG_SEPARATOR = "=" * 80
-
-# 配置常量
-HISTORICAL_POSTS_BATCH_SIZE = 5  # 每批处理的历史帖子数量
-MIN_QUALITY_SCORE = 15  # 最低质量分数阈值(总分3-30)
-
-# ==================== 数据结构定义 ====================
-
-
-class InspirationPointsAgent(BaseLLMAgent):
-    """灵感点提取Agent - 从创作者视角提取触发创作的刺激源
-    """
-
-    def __init__(
-        self,
-        name: str = "inspiration_points_agent",
-        description: str = "灵感点提取Agent - 从创作者视角提取小众、低概率、原发的创作灵感",
-        model_provider: str = "google_genai",
-        temperature: float = 0.7,
-        max_tokens: int = 20480
-    ):
-        """
-        初始化灵感点提取Agent
-
-        Args:
-            name: Agent名称
-            description: Agent描述
-            model_provider: 模型提供商
-            temperature: 生成温度,控制创造性
-            max_tokens: 最大token数
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词 - 只包含所有steps共享的核心信息"""
-        return """你是创作灵感分析专家,从创作者视角逆向推导触发创作的深层刺激源。
-
-# 灵感点定义(核心概念)
-**灵感点** = 创作前的触发源(让创作者产生创作冲动的刺激)
-- 区分:灵感点(触发源)≠ 目的点(动机)≠ 关键点(吸引点)
-- 验证:能否从这个灵感点推导出整个视频内容
-
-# 核心原则(所有分析遵循)
-- 透过表面内容挖掘深层心理动机、行为模式、价值观冲突
-- 灵感点可以来自视频的任何部分,包括画面、动作、场景、对话等
-- 视频的画面和音频同样重要,要捕捉动态变化和细节"""
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态 - 多步骤提取灵感点(视频分析版本)
-
-        流程:
-        1. 识别候选灵感点(逐帧分析视频)
-        2. 逆向推导验证
-        3. 去重检查
-        4. 三维度评分
-        5. 排序筛选
-        6. 组装最终输出
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("灵感点分析开始")
-
-        try:
-            # 从 state 中获取视频URL和文本信息
-            video_uri = state.get("video_uploaded_uri", "")
-            video_upload_error = state.get("video_upload_error")
-            video_url = state.get("video", "")
-            # 优先使用原始URL,如果没有则使用上传后的URI
-            video_url_for_prompt = video_url if video_url else video_uri
-            text_data = {
-                "title": state.get("title", ""),
-                "body": state.get("body_text", "")
-            }
-
-            if not video_uri:
-                # 检查是否有上传错误信息
-                error_msg = "未找到视频URI,无法进行分析"
-                if video_upload_error:
-                    error_msg += f"。视频上传失败: {video_upload_error}"
-                elif video_url:
-                    error_msg += f"。原始视频URL: {video_url}"
-                else:
-                    error_msg += "。未提供视频URL"
-                
-                logger.error(error_msg)
-                return {
-                    "inspiration_points": [],
-                    "metadata": {
-                        "error": error_msg,
-                        "video_upload_error": video_upload_error,
-                        "video_url": video_url,
-                        "total_count": 0
-                    }
-                }
-
-            # 从 state 获取视频文件对象
-            from src.utils.llm_invoker import get_video_file_from_state
-            video_file = get_video_file_from_state(state)
-            if not video_file:
-                error_msg = "无法获取视频文件对象"
-                logger.error(error_msg)
-                return {
-                    "inspiration_points": [],
-                    "metadata": {
-                        "error": error_msg,
-                        "video_upload_error": video_upload_error,
-                        "video_url": video_url,
-                        "total_count": 0
-                    }
-                }
-
-            # Step 1: 识别候选灵感点(逐帧分析视频)
-            logger.info("\n[Step 1/6] 识别候选灵感点(逐帧分析视频)...")
-            candidate_points_list = self._step2_identify_candidates(
-                text_data, video_file
-            )
-            candidate_count = len(candidate_points_list)
-            logger.info(f"候选灵感点: {candidate_count}个")
-
-            # Step 2: 逆向推导验证
-            logger.info("\n[Step 2/6] 逆向推导验证...")
-            verified_points_list = self._step3_backward_verification(
-                text_data, video_file, candidate_points_list
-            )
-            verified_count = len(verified_points_list)
-            logger.info(f"验证通过: {verified_count}个")
-
-            # Step 3: 去重检查
-            logger.info("\n[Step 3/6] 去重检查...")
-            deduplicated_points_list = self._step4_deduplication_check(verified_points_list, candidate_points_list)
-            deduplicated_count = len(deduplicated_points_list)
-            removed_count = verified_count - deduplicated_count
-            logger.info(f"去重后: {deduplicated_count}个 (移除{removed_count}个)")
-
-            # Step 4: 三维度评分
-            logger.info("\n[Step 4/6] 三维度评分...")
-            scored_points_list = self._step5_scoring(
-                deduplicated_points_list,
-                candidate_points_list,  # 传递原始候选点数据以便查找完整信息
-                video_file
-            )
-            scored_count = len(scored_points_list)
-            logger.info(f"评分完成: {scored_count}个")
-
-            # Step 5: 排序筛选
-            logger.info("\n[Step 5/6] 排序筛选...")
-            filtered_points_list = self._step6_sort_and_filter(scored_points_list)
-            total_filtered_count = len(filtered_points_list)
-            logger.info(f"筛选后剩余{total_filtered_count}个灵感点")
-
-            # Step 6: 组装最终输出
-            logger.info("\n[Step 6/6] 组装最终输出...")
-            final_points_list = self._step7_assemble_output(
-                filtered_points_list, candidate_points_list, verified_points_list
-            )
-            total_final_count = len(final_points_list)
-            logger.info(f"最终输出{total_final_count}个高质量灵感点")
-
-            # 构建最终输出
-            final_output = {
-                "inspiration_points": final_points_list,
-                "metadata": {
-                    "total_count": total_final_count,
-                    "perspective": "创作者视角",
-                    "analysis_mode": "视频逐帧分析",
-                    "process_summary": {
-                        "candidate_count": candidate_count,
-                        "verified_count": verified_count,
-                        "deduplicated_count": deduplicated_count,
-                        "scored_count": scored_count,
-                        "filtered_count": total_filtered_count,
-                        "final_count": total_final_count
-                    }
-                }
-            }
-
-            logger.info(f"最终结果: {total_final_count}个灵感点")
-
-            return final_output
-
-        except Exception as e:
-            logger.error(f"灵感点分析失败: {e}", exc_info=True)
-            return {
-                "inspiration_points": [],
-                "metadata": {
-                    "error": str(e),
-                    "total_count": 0
-                }
-            }
-
-    # ==================== 7个步骤方法 ====================
-
-    def _step2_identify_candidates(
-        self,
-        text_data: Dict[str, Any],
-        video_file: "genai.types.File"
-    ) -> List[Dict[str, Any]]:
-        """Step 1: 识别候选灵感点(逐帧分析视频)
-
-        输入:当前视频URI、文本信息
-        输出:候选灵感点列表
-
-        策略:
-        1. 对视频进行逐帧或关键帧分析
-        2. 从视频画面、动作、场景、对话等维度识别灵感点
-        """
-        prompt = f"""# 任务:识别候选灵感点(逐帧分析视频)
-
-## 当前视频
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,捕捉以下维度的信息:
-- 画面内容:场景、物体、人物、环境等视觉元素
-- 动作变化:人物的动作、物体的运动、场景的转换
-- 时间线索:视频的时间顺序、节奏变化、关键时间点
-- 视觉细节:颜色、光影、构图、视觉焦点等
-- 音频信息(如有):对话、音效、背景音乐等
-
-## 核心概念(统一说明,避免重复)
-
-### 字段定义
-**分类**(维度):创作者接收外界信息刺激的角度或通道
-- 格式:2-4个字,简洁直观,避免抽象表述
-
-**灵感点**:创作前遇到的、触发创作冲动的客观刺激源(作者被动接收的信息:看到的、听说的、发现的、观察到的、感知到的)
-- 格式:不超过15个字,使用自然、通俗、口语化的表达方式
-- 本质:刺激源的内容本身(讲什么、做什么、说什么),而非表达手段(如何呈现、如何表达)
-- 表达要求:
-  * 使用日常生活中的自然语言,避免学术化、抽象化的词汇堆砌
-  * 优先使用"的"字短语结构(如"夏日的热闹景象")或动宾短语(如"观察到的自然互动")
-  * 禁止使用多个抽象名词连用(如"具象化动态互动自然拟人")
-  * 表达要让普通人一看就懂,不需要"翻译"
-
-**描述**:对刺激源本身是什么的详细说明,讲清楚这个灵感点具体指的是什么
-- 内容要求:描述刺激源的具体特征、形态、场景、内容等客观信息,让读者能清晰理解"这个灵感点到底是什么东西"
-- 注意区分:刺激源内容本身 vs 呈现方式/表现形式
-
-**推理**:说明这个灵感点是如何得出来的,解释从视频内容推导出这个灵感点的过程
-- 内容要求:连贯的描述性文字,说明基于视频的哪些画面/动作/场景/对话等线索,推导出创作者接收到了这个刺激源
-
-### 严格禁止(适用于灵感点和描述)
-- 不描述创作者如何运用/展现/表达这个刺激,不使用推理性词汇
-- 不能是创作形式、表现手法、表达方式、呈现方式、风格、格式等创作应用层面的东西
-- 必须是被动接收的刺激,不能是主动创造的内容
-- 不解释创作者为什么被触发、如何使用这个刺激
-- 不进行主观推理和价值判断
-- 禁止词汇堆砌,避免多个抽象概念连用
-
-### 反思验证(识别每个灵感点后必须进行)
-- 这个灵感点描述的是内容本身,还是仅仅描述表达手段?
-- 如果仅描述表达手段(如呈现方式、创作形式、风格、格式等),应重新提炼真正的刺激源内容
-- 这个表达是否符合日常口语习惯?是否通俗易懂?
-- 如果描述的是内容本身且表达自然 → 保留
-
-### 识别要求
-- **独立性**: 不同灵感点必须是不同的刺激源,本质相同的只保留一个
-- **完整性**: 分类→灵感点→描述应形成从刺激通道到刺激内容的完整表达
-- **真实性**: 从"作者接收到了什么刺激"(被动接收)而非"作者如何创作"(主动输出)的角度出发
-- **视频特性**: 需要关注视频的动态特性,包括画面变化、动作序列、时间线索等
-
-## 输出格式(JSON)
-{{
-  "候选灵感点列表": [
-      {{
-        "候选编号": 1,
-        "分类": "...",
-        "灵感点": "...",
-        "描述": "...",
-        "推理": "..."
-      }}
-  ]
-}}
-
-**重要提醒**:
-- 推理必须是连贯的描述性文字,说明为什么这个灵感点能刺激创作
-- 候选编号全局唯一(连续编号:1, 2, 3...)
-- 必须基于视频的实际内容进行分析,不能凭空想象"""
-
-
-        # 将系统提示词合并到用户提示词中
-        full_prompt = f"{self.system_prompt}\n\n{prompt}"
-        
-        result = LLMInvoker.safe_invoke_video_analysis(
-            "Step1-识别候选灵感点(视频分析)",
-            video_file,
-            full_prompt,
-            agent=self,
-            fallback={}
-        )
-
-        return result.get("候选灵感点列表", [])
-
-    def _step4_deduplication_check(
-        self,
-        verified_points_list: List[Dict[str, Any]],
-        candidate_points_list: List[Dict[str, Any]]
-    ) -> List[Dict[str, Any]]:
-        """Step 4: 灵感点去重检查 - 识别并移除本质相同的灵感点
-
-        输入:
-            - verified_points_list: 验证通过的灵感点列表(包含候选编号和推导说明)
-            - candidate_points_list: 原始候选灵感点列表(包含完整字段)
-        输出:去重后的验证通过的灵感点列表
-
-        策略:
-        1. 从原始候选点中获取完整信息
-        2. 只对验证通过的点进行去重分析
-        3. 识别本质相同的灵感点组
-        4. 每组只保留最精准的一个
-        """
-        # 构建候选点映射表:编号 -> 完整候选点数据
-        candidate_map = {}
-        for candidate in candidate_points_list:
-            candidate_num = candidate.get("候选编号")
-            if candidate_num is not None:
-                candidate_map[candidate_num] = candidate
-
-        # 合并所有验证通过的点,并从原始候选点中获取完整信息
-        all_verified = []
-        for verified_point in verified_points_list:
-            candidate_num = verified_point.get("候选编号")
-            if candidate_num in candidate_map:
-                candidate_data = candidate_map[candidate_num]
-                all_verified.append({
-                    "candidate_num": candidate_num,
-                    "data": candidate_data,
-                    "推导说明": verified_point.get("推导说明", "")
-                })
-
-        if len(all_verified) <= 1:
-            # 只有0或1个验证通过的点,无需去重
-            return verified_points_list
-
-        # 构建候选点列表文本(不包含分类标签,只传递灵感点和描述)
-        candidates_text = "\n".join([
-            f"{p['candidate_num']}. "
-            f"灵感点: {p['data'].get('灵感点', '')}\n"
-            f"   描述: {p['data'].get('描述', '')}"
-            for p in all_verified
-        ])
-
-        prompt = f"""# 任务:灵感点去重
-
-## 候选灵感点列表
-{candidates_text}
-
-## 核心判断标准
-
-**唯一标准:是否指向同一个刺激源?**
-
-- 如果两个灵感点描述的是**同一个事物/现象/刺激**,只是从不同角度、不同层面、不同粒度来表达 → 重复,去重
-
-## 判断方法
-
-1. **识别刺激源本质**:每个灵感点背后的刺激源到底是什么?
-2. **对比刺激源**:是同一个刺激源的不同表述?还是两个不同的刺激源?
-3. **忽略表述差异**:不要被具体用词、分类标签、角度差异迷惑,抓住本质
-
-## 保留规则(重复时选择)
-
-从重复组中选择**最能直接反映刺激源本身**的那一个:
-- 优先保留描述刺激源本身的(客体)
-- 其次保留最具体、最精准的
-
-## 输出(JSON)
-{{
-  "去重分析": [
-    {{"重复组": [2, 3], "重复原因": "两者都在描述同一个刺激源:[具体说明]", "保留编号": 2, "保留理由": "[原因]"}}
-  ],
-  "保留的候选编号列表": [1, 2]
-}}
-
-## 原则
-
-- 宁可过度去重,不遗漏重复
-- 抓住本质,忽略表面差异
-- 不确定时 → 判定重复 → 去重"""
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": prompt}
-        ]
-
-        result = LLMInvoker.safe_invoke(
-            self,
-            "Step4-去重检查",
-            messages,
-            fallback={"保留的候选编号列表": [p['candidate_num'] for p in all_verified]}
-        )
-
-        # 提取保留的候选编号
-        kept_numbers = set(result.get("保留的候选编号列表", []))
-
-        # 记录去重分析结果
-        dedup_analysis = result.get("去重分析", [])
-        if dedup_analysis:
-            logger.info(f"发现{len(dedup_analysis)}组重复:")
-            for group in dedup_analysis:
-                logger.info(f"  - 重复组 {group.get('重复组', [])}: {group.get('重复原因', '')}")
-                logger.info(f"    保留: 候选{group.get('保留编号', '')} - {group.get('保留理由', '')}")
-
-        # 构建去重后的列表(保持验证通过的点的结构)
-        deduplicated_list = []
-        for verified_point in verified_points_list:
-            candidate_num = verified_point.get("候选编号")
-            if candidate_num in kept_numbers:
-                # 保留候选编号和推导说明
-                deduplicated_list.append(verified_point)
-
-        return deduplicated_list
-
-    def _step3_backward_verification(
-        self,
-        text_data: Dict[str, Any],
-        video_file: "genai.types.File",
-        candidate_points_list: List[Dict[str, Any]]
-    ) -> List[Dict[str, Any]]:
-        """Step 2: 逆向推导验证
-
-        输入:候选灵感点列表、当前视频
-        输出:验证通过的灵感点列表
-
-        优化:LLM只输出编号和验证结果,代码实现关联
-        """
-        if not candidate_points_list:
-            return []
-
-        # 将候选点转换为文本
-        candidates_text = "\n".join([
-            f"{p.get('候选编号', i + 1)}. 分类: {p.get('分类', '')}, 灵感点: {p.get('灵感点', '')}"
-            for i, p in enumerate(candidate_points_list)
-        ])
-
-        prompt = f"""# 任务:逆向推导验证
-
-## 当前视频
-请对视频进行逐帧或关键帧分析。
-{self._build_video_text_info(text_data)}
-
-## 候选灵感点
-{candidates_text}
-
-## 验证要求
-对每个候选灵感点进行逆向推导验证:
-
-**验证问题**:
-- 从这个灵感点出发,能否推导出整个视频的内容?
-- 这个刺激源是创作前的触发,而不是创作目的?
-- 这个灵感点是否真正捕捉到了深层动机?
-
-**验证规则**:
-- 能推导出 → 验证通过(true)
-- 不能推导出或逻辑不通 → 验证失败(false)
-
-## 输出格式(JSON - 简化版)
-{{
-  "验证结果列表": [
-    {{
-      "候选编号": 1,
-      "验证结果": true,
-      "推导说明": "从这个灵感点可以推导出..."
-    }},
-    {{
-      "候选编号": 2,
-      "验证结果": false,
-      "推导说明": "无法从这个灵感点推导出..."
-    }}
-  ]
-}}
-
-**格式说明**:
-- 候选编号:整数,对应上面的候选灵感点编号(1, 2, 3...)
-- 验证结果:布尔值(true或false)
-- 推导说明:能否从这个灵感点推导出视频内容的详细说明
-
-**重要**:只输出编号、验证结果和推导说明,不需要复制其他字段"""
-
-        # 使用新的视频分析API
-        # 将系统提示词合并到用户提示词中
-        full_prompt = f"{self.system_prompt}\n\n{prompt}"
-        
-        result = LLMInvoker.safe_invoke_video_analysis(
-            "Step2-逆向验证(视频分析)",
-            video_file,
-            full_prompt,
-            agent=self,
-            fallback={}
-        )
-
-        # 使用辅助方法关联验证结果
-        verification_results = result.get("验证结果列表", [])
-        verified_points_list = self._associate_verification_results(
-            candidate_points_list,
-            verification_results
-        )
-
-        return verified_points_list
-
-    def _step5_scoring(
-        self,
-        verified_points_list: List[Dict[str, Any]],
-        candidate_points_list: List[Dict[str, Any]] = None,
-        video_file: "genai.types.File" = None
-    ) -> List[Dict[str, Any]]:
-        """Step 4: 三维度评分
-
-        输入:验证通过的灵感点列表、原始候选点列表
-        输出:评分后的灵感点列表
-
-        优化:LLM只输出编号和评分,代码实现关联
-        """
-        if not verified_points_list:
-            return []
-
-        # 构建候选点映射:编号 -> 完整候选点数据(用于查找详细信息)
-        candidate_map = {}
-        if candidate_points_list:
-            for candidate in candidate_points_list:
-                candidate_num = candidate.get("候选编号")
-                if candidate_num is not None:
-                    candidate_map[candidate_num] = candidate
-
-        # 构建评分用的灵感点文本(从原始候选点中查找完整信息)
-        points_list = []
-        for p in verified_points_list:
-            candidate_num = p.get('候选编号')
-            # 从原始候选点中查找完整信息
-            candidate_data = candidate_map.get(candidate_num, {})
-            分类 = candidate_data.get('分类', '')
-            灵感点 = candidate_data.get('灵感点', '')
-            描述 = candidate_data.get('描述', '')
-            points_list.append(f"{candidate_num}. 分类: {分类}, 灵感点: {灵感点}\n   描述: {描述}")
-
-        points_text = "\n".join(points_list)
-
-        prompt = f"""# 任务:评估灵感点
-
-## 待评分的灵感点
-{points_text}
-
-## 评分标准
-
-对每个灵感点从三个维度评分(每个维度1-10分):
-
-### 1. 人设契合度(默认先都给5分)
-- 因没有历史视频信息无法推算人设,先都给5分
-
-### 2. 触发可能性(1-10分)
-- 评估这个刺激点触发创作的可能性有多大
-- 考虑刺激点的吸引力强度
-- 考虑创作者遇到这个刺激时产生创作冲动的概率
-
-### 3. 内容解释力(1-10分)
-- 评估这个刺激点能否解释视频的核心内容
-- 判断从这个刺激点能否推导出视频的主要内容
-- 考虑刺激点与视频内容的关联强度
-
-**总分 = 人设契合度 + 触发可能性 + 内容解释力(范围:3-30分)**
-
-## 评分原则
-- 客观评分,不要预设分数高低
-- 如果与视频内容不符,触发可能性也会很低
-- 如果是视频核心特征,触发可能性反而很高
-
-## 输出格式(JSON)
-{{
-  "评分结果列表": [
-    {{
-      "候选编号": 1,
-      "人设契合度": 8,
-      "触发可能性": 7,
-      "内容解释力": 9,
-      "总分": 24,
-      "评分说明": "详细说明评分理由:为什么这个刺激点符合人设、触发创作的可能性如何、能否解释视频内容"
-    }}
-  ]
-}}
-
-**要求**:
-- 候选编号:整数,对应待评分灵感点的编号
-- 人设契合度、触发可能性、内容解释力:1-10的整数
-- 总分:3-30的整数(三个评分之和)
-- 评分说明:详细说明评分理由
-- 只输出编号和评分信息,不需要复制其他字段"""
-
-        # 使用新的视频分析API
-        # 将系统提示词合并到用户提示词中
-        full_prompt = f"{self.system_prompt}\n\n{prompt}"
-        
-        result = LLMInvoker.safe_invoke_video_analysis(
-            "Step4-评分(视频分析)",
-            video_file,
-            full_prompt,
-            agent=self,
-            fallback={}
-        )
-
-        # 使用辅助方法关联评分结果
-        scoring_results = result.get("评分结果列表", []) if isinstance(result, dict) else result
-        scored_points_list = self._associate_scoring_results(
-            verified_points_list,
-            scoring_results
-        )
-
-        return scored_points_list
-
-
-    def _step6_sort_and_filter(
-        self,
-        scored_points_list: List[Dict[str, Any]]
-    ) -> List[Dict[str, Any]]:
-        """Step 6: 排序筛选
-
-        输入:评分后的灵感点列表(仅包含候选编号和评分)
-        输出:筛选后的灵感点列表(仅包含候选编号和评分)
-
-        筛选策略:
-        1. 按总分降序排序
-        2. 筛选规则:
-           - 优先保留总分 >= MIN_QUALITY_SCORE (15分) 的灵感点
-           - 如果都没有达标的,只保留1个分数最高的
-           - 最终输出最多2个灵感点(选择分数最高的2个)
-        """
-        if not scored_points_list:
-            return []
-
-        # 按总分降序排序
-        sorted_points = sorted(
-            scored_points_list,
-            key=lambda x: (x.get("总分", 0), x.get("人设契合度", 0)),
-            reverse=True
-        )
-
-        # 筛选:总分 >= MIN_QUALITY_SCORE
-        qualified_points = [p for p in sorted_points if p.get("总分", 0) >= MIN_QUALITY_SCORE]
-
-        if not qualified_points:
-            # 没有达标的,保底保留全局最高分的1个
-            if sorted_points:
-                best_point = sorted_points[0]
-                logger.warning(
-                    f"无达标灵感点,保底保留全局最高分: "
-                    f"总分={best_point.get('总分', 0)}"
-                )
-                return [best_point]
-            else:
-                logger.warning("没有候选灵感点")
-                return []
-        else:
-            logger.info(f"共{len(qualified_points)}个达标灵感点(总分>={MIN_QUALITY_SCORE})")
-
-        # 最多保留2个
-        top_points = qualified_points[:2]
-
-        logger.info(f"最终保留最多2个灵感点,实际保留{len(top_points)}个")
-        scores = [p.get("总分", 0) for p in top_points]
-        logger.info(f"  总分: {scores}")
-
-        # 统计最终结果
-        qualified_count = sum(
-            1 for p in top_points
-            if p.get("总分", 0) >= MIN_QUALITY_SCORE
-        )
-
-        logger.info(
-            f"筛选后剩余{len(top_points)}个灵感点 "
-            f"(其中{qualified_count}个总分>={MIN_QUALITY_SCORE})"
-        )
-
-        return top_points
-
-    def _step7_assemble_output(
-        self,
-        filtered_points_list: List[Dict[str, Any]],
-        candidate_points_list: List[Dict[str, Any]],
-        verified_points_list: List[Dict[str, Any]] = None
-    ) -> List[Dict[str, Any]]:
-        """Step 7: 组装最终输出(从原始数据中提取字段)
-
-        输入:
-            - filtered_points_list: 筛选后的灵感点列表(包含候选编号、评分、评分说明)
-            - candidate_points_list: 原始候选灵感点列表(包含所有详细字段)
-            - verified_points_list: 验证结果列表(包含推导说明) - 可选
-        输出:最终灵感点列表(完整字段 + 验证信息 + 评分信息)
-
-        组装策略:
-        1. 从原始候选点中提取完整字段
-        2. 从验证结果中提取推导说明
-        3. 从筛选结果中提取评分信息
-        4. 保持筛选后的排序
-        """
-        # 构建候选点映射表:编号 -> 完整候选点数据
-        candidate_map = {}
-        for candidate in candidate_points_list:
-            candidate_num = candidate.get("候选编号")
-            if candidate_num is not None:
-                candidate_map[candidate_num] = candidate
-
-        # 构建验证信息映射表:编号 -> 推导说明(如果提供了verified_points_list)
-        verification_map = {}
-        if verified_points_list:
-            for verified_point in verified_points_list:
-                candidate_num = verified_point.get("候选编号")
-                if candidate_num is not None:
-                    verification_map[candidate_num] = verified_point.get("推导说明", "")
-
-        final_points_list = []
-
-        # 从原始候选点和筛选结果中组装完整数据
-        for filtered_point in filtered_points_list:
-            candidate_num = filtered_point.get("候选编号")
-            original_data = candidate_map.get(candidate_num, {})
-
-            # 组装最终输出格式
-            formatted_point = {
-                "候选编号": candidate_num,
-                "分类": original_data.get("分类", ""),
-                "灵感点": original_data.get("灵感点", ""),
-                "描述": original_data.get("描述", ""),
-                "推理": original_data.get("推理", "")
-            }
-
-            # 添加验证信息(推导说明)
-            if verification_map:
-                formatted_point["推导说明"] = verification_map.get(candidate_num, "")
-
-            # 添加评分信息(重构为scoring对象)
-            formatted_point["scoring"] = {
-                "人设契合度": filtered_point.get("人设契合度", 0),
-                "触发可能性": filtered_point.get("触发可能性", 0),
-                "内容解释力": filtered_point.get("内容解释力", 0),
-                "总分": filtered_point.get("总分", 0),
-                "评分说明": filtered_point.get("评分说明", "")
-            }
-
-            final_points_list.append(formatted_point)
-
-        logger.info(f"最终组装完成,共{len(final_points_list)}个灵感点")
-
-        return final_points_list
-
-    def _build_messages(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """构建消息 - BaseLLMAgent要求实现(本Agent不使用此方法)"""
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """更新状态 - BaseLLMAgent要求实现(本Agent不使用此方法)"""
-        return state
-
-    def _associate_verification_results(
-        self,
-        candidate_points_list: List[Dict[str, Any]],
-        verification_results: List[Dict[str, Any]]
-    ) -> List[Dict[str, Any]]:
-        """关联验证结果到候选点
-
-        Args:
-            candidate_points_list: 候选灵感点列表
-            verification_results: 验证结果列表(包含候选编号、验证结果、推导说明)
-
-        Returns:
-            验证通过的灵感点列表(仅保留候选编号用于后续关联)
-        """
-        # 构建验证映射表:编号 -> 验证结果
-        verification_map = {}
-        for v in verification_results:
-            candidate_num = v.get("候选编号")
-            if candidate_num is not None:
-                verification_map[candidate_num] = {
-                    "验证结果": v.get("验证结果", False),
-                    "推导说明": v.get("推导说明", "")
-                }
-
-        # 关联候选点信息:只返回验证通过的
-        verified_points_list = []
-        total_candidates = len(candidate_points_list)
-        total_verified = 0
-
-        for candidate in candidate_points_list:
-            candidate_num = candidate.get("候选编号")
-            # 获取该候选点的验证结果
-            verification = verification_map.get(candidate_num, {})
-
-            if verification.get("验证结果", False):
-                # 保留候选编号和推导说明用于后续关联
-                verified_point = {
-                    "候选编号": candidate_num,
-                    "推导说明": verification.get("推导说明", "")
-                }
-                verified_points_list.append(verified_point)
-                total_verified += 1
-
-        logger.info(f"验证通过率: {total_verified}/{total_candidates}")
-
-        return verified_points_list
-
-    def _associate_scoring_results(
-        self,
-        verified_points_list: List[Dict[str, Any]],
-        scoring_results: List[Dict[str, Any]]
-    ) -> List[Dict[str, Any]]:
-        """关联评分结果到验证通过的灵感点
-
-        Args:
-            verified_points_list: 验证通过的灵感点列表
-            scoring_results: 评分结果列表(包含候选编号、三维度评分、总分、评分说明)
-
-        Returns:
-            评分后的灵感点列表(仅保留候选编号和评分信息)
-        """
-        # 构建评分映射表:编号 -> 评分信息
-        scoring_map = {}
-        for s in scoring_results:
-            candidate_num = s.get("候选编号")
-            if candidate_num is not None:
-                scoring_map[candidate_num] = {
-                    "人设契合度": s.get("人设契合度", 0),
-                    "触发可能性": s.get("触发可能性", 0),
-                    "内容解释力": s.get("内容解释力", 0),
-                    "总分": s.get("总分", 0),
-                    "评分说明": s.get("评分说明", "")
-                }
-
-        # 关联灵感点信息:只保留候选编号和评分
-        scored_points_list = []
-        all_scores = []
-
-        for point in verified_points_list:
-            candidate_num = point.get("候选编号")
-            # 获取该候选点的评分结果
-            scoring = scoring_map.get(candidate_num, {
-                "人设契合度": 0,
-                "触发可能性": 0,
-                "内容解释力": 0,
-                "总分": 0,
-                "评分说明": "评分失败"
-            })
-
-            # 只保留候选编号和评分信息
-            scored_point = {
-                "候选编号": candidate_num,
-                "人设契合度": scoring["人设契合度"],
-                "触发可能性": scoring["触发可能性"],
-                "内容解释力": scoring["内容解释力"],
-                "总分": scoring["总分"],
-                "评分说明": scoring["评分说明"]
-            }
-
-            scored_points_list.append(scored_point)
-            all_scores.append(scoring["总分"])
-
-        # 日志输出评分统计
-        if all_scores:
-            avg_score = sum(all_scores) / len(all_scores)
-            logger.info(f"评分统计: 平均分={avg_score:.1f}, 最高分={max(all_scores)}, 最低分={min(all_scores)}")
-
-        return scored_points_list
-
-    def _build_video_text_info(self, text_data: Dict[str, Any]) -> str:
-        """构建视频文本信息(视频分析版本)
-        
-        Args:
-            text_data: 包含标题和正文的字典
-            
-        Returns:
-            格式化的视频文本信息
-        """
-        parts = []
-        if text_data.get("title"):
-            parts.append(f"标题: {text_data['title']}")
-        if text_data.get("body"):
-            parts.append(f"正文: {text_data['body']}")
-        if text_data.get("hashtags"):
-            parts.append(f"话题标签: {' '.join(text_data['hashtags'])}")
-        return "\n".join(parts) if parts else "无文本内容"
-

+ 0 - 720
src/components/agents/key_points_agent.py

@@ -1,720 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-关键点提取Agent(视频分析版本)
-
-功能: 从视频内容中提取核心吸引点
-核心任务: 从消费者视角分析,视频中哪些要素能够引起关注
-特征: 消费者视角,基于视频逐帧分析,提取吸引点
-"""
-
-from typing import Any, Dict, List, TYPE_CHECKING
-import json
-
-if TYPE_CHECKING:
-    from google import genai
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker
-
-logger = get_logger(__name__)
-
-LOG_SEPARATOR = "=" * 80
-
-
-class KeyPointsAgent(BaseLLMAgent):
-
-    def __init__(
-        self,
-        name: str = "key_points_agent",
-        description: str = "关键点提取Agent - 消费者视角提取核心吸引点",
-        model_provider: str = "google_genai",
-        temperature: float = 0.3,
-        max_tokens: int = 20480
-    ):
-        """
-        初始化关键点提取Agent
-
-        Args:
-            name: Agent名称
-            description: Agent描述
-            model_provider: 模型提供商
-            temperature: 生成温度(较低,保持客观性)
-            max_tokens: 最大token数
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词 - 只包含所有任务共享的核心信息"""
-        return """你是内容分析专家,从消费者视角提取核心吸引点。
-
-# 关键点定义
-消费者视角下能够引起关注的核心要素
-
-# 维度定义(有限集合 - 二级分类)
-关键点必须归属于以下维度体系:
-
-## 维度大类:形式
-**细分维度:**
-1. **风格** - 关注内容的整体呈现方式、结构、格式等外在特征
-2. **关系** - 关注内容中各个实质要素之间的关联、逻辑关系
-
-## 维度大类:实质
-**细分维度:**
-1. **元素** - 关注内容中的具体实体、对象、要素等
-2. **分类** - 关注对具体元素的抽象、总结、归纳
-
-# 关键点层级关系
-关键点之间存在层级关系,形成树形结构:
-- **层级判断标准**:
-  - 大类 → 小类
-  - 抽象 → 具体
-  - 整体 → 局部
-- **层级表达**:一级关键点 → 二级关键点 → 三级关键点(支持多层嵌套)
-- **层级原则**:子关键点是父关键点的细分、具体化或局部
-
-# 核心原则
-- 消费者视角:思考"什么吸引了消费者"
-- 客观性:只陈述事实,不做主观判断
-- 视频逐帧分析:需要对视频进行逐帧或关键帧分析,捕捉画面、动作、场景、对话等维度的吸引点
-- 二级维度归类:必须明确标注维度大类(形式/实质)和维度细分(风格/关系/元素/分类)
-- 关键点精准:不超过15个字,只写核心发现(不写如何呈现)
-- 描述充分:详细解释关键点的含义和价值
-- 层级清晰:识别关键点之间的层级关系,构建树形结构
-"""
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态 - 消费者视角提取关键点(多步骤:识别 → 两阶段去重 → 层级构建 → 输出)
-
-        流程:
-        1. 识别候选关键点
-        2. 第一轮去重:与灵感点、目的点去重
-        3. 第二轮去重:关键点之间去重
-        4. 构建层级关系:将扁平关键点组织成树形结构
-        5. 组装最终输出
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("关键点分析开始")
-
-        try:
-            # 从 state 获取视频文件对象
-            from src.utils.llm_invoker import get_video_file_from_state
-            video_file = get_video_file_from_state(state)
-
-            if not video_file:
-                logger.warning("无法获取视频文件对象,跳过关键点提取")
-                return {
-                    "key_points": {
-                        "error": "无法获取视频文件对象",
-                        "key_points": [],
-                        "total_count": 0,
-                        "root_count": 0
-                    }
-                }
-
-            # 提取灵感点和目的点信息(用于去重)
-            inspiration_points_data = self._extract_inspiration_points(state)
-            purpose_point_data = self._extract_purpose_point(state)
-
-            # Step 1: 识别候选关键点
-            logger.info("\n[Step 1/5] 识别候选关键点(视频逐帧分析)...")
-            candidate_points = self._step1_identify_candidates(
-                video_file, inspiration_points_data, purpose_point_data
-            )
-            candidate_count = len(candidate_points)
-            logger.info(f"候选关键点: {candidate_count}个")
-
-            # Step 2: 第一轮去重 - 与灵感点、目的点去重
-            logger.info("\n[Step 2/5] 第一轮去重 - 与灵感点、目的点去重...")
-            after_external_dedup = self._step2_external_deduplication(
-                candidate_points, inspiration_points_data, purpose_point_data
-            )
-            after_external_count = len(after_external_dedup)
-            external_removed = candidate_count - after_external_count
-            logger.info(f"第一轮去重: {after_external_count}个 (移除{external_removed}个)")
-
-            # Step 3: 第二轮去重 - 关键点之间去重
-            logger.info("\n[Step 3/5] 第二轮去重 - 关键点之间去重...")
-            after_internal_dedup = self._step3_internal_deduplication(after_external_dedup)
-            after_internal_count = len(after_internal_dedup)
-            internal_removed = after_external_count - after_internal_count
-            logger.info(f"第二轮去重: {after_internal_count}个 (移除{internal_removed}个)")
-
-            # Step 4: 构建层级关系
-            logger.info("\n[Step 4/5] 构建层级关系...")
-            hierarchical_points = self._step4_build_hierarchy(after_internal_dedup)
-            logger.info(f"构建层级: {len(hierarchical_points)}个一级节点")
-
-            # Step 5: 组装最终输出
-            logger.info("\n[Step 5/5] 组装最终输出...")
-            final_output = {
-                "key_points": {
-                    "key_points": hierarchical_points,
-                    "total_count": after_internal_count,
-                    "root_count": len(hierarchical_points)
-                }
-            }
-
-            logger.info(f"最终结果: {after_internal_count}个关键点, {len(hierarchical_points)}个一级节点")
-
-            return final_output
-
-        except Exception as e:
-            logger.error(f"关键点分析失败: {e}", exc_info=True)
-            return {
-                "key_points": {
-                    "error": str(e),
-                    "key_points": [],
-                    "total_count": 0,
-                    "root_count": 0
-                }
-            }
-
-    def _build_messages(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """构建消息 - BaseLLMAgent要求实现(本Agent不使用此方法)"""
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """更新状态 - BaseLLMAgent要求实现(本Agent不使用此方法)"""
-        return state
-
-    # ==================== 辅助方法:提取灵感点和目的点 ====================
-
-    def _extract_inspiration_points(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """从state中提取灵感点信息(用于去重)"""
-        if "inspiration_points" not in state:
-            return None
-
-        inspiration_data = state.get("inspiration_points")
-        
-        # 新结构:inspiration_points 直接是列表
-        if isinstance(inspiration_data, list):
-            if inspiration_data:
-                logger.info(f"检测到 {len(inspiration_data)} 个灵感点,将用于去重检查")
-                # 提取核心字段用于去重
-                return [
-                    {
-                        "分类": p.get("分类", ""),
-                        "灵感点": p.get("灵感点", ""),
-                        "描述": p.get("描述", "")
-                    }
-                    for p in inspiration_data
-                ]
-            return None
-
-        # 兼容旧结构:{"inspiration_points": {"points": [...]}}
-        if isinstance(inspiration_data, dict):
-            points = inspiration_data.get("points", [])
-            if points:
-                logger.info(f"检测到 {len(points)} 个灵感点(旧格式),将用于去重检查")
-                return points
-
-        return None
-
-    def _extract_purpose_point(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """从state中提取目的点信息(用于去重)"""
-        if "purpose_point" not in state:
-            return None
-
-        purpose_data = state.get("purpose_point", {})
-
-        if not isinstance(purpose_data, dict):
-            return None
-
-        # 新结构:{"purpose_point": {"perspective": "创作者视角", "purposes": [...], "total_count": 2}}
-        if "purposes" in purpose_data:
-            purposes = purpose_data.get("purposes", [])
-            if purposes:
-                logger.info(f"检测到 {len(purposes)} 个目的点,将用于去重检查")
-                return purpose_data
-
-        # 旧结构(兼容):{"purpose_point": {"main_purpose": {"维度": "", "目的点": "", "描述": ""}, ...}}
-        if "main_purpose" in purpose_data:
-            main_purpose = purpose_data.get("main_purpose", {})
-            if isinstance(main_purpose, dict) and "目的点" in main_purpose:
-                logger.info(f"检测到目的点(旧结构): {main_purpose.get('目的点', '')}, 将用于去重检查")
-                return purpose_data
-
-        return None
-
-    # ==================== 4个步骤方法 ====================
-
-    def _step1_identify_candidates(
-        self,
-        video_file: "genai.types.File",
-        inspiration_points_data: List[Dict[str, Any]] = None,
-        purpose_point_data: Dict[str, Any] = None
-    ) -> List[Dict[str, Any]]:
-        """Step 1: 识别候选关键点(视频逐帧分析)
-
-        输入:视频文件对象、灵感点数据、目的点数据
-        输出:候选关键点列表(包含候选编号)
-
-        策略:
-        1. 对视频进行逐帧或关键帧分析
-        2. 从消费者视角识别所有可能的关键点
-        3. 每个候选点分配一个唯一编号
-        4. 严格按照有限集合维度分类(形式&关系、实质)
-        """
-        prompt = self._build_identification_prompt("")
-
-        # 使用新的视频分析API
-        # 将系统提示词合并到用户提示词中
-        full_prompt = f"{self.system_prompt}\n\n{prompt}"
-        
-        result = LLMInvoker.safe_invoke_video_analysis(
-            "Step1-识别候选关键点",
-            video_file,
-            full_prompt,
-            agent=self,
-            fallback={"候选关键点列表": []}
-        )
-
-        return result.get("候选关键点列表", [])
-
-    def _step2_external_deduplication(
-        self,
-        candidate_points: List[Dict[str, Any]],
-        inspiration_points_data: List[Dict[str, Any]] = None,
-        purpose_point_data: Dict[str, Any] = None
-    ) -> List[Dict[str, Any]]:
-        """Step 2: 第一轮去重 - 与灵感点、目的点去重
-
-        输入:候选关键点列表、灵感点数据、目的点数据
-        输出:去除与灵感点/目的点重复后的关键点列表
-
-        去重策略:
-        1. 与灵感点去重(关键点≠灵感点)
-        2. 与目的点去重(关键点≠目的点)
-        """
-        if not candidate_points:
-            return []
-
-        # 如果没有灵感点和目的点数据,直接返回候选点
-        if not inspiration_points_data and not purpose_point_data:
-            logger.info("无灵感点和目的点数据,跳过第一轮去重")
-            return candidate_points
-
-        # 构建候选点列表文本
-        candidates_text = "\n".join([
-            f"{p.get('候选编号', i + 1)}. "
-            f"维度: [{p.get('维度大类', '')}] {p.get('维度细分', '')}, "
-            f"关键点: {p.get('关键点', '')}\n"
-            f"   描述: {p.get('描述', '')}"
-            for i, p in enumerate(candidate_points)
-        ])
-
-        # 构建灵感点参考文本
-        inspiration_text = ""
-        if inspiration_points_data:
-            inspiration_items = []
-            for p in inspiration_points_data:
-                category = p.get("category", "")
-                分类 = p.get("分类", "")
-                灵感点 = p.get("灵感点", "")
-                描述 = p.get("描述", "")
-                inspiration_items.append(f"- [{category}] 分类: {分类}, 灵感点: {灵感点}, 描述: {描述}")
-            inspiration_text = "\n".join(inspiration_items)
-
-        # 构建目的点参考文本
-        purpose_text = ""
-        if purpose_point_data:
-            # 新结构:{"purposes": [...]}
-            if "purposes" in purpose_point_data:
-                purposes = purpose_point_data.get("purposes", [])
-                purpose_items = []
-                for p in purposes:
-                    维度 = p.get("维度", {})
-                    if isinstance(维度, dict):
-                        维度_str = f"{维度.get('一级分类', '')}/{维度.get('二级分类', '')}"
-                    else:
-                        维度_str = str(维度)
-                    目的点 = p.get("目的点", "")
-                    描述 = p.get("描述", "")
-                    purpose_items.append(f"- 维度: {维度_str}, 目的点: {目的点}, 描述: {描述}")
-                purpose_text = "\n".join(purpose_items)
-            # 旧结构(兼容):{"main_purpose": {...}}
-            elif "main_purpose" in purpose_point_data:
-                main_purpose = purpose_point_data.get("main_purpose", {})
-                维度 = main_purpose.get("维度", "")
-                目的点 = main_purpose.get("目的点", "")
-                描述 = main_purpose.get("描述", "")
-                purpose_text = f"维度: {维度}\n目的点: {目的点}\n描述: {描述}"
-
-        prompt = f"""# 任务:与灵感点、目的点去重
-
-## 候选关键点
-{candidates_text}
-
-## 灵感点(创作触发源)
-{'以下是已识别的灵感点:' if inspiration_text else '无灵感点'}
-{inspiration_text if inspiration_text else ''}
-
-## 目的点(创作目标)
-{'以下是已识别的目的点:' if purpose_text else '无目的点'}
-{purpose_text if purpose_text else ''}
-
-## 去重规则
-
-**核心原则**:关键点与灵感点/目的点重合 → 必须移除关键点
-
-**判断方法**:
-1. 角色检查:关键点应是"消费吸引点",不应是"触发源"或"创作目标"
-2. 语义去重:移除修饰词,提取核心概念对比 → 核心概念相同 → 移除关键点
-
-**重要**:只要存在重合(角色错误或核心概念相同),就移除关键点
-
-## 输出(JSON)
-{{
-  "去重分析": [{{"去重类型": "与灵感点去重|与目的点去重", "重复候选编号": 1, "重复原因": "说明"}}],
-  "保留的候选编号列表": [1, 2, 3]
-}}"""
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": prompt}
-        ]
-
-        result = LLMInvoker.safe_invoke(
-            self,
-            "Step2-与灵感点目的点去重",
-            messages,
-            fallback={"保留的候选编号列表": [p.get('候选编号', i+1) for i, p in enumerate(candidate_points)]}
-        )
-
-        # 提取保留的候选编号
-        kept_numbers = set(result.get("保留的候选编号列表", []))
-
-        # 记录去重分析结果
-        dedup_analysis = result.get("去重分析", [])
-        if dedup_analysis:
-            logger.info(f"发现{len(dedup_analysis)}个需要移除的候选:")
-            for item in dedup_analysis:
-                dedup_type = item.get('去重类型', '')
-                重复候选编号 = item.get('重复候选编号', '')
-                重复原因 = item.get('重复原因', '')
-                logger.info(f"  - [{dedup_type}] 候选{重复候选编号}: {重复原因}")
-
-        # 构建去重后的列表
-        deduplicated_points = []
-        for point in candidate_points:
-            candidate_num = point.get("候选编号")
-            if candidate_num in kept_numbers:
-                deduplicated_points.append(point)
-
-        return deduplicated_points
-
-    def _step3_internal_deduplication(
-        self,
-        candidate_points: List[Dict[str, Any]]
-    ) -> List[Dict[str, Any]]:
-        """Step 3: 第二轮去重 - 关键点之间去重
-
-        输入:第一轮去重后的关键点列表
-        输出:关键点之间去重后的最终列表
-
-        去重策略:
-        本质相同的关键点只保留一个
-        """
-        if not candidate_points:
-            return []
-
-        # 如果只有1个候选点,无需去重
-        if len(candidate_points) == 1:
-            logger.info("只有1个候选点,无需关键点之间去重")
-            return candidate_points
-
-        # 构建候选点列表文本
-        candidates_text = "\n".join([
-            f"{p.get('候选编号', i + 1)}. "
-            f"维度: [{p.get('维度大类', '')}] {p.get('维度细分', '')}, "
-            f"关键点: {p.get('关键点', '')}\n"
-            f"   描述: {p.get('描述', '')}"
-            for i, p in enumerate(candidate_points)
-        ])
-
-        prompt = f"""# 任务:关键点之间去重
-
-## 候选关键点
-{candidates_text}
-
-## 去重规则
-
-**本质相同的定义**:
-- 核心发现相同(描述同一个吸引点)
-- 包含关系(一个是另一个的具体/一般化)
-- 粒度不同(抽象层级不同,但指向同一吸引点)
-
-**判断方法**:移除修饰词 → 提取核心发现 → 核心相同 = 本质相同 → 去重
-
-**保留优先级**(本质相同时):
-1. 具体 > 一般
-2. 精准 > 模糊
-3. 原始 > 衍生
-4. 本质 > 形式
-
-## 输出(JSON)
-{{
-  "去重分析": [{{"重复组": [1, 5], "重复原因": "说明", "保留编号": 1, "保留理由": "说明"}}],
-  "保留的候选编号列表": [1, 2, 3]
-}}"""
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": prompt}
-        ]
-
-        result = LLMInvoker.safe_invoke(
-            self,
-            "Step3-关键点之间去重",
-            messages,
-            fallback={"保留的候选编号列表": [p.get('候选编号', i+1) for i, p in enumerate(candidate_points)]}
-        )
-
-        # 提取保留的候选编号
-        kept_numbers = set(result.get("保留的候选编号列表", []))
-
-        # 记录去重分析结果
-        dedup_analysis = result.get("去重分析", [])
-        if dedup_analysis:
-            logger.info(f"发现{len(dedup_analysis)}组重复:")
-            for group in dedup_analysis:
-                重复组 = group.get('重复组', [])
-                重复原因 = group.get('重复原因', '')
-                保留编号 = group.get('保留编号', '')
-                保留理由 = group.get('保留理由', '')
-                logger.info(f"  - 重复组 {重复组}: {重复原因}")
-                logger.info(f"    保留: 候选{保留编号} - {保留理由}")
-
-        # 构建去重后的列表
-        deduplicated_points = []
-        for point in candidate_points:
-            candidate_num = point.get("候选编号")
-            if candidate_num in kept_numbers:
-                deduplicated_points.append(point)
-
-        return deduplicated_points
-
-    def _step4_build_hierarchy(
-        self,
-        flat_points: List[Dict[str, Any]]
-    ) -> List[Dict[str, Any]]:
-        """Step 4: 构建层级关系 - 将扁平化关键点组织成树形结构
-
-        输入:扁平化的关键点列表
-        输出:具有层级关系的树形结构关键点列表
-
-        层级构建策略:
-        1. 识别关键点之间的层级关系(大类→小类、抽象→具体、整体→局部)
-        2. 构建树形结构(父节点 → 子节点)
-        3. 保持一级关键点作为根节点,子关键点嵌套在children字段中
-        """
-        if not flat_points:
-            return []
-
-        # 如果只有1个关键点,无需构建层级
-        if len(flat_points) == 1:
-            logger.info("只有1个关键点,无需构建层级关系")
-            point = flat_points[0].copy()
-            point["children"] = []
-            return [point]
-
-        # 构建关键点列表文本
-        points_text = "\n".join([
-            f"{p.get('候选编号', i + 1)}. "
-            f"维度: [{p.get('维度大类', '')}] {p.get('维度细分', '')}, "
-            f"关键点: {p.get('关键点', '')}\n"
-            f"   描述: {p.get('描述', '')}"
-            for i, p in enumerate(flat_points)
-        ])
-
-        prompt = f"""# 任务:构建关键点层级关系
-
-## 关键点列表(扁平化)
-{points_text}
-
-## 层级判断标准
-
-**层级关系定义**:
-- **大类 → 小类**:一个关键点是另一个关键点的具体类别
-- **抽象 → 具体**:一个关键点是另一个关键点的具体表现
-- **整体 → 局部**:一个关键点是另一个关键点的组成部分
-
-**判断方法**:
-1. 识别父子关系:子关键点是父关键点的细分、具体化或局部
-2. 构建树形结构:父节点可以有多个子节点,子节点也可以继续有子节点
-3. 确保无循环:不能出现A→B→A的情况
-
-**层级原则**:
-- 同一层级的关键点应该是并列关系(不存在包含、细分关系)
-- 不同层级的关键点应该是从属关系(父→子)
-- 一级关键点(根节点)应该是最抽象、最整体的关键点
-- 如果两个关键点没有明确的层级关系,则保持在同一层级
-
-## 输出(JSON)
-{{
-  "层级分析": [
-    {{
-      "父编号": 1,
-      "子编号列表": [2, 3],
-      "层级关系": "大类→小类|抽象→具体|整体→局部",
-      "关系说明": "说明为什么存在这个层级关系",
-      "子节点原因": {{
-        "2": "说明编号2作为子节点的具体原因",
-        "3": "说明编号3作为子节点的具体原因"
-      }}
-    }}
-  ],
-  "一级关键点编号列表": [1, 4, 5]
-}}
-
-注意:
-- "一级关键点编号列表":列出所有一级关键点(根节点)的候选编号
-- "层级分析":描述所有父子关系,可以有多层嵌套
-- "子节点原因":为每个子节点说明为什么它是父节点的子节点(细分、具体化、局部的具体原因)
-"""
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": prompt}
-        ]
-
-        result = LLMInvoker.safe_invoke(
-            self,
-            "Step4-构建层级关系",
-            messages,
-            fallback={
-                "一级关键点编号列表": [p.get('候选编号', i+1) for i, p in enumerate(flat_points)],
-                "层级分析": []
-            }
-        )
-
-        # 构建编号到关键点的映射
-        point_map = {p.get("候选编号"): p.copy() for p in flat_points}
-        for point in point_map.values():
-            point["children"] = []
-
-        # 记录层级分析
-        hierarchy_analysis = result.get("层级分析", [])
-        if hierarchy_analysis:
-            logger.info(f"识别到{len(hierarchy_analysis)}组父子关系:")
-            for item in hierarchy_analysis:
-                父编号 = item.get('父编号', '')
-                子编号列表 = item.get('子编号列表', [])
-                层级关系 = item.get('层级关系', '')
-                关系说明 = item.get('关系说明', '')
-                logger.info(f"  - 父节点{父编号} → 子节点{子编号列表}")
-                logger.info(f"    关系类型: {层级关系}")
-                logger.info(f"    说明: {关系说明}")
-        else:
-            logger.info("未识别到层级关系,所有关键点保持一级")
-
-        # 构建父子关系
-        for item in hierarchy_analysis:
-            父编号 = item.get('父编号')
-            子编号列表 = item.get('子编号列表', [])
-            子节点原因 = item.get('子节点原因', {})
-            if 父编号 in point_map:
-                for 子编号 in 子编号列表:
-                    if 子编号 in point_map:
-                        child_point = point_map[子编号].copy()
-                        # 添加"作为子节点的原因"字段
-                        child_point["作为子节点的原因"] = 子节点原因.get(str(子编号), "")
-                        point_map[父编号]["children"].append(child_point)
-
-        # 提取一级关键点(根节点)
-        root_numbers = set(result.get("一级关键点编号列表", []))
-        root_points = [point_map[num] for num in root_numbers if num in point_map]
-
-        # 如果没有识别出一级关键点,则所有点都作为一级关键点
-        if not root_points:
-            logger.info("未识别出一级关键点,所有关键点作为一级关键点")
-            root_points = list(point_map.values())
-
-        return root_points
-
-    def _build_identification_prompt(self, video_uri: str = "") -> str:
-        """构建识别候选关键点的prompt(视频逐帧分析版本)"""
-        prompt = f"""# 任务:识别候选关键点(视频逐帧分析,扁平化列表)
-
-## 视频内容
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,从以下维度捕捉吸引点:
-- **画面内容**:场景、物体、人物、环境等视觉元素中的吸引点
-- **动作变化**:人物的动作、物体的运动、场景的转换等动态吸引点
-- **时间线索**:视频的时间顺序、节奏变化、关键时间点等时序吸引点
-- **视觉细节**:颜色、光影、构图、视觉焦点等视觉吸引点
-- **音频信息**(如有):对话、音效、背景音乐等听觉吸引点
-
-## 字段定义
-
-### 维度大类
-只能是"形式"或"实质"
-
-### 维度细分
-- 形式类:只能是"风格"或"关系"
-- 实质类:只能是"元素"或"分类"
-
-### 关键点
-≤15字核心发现(只写"是什么",不写"如何呈现")
-
-### 描述
-- **本质**: 讲清楚这个关键点是什么(吸引点的具体特征、形态、内容等客观信息)
-- **内容要求**:
-  - 描述吸引点本身的客观特征
-  - 说明这个吸引点的完整上下文
-  - 让读者能清晰理解"这个关键点到底是什么"
-- **严格禁止**:
-  - 不解释为什么吸引
-  - 不进行价值判断和主观评价
-  - 不分析效果和影响
-
-## 维度分类要求(二级分类体系)
-
-**维度大类:形式**
-- **风格**:关注内容的整体呈现方式、结构、格式等外在特征
-- **关系**:关注内容中各个实质要素之间的关联、逻辑关系
-
-**维度大类:实质**
-- **元素**:关注内容中的具体实体、对象、要素等
-- **分类**:关注对具体元素的抽象、总结、归纳
-
-## 输出(JSON)
-{{
-  "候选关键点列表": [
-    {{
-      "候选编号": 1,
-      "维度大类": "形式|实质",
-      "维度细分": "风格|关系|元素|分类",
-      "关键点": "核心发现",
-      "描述": "充分说明"
-    }}
-  ]
-}}
-
-要求:
-- 广泛收集:从视频的各个维度(画面、动作、场景、对话等)广泛收集吸引点
-- 客观陈述:只陈述事实,不做主观判断
-- 严格按照二级维度体系归类:必须明确标注维度大类(形式/实质)和维度细分(风格/关系/元素/分类)
-- 编号连续:候选编号从1开始连续编号
-- 基于视频:所有关键点必须基于视频的实际内容,不能凭空想象
-
-注意:此步骤输出扁平化列表,后续会构建层级关系
-"""
-
-        return prompt
-
-
-

+ 0 - 238
src/components/agents/purpose_point_agent.py

@@ -1,238 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-目的点提取Agent(视频分析版本)
-
-功能: 从视频内容中提取创作目的
-核心任务: 从创作者视角分析,创作这个视频的目的是什么(为了什么而创作)
-特征: 创作者视角,分析创作动机和目标,基于视频逐帧分析
-
-升级说明:
-- 统一结构:维度 → 目的点 → 描述
-- 维度:观察创作目的的角度(必须从标准分类体系中选择)
-- 目的点:表示创作目标(≤15字)
-- 描述:对目的的客观描述
-- 数量:1-3个目的点,平等不分主次
-- 约束:同一维度只能有一个目的点,互斥的目的不能同时出现
-- 视频分析:需要对视频进行逐帧或关键帧分析,捕捉动态变化和细节
-"""
-
-from typing import Any, Dict, List
-import json
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker
-
-logger = get_logger(__name__)
-
-LOG_SEPARATOR = "=" * 80
-
-
-class PurposePointAgent(BaseLLMAgent):
-    """目的点提取Agent - 分析创作目的和动机
-
-    职责边界:
-    - 只提取"目的点"(创作时的动机)
-    - 不提取"灵感点"(创作前的触发源,由InspirationPointsAgent负责)
-    - 不提取"关键点"(消费者吸引点,由KeyPointsAgent负责)
-
-    核心特征:
-    1. 创作者视角:站在创作者角度思考"我为什么要创作这个"
-    2. 目标导向:分析创作者希望达到什么效果
-    3. 维度化表达:维度(观察角度)→ 目的点(目标)→ 描述(说明)
-    4. 证据支撑:基于内容推断,记录推导过程
-    """
-
-    def __init__(
-        self,
-        name: str = "purpose_point_agent",
-        description: str = "目的点提取Agent - 分析创作目的",
-        model_provider: str = "google_genai",
-        temperature: float = 0.5,
-        max_tokens: int = 8192
-    ):
-        """
-        初始化目的点提取Agent
-
-        Args:
-            name: Agent名称
-            description: Agent描述
-            model_provider: 模型提供商
-            temperature: 生成温度
-            max_tokens: 最大token数
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是内容创作分析专家,从创作者视角提取创作目的。
-
-# 核心概念
-
-**目的点** = 创作者希望通过内容对受众产生的影响或效果(Why创作)
-- 区分:目的点(Why)≠ 创作手段(How)≠ 灵感点(触发源)
-- 验证:问"创作者为什么要做这个内容?"而非"如何做"
-- 数量:1-3个,每个二级分类最多1个目的点
-
-# 输出结构
-
-每个目的点包含:
-1. **维度**:结构化对象 {"一级分类": "", "二级分类": ""}
-2. **目的点**:≤15字,描述创作者希望达成的最终效果
-3. **描述**:解释创作者希望受众产生什么反应/影响
-4. **推理**:说明这个目的点是如何得出来的,解释从视频内容推导出这个创作目的的过程(基于视频的哪些画面/动作/场景/对话等线索,推导出创作者的这个目的)
-
-# 标准分类体系
-
-```
-# 一级:个人
-## 二级:记录(个人记录、日记、生活点滴)
-## 二级:分享(分享经验、知识、感受、故事)
-
-# 一级:创作者
-## 二级:面向粉丝(粉丝运营、互动、维护关系)
-## 二级:品牌种草(推广产品/品牌、种草带货)
-## 二级:商业化效果(转化、ROI、销售业绩)
-
-# 一级:其他(无法归类的特殊情况)
-```
-
-# 核心原则
-
-- **目的优先**:问"为什么"
-- **创作者视角**:站在创作者角度思考"我想让受众产生什么反应"
-- **证据支撑**:基于视频内容具体体现推断目的
-- **分类规范**:维度必须从标准分类体系中选择,优先选择二级分类
-- **互斥约束**:每个二级分类只能出现一次
-- **宁缺毋滥**:如果目的不明确,不要强行生成,最少可以只有1个目的点
-- **视频逐帧分析**:需要对视频进行逐帧或关键帧分析,捕捉画面、动作、场景、对话等维度的信息
-"""
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态 - 提取目的点(三层结构)"""
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("目的点分析开始")
-
-        try:
-            # 从 state 获取视频文件对象
-            from src.utils.llm_invoker import get_video_file_from_state
-            video_file = get_video_file_from_state(state)
-
-            if not video_file:
-                logger.warning("无法获取视频文件对象,跳过目的点提取")
-                return {
-                    "purpose_point": {
-                        "error": "无法获取视频文件对象",
-                        "purposes": [],
-                        "total_count": 0
-                    }
-                }
-
-            # 构建分析prompt(只使用视频)
-            prompt = self._build_analysis_prompt("")
-
-            # 使用新的视频分析API
-            # 将系统提示词合并到用户提示词中
-            full_prompt = f"{self.system_prompt}\n\n{prompt}"
-            
-            result = LLMInvoker.safe_invoke_video_analysis(
-                "目的点提取",
-                video_file,
-                full_prompt,
-                agent=self,
-                fallback={
-                    "目的点列表": []
-                }
-            )
-
-            # 构建最终输出(扁平化展示所有目的点)
-            purpose_points = result.get("目的点列表", [])
-
-            final_output = {
-                "purpose_point": {
-                    "perspective": "创作者视角",
-                    "purposes": purpose_points,  # 所有目的点平等展示
-                    "total_count": len(purpose_points)
-                }
-            }
-
-            logger.info(f"最终结果: {len(purpose_points)}个目的点")
-
-            return final_output
-
-        except Exception as e:
-            logger.error(f"目的点分析失败: {e}", exc_info=True)
-            return {
-                "purpose_point": {
-                    "error": str(e),
-                    "purposes": [],
-                    "total_count": 0
-                }
-            }
-
-    def _build_messages(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """构建消息 - BaseLLMAgent要求实现(本Agent不使用此方法)"""
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """更新状态 - BaseLLMAgent要求实现(本Agent不使用此方法)"""
-        return state
-
-    def _build_analysis_prompt(self, video_url: str = "") -> str:
-        """构建分析prompt(视频逐帧分析版本)"""
-        prompt = f"""# 任务:提取创作目的(视频逐帧分析)
-
-## 视频内容
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,捕捉以下维度的信息:
-- **画面内容**:场景、物体、人物、环境等视觉元素
-- **动作变化**:人物的动作、物体的运动、场景的转换
-- **时间线索**:视频的时间顺序、节奏变化、关键时间点
-- **视觉细节**:颜色、光影、构图、视觉焦点等
-- **音频信息**(如有):对话、音效、背景音乐等
-
-## 分析步骤
-1. **逐帧分析**:观察视频的每一帧或关键帧,理解视频的整体内容和结构
-2. **识别意图**:从视频的画面、动作、场景、对话等维度,推断创作者想让受众产生什么反应/影响(Why)
-3. **维度归类**:从标准分类体系中选择最匹配的维度(优先二级分类)
-4. **提炼目的点**:在维度下提炼具体目标(≤15字,聚焦最终效果,不写手段)
-
-## 重要约束
-- **每个二级分类最多1个目的点**(例如不能同时有2个"记录"维度的目的点)
-- **宁缺毋滥**:如果某个维度的目的不明确,不要强行生成
-- **总数控制**:最终输出1-3个目的点,不要为了凑数而硬造
-- **证据支撑**:每个目的点都必须有足够的视频内容支撑,不能臆测
-- **基于视频**:所有目的点必须基于视频的实际内容,不能凭空想象
-
-## 输出格式(JSON)
-{{
-    "目的点列表": [
-        {{
-            "维度": {{
-                "一级分类": "",
-                "二级分类": ""
-            }},
-            "目的点": "",
-            "描述": "",
-            "推理": "说明基于视频的哪些画面/动作/场景/对话等线索,推导出创作者的这个目的"
-        }}
-    ]
-}}
-"""
-
-        return prompt
-
-
-

+ 0 - 1101
src/components/agents/recursive_image_deconstruction_agent.py

@@ -1,1101 +0,0 @@
-"""
-Recursive Image Deconstruction Agent.
-
-图片递归解构Agent:递归解构图片中的视觉元素。
-
-优化记录:
-- 2025-10: 提取JSON处理工具类,消除重复代码
-- 2025-10: 提取权重归一化工具类,提高可维护性
-- 2025-10: 提取LLM调用封装类,统一错误处理
-- 2025-10: 采用方案A(视觉优先),移除阶段0自由描述,直接基于图片生成树状结构
-- 2025-10: 优化Prompt设计,强化核心约束原则(意图驱动、真实可见、无主观推测)
-- 2025-10: 优化阶段1实现,明确职责分离:
-  * 阶段1(元素定位树): description轻量化(1句话10-20字),position详细化(核心重点)
-  * 阶段2(详细分析): 基于position精准定位,生成详细多维度描述
-  * 避免阶段1的重复劳动,提升Token利用效率
-- 2025-10-29: 并行化优化,使用共享线程池并行处理同一层级的子节点,显著提升处理效率
-  * 采用实例级共享ThreadPoolExecutor(避免递归创建导致的线程池爆炸问题)
-  * 在__init__中创建线程池,整个Agent生命周期内复用
-  * 保持结果顺序一致性
-  * 添加异常处理、降级策略和资源清理逻辑
-- 2025-10-29: 全树并行化优化,利用节点无状态特性一次性并行处理所有节点
-  * 扁平化树结构,将所有节点展开为带路径的节点列表
-  * 一次性提交所有节点处理任务,最大化并行度
-  * 处理完成后根据路径信息重组回树结构
-  * 结果与原树结构完全一一对应
-- 2025-10-29: 优化阶段1 Prompt,强化"每层穷尽所有子元素"的要求
-  * 增加🔴标记的关键约束,明确要求children必须包含该层级所有可见子元素
-  * 新增穷尽性检查清单,引导LLM系统性扫描所有区域和元素类型
-  * 在示例中明确展示多个同类元素(如芒果块1/2/3)的完整列举
-  * 确保LLM不遗漏任何可见元素,提升拆分完整性
-- 2025-10-30: 引入"XY轴拆分与Z轴拆分交织递归"的核心认知
-  * 明确两种拆分维度:XY轴拆分(X/Y轴位置)和Z轴拆分(Z轴层叠关系)
-  * 拆分顺序灵活化:可以是"XY轴→Z轴→重复",也可以是"Z轴→XY轴→重复"
-  * 提供判断标准:根据元素特征(空间分布 vs 层叠覆盖)选择优先顺序
-  * 强调两种拆分都要用到,确保完整捕获图片的3D结构信息
-  * 提供两个示例展示不同拆分顺序的实际应用场景
-- 2025-10-30: Prompt精简优化,提升可读性和执行效率
-  * 合并重复内容:将穷尽性约束统一到一个章节,减少冗余强调
-  * 精简交织递归说明:从~30行压缩到~4行,移除冗长示例
-  * 简化字段说明:position和description的说明更加简洁直接
-  * 补充完整示例:添加完整的JSON树状结构示例,展示所有关键点
-  * 整体减少~37%的Prompt长度(从~95行到~60行),同时保留所有核心要点
-- 2025-10-30: 添加split_dimension字段追踪拆分维度
-  * 在阶段1的Prompt中要求LLM为每个子节点标注split_dimension字段("XY轴拆分"或"Z轴拆分")
-  * 在_process_single_node中提取split_dimension并记录到日志
-  * 在结果构建时保留split_dimension信息,使最终输出明确标注每个元素的拆分维度
-  * 在所有降级/错误处理场景中也保留split_dimension字段
-  * 便于追踪和理解递归拆分的决策路径
-- 2025-10-30: 强化"同一层级规则"约束
-  * 明确规定:同一元素的children中,所有子元素的split_dimension必须相同
-  * 避免在同一层级混合使用XY轴拆分和Z轴拆分
-  * 确保拆分逻辑清晰,层级结构更合理
-  * 在Prompt中用🔴标记强调此规则,确保LLM严格遵守
-- 2025-10-30: 统一命名规范,提升语义清晰度
-  * 将"空间拆分"重命名为"XY轴拆分",更准确描述空间位置划分
-  * 将"图层拆分"重命名为"Z轴拆分",更准确描述层叠关系
-  * 使用XYZ三维坐标系统的直观表述,便于理解图片的3D结构分解
-"""
-
-from typing import Dict, Any, List, Optional
-import json
-from concurrent.futures import ThreadPoolExecutor, as_completed
-
-from src.components.agents.base import BaseLLMAgent
-from src.states.what_deconstruction_state import WhatDeconstructionState
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-# ============================================================================
-# 常量配置
-# ============================================================================
-
-# 默认描述维度
-DEFAULT_DIMENSIONS = [
-    "主体内容",
-    "构图方式",
-    "色彩风格",
-    "视觉焦点",
-    "氛围感",
-    "细节特征"
-]
-
-# 维度生成配置
-MIN_DIMENSION_COUNT = 3  # 最小维度数
-TARGET_DIMENSION_RANGE = (5, 8)  # 目标维度范围
-
-# 日志分隔符
-LOG_SEPARATOR = "=" * 80
-
-
-# ============================================================================
-# 工具类定义
-# ============================================================================
-
-class JSONExtractor:
-    """JSON提取和解析工具类
-
-    负责从LLM响应中提取JSON内容并安全解析
-    """
-
-    @staticmethod
-    def extract_json_from_response(content: str) -> str:
-        """从LLM响应中提取JSON内容
-
-        Args:
-            content: LLM响应文本
-
-        Returns:
-            提取的JSON字符串
-        """
-        content = content.strip()
-
-        # 提取代码块中的JSON
-        if "```json" in content:
-            content = content.split("```json")[1].split("```")[0].strip()
-        elif "```" in content:
-            content = content.split("```")[1].split("```")[0].strip()
-
-        return content
-
-    @staticmethod
-    def safe_parse_json(content: str, default: Any = None) -> Any:
-        """安全解析JSON,失败时返回默认值
-
-        Args:
-            content: JSON字符串
-            default: 解析失败时的默认值
-
-        Returns:
-            解析后的对象或默认值
-        """
-        try:
-            return json.loads(content)
-        except json.JSONDecodeError as e:
-            logger.error(
-                f"JSON解析失败: {e}\n"
-                f"内容前100字符: {content[:100]}..."
-            )
-            return default if default is not None else {}
-
-    @staticmethod
-    def extract_and_parse(content: str, default: Any = None) -> Any:
-        """一步完成提取和解析
-
-        Args:
-            content: LLM响应文本
-            default: 解析失败时的默认值
-
-        Returns:
-            解析后的对象或默认值
-        """
-        json_str = JSONExtractor.extract_json_from_response(content)
-        return JSONExtractor.safe_parse_json(json_str, default)
-
-
-class WeightNormalizer:
-    """权重归一化工具类
-
-    负责子节点权重的归一化处理
-    """
-
-    @staticmethod
-    def normalize_weights(children: List[Dict[str, Any]]) -> None:
-        """归一化子节点权重(原地修改)
-
-        Args:
-            children: 子节点列表
-        """
-        if not children:
-            return
-
-        # 计算总权重
-        total_weight = sum(
-            child.get("importance_weight", 0)
-            for child in children
-        )
-
-        if total_weight > 0:
-            # 按比例归一化
-            for child in children:
-                original_weight = child.get("importance_weight", 0)
-                normalized_weight = original_weight / total_weight
-                child["importance_weight"] = round(normalized_weight, 3)
-                logger.debug(
-                    f"权重归一化 - 原始: {original_weight:.3f}, "
-                    f"归一化: {normalized_weight:.3f}"
-                )
-        else:
-            # 均分权重
-            equal_weight = 1.0 / len(children)
-            for child in children:
-                child["importance_weight"] = round(equal_weight, 3)
-            logger.warning(
-                f"所有子节点权重为0,均分权重: {equal_weight:.3f}"
-            )
-
-
-class LLMInvoker:
-    """LLM调用封装类
-
-    提供统一的LLM调用接口,包含错误处理和日志记录
-    """
-
-    @staticmethod
-    def safe_invoke(
-        agent: Any,
-        operation_name: str,
-        messages: List[Dict[str, Any]],
-        fallback: Any = None
-    ) -> Any:
-        """安全的LLM调用
-
-        Args:
-            agent: Agent实例
-            operation_name: 操作名称(用于日志)
-            messages: 消息列表
-            fallback: 调用失败时的降级返回值
-
-        Returns:
-            解析后的JSON对象或降级值
-        """
-        try:
-            if not agent.is_initialized:
-                agent.initialize()
-
-            response = agent.model.invoke(messages)
-            content = response.content.strip()
-
-            # 提取和解析JSON
-            result = JSONExtractor.extract_and_parse(content, {})
-
-            if not result:
-                logger.warning(
-                    f"{operation_name} - LLM返回内容为空或解析失败\n"
-                    f"原始内容前200字符: {content[:200]}..."
-                )
-                return fallback if fallback is not None else {}
-
-            return result
-
-        except json.JSONDecodeError as e:
-            logger.error(
-                f"{operation_name} - JSON解析失败: {e}\n"
-                f"原始内容前200字符: {content[:200] if 'content' in locals() else 'N/A'}..."
-            )
-            return fallback if fallback is not None else {}
-
-        except Exception as e:
-            logger.error(
-                f"{operation_name} - 调用失败: {e}",
-                exc_info=True
-            )
-            return fallback if fallback is not None else {}
-
-
-class RecursiveImageDeconstructionAgent(BaseLLMAgent):
-    """图片递归解构Agent
-
-    功能:
-    - 递归解构图片视觉元素(最多10层)
-    - 粗理解图片生成what描述
-    - 使用大模型自带知识获取描述维度
-    - 根据大模型判断是否继续分割
-    - 递归处理子图片元素
-
-    """
-
-    def __init__(
-        self,
-        name: str = "recursive_image_deconstruction_agent",
-        description: str = "递归解构图片元素",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 40960,
-        max_depth: int = 10,
-        max_workers: int = 4
-    ):
-        system_prompt = """你是一个专业的视觉内容分析专家。你的任务是深入分析图片内容,提取视觉特征。
-
-你需要:
-1. 理解图片的整体构图、主体、风格
-2. 根据提供的描述维度,分析图片特征
-3. 识别图片中的视觉子元素(如果需要细分)
-4. 判断图片是否需要进一步分割为子元素
-
-要求:
-- 客观准确,基于视觉呈现
-- 描述要具体详细
-- 判断要合理
-"""
-
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-        self.max_depth = max_depth
-        # 创建共享线程池,避免递归时重复创建
-        self.max_workers = max_workers
-        self._executor = ThreadPoolExecutor(max_workers=max_workers)
-        logger.info(f"初始化共享线程池 - max_workers: {max_workers}")
-
-    def __del__(self):
-        """析构函数:清理线程池资源"""
-        self.cleanup()
-
-    def cleanup(self):
-        """显式清理线程池资源(建议在使用完Agent后调用)"""
-        if hasattr(self, '_executor') and self._executor:
-            try:
-                self._executor.shutdown(wait=True)
-                logger.info("共享线程池已关闭")
-            except Exception as e:
-                logger.warning(f"关闭线程池时出错: {e}")
-
-    def process(self, state: WhatDeconstructionState, config: Optional[Any] = None) -> Dict[str, Any]:
-        """处理图片递归解构(入口方法)
-
-        两阶段流程(优化版 - 职责分离):
-        1. 阶段1:生成元素定位树(轻量级description + 详细position)
-        2. 阶段2:基于定位树逐层进行维度提取和详细理解
-        """
-        current_image = state.get("current_image", "")
-
-        if not current_image:
-            logger.warning("图片URL为空")
-            return {}
-
-        logger.info(f"开始图片解构(两阶段流程 - 优化版): {current_image}")
-
-        # ===== 阶段1: 生成元素定位树 =====
-        logger.info(LOG_SEPARATOR)
-        logger.info("【阶段1】生成元素定位树(轻量description + 详细position)...")
-        logger.info(LOG_SEPARATOR)
-
-        tree_structure = self._generate_tree_structure(current_image)
-
-        logger.info(LOG_SEPARATOR)
-        logger.info("【阶段1】元素定位树生成完成")
-        logger.info(LOG_SEPARATOR)
-
-        # 输出树状结构(包含轻量description和详细position)
-        logger.info("\n【元素定位树】:")
-        self._print_tree_structure(tree_structure)
-
-        logger.info(LOG_SEPARATOR)
-
-        # ===== 阶段2: 逐层维度理解 =====
-        logger.info(LOG_SEPARATOR)
-        logger.info("【阶段2】开始逐层维度理解(生成详细描述)...")
-        logger.info(LOG_SEPARATOR)
-
-        deconstruction_dimensions = state.get("deconstruction_dimensions", [])
-        result = self._enrich_tree_with_dimensions(
-            tree_structure,
-            current_image,
-            deconstruction_dimensions
-        )
-
-        logger.info(LOG_SEPARATOR)
-        logger.info("【阶段2】逐层维度理解完成")
-        logger.info(LOG_SEPARATOR)
-
-        return result
-
-    def _generate_tree_structure(self, image_url: str) -> Dict[str, Any]:
-        """【阶段1】生成完整的树状结构(元素定位树)
-
-        基于图片直接生成整个图片的层级结构,主要用于元素定位导航。
-        每个节点包含:
-        - element: 元素名称(what值,简洁名词)
-        - description: 简要识别描述(1句话,用于确认元素身份)
-        - position: 精准位置坐标(关键!用于阶段2定位元素)
-        - children: 子节点列表
-
-        注意:详细描述在阶段2完成,此阶段重点是构建精准的定位树。
-
-        Args:
-            image_url: 图片URL
-
-        Returns:
-            树状结构(嵌套字典)
-        """
-        try:
-            prompt = """请生成图片的完整元素定位树(用于后续详细分析的导航坐标)。
-
-**目标**:构建层级清晰的树状结构,最多10层深度,一次性识别所有层级。
-
-**节点格式**:
-根节点:`{"element": "图片", "description": "简要概括", "children": [...]}`
-子节点:`{"element": "元素名", "description": "识别特征", "position": "精准位置坐标", "split_dimension": "拆分维度", "children": [...]}`
-
-**split_dimension说明**(子节点必需字段):
-- "XY轴拆分":X/Y轴位置划分(如左/右、上/下、前景/背景位置)
-- "Z轴拆分":Z轴层叠关系(如背景层→主体层→文字层)
-- 根节点不需要此字段
-
-**关键要求**:
-
-1. **position(核心)**:必须包含方位词、相对位置、区分性特征,确保能精准定位
-   示例:"碗的左上角区域,靠近碗边缘,是三块芒果中最大的一块,切面朝上"
-
-2. **description(轻量)**:仅用于识别"这是什么",不包含详细特征和位置信息
-
-3. **穷尽性约束**:
-   - 🔴 每层children必须包含该层级**所有**可见子元素,不能遗漏
-   - 系统性扫描所有区域:空间上的不同位置、视觉上的不同图层
-   - 同类元素有多个实例时,全部列出(如芒果块1/2/3)
-
-4. **拆分策略**:
-   - **意图驱动**:只拆分对理解帖子意图有帮助的元素
-   - **交织递归**:灵活运用XY轴拆分和Z轴拆分,可以是"XY轴→Z轴"或"Z轴→XY轴"
-     * 空间分布明显(如左右分栏)→ 优先XY轴拆分
-     * 层叠覆盖明显(如背景+主体+文字)→ 优先Z轴拆分
-   - 🔴 **同一层级规则**:同一元素的children中,所有子元素的split_dimension必须相同(要么全是"XY轴拆分",要么全是"Z轴拆分")
-   - **真实可见**:只识别清晰可见的元素,不主观推测
-   - **终止条件**:达到10层深度 或 无法再有意义细分 → children为空数组
-
-🔴 **最重要**:
-1. 确保每一层children包含该层级所有可见子元素,不要遗漏!
-2. 同一元素的所有子元素必须使用相同的split_dimension(不能混合XY轴拆分和Z轴拆分)!"""
-
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {
-                    "role": "user",
-                    "content": [
-                        {"type": "text", "text": prompt},
-                        {"type": "image_url", "image_url": {"url": image_url}}
-                    ]
-                }
-            ]
-
-            # 使用统一的LLM调用器
-            result = LLMInvoker.safe_invoke(
-                self,
-                "树状结构生成",
-                messages,
-                fallback={
-                    "element": "图片",
-                    "description": "图片内容",
-                    "children": []
-                }
-            )
-
-            logger.info(f"树状结构生成成功 - 根节点: {result.get('element')}")
-            return result
-
-        except Exception as e:
-            logger.error(f"树状结构生成失败: {e}", exc_info=True)
-            return {
-                "element": "图片",
-                "description": "图片内容",
-                "children": []
-            }
-
-    def _flatten_tree_nodes(
-        self,
-        tree_node: Dict[str, Any],
-        path: List[int] = None,
-        depth: int = 0
-    ) -> List[Dict[str, Any]]:
-        """将树结构扁平化为节点列表(带路径信息)
-
-        Args:
-            tree_node: 树节点
-            path: 当前节点路径(用于重组树结构时定位)
-            depth: 当前深度
-
-        Returns:
-            扁平化的节点列表,每个节点包含原始数据和路径信息
-        """
-        if path is None:
-            path = []
-
-        # 当前节点信息
-        flat_nodes = [{
-            "node": tree_node,
-            "path": path.copy(),
-            "depth": depth
-        }]
-
-        # 递归处理子节点
-        children = tree_node.get("children", [])
-        for idx, child in enumerate(children):
-            child_path = path + [idx]
-            flat_nodes.extend(
-                self._flatten_tree_nodes(child, child_path, depth + 1)
-            )
-
-        return flat_nodes
-
-    def _rebuild_tree_from_results(
-        self,
-        flat_results: Dict[str, Dict[str, Any]],
-        tree_node: Dict[str, Any],
-        path: List[int] = None
-    ) -> Dict[str, Any]:
-        """根据处理结果重组树结构
-
-        Args:
-            flat_results: 扁平化处理结果字典(key为路径字符串)
-            tree_node: 原始树节点
-            path: 当前路径
-
-        Returns:
-            重组后的节点
-        """
-        if path is None:
-            path = []
-
-        # 获取当前节点的处理结果
-        path_key = "_".join(map(str, path)) if path else "root"
-        result = flat_results.get(path_key)
-
-        if not result:
-            logger.warning(f"路径 {path_key} 没有找到处理结果")
-            return {
-                "what": tree_node.get("element", "未知元素"),
-                "position": tree_node.get("position", ""),
-                "split_dimension": tree_node.get("split_dimension", ""),
-                "description": tree_node.get("description", ""),
-                "image_path": "",
-                "relationships": [],
-                "importance_weight": 0.5,
-                "weight_reason": "默认权重",
-                "children": []
-            }
-
-        # 递归处理子节点
-        children = tree_node.get("children", [])
-        enriched_children = []
-        for idx, child in enumerate(children):
-            child_path = path + [idx]
-            enriched_child = self._rebuild_tree_from_results(
-                flat_results, child, child_path
-            )
-            enriched_children.append(enriched_child)
-
-        # 归一化子节点权重
-        if enriched_children:
-            WeightNormalizer.normalize_weights(enriched_children)
-
-        # 构建最终结果
-        result["children"] = enriched_children
-        result["relationships"] = self._extract_relationships(enriched_children) if enriched_children else []
-
-        return result
-
-    def _process_single_node(
-        self,
-        node_data: Dict[str, Any],
-        image_url: str,
-        deconstruction_dimensions: List[Dict[str, Any]]
-    ) -> Dict[str, Any]:
-        """处理单个节点(无状态,可并行)
-
-        Args:
-            node_data: 扁平化节点数据(包含node, path, depth)
-            image_url: 图片URL
-            deconstruction_dimensions: 解构维度列表
-
-        Returns:
-            处理后的节点结果
-        """
-        tree_node = node_data["node"]
-        depth = node_data["depth"]
-        path = node_data["path"]
-
-        try:
-            element = tree_node.get("element", "")
-            description = tree_node.get("description", "")
-            position = tree_node.get("position", "")
-            split_dimension = tree_node.get("split_dimension", "")  # 获取拆分维度
-
-            path_key = "_".join(map(str, path)) if path else "root"
-            logger.info(f"处理节点 - 路径: {path_key}, 深度: {depth}, 元素: {element}, 拆分维度: {split_dimension}")
-
-            # Step 1: 生成描述维度
-            if depth == 0:
-                dimensions = self._generate_dimensions_for_root(
-                    image_url,
-                    description,
-                    deconstruction_dimensions
-                )
-            else:
-                dimensions = self._generate_dimensions_for_element(
-                    image_url,
-                    element,
-                    description,
-                    deconstruction_dimensions
-                )
-
-            # Step 2: 根据维度进行详细理解
-            description_values = self._detailed_understanding(
-                image_url,
-                element,
-                position,
-                dimensions
-            )
-
-            # Step 3: 构建结果(不包含children,后续重组时添加)
-            result = {
-                "what": element,
-                "position": position,
-                "split_dimension": split_dimension,  # 添加拆分维度信息
-                "description": description_values,
-                "image_path": image_url,
-                "relationships": [],  # 后续重组时计算
-                "importance_weight": 0.5,
-                "weight_reason": "默认权重",
-                "children": []  # 后续重组时添加
-            }
-
-            logger.info(f"节点处理完成 - 路径: {path_key}, 元素: {element}")
-            return result
-
-        except Exception as e:
-            logger.error(f"节点处理失败 - 路径: {path}: {e}", exc_info=True)
-            return {
-                "what": tree_node.get("element", "未知元素"),
-                "position": tree_node.get("position", ""),
-                "split_dimension": tree_node.get("split_dimension", ""),
-                "description": tree_node.get("description", ""),
-                "image_path": image_url,
-                "relationships": [],
-                "importance_weight": 0.5,
-                "weight_reason": "默认权重",
-                "children": []
-            }
-
-    def _batch_process_nodes(
-        self,
-        flat_nodes: List[Dict[str, Any]],
-        image_url: str,
-        deconstruction_dimensions: List[Dict[str, Any]]
-    ) -> Dict[str, Dict[str, Any]]:
-        """批量并行处理所有节点
-
-        Args:
-            flat_nodes: 扁平化的节点列表
-            image_url: 图片URL
-            deconstruction_dimensions: 解构维度列表
-
-        Returns:
-            处理结果字典(key为路径字符串,value为处理后的节点)
-        """
-        logger.info(f"开始批量并行处理 - 节点总数: {len(flat_nodes)}")
-
-        # 提交所有节点处理任务
-        future_to_node = {
-            self._executor.submit(
-                self._process_single_node,
-                node_data,
-                image_url,
-                deconstruction_dimensions
-            ): node_data for node_data in flat_nodes
-        }
-
-        # 收集结果
-        results = {}
-        completed_count = 0
-        for future in as_completed(future_to_node):
-            node_data = future_to_node[future]
-            path = node_data["path"]
-            path_key = "_".join(map(str, path)) if path else "root"
-
-            try:
-                result = future.result()
-                results[path_key] = result
-                completed_count += 1
-
-                if completed_count % 10 == 0 or completed_count == len(flat_nodes):
-                    logger.info(f"处理进度: {completed_count}/{len(flat_nodes)}")
-
-            except Exception as e:
-                logger.error(f"节点处理失败 - 路径: {path_key}: {e}", exc_info=True)
-                # 使用降级策略
-                tree_node = node_data["node"]
-                results[path_key] = {
-                    "what": tree_node.get("element", "未知元素"),
-                    "position": tree_node.get("position", ""),
-                    "split_dimension": tree_node.get("split_dimension", ""),
-                    "description": tree_node.get("description", ""),
-                    "image_path": image_url,
-                    "relationships": [],
-                    "importance_weight": 0.5,
-                    "weight_reason": "默认权重",
-                    "children": []
-                }
-
-        logger.info(f"批量并行处理完成 - 成功: {len(results)}/{len(flat_nodes)}")
-        return results
-
-    def _enrich_tree_with_dimensions(
-        self,
-        tree_node: Dict[str, Any],
-        image_url: str,
-        deconstruction_dimensions: List[Dict[str, Any]],
-        depth: int = 0  # type: ignore  # Compatibility parameter, not used in parallel implementation
-    ) -> Dict[str, Any]:
-        """【阶段2】基于树状结构进行维度理解(全树并行优化版)
-
-        新架构:
-        1. 将树结构扁平化为带路径的节点列表
-        2. 批量并行处理所有节点(利用节点无状态特性)
-        3. 根据路径信息重组回树结构
-
-        优势:
-        - 一次性并行处理所有节点,最大化并行度
-        - 不受树深度限制,所有节点同时处理
-        - 结果与原树结构完全一一对应
-
-        Args:
-            tree_node: 树节点
-            image_url: 图片URL
-            deconstruction_dimensions: 外部传入的解构维度
-            depth: 当前深度(兼容参数,实际不使用)
-
-        Returns:
-            丰富后的节点
-        """
-        try:
-            logger.info(LOG_SEPARATOR)
-            logger.info("【全树并行处理】开始...")
-
-            # Step 1: 扁平化树结构
-            logger.info("Step 1: 扁平化树结构")
-            flat_nodes = self._flatten_tree_nodes(tree_node)
-            logger.info(f"扁平化完成 - 节点总数: {len(flat_nodes)}")
-
-            # Step 2: 批量并行处理所有节点
-            logger.info("Step 2: 批量并行处理所有节点")
-            flat_results = self._batch_process_nodes(
-                flat_nodes,
-                image_url,
-                deconstruction_dimensions
-            )
-            logger.info(f"批量处理完成 - 成功节点数: {len(flat_results)}")
-
-            # Step 3: 重组树结构
-            logger.info("Step 3: 重组树结构")
-            result = self._rebuild_tree_from_results(flat_results, tree_node)
-            logger.info("重组完成")
-
-            logger.info("【全树并行处理】完成")
-            logger.info(LOG_SEPARATOR)
-
-            return result
-
-        except Exception as e:
-            logger.error(f"全树并行处理失败: {e}", exc_info=True)
-            # 返回基础结构
-            return {
-                "what": tree_node.get("element", "未知元素"),
-                "position": tree_node.get("position", ""),
-                "split_dimension": tree_node.get("split_dimension", ""),
-                "description": tree_node.get("description", ""),
-                "image_path": image_url,
-                "relationships": [],
-                "importance_weight": 0.5,
-                "weight_reason": "默认权重",
-                "children": []
-            }
-
-    def _build_dimension_info(self, deconstruction_dimensions: List[Dict[str, Any]]) -> str:
-        """构建解构维度信息字符串
-
-        Args:
-            deconstruction_dimensions: 外部传入的解构维度列表
-
-        Returns:
-            格式化的维度信息字符串
-        """
-        if not deconstruction_dimensions:
-            return ""
-
-        dimensions_list = []
-        for idx, dimension in enumerate(deconstruction_dimensions):
-            dimension_name = dimension.get("dimension_name", "")
-            dimension_description = dimension.get("dimension_description", "")
-            if dimension_name:
-                dimensions_list.append(
-                    f"{idx + 1}. 维度: {dimension_name}\n   说明: {dimension_description}"
-                )
-
-        if not dimensions_list:
-            return ""
-
-        min_dim, max_dim = TARGET_DIMENSION_RANGE
-        return f"""
-
-**可选的解构维度列表(从评论中提取):**
-{chr(10).join(dimensions_list)}
-
-请在生成分析维度时:
-1. 首先判断这些解构维度是否适用于当前元素
-2. 筛选出相关的维度
-3. 在此基础上补充其他重要的视觉描述维度
-4. 总共生成{min_dim}-{max_dim}个分析维度"""
-
-    def _generate_dimensions_for_element(
-        self,
-        image_url: str,
-        element: str,
-        description: str,
-        deconstruction_dimensions: List[Dict[str, Any]] = None
-    ) -> List[str]:
-        """为普通元素生成描述维度
-
-        Args:
-            image_url: 图片URL
-            element: 元素名称
-            description: 元素描述
-            deconstruction_dimensions: 外部传入的解构维度
-
-        Returns:
-            维度列表
-        """
-        try:
-            # 构建解构维度信息
-            dimensions_info = self._build_dimension_info(deconstruction_dimensions or [])
-            min_dim, max_dim = TARGET_DIMENSION_RANGE
-
-            prompt = f"""请为图片中的指定元素生成用于分析的描述维度。
-
-**图片元素**: {element}
-**元素描述**: {description}{dimensions_info}
-
-**任务**:
-生成{min_dim}-{max_dim}个用于分析该元素的描述维度。
-
-请输出JSON格式:
-{{{{
-    "dimensions": ["维度1", "维度2", "维度3", ...],
-    "relevant_count": 从解构维度中筛选出的相关维度数量,
-    "reason": "简要说明筛选和生成维度的理由"
-}}}}"""
-
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {
-                    "role": "user",
-                    "content": [
-                        {"type": "text", "text": prompt},
-                        {"type": "image_url", "image_url": {"url": image_url}}
-                    ]
-                }
-            ]
-
-            # 使用统一的LLM调用器
-            result = LLMInvoker.safe_invoke(
-                self,
-                "元素维度生成",
-                messages,
-                fallback={"dimensions": self._get_default_dimensions()}
-            )
-
-            dimensions = result.get("dimensions", [])
-            relevant_count = result.get("relevant_count", 0)
-            reason = result.get("reason", "未提供理由")
-
-            # 验证维度列表
-            if not dimensions or len(dimensions) < MIN_DIMENSION_COUNT:
-                logger.warning(f"LLM返回的维度列表不符合要求,使用默认维度")
-                dimensions = self._get_default_dimensions()
-            else:
-                logger.info(
-                    f"元素维度生成完成 - 元素: {element}, "
-                    f"维度数: {len(dimensions)}, 相关维度: {relevant_count}, 理由: {reason}"
-                )
-
-            return dimensions
-
-        except Exception as e:
-            logger.error(f"元素维度生成失败: {e}", exc_info=True)
-            return self._get_default_dimensions()
-
-
-    def _generate_dimensions_for_root(
-        self,
-        original_image: str,
-        overall_description: str,
-        deconstruction_dimensions: List[Dict[str, Any]] = None
-    ) -> List[str]:
-        """为根节点生成描述维度
-
-        Args:
-            original_image: 完整原图路径
-            overall_description: 整体描述
-            deconstruction_dimensions: 外部传入的解构维度(从评论中提取)
-
-        Returns:
-            维度列表
-        """
-        try:
-            # 构建解构维度信息
-            dimensions_info = self._build_dimension_info(deconstruction_dimensions or [])
-            min_dim, max_dim = TARGET_DIMENSION_RANGE
-
-            prompt = f"""请为图片生成用于分析的描述维度。
-
-**图片整体描述**:
-{overall_description}{dimensions_info}
-
-**任务**:
-生成{min_dim}-{max_dim}个用于分析图片的描述维度。
-
-请输出JSON格式:
-{{{{
-    "dimensions": ["维度1", "维度2", "维度3", ...],
-    "relevant_count": 从解构维度中筛选出的相关维度数量,
-    "reason": "简要说明筛选和生成维度的理由"
-}}}}"""
-
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {
-                    "role": "user",
-                    "content": [
-                        {"type": "text", "text": prompt},
-                        {"type": "image_url", "image_url": {"url": original_image}}
-                    ]
-                }
-            ]
-
-            # 使用统一的LLM调用器
-            result = LLMInvoker.safe_invoke(
-                self,
-                "根节点维度生成",
-                messages,
-                fallback={"dimensions": self._get_default_dimensions()}
-            )
-
-            dimensions = result.get("dimensions", [])
-            relevant_count = result.get("relevant_count", 0)
-            reason = result.get("reason", "未提供理由")
-
-            # 验证维度列表
-            if not dimensions or len(dimensions) < MIN_DIMENSION_COUNT:
-                logger.warning(f"LLM返回的维度列表不符合要求,使用默认维度")
-                dimensions = self._get_default_dimensions()
-            else:
-                logger.info(
-                    f"根节点维度生成完成 - "
-                    f"维度数: {len(dimensions)}, 相关维度: {relevant_count}, 理由: {reason}"
-                )
-
-            return dimensions
-
-        except Exception as e:
-            logger.error(f"根节点维度生成失败: {e}", exc_info=True)
-            return self._get_default_dimensions()
-
-    def _get_default_dimensions(self) -> List[str]:
-        """获取默认描述维度"""
-        return DEFAULT_DIMENSIONS.copy()
-
-    def _detailed_understanding(
-        self,
-        original_image: str,
-        element_name: str,
-        element_position: str,
-        dimensions: List[str]
-    ) -> Dict[str, Any]:
-        """根据维度细致理解图片元素
-
-        使用element(what值)和position(位置坐标)精准定位元素
-
-        Args:
-            original_image: 完整原图路径(提供上下文)
-            element_name: 元素名称(what值,如"芒果块")
-            element_position: 元素位置坐标(如"位于碗的左上角,靠近碗边缘")
-            dimensions: 描述维度列表
-
-        Returns:
-            描述值字典
-        """
-        try:
-            # 使用图片路径
-            image_url = original_image
-
-            # 构建精准的定位描述
-            if element_position:
-                location_info = f"""
-【要分析的元素】: {element_name}
-【元素位置】: {element_position}
-
-请在图片中找到这个元素(根据上述元素名称和位置坐标精准定位),然后分析它的特征。"""
-            else:
-                # 根节点没有position
-                location_info = f"""
-【要分析的对象】: 整张图片"""
-
-            prompt = f"""请基于完整原图,分析指定元素的特征。
-
-{location_info}
-
-请从以下维度分析该元素:
-{chr(10).join(f'{i+1}. {dim}' for i, dim in enumerate(dimensions))}
-
-**重要约束**:
-
-1. **精准定位元素**:
-   - 根据【元素位置】的坐标描述,在图片中精准找到该元素
-   - 只分析这一个元素,不要分析其他元素
-
-2. **只描述该元素本身的客观特征**:
-   - 只描述直接观察到的物理特征(颜色、形状、质地、状态等)
-   - 不要添加主观推测(如"看起来新鲜"、"似乎多汁"等)
-
-3. **描述不包含位置信息**:
-   - 不要重复位置信息(position已经单独记录)
-   - 不要包含与其他元素的关系
-   - 不要包含数量描述
-
-4. **每个维度的值必须是元素的内在属性**:
-   - 每个维度的描述应该是当前元素自身的特征
-   - 不涉及与其他事物的比较或关系
-
-请输出JSON格式:
-{{{{
-    "维度1": "值1(只包含该元素的客观特征)",
-    "维度2": "值2(只包含该元素的客观特征)",
-    ...
-}}}}"""
-
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {
-                    "role": "user",
-                    "content": [
-                        {"type": "text", "text": prompt},
-                        {"type": "image_url", "image_url": {"url": image_url}}
-                    ]
-                }
-            ]
-
-            # 使用统一的LLM调用器
-            result = LLMInvoker.safe_invoke(
-                self,
-                "元素详细理解",
-                messages,
-                fallback={dim: "分析失败" for dim in dimensions}
-            )
-
-            return result
-
-        except Exception as e:
-            logger.error(f"图片详细分析失败: {e}", exc_info=True)
-            return {dim: "分析失败" for dim in dimensions}
-
-    def _extract_relationships(self, children: List[Dict[str, Any]]) -> List[str]:
-        """从子节点中提取关系信息
-
-        Args:
-            children: 子节点列表
-
-        Returns:
-            关系列表
-        """
-        relationships = []
-        for child in children:
-            what = child.get("what", "")
-            position = child.get("position", "")
-            if position:
-                relationships.append(f"{what}位于{position}")
-        return relationships
-
-    def _print_tree_structure(self, node: Dict[str, Any]) -> None:
-        """打印树状结构(JSON格式)
-
-        Args:
-            node: 树节点
-        """
-        # 直接打印JSON格式
-        tree_json = json.dumps(node, ensure_ascii=False, indent=2)
-        logger.info(tree_json)
-
-    # 实现BaseLLMAgent要求的抽象方法(虽然process方法已被重写,但仍需提供实现)
-    def _build_messages(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """构建消息(占位实现,实际不使用)
-
-        由于本Agent重写了process方法实现递归逻辑,此方法不会被调用。
-        但作为BaseLLMAgent的子类,必须实现此抽象方法。
-        """
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """更新状态(占位实现,实际不使用)
-
-        由于本Agent重写了process方法实现递归逻辑,此方法不会被调用。
-        但作为BaseLLMAgent的子类,必须实现此抽象方法。
-        """
-        return state

+ 0 - 691
src/components/agents/recursive_text_deconstruction_agent.py

@@ -1,691 +0,0 @@
-"""
-Recursive Text Deconstruction Agent.
-
-文本递归解构Agent:递归解构文本元素(标题/正文/标签)。
-"""
-
-from typing import Dict, Any, List, Optional
-import json
-
-from src.components.agents.base import BaseLLMAgent
-from src.states.what_deconstruction_state import WhatDeconstructionState
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class RecursiveTextDeconstructionAgent(BaseLLMAgent):
-    """文本递归解构Agent
-
-    功能:
-    - 递归解构文本元素(最多10层)
-    - what字段必须是文本原文,不可改写
-    - 使用大模型自带知识获取描述维度
-    - 根据大模型判断是否继续切分
-    - 递归处理子文本元素
-
-    实现方式:BaseLLMAgent(简化版本)
-    """
-
-    def __init__(
-        self,
-        name: str = "recursive_text_deconstruction_agent",
-        description: str = "递归解构文本元素",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 10240,
-        max_depth: int = 10
-    ):
-        system_prompt = """你是一个专业的文本分析专家。你的任务是深入分析文本内容,提取核心特征。
-
-你需要:
-1. 理解文本的语义和结构
-2. 根据提供的描述维度,分析文本特征
-3. 判断文本是否需要进一步切分为子元素
-4. 识别文本的重要性权重
-
-要求:
-- 客观准确,不要臆测
-- 描述要具体详细
-- 判断要合理
-"""
-
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-        self.max_depth = max_depth
-
-    def process(self, state: WhatDeconstructionState, config: Optional[Any] = None) -> Dict[str, Any]:
-
-        return {
-                "what": "",
-                "description": {},
-                "relationships": [],
-                "importance_weight": 0.0,
-                "children": []
-            }
-
-        """处理文本递归解构
-
-        重写process方法以实现递归逻辑
-        """
-        current_text = state.get("current_text", "")
-        text_type = state.get("text_type", "body")
-        parent_id = state.get("parent_id", "")
-        depth = state.get("depth", 0)
-        deconstruction_dimensions = state.get("deconstruction_dimensions", [])  # 获取解构维度
-
-        logger.info(f"开始解构文本 - 深度: {depth}, 父节点: {parent_id}, 类型: {text_type}")
-        if deconstruction_dimensions:
-            logger.info(f"解构维度数量: {len(deconstruction_dimensions)}")
-
-        # 检查递归深度
-        if depth >= self.max_depth:
-            logger.warning(f"达到最大递归深度 {self.max_depth},停止解构")
-            return {
-                "what": current_text,
-                "description": {"说明": "达到最大深度"},
-                "relationships": [],
-                "importance_weight": 0.5,
-                "children": []
-            }
-
-        # 检查文本长度
-        if not current_text or len(current_text.strip()) == 0:
-            logger.warning("文本为空,停止解构")
-            return {
-                "what": "",
-                "description": {},
-                "relationships": [],
-                "importance_weight": 0.0,
-                "children": []
-            }
-
-        # Step 1: what字段直接使用原文
-        what_value = current_text
-
-        # Step 2: 从知识库获取描述维度(利用解构维度)
-        description_dimensions = self._get_description_dimensions(what_value, text_type, parent_id, deconstruction_dimensions)
-
-        # Step 3: 根据描述维度分析文本
-        description_values = self._analyze_text(current_text, description_dimensions, text_type)
-
-        # Step 4: 先用LLM切分文本(执行分割)
-        # PRD变更:先分割,再用代码判断是否需要递归
-        sub_texts = self._llm_split_text(current_text, text_type, depth)
-
-        # Step 5: 基于分割结果用代码判断是否需要继续递归
-        # 判断原则:如果切分出2个或更多子文本,则需要递归
-        should_split = len(sub_texts) >= 2
-
-        logger.info(f"切分结果 - 子文本数量: {len(sub_texts)}, 是否继续递归: {should_split}")
-
-        # Step 5: 构建结果
-        # 计算权重并获取理由
-        weight_info = self._calculate_importance(description_values, what_value, text_type)
-
-        result = {
-            "what": what_value,
-            "description": description_values,
-            "relationships": [],
-            "importance_weight": weight_info.get("score", 0.5) if isinstance(weight_info, dict) else weight_info,
-            "weight_reason": weight_info.get("reason", "") if isinstance(weight_info, dict) else "",
-            "children": []
-        }
-
-        # Step 6: 如需递归,处理子文本元素
-        if should_split and depth < self.max_depth - 1:
-            children = []
-            for idx, sub_text in enumerate(sub_texts):
-                if not sub_text or len(sub_text.strip()) == 0:
-                    logger.warning(f"子元素 {idx + 1} 为空,跳过")
-                    continue
-
-                child_state = {
-                    "current_text": sub_text.strip(),
-                    "text_type": text_type,
-                    "parent_id": f"{parent_id}_{idx + 1}",
-                    "depth": depth + 1,
-                    "deconstruction_dimensions": deconstruction_dimensions  # 传递解构维度
-                }
-
-                logger.info(f"递归处理子元素 {idx + 1}/{len(sub_texts)}")
-                child_result = self.process(child_state)
-                children.append(child_result)
-
-            result["children"] = children
-            result["relationships"] = self._extract_relationships(children)
-
-        logger.info(f"文本解构完成 - 深度: {depth}, 子元素数: {len(result.get('children', []))}")
-
-        return result
-
-    def _get_description_dimensions(
-        self,
-        text: str,
-        text_type: str,
-        parent_id: str = "",
-        deconstruction_dimensions: List[Dict[str, Any]] = None
-    ) -> List[str]:
-        """使用大模型获取描述维度,优先考虑解构维度
-
-        Args:
-            text: 文本内容
-            text_type: 文本类型(title/body/hashtags)
-            parent_id: 父节点ID(如"title", "body", "hashtags")
-            deconstruction_dimensions: 解构维度列表
-
-        Returns:
-            描述维度列表
-        """
-        deconstruction_dimensions = deconstruction_dimensions or []
-
-        # 如果有解构维度,使用LLM筛选相关维度并生成补充维度(合并为一步)
-        if deconstruction_dimensions and text:
-            dimensions = self._llm_generate_dimensions_with_filter(text, text_type, deconstruction_dimensions)
-            if dimensions:
-                return dimensions
-
-        # 如果没有解构维度或筛选失败,使用原有逻辑(LLM生成)
-        # 限制文本长度用于query
-        text_preview = text[:500] if len(text) > 500 else text
-
-        try:
-            prompt = f"""请分析以下{text_type}类型的文本,列出用于刻画描述其核心特征的角度和维度。
-
-文本内容:
-{text_preview}
-
-要求:
-1. 提取3-6个最核心、最有价值的描述维度
-2. 维度名称要简洁明了(2-5个字)
-3. 维度应该具体可操作,能够用于分析文本
-4. 尽可能不重不漏
-
-请输出JSON格式:
-{{
-    "dimensions": ["维度1", "维度2", "维度3", ...]
-}}
-"""
-
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {"role": "user", "content": prompt}
-            ]
-
-            if not self.is_initialized:
-                self.initialize()
-
-            response = self.model.invoke(messages)
-            content = response.content.strip()
-
-            # 提取JSON
-            if "```json" in content:
-                content = content.split("```json")[1].split("```")[0].strip()
-            elif "```" in content:
-                content = content.split("```")[1].split("```")[0].strip()
-
-            # 解析JSON
-            result = json.loads(content)
-            dimensions = result.get("dimensions", [])
-
-            # 验证维度列表
-            if dimensions and len(dimensions) >= 3:
-                logger.info(f"获取到 {len(dimensions)} 个描述维度,维度列表: {dimensions}")
-                return dimensions
-            else:
-                logger.warning("LLM返回的维度列表不符合要求,使用默认维度")
-                return self._get_default_dimensions(text_type)
-
-        except Exception as e:
-            logger.error(f"获取描述维度失败: {e}")
-            return self._get_default_dimensions(text_type)
-
-    def _llm_generate_dimensions_with_filter(
-        self,
-        text: str,
-        text_type: str,
-        deconstruction_dimensions: List[Dict[str, Any]]
-    ) -> List[str]:
-        """使用LLM从解构维度中筛选相关维度并生成补充维度(一步完成)
-
-        Args:
-            text: 当前文本内容
-            text_type: 文本类型(title/body/hashtags)
-            deconstruction_dimensions: 所有解构维度列表
-
-        Returns:
-            完整的维度列表(筛选后的相关维度 + 补充维度)
-        """
-        try:
-            # 构建维度信息列表
-            dimensions_info = []
-            for idx, dimension in enumerate(deconstruction_dimensions):
-                dimension_name = dimension.get("dimension_name", "")
-                dimension_description = dimension.get("dimension_description", "")
-
-                if dimension_name:
-                    dimensions_info.append({
-                        "index": idx + 1,
-                        "dimension_name": dimension_name,
-                        "dimension_description": dimension_description
-                    })
-
-            if not dimensions_info:
-                logger.info("没有可用的解构维度")
-                return []
-
-            # 构建维度信息文本
-            dimensions_text = "\n".join([
-                f"{d['index']}. 维度: {d['dimension_name']}\n   说明: {d['dimension_description']}"
-                for d in dimensions_info
-            ])
-
-            # 限制文本长度
-            text_preview = text[:500] if len(text) > 500 else text
-
-            prompt = f"""请分析以下文本元素,完成两个任务:
-1. 从提供的解构维度中筛选出与当前元素相关的维度
-2. 在此基础上,补充其他重要的描述维度
-
-**当前要分析的{text_type}类型文本:**
-{text_preview}
-
-**可选的解构维度列表:**
-{dimensions_text}
-
-**任务1:筛选相关维度**
-- 分析当前文本元素的内容和特征
-- 逐一判断每个维度是否与当前文本元素相关
-- 判断标准:
-  * 维度名称(dimension_name)是否适用于当前元素
-  * 维度说明(dimension_description)中描述的内容是否有助于分析当前元素
-
-**任务2:生成补充维度**
-- 在筛选出的相关维度基础上
-- 补充其他重要的描述维度
-- 总共提取5-8个描述维度
-- 维度名称要简洁明了(2-5个字)
-- 维度应该具体可操作,能够用于分析文本
-
-**输出要求:**
-请输出JSON格式,包含完整的维度列表:
-{{
-    "dimensions": ["维度1", "维度2", "维度3", ...],
-    "relevant_count": 筛选出的相关维度数量,
-    "reason": "简要说明筛选和补充的理由"
-}}
-
-**重要**:dimensions数组中应包含所有维度(筛选的相关维度 + 补充的新维度)"""
-
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {"role": "user", "content": prompt}
-            ]
-
-            if not self.is_initialized:
-                self.initialize()
-
-            response = self.model.invoke(messages)
-            content = response.content.strip()
-
-            # 提取JSON
-            if "```json" in content:
-                content = content.split("```json")[1].split("```")[0].strip()
-            elif "```" in content:
-                content = content.split("```")[1].split("```")[0].strip()
-
-            # 解析JSON
-            result = json.loads(content)
-            dimensions = result.get("dimensions", [])
-            relevant_count = result.get("relevant_count", 0)
-            reason = result.get("reason", "未提供理由")
-
-            if dimensions and len(dimensions) >= 3:
-                logger.info(
-                    f"LLM生成维度 - 总数: {len(dimensions)}, "
-                    f"相关维度: {relevant_count}, 理由: {reason}"
-                )
-                return dimensions
-            else:
-                logger.warning(f"LLM返回的维度列表不符合要求,使用默认维度")
-                return []
-
-        except Exception as e:
-            logger.error(f"LLM筛选并生成维度失败: {e}")
-            return []
-
-    def _get_default_dimensions(self, text_type: str) -> List[str]:
-        """获取默认描述维度"""
-        if text_type == "title":
-            return ["核心主题", "吸引力", "关键词", "情感倾向"]
-        elif text_type == "hashtags":
-            return ["话题类型", "热度", "相关性"]
-        else:  # body
-            return ["核心内容", "结构", "语言风格", "信息密度", "价值点"]
-
-
-    def _analyze_text(self, text: str, dimensions: List[str], text_type: str) -> Dict[str, Any]:
-        """根据维度分析文本
-
-        Args:
-            text: 文本内容
-            dimensions: 描述维度列表
-            text_type: 文本类型
-
-        Returns:
-            描述值字典
-        """
-        # 构建分析prompt
-        prompt = f"""请分析以下{text_type}文本,按照给定维度提取特征:
-
-文本内容:
-{text}
-
-分析维度:
-{chr(10).join(f'- {dim}' for dim in dimensions)}
-
-请输出JSON格式:
-{{
-    "维度1": "值1",
-    "维度2": "值2",
-    ...
-}}
-"""
-
-        try:
-            # 调用LLM分析
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {"role": "user", "content": prompt}
-            ]
-
-            if not self.is_initialized:
-                self.initialize()
-
-            response = self.model.invoke(messages)
-            content = response.content.strip()
-
-            # 提取JSON
-            if "```json" in content:
-                content = content.split("```json")[1].split("```")[0].strip()
-            elif "```" in content:
-                content = content.split("```")[1].split("```")[0].strip()
-
-            # 解析JSON
-            result = json.loads(content)
-            return result
-
-        except Exception as e:
-            logger.error(f"文本分析失败: {e}")
-            # 返回空描述
-            return {dim: "分析失败" for dim in dimensions}
-
-    def _calculate_importance(
-        self,
-        description: Dict[str, Any],
-        what_value: str = "",
-        text_type: str = "body"
-    ) -> Dict[str, Any]:
-        """使用大模型计算元素重要性权重
-
-        权重策略:
-        - 节点权重值:大于0且小于等于1的数值
-        - 衡量该节点的内容对读者观众的吸引力大小
-        - 帖子中越是亮点、越是吸引观众关注的元素节点,权重值越大
-
-        注意:同一父节点下的各个子节点的权重值之和应固定为1
-        (归一化在 _split_and_recurse 方法中统一处理)
-
-        Args:
-            description: 节点的详细描述
-            what_value: 文本原文
-            text_type: 文本类型
-
-        Returns:
-            包含score和reason的字典:{"score": 吸引力得分, "reason": "评分理由"}
-        """
-        return {"score": 0.5, "reason": "默认权重"}
-
-    def _llm_split_text(
-        self,
-        text: str,
-        text_type: str,
-        depth: int
-    ) -> List[str]:
-        """使用LLM进行智能文本切分
-
-        Args:
-            text: 文本内容
-            text_type: 文本类型
-            depth: 当前深度
-
-        Returns:
-            切分后的子文本列表
-        """
-        try:
-            # 根据文本类型提供切分指导
-            if text_type == "title":
-                split_guidance = """标题文本切分指导:
-- 一般不需要切分,除非标题过长且包含多个独立主题
-- 可以按照标点符号(如顿号、分号)切分
-- 保持每个子标题的语义完整性
-"""
-            elif text_type == "hashtags":
-                split_guidance = """标签文本切分指导:
-- 按照标签分隔符(如#、空格、逗号)切分
-- 每个子元素应该是一个完整的标签
-- 保持标签的独立性
-"""
-            else:  # body
-                split_guidance = """正文文本切分指导:
-- 优先按照段落分隔符(\\n\\n)切分
-- 如果段落过长,可以按照句子分隔符(。!?)切分
-- 每个子元素应该是语义完整的段落或句子
-- 避免在关键词、短语中间切分
-- 保持逻辑连贯性
-"""
-
-            prompt = f"""请根据切分指导,将以下文本切分成多个子元素。
-
-**文本类型:** {text_type}
-**当前深度:** {depth}
-
-**原始文本:**
-{text}
-
-**切分指导:**
-{split_guidance}
-
-**切分要求:**
-1. 根据文本类型和切分指导,选择合适的切分点
-2. 保证每个子元素的语义完整性
-3. 避免过度切分,每个子元素应该有独立的意义
-4. 如果文本无需切分或已经足够简单,返回包含原文的单元素列表
-5. 切分后的子元素必须保持原文内容,不可改写或省略
-
-**输出格式:**
-请输出JSON格式,包含切分后的子文本列表:
-{{
-    "sub_texts": [
-        "子文本1(原文内容,不可改写)",
-        "子文本2(原文内容,不可改写)",
-        ...
-    ],
-    "split_reason": "切分理由(简要说明为什么这样切分)"
-}}
-
-**重要提示:** 子文本必须是原文的精确摘取,不允许改写、总结或省略!
-"""
-
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {"role": "user", "content": prompt}
-            ]
-
-            if not self.is_initialized:
-                self.initialize()
-
-            response = self.model.invoke(messages)
-            content = response.content.strip()
-
-            # 提取JSON
-            if "```json" in content:
-                content = content.split("```json")[1].split("```")[0].strip()
-            elif "```" in content:
-                content = content.split("```")[1].split("```")[0].strip()
-
-            # 解析JSON
-            result = json.loads(content)
-            sub_texts = result.get("sub_texts", [])
-            split_reason = result.get("split_reason", "未提供理由")
-
-            if not sub_texts:
-                logger.warning("LLM未返回有效切分结果")
-                return []
-
-            logger.info(f"LLM切分 - 子文本数: {len(sub_texts)}, 理由: {split_reason}")
-
-            # 验证切分结果的合理性
-            total_length = sum(len(s) for s in sub_texts)
-            original_length = len(text)
-
-            # 如果切分后的总长度与原文长度差异过大,说明可能有问题
-            if abs(total_length - original_length) > original_length * 0.3:
-                logger.warning(f"LLM切分结果长度异常(原文:{original_length}, 切分后:{total_length}),使用降级策略")
-                return []
-
-            return sub_texts
-
-        except Exception as e:
-            logger.error(f"LLM切分失败: {e}")
-            return []
-
-    def _extract_relationships(self, children: List[Dict[str, Any]]) -> List[str]:
-        """提取子节点之间的关系
-
-        使用LLM根据子元素的what和description,智能分析子节点之间的关系
-
-        Args:
-            children: 子节点列表
-
-        Returns:
-            关系描述列表
-        """
-        if not children:
-            return []
-
-        if len(children) == 1:
-            return ["单一子元素"]
-
-        try:
-            # 构建子元素信息摘要
-            children_summary = []
-            for idx, child in enumerate(children):
-                what = child.get("what", "")
-                desc = child.get("description", {})
-
-                # 限制what的长度
-                what_preview = what + "..." if len(what) > 100 else what
-
-                # 提取关键描述信息
-                desc_summary = ", ".join([f"{k}: {v}" for k, v in list(desc.items())])
-
-                children_summary.append(f"子元素{idx + 1}: {what_preview}\n  描述: {desc_summary}")
-
-            children_text = "\n\n".join(children_summary)
-
-            # 构建LLM prompt
-            prompt = f"""请分析以下子元素之间的关系,识别它们的组织结构、逻辑关系、以及整体特征。
-
-**子元素列表(共{len(children)}个):**
-{children_text}
-
-**分析要求:**
-1. 识别子元素之间的逻辑关系(如:并列、递进、因果、对比、总分等)
-2. 分析子元素的组织结构(如:时间顺序、空间顺序、重要性排序等)
-3. 总结子元素的整体特征(如:风格统一、主题一致、层次分明等)
-4. 每个关系描述要简洁明了,控制在10-20字
-
-**输出格式:**
-请输出JSON格式,包含3-5个关系描述:
-{{
-    "relationships": [
-        "关系描述1",
-        "关系描述2",
-        "关系描述3",
-        ...
-    ]
-}}
-
-示例输出:
-{{
-    "relationships": [
-        "子元素按时间顺序排列",
-        "前后元素存在因果关系",
-        "整体呈现递进式论述结构",
-        "各元素围绕同一主题展开"
-    ]
-}}
-"""
-
-            messages = [
-                {"role": "system", "content": self.system_prompt},
-                {"role": "user", "content": prompt}
-            ]
-
-            if not self.is_initialized:
-                self.initialize()
-
-            response = self.model.invoke(messages)
-            content = response.content.strip()
-
-            # 提取JSON
-            if "```json" in content:
-                content = content.split("```json")[1].split("```")[0].strip()
-            elif "```" in content:
-                content = content.split("```")[1].split("```")[0].strip()
-
-            # 解析JSON
-            result = json.loads(content)
-            relationships = result.get("relationships", [])
-
-            if relationships and len(relationships) > 0:
-                logger.info(f"LLM提取到 {len(relationships)} 个子节点关系")
-                return relationships
-            else:
-                logger.warning("LLM未返回有效关系,使用默认关系")
-                return self._get_default_relationships(len(children))
-
-        except Exception as e:
-            logger.error(f"提取子节点关系失败: {e}")
-            return self._get_default_relationships(len(children))
-
-    def _get_default_relationships(self, num_children: int) -> List[str]:
-        """获取默认关系描述
-
-        Args:
-            num_children: 子节点数量
-
-        Returns:
-            默认关系列表
-        """
-        return [
-            f"包含 {num_children} 个子元素",
-            "子元素按顺序排列",
-            "子元素共同构成完整内容"
-        ]
-
-    def _build_messages(self, state: WhatDeconstructionState) -> List[Dict[str, Any]]:
-        """构建消息(继承自BaseLLMAgent,但在process中不使用)"""
-        return []
-
-    def _update_state(self, state: WhatDeconstructionState, response: Any) -> Dict[str, Any]:
-        """更新状态(继承自BaseLLMAgent,但在process中不使用)"""
-        return {}

+ 0 - 1735
src/components/agents/script_form_extraction_agent.py

@@ -1,1735 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-形式提取Agent (ScriptFormExtractionAgent) - 视频版
-
-功能:
-- 从视频内容中提取形式元素(具象概念形式、具体元素形式、整体形式)
-- Step 1: 提取具象概念形式(画面文字 + 口播内容的表达方式)
-- Step 2: 提取具体元素形式(视频中的视觉呈现+映射,包括独立描述和映射关系)
-- Step 3: 提取整体形式(多模态组合,包括画面结构 + 叙事节奏 + 运镜等)
-- Step 4: 共性分析(当前省略)
-- Step 5: 意图支撑 (目的点/选题点/形式关键点)
-- Step 6: 筛选(基于相似度)
-- Step 7: 分类
-"""
-
-import json
-from typing import List, Dict, Any
-from concurrent.futures import ThreadPoolExecutor
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker, get_video_file_from_state
-
-logger = get_logger(__name__)
-
-# 全局线程池 - 用于并行处理
-_GLOBAL_THREAD_POOL = ThreadPoolExecutor(max_workers=16, thread_name_prefix="FormExtraction")
-
-
-class ScriptFormExtractionAgent(BaseLLMAgent):
-    """形式提取Agent - 从文字到视觉到整体的递进过程(视频版)"""
-
-    def __init__(
-        self,
-        name: str = "form_extraction_agent",
-        description: str = "形式提取Agent",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 40960
-    ):
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是一个专业的内容表达分析专家,擅长分析短视频的呈现方式和表达手法。
-
-# 核心定义
-
-## 形式(Form):"怎么呈现"
-表达方式和呈现手段,必须依附于实质内容。
-
-### 具象概念形式
-- **定义**:对具象概念的描述、修饰、表达方式(包括画面文字 + 口播)
-- **特征**:文字的描述、修饰、表达手法
-- **范围**:语气、强调手法、修辞、叙述方式等文字层面的特征
-- **重要**:包括画面文字和口播内容的表达方式
-
-### 具体元素形式
-- **定义**:视觉画面的形式,支撑具体元素、具象概念的视觉呈现
-- **特征**:图像的视觉表现手法、外观特征
-- **范围**:构图、色彩、光影、质感、视觉技巧等图片层面的特征
-- **重要**:不关注图片里的文字说了什么,只关注视觉呈现
-
-### 整体形式
-- **定义**:整体呈现的形式,不针对局部元素/概念
-- **特征**:多模态组合方式、整体结构特征
-- **范围**:图文配比、内容组织、呈现策略、节奏控制等整体层面的特征
-
-## 区分方法:"剥离测试"
-问题:如果去掉所有表达手法/风格/技巧,这个特征还存在吗?
-- 存在 → 实质(内容本身)
-- 不存在/失去意义 → 形式(表达方式)
-"""
-
-    def process(self, state: dict) -> dict:
-        """执行完整的形式提取流程(Step 1-7)"""
-        logger.info("=== 开始形式元素提取(视频版,完整流程) ===")
-
-        # 从 state 中获取视频文件(视频版本核心输入)
-        video_file = get_video_file_from_state(state)
-        if not video_file:
-            logger.error("无法从 state 中获取视频文件,形式提取终止")
-            return {
-                "concrete_concept_forms": [],
-                "concrete_element_forms": [],
-                "overall_forms": [],
-                "form_elements": [],
-                "form_analyzed_result": [],
-                "form_scored_result": {},
-                "form_weighted_result": {},
-                "form_filtered_ids": [],
-                "form_categorized_result": {},
-                "form_final_elements": []
-            }
-
-        # 处理灵感点:支持列表和字典两种格式
-        inspiration_points_raw = state.get("inspiration_points", {})
-        if isinstance(inspiration_points_raw, list):
-            inspiration_points = inspiration_points_raw
-        elif isinstance(inspiration_points_raw, dict):
-            # 兼容旧格式:{"points": [...]} 或直接是列表
-            inspiration_points = inspiration_points_raw.get("points", [])
-        else:
-            inspiration_points = []
-        
-        # 兼容 purpose_point 的多种格式:
-        # 1. 字典格式:{"purpose_point": {"purposes": [...], "total_count": ...}}
-        # 2. 列表格式:[...](直接是目的点列表)
-        # 3. 兼容旧格式:{"purpose_points": [...]}
-        purpose_point_raw = state.get("purpose_point", {})
-        if isinstance(purpose_point_raw, dict):
-            purpose_points = purpose_point_raw.get("purposes", [])
-        elif isinstance(purpose_point_raw, list):
-            purpose_points = purpose_point_raw
-        else:
-            # 兼容旧格式:直接读取 purpose_points(复数)
-            purpose_points = state.get("purpose_points", [])
-        
-        # 兼容 key_points 的多种格式:
-        # 1. 字典格式:{"key_points": [...], "total_count": ...}
-        # 2. 列表格式:[...](直接是关键点列表)
-        key_points_raw = state.get("key_points", {})
-        if isinstance(key_points_raw, dict):
-            key_points = key_points_raw.get("key_points", [])
-        elif isinstance(key_points_raw, list):
-            key_points = key_points_raw
-        else:
-            key_points = []
-
-        # 获取最终达标的实质元素(优先使用 substance_final_elements)
-        substance_final_elements = state.get("substance_final_elements", [])
-
-        if substance_final_elements:
-            concrete_elements = [
-                elem
-                for elem in substance_final_elements
-                if isinstance(elem, dict) and elem.get("维度", {}).get("二级") == "具体元素"
-            ]
-            concrete_concepts = [
-                elem
-                for elem in substance_final_elements
-                if isinstance(elem, dict) and elem.get("维度", {}).get("二级") == "具象概念"
-            ]
-            # 抽象概念
-            abstract_concepts = [
-                elem
-                for elem in substance_final_elements
-                if isinstance(elem, dict) and elem.get("维度", {}).get("二级") == "抽象概念"
-            ]
-            # 隐含概念相关逻辑已移除
-            implicit_concepts: List[dict] = []
-        else:
-            concrete_elements = state.get("concrete_elements", [])
-            concrete_concepts = state.get("concrete_concepts", [])
-            abstract_concepts = state.get("abstract_concepts", [])
-            # 隐含概念相关逻辑已移除,这里不再从 state 中读取
-            implicit_concepts: List[dict] = []
-
-        # 只保留形式类关键点
-        form_key_points = [
-            kp for kp in key_points
-            if isinstance(kp, dict) and kp.get("维度大类") == "形式"
-        ] if key_points else []
-
-        logger.info(
-            "输入: 具体元素=%d, 具象概念=%d, 抽象概念=%d, "
-            "灵感点=%d, 目的点=%d, 形式关键点=%d",
-            len(concrete_elements),
-            len(concrete_concepts),
-            len(abstract_concepts),
-            len(inspiration_points) if isinstance(inspiration_points, list) else 0,
-            len(purpose_points) if isinstance(purpose_points, list) else 0,
-            len(form_key_points),
-        )
-
-        # Step 1: 提取具象概念形式
-        logger.info("▶ Step 1: 提取具象概念形式")
-        concrete_concept_forms = self._step1_extract_concrete_concept_forms(
-            video_file, concrete_concepts
-        )
-        logger.info(
-            f"✅ Step 1 完成 - 提取了 {len(concrete_concept_forms)} 个具象概念形式"
-        )
-        if concrete_concept_forms:
-            logger.info(
-                f"   具象概念形式列表: {[f.get('名称', 'N/A') for f in concrete_concept_forms[:5]]}"
-                + (f" (共{len(concrete_concept_forms)}个)" if len(concrete_concept_forms) > 5 else "")
-            )
-
-        # Step 2: 提取具体元素形式
-        logger.info("▶ Step 2: 提取具体元素形式")
-        concrete_element_forms = self._step2_extract_concrete_element_forms(
-            video_file, concrete_elements, concrete_concepts
-        )
-        logger.info(
-            f"✅ Step 2 完成 - 提取了 {len(concrete_element_forms)} 个具体元素形式"
-        )
-        if concrete_element_forms:
-            logger.info(
-                f"   具体元素形式列表: {[f.get('名称', 'N/A') for f in concrete_element_forms[:5]]}"
-                + (f" (共{len(concrete_element_forms)}个)" if len(concrete_element_forms) > 5 else "")
-            )
-
-        # Step 3: 提取整体形式
-        logger.info("▶ Step 3: 提取整体形式")
-        overall_forms = self._step3_extract_overall_forms(
-            video_file,
-            concrete_elements,
-            concrete_concepts,
-            abstract_concepts,
-            implicit_concepts,
-        )
-        logger.info(
-            f"✅ Step 3 完成 - 提取了 {len(overall_forms)} 个整体形式"
-        )
-        if overall_forms:
-            logger.info(
-                f"   整体形式列表: {[f.get('名称', 'N/A') for f in overall_forms[:5]]}"
-                + (f" (共{len(overall_forms)}个)" if len(overall_forms) > 5 else "")
-            )
-
-        # 合并所有形式元素
-        all_form_elements = (
-            concrete_concept_forms + concrete_element_forms + overall_forms
-        )
-
-        logger.info(
-            f"📊 Step 1-3 汇总 - 总计提取: {len(all_form_elements)} 个形式元素 "
-            f"(具象概念形式:{len(concrete_concept_forms)}, "
-            f"具体元素形式:{len(concrete_element_forms)}, "
-            f"整体形式:{len(overall_forms)})"
-        )
-
-        # Step 4: 共性分析(统计形式在视频中的出现频次)
-        logger.info("▶ Step 4: 共性分析")
-        analyzed_result = self._step4_commonality_analysis(
-            video_file,
-            all_form_elements,
-        )
-        logger.info(
-            f"✅ Step 4 完成 - 分析了 {len(analyzed_result)} 个形式元素的频次"
-        )
-        if analyzed_result:
-            # 统计频次分布
-            freq_distribution = {}
-            for item in analyzed_result:
-                if not isinstance(item, dict):
-                    continue
-                freq = item.get("出现频次", 0)
-                freq_distribution[freq] = freq_distribution.get(freq, 0) + 1
-            logger.info(
-                f"   频次分布: {dict(sorted(freq_distribution.items(), reverse=True)[:5])}"
-            )
-            # 显示高频元素
-            dict_items = [item for item in analyzed_result if isinstance(item, dict)]
-            high_freq_items = sorted(dict_items, key=lambda x: x.get("出现频次", 0), reverse=True)[:3]
-            if high_freq_items:
-                logger.info(
-                    f"   高频元素(前3): {[(item.get('名称', 'N/A'), item.get('出现频次', 0)) for item in high_freq_items]}"
-                )
-
-        # Step 5: 支撑判断
-        logger.info("▶ Step 5: 支撑判断")
-        scored_result = self._step5_multi_dimensional_scoring(
-            video_file, all_form_elements, inspiration_points, purpose_points, form_key_points
-        )
-        logger.info(
-            f"✅ Step 5 完成 - 支撑判断结果: "
-            f"灵感点={len(scored_result.get('灵感点', []))}个元素, "
-            f"目的点={len(scored_result.get('目的点', []))}个元素, "
-            f"关键点={len(scored_result.get('关键点', []))}个元素"
-        )
-        # 统计有支撑的元素数量
-        supported_element_ids = set()
-        for dimension in ["灵感点", "目的点", "关键点"]:
-            for item in scored_result.get(dimension, []):
-                if isinstance(item, dict) and item.get("支撑结果"):
-                    supported_element_ids.add(item.get("id"))
-        logger.info(
-            f"   有支撑的元素总数: {len(supported_element_ids)}/{len(all_form_elements)}"
-        )
-
-        # Step 6: 权重计算
-        logger.info("▶ Step 6: 权重计算")
-        weighted_result = self._step6_calculate_weights(
-            analyzed_result, scored_result, all_form_elements
-        )
-        logger.info(
-            f"✅ Step 6 完成 - 权重计算完成: {len(weighted_result)} 个元素"
-        )
-        if weighted_result:
-            # 统计权重分布
-            weight_distribution = {}
-            for weight_info in weighted_result.values():
-                weight = weight_info.get("权重分", 0)
-                weight_range = f"{int(weight // 10) * 10}-{int(weight // 10) * 10 + 9}"
-                weight_distribution[weight_range] = weight_distribution.get(weight_range, 0) + 1
-            logger.info(
-                f"   权重分布: {dict(sorted(weight_distribution.items()))}"
-            )
-
-        # Step 7: 筛选
-        logger.info("▶ Step 7: 筛选")
-        filtered_ids = self._step7_filter_elements(analyzed_result, scored_result, weighted_result)
-        logger.info(
-            f"✅ Step 7 完成 - 筛选结果: {len(filtered_ids)}/{len(all_form_elements)} 个元素通过筛选"
-        )
-        if filtered_ids:
-            filtered_elements_info = [
-                elem.get("名称", "N/A") 
-                for elem in all_form_elements 
-                if elem.get("id") in filtered_ids[:5]
-            ]
-            logger.info(
-                f"   通过筛选的元素(前5): {filtered_elements_info}"
-                + (f" (共{len(filtered_ids)}个)" if len(filtered_ids) > 5 else "")
-            )
-
-        # Step 8: 分类
-        logger.info("▶ Step 8: 分类")
-        categorized_result = self._step8_categorize_elements(
-            all_form_elements, filtered_ids
-        )
-        logger.info(
-            f"✅ Step 8 完成 - 分类结果: {len(categorized_result)} 个维度完成分类"
-        )
-        for dim_name, dim_result in categorized_result.items():
-            classifications = dim_result.get("元素分类", [])
-            logger.info(
-                f"   {dim_name}: {len(classifications)} 个元素完成分类"
-            )
-
-        # 合并最终信息
-        logger.info("▶ 合并最终信息")
-        final_elements = self._merge_all_info(
-            all_form_elements, scored_result, weighted_result,
-            filtered_ids, categorized_result
-        )
-        logger.info(
-            f"✅ 合并完成 - 最终元素数: {len(final_elements)}"
-        )
-        if final_elements:
-            # 按维度统计最终元素
-            final_dimension_stats = {}
-            for elem in final_elements:
-                dim = elem.get("维度", {}).get("二级", "未知")
-                final_dimension_stats[dim] = final_dimension_stats.get(dim, 0) + 1
-            logger.info(
-                f"   最终元素维度分布: {final_dimension_stats}"
-            )
-
-        logger.info(f"🎉 形式元素提取完成 - 最终元素数: {len(final_elements)}")
-
-        return {
-            "concrete_concept_forms": concrete_concept_forms,
-            "concrete_element_forms": concrete_element_forms,
-            "overall_forms": overall_forms,
-            "form_elements": all_form_elements,
-            "form_analyzed_result": analyzed_result,
-            "form_scored_result": scored_result,
-            "form_weighted_result": weighted_result,
-            "form_filtered_ids": filtered_ids,
-            "form_categorized_result": categorized_result,
-            "form_final_elements": final_elements
-        }
-
-    # ========== Step 1-3: 形式提取 ==========
-
-    def _step1_extract_concrete_concept_forms(
-        self,
-        video_file,
-        concrete_concepts: List[dict]
-    ) -> List[dict]:
-        """Step 1: 提取具象概念形式 - 视频中对具象概念的表达方式(画面文字 + 口播)"""
-        if not self.is_initialized:
-            self.initialize()
-
-        if not video_file:
-            logger.warning("⚠️  没有视频文件,跳过具象概念形式提取")
-            return []
-
-        if not concrete_concepts:
-            logger.warning("⚠️  没有具象概念,跳过具象概念形式提取")
-            return []
-
-        concepts_text = json.dumps([
-            {"id": c.get("id"), "名称": c.get("名称"), "描述": c.get("描述"), "来源": c.get("来源", []),  # 图片来源
-                "出现段落列表": [p.get("段落ID") for p in c.get("共性分析", {}).get("出现段落列表", [])]}
-            for c in concrete_concepts
-        ], ensure_ascii=False, indent=2)
-
-        prompt = f"""# 任务
-从视频中提取"具象概念形式"元素(关注画面中的文字呈现方式和口播表达方式)
-
-# 已提取的具象概念(供参考,不允许从中直接抄写内容)
-{concepts_text}
-
-# 核心定义
-
-## 具象概念形式
-- 定义:对具象概念的描述、修饰、表达方式(包括画面文字呈现方式 + 口播表达方式)
-- 特征:表达方式本身,而不是概念内容
-- 包含:
-  - 对概念的强调方式(如:反复重复、放大字号、加重语气)
-  - 画面文字呈现方式(如:字体、颜色、大小、位置、动画等)
-  - 口播语气特性(如:教学口吻、科普式表达、聊天式口吻、语气特征、语速、停顿等)
-  - 口播内容的修饰手法(如:夸张、比喻、反问、排比)
-  - 口播内容的叙述方式(如:第一人称、第二人称、对话体)
-  - 口播内容的节奏特征(如:语速快慢、停顿设计、重音、语气变化等)
-
-## 提取原则
-1. 只提取画面文字展示方式和口播表达方式,不涉及视觉呈现
-2. 必须依附于已提取的具象概念
-3. 使用"剥离测试":去掉表达手法后,概念本身仍存在
-4. **原子化命名**:形式名称必须是最原子的,不包含实质内容
-5. **建立关联关系**:通过"支撑"字段建立形式与实质的关联边
-6. **段落级别**:形式作用域至少是整个段落,不能仅针对单句
-
-## 作用域约束(重要)
-1. **段落级别作用域**(核心约束):
-   - 形式的作用域至少是**整个段落级别**,不能仅针对段落中的某一句话
-   - 必须是该段落的整体画面文字呈现方式和口播表达方式,而非局部单句的特征
-2. **同段落作用域**:形式必须与其支撑的具象概念在同一段落中
-3. **事实对应关系**:形式对概念的支撑必须在视频中有明确的对应关系
-   - 不能仅凭推测建立支撑关系,必须实际看到这种支撑
-
-# 输出json结构
-[
-  {{
-    "id": "从1开始的自增序列",
-    "名称": "单一名词或短语(必须是原子化的形式名称)",
-    "描述": "说明形式的本质是什么,以及在视频中的具体定义",
-    "维度": {{"一级": "形式", "二级": "具象概念形式"}},
-    "支撑": [{{"id":"具象概念id", "名称":"具象概念名称"}}],
-    "推理": "在文字中的具体体现方式(引用视频中的说法或呈现方式)",
-    "段落来源": ["段落ID1", "段落ID2"]
-  }}
-]
-"""
-
-        result = LLMInvoker.safe_invoke_video_analysis(
-            operation_name="具象概念形式提取",
-            video_file=video_file,
-            prompt=prompt,
-            agent=self,
-            fallback=[]
-        )
-
-        for idx, form in enumerate(result, 1):
-            form["id"] = f"具象概念形式-{idx}"
-
-        logger.info(f"Step 1 完成 - 提取了 {len(result)} 个具象概念形式")
-        if result:
-            logger.debug(f"   具象概念形式详情: {[{'id': r.get('id'), '名称': r.get('名称')} for r in result[:3]]}")
-        return result
-
-    def _step2_extract_concrete_element_forms(
-        self,
-        video_file,
-        concrete_elements: List[dict],
-        concrete_concepts: List[dict]
-    ) -> List[dict]:
-        """Step 2: 提取具体元素形式 - 视觉呈现+映射关系(包括独立描述和映射关系)"""
-        if not self.is_initialized:
-            self.initialize()
-
-        if not video_file:
-            logger.warning("⚠️  没有视频文件,跳过具体元素形式提取")
-            return []
-
-        elements_text = json.dumps([
-            {"id": e.get("id"), "名称": e.get("名称"), "描述": e.get("描述"),
-                "来源": e.get("来源", []),  # 图片来源
-                "出现段落列表": [p.get("段落ID") for p in e.get("共性分析", {}).get("出现段落列表", [])]
-            }
-            for e in concrete_elements
-        ], ensure_ascii=False, indent=2) if concrete_elements else "无"
-
-        concepts_text = json.dumps([
-            {"id": c.get("id"), "名称": c.get("名称"), "描述": c.get("描述"),
-                "来源": c.get("来源", []),  # 图片来源
-                "出现段落列表": [p.get("段落ID") for p in c.get("共性分析", {}).get("出现段落列表", [])]
-            }
-            for c in concrete_concepts
-        ], ensure_ascii=False, indent=2) if concrete_concepts else "无"
-
-        prompt = f"""# 任务
-从视频中提取"具体元素形式"元素
-
-# 已提取的具体元素
-{elements_text}
-
-# 已提取的具象概念
-{concepts_text}
-
-# 核心定义
-
-## 具体元素形式
-- **定义**:视觉画面的形式,支撑具体元素、具象概念的视觉呈现以及背景音乐、音效等非口播内容的声音呈现方式
-- **特征**:图像的视觉表现手法、外观特征或者背景音乐和音效的类别、风格、节奏等
-- **范围**:构图、色彩、光影、质感、视觉技巧等图片层面的特征或者声音方面的特征
-- **重要**:不关注画面里的文字以及口播内容说了什么,只关注视觉呈现以及声音呈现方式
-
-## 提取原则
-1. **视觉性**:必须是画面层面或者背景音乐音效层面的形式,而非文字层面或者口播内容层面
-2. **边界明确**:不关注画面里的文字以及口播内容,只关注视觉呈现手法或者声音呈现方式
-3. **支撑性**:可以支撑具体元素,也可以支撑具象概念
-4. **语义完整性**:必须提取具有完整独立语义的形式特征
-5. 使用"剥离测试":去掉表达方式后,元素本身仍存在
-6. **段落级别**:形式作用域至少是整个段落,不能仅针对单个元素
-
-## 作用域约束(重要)
-1. **段落级别作用域**(核心约束):
-   - 形式的作用域至少是**整个段落级别**,不能仅针对段落中的某一句话
-   - 必须是该段落的整体呈现方式,而非局部单个元素的特征
-2. **同段落作用域**:形式必须与其支撑的实质元素/概念在同一段落中
-3. **事实对应关系**:形式对实质的支撑必须在视频中有明确的视觉对应关系
-   - 不能仅凭推测建立支撑关系,必须在画面中实际看到这种支撑
-
-# 命名与关联分离原则(重要)
-
-## 核心规则
-- **名称**:只描述形式手法本身(纯形式词汇)
-- **支撑**:说明这个形式手法应用在哪些实质内容上(通过id关联具体元素/具象概念)
-- **推理**:说明具体如何体现(在图像中的呈现细节或者声音方面的呈现方式)
-- **段落来源**:记录形式来自哪个段落
-
-## 关键要求
-1. **名称的纯粹性**:名称中不能包含任何实质内容(具体元素名称、具象概念名称等)
-2. **通过支撑建立关联**:实质内容通过"支撑"字段的id关联,而非写在名称中
-3. **推理的具体性**:在推理中详细说明这个形式手法如何应用在支撑的实质内容上
-4. **来源的准确性**:图片来源和段落来源必须与支撑的实质元素/概念一致
-
-# 输出json结构
-[
-    {{
-      "id": "从1开始的自增序列",
-      "名称": "单一名词或短语(必须是原子化的纯形式词汇)",
-      "描述": "说明形式的本质及在帖子中的具体定义",
-      "维度": {{"一级": "形式", "二级": "具体元素形式"}},
-      "支撑": {{
-        "具体元素": [{{"id":"元素id", "名称":"元素名称"}}],
-        "具象概念": [{{"id":"概念id", "名称":"概念名称"}}]
-      }},
-      "推理": "在画面中或者声音方面的具体体现(说明这个形式手法如何应用在支撑的实质内容上)",
-      "段落来源": ["段落ID1", "段落ID2"]
-    }}
-]
-
-注意:
-1. 支撑中"具体元素"必填,"具象概念"可选
-2. 名称必须是纯形式词汇,不能包含任何实质内容
-3. 段落来源必须与支撑的实质元素/概念的来源一致
-4. **作用域约束**:形式必须是段落级别的整体特征,不能是单个元素的局部视觉特征
-5. 只输出验证全部通过的形式元素
-"""
-
-        result = LLMInvoker.safe_invoke_video_analysis(
-            operation_name="具体元素形式提取",
-            video_file=video_file,
-            prompt=prompt,
-            agent=self,
-            fallback=[]
-        )
-
-        for idx, form in enumerate(result, 1):
-            form["id"] = f"具体元素形式-{idx}"
-
-        logger.info(f"Step 2 完成 - 提取了 {len(result)} 个具体元素形式")
-        if result:
-            logger.debug(f"   具体元素形式详情: {[{'id': r.get('id'), '名称': r.get('名称')} for r in result[:3]]}")
-        return result
-
-    def _step3_extract_overall_forms(
-        self,
-        video_file,
-        concrete_elements: List[dict],
-        concrete_concepts: List[dict],
-        abstract_concepts: List[dict],
-        implicit_concepts: List[dict],
-    ) -> List[dict]:
-        """Step 3: 提取整体形式 - 多模态组合方式、结构特征"""
-        if not self.is_initialized:
-            self.initialize()
-
-        if not video_file:
-            logger.warning("⚠️  没有视频文件,跳过整体形式提取")
-            return []
-
-        elements_text = json.dumps(
-            [
-                {"id": e.get("id"), "名称": e.get("名称"), "描述": e.get("描述"),
-                "来源": e.get("来源", []),
-                "出现段落列表": [p.get("段落ID") for p in e.get("共性分析", {}).get("出现段落列表", [])]
-            }
-                for e in concrete_elements
-            ],
-            ensure_ascii=False,
-            indent=2,
-        ) if concrete_elements else "无"
-
-        concepts_text = json.dumps(
-            [
-                {"id": c.get("id"), "名称": c.get("名称"), "描述": c.get("描述"),
-                "来源": c.get("来源", []),
-                "出现段落列表": [p.get("段落ID") for p in c.get("共性分析", {}).get("出现段落列表", [])]
-            }
-                for c in concrete_concepts
-            ],
-            ensure_ascii=False,
-            indent=2,
-        ) if concrete_concepts else "无"
-
-        abstracts_text = json.dumps(
-            [
-                {"id": a.get("id"), "名称": a.get("名称"), "描述": a.get("描述"),
-                "来源": a.get("来源", []),
-                "出现段落列表": [p.get("段落ID") for p in a.get("共性分析", {}).get("出现段落列表", [])]
-            }
-                for a in abstract_concepts
-            ],
-            ensure_ascii=False,
-            indent=2,
-        ) if abstract_concepts else "无"
-
-        prompt = f"""# 任务
-从视频中提取"整体形式"元素
-
-# 已提取的具体元素
-{elements_text}
-
-# 已提取的具象概念
-{concepts_text}
-
-# 已提取的抽象概念
-{abstracts_text}
-
-# 核心定义
-
-## 整体形式
-- 定义:多模态内容的组合方式、结构特征、整体呈现策略
-- 特点:不针对单个元素/概念,而是整体组织方式
-
-## 提取原则
-1. 整体性:必须是整体层面的特征,不是局部细节
-2. 形式性:使用"剥离测试" - 去掉表达方式后内容失去结构
-3. 可命名性:能用简洁的名词/短语概括
-4. 避免重复:与具体元素形式、具象概念形式区分开
-7. **宏观层面**:作用范围至少是整个段落或多段落,不能是单句或局部内容
-
-## 作用域约束(重要)
-1. **段落级别作用域**(核心约束):
-   - 形式的作用域至少是**整个段落级别**,不能仅针对段落中的局部内容
-   - 整体形式通常作用于整个帖子或多个段落,必须是宏观层面的特征
-2. **支撑关系**:整体形式支撑特定实质元素时,必须记录这些元素所在的段落
-3. **来源标注**:标注整体形式主要体现在哪些段落中
-
-## 常见维度参考(不限于此)
-- 多模态组合:画面+口播+字幕的配比、关系、时序、模态切换
-- 结构方式:内容组织逻辑(并列/递进/对比/因果/总分/时间/空间顺序等)
-- 呈现策略:整体表达风格(教程式/故事式/问答式/列表式/对话式等)
-- 节奏控制:信息密度、镜头节奏、高潮设计
-- 视角设计:叙述视角(第一人称/第二人称/第三人称)
-- 互动方式:对观众的提问、召回、引导
-
-# 输出json结构
-[
-  {{
-    "id": "从1开始的自增序列",
-    "名称": "单一名词或短语(简洁准确)",
-    "描述": "说明形式的本质是什么,以及在视频中的具体定义",
-    "维度": {{"一级": "形式", "二级": "整体形式"}},
-    "支撑": {{
-      "具体元素": [{{"id":"元素id1", "名称":"元素名称1"}}, {{"id":"元素id2", "名称":"元素名称2"}}],
-      "具象概念": [{{"id":"概念id1", "名称":"概念名称1"}}, {{"id":"概念id2", "名称":"概念名称2"}}]
-    }},
-    "推理": "在视频中的具体体现(结合镜头、节奏、结构说明)",
-    "段落来源": ["段落ID1", "段落ID2"]
-  }}
-]
-"""
-
-        result = LLMInvoker.safe_invoke_video_analysis(
-            operation_name="整体形式提取",
-            video_file=video_file,
-            prompt=prompt,
-            agent=self,
-            fallback=[]
-        )
-
-        for idx, form in enumerate(result, 1):
-            form["id"] = f"整体形式-{idx}"
-
-        logger.info(f"Step 3 完成 - 提取了 {len(result)} 个整体形式")
-        if result:
-            logger.debug(f"   整体形式详情: {[{'id': r.get('id'), '名称': r.get('名称')} for r in result[:3]]}")
-        return result
-
-    # ========== Step 4-7: 后续处理 ==========
-
-    def _step4_commonality_analysis(
-        self,
-        video_file,
-        form_elements: List[dict],
-    ) -> List[dict]:
-        """Step 4: 共性分析 - 基于视频统计形式在全文中的出现频次"""
-        if not form_elements:
-            return []
-
-        if not video_file:
-            logger.warning("⚠️  没有视频文件,跳过形式共性分析")
-            return []
-
-        if not self.is_initialized:
-            self.initialize()
-
-        elements_text = self._build_simple_items_text(form_elements)
-
-        prompt = f"""# 任务
-基于短视频内容,对已提取的形式元素进行"共性分析",统计每个形式元素在整个视频中的出现频次。
-
-# 已提取的形式元素列表
-{elements_text}
-
-# 统计规则
-
-## 1. 具象概念形式(文字表达方式)
-- **出现频次**: 统计该表达方式(如"反复重复强调"、"排比句式"、"教学口吻"等)
-  在视频中出现的总次数(包括口播和画面文字)。
-
-## 2. 具体元素形式(视觉呈现方式)
-- **出现频次**: 统计该视觉形式(如"顶视拍摄"、"暖色调光影"、"拼接画面"等)
-  在视频画面中被使用的总次数。
-
-## 3. 整体形式(整体结构 / 节奏 / 呈现策略)
-- **出现频次**: 统计该整体特征在视频中被体现的次数;
-  通常整体结构类形式频次为 1,但如果在不同段落中多次明显重复,也可以计为 >1。
-
-# 输出(JSON)
-[
-  {{
-    "id": "元素id",
-    "名称": "元素名称",
-    "出现频次": 0
-  }}
-]
-"""
-
-        analyzed_items = LLMInvoker.safe_invoke_video_analysis(
-            operation_name="形式共性分析",
-            video_file=video_file,
-            prompt=prompt,
-            agent=self,
-            fallback=[]
-        )
-
-        logger.info(f"共性分析完成 - 统计了 {len(analyzed_items)} 个形式元素的频次")
-        return analyzed_items
-
-    def _step5_multi_dimensional_scoring(
-        self,
-        video_file,
-        form_elements: List[dict],
-        inspiration_points: list,
-        purpose_points: list,
-        form_key_points: list
-    ) -> dict:
-        """Step 5: 支撑判断 - 按元素维度分别评估支撑关系"""
-        logger.info(f"【Step 5】开始支撑判断 (输入: {len(form_elements)} 个形式元素)")
-        logger.info(f"待评估的点: 灵感点={len(inspiration_points) if isinstance(inspiration_points, list) else 0}, 目的点={len(purpose_points) if isinstance(purpose_points, list) else 0}, 关键点={len(form_key_points) if isinstance(form_key_points, list) else 0}")
-
-        # 按二级维度分组(所有元素都参与评分)
-        dimension_groups = {
-            "具象概念形式": [],
-            "具体元素形式": [],
-            "整体形式": []
-        }
-
-        for elem in form_elements:
-            if not isinstance(elem, dict):
-                continue
-            second_level = elem.get("维度", {}).get("二级", "")
-            if second_level in dimension_groups:
-                dimension_groups[second_level].append(elem)
-
-        logger.info(f"维度分组: 具象概念形式={len(dimension_groups['具象概念形式'])}, 具体元素形式={len(dimension_groups['具体元素形式'])}, 整体形式={len(dimension_groups['整体形式'])}")
-
-        # 并行评估(3个维度 x 3个点类型 = 9个并行任务)
-        futures = {}
-
-        logger.info(f"开始提交评估任务...")
-
-        for dimension_name, elements in dimension_groups.items():
-            if not elements:
-                continue
-
-            # 评估灵感点
-            if inspiration_points and isinstance(inspiration_points, list) and len(inspiration_points) > 0:
-                key = (dimension_name, "灵感点")
-                logger.info(f"📤 提交任务: {dimension_name}-灵感点 ({len(elements)}个元素 x {len(inspiration_points)}个点)")
-                futures[key] = _GLOBAL_THREAD_POOL.submit(
-                    self._evaluate_support_by_dimension,
-                    video_file, dimension_name, elements, inspiration_points
-                )
-
-            # 评估目的点
-            if purpose_points and isinstance(purpose_points, list) and len(purpose_points) > 0:
-                key = (dimension_name, "目的点")
-                logger.info(f"📤 提交任务: {dimension_name}-目的点 ({len(elements)}个元素 x {len(purpose_points)}个点)")
-                futures[key] = _GLOBAL_THREAD_POOL.submit(
-                    self._evaluate_support_by_dimension,
-                    video_file, dimension_name, elements, purpose_points
-                )
-
-            # 评估关键点
-            if form_key_points and isinstance(form_key_points, list) and len(form_key_points) > 0:
-                key = (dimension_name, "关键点")
-                logger.info(f"📤 提交任务: {dimension_name}-关键点 ({len(elements)}个元素 x {len(form_key_points)}个点)")
-                futures[key] = _GLOBAL_THREAD_POOL.submit(
-                    self._evaluate_support_by_dimension,
-                    video_file, dimension_name, elements, form_key_points
-                )
-
-        logger.info(f"已提交 {len(futures)} 个评估任务,开始等待结果...")
-
-        # 收集结果(按点类型汇总)
-        result = {
-            "灵感点": [],
-            "目的点": [],
-            "关键点": []
-        }
-
-        for (dimension_name, point_type), future in futures.items():
-            try:
-                logger.info(f"⏳ 等待结果: {dimension_name}-{point_type}")
-                dimension_result = future.result()
-                if dimension_result:
-                    result[point_type].extend(dimension_result)
-                logger.info(f"✅ {dimension_name}-{point_type} 评估完成: {len(dimension_result)} 条支撑关系")
-            except Exception as e:
-                logger.error(f"❌ {dimension_name}-{point_type} 评估失败: {e}", exc_info=True)
-
-        logger.info(f"✅ Step 5 完成 - 灵感点支撑: {len(result['灵感点'])}, 目的点支撑: {len(result['目的点'])}, 关键点支撑: {len(result['关键点'])}")
-        return result
-
-    def _evaluate_support_by_dimension(
-        self,
-        video_file,
-        dimension_name: str,
-        elements: list,
-        points: list
-    ) -> list:
-        """按维度评估支撑关系(分批处理)"""
-        logger.info(f"🔍 [{dimension_name}] 开始评估: {len(elements)}个元素 x {len(points)}个点")
-
-        if not self.is_initialized:
-            self.initialize()
-
-        if not elements or not points:
-            logger.warning(f"⚠️  [{dimension_name}] 元素或点为空,跳过评估")
-            return []
-
-        if not video_file:
-            logger.warning(f"⚠️  [{dimension_name}] 没有视频文件,跳过评估")
-            return []
-
-        # 分批处理
-        num_elements = len(elements)
-        num_points = len(points)
-        max_batch_product = 100
-        max_elements_per_batch = max(1, int(max_batch_product / num_points))
-        num_batches = (num_elements + max_elements_per_batch - 1) // max_elements_per_batch
-
-        logger.info(f"📦 [{dimension_name}] 分批配置: 共{num_batches}批, 每批最多{max_elements_per_batch}个元素")
-
-        batch_futures = {}
-        for batch_idx in range(num_batches):
-            start_idx = batch_idx * max_elements_per_batch
-            end_idx = min(start_idx + max_elements_per_batch, num_elements)
-            batch_elements = elements[start_idx:end_idx]
-
-            logger.info(f"📤 [{dimension_name}] 提交批次{batch_idx + 1}/{num_batches}: {len(batch_elements)}个元素")
-            future = _GLOBAL_THREAD_POOL.submit(
-                self._evaluate_support_single_batch_by_dimension,
-                video_file, dimension_name, batch_elements, points
-            )
-            batch_futures[batch_idx] = future
-
-        # 收集结果
-        logger.info(f"⏳ [{dimension_name}] 等待{len(batch_futures)}个批次完成...")
-        all_results = []
-        for batch_idx, future in batch_futures.items():
-            try:
-                logger.info(f"⏳ [{dimension_name}] 等待批次{batch_idx + 1}结果...")
-                batch_result = future.result()
-                if batch_result:
-                    all_results.extend(batch_result)
-                logger.info(f"✅ [{dimension_name}] 批次{batch_idx + 1}完成: {len(batch_result) if batch_result else 0}条")
-            except Exception as e:
-                logger.error(f"❌ [{dimension_name}] 批次{batch_idx + 1}失败: {e}", exc_info=True)
-
-        # 合并结果
-        logger.info(f"🔄 [{dimension_name}] 合并{len(all_results)}条结果...")
-        merged_results = self._merge_batch_results_support(all_results)
-        logger.info(f"✅ [{dimension_name}] 评估完成: 最终{len(merged_results)}条支撑关系")
-        return merged_results
-
-    def _evaluate_support_single_batch_by_dimension(
-        self,
-        video_file,
-        dimension_name: str,
-        batch_elements: list,
-        points: list
-    ) -> list:
-        """单批次评估(按维度)"""
-        logger.info(f"🔧 [{dimension_name}] 批次评估开始: {len(batch_elements)}元素 x {len(points)}点")
-
-        if not self.is_initialized:
-            self.initialize()
-
-        elements_text = self._build_simple_items_text_dimension(batch_elements, dimension_name)
-        points_text = self._build_points_text("", points)
-
-        logger.info(f"📝 [{dimension_name}] 构建Prompt中...")
-
-        # 根据维度选择不同的 prompt
-        if dimension_name == "具象概念形式":
-            prompt = self._build_concrete_concept_form_prompt(elements_text, points_text)
-        elif dimension_name == "具体元素形式":
-            prompt = self._build_concrete_element_form_prompt(elements_text, points_text)
-        elif dimension_name == "整体形式":
-            prompt = self._build_overall_form_prompt(elements_text, points_text)
-        else:
-            logger.error(f"❌ 未知维度: {dimension_name}")
-            return []
-
-        logger.info(f"🤖 [{dimension_name}] 调用LLM中...")
-
-        result = LLMInvoker.safe_invoke_video_analysis(
-            operation_name=f"评估支撑-{dimension_name}",
-            video_file=video_file,
-            prompt=prompt,
-            agent=self,
-            fallback=[]
-        )
-
-        logger.info(f"✅ [{dimension_name}] LLM返回: {len(result) if result else 0}条结果")
-
-        return result
-
-    def _merge_batch_results_support(self, all_results: list) -> list:
-        """合并批次结果,直接返回所有支撑的元素-点对"""
-        if not all_results:
-            return []
-
-        merged_map = {}
-        for item in all_results:
-            if not isinstance(item, dict):
-                continue
-            element_id = item.get("id")
-            if element_id not in merged_map:
-                merged_map[element_id] = {
-                    "id": element_id,
-                    "名称": item.get("名称"),
-                    "支撑结果": []
-                }
-            if not merged_map[element_id]["支撑结果"]:
-                merged_map[element_id]["支撑结果"] = item.get("支撑结果", [])
-
-        return list(merged_map.values())
-
-    def _build_concrete_concept_form_prompt(self, elements_text: str, points_text: str) -> str:
-        """构建具象概念形式的支撑判断 prompt"""
-        return f"""# 具象概念形式列表
-
-{elements_text}
-
-# 点列表
-
-{points_text}
-
-# 任务
-
-判断每个**具象概念形式**是否对点有关键支撑
-
-## 具象概念形式定义(重要!)
-
-- **定义**:文字层面或口播层面的形式,支撑具象概念的表达方式(包括画面文字呈现方式 + 口播内容表达方式)
-- **特征**:文字的描述、修饰、表达手法或口播的语气、强调手法、修辞、叙述方式等
-- **范围**:语气、强调手法、修辞、叙述方式等文字层面的特征
-- **重要**:包括画面文字呈现方式和口播内容的表达方式
-
-## 核心判断原则:仅基于文字呈现方式或口播内容表达方式
-
-### 关键约束
-
-1. **只看文字呈现方式或口播内容表达方式**:具象概念形式的支撑判断**只能基于文字层面的表达方式或口播内容的表达方式**,不涉及视觉呈现
-
-2. **表达方式角色**:该表达方式在文字呈现方式或口播内容表达方式中的作用是什么?
-   - ✅ 核心表达手法:该表达方式对文字论述或口播内容至关重要
-   - ❌ 次要/常规:该表达方式只是常规表述、无关紧要
-
-3. **关键支撑**:该表达方式在文字呈现方式或口播内容表达方式对点的表达是否关键?去掉它是否会削弱点的支撑?
-
-### 判断流程
-
-1. 理解点的意图,想表达什么
-2. 在视频中找到该表达方式的体现(画面文字或口播)
-3. 判断:去掉该表达方式,是否无法或强烈削弱表达点的支撑
-   - 如果是,支撑
-   - 如果不是,不支撑
-
-### 严格标准
-
-- **禁止使用视觉判断**:不能因为画面呈现某视觉效果,就认为文字呈现方式或口播内容表达方式支撑该点
-- **禁止字面匹配**:不能因为名称相同就判定支撑
-- **关注表达手法**:必须判断该表达方式在文字呈现方式或口播内容表达方式中的实际作用
-
-# 输出(JSON)
-
-只输出有关键支撑的元素-点对,不支撑的不输出
-
-[
-    {{
-      "id": "元素id",
-      "名称": "元素名称",
-      "支撑结果": [
-        {{
-            "点": "点的名称",
-            "点的意图": "点想表达什么",
-            "支撑理由": "说明为什么去掉该表达方式,会削弱点的表达,程度达到30%以上"
-        }}
-      ]
-    }}
-]
-
-注意:
-
-1. **只基于文字呈现方式或口播内容表达方式**:不能用视觉呈现来判断具象概念形式的支撑
-2. 只输出"关键支撑"的元素-点对
-3. 次要/常规表达直接排除,不输出
-4. 必须基于文字呈现方式或口播内容表达方式判断,不能字面匹配"""
-
-    def _build_concrete_element_form_prompt(self, elements_text: str, points_text: str) -> str:
-        """构建具体元素形式的支撑判断 prompt"""
-        return f"""# 具体元素形式列表
-
-{elements_text}
-
-# 点列表
-
-{points_text}
-
-# 任务
-
-判断每个**具体元素形式**是否对点有关键支撑
-
-## 具体元素形式定义(重要!)
-
-- **定义**:视觉画面的形式,支撑具体元素、具象概念的视觉呈现以及背景音乐、音效等非口播内容的声音呈现方式
-- **特征**:图像的视觉表现手法、外观特征或者背景音乐和音效的类别、风格、节奏等
-- **范围**:构图、色彩、光影、质感、视觉技巧等图片层面的特征或者声音方面的特征
-- **重要**:不关注画面里的文字以及口播内容说了什么,只关注视觉呈现以及声音呈现方式
-
-## 核心判断原则:仅基于视觉呈现方式或声音呈现方式
-
-### 关键约束
-
-1. **只看视觉呈现方式或声音呈现方式**:具体元素形式的支撑判断**只能基于视频画面的视觉呈现方式或声音呈现方式**,不涉及文字论述或口播内容表达方式
-
-2. **视觉手法或声音手法角色**:该视觉手法或声音手法在画面呈现方式或声音呈现方式中的作用是什么?
-   - ✅ 核心视觉技巧或声音技巧:该视觉手法或声音手法对画面呈现或声音呈现至关重要
-   - ❌ 次要/常规:该视觉手法或声音手法只是常规呈现、无关紧要
-
-3. **关键支撑**:该视觉手法或声音手法在画面呈现方式或声音呈现方式对点的表达是否关键?去掉它是否会削弱点的支撑?
-
-### 判断流程
-
-1. 理解点的意图,想表达什么
-2. 在视频画面中找到该视觉手法或声音手法的体现
-3. 判断:去掉该视觉手法或声音手法,是否无法或强烈削弱表达点的支撑
-   - 如果是,支撑
-   - 如果不是,不支撑
-
-### 严格标准
-
-- **禁止使用文字论述或口播内容表达方式判断**:不能因为口播或文字提到某概念,就认为视觉手法或声音手法支撑该概念
-- **禁止字面匹配**:不能因为名称相同就判定支撑
-- **关注视觉技巧或声音技巧**:必须判断该视觉手法或声音手法在画面呈现方式或声音呈现方式中的实际作用
-
-# 输出(JSON)
-
-只输出有关键支撑的元素-点对,不支撑的不输出
-
-[
-    {{
-      "id": "元素id",
-      "名称": "元素名称",
-      "支撑结果": [
-        {{
-            "点": "点的名称",
-            "点的意图": "点想表达什么",
-            "支撑理由": "说明为什么去掉该视觉手法,会削弱点的表达,程度达到30%以上"
-        }}
-      ]
-    }}
-]
-
-注意:
-
-1. **只基于视觉呈现方式或声音呈现方式**:不能用文字论述或口播内容表达方式来判断具体元素形式的支撑
-2. 只输出"关键支撑"的元素-点对
-3. 次要/常规视觉手法直接排除,不输出
-4. 必须基于视觉呈现方式或声音呈现方式判断,不能字面匹配"""
-
-    def _build_overall_form_prompt(self, elements_text: str, points_text: str) -> str:
-        """构建整体形式的支撑判断 prompt"""
-        return f"""# 整体形式列表
-
-{elements_text}
-
-# 点列表
-
-{points_text}
-
-# 任务
-
-判断每个**整体形式**是否对点有关键支撑
-
-## 整体形式定义(重要!)
-
-- **定义**:多模态内容的组合方式、结构特征、整体呈现策略
-- **特点**:不针对具体元素/概念,描述整体组织方式
-- **范围**:图文配比、内容组织、呈现策略、节奏控制等整体层面的特征
-
-## 核心判断原则:基于整体组织方式
-
-### 关键约束
-
-1. **整体性**:整体形式的支撑判断**必须基于宏观层面的组织特征**,不是局部细节
-
-2. **组织策略角色**:该组织策略在内容呈现中的作用是什么?
-   - ✅ 核心组织方式:该组织策略对内容呈现至关重要
-   - ❌ 次要/常规:该组织策略只是常规组织、无关紧要
-
-3. **关键支撑**:该组织策略对点的表达是否关键?去掉它是否会削弱点的支撑?
-
-### 判断流程
-
-1. 理解点的意图,想表达什么
-2. 识别该整体组织策略在视频中的体现
-3. 判断:去掉该组织策略,是否无法表达点
-   - 如果是,支撑
-   - 如果不是,不支撑
-
-### 严格标准
-
-- **禁止局部判断**:不能因为局部细节有某特征,就认为整体形式支撑该点
-- **禁止字面匹配**:不能因为名称相同就判定支撑
-- **关注整体策略**:必须判断该整体组织策略在宏观呈现中的实际作用
-
-# 输出(JSON)
-
-只输出有关键支撑的元素-点对,不支撑的不输出
-
-[
-    {{
-      "id": "元素id",
-      "名称": "元素名称",
-      "支撑结果": [
-        {{
-            "点": "点的名称",
-            "点的意图": "点想表达什么",
-            "支撑理由": "说明为什么去掉该组织策略,会削弱点的表达,程度达到30%以上"
-        }}
-      ]
-    }}
-]
-
-注意:
-
-1. **只基于整体组织**:必须基于宏观层面的组织策略来判断
-2. 只输出"关键支撑"的元素-点对
-3. 次要/常规组织方式直接排除,不输出
-4. 必须基于整体组织策略判断,不能字面匹配"""
-
-    def _step6_calculate_weights(
-        self,
-        analyzed_result: list,
-        scored_result: dict,
-        all_form_elements: list
-    ) -> dict:
-        """Step 6: 计算权重分(纯代码实现,不调用LLM)
-
-        对所有形式元素计算权重
-
-        动态自适应权重策略(总分100分):
-
-        【共性维度 50分】(适应不同视频的段落数差异)
-        加权公式: 频次40% + 覆盖段落数30% + 覆盖率30%
-
-        1. 频次分 (基础0-50分 × 40%)
-           - 1次=0分, 2次=10分, 3次=20分, 4次=30分, 5次=40分, ≥6次=50分
-           - 实际得分 = 基础分 × 0.4 (最高20分)
-
-        2. 覆盖段落数分 (基础0-50分 × 30%)
-           - 0段=0分, 1段=15分, 2段=30分, 3段=40分, ≥4段=50分
-           - 实际得分 = 基础分 × 0.3 (最高15分)
-
-        3. 覆盖率分 (基础0-50分 × 30%)
-           - 覆盖率 × 50 = 基础分 (如50%覆盖率=25分, 100%覆盖率=50分)
-           - 实际得分 = 基础分 × 0.3 (最高15分)
-
-        【支撑维度 50分】(动态分配,确保灵感点 > 目的点 ≈ 关键点)
-        总池分配: 灵感点20分(40%) + 目的点15分(30%) + 关键点15分(30%)
-
-        计算方式:
-           - 每个点的分值 = 总池分数 / 该类型点的实际总数量
-           - 元素得分 = Σ(元素支撑的每个点的分值)
-           - 示例: 2个灵感点→每个10分, 3个目的点→每个5分, 5个关键点→每个3分
-        """
-        logger.info(f"【Step 6】开始计算权重 (输入: {len(all_form_elements)} 个形式元素)")
-
-        if not analyzed_result or not all_form_elements:
-            logger.warning("⚠️  没有数据,跳过权重计算")
-            return {}
-
-        # 创建映射
-        analyzed_map = {item.get("id"): item for item in analyzed_result if isinstance(item, dict)}
-
-        # 统计每个元素支撑的点,并收集所有被支撑的点
-        element_support_count = {}
-        supported_points = {
-            "灵感点": set(),
-            "目的点": set(),
-            "关键点": set()
-        }
-
-        # 遍历scored_result,统计每个元素支撑了哪些点
-        for point_type in ["灵感点", "目的点", "关键点"]:
-            dimension_data = scored_result.get(point_type, [])
-            for item in dimension_data:
-                if not isinstance(item, dict):
-                    continue
-                element_id = item.get("id")
-                support_results = item.get("支撑结果", [])
-
-                if element_id not in element_support_count:
-                    element_support_count[element_id] = {
-                        "灵感点": [],
-                        "目的点": [],
-                        "关键点": []
-                    }
-
-                # 记录该元素支撑的具体点
-                for support_item in support_results:
-                    if not isinstance(support_item, dict):
-                        continue
-                    point_name = support_item.get("点", "")
-                    if point_name:
-                        element_support_count[element_id][point_type].append(point_name)
-                        supported_points[point_type].add(point_name)
-
-        # 计算每种类型点的总数量(只统计有被支撑的点)
-        total_inspiration_points = len(supported_points["灵感点"])
-        total_purpose_points = len(supported_points["目的点"])
-        total_key_points = len(supported_points["关键点"])
-
-        logger.info(f"📊 点数量统计: 灵感点={total_inspiration_points}, 目的点={total_purpose_points}, 关键点={total_key_points}")
-
-        # 动态计算每个点的分值(按总池分配)
-        inspiration_pool = 20  # 灵感点总池
-        purpose_pool = 15      # 目的点总池
-        key_pool = 15          # 关键点总池
-
-        # 计算每个点的单位分值
-        inspiration_unit_score = inspiration_pool / total_inspiration_points if total_inspiration_points > 0 else 0
-        purpose_unit_score = purpose_pool / total_purpose_points if total_purpose_points > 0 else 0
-        key_unit_score = key_pool / total_key_points if total_key_points > 0 else 0
-
-        logger.info(
-            f"💰 单位分值: 灵感点={inspiration_unit_score:.2f}分/个 (总池{inspiration_pool}分), "
-            f"目的点={purpose_unit_score:.2f}分/个 (总池{purpose_pool}分), "
-            f"关键点={key_unit_score:.2f}分/个 (总池{key_pool}分)"
-        )
-
-        # 计算权重
-        weighted_results = {}
-
-        # 对所有形式元素计算权重
-        for element in all_form_elements:
-            element_id = element.get("id")
-            if element_id not in analyzed_map:
-                logger.warning(f"元素 {element_id} 不在 analyzed_map 中,跳过权重计算")
-                continue
-
-            analyzed_data = analyzed_map[element_id]
-            element_name = analyzed_data.get("名称", "N/A")
-            frequency = analyzed_data.get("出现频次", 0)
-            coverage_rate = analyzed_data.get("段落覆盖率", 0)
-            coverage_count = analyzed_data.get("出现段落数", 0)
-
-            # ========== 共性维度 (50分) ==========
-            # 三个指标加权计算,适应不同视频的段落数差异
-
-            # 1. 频次分 (0-50分的基础分)
-            if frequency <= 1:
-                frequency_base_score = 0
-            elif frequency == 2:
-                frequency_base_score = 10
-            elif frequency == 3:
-                frequency_base_score = 20
-            elif frequency == 4:
-                frequency_base_score = 30
-            elif frequency == 5:
-                frequency_base_score = 40
-            else:
-                frequency_base_score = 50
-
-            # 2. 覆盖段落数分 (0-50分的基础分)
-            if coverage_count == 0:
-                coverage_count_base_score = 0
-            elif coverage_count == 1:
-                coverage_count_base_score = 15
-            elif coverage_count == 2:
-                coverage_count_base_score = 30
-            elif coverage_count == 3:
-                coverage_count_base_score = 40
-            else:  # >= 4
-                coverage_count_base_score = 50
-
-            # 3. 覆盖率分 (0-50分的基础分)
-            coverage_rate_base_score = coverage_rate * 50
-
-            # 加权策略:频次40% + 覆盖段落数30% + 覆盖率30%
-            frequency_score = frequency_base_score * 0.4
-            coverage_count_score = coverage_count_base_score * 0.3
-            coverage_rate_score = coverage_rate_base_score * 0.3
-
-            # 共性总分 (最高50分)
-            commonality_score = frequency_score + coverage_count_score + coverage_rate_score
-
-            # ========== 支撑维度 (50分) ==========
-            support_lists = element_support_count.get(element_id, {
-                "灵感点": [],
-                "目的点": [],
-                "关键点": []
-            })
-
-            inspiration_points = support_lists.get("灵感点", [])
-            purpose_points = support_lists.get("目的点", [])
-            key_points = support_lists.get("关键点", [])
-
-            inspiration_count = len(inspiration_points)
-            purpose_count = len(purpose_points)
-            key_count = len(key_points)
-
-            # 动态计算支撑点权重 (0-50分)
-            inspiration_score = inspiration_count * inspiration_unit_score
-            purpose_score = purpose_count * purpose_unit_score
-            key_score = key_count * key_unit_score
-
-            # 计算总分
-            total_score = (
-                frequency_score +
-                coverage_count_score +
-                coverage_rate_score +
-                inspiration_score +
-                purpose_score +
-                key_score
-            )
-
-            # 归一化到100分
-            normalized_score = round(min(100, total_score), 2)
-
-            weighted_results[element_id] = {
-                "id": element_id,
-                "名称": element_name,
-                "权重分": normalized_score,
-                "权重明细": {
-                    "频次分": round(frequency_score, 2),
-                    "覆盖段落数分": round(coverage_count_score, 2),
-                    "覆盖率分": round(coverage_rate_score, 2),
-                    "灵感点支撑分": round(inspiration_score, 2),
-                    "目的点支撑分": round(purpose_score, 2),
-                    "关键点支撑分": round(key_score, 2),
-                    "共性总分": round(commonality_score, 2),
-                    "支撑总分": round(inspiration_score + purpose_score + key_score, 2)
-                },
-                "支撑统计": {
-                    "灵感点数量": inspiration_count,
-                    "目的点数量": purpose_count,
-                    "关键点数量": key_count
-                }
-            }
-
-            logger.info(
-                f"⚖️  权重计算: id={element_id}, name={element_name}, "
-                f"总分={normalized_score:.2f} "
-                f"[共性={commonality_score:.2f}(频次{frequency_score:.2f}+段落数{coverage_count_score:.2f}+覆盖率{coverage_rate_score:.2f}), "
-                f"支撑={inspiration_score + purpose_score + key_score:.2f}(灵感{inspiration_score:.2f}×{inspiration_count}+目的{purpose_score:.2f}×{purpose_count}+关键{key_score:.2f}×{key_count})]"
-            )
-
-        logger.info(f"✅ Step 6 完成 - 权重计算完成: 共计算 {len(weighted_results)} 个元素的权重")
-        return weighted_results
-
-    def _step7_filter_elements(
-        self,
-        analyzed_result: list,
-        scored_result: dict,
-        weighted_result: dict
-    ) -> list:
-        """Step 7: 筛选形式元素
-
-        保留条件:权重分 >= 40
-        """
-        logger.info(f"【Step 7】开始筛选 (输入: {len(weighted_result)} 个元素)")
-
-        if not weighted_result:
-            logger.warning("⚠️  没有权重结果,跳过筛选")
-            return []
-
-        # 筛选
-        filtered_ids = []
-        rejected_ids = []
-
-        for element_id, weight_info in weighted_result.items():
-            element_name = weight_info.get("名称", "N/A")
-            weight_score = weight_info.get("权重分", 0)
-
-            # 筛选条件: 权重分 >= 40
-            if weight_score >= 40:
-                filtered_ids.append(element_id)
-                logger.info(f"✅ 保留: id={element_id}, name={element_name}, 权重分={weight_score}")
-            else:
-                rejected_ids.append(element_id)
-                logger.info(f"❌ 过滤: id={element_id}, name={element_name}, 权重分={weight_score} (< 40)")
-
-        logger.info(f"✅ Step 7 完成 - 筛选完成: {len(filtered_ids)}/{len(weighted_result)} 通过 (权重分>=40)")
-        return filtered_ids
-
-    def _step8_categorize_elements(
-        self,
-        form_elements: List[dict],
-        filtered_ids: list
-    ) -> dict:
-        """Step 7: 元素分类 - 按二级维度分别分类"""
-        logger.info(f"【分类】输入: {len(filtered_ids)} 个筛选后的形式元素")
-
-        if not filtered_ids:
-            return {}
-
-        filtered_elements = [
-            elem for elem in form_elements
-            if isinstance(elem, dict) and elem.get("id") in filtered_ids
-        ]
-
-        dimension_groups: Dict[str, List[dict]] = {
-            "具象概念形式": [],
-            "具体元素形式": [],
-            "整体形式": []
-        }
-
-        for elem in filtered_elements:
-            if not isinstance(elem, dict):
-                continue
-            second_level = elem.get("维度", {}).get("二级", "")
-            if second_level in dimension_groups:
-                dimension_groups[second_level].append(elem)
-
-        categorization_results: Dict[str, Any] = {}
-        futures = {}
-
-        for dimension_name, elements in dimension_groups.items():
-            if not elements:
-                continue
-
-            future = _GLOBAL_THREAD_POOL.submit(
-                self._categorize_single_dimension,
-                dimension_name,
-                elements
-            )
-            futures[dimension_name] = future
-
-        for dimension_name, future in futures.items():
-            try:
-                categorization_results[dimension_name] = future.result()
-            except Exception as e:
-                logger.error(f"{dimension_name} 分类失败: {e}")
-                categorization_results[dimension_name] = {"元素分类": []}
-
-        logger.info(f"分类完成: {len(categorization_results)} 个维度")
-        # 统计每个维度的分类情况
-        for dim_name, dim_result in categorization_results.items():
-            classifications = dim_result.get("元素分类", [])
-            classified_count = sum(1 for c in classifications if c.get("分类"))
-            logger.info(
-                f"   {dim_name}: {classified_count}/{len(classifications)} 个元素有分类"
-            )
-        return categorization_results
-
-    def _categorize_single_dimension(
-        self,
-        dimension_name: str,
-        elements: list
-    ) -> dict:
-        """对单个维度的元素进行分类"""
-        if not self.is_initialized:
-            self.initialize()
-
-        elements_text = json.dumps([
-            {"id": elem.get("id"), "名称": elem.get("名称"), "描述": elem.get("描述")}
-            for elem in elements if isinstance(elem, dict)
-        ], ensure_ascii=False, indent=2)
-
-        prompt = f"""# 任务
-对"{dimension_name}"维度的元素进行分类
-
-# 元素列表
-{elements_text}
-
-# 分类要求
-
-## 核心原则
-1. 单一原子名词: 分类名称必须是单一的原子名词
-2. MECE原则: 分类之间相互独立、完全穷尽
-3. 确定性归属: 每个元素只能归属唯一一个分类
-4. 层级限制: 最多2层
-5. 元素可以没有分类,不要强行归类
-6. 分类下面至少要有2个元素,否则不要分类
-
-## 形式维度的分类逻辑
-- 核心原则:按照表现手法、呈现方式、修辞技巧等角度来分类
-
-# 输出格式(JSON)
-{{
-  "元素分类": [
-    {{
-      "元素id": "元素的ID",
-      "元素名称": "元素名称",
-      "分类": ["一级分类","二级分类","..."]
-    }}
-  ]
-}}
-"""
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": prompt}
-        ]
-
-        result = LLMInvoker.safe_invoke(
-            self,
-            f"分类-{dimension_name}",
-            messages,
-            fallback={}
-        )
-
-        return result
-
-    def _merge_all_info(
-        self,
-        form_elements: List[dict],
-        scored_result: dict,
-        weighted_result: dict,
-        filtered_ids: list,
-        categorized_result: dict
-    ) -> list:
-        """合并所有信息 - 每个元素包含所有中间信息"""
-        logger.info(f"【合并信息】输入: {len(filtered_ids)} 个筛选后的元素ID")
-
-        if not filtered_ids:
-            return []
-
-        extraction_map = {item.get("id"): item for item in form_elements if isinstance(item, dict)}
-
-        scored_map: Dict[str, Dict[str, list]] = {}
-        for dimension in ["灵感点", "目的点", "关键点"]:
-            dimension_data = scored_result.get(dimension, [])
-            for item in dimension_data:
-                if not isinstance(item, dict):
-                    continue
-                element_id = item.get("id")
-                if element_id not in scored_map:
-                    scored_map[element_id] = {}
-                support_results = item.get("支撑结果", [])
-                scored_map[element_id][dimension] = support_results
-
-        category_map: Dict[str, Any] = {}
-        for dimension_data in categorized_result.values():
-            if not isinstance(dimension_data, dict):
-                continue
-            element_classifications = dimension_data.get("元素分类", [])
-            for classification in element_classifications:
-                if not isinstance(classification, dict):
-                    continue
-                element_id = classification.get("元素id")
-                category_info = classification.get("分类", {})
-                if element_id:
-                    category_map[element_id] = category_info
-
-        final_elements = []
-        for element_id in filtered_ids:
-            base_info = extraction_map.get(element_id, {})
-            scoring_info = scored_map.get(element_id, {})
-            weight_info = weighted_result.get(element_id, {})
-            category_info = category_map.get(element_id, {})
-
-            merged_element = {
-                "id": base_info.get("id"),
-                "名称": base_info.get("名称"),
-                "描述": base_info.get("描述"),
-                "维度": base_info.get("维度", {}),
-                "分类": category_info,
-                "共性分析": None,
-                "意图支撑": {
-                    "灵感点": scoring_info.get("灵感点", []),
-                    "目的点": scoring_info.get("目的点", []),
-                    "关键点": scoring_info.get("关键点", [])
-                }
-            }
-
-            # 添加权重信息
-            if weight_info:
-                merged_element["权重分"] = weight_info.get("权重分", 0)
-                merged_element["权重明细"] = weight_info.get("权重明细", {})
-                merged_element["支撑统计"] = weight_info.get("支撑统计", {})
-
-            second_level = base_info.get("维度", {}).get("二级", "")
-            if second_level == "具象概念形式":
-                merged_element["支撑"] = base_info.get("支撑", [])
-                merged_element["推理"] = base_info.get("推理", "")
-            elif second_level == "具体元素形式":
-                merged_element["支撑"] = base_info.get("支撑", {})
-                merged_element["推理"] = base_info.get("推理", "")
-            elif second_level == "整体形式":
-                merged_element["推理"] = base_info.get("推理", "")
-                merged_element["支撑"] = base_info.get("支撑", {})
-
-            final_elements.append(merged_element)
-
-        logger.info(f"合并完成: {len(final_elements)} 个元素")
-        # 统计合并后的元素信息
-        if final_elements:
-            elements_with_support = sum(
-                1 for elem in final_elements
-                if any(elem.get("意图支撑", {}).get(dim, []) for dim in ["灵感点", "目的点", "关键点"])
-            )
-            elements_with_category = sum(
-                1 for elem in final_elements
-                if elem.get("分类")
-            )
-            logger.info(
-                f"   有支撑的元素: {elements_with_support}/{len(final_elements)}, "
-                f"有分类的元素: {elements_with_category}/{len(final_elements)}"
-            )
-        return final_elements
-
-    # ========== 辅助方法 ==========
-
-    def _build_simple_items_text(self, elements: list) -> str:
-        """构建元素列表文本"""
-        grouped_elements: Dict[str, List[Dict[str, Any]]] = {
-            "具象概念形式": [],
-            "具体元素形式": [],
-            "整体形式": []
-        }
-
-        for elem in elements:
-            if not isinstance(elem, dict):
-                continue
-            element_type = elem.get("维度", {}).get("二级", "N/A")
-            element_data = {
-                "id": elem.get("id", "N/A"),
-                "名称": elem.get("名称", "N/A"),
-                "描述": elem.get("描述", "N/A")
-            }
-            if element_type in grouped_elements:
-                grouped_elements[element_type].append(element_data)
-
-        filtered_groups = {k: v for k, v in grouped_elements.items() if v}
-        return json.dumps(filtered_groups, ensure_ascii=False, indent=2)
-
-    def _build_simple_items_text_dimension(self, elements: list, dimension_name: str) -> str:
-        """构建元素列表文本(按维度)"""
-        dimension_elements = []
-
-        for elem in elements:
-            if not isinstance(elem, dict):
-                continue
-            element_type = elem.get('维度', {}).get('二级', 'N/A')
-            element_data = {
-                "id": elem.get('id', 'N/A'),
-                "名称": elem.get('名称', 'N/A')
-            }
-            if element_type == dimension_name:
-                dimension_elements.append(element_data)
-        return json.dumps(dimension_elements, ensure_ascii=False, indent=2)
-
-    def _build_points_text(self, point_type: str, points_data) -> str:
-        """构建点列表文本"""
-        if not points_data:
-            return "无点信息"
-        # 处理不同格式的点数据
-        filtered_points = []
-        for item in points_data:
-            if not isinstance(item, dict):
-                continue
-            # 尝试多种可能的字段名
-            point_name = item.get("灵感点") or item.get("目的点") or item.get("关键点") or item.get("点") or item.get("名称", "N/A")
-            point_desc = item.get("描述", "")
-            point_data = {"名称": point_name}
-            if point_desc:
-                point_data["描述"] = point_desc
-            filtered_points.append(point_data)
-        return json.dumps(filtered_points, ensure_ascii=False, indent=2)
-
-    def _build_messages(self, state: dict) -> List[dict]:
-        """构建消息 - 本Agent不使用此方法"""
-        return []
-
-    def _update_state(self, state: dict, response) -> dict:
-        """更新状态 - 本Agent不使用此方法"""
-        return state
-
-

+ 0 - 164
src/components/agents/script_keyword_agent.py

@@ -1,164 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-金句提取Agent(视频分析版本)
-
-功能:
-1. 从 state 中读取视频文件对象
-2. 使用 Gemini 视频分析能力,直接基于视频内容提取"钩子"和"金句"
-"""
-
-from typing import Any, Dict
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker, get_video_file_from_state
-
-logger = get_logger(__name__)
-
-
-class ScriptKeywordAgent(BaseLLMAgent):
-    """金句提取Agent - 基于视频分析提取钩子和金句"""
-
-    def __init__(
-        self,
-        name: str = "script_keyword_agent",
-        description: str = "金句提取Agent - 直接基于视频内容提取钩子和金句",
-        model_provider: str = "google_genai",
-        temperature: float = 0.4,
-        max_tokens: int = 20480,
-    ):
-        # 这里的 system_prompt 仅用于初始化模型(提供 model_name),
-        # 实际的视频分析逻辑全部通过 safe_invoke_video_analysis 的 prompt 控制
-        system_prompt = "你是一名专注于中国60岁+老年群体短视频生态的内容分析专家。"
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens,
-        )
-
-    # ==================== 对外主入口 ====================
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态
-
-        步骤:
-        1. 从 state 中读取视频文件对象
-        2. 直接调用 safe_invoke_video_analysis,基于视频内容提取钩子和金句
-        3. 将金句提取结果写回 state
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("金句提取Agent开始执行")
-
-        # Step 0: 从 state 中获取视频文件对象
-        video_file = get_video_file_from_state(state)
-        if not video_file:
-            logger.warning("无法获取视频文件对象,跳过金句提取")
-            return {
-                "script_keywords": {
-                    "error": "无法获取视频文件对象",
-                },
-            }
-
-        try:
-            # 直接基于视频内容进行金句提取
-            logger.info("基于视频内容进行金句提取...")
-            keyword_prompt = self._build_keyword_prompt()
-
-            keyword_result = LLMInvoker.safe_invoke_video_analysis(
-                "金句提取(钩子和金句)",
-                video_file,
-                keyword_prompt,
-                agent=self,
-                fallback={
-                    "script_type": "",
-                    "hooks": [],
-                    "golden_sentences": [],
-                },
-            )
-
-            logger.info("金句提取Agent执行完成")
-
-            return {
-                "script_keywords": keyword_result,
-            }
-
-        except Exception as e:
-            logger.error(f"金句提取Agent执行失败: {e}", exc_info=True)
-            return {
-                "script_keywords": {
-                    "error": str(e),
-                },
-            }
-
-    # ==================== Prompt 构建 ====================
-
-    def _build_keyword_prompt(self) -> str:
-        """构建基于视频内容的金句提取 Prompt"""
-        return """
-# Role
-你是一名专注于中国 60岁+ 老年群体短视频生态的内容分析专家。你的核心能力是识别视频脚本中的"传播基因"和"心理抓手"。
-
-# Task
-请根据视频,利用【认知缺口】和【高能评判】两大底层逻辑,提取脚本中的"钩子(Hooks)"和"金句(Golden Sentences)"。
-
-# Extraction Rules (必须严格遵守)
-## 1. 场景预判 (Scenario Detection)
-首先判断脚本属于以下哪种类型,并调整提取侧重:
-- **类型 A:叙事/人物/科普类**(如名人故事、大国重器)
-  -> 侧重提取:核心冲突、悲情叙事、民族情绪、正名/喊冤。
-- **类型 B:盘点/猎奇/生活类**(如奇葩村庄、顺口溜、养生)
-  -> 侧重提取:反常识特征、极低成本获益、顺口溜韵脚。
-
-## 2. 定义与提取「钩子」(The Hooks)
-*目标:提取制造悬念、打破常识或直击痛点的短句(用于留存)。*
-
-请扫描脚本开头及段落转折处,提取符合以下逻辑的句子:
-1.  **逻辑悬空 (The Open Loop):** 展示了惊人的结果或高唤醒度的状态,但隐去了原因或过程。(例如:"看完才明白他的良苦用心"、"99%的人都不知道")
-2.  **反常识特征 (The Strange Feature):** 描述违背常理、伦理或物理规律的现象。**注意:将具体的生僻地名/人名抽象为特征描述。**(例如:"男人一辈子只能生活在树上"、"娶媳妇只需要一头猪")
-3.  **痛点与利益 (Pain & Benefit):** 直接针对健康、养老金、子女关系的强引导。(例如:"千万别再这样吃"、"过了60岁要注意")
-
-## 3. 定义与提取「金句」(The Golden Sentences)
-*目标:提取用于社交货币、情绪宣泄或身份认同的短句(用于分享)。*
-请扫描全文(特别是结尾和高潮处),提取符合以下逻辑的句子:
-1.  **绝对化定性 (Absolute Judgment):** 脱离具体时空叙事,对人/事/物进行盖棺定论的总结。包含"一辈子"、"凡是"、"绝对"、"就是"等词。(例如:"手里有钱才是硬道理"、"他一辈子就做了一件事")
-2.  **核心冲突与宣泄 (Conflict & Venting):** 描述 [付出 vs 误解]、[高尚 vs 庸俗]、[我们 vs 敌人] 的强烈对比。包含"嘲讽"、"心寒"、"咬牙切齿"、"不再看脸色"等词。(例如:"把一生献给国家的科学家被人误解多年")
-3.  **乌托邦与治愈 (The Utopia):** 描绘老年人向往的理想状态,或否定现实压力。(例如:"没有复杂的婆媳关系"、"把老年变玩年")
-4.  **韵律与警世 (Rhymes):** 押韵的顺口溜或具有视觉冲击力的警世格言。(例如:"饭再好没有牙,钱再多床上趴")
-
-## 4. 负面清单 (Negative List - 不要提取)
-- **纯事实叙事:** 具体的年份、复杂的数字、毫无特征的流水账(如"1996年他去了北京")。
-- **主播口水话:** "点好关注"、"屏幕前的朋友"、"您收到了吗"。
-- **长难句:** 如果句子超过 30 字且无法拆解出独立观点,请忽略。
-- **无意义连接词:** "话说"、"那么"、"之所以"。
-
-# Output Format (JSON)
-请直接输出 JSON 格式数据,不要包含任何解释性文字。
-{
-  "script_type": "判断是 类型A 或 类型B",
-  "hooks": [
-    "提取的钩子1",
-    "提取的钩子2 (必须保留原文语气,如'竟然'、'千万')"
-  ],
-  "golden_sentences": [
-    "提取的金句1",
-    "提取的金句2",
-    "提取的金句3 (必须是完整的观点或情绪表达)"
-  ]
-}
-""".strip()
-
-    # ==================== BaseLLMAgent 抽象方法占位实现 ====================
-
-    def _build_messages(self, state: Dict[str, Any]) -> list:
-        """满足 BaseLLMAgent 抽象要求(本 Agent 不通过该路径调用)"""
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """满足 BaseLLMAgent 抽象要求(本 Agent 不通过该路径调用)"""
-        return state

+ 0 - 254
src/components/agents/script_section_division_agent.py

@@ -1,254 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-脚本段落划分Agent
-
-功能: 分析视频的分段结构,理解创作者是如何组织内容的(创作者视角)
-输出: 段落列表(树状结构)
-"""
-
-from typing import List, Dict, Any
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker, get_video_file_from_state
-
-logger = get_logger(__name__)
-
-
-class ScriptSectionDivisionAgent(BaseLLMAgent):
-    """脚本段落划分Agent - 分析视频的分段结构(创作者视角)
-    """
-
-    def __init__(
-        self,
-        name: str = "script_section_division_agent",
-        description: str = "脚本段落划分Agent - 分析视频的分段结构(创作者视角)",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 40960
-    ):
-        """初始化脚本段落划分Agent
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是脚本结构分析专家,擅长从创作者视角理解视频内容的分段结构。
-
-# 核心能力
-1. 结构识别:分析视频内容,识别创作者的分段逻辑
-2. 树状输出:通过子项字段表示层级关系,不使用parent_id
-
-# 工作原则
-- 识别创作者如何分段组织视频内容(树状结构)
-- 需要对视频进行逐帧或关键帧分析,捕捉画面、动作、场景、对话等维度的结构变化"""
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理state,执行段落划分
-
-        从state中提取视频文件,执行段落划分,并返回结果
-
-        Args:
-            state: 工作流状态,包含视频文件对象
-
-        Returns:
-            dict: 包含段落列表和内容品类
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("=== 开始脚本段落划分 ===")
-
-        # 从state中获取视频文件对象
-        video_file = get_video_file_from_state(state)
-
-        if not video_file:
-            logger.warning("无法获取视频文件对象,跳过段落划分")
-            return {
-                "内容品类": "未知品类",
-                "段落列表": []
-            }
-
-        # 执行段落划分
-        logger.info("执行段落划分")
-        result = self._divide_sections(video_file)
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"段落划分结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def _divide_sections(self, video_file) -> dict:
-        """段落划分
-        分析视频内容,识别创作者是如何分段组织内容的
-        """
-        logger.info("【段落划分】分析视频的分段结构")
-
-        # 构建用户prompt
-        user_prompt = """# 任务
-
-从**创作者视角**分析这个视频是如何组织内容的。
-
-## Section切分流程
-
-**第一步:识别主题显著变化位置**
-扫描整个视频,识别**主题发生显著变化**的位置:
-
-- **判断标准**:
-  * 语义跃迁: 讨论对象发生根本性改变
-  * 逻辑转换: 从"是什么"转向"为什么"或"怎么办"
-  * 功能变化: 从"问题陈述"转向"解决方案"
-  * 场景切换: 画面、场景、动作发生明显变化
-  * 对话转换: 话题、语气、情绪发生明显变化
-
-- **划分原则**:
-  * 避免过度细分(每个小变化都成为顶层段落)
-  * 避免过度粗放(将所有内容合并为1个顶层段落)
-  * 以"主题板块"而非"内容单元"为划分粒度
-
-**第二步:初步划分**
-- 基于主题显著变化位置进行划分
-- 支持主Section和子Section的层级结构
-- 需要对视频进行逐帧或关键帧分析,捕捉画面、动作、场景、对话等维度的结构变化
-
-**第三步:顺序验证与反思**
-- 检查每个Section内的视频片段是否保持时间顺序的连续性
-- 验证同一Section内的内容是否有语义关联
-- 确认段落之间的过渡是否自然合理
-
-## 层级要求
-
-**段落必须至少保留2层结构**:
-1. **第1层(抽象层)**:从具象中聚合出的共性维度
-2. **第2层(具象层)**:具体的内容细节
-
-**层级关系说明**:
-- 抽象层是对多个具象内容的归纳和提炼
-- 具象层是抽象层的具体展开
-- 每个抽象层下必须有至少1个具象层子项
-
-### 整体情绪价值(Emotion Value)
-**内容背后的情绪钩子或社会价值**。仅在有明显的升维价值时提取(如:民族自信、致青春、趋吉避凶、鞠躬尽瘁),否则留空。
-** 输出案例: "致青春", "友谊长存", "猎奇", "颠覆认知"等 6字以内的词语,可输出多个,用逗号分隔**
-
-## Section字段
-
-- 描述: 段落描述(共性维度名称;具体内容概括)
-- 内容范围: **列表格式,包含具体视频片段**
-  - 格式:["0:00-0:30", "0:30-1:15"] 或 ["片段1: 开场介绍", "片段2: 问题阐述"]
-  - 要求:必须包含具体的时间戳范围或视频片段描述,清晰标识该Section涵盖的视频内容
-- 推理依据: 为什么这样划分
-- 子项: 子Section列表(树状结构)
-
-# 输出(JSON)- 树状结构
-
-{
-  "内容品类": "内容品类",
-  "整体情绪分析": ["整体情绪分析1(6个字以内)", "整体情绪分析2(6个字以内)"],
-  "段落衔接机制": [
-    {
-      "from": "段落1",
-      "to": "段落2",
-      "衔接方式": "如何过渡到下一个段落",
-      "衔接语/画面特征": "衔接语或画面特征",
-      "设计意图": "设计意图"
-    }
-  ],
-  "段落列表": [
-    {
-      "描述": "共性维度名称",
-      "内容范围": ["0:00-0:30", "0:30-1:15"],
-      "推理依据": "为什么这样划分这个抽象层",
-      "子项": [
-        {
-          "描述": "具体内容概括",
-          "内容范围": ["0:00-0:30"],
-          "推理依据": "这个具象内容如何支撑上层抽象",
-          "子项": []
-        }
-      ]
-    }
-  ]
-}"""
-
-        # 合并system_prompt和user_prompt
-        full_prompt = f"""{self.system_prompt}
-
-{user_prompt}"""
-
-        # 调用LLM进行视频分析
-        result = LLMInvoker.safe_invoke_video_analysis(
-            operation_name="Section划分",
-            video_file=video_file,
-            prompt=full_prompt,
-            agent=self,
-            fallback={"内容品类": "未知品类", "段落列表": []}
-        )
-
-        # 为每个段落生成唯一ID
-        sections_with_id = self._add_section_ids(result.get("段落列表", []))
-        result["段落列表"] = sections_with_id
-
-        # 只打印JSON结构
-        import json
-        logger.info(f"段落划分详细结果:\n{json.dumps(result, ensure_ascii=False, indent=2)}")
-
-        return result
-
-    def _add_section_ids(self, sections: list, parent_prefix: str = "") -> list:
-        """递归为段落添加唯一ID
-
-        Args:
-            sections: 段落列表
-            parent_prefix: 父级前缀(用于生成层级ID)
-
-        Returns:
-            list: 添加了ID的段落列表
-        """
-        result = []
-        for idx, section in enumerate(sections, 1):
-            # 生成当前段落ID
-            if parent_prefix:
-                section_id = f"{parent_prefix}.{idx}"
-            else:
-                section_id = f"段落{idx}"
-
-            # 添加ID字段
-            section_with_id = {
-                "id": section_id,
-                **section
-            }
-
-            # 递归处理子项
-            if section.get("子项"):
-                section_with_id["子项"] = self._add_section_ids(
-                    section["子项"],
-                    section_id
-                )
-
-            result.append(section_with_id)
-
-        return result
-
-    def _build_messages(self, state: dict) -> List[dict]:
-        """构建消息 - ScriptSectionDivisionAgent 不使用此方法
-
-        本 Agent 使用 _divide_sections 方法直接构建消息
-        """
-        return []
-
-    def _update_state(self, state: dict, response) -> dict:
-        """更新状态 - ScriptSectionDivisionAgent 不使用此方法
-
-        本 Agent 使用 _divide_sections 方法直接返回结果
-        """
-        return state

+ 0 - 1726
src/components/agents/script_substance_extraction_agent.py

@@ -1,1726 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-实质提取Agent (SubstanceExtractionAgent)
-
-功能:
-- 从视频内容中提取实质元素(具体元素、具象概念、抽象概念)
-- Step 1: 提取具体元素(只看视频画面中的实体)
-- Step 2: 提取具象概念(只看画面中的文字 + 口播内容中的文字)
-- Step 3: 总结抽象概念(基于Step 1+2)
-- Step 4: 共性分析(频次、段落覆盖率)
-- Step 5: 多维度评分(vs 灵感点/目的点/实质关键点)
-- Step 6: 筛选(基于频次+覆盖率+相似度)
-- Step 7: 分类
-- Step 8: 合并所有信息
-
-参考:元素提取新方案设计文档.md
-"""
-
-import json
-from typing import List, Dict, Any
-from concurrent.futures import ThreadPoolExecutor
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker, get_video_file_from_state
-
-logger = get_logger(__name__)
-
-# 全局线程池 - 用于并行处理
-_GLOBAL_THREAD_POOL = ThreadPoolExecutor(max_workers=16, thread_name_prefix="SubstanceExtraction")
-
-
-class ScriptSubstanceExtractionAgent(BaseLLMAgent):
-    """实质提取Agent - 自底向上的归纳过程
-
-    提取流程(视频版):
-    - Step 1: 提取具体元素(只看视频画面中的实体)
-    - Step 2: 提取具象概念(只看画面中的文字 + 口播内容中的文字)
-    - Step 3: 总结抽象概念(基于Step 1+2)
-    - Step 4: 共性分析(频次、段落覆盖率)
-    - Step 5: 多维度评分(vs 灵感点/目的点/实质关键点)
-    - Step 6: 筛选(基于频次+覆盖率+相似度)
-    - Step 7: 分类
-    - Step 8: 合并所有信息
-    """
-
-    def __init__(
-        self,
-        name: str = "substance_extraction_agent",
-        description: str = "实质提取Agent",
-        model_provider: str = "google_genai",
-        temperature: float = 0.1,
-        max_tokens: int = 40960
-    ):
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词"""
-        return """你是一个专业的内容分析专家,擅长从图文内容中提取实质性元素。
-
-# 核心定义
-
-## 实质(Substance):"是什么"
-内容本身,独立于表达方式而存在
-
-### 具体元素
-- 定义:从图像中直接观察到的单一视觉实体对象
-- 判断标准:可以指着图片说"这是一个X"
-
-### 具象概念
-- 定义:画面或者口播内容中出现的名词
-- 判断标准:画面或者口播内容中实际出现,禁止语义推导
-
-### 抽象概念
-- 定义:从具体元素和具象概念中理解到的上位抽象
-- 类型1:上位抽象(归类)- 是下位元素的类别、分类
-- 类型2:引申含义 - 需要理解上下文的深层含义
-
-## 区分方法:"剥离测试"
-问题:如果去掉所有表达手法/风格/技巧,这个特征还存在吗?
-- 存在 → 实质(内容本身)
-- 不存在/失去意义 → 形式(表达方式)
-"""
-
-    def process(self, state: dict) -> dict:
-        """执行完整的实质提取流程(Step 1-8)"""
-        logger.info("=== 开始实质元素提取(完整流程) ===")
-
-        # 从 state 中获取视频文件(视频版本核心输入)
-        video_file = get_video_file_from_state(state)
-        if not video_file:
-            logger.error("无法从 state 中获取视频文件,实质提取终止")
-            return {
-                "concrete_elements": [],
-                "concrete_concepts": [],
-                "implicit_concepts": [],
-                "abstract_concepts": [],
-                "substance_elements": [],
-                "substance_analyzed_result": [],
-                "substance_scored_result": {},
-                "substance_filtered_ids": [],
-                "substance_categorized_result": {},
-                "substance_final_elements": []
-            }
-
-        # 从state中提取其他文本/上下文数据
-        text_data = state.get("text", {})
-        section_division = state.get("section_division", {})
-        
-        # 处理灵感点:支持列表和字典两种格式
-        inspiration_points_raw = state.get("inspiration_points", {})
-        if isinstance(inspiration_points_raw, list):
-            inspiration_points = inspiration_points_raw
-        elif isinstance(inspiration_points_raw, dict):
-            # 兼容旧格式:{"points": [...]} 或直接是列表
-            inspiration_points = inspiration_points_raw.get("points", [])
-        else:
-            inspiration_points = []
-        
-        # 兼容 purpose_point 的多种格式:
-        # 1. 字典格式:{"purpose_point": {"purposes": [...], "total_count": ...}}
-        # 2. 列表格式:[...](直接是目的点列表)
-        purpose_point_raw = state.get("purpose_point", {})
-        if isinstance(purpose_point_raw, dict):
-            purpose_points = purpose_point_raw.get("purposes", [])
-        elif isinstance(purpose_point_raw, list):
-            purpose_points = purpose_point_raw
-        else:
-            purpose_points = []
-        
-        # 兼容 key_points 的多种格式:
-        # 1. 字典格式:{"key_points": [...], "total_count": ...}
-        # 2. 列表格式:[...](直接是关键点列表)
-        key_points_raw = state.get("key_points", {})
-        if isinstance(key_points_raw, dict):
-            key_points = key_points_raw.get("key_points", [])
-        elif isinstance(key_points_raw, list):
-            key_points = key_points_raw
-        else:
-            key_points = []
-
-        # 只保留实质类关键点
-        substance_key_points = [
-            kp for kp in key_points
-            if isinstance(kp, dict) and kp.get("维度大类") == "实质"
-        ] if key_points else []
-        
-        logger.info(
-            f"意图支撑评估输入: 灵感点={len(inspiration_points)}, "
-            f"目的点={len(purpose_points)}, 关键点(实质类)={len(substance_key_points)}"
-        )
-
-        # Step 1: 提取具体元素(画面中的实体)
-        logger.info("▶ Step 1: 提取具体元素")
-        concrete_elements = self._step1_extract_concrete_elements(video_file)
-
-        # Step 2: 提取具象概念(画面中的文字 + 口播内容中的文字)
-        logger.info("▶ Step 2: 提取具象概念")
-        concrete_concepts = self._step2_extract_concrete_concepts(
-            video_file, text_data, concrete_elements
-        )
-
-        # 隐含概念相关逻辑已移除,不再单独提取
-        implicit_concepts: List[dict] = []
-
-        # Step 3: 总结抽象概念(基于Step 1+2)
-        logger.info("▶ Step 3: 总结抽象概念")
-        abstract_concepts = self._step3_summarize_abstract_concepts(
-            video_file, concrete_elements, concrete_concepts, implicit_concepts
-        )
-
-        # 合并所有实质元素(不再包含隐含概念)
-        all_substance_elements = (
-            concrete_elements + concrete_concepts + abstract_concepts
-        )
-
-        logger.info(
-            "Step 1-3 完成 - 总计: %d 个元素 (具体:%d, 具象:%d, 抽象:%d)",
-            len(all_substance_elements),
-            len(concrete_elements),
-            len(concrete_concepts),
-            len(abstract_concepts),
-        )
-
-        # Step 4: 共性分析
-        logger.info("▶ Step 4: 共性分析")
-        analyzed_result = self._step4_commonality_analysis(
-            video_file, all_substance_elements, text_data, section_division
-        )
-
-        # Step 5: 多维度评分(已废弃相似度比较逻辑,当前不再进行相似度评分)
-        logger.info("▶ Step 5: 多维度评分(已停用相似度计算,仅返回空结果)")
-        scored_result = self._step5_multi_dimensional_scoring(
-            all_substance_elements, analyzed_result, inspiration_points, purpose_points, substance_key_points
-        )
-
-        # Step 5.1: 意图支撑评估(基于视频与文本)
-        logger.info("▶ Step 5.1: 意图支撑评估")
-        intention_support_result = self._step5_1_intention_support_evaluation(
-            video_file,
-            all_substance_elements,
-            analyzed_result,
-            inspiration_points,
-            purpose_points,
-            substance_key_points,
-            text_data,
-        )
-
-        # Step 6: 筛选
-        logger.info("▶ Step 6: 筛选")
-        filtered_ids = self._step6_filter_elements(
-            analyzed_result,
-            scored_result,
-            intention_support_result,
-        )
-
-        # Step 7: 分类
-        logger.info("▶ Step 7: 分类")
-        categorized_result = self._step7_categorize_elements(
-            all_substance_elements, filtered_ids
-        )
-
-        # Step 8: 合并信息
-        logger.info("▶ Step 8: 合并信息")
-        final_elements = self._merge_all_info(
-            all_substance_elements,
-            analyzed_result,
-            scored_result,
-            intention_support_result,
-            filtered_ids,
-            categorized_result,
-        )
-
-        logger.info(f"实质元素提取完成 - 最终元素数: {len(final_elements)}")
-
-        # 返回所有结果
-        return {
-            # Step 1-3 原始提取结果
-            "concrete_elements": concrete_elements,
-            "concrete_concepts": concrete_concepts,
-            "implicit_concepts": implicit_concepts,
-            "abstract_concepts": abstract_concepts,
-            "substance_elements": all_substance_elements,
-            # Step 4-8 处理结果
-            "substance_analyzed_result": analyzed_result,
-            "substance_scored_result": scored_result,
-            "substance_intention_support_result": intention_support_result,
-            "substance_filtered_ids": filtered_ids,
-            "substance_categorized_result": categorized_result,
-            # 最终结果
-            "substance_final_elements": final_elements
-        }
-
-    # ========== Step 1-3: 实质提取 ==========
-
-    def _step1_extract_concrete_elements(
-        self,
-        video_file
-    ) -> List[dict]:
-        """Step 1: 提取具体元素 - 从图像中直接观察到的单一视觉实体对象"""
-        if not self.is_initialized:
-            self.initialize()
-
-        if not video_file:
-            logger.warning("⚠️  没有视频文件,跳过具体元素提取")
-            return []
-
-        prompt = """# 任务
-从视频中提取"具体元素"
-
-# 核心定义
-
-## 具体元素
-- **定义**:
-  -- 1.从视频画面中直接观察到的、可独立存在的**单一视觉实体对象**
-  -- 2.视频的背景音乐、音效等非口播内容的声音
-- **判断标准**:
-  -- 1.可以指着画面说"这是一个X"(单一、具体、可见的实体)
-  -- 2.有背景音乐、音效等非口播内容的声音,直接用"背景音乐/音效声"作为名称即可,不要重复提取
-- **示例**:
-  -- 1.胡萝卜、青椒、西兰花(每个都是单独的实体)
-  -- 2.背景音乐/音效声
-- **禁止**:
-  - 归类词(蔬菜、水果)
-  - 概念性名词(食物、植物、人)
-  - 文字内容(只关注视觉实体)
-
-## 提取原则(仅针对画面中的视觉实体对象)
-- 只从视频画面中提取,不关注文字
-- 每个元素必须是单一的、具体的视觉实体
-- 使用"剥离测试":去掉表达方式后,这个实体仍然存在
-
-# 命名规范
-- 原子性:单一原子名词,不可再拆分
-- 名词性:纯名词,严禁形容词、动词、副词
-- 具体性:直接指向可观察的实体
-
-# 输出json结构
-[
-    {
-      "id": "从1开始的自增序列",
-      "名称": "单一原子名词",
-      "描述": "说明这个元素是什么,外观特征",
-      "维度": {"一级": "实质", "二级": "具体元素"},
-      "来源": ["视频画面"],
-      "推理": "为什么识别这个具体元素"
-    },
-    {
-      "id": "从1开始的自增序列",
-      "名称": "背景音乐/音效声",
-      "描述": "说明背景音乐/音效声是什么",
-      "维度": {"一级": "实质", "二级": "具体元素"},
-      "来源": ["视频"],
-      "推理": "为什么识别这个背景音乐/音效声"
-    }
-]
-
-注意:只提取具体的视觉实体对象,不要提取抽象概念或归类词
-"""
-
-        # 使用视频分析接口
-        result = LLMInvoker.safe_invoke_video_analysis(
-            operation_name="具体元素提取",
-            video_file=video_file,
-            prompt=prompt,
-            agent=self,
-            fallback=[]
-        )
-
-        # 为每个具体元素添加id
-        for idx, element in enumerate(result, 1):
-            element["id"] = f"具体元素-{idx}"
-
-        return result
-
-    def _step2_extract_concrete_concepts(
-        self,
-        video_file,
-        text_data: dict,
-        concrete_elements: List[dict],
-    ) -> List[dict]:
-        """Step 2: 提取具象概念 - 文字中字面出现的名词"""
-        if not self.is_initialized:
-            self.initialize()
-
-        # 从第一步结果中提取已识别的具体元素名称,供本步骤排除使用
-        element_names = [
-            e.get("名称") for e in (concrete_elements or []) if e.get("名称")
-        ]
-        element_names_text = (
-            json.dumps(element_names, ensure_ascii=False, indent=2)
-            if element_names
-            else "[]"
-        )
-
-        prompt = f"""# 任务
-从视频中提取"具象概念"
-
-# 核心定义
-## 具象概念
-- **定义**:视频画面内的文字或者口播内容中明确提到的完整名词
-
-## 排除的名称(来自第一步,仅用于排除)
-**禁止提取的名称**:{element_names_text}
-
-## 判断标准
-- **视频画面内的文字或者口播内容**中实际出现的**完整名词**
-- **不能是视频画面中出现的元素的名称等归类词**
-- 去掉表达方式后,这个概念仍然存在
-
-# 约束
-- 禁止通过语义推导、联想、理解得出的名词
-- **禁止归类词(蔬菜、水果、人等)**
-- **禁止使用第一步中已提取的具体元素名称**
-- 禁止拆分复合词
-- 禁止提取形容词、动词
-- 禁止提取谓语、定语、状语、补语
-- 禁止提取副词
-
-## 提取原则
-- **词语完整性**:必须提取完整的**名词**,不允许拆分复合词
-- **严格约束**:必须是**画面文字或者口播内容中实际出现**的完整名词
-- **严格的名词验证**(必须同时满足以下两个条件):
-   - 条件1:词性是名词(词典意义上的名词)
-   - 条件2:在当前上下文中作为名词使用(语境判断)
-
-   **验证方法**:
-   - 找到该词在视频画面内的文字或者口播内容中的具体位置
-   - 分析该词在句子中的语法成分和实际作用
-   - 判断:该词是否在这个语境中充当"事物/对象/概念"的角色?
-
-# 输出json结构
-[
-    {{
-      "id": "从1开始的自增序列",
-      "名称": "字面原词(完整名词)",
-      "描述": "说明这个概念是什么",
-      "维度": {{"一级": "实质", "二级": "具象概念"}},
-      "来源": "HH:MM:SS",
-      "上下文验证": {{
-        "原文位置": "该词在原视频画面内的文字或者口播内容中的具体句子",
-        "语法成分": "该词在句子中的语法成分(主语/宾语/定语中心语等)",
-        "语境判断": "说明该词在此语境中确实作为名词使用的理由"
-      }},
-      "推理": "为什么这个名词被认为是具象概念"
-    }}
-]
-
-注意:只输出同时满足"词性是名词"和"上下文中作为名词使用"两个条件的概念
-"""
-
-        # 使用视频分析接口(可综合语音与画面中的文字)
-        result = LLMInvoker.safe_invoke_video_analysis(
-            operation_name="具象概念提取",
-            video_file=video_file,
-            prompt=prompt,
-            agent=self,
-            fallback=[]
-        )
-
-        # 为每个具象概念添加id
-        for idx, concept in enumerate(result, 1):
-            concept["id"] = f"具象概念-{idx}"
-
-        return result
-
-    def _step3_summarize_abstract_concepts(
-        self,
-        video_file,
-        concrete_elements: List[dict],
-        concrete_concepts: List[dict],
-        implicit_concepts: List[dict]
-    ) -> List[dict]:
-        """Step 3: 总结抽象概念 - 从具体元素和具象概念中归纳上位抽象"""
-        if not self.is_initialized:
-            self.initialize()
-
-        if not concrete_elements and not concrete_concepts:
-            logger.warning("⚠️  没有具体元素或具象概念,跳过抽象概念提取")
-            return []
-
-        # 构建已提取的元素文本
-        elements_text = json.dumps([
-            {"id": e.get("id"), "名称": e.get("名称"), "描述": e.get("描述")}
-            for e in concrete_elements
-        ], ensure_ascii=False, indent=2) if concrete_elements else "无"
-
-        concepts_text = json.dumps([
-            {"id": c.get("id"), "名称": c.get("名称"), "描述": c.get("描述")}
-            for c in concrete_concepts
-        ], ensure_ascii=False, indent=2) if concrete_concepts else "无"
-
-        prompt = f"""# 任务
-基于已提取的具体元素和具象概念,总结新的"抽象概念"
-
-# 已提取的具体元素
-{elements_text}
-
-# 已提取的具象概念
-{concepts_text}
-
-# 核心定义
-
-# 定义与分类
-**抽象概念**分两类:
-
-**类型1-上位抽象**:对具体元素/具象概念的归类
-**类型2-引申含义**:具体元素/具象概念无法直接表达的深层含义
-
-# 提取原则
-- 对具体元素/具象概念的归类
-- 具体元素和具象概念无法直接表达的深层含义
-- 基于归纳:基于已提取的具体元素/具象概念
-- 来源追溯:准确标明所有来源ID(具体元素ID、具象概念ID),必须完整可追溯
-
-# 命名规范
-- 有完整独立语义的概念
-- 单一原子名词,不可拆分
-- 纯名词,禁止形容词、动词、副词
-- 精准描述概念,不做修饰
-
-# 判断标准
-- 去掉表达方式后,概念仍存在
-
-# 输出json结构
-[
-    {{
-      "id": "从1开始的自增序列",
-      "名称": "单一名词或短语",
-      "描述": "说明这个抽象概念是什么",
-      "维度": {{"一级": "实质", "二级": "抽象概念"}},
-      "类型": "上位抽象 | 引申含义",
-        "来源": {{
-        "具体元素": [{{"id":"具体元素-X", "名称":"具体元素-X的名称"}}, {{"id":"具体元素-Y", "名称":"具体元素-Y的名称"}}],
-        "具象概念": [{{"id":"具象概念-A", "名称":"具象概念-A的名称"}}, {{"id":"具象概念-B", "名称":"具象概念-B的名称"}}]
-      }},
-      "推理过程": "明确说明如何从上述来源(具体哪些元素ID和概念ID)推导出这个抽象概念",
-    }}
-]
-
-注意:只输出验证全部通过的概念
-"""
-
-        # 使用视频分析接口总结抽象概念
-        result = LLMInvoker.safe_invoke_video_analysis(
-            operation_name="抽象概念总结",
-            video_file=video_file,
-            prompt=prompt,
-            agent=self,
-            fallback=[]
-        )
-
-        # 为每个抽象概念添加id
-        for idx, concept in enumerate(result, 1):
-            concept["id"] = f"抽象概念-{idx}"
-
-        return result
-
-    # ========== Step 4-8: 后续处理 ==========
-
-    def _step4_commonality_analysis(
-        self,
-        video_file,
-        substance_elements: List[dict],
-        text_data: dict,
-        section_division: dict
-    ) -> List[dict]:
-        """Step 4: 共性分析 - 统计频次和段落覆盖率"""
-        if not substance_elements:
-            return []
-
-        total_sections = self._count_sections(section_division)
-
-        # 分批处理
-        analyzed_items = self._commonality_analysis_in_batches(
-            video_file, substance_elements, text_data, section_division, total_sections,
-            max_batch_size=100
-        )
-
-        return analyzed_items
-
-    def _commonality_analysis_in_batches(
-        self,
-        video_file,
-        substance_elements: list,
-        text_data: dict,
-        section_division: dict,
-        total_sections: int,
-        max_batch_size: int = 100
-    ) -> list:
-        """分批处理共性分析"""
-        if not self.is_initialized:
-            self.initialize()
-
-        num_elements = len(substance_elements)
-        if num_elements == 0:
-            return []
-
-        # 如果元素数少于批次大小,一次性处理
-        if num_elements <= max_batch_size:
-            return self._commonality_analysis_single_batch(
-                video_file, substance_elements, text_data, section_division, total_sections
-            )
-
-        # 分批处理
-        num_batches = (num_elements + max_batch_size - 1) // max_batch_size
-
-        batch_futures = {}
-        for batch_idx in range(num_batches):
-            start_idx = batch_idx * max_batch_size
-            end_idx = min(start_idx + max_batch_size, num_elements)
-            batch_elements = substance_elements[start_idx:end_idx]
-
-            future = _GLOBAL_THREAD_POOL.submit(
-                self._commonality_analysis_single_batch,
-                video_file, batch_elements, text_data, section_division, total_sections
-            )
-            batch_futures[batch_idx] = future
-
-        # 收集结果
-        all_results = []
-        for batch_idx, future in batch_futures.items():
-            try:
-                batch_result = future.result()
-                if batch_result:
-                    all_results.extend(batch_result)
-            except Exception as e:
-                logger.error(f"批次 {batch_idx + 1} 失败: {e}")
-
-        return all_results
-
-    def _commonality_analysis_single_batch(
-        self,
-        video_file,
-        batch_elements: list,
-        text_data: dict,
-        section_division: dict,
-        total_sections: int
-    ) -> list:
-        """单批次共性分析"""
-        if not self.is_initialized:
-            self.initialize()
-
-        section_text = self._build_section_text(section_division)
-        elements_text = self._build_simple_items_text(batch_elements)
-
-        prompt = f"""# 段落列表
-{section_text}
-
-# 元素列表
-{elements_text}
-
-# 任务
-对每个元素统计出现的段落和频次
-
-## 统计规则
-
-### 1. 具体元素统计(只统计视觉实体)
-- **出现频次**: 统计该**单一视觉实体对象**在视频图像中直接观察到的次数
-- **出现段落列表**: 只统计能在视频图像中**直接看到该视觉实体**的段落
-
-### 2. 具象概念统计(只统计文字字面)
-- **出现频次**: 统计该名词在视频画面文字和口播内容中**画面或者口播内容中出现**的次数
-- **出现段落列表**: 只统计**视频画面文字或者口播内容中包含该名词**的段落
-
-### 3. 抽象概念统计(统计语义归类)
-- **出现频次**: 统计该概念被**隐含表达**的总次数
-- **出现段落列表**: 统计**包含该概念所归类的具体元素/具象概念**的段落
-
-# 输出(JSON)
-[
-    {{
-      "id": "元素id",
-      "名称": "元素名称",
-      "出现频次": 0,
-      "出现段落列表": [
-        {{
-          "段落ID": "段落id",
-          "如何体现": "描述该元素在这个段落中的具体体现方式"
-        }}
-      ]
-    }}
-]
-"""
-
-        # 使用视频分析接口做共性分析
-        llm_result = LLMInvoker.safe_invoke_video_analysis(
-            operation_name="共性分析",
-            video_file=video_file,
-            prompt=prompt,
-            agent=self,
-            fallback=[]
-        )
-
-        # 计算覆盖率
-        analyzed_items = []
-        for analysis in llm_result:
-            section_list = analysis.get("出现段落列表", [])
-            unique_paragraph_ids = set()
-            for item in section_list:
-                unique_paragraph_ids.add(item.get("段落ID", ""))
-            coverage_count = len(unique_paragraph_ids)
-            coverage_rate = round(coverage_count / total_sections, 4) if total_sections > 0 else 0
-
-            analyzed_items.append({
-                "id": analysis.get("id", 0),
-                "名称": analysis.get("名称", ""),
-                "出现频次": analysis.get("出现频次", 0),
-                "出现段落列表": section_list,
-                "出现段落数": coverage_count,
-                "段落覆盖率": coverage_rate
-            })
-
-        return analyzed_items
-
-    def _step5_multi_dimensional_scoring(
-        self,
-        substance_elements: List[dict],
-        analyzed_result: list,
-        inspiration_points: dict,
-        purpose_points: list,
-        substance_key_points: list
-    ) -> dict:
-        """Step 5: 多维度评分(相似度逻辑已废弃)
-
-        说明:
-        - 不再进行任何相似度计算,完全依赖后续的“意图支撑”进行筛选
-        - 保留函数与返回结构,仅返回空结果,避免下游依赖崩溃
-        """
-        logger.info(
-            "【多维度评分】相似度比较逻辑已关闭,当前不进行评分,仅返回空结果。"
-        )
-        return {
-            "灵感点": [],
-            "目的点": [],
-            "关键点": [],
-        }
-
-    def _step5_1_intention_support_evaluation(
-        self,
-        video_file,
-        substance_elements: List[dict],
-        analyzed_result: list,
-        inspiration_points: dict,
-        purpose_points: list,
-        substance_key_points: list,
-        text_data: dict,
-    ) -> dict:
-        """Step 5.1: 意图支撑评估
-
-        说明:
-        - 在保留相似度评分的基础上,增加一套“意图支撑”视角的评估
-        - 不再使用频次/覆盖率作为筛选条件,仅用于日志与统计
-        - 结果以元素-点的支撑关系形式返回,不直接参与筛选决策
-        """
-        if not substance_elements:
-            return {"灵感点": [], "目的点": [], "关键点": []}
-
-        logger.info(f"【意图支撑评估】输入: {len(substance_elements)} 个实质元素")
-
-        # 按二级维度分组(不做频次过滤,全部评估)
-        dimension_groups = {
-            "具体元素": [],
-            "具象概念": [],
-            "抽象概念": [],
-        }
-
-        for elem in substance_elements:
-            second_level = elem.get("维度", {}).get("二级", "")
-            if second_level in dimension_groups:
-                dimension_groups[second_level].append(elem)
-
-        logger.info(
-            "维度分组(意图支撑): 具体元素=%d, 具象概念=%d, 抽象概念=%d",
-            len(dimension_groups["具体元素"]),
-            len(dimension_groups["具象概念"]),
-            len(dimension_groups["抽象概念"]),
-        )
-
-        # 并行评估(各维度 x 3个点类型)
-        futures = {}
-
-        def submit_if_needed(dimension_name: str, point_type: str, points_list):
-            if not points_list:
-                logger.info(
-                    f"⏭️  跳过意图支撑评估: {dimension_name}-{point_type} "
-                    f"(原因: 点列表为空, len={len(points_list) if isinstance(points_list, list) else 'N/A'})"
-                )
-                return
-            if not dimension_groups.get(dimension_name):
-                logger.info(
-                    f"⏭️  跳过意图支撑评估: {dimension_name}-{point_type} "
-                    f"(原因: 该维度无元素, len={len(dimension_groups.get(dimension_name, []))})"
-                )
-                return
-            key = (dimension_name, point_type)
-            logger.info(
-                f"📤 提交意图支撑评估任务: {dimension_name}-{point_type} "
-                f"(元素数={len(dimension_groups[dimension_name])}, 点数={len(points_list)})"
-            )
-            futures[key] = _GLOBAL_THREAD_POOL.submit(
-                self._evaluate_support_by_dimension,
-                video_file,
-                dimension_name,
-                dimension_groups[dimension_name],
-                points_list,
-                point_type,
-                text_data,
-            )
-
-        # 具体元素 / 具象概念 / 抽象概念 × 灵感点 / 目的点 / 关键点
-        for dim in ["具体元素", "具象概念", "抽象概念"]:
-            submit_if_needed(dim, "灵感点", inspiration_points if isinstance(inspiration_points, list) else [])
-            submit_if_needed(dim, "目的点", purpose_points if isinstance(purpose_points, list) else [])
-            submit_if_needed(dim, "关键点", substance_key_points if isinstance(substance_key_points, list) else [])
-
-        # 收集结果(按点类型汇总)
-        result = {
-            "灵感点": [],
-            "目的点": [],
-            "关键点": [],
-        }
-
-        for (dimension_name, point_type), future in futures.items():
-            try:
-                dimension_result = future.result()
-                if dimension_result:
-                    result[point_type].extend(dimension_result)
-                logger.info(
-                    f"✅ 意图支撑-{dimension_name}-{point_type} 评估完成: {len(dimension_result)} 条支撑关系"
-                )
-            except Exception as e:
-                logger.error(f"❌ 意图支撑-{dimension_name}-{point_type} 评估失败: {e}")
-
-        return result
-
-    def _evaluate_support_by_dimension(
-        self,
-        video_file,
-        dimension_name: str,
-        elements: list,
-        points: list,
-        point_type: str,
-        text_data: dict,
-    ) -> list:
-        """按维度评估意图支撑关系(分批处理)"""
-        if not self.is_initialized:
-            self.initialize()
-
-        if not elements or not points:
-            return []
-
-        # 分批控制:元素数 × 点数 ≈ 100 以内
-        num_elements = len(elements)
-        num_points = len(points)
-        max_batch_product = 100
-        max_elements_per_batch = max(1, int(max_batch_product / max(1, num_points)))
-        num_batches = (num_elements + max_elements_per_batch - 1) // max_elements_per_batch
-
-        batch_futures = {}
-        for batch_idx in range(num_batches):
-            start_idx = batch_idx * max_elements_per_batch
-            end_idx = min(start_idx + max_elements_per_batch, num_elements)
-            batch_elements = elements[start_idx:end_idx]
-
-            future = _GLOBAL_THREAD_POOL.submit(
-                self._evaluate_support_single_batch_by_dimension,
-                video_file,
-                dimension_name,
-                batch_elements,
-                points,
-                point_type,
-                text_data,
-            )
-            batch_futures[batch_idx] = future
-
-        # 收集结果
-        all_results = []
-        for batch_idx, future in batch_futures.items():
-            try:
-                batch_result = future.result()
-                if batch_result:
-                    all_results.extend(batch_result)
-            except Exception as e:
-                logger.error(f"【意图支撑-{dimension_name}】批次 {batch_idx + 1} 失败: {e}")
-
-        # 合并结果(支撑结果)
-        merged_results = self._merge_support_batch_results(all_results)
-        return merged_results
-
-    def _evaluate_support_single_batch_by_dimension(
-        self,
-        video_file,
-        dimension_name: str,
-        batch_elements: list,
-        points: list,
-        point_type: str,
-        text_data: dict,
-    ) -> list:
-        """单批次意图支撑评估(按维度)"""
-        if not self.is_initialized:
-            self.initialize()
-
-        post_content = self._build_post_content(text_data)
-        elements_text = self._build_simple_items_text_dimension(batch_elements, dimension_name)
-        points_text = self._build_points_text(point_type, points)
-
-        # 根据维度选择不同的 prompt
-        if dimension_name == "具体元素":
-            prompt = self._build_concrete_element_support_prompt(post_content, elements_text, points_text)
-        elif dimension_name == "具象概念":
-            prompt = self._build_concrete_concept_support_prompt(post_content, elements_text, points_text)
-        elif dimension_name == "抽象概念":
-            prompt = self._build_abstract_concept_support_prompt(post_content, elements_text, points_text)
-        else:
-            logger.error(f"未知维度(意图支撑): {dimension_name}")
-            return []
-
-        # 使用视频分析接口,多模态评估意图支撑
-        result = LLMInvoker.safe_invoke_video_analysis(
-            operation_name=f"意图支撑评估-{dimension_name}-{point_type}",
-            video_file=video_file,
-            prompt=prompt,
-            agent=self,
-            fallback=[],
-        )
-
-        return result
-
-    def _evaluate_support_in_batches(
-        self,
-        elements: list,
-        points: list,
-        point_type: str,
-        max_batch_product: int = 100
-    ) -> list:
-        """分批评估相似度"""
-        if not self.is_initialized:
-            self.initialize()
-
-        if not points:
-            return []
-
-        num_elements = len(elements)
-        num_points = len(points)
-        max_elements_per_batch = max(1, int(max_batch_product / num_points))
-        num_batches = (num_elements + max_elements_per_batch - 1) // max_elements_per_batch
-
-        # 分批处理
-        batch_futures = {}
-        for batch_idx in range(num_batches):
-            start_idx = batch_idx * max_elements_per_batch
-            end_idx = min(start_idx + max_elements_per_batch, num_elements)
-            batch_elements = elements[start_idx:end_idx]
-
-            future = _GLOBAL_THREAD_POOL.submit(
-                self._evaluate_support_single_batch,
-                batch_elements, points, point_type
-            )
-            batch_futures[batch_idx] = future
-
-        # 收集结果
-        all_results = []
-        for batch_idx, future in batch_futures.items():
-            try:
-                batch_result = future.result()
-                if batch_result:
-                    all_results.extend(batch_result)
-            except Exception as e:
-                logger.error(f"批次 {batch_idx + 1} 失败: {e}")
-
-        # 合并并筛选(每个元素保留最相关的1-2个点)
-        merged_results = self._merge_batch_results(all_results)
-        return merged_results
-
-    def _evaluate_support_single_batch(
-        self,
-        batch_elements: list,
-        points: list,
-        point_type: str
-    ) -> list:
-        """单批次评估"""
-        if not self.is_initialized:
-            self.initialize()
-
-        elements_text = self._build_simple_items_text(batch_elements)
-        points_text = self._build_points_text(point_type, points)
-
-        prompt = f"""# 元素列表
-{elements_text}
-
-# 点列表
-{points_text}
-
-# 任务
-对每个元素计算元素与点的文本相似度和语义相似度
-
-# 输出(JSON)
-[
-    {{
-      "id": "元素id",
-      "名称": "元素名称",
-      "相似度结果": [
-        {{
-            "点":"点的名称",
-            "语义相似度":0.21,
-            "语义相似度理由": "理由",
-            "文本相似度":0.33,
-            "文本相似度理由": "理由"
-        }}
-      ]
-    }}
-]
-"""
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": prompt}
-        ]
-
-        result = LLMInvoker.safe_invoke(
-            self,
-            f"评估支撑{point_type}",
-            messages,
-            fallback=[]
-        )
-
-        return result
-
-    def _merge_batch_results(self, all_results: list) -> list:
-        """合并批次结果,每个元素只保留最相关的1-2个点"""
-        if not all_results:
-            return []
-
-        merged_map = {}
-        for item in all_results:
-            element_id = item.get("id")
-            if element_id not in merged_map:
-                merged_map[element_id] = {
-                    "id": element_id,
-                    "名称": item.get("名称"),
-                    "相似度结果": []
-                }
-            if not merged_map[element_id]["相似度结果"]:
-                merged_map[element_id]["相似度结果"] = item.get("相似度结果", [])
-
-        # 筛选每个元素的相似度结果
-        for element_data in merged_map.values():
-            similarity_results = element_data.get("相似度结果", [])
-            if not similarity_results:
-                continue
-
-            max_text_sim_point = max(similarity_results, key=lambda x: x.get("文本相似度", 0))
-            max_semantic_sim_point = max(similarity_results, key=lambda x: x.get("语义相似度", 0))
-
-            if max_text_sim_point.get("点") == max_semantic_sim_point.get("点"):
-                filtered_results = [max_text_sim_point]
-            else:
-                filtered_results = [max_text_sim_point, max_semantic_sim_point]
-
-            element_data["相似度结果"] = filtered_results
-
-        return list(merged_map.values())
-
-    def _merge_support_batch_results(self, all_results: list) -> list:
-        """合并批次结果(意图支撑),直接合并支撑的元素-点对"""
-        if not all_results:
-            return []
-
-        merged_map = {}
-        for item in all_results:
-            element_id = item.get("id")
-            if element_id not in merged_map:
-                merged_map[element_id] = {
-                    "id": element_id,
-                    "名称": item.get("名称"),
-                    "支撑结果": [],
-                }
-            # 这里假设下游会控制去重,只在首次合并时写入
-            if not merged_map[element_id]["支撑结果"]:
-                merged_map[element_id]["支撑结果"] = item.get("支撑结果", [])
-
-        return list(merged_map.values())
-
-    def _step6_filter_elements(
-        self,
-        analyzed_result: list,
-        scored_result: dict,
-        intention_support_result: dict,
-    ) -> list:
-        """Step 6: 筛选实质元素
-
-        新的保留策略(基于意图支撑关系 + 覆盖率进行筛选):
-        - 覆盖率和频次主要用于统计展示,但会作为必要条件之一
-        - 必须**同时**满足以下三个条件才保留:
-            - 出现频次 > 1
-            - 存在任意“意图支撑”关系
-            - 段落覆盖率 > 0.3(30%)
-        - 相似度评分相关逻辑已全部停用,不再参与筛选
-        """
-        if not analyzed_result:
-            return []
-
-        # 创建 analyzed_result 的映射
-        analyzed_map = {item.get("id"): item for item in analyzed_result}
-
-        # 创建意图支撑映射:只要某个元素在任一维度、任一点类型下有支撑关系,即视为“有支撑”
-        intention_support_map = {}
-        if intention_support_result:
-            for point_type in ["灵感点", "目的点", "关键点"]:
-                dimension_data = intention_support_result.get(point_type, [])
-                for item in dimension_data:
-                    if not isinstance(item, dict):
-                        continue
-                    element_id = item.get("id")
-                    support_results = item.get("支撑结果", [])
-                    if not element_id:
-                        continue
-                    if element_id not in intention_support_map:
-                        intention_support_map[element_id] = []
-                    # 只要有一条支撑结果就认为该元素“有支撑关系”
-                    if support_results:
-                        intention_support_map[element_id].extend(support_results)
-
-        # 筛选
-        filtered_ids = []
-        rejected_ids = []
-        for element_id, analyzed_data in analyzed_map.items():
-            element_name = analyzed_data.get("名称", "N/A")
-            
-            # 声音类型的实质元素(背景音乐、音效声等)直接通过筛选,不参与后续判断
-            sound_type_names = ["背景音乐", "音效声"]
-            if element_name in sound_type_names:
-                filtered_ids.append(element_id)
-                logger.info(
-                    f"✅ 保留: id={element_id}, name={element_name}, 原因=声音类型元素,豁免筛选"
-                )
-                continue
-            
-            # 确保 frequency 是整数类型
-            frequency_raw = analyzed_data.get("出现频次", 0)
-            try:
-                frequency = int(frequency_raw) if frequency_raw is not None else 0
-            except (ValueError, TypeError):
-                frequency = 0
-            # 确保 coverage_rate 是浮点数类型
-            coverage_rate_raw = analyzed_data.get("段落覆盖率", 0.0)
-            try:
-                coverage_rate = float(coverage_rate_raw) if coverage_rate_raw is not None else 0.0
-            except (ValueError, TypeError):
-                coverage_rate = 0.0
-
-            # 频次过滤:出现频次<=1 的直接过滤(不再继续做支撑和覆盖率判断)
-            if frequency <= 1:
-                rejected_ids.append(element_id)
-                logger.info(
-                    "❌ 过滤: id=%s, name=%s, 原因=出现频次<=1 (frequency=%d)",
-                    element_id,
-                    element_name,
-                    frequency,
-                )
-                continue
-
-            support_info = intention_support_map.get(element_id, [])
-            has_support = bool(support_info)
-            has_high_coverage = coverage_rate > 0.3
-
-            # 出现频次>1 且 有意图支撑关系 且 段落覆盖率 > 30%:直接保留,不进行相似度比较
-            if has_support and has_high_coverage:
-                filtered_ids.append(element_id)
-                logger.info(
-                    f"✅ 保留: id={element_id}, name={element_name}, "
-                    f"support_count={len(support_info)}, coverage={coverage_rate}"
-                )
-                continue
-
-            # 不满足“出现频次>1 + 有意图支撑 + 覆盖率>30%”的元素全部过滤
-            rejected_ids.append(element_id)
-            logger.info(
-                "❌ 过滤: id=%s, name=%s, 原因=无隐含概念豁免且"
-                "未同时满足出现频次>1、有意图支撑关系和段落覆盖率>0.3 "
-                "(frequency=%d, coverage=%.4f)",
-                element_id,
-                element_name,
-                frequency,
-                coverage_rate,
-            )
-
-        logger.info(f"筛选完成: {len(filtered_ids)}/{len(analyzed_result)} 通过")
-
-        return filtered_ids
-
-    def _step7_categorize_elements(
-        self,
-        substance_elements: List[dict],
-        filtered_ids: list
-    ) -> dict:
-        """Step 7: 元素分类 - 按二级维度分别分类"""
-        if not filtered_ids:
-            return {}
-
-        # 只保留筛选后的元素
-        filtered_elements = [
-            elem for elem in substance_elements
-            if elem.get("id") in filtered_ids
-        ]
-
-        # 按二级维度分组
-        dimension_groups = {
-            "具体元素": [],
-            "具象概念": [],
-            "抽象概念": [],
-        }
-
-        for elem in filtered_elements:
-            second_level = elem.get("维度", {}).get("二级", "")
-            if second_level in dimension_groups:
-                dimension_groups[second_level].append(elem)
-
-        # 并行分类
-        categorization_results = {}
-        futures = {}
-
-        for dimension_name, elements in dimension_groups.items():
-            if not elements:
-                continue
-
-            future = _GLOBAL_THREAD_POOL.submit(
-                self._categorize_single_dimension,
-                dimension_name,
-                elements
-            )
-            futures[dimension_name] = future
-
-        # 收集结果
-        for dimension_name, future in futures.items():
-            try:
-                categorization_results[dimension_name] = future.result()
-            except Exception as e:
-                logger.error(f"{dimension_name} 分类失败: {e}")
-                categorization_results[dimension_name] = {"元素分类": []}
-
-        return categorization_results
-
-    def _categorize_single_dimension(
-        self,
-        dimension_name: str,
-        elements: list
-    ) -> dict:
-        """对单个维度的元素进行分类"""
-        if not self.is_initialized:
-            self.initialize()
-
-        elements_text = json.dumps([
-            {"id": elem.get("id"), "名称": elem.get("名称"), "描述": elem.get("描述")}
-            for elem in elements
-        ], ensure_ascii=False, indent=2)
-
-        prompt = f"""# 任务
-对"{dimension_name}"维度的元素进行分类
-
-# 元素列表
-{elements_text}
-
-# 分类要求
-
-## 核心原则
-1. **单一原子名词**: 分类名称必须是单一的原子名词
-2. **MECE原则**: 分类之间相互独立、完全穷尽
-3. **确定性归属**: 每个元素只能归属唯一一个分类
-4. **层级限制**: 最多2层
-5. 元素可以没有分类,不要强行归类
-6. 分类下面至少要有2个元素,否则不要分类
-
-## 实质维度的分类逻辑
-- **核心原则**:按照**内容本质、属性特征、功能作用**等角度来分类
-- **包含**:
-    - 物理特征:形态、材质、颜色等
-    - 功能用途:工具、装饰、食物等
-    - 概念类别:情感、价值观、技能等
-    - 领域归属:科技、艺术、健康等
-
-# 输出格式(JSON)
-{{
-  "元素分类": [
-    {{
-      "元素id": "元素的ID",
-      "元素名称": "元素名称",
-      "分类": ["一级分类","二级分类","..."]
-    }}
-  ]
-}}
-"""
-
-        messages = [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": prompt}
-        ]
-
-        result = LLMInvoker.safe_invoke(
-            self,
-            f"分类-{dimension_name}",
-            messages,
-            fallback={}
-        )
-
-        return result
-
-    def _merge_all_info(
-        self,
-        substance_elements: List[dict],
-        analyzed_result: list,
-        scored_result: dict,
-        intention_support_result: dict,
-        filtered_ids: list,
-        categorized_result: dict
-    ) -> list:
-        """Step 8: 合并所有信息 - 每个元素包含所有中间信息"""
-        if not filtered_ids:
-            return []
-
-        # 创建映射
-        extraction_map = {item.get("id"): item for item in substance_elements}
-        analyzed_map = {item.get("id"): item for item in analyzed_result}
-
-        # 创建评分映射(相似度)
-        scored_map = {}
-        for dimension in ["灵感点", "目的点", "关键点"]:
-            dimension_data = scored_result.get(dimension, [])
-            for item in dimension_data:
-                if not isinstance(item, dict):
-                    continue
-                element_id = item.get("id")
-                if element_id not in scored_map:
-                    scored_map[element_id] = {}
-                similarity_results = item.get("相似度结果", [])
-                sorted_results = sorted(
-                    similarity_results,
-                    key=lambda x: (x.get("文本相似度", 0), x.get("语义相似度", 0)),
-                    reverse=True
-                )
-
-                scored_map[element_id][dimension] = sorted_results
-
-        # 创建意图支撑映射
-        intention_support_map = {}
-        if intention_support_result:
-            for dimension in ["灵感点", "目的点", "关键点"]:
-                dimension_data = intention_support_result.get(dimension, [])
-                for item in dimension_data:
-                    if not isinstance(item, dict):
-                        continue
-                    element_id = item.get("id")
-                    if element_id not in intention_support_map:
-                        intention_support_map[element_id] = {}
-                    support_results = item.get("支撑结果", [])
-                    intention_support_map[element_id][dimension] = support_results
-
-        # 创建分类映射
-        category_map = {}
-        for dimension_data in categorized_result.values():
-            element_classifications = dimension_data.get("元素分类", [])
-            for classification in element_classifications:
-                element_id = classification.get("元素id")
-                category_info = classification.get("分类", {})
-                if element_id:
-                    category_map[element_id] = category_info
-
-        # 合并信息
-        final_elements = []
-        for element_id in filtered_ids:
-            base_info = extraction_map.get(element_id, {})
-            analysis_info = analyzed_map.get(element_id, {})
-            scoring_info = scored_map.get(element_id, {})
-            intention_info = intention_support_map.get(element_id, {})
-            category_info = category_map.get(element_id, {})
-
-            merged_element = {
-                "id": base_info.get("id"),
-                "名称": base_info.get("名称"),
-                "描述": base_info.get("描述"),
-                "维度": base_info.get("维度", {}),
-                "分类": category_info,
-                "共性分析": {
-                    "出现频次": analysis_info.get("出现频次", 0),
-                    "出现段落列表": analysis_info.get("出现段落列表", []),
-                    "出现段落数": analysis_info.get("出现段落数", 0),
-                    "段落覆盖率": analysis_info.get("段落覆盖率", 0.0)
-                },
-                "多维度评分": {
-                    "灵感点": scoring_info.get("灵感点", []),
-                    "目的点": scoring_info.get("目的点", []),
-                    "关键点": scoring_info.get("关键点", [])
-                },
-                "意图支撑": {
-                    "灵感点": intention_info.get("灵感点", []),
-                    "目的点": intention_info.get("目的点", []),
-                    "关键点": intention_info.get("关键点", [])
-                }
-            }
-
-            # 根据不同类型添加特定字段
-            second_level = base_info.get("维度", {}).get("二级", "")
-            if second_level == "具体元素":
-                merged_element["来源"] = base_info.get("来源", [])
-            elif second_level == "具象概念":
-                merged_element["来源"] = base_info.get("来源", [])
-                merged_element["字面位置"] = base_info.get("字面位置", [])
-            elif second_level == "抽象概念" or second_level == "隐含概念":
-                merged_element["类型"] = base_info.get("类型", "")
-                merged_element["来源"] = base_info.get("来源", {})
-                merged_element["推理过程"] = base_info.get("推理过程", "")
-                merged_element["推理层次"] = base_info.get("推理层次", 1)
-
-            final_elements.append(merged_element)
-
-        return final_elements
-
-    # ========== 辅助方法 ==========
-
-    def _build_section_text(self, section_division: dict) -> str:
-        """构建段落划分文本"""
-        if not section_division:
-            return "无段落划分信息"
-
-        sections = section_division.get("段落列表", [])
-        if not sections:
-            return "无段落信息"
-
-        def build_section_list(section_list, indent=0):
-            text = ""
-            for section in section_list:
-                if section.get('子项'):
-                    text += build_section_list(section['子项'], indent + 1)
-                else:
-                    section_id = section.get('id', 'N/A')
-                    section_desc = section.get('描述', 'N/A')
-                    content_range = section.get('内容范围', 'N/A')
-                    text += f"{section_id}: {section_desc}\n内容范围: {content_range}\n"
-            return text
-
-        return "段落列表:\n" + build_section_list(sections)
-
-    def _build_post_content(self, text_data: dict) -> str:
-        """构建原文内容文本(用于意图支撑判断)
-
-        这里不假设具体结构,直接以 JSON 形式展开,保证信息完整可见。
-        """
-        if not text_data:
-            return "无文本内容"
-        try:
-            return json.dumps(text_data, ensure_ascii=False, indent=2)
-        except TypeError:
-            # 避免非序列化对象导致报错
-            return str(text_data)
-
-    def _build_simple_items_text_dimension(self, elements: list, dimension_name: str) -> str:
-        """构建某个维度下元素列表文本(用于意图支撑判断)"""
-        simple_items = [
-            {
-                "id": elem.get("id", "N/A"),
-                "名称": elem.get("名称", "N/A"),
-                "描述": elem.get("描述", "N/A"),
-            }
-            for elem in elements
-        ]
-        return json.dumps(
-            {"维度": dimension_name, "元素列表": simple_items},
-            ensure_ascii=False,
-            indent=2,
-        )
-
-
-    def _build_simple_items_text(self, elements: list) -> str:
-        """构建元素列表文本"""
-        grouped_elements = {
-            "具体元素": [],
-            "具象概念": [],
-            "抽象概念": [],
-        }
-
-        for elem in elements:
-            element_type = elem.get('维度', {}).get('二级', 'N/A')
-            element_data = {
-                "id": elem.get('id', 'N/A'),
-                "名称": elem.get('名称', 'N/A'),
-                "描述": elem.get('描述', 'N/A')
-            }
-            if element_type in grouped_elements:
-                grouped_elements[element_type].append(element_data)
-
-        filtered_groups = {k: v for k, v in grouped_elements.items() if v}
-        return json.dumps(filtered_groups, ensure_ascii=False, indent=2)
-
-    def _build_points_text(self, point_type: str, points_data) -> str:
-        """构建点列表文本"""
-        if not points_data:
-            return f"无{point_type}信息"
-        filtered_points = [
-            {"名称": item.get(point_type, 'N/A')}
-            for item in points_data if isinstance(item, dict)
-        ]
-        return json.dumps(filtered_points, ensure_ascii=False, indent=2)
-
-    def _build_concrete_element_support_prompt(
-        self, post_content: str, elements_text: str, points_text: str
-    ) -> str:
-        """构建具体元素的意图支撑判断 prompt(基于视频画面)"""
-        return f"""# 原文内容
-
-{post_content}
-
-# 具体元素列表
-
-{elements_text}
-
-# 点列表
-
-{points_text}
-
-# 任务
-
-判断每个**具体元素**是否对点有关键支撑
-
-## 具体元素定义(重要!)
-
-- 定义:视频画面中直接观察到的单一视觉实体对象
-- 判断标准:可以指着画面说"这是一个X"
-- 剥离测试:去掉表达方式后,该视觉实体仍然存在
-
-## 核心判断原则:仅基于视频画面语境
-
-### 关键约束
-
-1. 只看视频画面:具体元素的支撑判断**只能基于视频中的视觉实体**,不能基于文字论述
-2. 视觉实体角色:该视觉实体在视频画面中的作用是什么?
-   - ✅ 核心展示对象:该视觉实体是画面的核心展示内容
-   - ❌ 辅助/装饰:该视觉实体只是背景、装饰、示意
-3. 关键支撑:该视觉实体对点的表达是否关键?去掉它是否会明显削弱点的支撑?
-
-### 判断流程
-
-1. 理解点的意图,点想表达什么
-2. 在视频画面中找到该视觉实体
-3. 判断:去掉该视觉实体,是否无法完整表达点
-   - 如果是,支撑
-   - 如果不是,不支撑
-
-### 严格标准
-
-- 禁止使用文字内容来判断具体元素的支撑
-- 禁止仅凭名称字面匹配判定支撑
-- 必须基于该视觉实体在画面中的实际角色
-
-# 输出(JSON)
-
-只输出有关键支撑的元素-点对,不支撑的不输出
-
-[
-  {{
-    "id": "元素id",
-    "名称": "元素名称",
-    "支撑结果": [
-      {{
-        "点": "点的名称",
-        "点的意图": "点想表达什么",
-        "支撑理由": "说明为什么去掉该视觉实体,会削弱点的表达,程度达到30%以上"
-      }}
-    ]
-  }}
-]
-
-注意:
-1. 只基于视频画面判断
-2. 只输出"关键支撑"的元素-点对
-3. 辅助/装饰元素直接排除,不输出
-4. 必须基于视频画面中的视觉实体判断,不能做字面匹配"""
-
-    def _build_concrete_concept_support_prompt(
-        self, post_content: str, elements_text: str, points_text: str
-    ) -> str:
-        """构建具象概念的意图支撑判断 prompt(基于文字语境)"""
-        return f"""# 原文内容
-
-{post_content}
-
-# 具象概念列表
-
-{elements_text}
-
-# 点列表
-
-{points_text}
-
-# 任务
-
-判断每个**具象概念**是否对点有关键支撑
-
-## 具象概念定义(重要!)
-
-- 定义:文字中字面出现的名词(包括标题、正文、字幕、视频画面中的文字)
-- 判断标准:文字中实际出现,禁止语义推导
-
-## 核心判断原则:仅基于文字语境(包含视频中的文字)
-
-### 关键约束
-
-1. 只看文字:具象概念的支撑判断**只能基于文字中的概念论述**,不能基于视频中的视觉实体
-2. 概念角色:该概念在文字论述中的作用是什么?
-   - ✅ 核心论述概念:该概念是文字论述的核心对象、关键主题
-   - ❌ 次要提及:该概念只是顺带提及、举例说明
-3. 关键支撑:该概念对点的表达是否关键?去掉它是否会明显削弱点的支撑?
-
-### 判断流程
-
-1. 理解点的意图,点想表达什么
-2. 在标题、正文、字幕、画面文字中找到该概念出现的位置
-3. 判断:去掉该段文字,是否无法完整表达点
-   - 如果是,支撑
-   - 如果不是,不支撑
-
-### 严格标准
-
-- 禁止用视频画面中的视觉实体来判断具象概念的支撑
-- 禁止仅凭名称字面匹配判定支撑
-- 必须判断该概念在文字论述中的实际角色
-
-# 输出(JSON)
-
-只输出有关键支撑的元素-点对,不支撑的不输出
-
-[
-  {{
-    "id": "元素id",
-    "名称": "元素名称",
-    "支撑结果": [
-      {{
-        "点": "点的名称",
-        "点的意图": "点想表达什么",
-        "支撑理由": "说明为什么去掉该概念,会削弱点的表达,程度达到30%以上"
-      }}
-    ]
-  }}
-]
-
-注意:
-1. 只基于文字判断
-2. 只输出"关键支撑"的元素-点对
-3. 次要提及的概念直接排除,不输出
-4. 必须基于文字中的概念论述判断,不能做字面匹配"""
-
-    def _build_abstract_concept_support_prompt(
-        self, post_content: str, elements_text: str, points_text: str
-    ) -> str:
-        """构建抽象概念的意图支撑判断 prompt"""
-        return f"""# 原文内容
-
-{post_content}
-
-# 抽象概念列表
-
-{elements_text}
-
-# 点列表
-
-{points_text}
-
-# 任务
-
-判断每个**抽象概念**是否对点有关键支撑
-
-## 抽象概念定义(重要!)
-
-- 定义:从具体元素和具象概念中理解到的上位抽象
-- 类型1-上位抽象(归类):是下位元素的类别、分类
-- 类型2-引申含义:需要理解上下文的深层含义
-- 剥离测试:去掉表达方式后,该抽象概念仍然存在
-
-## 核心判断原则:基于来源语境
-
-### 关键约束
-
-1. 追溯来源:抽象概念来源于具体元素和/或具象概念,必须追溯到来源
-2. 继承语境:抽象概念的语境继承自其来源
-   - 如果来源主要是具体元素 → 语境偏向视频画面
-   - 如果来源主要是具象概念 → 语境偏向文字
-   - 如果来源混合 → 综合判断
-3. 关键支撑:该抽象概念对点的表达是否关键?
-
-### 判断流程
-
-1. 理解点的意图:点想表达什么?
-2. 根据来源确定该抽象概念的主要语境
-3. 判断:去掉该抽象概念,是否无法完整表达点
-   - 如果是,支撑
-   - 如果不是,不支撑
-
-### 严格标准
-
-- 必须基于来源的语境来判断
-- 禁止仅凭名称字面匹配判定支撑
-- 必须能够追溯到来源元素,验证支撑关系
-
-# 输出(JSON)
-
-只输出有关键支撑的元素-点对,不支撑的不输出
-
-[
-  {{
-    "id": "元素id",
-    "名称": "元素名称",
-    "支撑结果": [
-      {{
-        "点": "点的名称",
-        "来源追溯": "该抽象概念的来源(具体元素/具象概念)及其语境",
-        "语境分析": "基于来源确定的语境(画面/文字/混合)",
-        "支撑理由": "说明该抽象概念为什么对该点有关键支撑"
-      }}
-    ]
-  }}
-]
-
-注意:
-1. 必须追溯到来源元素
-2. 必须继承来源的语境来判断
-3. 只输出"关键支撑"的元素-点对
-4. 禁止字面匹配"""
-
-    # 隐含概念相关的意图支撑判断已移除
-
-    def _count_sections(self, section_division: dict) -> int:
-        """统计段落总数(只统计叶子节点)"""
-        if not section_division:
-            return 0
-
-        sections = section_division.get("段落列表", [])
-        if not sections:
-            return 0
-
-        def count_leaf_nodes(section_list):
-            count = 0
-            for section in section_list:
-                children = section.get("子项", [])
-                if children:
-                    count += count_leaf_nodes(children)
-                else:
-                    count += 1
-            return count
-
-        return count_leaf_nodes(sections)
-
-    def _build_messages(self, state: dict) -> List[dict]:
-        """构建消息 - 本Agent不使用此方法"""
-        return []
-
-    def _update_state(self, state: dict, response) -> dict:
-        """更新状态 - 本Agent不使用此方法"""
-        return state

+ 0 - 380
src/components/agents/search_keyword_agent.py

@@ -1,380 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-搜索关键词Agent
-
-功能: 基于视频分析结果(灵感点、目的点、关键点、选题理解)生成抖音/TikTok组合搜索词列表
-核心任务: 分析视频核心构成,输出用于检索竞品内容的搜索词组合
-特征: 使用"元素+语境"组合法,生成精准搜索词
-"""
-
-from typing import Any, Dict, List
-import json
-import re
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class SearchKeywordAgent(BaseLLMAgent):
-    """搜索关键词Agent - 生成抖音/TikTok组合搜索词列表"""
-
-    def __init__(
-        self,
-        name: str = "search_keyword_agent",
-        description: str = "搜索关键词Agent - 生成组合搜索词列表",
-        model_provider: str = "google_genai",
-        temperature: float = 0.7,
-        max_tokens: int = 8192
-    ):
-        """
-        初始化搜索关键词Agent
-
-        Args:
-            name: Agent名称
-            description: Agent描述
-            model_provider: 模型提供商 ("openai" 或 "google_genai")
-            temperature: 生成温度,控制创造性
-            max_tokens: 最大token数
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词 - 定义角色、能力、输出规范"""
-        return """# Role
-你是一位精通全网内容分发逻辑与搜索引擎优化(SEO)的数据分析师。
-
-# Task
-我将提供一段关于视频内容的详细结构化数据(JSON格式),包含【灵感点】、【目的点】和【关键点】。
-你的任务是分析该视频的核心构成,并输出一份**抖音/TikTok组合搜索词列表**,用于检索与该视频**题材相同、切入点相似、受众重合**的竞品内容。
-
-# Core Strategy: "元素+语境" 组合法
-为了保证搜索结果的精准度,请严格遵循 **`[核心实体/具象元素]` + `[主题语境/内容属性]`** 的组合公式进行输出。
-
-### 1. 定义"核心实体" (Left Side)
-请从 JSON 的 `关键点 (Key Points)` 和 `视频信息` 中提取最具体的名词。
-* **包括**:具体的人名/地名/产品名、特定的时间节点、核心物体、显性的视觉符号、特定的动作名称。
-* **排除**:泛泛的大类词(如"生活"、"快乐"、"健康")。
-
-### 2. 定义"主题语境" (Right Side)
-请从 JSON 的 `灵感点 (Inspiration)` 和 `目的点 (Purpose)` 中提取修饰性的词汇。
-* **包括**:内容的体裁(教程/测评/Vlog)、情感色彩(治愈/吐槽/焦虑)、功能属性(避坑/省钱/变美)、特定的叙事逻辑(反转/揭秘/老话)。
-
-# Execution Rules (执行规则)
-请扫描 JSON 数据,针对不同维度生成搜索词:
-
-1.  **提取"强钩子"组合**:
-    * 找到视频开头最吸引人的视觉或概念元素(灵感点)。
-    * *通用公式*:`[核心视觉/事件]` + `[钩子属性(如:真相/后续/反常)]`
-2.  **提取"核心内容"组合**:
-    * 找到视频主要传达的知识、剧情或展示物(关键点)。
-    * *通用公式*:`[具体事物/动作]` + `[垂直领域词(如:做法/技巧/评价)]`
-3.  **提取"受众痛点"组合**:
-    * 找到视频想要解决的问题或满足的需求(目的点)。
-    * *通用公式*:`[受众标签/场景]` + `[痛点/解决方案]`
-
-# Output Format
-请输出 10-12 个组合搜索词,按以下格式排列,并简要解释组合逻辑:
-* **`关键词` + `语境词`** (例如:*空气炸锅 翻车* / *重阳节 禁忌* / *面试 自我介绍*)
-    * *逻辑:提取了[X]元素,结合[Y]主题。*
-
-输出格式为JSON:
-```json
-{
-  "搜索词列表": [
-    {
-      "搜索词": "关键词 语境词",
-      "组合逻辑": "提取了[X]元素,结合[Y]主题。"
-    }
-  ]
-}
-```"""
-
-    def _build_messages(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """
-        构建LLM消息
-
-        Args:
-            state: 包含视频和其他agent结果的状态字典,需要包含:
-                - inspiration_points: 灵感点提取结果
-                - purpose_point: 目的点提取结果
-                - key_points: 关键点提取结果
-                - topic_selection_understanding: 选题理解结果
-
-        Returns:
-            消息列表
-        """
-        # 提取输入数据
-        inspiration_points = state.get("inspiration_points", [])
-        purpose_point = state.get("purpose_point", {})
-        key_points = state.get("key_points", {})
-        topic_selection_understanding = state.get("topic_selection_understanding", {})
-
-        # 构建提示词
-        prompt = self._build_user_prompt(
-            inspiration_points,
-            purpose_point,
-            key_points,
-            topic_selection_understanding
-        )
-
-        # 构建消息
-        return [
-            {"role": "system", "content": self.system_prompt},
-            {"role": "user", "content": prompt}
-        ]
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """
-        更新状态
-
-        Args:
-            state: 原始状态
-            response: LLM响应
-
-        Returns:
-            更新后的状态
-        """
-        # 解析响应
-        result = self._parse_response(response.content)
-
-        return {
-            "search_keywords": result
-        }
-
-    def _build_user_prompt(
-        self,
-        inspiration_points: Any,
-        purpose_point: Dict[str, Any],
-        key_points: Dict[str, Any],
-        topic_selection_understanding: Dict[str, Any]
-    ) -> str:
-        """构建用户提示 - 提供数据和执行指令"""
-        # 格式化各维度数据
-        inspiration_section = self._format_inspiration_points(inspiration_points)
-        purpose_section = self._format_purpose_point(purpose_point)
-        key_points_section = self._format_key_points(key_points)
-        topic_section = self._format_topic_selection_understanding(topic_selection_understanding)
-
-        return f"""# Input Data
-
-## 选题理解结果
-{topic_section}
-
-## 灵感点分析
-{inspiration_section}
-
-## 目的点分析
-{purpose_section}
-
-## 关键点分析
-{key_points_section}
-
----
-
-# 执行指令
-
-基于以上数据,输出 10-12 个组合搜索词。
-
-## 质量要求
-- **搜索词数量**: 10-12 个
-- **组合公式**: `[核心实体/具象元素]` + `[主题语境/内容属性]`
-- **覆盖维度**: 需要覆盖"强钩子"、"核心内容"、"受众痛点"三个维度
-- **精准度**: 搜索词应该能够检索到与该视频题材相同、切入点相似、受众重合的竞品内容
-- **组合逻辑**: 每个搜索词需要简要说明提取了哪些元素,结合了哪些主题
-
-**重要提醒**:
-- 核心实体必须具体,避免泛泛的大类词
-- 主题语境要能体现内容的体裁、情感、功能或叙事逻辑
-- 搜索词不能出现雷同和近似的词语
-- 搜索词应该符合抖音/TikTok用户的搜索习惯"""
-
-    def _format_topic_selection_understanding(self, topic_selection_understanding: Dict[str, Any]) -> str:
-        """格式化选题理解信息"""
-        if not topic_selection_understanding:
-            return "**未找到选题理解结果**"
-
-        主题 = topic_selection_understanding.get("主题", "")
-        描述 = topic_selection_understanding.get("描述", "")
-
-        formatted = ""
-        if 主题:
-            formatted += f"**主题**: {主题}\n"
-        if 描述:
-            formatted += f"**描述**: {描述}\n"
-
-        return formatted if formatted else "**未找到选题理解结果**"
-
-    def _format_inspiration_points(self, inspiration_points: Any) -> str:
-        """格式化灵感点信息"""
-        # 新结构:inspiration_points 直接是列表
-        if isinstance(inspiration_points, list):
-            if not inspiration_points:
-                return "**未提取到灵感点**"
-
-            formatted = f"**灵感点总数**: {len(inspiration_points)}\n\n"
-
-            for idx, point in enumerate(inspiration_points, 1):
-                formatted += f"### 灵感点 {idx}: {point.get('灵感点', 'N/A')}\n"
-                formatted += f"**分类**: {point.get('分类', '')}\n"
-                formatted += f"**描述**: {point.get('描述', '')}\n"
-                if point.get("推理"):
-                    formatted += f"**推理**: {point.get('推理', '')}\n"
-                formatted += "\n"
-
-            return formatted
-
-        # 旧结构:{"points": [...]} 或字典格式
-        if isinstance(inspiration_points, dict):
-            # 旧结构:points列表
-            if "points" in inspiration_points:
-                points = inspiration_points.get("points", [])
-                if not points:
-                    return "**未提取到灵感点**"
-
-                formatted = f"**灵感点总数**: {len(points)}\n\n"
-
-                for idx, point in enumerate(points, 1):
-                    formatted += f"### 灵感点 {idx}: {point.get('灵感点表象', 'N/A')}\n"
-                    formatted += f"**本质**: {point.get('灵感点本质', '')}\n"
-                    formatted += f"{point.get('本质详细说明', '')}\n\n"
-
-                return formatted
-
-        return "**未提取到灵感点**"
-
-    def _format_purpose_point(self, purpose_point: Dict[str, Any]) -> str:
-        """格式化目的点信息"""
-        if not purpose_point:
-            return "**未提取到目的点**"
-
-        # 新结构:{"perspective": "创作者视角", "purposes": [...], "total_count": 2}
-        purposes = purpose_point.get("purposes", [])
-        if not purposes:
-            return "**未提取到目的点**"
-
-        formatted = f"**目的点总数**: {len(purposes)}\n\n"
-
-        for idx, purpose in enumerate(purposes, 1):
-            维度 = purpose.get("维度", {})
-            if isinstance(维度, dict):
-                维度_str = f"{维度.get('一级分类', '')}/{维度.get('二级分类', '')}"
-            else:
-                维度_str = str(维度)
-
-            目的点 = purpose.get("目的点", "N/A")
-            描述 = purpose.get("描述", "")
-
-            formatted += f"### 目的点 {idx}: {目的点}\n"
-            formatted += f"**维度**: {维度_str}\n"
-            if 描述:
-                formatted += f"**描述**: {描述}\n"
-            formatted += "\n"
-
-        return formatted
-
-    def _format_key_points(self, key_points: Dict[str, Any]) -> str:
-        """格式化关键点信息 - 支持层级结构"""
-        if not key_points or "key_points" not in key_points:
-            return "**未提取到关键点**"
-
-        points = key_points.get("key_points", [])
-        if not points:
-            return "**未提取到关键点**"
-
-        total_count = key_points.get("total_count", len(points))
-        root_count = key_points.get("root_count", len(points))
-        hierarchy_enabled = key_points.get("hierarchy_enabled", False)
-
-        formatted = f"**关键点总数**: {total_count}\n"
-        if hierarchy_enabled:
-            formatted += f"**一级关键点数**: {root_count}\n"
-            formatted += f"**层级结构**: 已启用\n\n"
-        else:
-            formatted += "\n"
-
-        # 递归格式化关键点树
-        def format_point_tree(point: Dict[str, Any], level: int = 1, index: int = 1) -> str:
-            indent = "  " * (level - 1)
-            result = f"{indent}### 关键点 {index} (层级 {level}): {point.get('关键点', 'N/A')}\n"
-            result += f"{indent}**维度**: [{point.get('维度大类', '')}] {point.get('维度细分', '')}\n"
-            result += f"{indent}**描述**: {point.get('描述', '')}\n"
-
-            # 递归处理子关键点
-            children = point.get("children", [])
-            if children:
-                result += f"{indent}**子关键点**: {len(children)}个\n\n"
-                for child_idx, child in enumerate(children, 1):
-                    result += format_point_tree(child, level + 1, child_idx)
-            else:
-                result += "\n"
-
-            return result
-
-        # 格式化所有一级关键点
-        for idx, point in enumerate(points, 1):
-            formatted += format_point_tree(point, level=1, index=idx)
-
-        return formatted
-
-    def _parse_response(self, response: str) -> Dict[str, Any]:
-        """解析LLM响应"""
-        try:
-            # 尝试提取JSON代码块
-            json_match = re.search(r'```(?:json)?\s*(.*?)\s*```', response, re.DOTALL)
-            if json_match:
-                json_str = json_match.group(1)
-            else:
-                json_str = response
-
-            # 解析JSON
-            result = json.loads(json_str)
-
-            # 确保包含必需字段
-            if "搜索词列表" not in result:
-                result["搜索词列表"] = []
-
-            # 验证每个搜索词的结构
-            search_keywords = result.get("搜索词列表", [])
-            validated_keywords = []
-            for keyword in search_keywords:
-                if isinstance(keyword, dict):
-                    # 确保包含必需字段
-                    validated_keyword = {
-                        "搜索词": keyword.get("搜索词", ""),
-                        "组合逻辑": keyword.get("组合逻辑", "")
-                    }
-                    if validated_keyword["搜索词"]:  # 只保留有效的搜索词
-                        validated_keywords.append(validated_keyword)
-
-            result["搜索词列表"] = validated_keywords
-            result["总数"] = len(validated_keywords)
-
-            return result
-
-        except json.JSONDecodeError as e:
-            logger.error(f"JSON解析失败: {e}")
-            logger.error(f"响应内容: {response[:500]}")
-            # JSON解析失败,返回默认结构
-            return {
-                "搜索词列表": [],
-                "总数": 0,
-                "错误": "解析失败"
-            }
-        except Exception as e:
-            logger.error(f"解析响应时发生错误: {e}", exc_info=True)
-            return {
-                "搜索词列表": [],
-                "总数": 0,
-                "错误": str(e)
-            }

+ 0 - 196
src/components/agents/structure_agent.py

@@ -1,196 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-结构化内容库Agent(视频分析版本)
-
-功能:
-1. 从 state 中读取视频文件对象
-2. 使用 Gemini 视频分析能力,直接基于视频内容进行结构化解构
-3. 输出严格符合约定结构的 JSON 数据,用于内容库建设
-"""
-
-from typing import Any, Dict, List
-
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker, get_video_file_from_state
-
-logger = get_logger(__name__)
-
-
-class StructureAgent(BaseLLMAgent):
-    """结构化内容库Agent - 基于视频分析的显性事实解构"""
-
-    def __init__(
-        self,
-        name: str = "structure_agent",
-        description: str = "结构化内容库Agent - 基于视频内容显性事实进行解构",
-        model_provider: str = "google_genai",
-        temperature: float = 0.2,
-        max_tokens: int = 20480,
-    ):
-        # 这里的 system_prompt 仅用于初始化模型(提供 model_name),
-        # 实际的视频分析逻辑全部通过 safe_invoke_video_analysis 的 prompt 控制
-        system_prompt = "你是一名专注于中国60岁以上老年群体内容分析的结构化解构专家。"
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens,
-        )
-
-    # ==================== 对外主入口 ====================
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态:读取视频 → 调用视频分析 → 写回结构化结果
-
-        步骤:
-        1. 从 state 中读取视频文件对象
-        2. 直接调用 safe_invoke_video_analysis,基于视频内容做结构化解构
-        3. 将结构化结果写回 state
-
-        Returns:
-            dict: {"structure_data": <LLM解析后的JSON>}
-        """
-        if not self.is_initialized:
-            self.initialize()
-
-        logger.info("StructureAgent 开始执行")
-
-        # Step 0: 从 state 中获取视频文件对象
-        video_file = get_video_file_from_state(state)
-        if not video_file:
-            logger.warning("无法获取视频文件对象,跳过结构化解构分析")
-            return {
-                "structure_data": {
-                    "错误": "无法获取视频文件对象",
-                },
-            }
-
-        try:
-            # 直接基于视频内容进行结构化解构分析
-            logger.info("基于视频内容进行结构化解构分析...")
-            prompt = self._build_prompt()
-
-            result = LLMInvoker.safe_invoke_video_analysis(
-                "结构化内容库解构",
-                video_file,
-                prompt,
-                agent=self,
-                fallback=self._build_empty_result(),
-            )
-
-            logger.info("StructureAgent 执行完成")
-
-            # 统一写回到 state 的字段名
-            return {"structure_data": result}
-
-        except Exception as e:
-            logger.error(f"StructureAgent 执行失败: {e}", exc_info=True)
-            return {
-                "structure_data": {
-                    "错误": str(e),
-                }
-            }
-
-    # ==================== Prompt 构建 ====================
-
-    def _build_prompt(self) -> str:
-        """构建基于视频内容的结构化解构 Prompt"""
-        return """
-# 角色设定
-你是一名专注于中国60岁以上老年群体的内容分析专家。你的核心能力是从视频内容中基于"显性事实"精准提炼信息,构建结构化的内容库数据。
-
-# 任务目标
-请仔细观看我提供的【视频内容】,进行深度解构,并输出一个严格符合要求的 JSON 格式数据。
-
-# 核心原则
-1. 显性事实原则:只解构视频中明确出现过的人、事、物、观点。绝对禁止基于经验进行主观推测(例如:视频未提"老年人",不可强行打上"老年人"标签)。
-2. 全中文原则:输出结果中的所有字段名(Key)和内容值(Value)必须全部使用简体中文,不得出现任何英语单词。
-3. 格式规范原则:输出纯净的 JSON 格式,不要包含 Markdown 代码块标记(```json),也不要包含任何解释性文字。
-
-# 解构维度说明
-你需要将信息解构为以下三个核心表:
-## 1. 选题信息表
-目的:提炼视频的核心事实命题。
-- 宏观母题:用一句话概括视频整体内容的完整命题(格式:主语 + 核心事件/结论)。
-- 高潜子题列表:
-  - 逻辑:仅当视频为"盘点/合集/并列结构"时(如"12个村庄"、"4个养生妙招"),提取其中具备独立爆款潜力的具体条目。
-  - 注意:如果视频为"线性/单体/情感叙事"(如"怀念老同学"、"老同学情谊"),此项请输出空数组 [],严禁强行拆解。
-## 2. 模型信息表
-目的:提取逻辑公式,用于横向扩展匹配同构内容。
-- 抽象逻辑模型列表:将具体的人/事/物抽象为变量,形成逻辑公式。
-  - 格式示例:"[对象:伟人] + [反常细节:补丁衣服] + [精神:艰苦朴素]"
-  - 要求:提取视频中最核心的 1-2 个逻辑模型。
-## 3. 层级标签树
-目的:提取实体层级,用于精准搜索与纵向升维。
-- L1_实例名词:视频中无可辩驳的、最具体的专有名词(人名、地名、物名)。
-- L1_限定词:关键!视频中明确出现的、限定了受众身份或时间状态的词(如:晚年、白头、退休、1978年、农村、老祖宗)。如果没有则留空。
-- L2_具体品类:L1 的直接父级分类。必须精准(例如:是"晚年社交"而不是"社交",是"红色地标"而不是"景点")。
-- L3_兴趣领域:注意!必须使用"用户兴趣/情绪分类",严禁使用"历史、地理、生物"等学术分类。
-  - 正确示例:红色记忆、奇闻异事、大国工程、中医智慧、怀旧岁月、玄学民俗。
-  - 错误示例:历史、旅游、建筑、医学、生物。
-- L4_情绪价值:内容背后的情绪钩子或社会价值。仅在有明显的升维价值时提取(如:民族自信、致青春、趋吉避凶、鞠躬尽瘁),否则留空。
-
-# 参考案例 (Few-Shot)
-## 案例一:情感叙事/限定词应用
-**输入视频内容**: "人到晚年,最想念老同学...同窗三两载,却是悠悠一生情...头发白了..."
-**输出 JSON**:
-{
-  "选题信息表": {
-    "宏观母题": "人到晚年最想念老同学且认为同学情是一世情",
-    "高潜子题列表": []
-  },
-  "模型信息表": {
-    "抽象逻辑模型列表": [
-      "[时间:晚年/余生] + [对象:老同学/老战友] + [情感:想念/聚会/重逢]"
-    ]
-  },
-  "层级标签树": {
-    "L1_实例名词": ["老同学"],
-    "L1_限定词": ["晚年", "白了头", "悠悠一生"],
-    "L2_具体品类": ["晚年社交", "夕阳红情谊"],
-    "L3_兴趣领域": ["怀旧岁月", "情感感悟"],
-    "L4_情绪价值": ["致青春", "友谊长存"]
-  }
-}
-
-注意:
-- 所有字段名和值必须使用简体中文。
-- 严格遵守"显性事实原则",不要凭空添加视频中不存在的信息。
-- 如果某个字段在视频中完全找不到依据,可以留空字符串 "" 或空数组 []。
-        """.strip()
-
-    @staticmethod
-    def _build_empty_result() -> Dict[str, Any]:
-        """构建一个结构完整但内容为空的降级结果"""
-        return {
-            "选题信息表": {
-                "宏观母题": "",
-                "高潜子题列表": [],
-            },
-            "模型信息表": {
-                "抽象逻辑模型列表": [],
-            },
-            "层级标签树": {
-                "L1_实例名词": [],
-                "L1_限定词": [],
-                "L2_具体品类": [],
-                "L3_兴趣领域": [],
-                "L4_情绪价值": [],
-            },
-        }
-
-    # ==================== BaseLLMAgent 抽象方法占位实现 ====================
-
-    def _build_messages(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
-        """满足 BaseLLMAgent 抽象要求(本 Agent 不通过该通用路径调用)"""
-        return []
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """满足 BaseLLMAgent 抽象要求(本 Agent 不通过该通用路径调用)"""
-        return state
-
-# 这个agent用于提炼视频信息,构建结构化的内容数据库

+ 0 - 581
src/components/agents/topic_selection_understanding_agent.py

@@ -1,581 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-选题理解Agent(视频分析版本)
-
-功能: 分析和理解视频的选题策略和定位
-核心任务: 基于视频内容和三点分析结果,理解选题逻辑
-特征: 整合视频逐帧分析结果和三点数据,揭示选题策略
-"""
-
-from typing import Any, Dict
-from src.components.agents.base import BaseLLMAgent
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class TopicSelectionUnderstandingAgent(BaseLLMAgent):
-    """选题理解Agent - 分析和理解视频的选题策略和定位(视频分析版本)"""
-
-    def __init__(
-        self,
-        name: str = "topic_selection_understanding_agent",
-        description: str = "选题理解Agent",
-        model_provider: str = "google_genai",
-        temperature: float = 0.7,
-        max_tokens: int = 20480
-    ):
-        """
-        初始化选题理解Agent
-
-        Args:
-            name: Agent名称
-            description: Agent描述
-            model_provider: 模型提供商 ("openai" 或 "google_genai")
-            temperature: 生成温度,控制创造性
-            max_tokens: 最大token数
-        """
-        system_prompt = self._build_system_prompt()
-        super().__init__(
-            name=name,
-            description=description,
-            model_provider=model_provider,
-            system_prompt=system_prompt,
-            temperature=temperature,
-            max_tokens=max_tokens
-        )
-
-    def _build_system_prompt(self) -> str:
-        """构建系统提示词 - 定义角色、能力、输出规范"""
-        return """你是选题策略分析专家,擅长整合多维度信息揭示视频内容的选题逻辑。
-
-# 视频分析要求
-- 需要对视频进行逐帧或关键帧分析,理解视频的整体内容和结构
-- 关注视频的画面、动作、场景、对话等各个维度
-- 基于视频的实际内容进行分析,不能凭空想象
-
-# 核心能力
-1. **三点关系理解**:
-   - 灵感点 = 创作前的触发源(什么刺激触发了创作)
-   - 目的点 = 创作时的动机(为什么要创作)
-   - 关键点 = 消费时的吸引力(什么吸引受众)
-   - 选题 = 将灵感转化为目的,通过关键点实现
-
-2. **有机整合**:分析三点之间的内在关联,而非简单罗列
-
-3. **证据驱动**:所有结论基于具体数据推导
-
-4. **覆盖验证**:确保选题理解覆盖所有灵感点、目的点、关键点,未覆盖的点需要说明原因
-
-# 输出规范
-严格按照以下JSON结构输出:
-```json
-{
-    "主题": "string - 一句话概括选题主题",
-    "描述": "string - 用具体细节描述核心逻辑(灵感来源→关键手法→呈现效果→创作目的),必须明确体现所有目的点",
-    "覆盖情况": {
-        "灵感点覆盖情况": [
-            {
-                "灵感点": "string - 灵感点名称",
-                "是否覆盖": true/false,
-                "覆盖说明": "string - 如何在选题中体现,如果未覆盖说明原因"
-            }
-        ],
-        "目的点覆盖情况": [
-            {
-                "目的点": "string - 目的点名称",
-                "是否覆盖": true/false,
-                "覆盖说明": "string - 如何在选题中实现,如果未覆盖说明原因"
-            }
-        ],
-        "关键点覆盖情况": [
-            {
-                "关键点": "string - 关键点名称",
-                "是否覆盖": true/false,
-                "覆盖说明": "string - 如何在选题中支撑,如果未覆盖说明原因"
-            }
-        ]
-    }
-}
-```
-
-**注意**:不需要输出 `explicit_elements` 字段,该字段由系统自动提取。
-
-# topic_description 核心原则
-**描述本质,精炼核心**:
-1. **拒绝笼统概念** - 要具体到本质特征,不用抽象方法论
-2. **删除非核心修饰** - 只保留最本质的词
-3. **具体但不冗余** - 描述具体现象,不堆砌修饰词
-
-# coverage_analysis 核心原则
-**确保完整覆盖**:
-1. **逐一检查** - 检查每个灵感点、目的点、关键点是否在选题中有体现
-2. **明确说明** - 如果覆盖,说明如何体现;如果未覆盖,说明为什么未体现
-3. **追求完整** - 努力让选题理解覆盖所有三点,未覆盖需要充分理由"""
-
-    def process(self, state: Dict[str, Any], config=None) -> Dict[str, Any]:
-        """处理状态:读取视频和prompt → 调用视频分析 → 写回选题理解结果
-        
-        步骤:
-        1. 从 state 中获取视频文件对象
-        2. 构建完整的 prompt(包含系统提示词和用户提示词)
-        3. 使用 safe_invoke_video_analysis 进行分析
-        4. 将选题理解结果写回 state
-        
-        Returns:
-            dict: {"topic_selection_understanding": <解析后的JSON>}
-        """
-        if not self.is_initialized:
-            self.initialize()
-        
-        logger.info("TopicSelectionUnderstandingAgent 开始执行")
-        
-        try:
-            # Step 1: 从 state 中获取视频文件对象
-            from src.utils.llm_invoker import get_video_file_from_state, LLMInvoker
-            video_file = get_video_file_from_state(state)
-            
-            if not video_file:
-                logger.warning("无法获取视频文件对象,跳过选题理解分析")
-                return {
-                    "topic_selection_understanding": {
-                        "主题": "",
-                        "描述": "",
-                        "覆盖情况": {
-                            "灵感点覆盖情况": [],
-                            "目的点覆盖情况": [],
-                            "关键点覆盖情况": []
-                        },
-                        "explicit_elements": self._extract_explicit_elements(state),
-                        "错误": "无法获取视频文件对象"
-                    }
-                }
-            
-            # Step 2: 构建完整的 prompt(包含系统提示词和用户提示词)
-            prompt = self._build_all_prompt(state)
-            
-            # Step 3: 使用 safe_invoke_video_analysis 进行分析
-            logger.info("基于视频内容进行选题理解分析...")
-            result = LLMInvoker.safe_invoke_video_analysis(
-                "选题理解分析",
-                video_file,
-                prompt,
-                agent=self,
-                fallback={
-                    "主题": "",
-                    "描述": "",
-                    "覆盖情况": {
-                        "灵感点覆盖情况": [],
-                        "目的点覆盖情况": [],
-                        "关键点覆盖情况": []
-                    }
-                }
-            )
-            
-            logger.info(f"TopicSelectionUnderstandingAgent 执行完成: {result}")
-            
-            return self._update_state(state, result)
-            
-        except Exception as e:
-            logger.error(f"TopicSelectionUnderstandingAgent 执行失败: {e}", exc_info=True)
-            return self._update_state(state, {
-                "主题": "",
-                "描述": "",
-                "覆盖情况": {
-                    "灵感点覆盖情况": [],
-                    "目的点覆盖情况": [],
-                    "关键点覆盖情况": []
-                },
-                "错误": str(e)
-            })
-        
-    def _build_all_prompt(self, state: Dict[str, Any]) -> list:  # BaseLLMAgent要求实现
-        """
-        构建LLM消息
-
-        Args:
-            state: 包含视频和其他agent结果的状态字典,需要包含:
-                - video_uploaded_uri: 上传的视频URI
-                - inspiration_points: 灵感点提取结果
-                - purpose_point: 目的点提取结果
-                - key_points: 关键点提取结果
-
-        Returns:
-            消息列表
-        """
-        # 提取输入数据(视频分析版本)
-        video_url = state.get("video", "")
-        video_uri = state.get("video_uploaded_uri", "")
-        video_url_for_prompt = video_url if video_url else video_uri
-        inspiration_points = state.get("inspiration_points", [])
-        purpose_point = state.get("purpose_point", {})
-        key_points = state.get("key_points", {})
-
-        if not video_url_for_prompt:
-            logger.warning("未找到视频URL或URI,跳过选题理解")
-            return [
-                {"role": "system", "content": self.system_prompt},
-                {"role": "user", "content": "未找到视频URL或URI"}
-            ]
-
-
-        # 构建提示词(只使用视频和其他agent的结果)
-        prompt = self._build_user_prompt(
-            video_url_for_prompt,
-            inspiration_points,
-            purpose_point,
-            key_points
-        )
-
-        # 构建消息(将视频URL直接添加到prompt中)
-        return self.system_prompt + "\n\n" + prompt
-
-    def _update_state(self, state: Dict[str, Any], response: Any) -> Dict[str, Any]:
-        """
-        更新状态
-
-        Args:
-            state: 原始状态
-            response: LLM响应或已解析的字典
-
-        Returns:
-            更新后的状态
-        """
-        # 检查 response 是否已经是解析后的字典(来自 safe_invoke_video_analysis)
-        if isinstance(response, dict):
-            # 已经是解析后的结果,直接使用
-            result = response
-        else:
-            # 是 LLM 响应对象,需要提取 content 并解析
-            if hasattr(response, 'content'):
-                content = response.content
-            elif hasattr(response, 'text'):
-                content = response.text
-            else:
-                content = str(response)
-            
-            result = self._parse_response(content)
-
-        # 确保 result 是字典
-        if not isinstance(result, dict):
-            logger.warning(f"解析结果不是字典类型: {type(result)}")
-            result = {
-                "主题": "",
-                "描述": "",
-                "覆盖情况": {
-                    "灵感点覆盖情况": [],
-                    "目的点覆盖情况": [],
-                    "关键点覆盖情况": []
-                }
-            }
-
-        # 通过代码直接映射 explicit_elements(不依赖LLM输出)
-        result["explicit_elements"] = self._extract_explicit_elements(state)
-
-        return {
-            "topic_selection_understanding": result
-        }
-
-    def _build_user_prompt(
-        self,
-        video_url: str,
-        inspiration_points: Any,
-        purpose_point: Dict[str, Any],
-        key_points: Dict[str, Any]
-    ) -> str:
-        """构建用户提示 - 提供数据和执行指令(视频分析版本)"""
-        # 格式化三点数据
-        inspiration_section = self._format_inspiration_points(inspiration_points)
-        purpose_section = self._format_purpose_point(purpose_point)
-        key_points_section = self._format_key_points(key_points)
-
-        return f"""# 输入数据
-
-## 视频内容 
-请对视频进行逐帧或关键帧分析。
-
-**重要**:你需要对视频进行逐帧或关键帧分析,从以下维度理解视频内容:
-- **画面内容**:场景、物体、人物、环境等视觉元素
-- **动作变化**:人物的动作、物体的运动、场景的转换
-- **时间线索**:视频的时间顺序、节奏变化、关键时间点
-- **视觉细节**:颜色、光影、构图、视觉焦点等
-- **音频信息**(如有):对话、音效、背景音乐等
-
-基于视频的实际内容,结合以下三点分析结果,理解选题策略。
-
-## 灵感点分析
-{inspiration_section}
-
-## 目的点分析
-{purpose_section}
-
-## 关键点分析
-{key_points_section}
-
----
-
-# 执行指令
-
-基于以上数据,输出选题策略分析。
-
-## 质量要求
-- **描述**: 必须是完整流畅的1-2句话,清晰展现完整逻辑链"灵感来源→关键手法→呈现效果→创作目的"
-  - **必须包含目的点**: 明确说明创作者的目的,不能只描述内容本身
-  - **必须体现三点内在关联**: 分析三点的有机融合逻辑,而非简单罗列
-  - **基于视频内容**: 所有分析必须基于视频的实际内容,不能凭空想象
-  - 拒绝笼统概念,要具体到本质特征
-  - 删除非核心修饰词,只保留最本质的词
-  - 禁用空洞词汇: "精心"、"独特"、"全面"、"旨在"、"充分"、"深度"、"夸张的"、"强烈的"、"明显的"
-
-**重要提醒**:
-- 不需要输出 `explicit_elements` 字段,系统会自动从三点数据中提取
-- 所有分析必须基于视频的实际内容和三点分析结果"""
-
-    def _format_inspiration_points(self, inspiration_points: Any) -> str:
-        """格式化灵感点信息 - 只保留核心信息
-
-        支持两种数据结构:
-        1. 新结构(列表):直接是列表 [...]
-        2. 旧结构:{"points": [...]}
-        """
-        # 新结构:inspiration_points 直接是列表
-        if isinstance(inspiration_points, list):
-            if not inspiration_points:
-                return "**未提取到灵感点**"
-
-            formatted = f"**灵感点总数**: {len(inspiration_points)}\n\n"
-
-            for idx, point in enumerate(inspiration_points, 1):
-                formatted += f"### 灵感点 {idx}: {point.get('灵感点', 'N/A')}\n"
-                formatted += f"**分类**: {point.get('分类', '')}\n"
-                formatted += f"**描述**: {point.get('描述', '')}\n"
-                if point.get("推理"):
-                    formatted += f"**推理**: {point.get('推理', '')}\n"
-                formatted += "\n"
-
-            return formatted
-
-        # 旧结构:{"points": [...]} 或字典格式
-        if isinstance(inspiration_points, dict):
-            # 旧结构:points列表
-            if "points" in inspiration_points:
-                points = inspiration_points.get("points", [])
-                if not points:
-                    return "**未提取到灵感点**"
-
-                formatted = f"**灵感点总数**: {len(points)}\n\n"
-
-                for idx, point in enumerate(points, 1):
-                    formatted += f"### 灵感点 {idx}: {point.get('灵感点表象', 'N/A')}\n"
-                    formatted += f"**本质**: {point.get('灵感点本质', '')}\n"
-                    formatted += f"{point.get('本质详细说明', '')}\n\n"
-
-                return formatted
-
-        return "**未提取到灵感点**"
-
-    def _format_purpose_point(self, purpose_point: Dict[str, Any]) -> str:
-        """格式化目的点信息 - 只支持新结构,确保包含所有目的点"""
-        if not purpose_point:
-            return "**未提取到目的点**"
-
-        # 新结构:{"perspective": "创作者视角", "purposes": [...], "total_count": 2}
-        purposes = purpose_point.get("purposes", [])
-        if not purposes:
-            return "**未提取到目的点**"
-
-        formatted = f"**目的点总数**: {len(purposes)}(所有目的点平等不分主次)\n\n"
-
-        for idx, purpose in enumerate(purposes, 1):
-            维度 = purpose.get("维度", {})
-            if isinstance(维度, dict):
-                维度_str = f"{维度.get('一级分类', '')}/{维度.get('二级分类', '')}"
-            else:
-                维度_str = str(维度)
-
-            目的点 = purpose.get("目的点", "N/A")
-            描述 = purpose.get("描述", "")
-
-            formatted += f"### 目的点 {idx}: {目的点}\n"
-            formatted += f"**维度**: {维度_str}\n"
-            if 描述:
-                formatted += f"**描述**: {描述}\n"
-            formatted += "\n"
-
-        return formatted
-
-    def _extract_explicit_elements(self, state: Dict[str, Any]) -> Dict[str, list]:
-        """从状态中提取显式元素(通过代码映射,不依赖LLM)
-
-        Args:
-            state: 包含三点数据的状态字典
-
-        Returns:
-            显式元素字典,包含灵感点列表、目的点、关键点列表
-        """
-        # 提取灵感点列表
-        inspiration_points_data = state.get("inspiration_points", [])
-        灵感点列表 = []
-
-        # 新结构:inspiration_points 直接是列表
-        if isinstance(inspiration_points_data, list):
-            for point in inspiration_points_data:
-                灵感点 = point.get("灵感点", "")
-                if 灵感点:
-                    灵感点列表.append(灵感点)
-        # 旧结构:字典格式
-        elif isinstance(inspiration_points_data, dict):
-            # 尝试旧的points结构
-            if "points" in inspiration_points_data:
-                points = inspiration_points_data.get("points", [])
-                for point in points:
-                    灵感点表象 = point.get("灵感点表象", "")
-                    if 灵感点表象:
-                        灵感点列表.append(灵感点表象)
-
-        # 提取目的点(支持新旧两种结构)
-        purpose_point_data = state.get("purpose_point", {})
-        目的点列表 = []
-
-        # 新结构:{"purposes": [...]}
-        if "purposes" in purpose_point_data:
-            purposes = purpose_point_data.get("purposes", [])
-            # 提取所有目的点
-            for purpose in purposes:
-                purpose_text = purpose.get("目的点", "")
-                if purpose_text:
-                    目的点列表.append(purpose_text)
-        # 旧结构(兼容):{"main_purpose": {...}}
-        elif "main_purpose" in purpose_point_data:
-            main_purpose = purpose_point_data.get("main_purpose", {})
-            purpose_text = main_purpose.get("目的点", "") if isinstance(main_purpose, dict) else ""
-            if purpose_text:
-                目的点列表.append(purpose_text)
-
-        # 提取关键点列表(支持层级结构)
-        key_points_data = state.get("key_points", {})
-        关键点列表 = []
-
-        # 递归提取所有关键点(包括子关键点)
-        def extract_points_recursive(points: list):
-            for point in points:
-                关键点 = point.get("关键点", "")
-                if 关键点:
-                    关键点列表.append(关键点)
-
-                # 递归处理子关键点
-                children = point.get("children", [])
-                if children:
-                    extract_points_recursive(children)
-
-        key_points_list = key_points_data.get("key_points", [])
-        extract_points_recursive(key_points_list)
-
-        return {
-            "灵感点列表": 灵感点列表,
-            "目的点列表": 目的点列表,
-            "关键点列表": 关键点列表
-        }
-
-    def _format_key_points(self, key_points: Dict[str, Any]) -> str:
-        """格式化关键点信息 - 支持层级结构"""
-        if not key_points or "key_points" not in key_points:
-            return "**未提取到关键点**"
-
-        points = key_points.get("key_points", [])
-        if not points:
-            return "**未提取到关键点**"
-
-        total_count = key_points.get("total_count", len(points))
-        root_count = key_points.get("root_count", len(points))
-        hierarchy_enabled = key_points.get("hierarchy_enabled", False)
-
-        formatted = f"**关键点总数**: {total_count}\n"
-        if hierarchy_enabled:
-            formatted += f"**一级关键点数**: {root_count}\n"
-            formatted += f"**层级结构**: 已启用\n\n"
-        else:
-            formatted += "\n"
-
-        # 递归格式化关键点树
-        def format_point_tree(point: Dict[str, Any], level: int = 1, index: int = 1) -> str:
-            indent = "  " * (level - 1)
-            result = f"{indent}### 关键点 {index} (层级 {level}): {point.get('关键点', 'N/A')}\n"
-            result += f"{indent}**维度**: [{point.get('维度大类', '')}] {point.get('维度细分', '')}\n"
-            result += f"{indent}**描述**: {point.get('描述', '')}\n"
-
-            # 递归处理子关键点
-            children = point.get("children", [])
-            if children:
-                result += f"{indent}**子关键点**: {len(children)}个\n\n"
-                for child_idx, child in enumerate(children, 1):
-                    result += format_point_tree(child, level + 1, child_idx)
-            else:
-                result += "\n"
-
-            return result
-
-        # 格式化所有一级关键点
-        for idx, point in enumerate(points, 1):
-            formatted += format_point_tree(point, level=1, index=idx)
-
-        return formatted
-
-    def _parse_response(self, response: str) -> Dict[str, Any]:
-        """解析LLM响应"""
-        import json
-        import re
-
-        try:
-            # 尝试提取JSON代码块
-            json_match = re.search(r'```(?:json)?\s*(.*?)\s*```', response, re.DOTALL)
-            if json_match:
-                json_str = json_match.group(1)
-            else:
-                json_str = response
-
-            # 解析JSON
-            result = json.loads(json_str)
-
-            # 确保包含必需字段(explicit_elements 由代码添加,不在这里处理)
-            required_fields = {
-                "主题": "",
-                "描述": "",
-                "覆盖情况": {
-                    "灵感点覆盖情况": [],
-                    "目的点覆盖情况": [],
-                    "关键点覆盖情况": []
-                }
-            }
-
-            # 填充缺失字段
-            for key, default_value in required_fields.items():
-                if key not in result:
-                    result[key] = default_value
-                elif key == "覆盖情况" and isinstance(result[key], dict):
-                    # 确保覆盖情况的子字段存在
-                    if "灵感点覆盖情况" not in result[key]:
-                        result[key]["灵感点覆盖情况"] = []
-                    if "目的点覆盖情况" not in result[key]:
-                        result[key]["目的点覆盖情况"] = []
-                    if "关键点覆盖情况" not in result[key]:
-                        result[key]["关键点覆盖情况"] = []
-
-            return result
-
-        except json.JSONDecodeError as e:
-            logger.error(f"JSON解析失败: {e}")
-            # JSON解析失败,返回默认结构(explicit_elements 由代码添加)
-            return {
-                "主题": response[:200] if len(response) > 200 else response,
-                "描述": "解析失败",
-                "覆盖情况": {
-                    "灵感点覆盖情况": [],
-                    "目的点覆盖情况": [],
-                    "关键点覆盖情况": []
-                }
-            }
-    def _build_messages(self, state: Dict[str, Any]) -> list[Dict[str, Any]]:
-        """构建消息"""
-        return []

+ 0 - 49
src/components/functions/__init__.py

@@ -1,49 +0,0 @@
-# 函数组件包
-
-from .base import (
-    BaseFunction,
-    SimpleFunction,
-    ConfigurableFunction,
-    PipelineFunction,
-    ConditionalFunction,
-    FunctionRegistry,
-    function_registry,
-    component_function,
-    create_simple_function,
-    create_configurable_function,
-    create_pipeline
-)
-
-from .json_utils import (
-    JSONParseError,
-    JsonSafeParseFunction,
-    json_safe_parse_function,
-    safe_json_parse,
-    batch_json_parse,
-    validate_json_structure
-)
-
-__all__ = [
-    # Base classes
-    'BaseFunction',
-    'SimpleFunction',
-    'ConfigurableFunction',
-    'PipelineFunction',
-    'ConditionalFunction',
-    'FunctionRegistry',
-    'function_registry',
-    
-    # Decorators and creators
-    'component_function',
-    'create_simple_function',
-    'create_configurable_function',
-    'create_pipeline',
-    
-    # JSON parsing functions
-    'JSONParseError',
-    'JsonSafeParseFunction',
-    'json_safe_parse_function',
-    'safe_json_parse',
-    'batch_json_parse',
-    'validate_json_structure'
-]

+ 0 - 394
src/components/functions/base.py

@@ -1,394 +0,0 @@
-"""
-组件抽象层:Function 基础抽象类
-
-定义纯函数组件的抽象基类,用于处理数据转换、计算等无副作用的操作
-"""
-
-from abc import ABC, abstractmethod
-from typing import Any, Dict, Optional, TypeVar, Generic, Callable, List
-import inspect
-import functools
-
-# 输入输出类型变量
-InputType = TypeVar('InputType')
-OutputType = TypeVar('OutputType')
-
-
-class BaseFunction(ABC, Generic[InputType, OutputType]):
-    """函数组件基类
-    
-    定义纯函数组件的接口,用于处理数据转换、计算等操作
-    """
-    
-    def __init__(self, name: str, description: str = ""):
-        """初始化函数组件
-        
-        Args:
-            name: 函数名称
-            description: 函数描述
-        """
-        self.name = name
-        self.description = description
-        self._initialized = False
-    
-    @abstractmethod
-    def execute(self, input_data: InputType, context: Optional[Dict[str, Any]] = None) -> OutputType:
-        """执行函数逻辑
-        
-        Args:
-            input_data: 输入数据
-            context: 上下文信息
-            
-        Returns:
-            处理后的输出数据
-        """
-        pass
-    
-    def initialize(self) -> None:
-        """初始化函数组件"""
-        if not self._initialized:
-            self._setup()
-            self._initialized = True
-    
-    def _setup(self) -> None:
-        """设置函数组件,子类可以重写"""
-        pass
-    
-    @property
-    def is_initialized(self) -> bool:
-        """检查是否已初始化"""
-        return self._initialized
-    
-    def __call__(self, input_data: InputType, context: Optional[Dict[str, Any]] = None) -> OutputType:
-        """使函数组件可调用"""
-        if not self._initialized:
-            self.initialize()
-        return self.execute(input_data, context)
-    
-    def __str__(self) -> str:
-        return f"{self.__class__.__name__}(name='{self.name}')"
-    
-    def __repr__(self) -> str:
-        return self.__str__()
-
-
-class SimpleFunction(BaseFunction[InputType, OutputType]):
-    """简单函数组件
-    
-    基于普通函数的简单包装
-    """
-    
-    def __init__(
-        self,
-        name: str,
-        description: str,
-        func: Callable[[InputType, Optional[Dict[str, Any]]], OutputType]
-    ):
-        """初始化简单函数组件
-        
-        Args:
-            name: 函数名称
-            description: 函数描述
-            func: 执行函数
-        """
-        super().__init__(name, description)
-        self.func = func
-    
-    def execute(self, input_data: InputType, context: Optional[Dict[str, Any]] = None) -> OutputType:
-        """执行函数"""
-        return self.func(input_data, context)
-
-
-class ConfigurableFunction(BaseFunction[InputType, OutputType]):
-    """可配置函数组件
-    
-    支持配置参数的函数组件
-    """
-    
-    def __init__(
-        self,
-        name: str,
-        description: str,
-        func: Callable,
-        config: Optional[Dict[str, Any]] = None
-    ):
-        """初始化可配置函数组件
-        
-        Args:
-            name: 函数名称
-            description: 函数描述
-            func: 执行函数
-            config: 配置参数
-        """
-        super().__init__(name, description)
-        self.func = func
-        self.config = config or {}
-    
-    def execute(self, input_data: InputType, context: Optional[Dict[str, Any]] = None) -> OutputType:
-        """执行函数"""
-        # 合并上下文和配置
-        merged_context = {**(context or {}), **self.config}
-        return self.func(input_data, merged_context)
-    
-    def update_config(self, new_config: Dict[str, Any]) -> None:
-        """更新配置"""
-        self.config.update(new_config)
-
-
-class PipelineFunction(BaseFunction[InputType, OutputType]):
-    """管道函数组件
-    
-    将多个函数组件串联成管道
-    """
-    
-    def __init__(
-        self,
-        name: str,
-        description: str,
-        functions: List[BaseFunction]
-    ):
-        """初始化管道函数组件
-        
-        Args:
-            name: 函数名称
-            description: 函数描述
-            functions: 函数组件列表
-        """
-        super().__init__(name, description)
-        self.functions = functions
-    
-    def execute(self, input_data: InputType, context: Optional[Dict[str, Any]] = None) -> OutputType:
-        """执行管道"""
-        result = input_data
-        for func in self.functions:
-            result = func(result, context)
-        return result
-    
-    def add_function(self, func: BaseFunction) -> None:
-        """添加函数到管道"""
-        self.functions.append(func)
-    
-    def initialize(self) -> None:
-        """初始化管道中的所有函数"""
-        super().initialize()
-        for func in self.functions:
-            if not func.is_initialized:
-                func.initialize()
-
-
-class ConditionalFunction(BaseFunction[InputType, OutputType]):
-    """条件函数组件
-    
-    根据条件选择不同的函数执行
-    """
-    
-    def __init__(
-        self,
-        name: str,
-        description: str,
-        condition_func: Callable[[InputType, Optional[Dict[str, Any]]], bool],
-        true_func: BaseFunction,
-        false_func: BaseFunction
-    ):
-        """初始化条件函数组件
-        
-        Args:
-            name: 函数名称
-            description: 函数描述
-            condition_func: 条件判断函数
-            true_func: 条件为真时执行的函数
-            false_func: 条件为假时执行的函数
-        """
-        super().__init__(name, description)
-        self.condition_func = condition_func
-        self.true_func = true_func
-        self.false_func = false_func
-    
-    def execute(self, input_data: InputType, context: Optional[Dict[str, Any]] = None) -> OutputType:
-        """执行条件函数"""
-        if self.condition_func(input_data, context):
-            return self.true_func(input_data, context)
-        else:
-            return self.false_func(input_data, context)
-    
-    def initialize(self) -> None:
-        """初始化条件函数中的所有函数"""
-        super().initialize()
-        if not self.true_func.is_initialized:
-            self.true_func.initialize()
-        if not self.false_func.is_initialized:
-            self.false_func.initialize()
-
-
-class FunctionRegistry:
-    """函数注册表
-    
-    管理和组织函数组件
-    """
-    
-    def __init__(self):
-        self._functions: Dict[str, BaseFunction] = {}
-        self._categories: Dict[str, List[str]] = {}
-    
-    def register(self, function: BaseFunction, category: Optional[str] = None) -> None:
-        """注册函数
-        
-        Args:
-            function: 函数实例
-            category: 函数分类
-        """
-        if function.name in self._functions:
-            raise ValueError(f"Function '{function.name}' already registered")
-        
-        self._functions[function.name] = function
-        
-        if category:
-            if category not in self._categories:
-                self._categories[category] = []
-            self._categories[category].append(function.name)
-    
-    def get_function(self, name: str) -> Optional[BaseFunction]:
-        """获取函数
-        
-        Args:
-            name: 函数名称
-            
-        Returns:
-            函数实例或 None
-        """
-        return self._functions.get(name)
-    
-    def get_functions_by_category(self, category: str) -> List[BaseFunction]:
-        """根据分类获取函数
-        
-        Args:
-            category: 分类名称
-            
-        Returns:
-            函数列表
-        """
-        function_names = self._categories.get(category, [])
-        return [self._functions[name] for name in function_names if name in self._functions]
-    
-    def list_functions(self) -> Dict[str, str]:
-        """列出所有函数
-        
-        Returns:
-            函数名称到描述的映射
-        """
-        return {name: func.description for name, func in self._functions.items()}
-    
-    def execute_function(self, name: str, input_data: Any, context: Optional[Dict[str, Any]] = None) -> Any:
-        """执行函数
-        
-        Args:
-            name: 函数名称
-            input_data: 输入数据
-            context: 上下文
-            
-        Returns:
-            函数执行结果
-        """
-        function = self._functions.get(name)
-        if not function:
-            raise ValueError(f"Function '{name}' not found")
-        
-        return function(input_data, context)
-
-
-# 全局函数注册表实例
-function_registry = FunctionRegistry()
-
-
-# 便捷装饰器
-def component_function(
-    name: Optional[str] = None,
-    description: Optional[str] = None,
-    category: Optional[str] = None,
-    auto_register: bool = True
-):
-    """组件函数装饰器
-    
-    将普通函数转换为函数组件并可选择自动注册
-    
-    Args:
-        name: 函数名称,默认使用函数名
-        description: 函数描述,默认使用函数文档字符串
-        category: 函数分类
-        auto_register: 是否自动注册到全局注册表
-    """
-    def decorator(func):
-        func_name = name or func.__name__
-        func_description = description or (func.__doc__ or "").strip()
-        
-        # 创建函数组件
-        function_component = SimpleFunction(
-            name=func_name,
-            description=func_description,
-            func=func
-        )
-        
-        # 自动注册
-        if auto_register:
-            function_registry.register(function_component, category)
-        
-        # 返回包装后的函数,保持原有接口
-        @functools.wraps(func)
-        def wrapper(*args, **kwargs):
-            return func(*args, **kwargs)
-        
-        # 将函数组件添加为函数属性
-        wrapper._function_component = function_component
-        
-        return wrapper
-    
-    return decorator
-
-
-# 便捷函数创建器
-def create_simple_function(name: str, description: str, func: Callable) -> SimpleFunction:
-    """创建简单函数组件
-    
-    Args:
-        name: 函数名称
-        description: 函数描述
-        func: 执行函数
-        
-    Returns:
-        简单函数组件实例
-    """
-    return SimpleFunction(name, description, func)
-
-
-def create_configurable_function(
-    name: str, 
-    description: str, 
-    func: Callable, 
-    config: Dict[str, Any]
-) -> ConfigurableFunction:
-    """创建可配置函数组件
-    
-    Args:
-        name: 函数名称
-        description: 函数描述
-        func: 执行函数
-        config: 配置参数
-        
-    Returns:
-        可配置函数组件实例
-    """
-    return ConfigurableFunction(name, description, func, config)
-
-
-def create_pipeline(name: str, description: str, functions: List[BaseFunction]) -> PipelineFunction:
-    """创建管道函数组件
-    
-    Args:
-        name: 函数名称
-        description: 函数描述
-        functions: 函数组件列表
-        
-    Returns:
-        管道函数组件实例
-    """
-    return PipelineFunction(name, description, functions)

+ 0 - 238
src/components/functions/json_utils.py

@@ -1,238 +0,0 @@
-"""
-JSON 安全解析功能组件
-
-提供安全的 JSON 解析功能,专门处理大模型返回的包装 JSON 格式
-"""
-
-import json
-import re
-from typing import Any, Dict, Optional, Union, List
-from .base import SimpleFunction, component_function
-
-
-class JSONParseError(Exception):
-    """JSON 解析异常"""
-    pass
-
-
-class JsonSafeParseFunction(SimpleFunction[str, Any]):
-    """JSON 安全解析函数组件
-    
-    专门处理大模型返回的包装 JSON,支持:
-    1. 去除 ```json ``` 包装格式
-    2. 安全的 JSON 解析
-    3. 严格的异常处理
-    """
-    
-    def __init__(self):
-        """初始化 JSON 安全解析函数"""
-        super().__init__(
-            name="json_safe_parse",
-            description="安全解析 JSON 字符串,支持处理大模型返回的包装格式",
-            func=self._parse_json
-        )
-    
-    def _parse_json(self, input_data: str, context: Optional[Dict[str, Any]] = None) -> Any:
-        """执行 JSON 解析
-        
-        Args:
-            input_data: 待解析的 JSON 字符串
-            context: 上下文信息,可包含解析选项
-            
-        Returns:
-            解析后的 Python 对象
-            
-        Raises:
-            JSONParseError: 当 JSON 解析失败时抛出
-        """
-        if not isinstance(input_data, str):
-            raise JSONParseError(f"Expected string input, got {type(input_data)}")
-        
-        # 获取上下文选项
-        options = context.get('options', {}) if context else {}
-        strict_mode = options.get('strict', True)  # 默认严格模式
-        
-        try:
-            # 1. 清理输入字符串
-            cleaned_json = self._extract_json_from_wrapped_string(input_data.strip())
-            
-            # 2. 尝试解析 JSON
-            if not cleaned_json:
-                raise JSONParseError("Empty or invalid JSON content after extraction")
-            
-            parsed_data = json.loads(cleaned_json, strict=strict_mode)
-            return parsed_data
-            
-        except json.JSONDecodeError as e:
-            raise JSONParseError(f"JSON decode error: {str(e)}")
-        except Exception as e:
-            raise JSONParseError(f"Unexpected error during JSON parsing: {str(e)}")
-    
-    def _extract_json_from_wrapped_string(self, text: str) -> str:
-        """从包装的字符串中提取 JSON 内容
-        
-        支持的格式:
-        - ```json { ... } ```
-        - ```{ ... }```
-        - { ... }
-        - [ ... ]
-        
-        Args:
-            text: 包装的字符串
-            
-        Returns:
-            提取的 JSON 字符串
-        """
-        # 去除首尾空白
-        text = text.strip()
-        
-        # 模式1: ```json ... ``` 或 ```... ```
-        json_block_pattern = r'```(?:json)?\s*\n?(.*?)\n?```'
-        match = re.search(json_block_pattern, text, re.DOTALL | re.IGNORECASE)
-        if match:
-            return match.group(1).strip()
-        
-        # 模式2: 直接的 JSON 对象或数组
-        # 寻找完整的 JSON 结构
-        json_patterns = [
-            r'(\{.*\})',  # JSON 对象
-            r'(\[.*\])',  # JSON 数组
-        ]
-        
-        for pattern in json_patterns:
-            match = re.search(pattern, text, re.DOTALL)
-            if match:
-                potential_json = match.group(1).strip()
-                # 简单验证是否是有效的 JSON 开始
-                if (potential_json.startswith('{') and potential_json.endswith('}')) or \
-                   (potential_json.startswith('[') and potential_json.endswith(']')):
-                    return potential_json
-        
-        # 如果没有找到明确的 JSON 结构,返回原文本
-        return text
-
-
-# 创建全局实例
-json_safe_parse_function = JsonSafeParseFunction()
-
-
-@component_function(
-    name="json_safe_parse",
-    description="安全解析 JSON 字符串,支持处理大模型返回的包装格式",
-    category="json_utils",
-    auto_register=True
-)
-def safe_json_parse(input_data: str, context: Optional[Dict[str, Any]] = None) -> Any:
-    """安全解析 JSON 字符串的便捷函数
-    
-    Args:
-        input_data: 待解析的 JSON 字符串
-        context: 上下文信息
-        
-    Returns:
-        解析后的 Python 对象
-        
-    Raises:
-        JSONParseError: 当 JSON 解析失败时抛出
-    """
-    return json_safe_parse_function(input_data, context)
-
-
-@component_function(
-    name="batch_json_parse",
-    description="批量解析 JSON 字符串列表",
-    category="json_utils",
-    auto_register=True
-)
-def batch_json_parse(input_data: List[str], context: Optional[Dict[str, Any]] = None) -> List[Any]:
-    """批量解析 JSON 字符串
-    
-    Args:
-        input_data: 待解析的 JSON 字符串列表
-        context: 上下文信息
-        
-    Returns:
-        解析后的 Python 对象列表
-        
-    Raises:
-        JSONParseError: 当任何 JSON 解析失败时抛出
-    """
-    if not isinstance(input_data, list):
-        raise JSONParseError(f"Expected list input, got {type(input_data)}")
-    
-    results = []
-    for i, json_str in enumerate(input_data):
-        try:
-            result = safe_json_parse(json_str, context)
-            results.append(result)
-        except JSONParseError as e:
-            raise JSONParseError(f"Failed to parse JSON at index {i}: {str(e)}")
-    
-    return results
-
-
-@component_function(
-    name="validate_json_structure",
-    description="验证 JSON 结构是否符合预期",
-    category="json_utils",
-    auto_register=True
-)
-def validate_json_structure(
-    input_data: Union[str, Dict, List], 
-    context: Optional[Dict[str, Any]] = None
-) -> Dict[str, Any]:
-    """验证 JSON 结构
-    
-    Args:
-        input_data: JSON 字符串或已解析的对象
-        context: 上下文信息,可包含验证规则
-        
-    Returns:
-        验证结果字典,包含 'valid', 'data', 'errors' 字段
-    """
-    result = {
-        'valid': False,
-        'data': None,
-        'errors': []
-    }
-    
-    try:
-        # 如果是字符串,先解析
-        if isinstance(input_data, str):
-            data = safe_json_parse(input_data, context)
-        else:
-            data = input_data
-        
-        result['data'] = data
-        
-        # 获取验证规则
-        validation_rules = context.get('validation_rules', {}) if context else {}
-        
-        # 基本验证
-        if not isinstance(data, (dict, list)):
-            result['errors'].append("Parsed data is not a JSON object or array")
-            return result
-        
-        # 自定义验证规则
-        if 'required_keys' in validation_rules and isinstance(data, dict):
-            required_keys = validation_rules['required_keys']
-            missing_keys = [key for key in required_keys if key not in data]
-            if missing_keys:
-                result['errors'].append(f"Missing required keys: {missing_keys}")
-        
-        if 'expected_type' in validation_rules:
-            expected_type = validation_rules['expected_type']
-            if expected_type == 'object' and not isinstance(data, dict):
-                result['errors'].append("Expected JSON object")
-            elif expected_type == 'array' and not isinstance(data, list):
-                result['errors'].append("Expected JSON array")
-        
-        # 如果没有错误,标记为有效
-        result['valid'] = len(result['errors']) == 0
-        
-    except JSONParseError as e:
-        result['errors'].append(str(e))
-    except Exception as e:
-        result['errors'].append(f"Validation error: {str(e)}")
-    
-    return result

+ 0 - 323
src/components/functions/result_aggregation_function.py

@@ -1,323 +0,0 @@
-"""
-Result Aggregation Function(解码工作流版本)
-
-结果汇总函数:将所有解构结果汇总为最终的JSON结构,包括三点解构(灵感点、目的点、关键点)、选题理解、脚本理解(实质、形式)等所有数据。
-"""
-
-from typing import Dict, Any, List, Optional
-
-from src.components.functions.base import BaseFunction
-from src.states.what_deconstruction_state import WhatDeconstructionState, ElementNode, FinalOutput
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class ResultAggregationFunction(BaseFunction[WhatDeconstructionState, Dict[str, Any]]):
-    """结果汇总函数(解码工作流版本)
-
-    功能:
-    - 将各Agent的解构结果组装为JSON结构
-    - 汇总三点解构结果(灵感点、目的点、关键点)
-    - 汇总选题理解结果
-    - 汇总脚本理解结果(实质、形式、段落划分等)
-    - 验证JSON格式
-    """
-
-    def __init__(
-        self,
-        name: str = "result_aggregation_function",
-        description: str = "汇总所有解构结果为最终的JSON结构(包括三点解构、选题理解、脚本理解等所有数据)"
-    ):
-        super().__init__(name, description)
-
-    def execute(
-        self,
-        input_data: WhatDeconstructionState,
-        context: Optional[Dict[str, Any]] = None
-    ) -> Dict[str, Any]:
-        """执行结果汇总
-
-        Args:
-            input_data: 工作流状态,包含所有解构结果
-            context: 上下文信息
-
-        Returns:
-            最终的树状JSON结构
-        """
-        try:
-            # 提取视频相关结果
-            video_url = input_data.get("video", "")
-            
-            # 从 state 中获取标题和正文(优先从直接字段获取,兼容text字典)
-            title = input_data.get("title", "")
-            body_text = input_data.get("body_text", "")
-            if not title or not body_text:
-                text_data = input_data.get("text", {})
-                title = title or text_data.get("title", "")
-                body_text = body_text or text_data.get("body", "")
-
-            # 提取三点解构结果
-            # 灵感点:可能是数组或包含数组的对象
-            inspiration_points_raw = input_data.get("inspiration_points", {})
-            if isinstance(inspiration_points_raw, list):
-                inspiration_points = inspiration_points_raw
-            elif isinstance(inspiration_points_raw, dict):
-                # 可能是包含 points 或直接是数组的字典
-                inspiration_points = inspiration_points_raw.get("points", inspiration_points_raw.get("inspiration_points", []))
-            else:
-                inspiration_points = []
-            
-            # 目的点:保持原始结构(包含 perspective, purposes, total_count)
-            purpose_point = input_data.get("purpose_point", {})
-            
-            # 关键点:保持原始结构(包含 key_points, total_count, root_count 等)
-            key_points = input_data.get("key_points", {})
-
-            # 提取选题理解结果 - 保持完整结构(包含主题、描述、覆盖情况等所有字段)
-            topic_selection_understanding = input_data.get("topic_selection_understanding", {})
-
-            # 提取脚本理解结果
-            # 段落划分相关
-            content_category = input_data.get("内容品类", "未知")
-            section_list = input_data.get("段落列表", [])
-            
-            # 实质和形式列表
-            substance_list = input_data.get("实质列表", [])
-            form_list = input_data.get("形式列表", [])
-            
-            # 如果没有实质列表,尝试从 substance_final_elements 获取
-            if not substance_list:
-                substance_list = input_data.get("substance_final_elements", [])
-            
-            # 如果没有形式列表,尝试从 form_final_elements 获取
-            if not form_list:
-                form_list = input_data.get("form_final_elements", [])
-
-            # 构建视频基本信息
-            video_info = {
-                "视频URL": video_url,
-                "标题": title,
-                "正文": body_text
-            }
-
-            # 组装最终JSON(包含所有数据,参考 output_demo_script.json 格式)
-            final_result = {
-                "视频信息": video_info,
-                "三点解构": {
-                    "灵感点": inspiration_points,  # 数组格式
-                    "目的点": purpose_point,  # 对象,包含 perspective, purposes, total_count
-                    "关键点": key_points  # 对象,包含 key_points, total_count, root_count
-                },
-                "选题理解": topic_selection_understanding,  # 保持完整结构,包含 主题、描述、覆盖情况等
-                "脚本理解": {
-                    "内容品类": content_category,
-                    "段落列表": section_list,
-                    "实质列表": substance_list,  # 数组格式
-                    "形式列表": form_list  # 数组格式
-                }
-            }
-
-            # 验证JSON格式
-            self._validate_result(final_result)
-
-            substance_count = len(substance_list) if isinstance(substance_list, list) else 0
-            form_count = len(form_list) if isinstance(form_list, list) else 0
-            logger.info(
-                f"结果汇总完成:选题 + 三点解构 + 脚本理解(实质:{substance_count}个, 形式:{form_count}个)"
-            )
-
-            return final_result
-
-        except Exception as e:
-            logger.error(f"结果汇总失败: {e}", exc_info=True)
-            # 返回错误结果(保持格式一致性)
-            return {
-                "视频信息": {},
-                "三点解构": {
-                    "灵感点": [],
-                    "目的点": {},
-                    "关键点": {}
-                },
-                "选题理解": {},
-                "脚本理解": {
-                    "内容品类": "未知",
-                    "段落列表": [],
-                    "实质列表": [],
-                    "形式列表": []
-                },
-                "错误": f"汇总失败: {str(e)}"
-            }
-
-    def _build_element_tree(
-        self,
-        id: str,
-        result: Dict[str, Any],
-        element_type: str
-    ) -> ElementNode:
-        """递归构建元素树(视频分析版本中不再使用,保留以保持代码完整性)
-
-        Args:
-            id: 节点ID
-            result: 解构结果
-            element_type: 元素类型(image/text)
-
-        Returns:
-            元素节点
-        """
-        element: ElementNode = {
-            "id": id,
-            "what": result.get("what", ""),
-            "position": result.get("position", ""),  # 添加position字段
-            "split_dimension": result.get("split_dimension", ""),  # 添加split_dimension字段
-            "描述": result.get("description", result.get("描述", {})),
-            "子节点元素关系": result.get("relationships", result.get("子节点元素关系", [])),
-            "元素重要性权重": result.get("importance_weight", result.get("元素重要性权重", 0.5)),
-            "权重理由": result.get("weight_reason", result.get("权重理由", "")),
-            "子节点元素": []
-        }
-
-        # 添加图片链接(仅图片元素)
-        if element_type == "image" and "image_path" in result:
-            element["图片链接"] = result["image_path"]
-        elif "图片链接" in result:
-            element["图片链接"] = result["图片链接"]
-
-        # 递归处理子节点
-        children = result.get("children", result.get("子节点元素", []))
-        if children:
-            for idx, child in enumerate(children):
-                child_element = self._build_element_tree(
-                    id=f"{id}_{idx + 1}",
-                    result=child,
-                    element_type=element_type
-                )
-                element["子节点元素"].append(child_element)
-
-        return element
-
-    def _validate_result(self, result: Dict[str, Any]) -> None:
-        """验证JSON格式(解码工作流版本)
-
-        Args:
-            result: 最终结果
-
-        Raises:
-            ValueError: 格式不正确时抛出异常
-        """
-        # 检查必需字段
-        if "视频信息" not in result:
-            logger.warning("缺少'视频信息'字段")
-            result["视频信息"] = {}
-
-        if "三点解构" not in result:
-            logger.warning("缺少'三点解构'字段")
-            result["三点解构"] = {
-                "灵感点": [],
-                "目的点": {},
-                "关键点": {}
-            }
-
-        if "选题理解" not in result:
-            logger.warning("缺少'选题理解'字段")
-            result["选题理解"] = {}
-
-        if "脚本理解" not in result:
-            logger.warning("缺少'脚本理解'字段")
-            result["脚本理解"] = {
-                "内容品类": "未知",
-                "段落列表": [],
-                "实质列表": [],
-                "形式列表": []
-            }
-
-        # 验证三点解构结构
-        three_points = result.get("三点解构", {})
-        if not isinstance(three_points, dict):
-            logger.warning("'三点解构'必须是字典")
-            result["三点解构"] = {
-                "灵感点": [],
-                "目的点": {},
-                "关键点": {}
-            }
-        else:
-            # 确保三点字段存在,灵感点是数组
-            if "灵感点" not in three_points:
-                logger.warning("三点解构缺少'灵感点'字段")
-                three_points["灵感点"] = []
-            elif not isinstance(three_points["灵感点"], list):
-                logger.warning("'灵感点'必须是列表")
-                three_points["灵感点"] = []
-            
-            if "目的点" not in three_points:
-                logger.warning("三点解构缺少'目的点'字段")
-                three_points["目的点"] = {}
-            elif not isinstance(three_points["目的点"], dict):
-                logger.warning("'目的点'必须是字典")
-                three_points["目的点"] = {}
-            
-            if "关键点" not in three_points:
-                logger.warning("三点解构缺少'关键点'字段")
-                three_points["关键点"] = {}
-            elif not isinstance(three_points["关键点"], dict):
-                logger.warning("'关键点'必须是字典")
-                three_points["关键点"] = {}
-
-        # 验证选题理解(保持完整结构,不做拆解)
-        if "选题理解" not in result:
-            result["选题理解"] = {}
-        elif not isinstance(result["选题理解"], dict):
-            logger.warning("'选题理解'必须是字典")
-            result["选题理解"] = {}
-
-        # 验证脚本理解结构
-        script_understanding = result.get("脚本理解", {})
-        if not isinstance(script_understanding, dict):
-            logger.warning("'脚本理解'必须是字典")
-            result["脚本理解"] = {
-                "内容品类": "未知",
-                "段落列表": [],
-                "实质列表": [],
-                "形式列表": []
-            }
-        else:
-            # 确保脚本理解字段存在
-            if "内容品类" not in script_understanding:
-                script_understanding["内容品类"] = "未知"
-            if "段落列表" not in script_understanding:
-                script_understanding["段落列表"] = []
-            if "实质列表" not in script_understanding:
-                script_understanding["实质列表"] = []
-            if "形式列表" not in script_understanding:
-                script_understanding["形式列表"] = []
-            
-            # 确保列表类型正确
-            for field in ["段落列表", "实质列表", "形式列表"]:
-                if not isinstance(script_understanding.get(field), list):
-                    logger.warning(f"脚本理解中的'{field}'必须是列表")
-                    script_understanding[field] = []
-
-    def _validate_element(self, element: Dict[str, Any]) -> None:
-        """验证元素节点格式(视频分析版本中不再使用,保留以保持代码完整性)
-
-        Args:
-            element: 元素节点
-
-        Raises:
-            ValueError: 格式不正确时抛出异常
-        """
-        required_fields = ["id", "what", "描述", "子节点元素关系", "元素重要性权重", "权重理由", "子节点元素"]
-
-        for field in required_fields:
-            if field not in element:
-                raise ValueError(f"元素节点缺少'{field}'字段")
-
-        # 验证权重范围
-        weight = element["元素重要性权重"]
-        if not isinstance(weight, (int, float)) or weight < 0 or weight > 1:
-            logger.warning(f"元素权重不在[0, 1]范围内: {weight},已修正")
-            element["元素重要性权重"] = max(0, min(1, weight))
-
-        # 递归验证子节点
-        for child in element["子节点元素"]:
-            self._validate_element(child)

+ 0 - 299
src/components/functions/video_upload_function.py

@@ -1,299 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-视频上传Function
-
-功能: 下载视频到本地并上传至Gemini,保存上传链接到state
-"""
-
-import os
-import time
-import tempfile
-import requests
-from pathlib import Path
-from urllib.parse import urlparse
-from typing import Dict, Any, Optional, Tuple
-
-from google import genai
-
-from src.components.functions.base import BaseFunction
-from src.states.what_deconstruction_state import WhatDeconstructionState
-from src.utils.logger import get_logger
-from src.utils.llm_invoker import LLMInvoker
-
-logger = get_logger(__name__)
-
-
-class VideoUploadFunction(BaseFunction[Dict[str, Any], Dict[str, Any]]):
-    """视频上传函数
-    
-    功能:
-    - 从URL下载视频到本地
-    - 上传视频到Gemini File API
-    - 保存上传后的文件URI到state
-    """
-
-    def __init__(
-        self,
-        name: str = "video_upload_function",
-        description: str = "下载视频并上传至Gemini,保存上传链接"
-    ):
-        super().__init__(name, description)
-
-    def execute(
-        self,
-        input_data: Dict[str, Any],
-        context: Optional[Dict[str, Any]] = None
-    ) -> Dict[str, Any]:
-        """执行视频上传
-        
-        Args:
-            input_data: 包含video字段的状态字典
-            context: 上下文信息
-            
-        Returns:
-            更新后的状态字典,包含video_uploaded_uri字段
-        """
-        try:
-            video_url = input_data.get("video", "")
-            
-            if not video_url:
-                logger.warning("未提供视频URL,跳过上传")
-                return {
-                    **input_data,
-                    "video_file": None,
-                    "video_uploaded_uri": None,
-                    "video_upload_error": "未提供视频URL"
-                }
-
-            logger.info(f"开始下载视频: {video_url}")
-
-            # 1. 下载视频到本地(或使用examples/videos目录下的现有文件)
-            # 从input_data中获取video_id,用于查找examples/videos目录下的文件
-            video_id = input_data.get("video_id", "")
-            local_video_path, is_temp_file = self._download_video(video_url, video_id)
-            
-            if not local_video_path:
-                return {
-                    **input_data,
-                    "video_file": None,
-                    "video_uploaded_uri": None,
-                    "video_upload_error": "视频下载失败"
-                }
-
-            logger.info(f"视频文件路径: {local_video_path}")
-
-            # 2. 上传视频到Gemini(使用新的API,带state校验)
-            video_file = LLMInvoker.upload_video_to_gemini(local_video_path)
-            
-            # # 3. 清理临时文件(只有临时文件才需要清理)
-            # if is_temp_file:
-            #     try:
-            #         os.remove(local_video_path)
-            #         logger.info(f"临时文件已删除: {local_video_path}")
-            #     except Exception as e:
-            #         logger.warning(f"删除临时文件失败: {e}")
-            # else:
-            #     logger.info(f"使用examples目录下的文件,不删除: {local_video_path}")
-
-            if not video_file:
-                return {
-                    **input_data,
-                    "video_file": None,
-                    "video_uploaded_uri": None,
-                    "video_file_name": None,
-                    "video_upload_error": "视频上传到Gemini失败"
-                }
-
-            # 获取文件URI和名称
-            file_uri = None
-            file_name = None
-            if hasattr(video_file, 'uri'):
-                file_uri = video_file.uri
-            elif hasattr(video_file, 'name'):
-                file_name = video_file.name
-                file_uri = f"https://generativelanguage.googleapis.com/v1beta/files/{file_name}"
-            
-            logger.info(f"视频上传成功,文件名称: {file_name}")
-
-            # 4. 更新state
-            return {
-                **input_data,
-                "video_file": video_file,  # 直接保存文件对象,避免后续通过API重新获取
-                "video_uploaded_uri": file_uri,  # 兼容旧版本
-                "video_file_name": file_name,  # 新字段,用于获取文件对象
-                "video_upload_error": None
-            }
-
-        except Exception as e:
-            logger.error(f"视频上传失败: {e}", exc_info=True)
-            return {
-                **input_data,
-                "video_file": None,
-                "video_uploaded_uri": None,
-                "video_file_name": None,
-                "video_upload_error": str(e)
-            }
-
-    def _download_video(self, video_url: str, video_id: str = "") -> Tuple[Optional[str], bool]:
-        """下载视频到本地,或使用examples/videos目录下的现有文件
-        
-        Args:
-            video_url: 视频URL
-            video_id: 视频ID,用于查找examples/videos目录下的文件
-            
-        Returns:
-            (本地文件路径, 是否为临时文件) 的元组,失败返回 (None, True)
-            如果使用examples/videos目录下的文件,返回 (文件路径, False)
-            如果下载到examples/videos目录,返回 (文件路径, False)
-        """
-        try:
-            # 1. 首先检查examples/videos目录下是否有对应的mp4文件
-            existing_file = self._check_examples_directory(video_id)
-            if existing_file:
-                logger.info(f"在examples/videos目录下找到现有文件,直接使用: {existing_file}")
-                return existing_file, False
-            
-            # 2. 如果没有找到,则下载到examples/videos目录
-            if not video_id:
-                logger.warning("未提供video_id,无法保存到examples/videos目录")
-                return None, True
-            
-            logger.info("未在examples/videos目录下找到同名文件,开始下载...")
-            
-            # 获取项目根目录
-            project_root = Path(__file__).parent.parent.parent.parent
-            videos_dir = project_root / "examples" / "videos"
-            
-            # 确保目录存在
-            videos_dir.mkdir(parents=True, exist_ok=True)
-            
-            # 构建文件路径:examples/videos/{video_id}.mp4
-            target_path = videos_dir / f"{video_id}.mp4"
-            
-            # 如果文件已存在(并发情况),直接返回
-            if target_path.exists():
-                logger.info(f"文件已存在: {target_path}")
-                return str(target_path), False
-
-            # 下载视频(带重试机制)
-            max_retries = 3
-            retry_count = 0
-            last_exception = None
-            
-            while retry_count < max_retries:
-                try:
-                    if retry_count > 0:
-                        logger.info(f"重试下载视频 (第 {retry_count}/{max_retries-1} 次)...")
-                    
-                    # 使用 Session 进行下载
-                    session = requests.Session()
-                    session.headers.update({
-                        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
-                    })
-                    
-                    # 下载视频(增加超时时间)
-                    response = session.get(
-                        video_url,
-                        timeout=120,  # (连接超时, 读取超时)
-                        stream=True
-                    )
-                    response.raise_for_status()
-
-                    # 写入文件
-                    with open(target_path, "wb") as f:
-                        for chunk in response.iter_content(chunk_size=8192):
-                            if chunk:
-                                f.write(chunk)
-                    
-                    # 验证文件大小
-                    file_size = os.path.getsize(target_path)
-                    if file_size == 0:
-                        raise ValueError("下载的文件大小为0")
-                    
-                    logger.info(f"视频下载完成,大小: {file_size / 1024 / 1024:.2f} MB,保存到: {target_path}")
-                    return str(target_path), False
-                    
-                except (requests.exceptions.ChunkedEncodingError, 
-                        requests.exceptions.ConnectionError,
-                        requests.exceptions.Timeout,
-                        requests.exceptions.RequestException) as e:
-                    last_exception = e
-                    retry_count += 1
-                    
-                    # 清理不完整的文件
-                    if target_path.exists():
-                        try:
-                            os.remove(target_path)
-                        except:
-                            pass
-                    
-                    if retry_count < max_retries:
-                        wait_time = retry_count * 2  # 递增等待时间:2秒、4秒
-                        logger.warning(f"下载失败 (尝试 {retry_count}/{max_retries}): {e}")
-                        logger.info(f"等待 {wait_time} 秒后重试...")
-                        time.sleep(wait_time)
-                    else:
-                        logger.error(f"下载失败,已重试 {max_retries} 次: {e}")
-                        raise
-                except Exception as e:
-                    # 其他类型的异常直接抛出,不重试
-                    if target_path.exists():
-                        try:
-                            os.remove(target_path)
-                        except:
-                            pass
-                    raise
-            
-            # 如果所有重试都失败了
-            if last_exception:
-                raise last_exception
-
-        except Exception as e:
-            logger.error(f"下载视频失败: {e}", exc_info=True)
-            return None, True
-    
-    def _check_examples_directory(self, video_id: str) -> Optional[str]:
-        """检查examples/videos目录下是否有对应的mp4文件
-        
-        文件路径格式:examples/videos/{video_id}.mp4
-        
-        Args:
-            video_id: 视频ID
-            
-        Returns:
-            如果找到文件,返回文件路径;否则返回None
-        """
-        try:
-            # 如果没有提供video_id,无法查找
-            if not video_id:
-                logger.info("未提供video_id,跳过examples/videos目录检查")
-                return None
-            
-            # 获取项目根目录
-            # __file__ 是 src/components/functions/video_upload_function.py
-            # 需要往上4层才能到项目根目录
-            project_root = Path(__file__).parent.parent.parent.parent
-            videos_dir = project_root / "examples" / "videos"
-            
-            if not videos_dir.exists():
-                logger.info(f"examples/videos目录不存在: {videos_dir}")
-                return None
-            
-            # 构建文件路径:examples/videos/{video_id}.mp4
-            mp4_file = videos_dir / f"{video_id}.mp4"
-            logger.info(f"构建文件路径: {mp4_file}")
-            
-            # 检查文件是否存在
-            if mp4_file.exists() and mp4_file.is_file():
-                logger.info(f"在examples/videos目录下找到文件: {mp4_file}")
-                return str(mp4_file)
-            
-            logger.debug(f"在examples/videos目录下未找到文件: {mp4_file}")
-            return None
-            
-        except Exception as e:
-            logger.warning(f"检查examples/videos目录时出错: {e}", exc_info=True)
-            return None
-
-

+ 0 - 72
src/components/tools/__init__.py

@@ -1,72 +0,0 @@
-"""
-Tool 组件包
-
-自动导入所有工具模块,实现工具的自动发现和注册
-"""
-
-import os
-import importlib
-from pathlib import Path
-
-
-def auto_import_tools():
-    """自动导入当前包下的所有 Python 模块"""
-    # 获取当前包的路径
-    current_dir = Path(__file__).parent
-    
-    # 获取所有 Python 文件(排除 __init__.py 和 base.py)
-    tool_files = [
-        f.stem for f in current_dir.glob("*.py") 
-        if f.name not in ["__init__.py", "base.py"] and not f.name.startswith("_")
-    ]
-    
-    imported_modules = []
-    failed_imports = []
-    
-    for module_name in tool_files:
-        try:
-            # 动态导入模块
-            module = importlib.import_module(f".{module_name}", package=__name__)
-            imported_modules.append(module_name)
-            
-            # 可选:添加到当前命名空间
-            globals()[module_name] = module
-            
-        except Exception as e:
-            failed_imports.append((module_name, str(e)))
-    
-    # 输出导入结果(仅在开发模式下)
-    if os.getenv("DEBUG_TOOL_IMPORT", "").lower() in ["true", "1", "yes"]:
-        print(f"✅ Successfully imported {len(imported_modules)} tool modules: {imported_modules}")
-        if failed_imports:
-            print(f"❌ Failed to import {len(failed_imports)} modules:")
-            for module_name, error in failed_imports:
-                print(f"  - {module_name}: {error}")
-    
-    return imported_modules, failed_imports
-
-
-# 执行自动导入
-auto_import_tools()
-
-# 导出基础组件
-from .base import (
-    BaseToolComponent,
-    SimpleTool,
-    AsyncTool,
-    ConfigurableTool,
-    ToolRegistry,
-    tool_registry,
-    component_tool
-)
-
-__all__ = [
-    "BaseToolComponent",
-    "SimpleTool", 
-    "AsyncTool",
-    "ConfigurableTool",
-    "ToolRegistry",
-    "tool_registry",
-    "component_tool",
-    "auto_import_tools"
-]

+ 0 - 381
src/components/tools/base.py

@@ -1,381 +0,0 @@
-"""
-组件抽象层:Tool 基础抽象类
-
-定义工具组件的抽象基类,支持 LangGraph 工具注解和标准化接口
-"""
-
-from abc import ABC, abstractmethod
-from typing import Any, Dict, Optional, Union, Type, List
-from langchain_core.tools import BaseTool, tool
-from pydantic import BaseModel, Field
-import inspect
-import functools
-
-
-class BaseToolComponent(ABC):
-    """工具组件基类
-    
-    为工具提供标准化的接口和生命周期管理
-    """
-    
-    def __init__(self, name: str, description: str = "", enabled: bool = True):
-        """初始化工具组件
-        
-        Args:
-            name: 工具名称
-            description: 工具描述
-            enabled: 是否启用
-        """
-        self.name = name
-        self.description = description
-        self.enabled = enabled
-        self._tool_instance: Optional[BaseTool] = None
-        self._initialized = False
-    
-    @abstractmethod
-    def _create_tool(self) -> BaseTool:
-        """创建工具实例,子类必须实现"""
-        pass
-    
-    def initialize(self) -> None:
-        """初始化工具"""
-        if not self._initialized:
-            self._tool_instance = self._create_tool()
-            self._initialized = True
-    
-    def get_tool(self) -> BaseTool:
-        """获取工具实例"""
-        if not self._initialized:
-            self.initialize()
-        return self._tool_instance
-    
-    @property
-    def is_initialized(self) -> bool:
-        """检查是否已初始化"""
-        return self._initialized
-    
-    def enable(self) -> None:
-        """启用工具"""
-        self.enabled = True
-    
-    def disable(self) -> None:
-        """禁用工具"""
-        self.enabled = False
-    
-    def __str__(self) -> str:
-        status = "enabled" if self.enabled else "disabled"
-        return f"{self.__class__.__name__}(name='{self.name}', status='{status}')"
-    
-    def __repr__(self) -> str:
-        return self.__str__()
-
-
-class SimpleTool(BaseToolComponent):
-    """简单工具实现
-    
-    基于函数的简单工具,自动使用 @tool 装饰器
-    """
-    
-    def __init__(
-        self,
-        name: str,
-        description: str,
-        func: callable,
-        schema: Optional[Type[BaseModel]] = None,
-        return_direct: bool = False
-    ):
-        """初始化简单工具
-        
-        Args:
-            name: 工具名称
-            description: 工具描述
-            func: 工具执行函数
-            schema: 输入参数 schema
-            return_direct: 是否直接返回结果
-        """
-        super().__init__(name, description)
-        self.func = func
-        self.schema = schema
-        self.return_direct = return_direct
-    
-    def _create_tool(self) -> BaseTool:
-        """创建工具实例"""
-        # 使用 @tool 装饰器创建工具
-        # 先设置函数的名称和文档
-        self.func.__name__ = self.name
-        self.func.__doc__ = self.description
-        
-        if self.schema:
-            return tool(
-                args_schema=self.schema,
-                return_direct=self.return_direct
-            )(self.func)
-        else:
-            return tool(
-                return_direct=self.return_direct
-            )(self.func)
-
-
-class AsyncTool(BaseToolComponent):
-    """异步工具实现
-    
-    支持异步操作的工具
-    """
-    
-    def __init__(
-        self,
-        name: str,
-        description: str,
-        async_func: callable,
-        schema: Optional[Type[BaseModel]] = None,
-        return_direct: bool = False
-    ):
-        """初始化异步工具
-        
-        Args:
-            name: 工具名称
-            description: 工具描述
-            async_func: 异步工具执行函数
-            schema: 输入参数 schema
-            return_direct: 是否直接返回结果
-        """
-        super().__init__(name, description)
-        self.async_func = async_func
-        self.schema = schema
-        self.return_direct = return_direct
-    
-    def _create_tool(self) -> BaseTool:
-        """创建异步工具实例"""
-        # 先设置函数的名称和文档
-        self.async_func.__name__ = self.name
-        self.async_func.__doc__ = self.description
-        
-        if self.schema:
-            return tool(
-                args_schema=self.schema,
-                return_direct=self.return_direct
-            )(self.async_func)
-        else:
-            return tool(
-                return_direct=self.return_direct
-            )(self.async_func)
-
-
-class ConfigurableTool(BaseToolComponent):
-    """可配置工具
-    
-    支持运行时配置的工具
-    """
-    
-    def __init__(
-        self,
-        name: str,
-        description: str,
-        base_func: callable,
-        config: Optional[Dict[str, Any]] = None,
-        schema: Optional[Type[BaseModel]] = None
-    ):
-        """初始化可配置工具
-        
-        Args:
-            name: 工具名称
-            description: 工具描述
-            base_func: 基础执行函数
-            config: 配置参数
-            schema: 输入参数 schema
-        """
-        super().__init__(name, description)
-        self.base_func = base_func
-        self.config = config or {}
-        self.schema = schema
-    
-    def _create_tool(self) -> BaseTool:
-        """创建可配置工具实例"""
-        # 创建带配置的包装函数
-        def configured_func(*args, **kwargs):
-            # 将配置参数传递给基础函数
-            return self.base_func(*args, config=self.config, **kwargs)
-        
-        # 更新函数签名和文档
-        configured_func.__name__ = self.name
-        configured_func.__doc__ = self.description
-        
-        if self.schema:
-            return tool(
-                args_schema=self.schema
-            )(configured_func)
-        else:
-            return tool()(configured_func)
-    
-    def update_config(self, new_config: Dict[str, Any]) -> None:
-        """更新配置"""
-        self.config.update(new_config)
-        # 重新初始化工具
-        if self._initialized:
-            self._initialized = False
-            self.initialize()
-
-
-class ToolRegistry:
-    """工具注册表
-    
-    管理和组织工具组件
-    """
-    
-    def __init__(self):
-        self._tools: Dict[str, BaseToolComponent] = {}
-        self._categories: Dict[str, List[str]] = {}
-    
-    def register(self, tool: BaseToolComponent, category: Optional[str] = None) -> None:
-        """注册工具
-        
-        Args:
-            tool: 工具实例
-            category: 工具分类
-        """
-        if tool.name in self._tools:
-            raise ValueError(f"Tool '{tool.name}' already registered")
-        
-        self._tools[tool.name] = tool
-        
-        if category:
-            if category not in self._categories:
-                self._categories[category] = []
-            self._categories[category].append(tool.name)
-    
-    def get_tool(self, name: str) -> Optional[BaseToolComponent]:
-        """获取工具
-        
-        Args:
-            name: 工具名称
-            
-        Returns:
-            工具实例或 None
-        """
-        return self._tools.get(name)
-    
-    def get_tools_by_category(self, category: str) -> List[BaseToolComponent]:
-        """根据分类获取工具
-        
-        Args:
-            category: 分类名称
-            
-        Returns:
-            工具列表
-        """
-        tool_names = self._categories.get(category, [])
-        return [self._tools[name] for name in tool_names if name in self._tools]
-    
-    def get_enabled_tools(self) -> List[BaseTool]:
-        """获取所有启用的工具
-        
-        Returns:
-            启用的工具列表
-        """
-        enabled_tools = []
-        for tool_component in self._tools.values():
-            if tool_component.enabled:
-                enabled_tools.append(tool_component.get_tool())
-        return enabled_tools
-    
-    def list_tools(self) -> Dict[str, str]:
-        """列出所有工具
-        
-        Returns:
-            工具名称到描述的映射
-        """
-        return {name: tool.description for name, tool in self._tools.items()}
-    
-    def enable_tool(self, name: str) -> bool:
-        """启用工具
-        
-        Args:
-            name: 工具名称
-            
-        Returns:
-            是否成功
-        """
-        tool = self._tools.get(name)
-        if tool:
-            tool.enable()
-            return True
-        return False
-    
-    def disable_tool(self, name: str) -> bool:
-        """禁用工具
-        
-        Args:
-            name: 工具名称
-            
-        Returns:
-            是否成功
-        """
-        tool = self._tools.get(name)
-        if tool:
-            tool.disable()
-            return True
-        return False
-
-
-# 全局工具注册表实例
-tool_registry = ToolRegistry()
-
-
-# 便捷装饰器
-def component_tool(
-    name: Optional[str] = None,
-    description: Optional[str] = None,
-    category: Optional[str] = None,
-    schema: Optional[Type[BaseModel]] = None,
-    return_direct: bool = False,
-    auto_register: bool = True
-):
-    """组件工具装饰器
-    
-    将函数转换为工具组件并可选择自动注册
-    
-    Args:
-        name: 工具名称,默认使用函数名
-        description: 工具描述,默认使用函数文档字符串
-        category: 工具分类
-        schema: 输入参数 schema
-        return_direct: 是否直接返回结果
-        auto_register: 是否自动注册到全局注册表
-    """
-    def decorator(func):
-        tool_name = name or func.__name__
-        tool_description = description or (func.__doc__ or "").strip()
-        
-        # 创建工具组件
-        if inspect.iscoroutinefunction(func):
-            tool_component = AsyncTool(
-                name=tool_name,
-                description=tool_description,
-                async_func=func,
-                schema=schema,
-                return_direct=return_direct
-            )
-        else:
-            tool_component = SimpleTool(
-                name=tool_name,
-                description=tool_description,
-                func=func,
-                schema=schema,
-                return_direct=return_direct
-            )
-        
-        # 自动注册
-        if auto_register:
-            tool_registry.register(tool_component, category)
-        
-        # 返回包装后的函数,保持原有接口
-        @functools.wraps(func)
-        def wrapper(*args, **kwargs):
-            return func(*args, **kwargs)
-        
-        # 将工具组件添加为函数属性
-        wrapper._tool_component = tool_component
-        
-        return wrapper
-    
-    return decorator

+ 0 - 235
src/components/tools/knowledge_retrieval_tools.py

@@ -1,235 +0,0 @@
-"""知识检索工具
-
-KnowledgeRetrievalTool - 从知识库检索解构所需的知识
-"""
-
-from typing import Dict, Any, List, Optional
-from pydantic import BaseModel, Field
-from langchain_core.tools import BaseTool
-import requests
-import urllib.parse
-
-from src.components.tools.base import SimpleTool, AsyncTool
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class KnowledgeRetrievalInput(BaseModel):
-    """知识检索工具输入Schema"""
-    query: str = Field(..., description="知识查询文本")
-    knowledge_type: str = Field(
-        default="general",
-        description="知识类型: category(品类知识), element(元素知识), split(拆分知识), general(通用)"
-    )
-    top_k: int = Field(default=3, description="返回Top K结果", ge=1, le=10)
-
-
-class KnowledgeRetrievalTool:
-    """知识检索工具
-
-    根据Query从知识库检索解构所需的知识
-
-    功能:
-    - 接收Query并调用知识库API
-    - 支持多种知识库类型(向量检索、关系型等)
-    - 返回结构化知识
-
-    实现方式: Tool组件(确定性API调用 + 需要被Agent调用)
-    依据: PRD 3.3节"外部知识调用"
-    """
-
-    def __init__(
-        self,
-        api_url: str = "http://61.48.133.26:8001/api/query",
-        dataset_ids: str = "11",
-        timeout: int = 10
-    ):
-        """初始化知识检索工具
-
-        Args:
-            api_url: 知识库API地址
-            dataset_ids: 数据集ID,逗号分隔(11=内容知识库V2, 12=工具使用库V2)
-            timeout: API请求超时时间(秒)
-        """
-        self.api_url = api_url
-        self.dataset_ids = dataset_ids
-        self.timeout = timeout
-        self._tool: Optional[SimpleTool] = None
-
-    def _retrieve_knowledge_api(
-        self,
-        query: str,
-        knowledge_type: str = "general",
-        top_k: int = 3
-    ) -> Dict[str, Any]:
-        """通过API检索知识
-
-        Args:
-            query: 查询文本
-            knowledge_type: 知识类型(仅用于标记,API统一检索)
-            top_k: 返回Top K结果
-
-        Returns:
-            检索结果字典
-        """
-        logger.info(f"API知识检索 - Query: {query}, Type: {knowledge_type}, Top_k: {top_k}")
-
-        try:
-            # 构建请求URL
-            params = {
-                "query": query,
-                "datasetIds": self.dataset_ids
-            }
-            url = f"{self.api_url}?{urllib.parse.urlencode(params)}"
-
-            # 发送请求
-            logger.debug(f"请求URL: {url}")
-            response = requests.get(url, timeout=self.timeout)
-            response.raise_for_status()
-
-            # 解析响应
-            data = response.json()
-
-            if data.get("status_code") != 200:
-                logger.warning(f"API返回错误: {data.get('detail', 'Unknown error')}")
-                return self._create_empty_result(query, knowledge_type, "API返回错误")
-
-            # 提取检索结果
-            results = data.get("data", {}).get("results", [])
-
-            if not results:
-                logger.info("API未返回任何结果,使用默认知识")
-
-            # 限制返回数量
-            results = results[:top_k]
-
-            # 整理知识内容
-            knowledge_items = []
-            for item in results:
-                content = item.get("content", item.get("content", ""))
-                if content:
-                    knowledge_items.append({
-                        "content": content,
-                        "source": item.get("datasetName", "Unknown"),
-                        "score": item.get("score", 0.0),
-                        "doc_id": item.get("docId", "")
-                    })
-
-            # 合并知识内容
-            combined_knowledge = "\n\n".join([
-                f"[来源: {item['source']}]\n{item['content']}"
-                for item in knowledge_items
-            ])
-
-            # 计算平均置信度
-            avg_confidence = sum([item["score"] for item in knowledge_items]) / len(knowledge_items) if knowledge_items else 0.0
-
-            result = {
-                "knowledge": combined_knowledge,
-                "sources": [item["source"] for item in knowledge_items],
-                "confidence": avg_confidence,
-                "query": query,
-                "knowledge_type": knowledge_type,
-                "results_count": len(knowledge_items),
-                "api_response": True
-            }
-
-            logger.info(f"API检索成功,返回 {len(knowledge_items)} 条知识,平均置信度: {avg_confidence:.4f}")
-            logger.info(f"API检索成功,返回 - Query: {query}, 知识: {combined_knowledge}")
-
-            return result
-
-        except requests.exceptions.Timeout:
-            logger.error(f"API请求超时({self.timeout}秒)")
-            return self._create_empty_result(query, knowledge_type, "API请求超时,使用备用知识")
-
-        except requests.exceptions.RequestException as e:
-            logger.error(f"API请求失败: {e}")
-
-        except Exception as e:
-            logger.error(f"知识检索异常: {e}", exc_info=True)
-            return self._create_empty_result(query, knowledge_type, f"检索异常: {str(e)}")
-
-    def _create_empty_result(self, query: str, knowledge_type: str, reason: str) -> Dict[str, Any]:
-        """创建空结果
-
-        Args:
-            query: 查询文本
-            knowledge_type: 知识类型
-            reason: 原因
-
-        Returns:
-            空结果字典
-        """
-        return {
-            "knowledge": f"暂无相关知识。原因: {reason}",
-            "sources": [],
-            "confidence": 0.0,
-            "query": query,
-            "knowledge_type": knowledge_type,
-            "results_count": 0,
-            "error": reason
-        }
-
-    def _create_tool_function(self):
-        """创建工具函数"""
-        def retrieve_knowledge(
-            query: str,
-            knowledge_type: str = "general",
-            top_k: int = 3
-        ) -> Dict[str, Any]:
-            """从知识库检索解构所需的知识
-
-            Args:
-                query: 知识查询文本
-                knowledge_type: 知识类型 (category/element/split/general)
-                top_k: 返回Top K结果
-
-            Returns:
-                包含知识内容、来源和置信度的字典
-            """
-            return self._retrieve_knowledge_api(query, knowledge_type, top_k)
-
-        return retrieve_knowledge
-
-    def get_tool(self) -> BaseTool:
-        """获取工具实例
-
-        Returns:
-            LangChain BaseTool实例
-        """
-        if self._tool is None:
-            func = self._create_tool_function()
-            self._tool = SimpleTool(
-                name="knowledge_retrieval",
-                description="从知识库检索What解构所需的知识。支持品类知识、元素知识、拆分知识等多种类型的查询。",
-                func=func,
-                schema=KnowledgeRetrievalInput
-            )
-            self._tool.initialize()
-
-        return self._tool.get_tool()
-
-    def retrieve(
-        self,
-        query: str,
-        knowledge_type: str = "general",
-        top_k: int = 3
-    ) -> Dict[str, Any]:
-        """直接调用检索方法(非Tool调用)
-
-        Args:
-            query: 查询文本
-            knowledge_type: 知识类型
-            top_k: 返回Top K结果
-
-        Returns:
-            检索结果
-        """
-        func = self._create_tool_function()
-        return func(query=query, knowledge_type=knowledge_type, top_k=top_k)
-
-
-# 创建默认实例(使用真实API)
-default_knowledge_tool = KnowledgeRetrievalTool()

+ 0 - 160
src/components/tools/nanobanana_tools.py

@@ -1,160 +0,0 @@
-"""
-Nano Banana图像生成与编辑工具组件实现
-
-提供基于远程API的图像生成和编辑功能
-"""
-
-import json
-import requests
-from pydantic import BaseModel, Field
-
-from src.components.tools.base import component_tool
-from src.utils.logger import get_logger
-
-
-class NanoBananaInput(BaseModel):
-    """Nano Banana图像生成/编辑输入参数 schema"""
-    prompt: str = Field(
-        description="生图/图片编辑提示词,提示词要表现出你的意图是得到一张图片"
-    )
-    images: str = Field(
-        default="",
-        description="要编辑的图片url。多张图片使用空格进行分隔。如果是生图任务则不需要提供"
-    )
-
-
-@component_tool(
-    name="nano_banana_generate_image",
-    description="""Nano Banana是谷歌最新的图像生成与编辑模型。
-    核心优点:非常好的角色一致性
-    使用场景:
-    1. 提示词描述生成图片。提示词尽量详细且具体
-    2. 图片编辑。在提示中描述清楚需要对图片如何编辑,比如修改图片背景、替换图片中某个元素、调整图片中人物的动作等等
-    3. 图片融合。将多张图片的元素融合成在一张图片中
-    4. 图片元素提取。提示词中清晰描述需要提取的元素
-
-    Args:
-        prompt: 生图/图片编辑提示词,提示词要表现出你的意图是得到一张图片
-        images: 要编辑的图片url。多张图片使用空格进行分隔。如果是生图任务则不需要提供
-
-    Returns:
-        生成或编辑后的图片URL
-    """,
-    category="image_processing",
-    schema=NanoBananaInput
-)
-def nano_banana_generate_image(prompt: str, images: str = "") -> str:
-    """
-    Nano Banana图像生成与编辑工具
-
-    Args:
-        prompt: 生图/图片编辑提示词
-        images: 要编辑的图片url(可选)
-
-    Returns:
-        生成或编辑后的图片URL或错误信息
-    """
-    logger = get_logger("NanoBanana")
-
-    try:
-        # API接口地址
-        api_url = "http://47.84.182.56:8001/tools/call/nano_banana_generate_image"
-
-        # 构建请求参数
-        payload = {
-            "prompt": prompt
-        }
-
-        # 如果提供了图片URL,添加到请求参数中
-        if images:
-            payload["images"] = images
-            logger.info(f"Requesting image editing with prompt: {prompt}, images: {images}...")
-        else:
-            logger.info(f"Requesting image generation with prompt: {prompt}")
-
-        # 发起POST请求
-        response = requests.post(api_url, json=payload, timeout=120)
-        response.raise_for_status()
-
-        # 解析响应
-        result = response.json()
-
-        logger.info(f"NanoBanana Raw response: {result}")
-
-        # 检查响应格式
-        if isinstance(result, dict):
-            # 标准响应格式: {"success": True/False, "result": "...", ...}
-            if "success" in result:
-                success = result.get("success", False)
-                result_data = result.get("result", "")
-
-                # 处理result_data可能是JSON字符串的情况
-                if isinstance(result_data, str):
-                    try:
-                        # 尝试解析JSON字符串
-                        parsed_data = json.loads(result_data)
-                        # 如果是数组,取第一个元素
-                        if isinstance(parsed_data, list) and len(parsed_data) > 0:
-                            result_data = parsed_data[0]
-                        elif isinstance(parsed_data, list) and len(parsed_data) == 0:
-                            error_msg = "Nano Banana返回空结果"
-                            logger.error(error_msg)
-                            return error_msg
-                    except json.JSONDecodeError:
-                        # 如果不是JSON,保持原样
-                        pass
-                # 处理result_data可能是数组的情况
-                elif isinstance(result_data, list) and len(result_data) > 0:
-                    result_data = result_data[0]  # 取第一个元素
-
-                if success and result_data and not result_data.startswith("tools运行失败"):
-                    # 成功且有有效结果
-                    logger.info(f"Nano Banana completed successfully: {result_data}")
-                    return result_data
-                else:
-                    # 失败或错误
-                    error_msg = f"Nano Banana执行失败: {result_data}"
-                    logger.error(error_msg)
-                    return error_msg
-
-            # 直接返回image_url的格式
-            elif "image_url" in result:
-                image_url = result["image_url"]
-                # 处理image_url可能是数组的情况
-                if isinstance(image_url, list) and len(image_url) > 0:
-                    image_url = image_url[0]
-                logger.info(f"Nano Banana completed successfully: {image_url}")
-                return image_url
-
-            # 其他字典格式
-            else:
-                logger.warning(f"Unexpected response format: {result}")
-                return str(result)
-
-        elif isinstance(result, str):
-            # 如果直接返回字符串,假设是图片URL或错误信息
-            if result.startswith("http"):
-                logger.info(f"Nano Banana completed: {result}")
-                return result
-            else:
-                logger.warning(f"Received non-URL string response: {result}")
-                return result
-
-        else:
-            logger.warning(f"Unexpected response type: {type(result)}, value: {result}")
-            return str(result)
-
-    except requests.exceptions.Timeout:
-        error_msg = "Nano Banana请求超时,请稍后重试"
-        logger.error(error_msg)
-        return error_msg
-
-    except requests.exceptions.RequestException as e:
-        error_msg = f"Nano Banana请求失败: {str(e)}"
-        logger.error(error_msg, exc_info=True)
-        return error_msg
-
-    except Exception as e:
-        error_msg = f"Nano Banana处理异常: {str(e)}"
-        logger.error(error_msg, exc_info=True)
-        return error_msg

+ 0 - 156
src/components/tools/segment_tools.py

@@ -1,156 +0,0 @@
-"""
-图像分割工具组件实现
-
-提供基于远程API的图像分割功能
-"""
-
-import json
-import requests
-from pydantic import BaseModel, Field
-
-from src.components.tools.base import component_tool
-from src.utils.logger import get_logger
-
-
-class SegmentImageInput(BaseModel):
-    """图像分割输入参数 schema"""
-    image: str = Field(description="要分割的图片URL")
-    prompt: str = Field(
-        description="分割对象描述,必须是简单的英文单词(1-3个单词),"
-                    "应该是通俗易懂的描述,从小孩子的视角,不要使用具体的人名等;"
-                    "要求描述能在图片中唯一确定一个对象"
-    )
-
-
-@component_tool(
-    name="segment_image",
-    description="""图片分割工具,用于:
-    根据对象名称对图像的进行分割,抽取指定对象的切片
-    图像对象识别与分离
-    前景背景分离
-    多目标实例分割
-    精确边缘提取
-
-    Args:
-        image: 要分割的图片URL
-        prompt: 分割对象描述,必须是简单的英文单词(1-3个单词),
-               应该是通俗易懂的描述,从小孩子的视角,不要使用具体的人名等;
-               要求描述能在图片中唯一确定一个对象
-
-    Returns:
-        分割后的图片URL
-    """,
-    category="image_processing",
-    schema=SegmentImageInput
-)
-def segment_image(image: str, prompt: str) -> str:
-    """
-    图片分割工具,根据对象描述从图像中分割出指定对象
-
-    Args:
-        image: 要分割的图片URL
-        prompt: 分割对象描述(必须是简单的英文单词,1-3个单词)
-
-    Returns:
-        分割后的图片URL或错误信息
-    """
-    logger = get_logger("SegmentImage")
-
-    try:
-        # API接口地址
-        api_url = "http://47.84.182.56:8000/tools/call/segment_image"
-
-        # 构建请求参数
-        payload = {
-            "image": image,
-            "prompt": prompt
-        }
-
-        logger.info(f"Requesting image segmentation for: {image}... with prompt: {prompt}")
-
-        # 发起POST请求
-        response = requests.post(api_url, json=payload, timeout=60)
-        response.raise_for_status()
-
-        # 解析响应
-        result = response.json()
-
-        logger.info(f"segmentation Raw response: {result}")
-
-        # 检查响应格式
-        if isinstance(result, dict):
-            # 标准响应格式: {"success": True/False, "result": "...", ...}
-            if "success" in result:
-                success = result.get("success", False)
-                result_data = result.get("result", "")
-
-                # 处理result_data可能是JSON字符串的情况
-                if isinstance(result_data, str):
-                    try:
-                        # 尝试解析JSON字符串
-                        parsed_data = json.loads(result_data)
-                        # 如果是数组,取第一个元素
-                        if isinstance(parsed_data, list) and len(parsed_data) > 0:
-                            result_data = parsed_data[0]
-                        elif isinstance(parsed_data, list) and len(parsed_data) == 0:
-                            error_msg = "图像分割返回空结果"
-                            logger.error(error_msg)
-                            return error_msg
-                    except json.JSONDecodeError:
-                        # 如果不是JSON,保持原样
-                        pass
-                # 处理result_data可能是数组的情况
-                elif isinstance(result_data, list) and len(result_data) > 0:
-                    result_data = result_data[0]  # 取第一个元素
-
-                if success and result_data and not result_data.startswith("tools运行失败"):
-                    # 成功且有有效结果
-                    logger.info(f"Image segmentation completed successfully: {result_data}")
-                    return result_data
-                else:
-                    # 失败或错误
-                    error_msg = f"图像分割失败: {result_data}"
-                    logger.error(error_msg)
-                    return error_msg
-
-            # 直接返回image_url的格式
-            elif "image_url" in result:
-                image_url = result["image_url"]
-                # 处理image_url可能是数组的情况
-                if isinstance(image_url, list) and len(image_url) > 0:
-                    image_url = image_url[0]
-                logger.info(f"Image segmentation completed successfully: {image_url}")
-                return image_url
-
-            # 其他字典格式
-            else:
-                logger.warning(f"Unexpected response format: {result}")
-                return str(result)
-
-        elif isinstance(result, str):
-            # 如果直接返回字符串,假设是图片URL或错误信息
-            if result.startswith("http"):
-                logger.info(f"Image segmentation completed: {result}")
-                return result
-            else:
-                logger.warning(f"Received non-URL string response: {result}")
-                return result
-
-        else:
-            logger.warning(f"Unexpected response type: {type(result)}, value: {result}")
-            return str(result)
-
-    except requests.exceptions.Timeout:
-        error_msg = "图像分割请求超时,请稍后重试"
-        logger.error(error_msg)
-        return error_msg
-
-    except requests.exceptions.RequestException as e:
-        error_msg = f"图像分割请求失败: {str(e)}"
-        logger.error(error_msg, exc_info=True)
-        return error_msg
-
-    except Exception as e:
-        error_msg = f"图像分割处理异常: {str(e)}"
-        logger.error(error_msg, exc_info=True)
-        return error_msg

+ 0 - 315
src/models/README.md

@@ -1,315 +0,0 @@
-# Models 模块
-
-数据库模型模块,用于同步数据库表设计,提供 ORM 支持。
-
-## 目录结构
-
-```
-src/models/
-├── __init__.py          # 模块导出
-├── database.py          # 数据库配置和连接管理
-├── decode_video.py      # DecodeVideo 模型
-└── README.md            # 本文件
-```
-
-## 环境变量配置
-
-在使用模型之前,需要配置以下环境变量(如果使用默认配置,则无需设置):
-
-```bash
-# 数据库配置(使用外网地址)
-export DB_HOST=rm-t4nh1xx6o2a6vj8qu3o.mysql.singapore.rds.aliyuncs.com  # 数据库主机(默认已配置)
-export DB_PORT=3306              # 数据库端口(默认: 3306)
-export DB_USER=content_rw        # 数据库用户名(默认已配置)
-export DB_PASSWORD=bC1aH4bA1lB0 # 数据库密码(默认已配置)
-export DB_NAME=content-deconstruction  # 数据库名称(默认已配置)
-```
-
-或者在 `.env` 文件中配置:
-
-```env
-# 使用外网地址访问数据库
-DB_HOST=rm-t4nh1xx6o2a6vj8qu3o.mysql.singapore.rds.aliyuncs.com
-DB_PORT=3306
-DB_USER=content_rw
-DB_PASSWORD=bC1aH4bA1lB0
-DB_NAME=content-deconstruction
-
-# 如需使用内网地址,可切换为:
-# DB_HOST=rm-t4nh1xx6o2a6vj8qu.mysql.singapore.rds.aliyuncs.com
-```
-
-**注意**:代码中已配置默认值,可以直接使用。如需覆盖,可通过环境变量设置。
-
-## 快速开始
-
-### 1. 初始化数据库
-
-```python
-from src.models import init_db
-
-# 创建所有表(如果不存在)
-init_db()
-```
-
-### 2. 使用模型
-
-#### 创建记录
-
-```python
-from src.models import get_db, DecodeVideo, DecodeStatus
-
-# 获取数据库会话
-db = next(get_db())
-
-# 创建新记录
-video = DecodeVideo.create(
-    task_id=12345,
-    video_id="58840748",
-    status=DecodeStatus.PENDING
-)
-
-# 保存到数据库
-db.add(video)
-db.commit()
-db.close()
-```
-
-#### 查询记录
-
-```python
-from src.models import get_db, DecodeVideo, DecodeStatus
-
-db = next(get_db())
-
-# 根据 task_id 查询
-video = db.query(DecodeVideo).filter_by(task_id=12345).first()
-
-# 根据 video_id 查询
-videos = db.query(DecodeVideo).filter_by(video_id="58840748").all()
-
-# 根据状态查询
-pending_videos = db.query(DecodeVideo).filter_by(status=DecodeStatus.PENDING).all()
-
-# 查询所有记录
-all_videos = db.query(DecodeVideo).all()
-
-db.close()
-```
-
-#### 更新记录
-
-```python
-from src.models import get_db, DecodeVideo, DecodeStatus
-
-db = next(get_db()
-
-# 查询记录
-video = db.query(DecodeVideo).filter_by(task_id=12345).first()
-
-if video:
-    # 更新状态
-    video.update_status(DecodeStatus.EXECUTING)
-    
-    # 更新结果
-    video.update_result('{"result": "..."}')
-    
-    # 或者直接更新字段
-    video.status = DecodeStatus.SUCCESS
-    video.result = '{"result": "..."}'
-    
-    db.commit()
-
-db.close()
-```
-
-#### 删除记录
-
-```python
-from src.models import get_db, DecodeVideo
-
-db = next(get_db())
-
-video = db.query(DecodeVideo).filter_by(task_id=12345).first()
-if video:
-    db.delete(video)
-    db.commit()
-
-db.close()
-```
-
-### 3. 使用上下文管理器(推荐)
-
-```python
-from contextlib import contextmanager
-from src.models import get_db, DecodeVideo, DecodeStatus
-
-@contextmanager
-def db_session():
-    """数据库会话上下文管理器"""
-    db = next(get_db())
-    try:
-        yield db
-        db.commit()
-    except Exception:
-        db.rollback()
-        raise
-    finally:
-        db.close()
-
-# 使用示例
-with db_session() as db:
-    video = DecodeVideo.create(
-        task_id=12345,
-        video_id="58840748",
-        status=DecodeStatus.PENDING
-    )
-    db.add(video)
-    # 自动提交和关闭
-```
-
-## 模型说明
-
-### DecodeVideo
-
-对应数据库表 `decode_videos`
-
-#### 字段
-
-- `task_id` (BigInteger, Primary Key): 任务ID,非空
-- `video_id` (String(100)): 视频ID,可为空,已建立索引
-- `result` (Text): 解码结果(JSON 格式),可为空
-- `status` (Integer): 状态,可为空,默认值为 0(待执行),已建立索引
-  - 0: 待执行 (PENDING)
-  - 1: 执行中 (EXECUTING)
-  - 2: 执行成功 (SUCCESS)
-  - 3: 执行失败 (FAILED)
-- `error_reason` (Text): 失败原因,可为空
-- `created_at` (DateTime): 创建时间,自动设置
-- `updated_at` (DateTime): 更新时间,自动更新
-
-#### 方法
-
-- `to_dict()`: 将模型转换为字典
-- `create()`: 类方法,创建新实例
-- `update_status()`: 更新状态
-- `update_result()`: 更新解码结果
-
-### DecodeStatus
-
-状态枚举类
-
-```python
-DecodeStatus.PENDING    # 0 - 待执行
-DecodeStatus.EXECUTING  # 1 - 执行中
-DecodeStatus.SUCCESS    # 2 - 执行成功
-DecodeStatus.FAILED     # 3 - 执行失败
-```
-
-## 完整示例
-
-```python
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-使用 DecodeVideo 模型的完整示例
-"""
-
-from contextlib import contextmanager
-from src.models import get_db, DecodeVideo, DecodeStatus, init_db
-
-@contextmanager
-def db_session():
-    """数据库会话上下文管理器"""
-    db = next(get_db())
-    try:
-        yield db
-        db.commit()
-    except Exception:
-        db.rollback()
-        raise
-    finally:
-        db.close()
-
-
-def main():
-    # 1. 初始化数据库(首次使用)
-    init_db()
-    
-    # 2. 创建新记录
-    with db_session() as db:
-        video = DecodeVideo.create(
-            task_id=12345,
-            video_id="58840748",
-            status=DecodeStatus.PENDING
-        )
-        db.add(video)
-        print(f"创建记录: {video}")
-    
-    # 3. 查询记录
-    with db_session() as db:
-        video = db.query(DecodeVideo).filter_by(task_id=12345).first()
-        if video:
-            print(f"查询结果: {video.to_dict()}")
-    
-    # 4. 更新状态
-    with db_session() as db:
-        video = db.query(DecodeVideo).filter_by(task_id=12345).first()
-        if video:
-            video.update_status(DecodeStatus.EXECUTING)
-            print(f"更新状态为: {DecodeStatus.get_description(video.status)}")
-    
-    # 5. 更新结果
-    with db_session() as db:
-        video = db.query(DecodeVideo).filter_by(task_id=12345).first()
-        if video:
-            result = '{"result": "解码成功"}'
-            video.update_result(result)
-            print(f"更新结果: {video.result}")
-    
-    # 6. 查询所有待执行的记录
-    with db_session() as db:
-        pending_videos = db.query(DecodeVideo).filter_by(
-            status=DecodeStatus.PENDING
-        ).all()
-        print(f"待执行任务数: {len(pending_videos)}")
-
-
-if __name__ == "__main__":
-    main()
-```
-
-## 注意事项
-
-1. **数据库连接**: 使用 `get_db()` 获取数据库会话后,务必在最后调用 `db.close()` 关闭连接
-2. **事务管理**: 修改数据后需要调用 `db.commit()` 提交事务,出错时调用 `db.rollback()` 回滚
-3. **环境变量**: 确保正确配置数据库连接相关的环境变量
-4. **表结构同步**: 如果数据库表结构发生变化,需要更新模型定义以保持同步
-5. **索引**: `video_id` 和 `status` 字段已建立索引,适合用于查询条件
-
-## 与工作流集成
-
-可以在工作流中使用模型来保存和查询解码结果:
-
-```python
-from src.models import get_db, DecodeVideo, DecodeStatus
-
-def save_decode_result(task_id: int, video_id: str, result: dict):
-    """保存解码结果到数据库"""
-    db = next(get_db())
-    try:
-        video = db.query(DecodeVideo).filter_by(task_id=task_id).first()
-        if not video:
-            video = DecodeVideo.create(task_id=task_id, video_id=video_id)
-            db.add(video)
-        
-        import json
-        video.update_result(json.dumps(result, ensure_ascii=False))
-        db.commit()
-    except Exception as e:
-        db.rollback()
-        raise
-    finally:
-        db.close()
-```
-

+ 0 - 21
src/models/__init__.py

@@ -1,21 +0,0 @@
-"""
-Models 模块
-
-提供数据库模型定义,用于同步数据库表设计
-"""
-
-from src.models.database import Base, get_db, get_engine, init_db
-from src.models.decode_video import DecodeVideo, DecodeStatus
-from src.models.evaluate_record import EvaluateRecord, EvaluateStatus
-
-__all__ = [
-    "Base",
-    "get_db",
-    "get_engine",
-    "init_db",
-    "DecodeVideo",
-    "DecodeStatus",
-    "EvaluateRecord",
-    "EvaluateStatus",
-]
-

+ 0 - 129
src/models/database.py

@@ -1,129 +0,0 @@
-"""
-数据库配置模块
-
-提供数据库连接和会话管理
-"""
-
-import os
-from dotenv import load_dotenv, find_dotenv
-from typing import Generator
-from sqlalchemy import create_engine
-from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import sessionmaker, Session
-from sqlalchemy.pool import QueuePool
-
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-# 数据库基础类
-Base = declarative_base()
-
-# 全局变量
-_engine = None
-_SessionLocal = None
-
-
-def get_database_url() -> str:
-    """获取数据库连接URL
-    
-    Returns:
-        str: 数据库连接URL
-        
-    Environment Variables:
-        DB_HOST: 数据库主机地址 (默认: rm-t4nh1xx6o2a6vj8qu3o.mysql.singapore.rds.aliyuncs.com)
-        DB_PORT: 数据库端口 (默认: 3306)
-        DB_USER: 数据库用户名 (默认: content_rw)
-        DB_PASSWORD: 数据库密码 (必需)
-        DB_NAME: 数据库名称 (默认: content-deconstruction)
-    """
-    load_dotenv(find_dotenv(), override=False)
-    env = (os.getenv("APP_ENV") or os.getenv("ENV") or "local").lower()
-    host = os.getenv("DB_HOST", "rm-t4nh1xx6o2a6vj8qu3o.mysql.singapore.rds.aliyuncs.com")
-    port = os.getenv("DB_PORT", "3306")
-    user = os.getenv("DB_USER", "content_rw")
-    password = os.getenv("DB_PASSWORD", "bC1aH4bA1lB0")
-    database = os.getenv("DB_NAME", "content-deconstruction-test" if env in ("local","dev","development") else "content-deconstruction")
-    
-    if not password:
-        raise ValueError("DB_PASSWORD environment variable is required")
-    
-    return f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset=utf8mb4"
-
-
-def get_engine():
-    """获取数据库引擎(单例模式)
-    
-    Returns:
-        Engine: SQLAlchemy 数据库引擎
-    """
-    global _engine
-    if _engine is None:
-        database_url = get_database_url()
-        _engine = create_engine(
-            database_url,
-            poolclass=QueuePool,
-            pool_size=10,
-            max_overflow=20,
-            pool_pre_ping=True,  # 连接前检查连接是否有效
-            echo=False,  # 设置为 True 可以打印 SQL 语句,用于调试
-        )
-        db_name = database_url.rsplit('/', 1)[-1].split('?')[0]
-        logger.info(f"Database engine created for database: {db_name}")
-    return _engine
-
-
-def get_session_local():
-    """获取会话工厂(单例模式)
-    
-    Returns:
-        sessionmaker: SQLAlchemy 会话工厂
-    """
-    global _SessionLocal
-    if _SessionLocal is None:
-        engine = get_engine()
-        _SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
-    return _SessionLocal
-
-
-def get_db() -> Generator[Session, None, None]:
-    """获取数据库会话(依赖注入)
-    
-    Yields:
-        Session: SQLAlchemy 数据库会话
-        
-    Example:
-        ```python
-        db = next(get_db())
-        video = db.query(DecodeVideo).filter_by(video_id="123").first()
-        db.close()
-        ```
-    """
-    SessionLocal = get_session_local()
-    db = SessionLocal()
-    try:
-        yield db
-    finally:
-        db.close()
-
-
-def init_db():
-    """初始化数据库(创建所有表)
-    
-    注意:此方法会创建所有在 Base.metadata 中注册的表
-    如果表已存在,不会重复创建
-    """
-    engine = get_engine()
-    Base.metadata.create_all(bind=engine)
-    logger.info("Database tables initialized")
-
-
-def drop_db():
-    """删除所有表(谨慎使用)
-    
-    警告:此方法会删除所有在 Base.metadata 中注册的表
-    仅用于开发和测试环境
-    """
-    engine = get_engine()
-    Base.metadata.drop_all(bind=engine)
-    logger.warning("All database tables dropped")

+ 0 - 195
src/models/decode_video.py

@@ -1,195 +0,0 @@
-"""
-DecodeVideo 模型
-
-对应数据库表 decode_videos 的 ORM 模型
-"""
-
-from enum import IntEnum
-from typing import Optional
-from sqlalchemy import Column, String, BigInteger, Integer, Text, DateTime
-from sqlalchemy.sql import func
-
-from src.models.database import Base
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class DecodeStatus(IntEnum):
-    """解码状态枚举
-    
-    对应数据库字段 status 的值:
-    - PENDING: 0 - 待执行
-    - EXECUTING: 1 - 执行中
-    - SUCCESS: 2 - 执行成功
-    - FAILED: 3 - 执行失败
-    """
-    PENDING = 0
-    EXECUTING = 1
-    SUCCESS = 2
-    FAILED = 3
-    
-    @classmethod
-    def get_description(cls, status: int) -> str:
-        """获取状态描述
-        
-        Args:
-            status: 状态值
-            
-        Returns:
-            str: 状态描述
-        """
-        descriptions = {
-            cls.PENDING: "待执行",
-            cls.EXECUTING: "执行中",
-            cls.SUCCESS: "执行成功",
-            cls.FAILED: "执行失败",
-        }
-        return descriptions.get(status, "未知状态")
-
-
-class DecodeVideo(Base):
-    """解码视频模型
-    
-    对应数据库表: decode_videos
-    
-    字段说明:
-    - video_id: 视频ID (varchar(100), nullable)
-    - task_id: 任务ID (bigint, not null)
-    - result: 解码结果 (mediumtext, nullable)
-    - decode_result_v2: 解码结果V2 (mediumtext, nullable) - ScriptWorkflowV2分支的结果
-    - search_keywords: 搜索关键词 (text, nullable) - 字符串数组的JSON格式
-    - status: 状态 (int, nullable) - 0:待执行 / 1:执行中 / 2:执行成功 / 3:执行失败
-    - error_reason: 失败原因 (mediumtext, nullable)
-    """
-    
-    __tablename__ = "decode_videos"
-    
-    # 主键:使用 task_id 作为主键(根据业务需求,可能需要调整)
-    task_id = Column(BigInteger, primary_key=True, nullable=False, comment="任务ID")
-    
-    # 视频ID
-    video_id = Column(String(100), nullable=True, index=True, comment="视频ID")
-    
-    # 解码结果(JSON 格式)
-    result = Column(Text, nullable=True, comment="解码结果")
-    
-    # 解码结果V2(分支2的结果,JSON 格式)
-    decode_result_v2 = Column(Text, nullable=True, comment="解码结果V2(ScriptWorkflowV2分支结果)")
-    
-    # 搜索关键词(字符串数组的JSON格式)
-    search_keywords = Column(Text, nullable=True, comment="搜索关键词(字符串数组的JSON格式)")
-    
-    # 状态
-    status = Column(Integer, nullable=True, default=int(DecodeStatus.PENDING), index=True, comment="状态: 0:待执行 / 1:执行中 / 2:执行成功 / 3:执行失败")
-    
-    # 失败原因
-    error_reason = Column(Text, nullable=True, comment="失败原因")
-    
-    # 时间戳字段(可选,用于记录创建和更新时间)
-    created_at = Column(DateTime, nullable=True, server_default=func.now(), comment="创建时间")
-    updated_at = Column(DateTime, nullable=True, server_default=func.now(), onupdate=func.now(), comment="更新时间")
-    
-    def __repr__(self) -> str:
-        """对象字符串表示"""
-        return f"<DecodeVideo(task_id={self.task_id}, video_id={self.video_id}, status={self.status})>"
-    
-    def to_dict(self) -> dict:
-        """转换为字典
-        
-        Returns:
-            dict: 模型数据字典
-        """
-        return {
-            "task_id": self.task_id,
-            "video_id": self.video_id,
-            "result": self.result,
-            "decode_result_v2": self.decode_result_v2,
-            "search_keywords": self.search_keywords,
-            "status": self.status,
-            "error_reason": self.error_reason,
-            "created_at": self.created_at.isoformat() if self.created_at else None,
-            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
-        }
-    
-    @classmethod
-    def create(
-        cls,
-        task_id: int,
-        video_id: Optional[str] = None,
-        status: Optional[int] = None,
-        result: Optional[str] = None,
-        decode_result_v2: Optional[str] = None,
-        error_reason: Optional[str] = None
-    ) -> "DecodeVideo":
-        """创建新的解码视频记录
-        
-        Args:
-            task_id: 任务ID
-            video_id: 视频ID
-            status: 状态(默认: PENDING)
-            result: 解码结果
-            decode_result_v2: 解码结果V2(ScriptWorkflowV2分支结果)
-            error_reason: 失败原因
-            
-        Returns:
-            DecodeVideo: 新创建的模型实例
-        """
-        # 统一将枚举转换为整型值
-        if status is None:
-            status_value = int(DecodeStatus.PENDING)
-        else:
-            try:
-                # 支持传入 IntEnum 或具体的整数
-                status_value = int(status)
-            except Exception:
-                status_value = int(DecodeStatus.PENDING)
-
-        return cls(
-            task_id=task_id,
-            video_id=video_id,
-            status=status_value,
-            created_at=func.now(),
-            result=result,
-            decode_result_v2=decode_result_v2,
-            error_reason=error_reason
-        )
-    
-    def update_status(self, status: DecodeStatus, error_reason: Optional[str] = None):
-        """更新状态
-        
-        Args:
-            status: 新状态
-            error_reason: 失败原因(仅在失败时使用)
-        """
-        self.status = status.value
-        if status == DecodeStatus.FAILED and error_reason:
-            self.error_reason = error_reason
-        elif status != DecodeStatus.FAILED:
-            self.error_reason = None
-    
-    def update_result(self, result: str):
-        """更新解码结果
-        
-        Args:
-            result: 解码结果(JSON 字符串)
-        """
-        self.result = result
-        if self.status == DecodeStatus.EXECUTING:
-            self.update_status(DecodeStatus.SUCCESS)
-    
-    def update_result_v2(self, result_v2: str):
-        """更新解码结果V2(ScriptWorkflowV2分支结果)
-        
-        Args:
-            result_v2: 解码结果V2(JSON 字符串)
-        """
-        self.decode_result_v2 = result_v2
-    
-    def update_search_keywords(self, search_keywords: str):
-        """更新搜索关键词(字符串数组的JSON格式)
-        
-        Args:
-            search_keywords: 搜索关键词(字符串数组的JSON格式,如:["关键词1", "关键词2"])
-        """
-        self.search_keywords = search_keywords

+ 0 - 156
src/models/evaluate_record.py

@@ -1,156 +0,0 @@
-"""
-EvaluateRecord 模型
-
-对应数据库表 evaluate_record 的 ORM 模型
-"""
-
-from enum import IntEnum
-from typing import Optional
-from sqlalchemy import Column, String, BigInteger, Integer, Text, DateTime
-from sqlalchemy.sql import func
-
-from src.models.database import Base
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class EvaluateStatus(IntEnum):
-    """评估状态枚举
-    
-    对应数据库字段 status 的值:
-    - PENDING: 0 - 待执行
-    - EXECUTING: 1 - 执行中
-    - SUCCESS: 2 - 执行成功
-    - FAILED: 3 - 执行失败
-    """
-    PENDING = 0
-    EXECUTING = 1
-    SUCCESS = 2
-    FAILED = 3
-    
-    @classmethod
-    def get_description(cls, status: int) -> str:
-        """获取状态描述
-        
-        Args:
-            status: 状态值
-            
-        Returns:
-            str: 状态描述
-        """
-        descriptions = {
-            cls.PENDING: "待执行",
-            cls.EXECUTING: "执行中",
-            cls.SUCCESS: "执行成功",
-            cls.FAILED: "执行失败",
-        }
-        return descriptions.get(status, "未知状态")
-
-
-class EvaluateRecord(Base):
-    """评估记录模型
-    
-    对应数据库表: evaluate_record
-    
-    字段说明:
-    - evaluate_id: 评估ID (bigint, primary key)
-    - task_id: 任务ID (bigint, nullable)
-    - evaluate_result: 评估结果 (mediumtext, nullable) - JSON格式
-    - status: 状态 (int, nullable) - 0:待执行 / 1:执行中 / 2:执行成功 / 3:执行失败
-    - created_at: 创建时间 (datetime, nullable)
-    - updated_at: 更新时间 (datetime, nullable)
-    """
-    
-    __tablename__ = "evaluate_record"
-    
-    # 主键
-    evaluate_id = Column(BigInteger, primary_key=True, nullable=False, comment="评估ID")
-    
-    # 任务ID
-    task_id = Column(BigInteger, nullable=True, index=True, comment="任务ID")
-    
-    # 评估结果(JSON 格式)
-    evaluate_result = Column(Text, nullable=True, comment="评估结果(JSON格式)")
-    
-    # 状态
-    status = Column(Integer, nullable=True, default=int(EvaluateStatus.PENDING), index=True, comment="状态: 0:待执行 / 1:执行中 / 2:执行成功 / 3:执行失败")
-    
-    # 时间戳字段
-    created_at = Column(DateTime, nullable=True, server_default=func.now(), comment="创建时间")
-    updated_at = Column(DateTime, nullable=True, server_default=func.now(), onupdate=func.now(), comment="更新时间")
-    
-    def __repr__(self) -> str:
-        """对象字符串表示"""
-        return f"<EvaluateRecord(evaluate_id={self.evaluate_id}, task_id={self.task_id}, status={self.status})>"
-    
-    def to_dict(self) -> dict:
-        """转换为字典
-        
-        Returns:
-            dict: 模型数据字典
-        """
-        return {
-            "evaluate_id": self.evaluate_id,
-            "task_id": self.task_id,
-            "evaluate_result": self.evaluate_result,
-            "status": self.status,
-            "created_at": self.created_at.isoformat() if self.created_at else None,
-            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
-        }
-    
-    @classmethod
-    def create(
-        cls,
-        evaluate_id: int,
-        task_id: Optional[int] = None,
-        status: Optional[int] = None,
-        evaluate_result: Optional[str] = None
-    ) -> "EvaluateRecord":
-        """创建新的评估记录
-        
-        Args:
-            evaluate_id: 评估ID
-            task_id: 任务ID
-            status: 状态(默认: PENDING)
-            evaluate_result: 评估结果
-            
-        Returns:
-            EvaluateRecord: 新创建的模型实例
-        """
-        # 统一将枚举转换为整型值
-        if status is None:
-            status_value = int(EvaluateStatus.PENDING)
-        else:
-            try:
-                # 支持传入 IntEnum 或具体的整数
-                status_value = int(status)
-            except Exception:
-                status_value = int(EvaluateStatus.PENDING)
-
-        return cls(
-            evaluate_id=evaluate_id,
-            task_id=task_id,
-            status=status_value,
-            created_at=func.now(),
-            evaluate_result=evaluate_result
-        )
-    
-    def update_status(self, status: EvaluateStatus):
-        """更新状态
-        
-        Args:
-            status: 新状态
-        """
-        self.status = status.value
-    
-    def update_result(self, result: str):
-        """更新评估结果
-        
-        Args:
-            result: 评估结果(JSON 字符串)
-        """
-        self.evaluate_result = result
-        if self.status == EvaluateStatus.EXECUTING:
-            self.update_status(EvaluateStatus.SUCCESS)
-

+ 0 - 11
src/states/__init__.py

@@ -1,11 +0,0 @@
-"""
-States module for workflow state definitions.
-"""
-
-from .what_deconstruction_state import WhatDeconstructionState, ElementNode, FinalOutput
-
-__all__ = [
-    "WhatDeconstructionState",
-    "ElementNode",
-    "FinalOutput"
-]

+ 0 - 57
src/states/script_state.py

@@ -1,57 +0,0 @@
-"""
-Script Workflow State Definitions.
-
-Defines the state structure for the Script workflow.
-"""
-
-from typing import TypedDict, List, Dict, Any, Optional
-
-
-class ScriptState(TypedDict, total=False):
-    """脚本理解工作流状态"""
-
-    # ========== 输入数据 ==========
-    text: Dict[str, Any]  # {title, body}
-    images: List[str]  # 图片路径列表
-    topic_selection_understanding: Optional[Dict[str, Any]]  # 选题理解结果
-    content_weight: Optional[Dict[str, Any]]  # 内容权重结果
-    inspiration_points: Optional[List[Dict[str, Any]]]  # 灵感点列表
-    purpose_points: Optional[List[Dict[str, Any]]]  # 目的点列表
-    key_points: Optional[List[Dict[str, Any]]]  # 关键点列表
-
-    # ========== 段落划分结果 ==========
-    section_division: Optional[Dict[str, Any]]  # 段落划分结果
-    内容品类: str  # 内容品类
-    段落列表: List[Dict[str, Any]]  # 段落列表
-
-    # ========== SubstanceExtractionAgent 输出(实质提取)==========
-    concrete_elements: List[Dict[str, Any]]  # 具体元素列表
-    concrete_concepts: List[Dict[str, Any]]  # 具象概念列表
-    implicit_concepts: List[Dict[str, Any]]  # 隐含概念列表
-    abstract_concepts: List[Dict[str, Any]]  # 抽象概念列表
-    substance_elements: List[Dict[str, Any]]  # 合并后的实质元素
-    substance_analyzed_result: List[Dict[str, Any]]  # Step 3: 共性分析结果
-    substance_scored_result: Dict[str, Any]  # Step 4: 评分结果
-    substance_filtered_ids: List[int]  # Step 5: 筛选的元素ID
-    substance_categorized_result: Dict[str, Any]  # Step 6: 分类结果
-    substance_final_elements: List[Dict[str, Any]]  # 实质元素最终结果
-
-    # ========== FormExtractionAgent 输出(形式提取)==========
-    concrete_concept_forms: List[Dict[str, Any]]  # 具象概念形式
-    concrete_element_forms: List[Dict[str, Any]]  # 具体元素形式
-    overall_forms: List[Dict[str, Any]]  # 整体形式
-    form_elements: List[Dict[str, Any]]  # 合并后的形式元素
-    form_analyzed_result: List[Dict[str, Any]]  # Step 4: 共性分析结果
-    form_scored_result: Dict[str, Any]  # Step 5: 支撑判断结果
-    form_weighted_result: Dict[str, Any]  # Step 6: 权重计算结果
-    form_filtered_ids: List[int]  # Step 7: 筛选的元素ID
-    form_categorized_result: Dict[str, Any]  # Step 8: 分类结果
-    form_final_elements: List[Dict[str, Any]]  # 形式元素最终结果
-
-    # ========== 分离后的元素列表 ==========
-    实质列表: List[Dict[str, Any]]  # 实质元素列表
-    形式列表: List[Dict[str, Any]]  # 形式元素列表
-    帖子倾向判断: Optional[Dict[str, Any]]  # 帖子倾向判断
-
-    # ========== 最终输出 ==========
-    final_result: Dict[str, Any]  # 最终的结果

+ 0 - 209
src/states/what_deconstruction_state.py

@@ -1,209 +0,0 @@
-"""
-What Deconstruction State Definitions.
-
-Defines the state structure for the What Deconstruction workflow.
-"""
-
-from typing import TypedDict, List, Dict, Any, Optional
-
-
-class WhatDeconstructionState(TypedDict, total=False):
-    """What解构工作流状态(仅支持视频输入)"""
-
-    # ========== 输入数据 ==========
-    video: Optional[str]  # 视频URL
-    video_id: Optional[str]  # 视频ID
-    title: Optional[str]  # 视频标题
-    body_text: Optional[str]  # 视频正文
-    text: Optional[Dict[str, Any]]  # {title, body, hashtags}(兼容格式)
-    historical_posts: Optional[List[Dict[str, Any]]]  # 历史帖子(可选)
-    history_baseline: Optional[Dict[str, Any]]  # 历史基准(可选)
-
-    # ========== 中间结果 ==========
-    video_file: Optional[Any]  # 视频文件对象(Gemini File对象,优先使用)
-    video_uploaded_uri: Optional[str]  # 视频上传后的URI(兼容旧版本)
-    video_file_name: Optional[str]  # 视频上传后的文件名称(用于获取文件对象)
-    video_upload_error: Optional[str]  # 视频上传错误信息
-    inspiration_points: Optional[Dict[str, Any]]  # 灵感点解构结果
-    purpose_point: Optional[Dict[str, Any]]  # 目的点解构结果
-    key_points: Optional[Dict[str, Any]]  # 关键点解构结果
-    topic_selection_understanding: Optional[Dict[str, Any]]  # 选题理解结果
-
-    # ========== 最终输出 ==========
-    final_result: Optional[Dict[str, Any]]  # 最终的树状JSON结果
-
-
-class ElementNode(TypedDict, total=False):
-    """元素节点结构(树状结构的节点)
-
-    根据 PRD 1.4 - 2.2.2.2 节点字段定义:
-
-    **中间元素节点包含6个字段**:
-    - id: 节点编号
-    - what: 元素概述
-    - 描述: 动态描述维度
-    - 子节点元素关系: 子节点间关系描述
-    - 元素重要性权重: 权重值
-    - 子节点元素: 子节点列表
-
-    **叶子节点包含4个字段**(不含子节点元素、子节点元素关系):
-    - id: 节点编号
-    - what: 元素概述
-    - 描述: 动态描述维度
-    - 元素重要性权重: 权重值
-    """
-
-    id: str
-    """节点编号:由数字与下划线动态组合
-
-    - 第一层元素编号:1、2、3...
-    - 第二层元素编号:1_1、2_1、3_2...
-    - 第N层元素编号:以此类推
-    - 示例:"1", "1_1", "1_1_2"
-    """
-
-    what: str
-    """元素概述
-
-    根据节点元素类型:
-    - **文本元素**(标题、正文、话题标签):what字段的值是节点文本的原文,不要对其做任何改造、总结、概括
-    - **视觉元素**(图片、图片的局部):what字段的值是一句针对该视觉元素的文本描述,要涵盖该视觉元素的核心元素和核心要点特征
-    """
-
-    描述: Dict[str, Any]
-    """动态描述维度(由知识库决定)
-
-    约束条件:
-    1. 描述维度一定要包含待解构元素最关键/最吸引人的亮点维度
-    2. 描述维度的数量不得少于3个,要尽可能多而全的覆盖这个元素的所有可描述角度
-    3. 描述维度有哪些、有多少个,需要根据待解构元素向知识库动态请求询问
-
-    Query句式:
-    刻画描述"{元素的what字段值}"核心特征的角度和维度有哪些?请尽可能不重不漏列举全。
-    """
-
-    子节点元素关系: List[str]
-    """子节点元素关系:string数组
-
-    数组中每个元素表示一条该节点所属子节点间的关联关系的文字描述。
-
-    **注意**:叶子节点不包含此字段。
-    """
-
-    元素重要性权重: float
-    """元素重要性权重:大于0且小于等于1的数值
-
-    衡量该节点的内容对读者观众的吸引力大小。帖子中越是亮点、越是吸引观众关注的元素节点,权重值越大。
-
-    **约束条件**:
-    同一父节点下的各个子节点的权重值之和应固定为1。
-
-    示例:
-    - 元素1_1下包含3个子节点1_1_1、1_1_2、1_1_3
-    - 不论这3个节点的权重值如何设定分配,3者之和应为1
-    - 如:0.2 + 0.3 + 0.5 = 1.0
-    """
-
-    权重理由: str
-    """权重理由:解释为什么给出该权重分数
-
-    说明该元素为什么具有这样的吸引力权重,基于什么维度和特征进行评估。
-    """
-
-    子节点元素: List['ElementNode']
-    """子节点元素:JSON数组
-
-    表示该视觉元素包含的子节点视觉元素,也就是树状结构的子节点元素。
-    - 子节点元素的数量是动态不固定的
-
-    **注意**:叶子节点不包含此字段。
-    """
-
-    图片链接: Optional[str]
-    """图片链接(可选):用于关联原始多媒体素材
-
-    - 对于视觉元素节点,存储图片分割后的独立图片路径
-    - 用于直接关联原始多媒体素材(PRD 2.2.1要求)
-    """
-
-    完整描述: Optional[str]
-    """完整描述(可选):图片的整体描述(仅根节点)
-
-    - 仅用于图片元素的根节点(第一层元素)
-    - 从消费者视角描述图片的核心元素
-    - 描述粒度聚焦于消费者关注的内容,避免无关细节
-    - 例如:如果帖子主题是事物处理前后对比,就不应关注托举食物的手指甲等细节
-    """
-
-
-class FinalOutput(TypedDict):
-    """最终输出的JSON结构
-
-    根据 PRD 1.4 - 2.2 系统输出定义:
-
-    输出一个JSON对象,以层级递归地、自上而下地、从宏观到微观、从整体到局部的解构一篇笔记的全部"What"元素,
-    解构形成一棵"what元素树"。输出结果为key-value组织的层级化要素集合,每个要素包含结构化的核心属性、
-    关键特征、消费者亮点和素材关联,直接关联原始多媒体素材。
-
-    输出格式:
-    {
-        "帖子整体": {
-            "描述维度1": "值1",
-            "描述维度2": "值2",
-            ...  # 动态描述维度(由知识库决定)
-        },
-        "评论分析": {
-            "亮点列表": [...],  # 消费者关注的亮点
-            "整体情感": "正面/中性/负面",
-            "关键话题": ["话题1", "话题2", ...]
-        },
-        "帖子包含元素": [...]  # 元素树的根节点列表
-    }
-    """
-
-    帖子整体: Dict[str, Any]
-    """帖子整体的各种描述维度
-
-    根据 PRD 1.4 - 2.2.2.2 要求:
-    1. 帖子总体的各种描述、总结、归纳等维度或角度,每一个维度都是一个字段项
-    2. 包括但不限于:品类、主题、脚本等维度
-    3. **一定要包含本帖子最关键/最吸引内容消费者的亮点维度**,包括但不限于:内容亮点/情绪共鸣点/创作手法等维度
-    4. 描述维度的数量不得少于3个,要尽可能多而全的覆盖这个帖子的所有可描述角度
-    5. 描述维度有哪些、有多少个,需要根据本帖子的品类、主题、关键词等信息来向知识库动态请求询问
-
-    Query句式(参见 PRD 1.4 - 3.2 环节):
-    对于一篇主题为"{帖子主题}",品类为"{帖子品类}",关键词包含"{帖子关键词列表}"的多模态社交媒体帖子,
-    从内容创作者视角进行What要素的初步识别和分类,需要使用哪些通用工具?
-    """
-
-    评论分析: Dict[str, Any]
-    """评论分析的完整信息
-
-    包含从评论区提取的消费者关注点的完整分析结果,完全保留 CommentAnalysisAgent 的输出。
-
-    结构:
-    {
-        "亮点列表": [
-            {
-                "element": "具体的帖子元素标识,格式为'位置标识(具体内容)'",
-                "reason": "为什么这个元素是亮点(基于评论的客观分析)",
-                "original_comments": ["评论原文1", "评论原文2", "评论原文3"],
-                "emotion": "情感类型(兴趣/共鸣/惊喜/认同/幽默等)",
-                "frequency": "提及频率(高/中/低)"
-            }
-        ],
-        "整体情感": "评论区的整体情感倾向(正面/中性/负面)",
-        "关键话题": ["高频话题1", "高频话题2", "高频话题3"]
-    }
-
-    说明:
-    - 亮点列表:包含消费者关注的每个亮点的完整信息(元素标识、理由、原始评论、情感类型、提及频率)
-    - 整体情感:评论区的整体情感倾向
-    - 关键话题:从评论中提取的高频话题
-    """
-
-    帖子包含元素: List[ElementNode]
-    """帖子包含的顶层元素列表(元素树的根节点列表)
-
-    包含所有一级元素节点(如封面图、其他图片、标题、正文、话题标签等),每个元素节点可递归包含子节点。
-    """

+ 0 - 17
src/utils/__init__.py

@@ -1,17 +0,0 @@
-from .logger import (
-    get_logger,
-    log_agent_start,
-    log_agent_finish,
-    log_tool_call,
-    log_validation_result,
-    log_error_with_context
-)
-
-__all__ = [
-    'get_logger',
-    'log_agent_start', 
-    'log_agent_finish',
-    'log_tool_call',
-    'log_validation_result',
-    'log_error_with_context'
-]

+ 0 - 110
src/utils/json_extractor.py

@@ -1,110 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-JSON提取和解析工具类
-
-功能: 从LLM响应中提取和解析JSON内容
-"""
-
-import json
-import re
-from typing import Any
-
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class JSONExtractor:
-    """JSON提取和解析工具类"""
-
-    @staticmethod
-    def extract_json_from_response(content: str) -> str:
-        """从LLM响应中提取JSON内容
-
-        Args:
-            content: LLM返回的原始文本内容
-
-        Returns:
-            提取出的JSON字符串
-        """
-        content = content.strip()
-        if "```json" in content:
-            content = content.split("```json")[1].split("```")[0].strip()
-        elif "```" in content:
-            content = content.split("```")[1].split("```")[0].strip()
-        return content
-
-    @staticmethod
-    def safe_parse_json(content: str, default: Any = None) -> Any:
-        """安全解析JSON,失败时返回默认值
-
-        Args:
-            content: JSON字符串
-            default: 解析失败时返回的默认值
-
-        Returns:
-            解析后的Python对象,失败时返回default
-        """
-        try:
-            return json.loads(content)
-        except json.JSONDecodeError as e:
-            # 尝试修复常见的JSON格式问题
-            try:
-                fixed_content = JSONExtractor._fix_json_format(content)
-                return json.loads(fixed_content)
-            except Exception as fix_error:
-                # 截断过长的内容,避免日志过大
-                content_preview = content[:500] if len(content) > 500 else content
-                logger.error(
-                    f"JSON解析失败: {e}\n"
-                    f"JSON修复也失败: {fix_error}\n"
-                    f"内容预览: {content_preview}{'...' if len(content) > 500 else ''}"
-                )
-                return default if default is not None else {}
-
-    @staticmethod
-    def _fix_json_format(content: str) -> str:
-        """修复常见的JSON格式问题
-
-        Args:
-            content: 可能有格式问题的JSON字符串
-
-        Returns:
-            修复后的JSON字符串
-        """
-        # 移除或替换控制字符(保留合法的 \n \t \r)
-        # 先将合法的转义字符临时替换
-        content = content.replace('\\n', '\x00NEWLINE\x00')
-        content = content.replace('\\t', '\x00TAB\x00')
-        content = content.replace('\\r', '\x00RETURN\x00')
-
-        # 移除所有ASCII控制字符(0-31,除了合法的换行、制表、回车)
-        content = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', content)
-
-        # 恢复合法的转义字符
-        content = content.replace('\x00NEWLINE\x00', '\\n')
-        content = content.replace('\x00TAB\x00', '\\t')
-        content = content.replace('\x00RETURN\x00', '\\r')
-
-        # 移除对象中的尾随逗号: ,} 或 , } 或 ,\n}
-        content = re.sub(r',\s*}', '}', content)
-
-        # 移除数组中的尾随逗号: ,] 或 , ] 或 ,\n]
-        content = re.sub(r',\s*]', ']', content)
-
-        return content
-
-    @staticmethod
-    def extract_and_parse(content: str, default: Any = None) -> Any:
-        """一步完成提取和解析
-
-        Args:
-            content: LLM返回的原始文本内容
-            default: 解析失败时返回的默认值
-
-        Returns:
-            解析后的Python对象,失败时返回default
-        """
-        json_str = JSONExtractor.extract_json_from_response(content)
-        return JSONExtractor.safe_parse_json(json_str, default)

+ 0 - 346
src/utils/llm_invoker.py

@@ -1,346 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-LLM调用封装工具类
-
-功能: 统一封装LLM调用逻辑,提供安全的调用接口
-"""
-
-import json
-import os
-import time
-from typing import Any, List, Dict, Optional
-
-from google import genai
-from dotenv import load_dotenv, find_dotenv
-
-from src.utils.logger import get_logger
-from src.utils.json_extractor import JSONExtractor
-
-logger = get_logger(__name__)
-
-# Gemini 文件处理相关常量
-FILE_PROCESS_TIMEOUT = 300  # 文件处理超时时间(秒):5分钟
-API_RETRY_INTERVAL = 5  # API重试间隔(秒)
-
-# 全局 client 实例
-_client = None
-
-
-def _get_client():
-    """获取 Gemini Client 实例"""
-    global _client
-    if _client is None:
-        load_dotenv(find_dotenv(), override=False)
-        api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
-        if not api_key:
-            raise ValueError("GEMINI_API_KEY or GOOGLE_API_KEY not found in environment")
-        _client = genai.Client(api_key=api_key)
-    return _client
-
-
-def get_video_file_from_state(state: Dict[str, Any]) -> Optional["genai.types.File"]:
-    """从 state 中获取视频文件对象
-    
-    Args:
-        state: 状态字典,可能包含:
-            - video_file: 直接存储的文件对象(优先)
-            - video_file_name: 文件名称
-            - video_uploaded_uri: 文件URI
-        
-    Returns:
-        Gemini文件对象,失败返回None
-    """
-    try:
-        # 优先检查是否直接存储了文件对象
-        video_file = state.get("video_file")
-        if video_file:
-            logger.info("从state中直接获取视频文件对象")
-            return video_file
-        
-        # 如果没有直接存储,尝试通过文件名或URI获取
-        client = _get_client()
-        
-        # 优先使用 video_file_name
-        file_name = state.get("video_file_name")
-        if file_name:
-            try:
-                logger.info(f"通过 file_name 获取文件: {file_name}")
-                return client.files.get(name=file_name)
-            except Exception as e:
-                logger.warning(f"通过 file_name 获取文件失败: {e}")
-        
-        # 如果没有 file_name,尝试从 URI 中提取
-        video_uri = state.get("video_uploaded_uri", "")
-        if video_uri:
-            # 从 URI 中提取文件名称
-            # URI 格式: https://generativelanguage.googleapis.com/v1beta/files/{file_name}
-            if "/files/" in video_uri:
-                file_name = video_uri.split("/files/")[-1]
-                try:
-                    logger.info(f"通过 URI 提取的 file_name 获取文件: {file_name}")
-                    return client.files.get(name=file_name)
-                except Exception as e:
-                    logger.warning(f"通过 URI 提取的 file_name 获取文件失败: {e}")
-        
-        logger.error("无法从 state 中获取视频文件对象")
-        return None
-    except Exception as e:
-        logger.error(f"获取视频文件对象失败: {e}", exc_info=True)
-        return None
-
-
-class LLMInvoker:
-    """LLM调用封装类"""
-
-    @staticmethod
-    def wait_for_file_processing(uploaded_file: "genai.types.File") -> Optional["genai.types.File"]:
-        """等待 Gemini 完成文件处理
-        
-        Args:
-            uploaded_file: 已上传的文件对象
-            
-        Returns:
-            处理完成后的文件对象,失败或超时返回None
-        """
-        start = time.time()
-        current = uploaded_file
-        client = _get_client()
-        
-        if not hasattr(current, 'state'):
-            return current
-        
-        state_name = getattr(current.state, 'name', None) if hasattr(current.state, 'name') else str(current.state)
-        
-        if state_name != "PROCESSING":
-            if state_name == "FAILED":
-                logger.error(f"文件处理失败: {current.state}")
-                return None
-            elif state_name in ["ACTIVE", "COMPLETE", "READY"]:
-                return current
-        
-        logger.info("等待文件处理...")
-        
-        while True:
-            elapsed = time.time() - start
-            if elapsed > FILE_PROCESS_TIMEOUT:
-                logger.error(f"文件处理超时(超过 {FILE_PROCESS_TIMEOUT} 秒)")
-                return None
-            
-            time.sleep(API_RETRY_INTERVAL)
-            
-            if hasattr(current, 'name'):
-                try:
-                    current = client.files.get(name=current.name)
-                    if not hasattr(current, 'state'):
-                        break
-                    
-                    state_name = getattr(current.state, 'name', None) if hasattr(current.state, 'name') else str(current.state)
-                    
-                    if state_name == "FAILED":
-                        logger.error(f"文件处理失败: {current.state}")
-                        return None
-                    elif state_name in ["ACTIVE", "COMPLETE", "READY"]:
-                        break
-                except Exception as e:
-                    logger.error(f"获取文件状态失败: {e}")
-                    return None
-            else:
-                return None
-        
-        logger.info("文件处理完成")
-        return current
-
-    @staticmethod
-    def upload_video_to_gemini(local_file_path: str) -> Optional["genai.types.File"]:
-        """上传视频到Gemini并等待处理完成
-        
-        Args:
-            local_file_path: 本地视频文件路径
-            
-        Returns:
-            处理完成后的文件对象,失败返回None
-        """
-        try:
-            client = _get_client()
-            logger.info(f"上传视频到Gemini: {local_file_path}")
-            
-            uploaded_file = client.files.upload(file=local_file_path)
-            
-            processed_file = LLMInvoker.wait_for_file_processing(uploaded_file)
-            if not processed_file:
-                raise Exception("文件处理失败")
-            
-            logger.info("视频上传并处理完成")
-            return processed_file
-        except Exception as e:
-            logger.error(f"上传视频到Gemini失败: {e}", exc_info=True)
-            return None
-
-    @staticmethod
-    def safe_invoke(
-        agent: Any,
-        operation_name: str,
-        messages: List[Dict[str, Any]],
-        fallback: Any = None
-    ) -> Any:
-        """安全的LLM调用
-
-        Args:
-            agent: Agent实例,必须有model属性和is_initialized属性
-            operation_name: 操作名称,用于日志记录
-            messages: LLM消息列表
-            fallback: 调用失败时返回的降级值
-
-        Returns:
-            解析后的JSON对象,失败时返回fallback
-        """
-        try:
-            if not agent.is_initialized:
-                agent.initialize()
-
-            response = agent.model.invoke(messages)
-            content = response.content.strip()
-
-            result = JSONExtractor.extract_and_parse(content, {})
-
-            if not result:
-                logger.warning(
-                    f"{operation_name} - LLM返回内容为空或解析失败\n"
-                    f"原始内容: {content}..."
-                )
-                return fallback if fallback is not None else {}
-
-            return result
-
-        except json.JSONDecodeError as e:
-            logger.error(
-                f"{operation_name} - JSON解析失败: {e}\n"
-                f"原始内容: {content if 'content' in locals() else 'N/A'}..."
-            )
-            return fallback if fallback is not None else {}
-
-        except Exception as e:
-            logger.error(f"{operation_name} - 调用失败: {e}", exc_info=True)
-            return fallback if fallback is not None else {}
-
-    @staticmethod
-    def safe_invoke_video_analysis(
-        operation_name: str,
-        video_file: "genai.types.File",
-        prompt: str,
-        agent: Any = None,
-        fallback: Any = None
-    ) -> Any:
-        """安全的视频分析LLM调用(使用新的Gemini API)
-        
-        Args:
-            operation_name: 操作名称,用于日志记录
-            video_file: Gemini文件对象(已上传并处理完成的文件)
-            prompt: 分析提示词
-            agent: Agent实例(可选,用于获取模型配置)
-            fallback: 调用失败时返回的降级值
-            
-        Returns:
-            解析后的JSON对象,失败时返回fallback
-        """
-        try:
-            client = _get_client()
-            
-            # 获取模型名称(从agent获取,或使用默认值)
-            model_name = "gemini-2.5-flash"
-            if agent is not None:
-                # 尝试从agent获取模型配置
-                if hasattr(agent, 'model') and hasattr(agent.model, 'model_name'):
-                    model_name = agent.model.model_name
-                elif hasattr(agent, 'model_provider'):
-                    # 如果agent有model_provider,使用默认模型
-                    pass
-            
-            logger.info(f"{operation_name} - 开始视频分析...")
-            
-            # 调用模型进行视频分析(参考 test.py 的方式)
-            response = client.models.generate_content(
-                model=model_name,
-                contents=[video_file, prompt]
-            )
-            
-            # 提取响应内容
-            if not response:
-                logger.warning(f"{operation_name} - 响应为空")
-                return fallback if fallback is not None else {}
-            
-            # 检查内容是否被阻止
-            if hasattr(response, 'prompt_feedback') and response.prompt_feedback:
-                if hasattr(response.prompt_feedback, 'block_reason') and response.prompt_feedback.block_reason:
-                    block_reason = str(response.prompt_feedback.block_reason)
-                    logger.error(
-                        f"{operation_name} - 内容被阻止: {block_reason}\n"
-                        f"提示词可能包含被禁止的内容,或视频内容触发了安全策略"
-                    )
-                    return fallback if fallback is not None else {}
-            
-            # 检查candidates是否为空(通常表示内容被阻止)
-            if hasattr(response, 'candidates') and response.candidates is None:
-                logger.error(
-                    f"{operation_name} - 响应candidates为空,内容可能被阻止"
-                )
-                return fallback if fallback is not None else {}
-            
-            # 获取文本内容
-            content = None
-            if hasattr(response, 'text') and response.text is not None:
-                content = response.text.strip()
-            elif hasattr(response, 'candidates') and response.candidates:
-                # 尝试从candidates中提取内容
-                for candidate in response.candidates:
-                    if hasattr(candidate, 'content') and candidate.content:
-                        if hasattr(candidate.content, 'parts'):
-                            # 提取所有parts中的文本
-                            text_parts = []
-                            for part in candidate.content.parts:
-                                if hasattr(part, 'text') and part.text:
-                                    text_parts.append(part.text)
-                            if text_parts:
-                                content = ' '.join(text_parts).strip()
-                                break
-                        elif isinstance(candidate.content, str):
-                            content = candidate.content.strip()
-                            break
-            elif hasattr(response, 'content'):
-                if isinstance(response.content, str) and response.content is not None:
-                    content = response.content.strip()
-                elif hasattr(response.content, 'text') and response.content.text is not None:
-                    content = response.content.text.strip()
-            
-            if not content:
-                logger.warning(
-                    f"{operation_name} - 响应内容为空\n"
-                    f"响应对象: {type(response).__name__}\n"
-                    f"响应属性: {[attr for attr in dir(response) if not attr.startswith('_')]}"
-                )
-                return fallback if fallback is not None else {}
-            
-            # 解析JSON
-            result = JSONExtractor.extract_and_parse(content, {})
-            
-            if not result:
-                logger.warning(
-                    f"{operation_name} - LLM返回内容为空或解析失败\n"
-                    f"原始内容: {content[:200]}..."
-                )
-                return fallback if fallback is not None else {}
-            
-            logger.info(f"{operation_name} - 视频分析完成")
-            return result
-            
-        except json.JSONDecodeError as e:
-            logger.error(
-                f"{operation_name} - JSON解析失败: {e}\n"
-                f"原始内容: {content if 'content' in locals() else 'N/A'}..."
-            )
-            return fallback if fallback is not None else {}
-            
-        except Exception as e:
-            logger.error(f"{operation_name} - 视频分析调用失败: {e}", exc_info=True)
-            return fallback if fallback is not None else {}

+ 0 - 149
src/utils/logger.py

@@ -1,149 +0,0 @@
-import json
-import logging
-import os
-from datetime import datetime
-from typing import Optional
-import sys
-
-
-class DeconstructionLogger:
-    """解构系统专用日志器"""
-
-    _instance: Optional['DeconstructionLogger'] = None
-
-    def __new__(cls):
-        if cls._instance is None:
-            cls._instance = super().__new__(cls)
-            cls._instance._initialized = False
-        return cls._instance
-
-    def __init__(self):
-        if self._initialized:
-            return
-
-        # 创建日志目录
-        log_dir = os.path.join(os.getcwd(), "logs")
-        os.makedirs(log_dir, exist_ok=True)
-
-        # 创建日志文件名
-        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-        log_file = os.path.join(log_dir, f"deconstruction_system_{timestamp}.log")
-
-        # 配置日志格式
-        formatter = logging.Formatter(
-            '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s',
-            datefmt='%Y-%m-%d %H:%M:%S'
-        )
-
-        # 创建文件处理器
-        file_handler = logging.FileHandler(log_file, encoding='utf-8')
-        file_handler.setFormatter(formatter)
-        file_handler.setLevel(logging.DEBUG)
-
-        # 创建控制台处理器
-        console_handler = logging.StreamHandler(sys.stdout)
-        console_handler.setFormatter(formatter)
-        console_handler.setLevel(logging.INFO)
-
-        # 配置根日志器
-        self.logger = logging.getLogger('DeconstructionSystem')
-        self.logger.setLevel(logging.INFO)
-        self.logger.addHandler(file_handler)
-        self.logger.addHandler(console_handler)
-
-        # 避免重复日志
-        self.logger.propagate = False
-
-        self._initialized = True
-        self.logger.info("DeconstructionLogger initialized successfully")
-
-    def get_logger(self, name: str = None) -> logging.Logger:
-        """获取指定名称的子日志器"""
-        if name:
-            return self.logger.getChild(name)
-        return self.logger
-
-    def info(self, message: str, component: str = None):
-        """记录信息级别日志"""
-        if component:
-            self.get_logger(component).info(message)
-        else:
-            self.logger.info(message)
-
-    def warning(self, message: str, component: str = None):
-        """记录警告级别日志"""
-        if component:
-            self.get_logger(component).warning(message)
-        else:
-            self.logger.warning(message)
-
-    def error(self, message: str, component: str = None, exc_info=False):
-        """记录错误级别日志"""
-        if component:
-            self.get_logger(component).error(message, exc_info=exc_info)
-        else:
-            self.logger.error(message, exc_info=exc_info)
-
-    def debug(self, message: str, component: str = None):
-        """记录调试级别日志"""
-        if component:
-            self.get_logger(component).debug(message)
-        else:
-            self.logger.debug(message)
-
-
-# 全局日志器实例
-logger_instance = DeconstructionLogger()
-
-
-def get_logger(component: str = None) -> logging.Logger:
-    """获取日志器的便捷函数"""
-    return logger_instance.get_logger(component)
-
-
-def log_agent_start(agent_name: str, input_data: dict = None):
-    """记录Agent开始执行"""
-    message = f"Agent {agent_name} started execution"
-    if input_data:
-        message += f" with input keys: {list(input_data.keys())}"
-    logger_instance.info(message, f"Agent.{agent_name}")
-    # if input_data is dict:
-    #     logger_instance.info(json.dumps(input_data, ensure_ascii=False, indent=4), f"Agent.{agent_name}")
-    # else:
-    #     logger_instance.info(str(input_data), f"Agent.{agent_name}")
-
-
-def log_agent_finish(agent_name: str, output_data: dict = None):
-    """记录Agent完成执行"""
-    message = f"Agent {agent_name} finished execution"
-    if output_data:
-        message += f" with output keys: {list(output_data.keys())}"
-    logger_instance.info(message, f"Agent.{agent_name}")
-    logger_instance.info(json.dumps(output_data, ensure_ascii=False, indent=4), f"Agent.{agent_name}")
-
-
-def log_tool_call(tool_name: str, parameters: dict = None):
-    """记录工具调用"""
-    message = f"Tool {tool_name} called"
-    if parameters:
-        message += f" with parameters: {list(parameters.keys())}"
-    logger_instance.info(message, f"Tool.{tool_name}")
-
-
-def log_validation_result(validation_type: str, passed: bool, details: str = None):
-    """记录验证结果"""
-    status = "PASSED" if passed else "FAILED"
-    message = f"Validation {validation_type} {status}"
-    if details:
-        message += f": {details}"
-
-    if passed:
-        logger_instance.info(message, "Validation")
-    else:
-        logger_instance.warning(message, "Validation")
-
-
-def log_error_with_context(error: Exception, context: str, component: str = None):
-    """记录带上下文的错误"""
-    message = f"Error in {context}: {str(error)}"
-    logger_instance.error(message, component, exc_info=True)

+ 0 - 21
src/utils/utils.py

@@ -1,21 +0,0 @@
-def remove_duplicates(input_list):
-    """
-    Remove duplicates from a list while preserving the order of elements.
-
-    :param input_list: List of elements that may contain duplicates
-    :return: List of elements with duplicates removed
-    """
-    seen = set()
-    result = []
-    for item in input_list:
-        if item not in seen:
-            seen.add(item)
-            result.append(item)
-    return result
-
-
-def oss_resize_image(url):
-    if "res.cybertogether.net" in url and "x-oss-process" not in url:
-        return f"{url}?x-oss-process=image/resize,h_300"
-
-    return url

+ 0 - 10
src/workflows/__init__.py

@@ -1,10 +0,0 @@
-try:
-    from src.workflows.default.main_workflow import create_deconstruction_workflow, DeconstructionWorkflow
-    __all__ = ['create_deconstruction_workflow', 'DeconstructionWorkflow']
-except ImportError:
-    # main_workflow.py 不存在,使用递归解构工作流
-    try:
-        from src.workflows.default.recursive_deconstruction_workflow import RecursiveDeconstructionWorkflow
-        __all__ = ['RecursiveDeconstructionWorkflow']
-    except ImportError:
-        __all__ = []

+ 0 - 1499
src/workflows/decode_workflow.py

@@ -1,1499 +0,0 @@
-"""
-Decode Workflow.
-
-解码工作流:合并 What 解构工作流和脚本理解工作流的完整流程。
-流程:初始化数据库记录 → 视频上传 → 灵感点提取 → 目的点提取 → 关键点提取 → 选题理解 → 
-      段落划分 → 实质提取 → 形式提取 → 分离结果 → 结果汇总
-"""
-
-from typing import Dict, Any
-from langgraph.graph import StateGraph, END
-
-from src.components.agents.base import BaseGraphAgent
-from src.components.agents.topic_selection_understanding_agent import TopicSelectionUnderstandingAgent
-from src.components.functions.result_aggregation_function import ResultAggregationFunction
-from src.components.functions.video_upload_function import VideoUploadFunction
-# What解构相关Agent
-from src.components.agents.inspiration_points_agent import InspirationPointsAgent
-from src.components.agents.purpose_point_agent import PurposePointAgent
-from src.components.agents.key_points_agent import KeyPointsAgent
-# 脚本理解相关Agent
-from src.components.agents.script_section_division_agent import ScriptSectionDivisionAgent
-from src.components.agents.script_substance_extraction_agent import ScriptSubstanceExtractionAgent
-from src.components.agents.script_form_extraction_agent import ScriptFormExtractionAgent
-# ScriptWorkflowV2 相关Agent
-from src.components.agents.structure_agent import StructureAgent
-from src.components.agents.content_unit_split_agent import ContentUnitSplitAgent
-from src.components.agents.content_unit_understand import ContentUnitUnderstandAgent
-from src.components.agents.script_keyword_agent import ScriptKeywordAgent
-# 搜索关键词Agent
-from src.components.agents.search_keyword_agent import SearchKeywordAgent
-from src.models import get_db, DecodeVideo, DecodeStatus
-from src.utils.logger import get_logger
-from utils.general import get_now_ts
-
-logger = get_logger(__name__)
-
-
-class DecodeWorkflow(BaseGraphAgent):
-    """解码工作流(合并 What 解构和脚本理解)
-
-    功能:
-    - 编排完整的解码流程(视频分析)
-    - 流程:初始化数据库记录 → 视频上传 → 灵感点提取 → 目的点提取 → 关键点提取 → 选题理解 → 
-           段落划分 → 实质提取 → 形式提取 → 分离结果 → 结果汇总
-    - 管理状态传递
-    - 仅支持单视频输入
-
-    实现方式:BaseGraphAgent (LangGraph)
-    """
-
-    def __init__(
-        self,
-        name: str = "decode_workflow",
-        description: str = "解码工作流(合并 What 解构和脚本理解)",
-        model_provider: str = "google_genai",
-        max_depth: int = 10
-    ):
-        super().__init__(
-            name=name,
-            description=description,
-            state_class=dict
-        )
-
-        self.max_depth = max_depth
-        self.model_provider = model_provider
-
-        # 初始化视频上传Function
-        self.video_upload_func = VideoUploadFunction()
-
-        # 初始化What解构相关Agent
-        self.inspiration_points_agent = InspirationPointsAgent(
-            model_provider=model_provider
-        )
-        self.purpose_point_agent = PurposePointAgent(
-            model_provider=model_provider
-        )
-        self.key_points_agent = KeyPointsAgent(
-            model_provider=model_provider
-        )
-        self.topic_selection_understanding_agent = TopicSelectionUnderstandingAgent(
-            model_provider=model_provider
-        )
-
-        # 初始化脚本理解相关Agent
-        self.section_agent = ScriptSectionDivisionAgent(
-            model_provider=model_provider
-        )
-        self.substance_agent = ScriptSubstanceExtractionAgent(
-            model_provider=model_provider
-        )
-        self.form_agent = ScriptFormExtractionAgent(
-            model_provider=model_provider
-        )
-
-        # 初始化 ScriptWorkflowV2 相关Agent
-        self.structure_agent = StructureAgent(
-            model_provider=model_provider
-        )
-        self.content_unit_split_agent = ContentUnitSplitAgent(
-            model_provider=model_provider
-        )
-        self.content_unit_understand_agent = ContentUnitUnderstandAgent(
-            model_provider=model_provider
-        )
-        self.script_keyword_agent = ScriptKeywordAgent(
-            model_provider=model_provider
-        )
-
-        # 初始化搜索关键词Agent
-        self.search_keyword_agent = SearchKeywordAgent(
-            model_provider=model_provider
-        )
-
-        # 初始化结果汇总Function
-        self.result_aggregation_func = ResultAggregationFunction()
-
-        logger.info(f"DecodeWorkflow 初始化完成")
-
-    def _build_graph(self) -> StateGraph:
-        """构建工作流图
-
-        完整流程(并行分支):
-        START → 初始化数据库记录 → 视频上传 → [并行分支] → 合并结果 → 结果汇总 → END
-        分支1:灵感点提取 → 目的点提取 → 关键点提取 → 选题理解 → 段落划分 → 实质提取 → 形式提取 → 分离结果
-        分支2:结构化分析 → L3单元拆分 → 整体理解 → 金句提取
-        """
-        workflow = StateGraph(dict)  # 使用dict作为状态类型
-
-        # 添加所有节点
-        workflow.add_node("init_db_record", self._init_db_record_node)
-        workflow.add_node("video_upload", self._video_upload_node)
-        # 分叉节点:用于启动两个并行分支
-        workflow.add_node("fork_branches", self._fork_branches_node)
-        
-        # 分支1:原有 decode_workflow 流程
-        # What解构节点
-        workflow.add_node("inspiration_points_extraction", self._inspiration_points_node)
-        workflow.add_node("purpose_point_extraction", self._purpose_point_node)
-        workflow.add_node("key_points_extraction", self._key_points_node)
-        workflow.add_node("topic_selection_understanding", self._topic_selection_understanding_node)
-        # 脚本理解节点
-        workflow.add_node("section_division", self._section_division_node)
-        workflow.add_node("substance_extraction", self._substance_extraction_node)
-        workflow.add_node("form_extraction", self._form_extraction_node)
-        workflow.add_node("merge_all_results", self._merge_all_results_node)
-        # 搜索关键词提取节点(分支1的最后一步)
-        workflow.add_node("search_keywords_extraction", self._search_keywords_extraction_node)
-        
-        # 分支2:ScriptWorkflowV2 流程
-        workflow.add_node("structure_analysis", self._structure_analysis_node)
-        workflow.add_node("content_unit_split", self._content_unit_split_node)
-        workflow.add_node("content_unit_understand", self._content_unit_understand_node)
-        workflow.add_node("keyword_extraction", self._keyword_extraction_node)
-        
-        # 合并节点和结果汇总
-        workflow.add_node("merge_branches", self._merge_branches_node)
-        workflow.add_node("result_aggregation", self._result_aggregation_node)
-
-        # 定义流程的边
-        workflow.set_entry_point("init_db_record")
-        # 数据库记录初始化后进入视频上传
-        workflow.add_edge("init_db_record", "video_upload")
-        # 视频上传后使用条件边:成功则进入分叉节点,失败则终止
-        workflow.add_conditional_edges(
-            "video_upload",
-            self._check_video_upload_success,
-            {
-                "success": "fork_branches",
-                "failure": END
-            }
-        )
-        
-        # 分叉节点:同时启动两个分支
-        # 注意:LangGraph 不支持从一个节点直接连接到多个节点
-        # 所以我们使用一个技巧:分叉节点连接到分支1,同时通过条件边也连接到分支2
-        # 但实际上,我们需要使用一个不同的方法
-        # 使用条件边,总是返回两个目标(通过返回一个列表或使用特殊的路由逻辑)
-        # 但 LangGraph 的条件边只能返回一个字符串目标
-        
-        # 解决方案:分叉节点连接到分支1,分支1的第一个节点执行后,也触发分支2
-        # 或者:使用一个"并行启动"节点,它内部调用两个分支的入口
-        # 实际上,最简单的方法是:分叉节点连接到分支1,然后在分支1的第一个节点中,也启动分支2
-        
-        # 更好的方法:分叉节点使用条件边,根据一个标志来决定路由
-        # 但我们需要确保两个分支都能被启动
-        
-        # 实际可行的方案:分叉节点连接到分支1,然后在状态中设置一个标志
-        # 在分支1的第一个节点中检查这个标志,如果分支2还没启动,则启动它
-        # 但这需要修改节点逻辑,比较复杂
-        
-        # 最简单的方案:分叉节点总是连接到分支1,然后在分支1的第一个节点中
-        # 检查并启动分支2(通过修改状态,让工作流能够同时执行两个分支)
-        # 但这在 LangGraph 中也不容易实现
-        
-        # 实际上,在 LangGraph 中实现真正的并行执行比较困难
-        # 我们可以使用一个变通方法:分叉节点连接到分支1,分支1的第一个节点执行后
-        # 通过修改状态来标记需要执行分支2,然后在适当的时候执行分支2
-        
-        # 但更简单的方法是:让分叉节点连接到分支1,然后在状态中设置一个标志
-        # 在合并节点中检查,如果分支2还没执行,则执行它(但这会变成串行)
-        
-        # 最佳方案:使用 LangGraph 的 add_edge 多次(但这不支持)
-        # 或者:创建一个"并行执行"节点,它内部调用两个分支的入口节点
-        
-        # 让我使用一个实用的方案:分叉节点连接到分支1,分支1的第一个节点执行时
-        # 也检查并启动分支2(通过异步或线程,但这在 LangGraph 中不容易实现)
-        
-        # 实际上,在 LangGraph 中,我们可以使用一个技巧:
-        # 分叉节点连接到分支1,然后在状态中设置一个标志
-        # 在合并节点中,如果分支2还没完成,则执行分支2的节点(但这会变成串行)
-        
-        # 让我采用一个更实用的方案:
-        # 1. 分叉节点连接到分支1
-        # 2. 分叉节点也通过条件边连接到分支2(使用一个总是返回"start_branch2"的条件函数)
-        # 但条件边只能有一个返回值
-        
-        # 最终方案:使用一个"并行启动"节点,它内部顺序调用两个分支的入口
-        # 虽然这不是真正的并行,但在实际执行中,由于每个节点都是独立的,可以认为是并行的
-        
-        # 或者:分叉节点连接到分支1,分支1完成后检查分支2是否完成,如果没完成则执行分支2
-        # 但这会变成串行执行
-        
-        # 让我采用一个折中方案:分叉节点连接到分支1,同时在状态中标记需要执行分支2
-        # 在分支1的某个节点中,检查并执行分支2(但这需要修改节点逻辑)
-        
-        # 实际上,在 LangGraph 中,最简单的方法是:
-        # 分叉节点连接到分支1,分支1完成后,如果分支2还没执行,则执行分支2
-        # 虽然这不是真正的并行,但在实际应用中,由于节点执行时间不同,可以认为是近似并行的
-        
-        # 分叉节点:连接到分支1,分支1的第一个节点会同时启动分支2
-        workflow.add_edge("fork_branches", "inspiration_points_extraction")  # 启动分支1
-        
-        # 分支1:原有 decode_workflow 流程
-        # What解构流程 - 在关键节点后添加错误检查
-        workflow.add_conditional_edges(
-            "inspiration_points_extraction",
-            self._check_workflow_status,
-            {
-                "continue": "purpose_point_extraction",
-                "terminate": END
-            }
-        )
-        workflow.add_conditional_edges(
-            "purpose_point_extraction",
-            self._check_workflow_status,
-            {
-                "continue": "key_points_extraction",
-                "terminate": END
-            }
-        )
-        workflow.add_conditional_edges(
-            "key_points_extraction",
-            self._check_workflow_status,
-            {
-                "continue": "topic_selection_understanding",
-                "terminate": END
-            }
-        )
-        workflow.add_conditional_edges(
-            "topic_selection_understanding",
-            self._check_workflow_status,
-            {
-                "continue": "section_division",
-                "terminate": END
-            }
-        )
-        # 脚本理解流程
-        workflow.add_conditional_edges(
-            "section_division",
-            self._check_workflow_status,
-            {
-                "continue": "substance_extraction",
-                "terminate": END
-            }
-        )
-        workflow.add_conditional_edges(
-            "substance_extraction",
-            self._check_workflow_status,
-            {
-                "continue": "form_extraction",
-                "terminate": END
-            }
-        )
-        workflow.add_conditional_edges(
-            "form_extraction",
-            self._check_workflow_status,
-            {
-                "continue": "merge_all_results",
-                "terminate": END
-            }
-        )
-        workflow.add_conditional_edges(
-            "merge_all_results",
-            self._check_workflow_status,
-            {
-                "continue": "search_keywords_extraction",
-                "terminate": END
-            }
-        )
-        workflow.add_conditional_edges(
-            "search_keywords_extraction",
-            self._check_workflow_status,
-            {
-                "continue": "merge_branches",
-                "terminate": END
-            }
-        )
-        
-        # 分支2:ScriptWorkflowV2 流程
-        # 注意:分支2的节点在分支1的第一个节点中直接执行,不通过图的边连接
-        # 这些节点保留在图中,但实际执行是在分支1的第一个节点中触发的
-        
-        # 合并节点后进入结果汇总
-        workflow.add_edge("merge_branches", "result_aggregation")
-        workflow.add_edge("result_aggregation", END)
-
-        logger.info("工作流图构建完成 - 完整流程:初始化数据库记录 → 视频上传 → [并行分支] → 合并结果 → 结果汇总")
-        logger.info("  分支1:灵感点提取 → 目的点提取 → 关键点提取 → 选题理解 → 段落划分 → 实质提取 → 形式提取 → 分离结果 → 搜索关键词提取")
-        logger.info("  分支2:结构化分析 → L3单元拆分 → 整体理解 → 金句提取")
-
-        return workflow
-
-    def _check_video_upload_success(self, state: Dict[str, Any]) -> str:
-        """检查视频上传是否成功
-        
-        Returns:
-            "success" 如果上传成功,否则返回 "failure"
-        """
-        video_uri = state.get("video_uploaded_uri")
-        video_error = state.get("video_upload_error")
-        
-        # 如果URI存在且没有错误,则认为成功
-        if video_uri and not video_error:
-            logger.info("视频上传成功,继续执行后续流程")
-            return "success"
-        else:
-            error_msg = video_error or "视频上传失败:未获取到视频URI"
-            logger.error(f"视频上传失败,终止workflow: {error_msg}")
-            # 设置失败信息到状态中
-            state["workflow_failed"] = True
-            state["workflow_error"] = error_msg
-            # 更新数据库记录为失败状态
-            self._update_db_record_after_workflow(state, success=False, error_msg=error_msg)
-            return "failure"
-    
-
-    def _check_critical_error(self, state: Dict[str, Any], error_source: str = "") -> bool:
-        """检查关键错误,如果存在则设置失败标志
-        
-        Args:
-            state: 状态字典
-            error_source: 错误来源(用于日志)
-            
-        Returns:
-            True 如果存在致命错误,False 否则
-        """
-        # 检查是否已经失败
-        if state.get("workflow_failed"):
-            return True
-        
-        # 检查视频文件是否可用
-        from src.utils.llm_invoker import get_video_file_from_state
-        video_file = get_video_file_from_state(state)
-        if not video_file:
-            error_msg = f"无法获取视频文件对象{('(' + error_source + ')' if error_source else '')}"
-            logger.error(f"{error_msg},终止workflow")
-            state["workflow_failed"] = True
-            state["workflow_error"] = error_msg
-            return True
-        
-        # 检查视频URI是否存在
-        video_uri = state.get("video_uploaded_uri")
-        if not video_uri:
-            video_error = state.get("video_upload_error", "未知错误")
-            error_msg = f"视频URI不存在{('(' + error_source + ')' if error_source else '')}:{video_error}"
-            logger.error(f"{error_msg},终止workflow")
-            state["workflow_failed"] = True
-            state["workflow_error"] = error_msg
-            return True
-        
-        return False
-
-    def _check_agent_result_for_errors(self, result: Dict[str, Any], agent_name: str) -> bool:
-        """检查Agent返回结果中是否包含关键错误
-        
-        Args:
-            result: Agent返回的结果字典
-            agent_name: Agent名称(用于日志)
-            
-        Returns:
-            True 如果存在关键错误,False 否则
-        """
-        if not isinstance(result, dict):
-            return False
-        
-        # 检查常见的错误字段
-        error_fields = ["error", "错误", "video_upload_error"]
-        for field in error_fields:
-            error_value = result.get(field)
-            if error_value:
-                # 检查是否是关键错误(无法获取视频文件等)
-                error_str = str(error_value)
-                if "无法获取视频文件" in error_str or "无法获取视频文件对象" in error_str:
-                    logger.error(f"{agent_name}返回关键错误: {error_str}")
-                    return True
-        
-        # 检查metadata中的错误
-        metadata = result.get("metadata", {})
-        if isinstance(metadata, dict):
-            error_value = metadata.get("error")
-            if error_value:
-                error_str = str(error_value)
-                if "无法获取视频文件" in error_str or "无法获取视频文件对象" in error_str or "未找到视频URI" in error_str:
-                    logger.error(f"{agent_name}返回关键错误: {error_str}")
-                    return True
-        
-        return False
-
-    def _check_workflow_status(self, state: Dict[str, Any]) -> str:
-        """检查workflow状态,用于条件边
-        
-        Returns:
-            "continue" 如果继续执行,否则返回 "terminate"
-        """
-        if state.get("workflow_failed"):
-            return "terminate"
-        return "continue"
-
-    def _video_upload_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:视频上传(第一步)- 下载视频并上传至Gemini"""
-        logger.info("=== 执行节点:视频上传 ===")
-
-        try:
-            # 初始化Function
-            if not self.video_upload_func.is_initialized:
-                self.video_upload_func.initialize()
-
-            # 执行视频上传
-            result = self.video_upload_func.execute(state)
-
-            # 更新状态
-            state.update(result)
-
-            video_uri = result.get("video_uploaded_uri")
-            if video_uri:
-                logger.info(f"视频上传完成 - URI: {video_uri}")
-            else:
-                error = result.get("video_upload_error", "未知错误")
-                logger.error(f"视频上传失败: {error}")
-                # 确保失败信息被设置
-                state.update({
-                    "video_uploaded_uri": None,
-                    "video_upload_error": error
-                })
-
-        except Exception as e:
-            logger.error(f"视频上传失败: {e}", exc_info=True)
-            state.update({
-                "video_uploaded_uri": None,
-                "video_upload_error": str(e)
-            })
-
-        return state
-
-    def _init_db_record_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:初始化数据库记录
-        
-        根据 video_id 查询 decode_videos 表:
-        - 如果存在记录,则不新建,使用现有记录
-        - 如果不存在,则新建记录(状态为 EXECUTING)
-        """
-        logger.info("=== 执行节点:初始化数据库记录 ===")
-        
-        try:
-            video_id = state.get("video_id", "")
-            task_id = state.get("task_id")
-            
-            if not task_id:
-                logger.warning("未提供 task_id,跳过数据库记录初始化")
-                return state
-            
-            db = next(get_db())
-            try:
-                # 根据 video_id 查询是否已有记录
-                existing_record = db.query(DecodeVideo).filter_by(task_id=task_id).first()
-                
-                if existing_record:
-                    # 如果存在记录,使用现有的 task_id
-                    logger.info(f"找到已存在的数据库记录: task_id={existing_record.task_id}, video_id={video_id}")
-                    state["db_task_id"] = existing_record.task_id
-                    state["db_record_exists"] = True
-                    # 更新状态为执行中
-                    existing_record.update_status(DecodeStatus.EXECUTING)
-                    db.commit()
-                else:
-                    # 如果不存在,创建新记录
-                    # 如果没有提供 task_id,使用 video_id 的 hash 值作为 task_id
-                    if not task_id:
-                        import hashlib
-                        task_id = int(hashlib.md5(video_id.encode()).hexdigest()[:15], 16) % (10 ** 15)
-                        logger.info(f"未提供 task_id,自动生成: {task_id}")
-                    
-                    new_record = DecodeVideo.create(
-                        task_id=task_id,
-                        video_id=video_id,
-                        status=DecodeStatus.EXECUTING
-                    )
-                    db.add(new_record)
-                    db.commit()
-                    logger.info(f"创建新的数据库记录: task_id={task_id}, video_id={video_id}")
-                    state["db_task_id"] = task_id
-                    state["db_record_exists"] = False
-                    
-            except Exception as e:
-                logger.error(f"数据库操作失败: {e}", exc_info=True)
-                db.rollback()
-                # 数据库操作失败不影响 workflow 继续执行
-            finally:
-                db.close()
-                
-        except Exception as e:
-            logger.error(f"初始化数据库记录节点执行失败: {e}", exc_info=True)
-            # 数据库操作失败不影响 workflow 继续执行
-        
-        return state
-
-    def _fork_branches_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:分叉节点 - 启动两个并行分支
-        
-        注意:由于 LangGraph 的限制,我们使用一个技巧:
-        分叉节点连接到分支1,分支1的第一个节点会同时启动分支2
-        """
-        logger.info("=== 执行节点:分叉节点(启动并行分支) ===")
-        
-        # 标记需要启动分支2
-        state["start_branch2"] = True
-        
-        return state
-
-    def _inspiration_points_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:灵感点提取(What解构)
-        
-        同时启动分支2(如果还没启动)
-        """
-        logger.info("=== 执行节点:灵感点提取 ===")
-
-        # 检查是否需要启动分支2
-        if state.get("start_branch2") and not state.get("branch2_started"):
-            logger.info("在分支1的第一个节点中启动分支2(ScriptWorkflowV2流程)")
-            state["branch2_started"] = True
-            # 启动分支2:执行结构化分析
-            try:
-                if not self.structure_agent.is_initialized:
-                    self.structure_agent.initialize()
-                structure_result = self.structure_agent.process(state)
-                state["topic"] = structure_result.get("structure_data", {})
-                
-                # 继续执行分支2的后续节点
-                if not self.content_unit_split_agent.is_initialized:
-                    self.content_unit_split_agent.initialize()
-                split_result = self.content_unit_split_agent.process(state)
-                state.update(split_result)
-                
-                if not self.content_unit_understand_agent.is_initialized:
-                    self.content_unit_understand_agent.initialize()
-                understand_result = self.content_unit_understand_agent.process(state)
-                state.update(understand_result)
-                
-                if not self.script_keyword_agent.is_initialized:
-                    self.script_keyword_agent.initialize()
-                keyword_result = self.script_keyword_agent.process(state)
-                state.update(keyword_result)
-                
-                state["branch2_completed"] = True
-                logger.info("分支2(ScriptWorkflowV2流程)执行完成")
-            except Exception as e:
-                logger.error(f"分支2执行失败: {e}", exc_info=True)
-                state["branch2_completed"] = True  # 即使失败也标记为完成,避免阻塞
-
-        # 检查关键错误
-        if self._check_critical_error(state, "灵感点提取"):
-            return state
-
-        try:
-            # 初始化Agent
-            if not self.inspiration_points_agent.is_initialized:
-                self.inspiration_points_agent.initialize()
-
-            # 执行Agent
-            result = self.inspiration_points_agent.process(state)
-
-            # 更新状态
-            state.update(result)
-
-            # 检查Agent返回结果中是否包含关键错误
-            if self._check_agent_result_for_errors(result, "灵感点提取Agent"):
-                error_msg = "灵感点提取失败:无法获取视频文件"
-                state["workflow_failed"] = True
-                state["workflow_error"] = error_msg
-                return state
-
-            # 安全地获取灵感点数量:total_count 在 metadata 中
-            if isinstance(result, dict):
-                metadata = result.get("metadata", {})
-                inspiration_count = metadata.get("total_count", 0) if isinstance(metadata, dict) else 0
-                # 如果 metadata 中没有,尝试从 inspiration_points 列表长度获取
-                if inspiration_count == 0:
-                    inspiration_points = result.get("inspiration_points", [])
-                    if isinstance(inspiration_points, list):
-                        inspiration_count = len(inspiration_points)
-            else:
-                # 如果 result 不是 dict(比如是列表),尝试获取长度
-                inspiration_count = len(result) if isinstance(result, list) else 0
-            
-            logger.info(f"灵感点提取完成 - 共 {inspiration_count} 个灵感点")
-
-        except Exception as e:
-            logger.error(f"灵感点提取失败: {e}", exc_info=True)
-            state["workflow_failed"] = True
-            state["workflow_error"] = f"灵感点提取异常: {str(e)}"
-            state.update({
-                "inspiration_points": {
-                    "error": str(e),
-                    "points": [],
-                    "total_count": 0
-                }
-            })
-
-        return state
-
-    def _purpose_point_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:目的点提取(What解构)"""
-        logger.info("=== 执行节点:目的点提取 ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "目的点提取"):
-            return state
-
-        try:
-            # 初始化Agent
-            if not self.purpose_point_agent.is_initialized:
-                self.purpose_point_agent.initialize()
-
-            # 执行Agent
-            result = self.purpose_point_agent.process(state)
-
-            # 更新状态
-            state.update(result)
-
-            # 检查Agent返回结果中是否包含关键错误
-            if self._check_agent_result_for_errors(result, "目的点提取Agent"):
-                error_msg = "目的点提取失败:无法获取视频文件"
-                state["workflow_failed"] = True
-                state["workflow_error"] = error_msg
-                return state
-
-            main_purpose = result.get("purpose_point", {}).get("main_purpose", "未知")
-            logger.info(f"目的点提取完成 - 主要目的: {main_purpose}")
-
-        except Exception as e:
-            logger.error(f"目的点提取失败: {e}", exc_info=True)
-            state["workflow_failed"] = True
-            state["workflow_error"] = f"目的点提取异常: {str(e)}"
-            state.update({
-                "purpose_point": {
-                    "error": str(e),
-                    "main_purpose": "未知"
-                }
-            })
-
-        return state
-
-    def _key_points_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:关键点提取(What解构)"""
-        logger.info("=== 执行节点:关键点提取 ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "关键点提取"):
-            return state
-
-        try:
-            # 初始化Agent
-            if not self.key_points_agent.is_initialized:
-                self.key_points_agent.initialize()
-
-            # 执行Agent
-            result = self.key_points_agent.process(state)
-
-            # 更新状态
-            state.update(result)
-
-            # 检查Agent返回结果中是否包含关键错误
-            if self._check_agent_result_for_errors(result, "关键点提取Agent"):
-                error_msg = "关键点提取失败:无法获取视频文件"
-                state["workflow_failed"] = True
-                state["workflow_error"] = error_msg
-                return state
-
-            total_key_points = result.get("key_points", {}).get("total_count", 0)
-            logger.info(f"关键点提取完成 - 共 {total_key_points} 个关键点")
-
-        except Exception as e:
-            logger.error(f"关键点提取失败: {e}", exc_info=True)
-            state["workflow_failed"] = True
-            state["workflow_error"] = f"关键点提取异常: {str(e)}"
-            state.update({
-                "key_points": {
-                    "error": str(e),
-                    "creator_perspective": {"key_points": [], "summary": ""},
-                    "consumer_perspective": {"key_points": [], "summary": ""}
-                }
-            })
-
-        return state
-
-    def _topic_selection_understanding_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:选题理解(What解构)"""
-        logger.info("=== 执行节点:选题理解 ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "选题理解"):
-            return state
-
-        try:
-            # 初始化Agent
-            if not self.topic_selection_understanding_agent.is_initialized:
-                self.topic_selection_understanding_agent.initialize()
-
-            # 执行Agent
-            result = self.topic_selection_understanding_agent.process(state)
-
-            # 更新状态
-            state.update(result)
-
-            # 检查Agent返回结果中是否包含关键错误
-            if self._check_agent_result_for_errors(result, "选题理解Agent"):
-                error_msg = "选题理解失败:无法获取视频文件"
-                state["workflow_failed"] = True
-                state["workflow_error"] = error_msg
-                return state
-
-            logger.info(f"选题理解完成 - result: {result}")
-
-        except Exception as e:
-            logger.error(f"选题理解失败: {e}", exc_info=True)
-            state["workflow_failed"] = True
-            state["workflow_error"] = f"选题理解异常: {str(e)}"
-            state.update({
-                "topic_selection_understanding": {
-                    "错误": str(e)
-                }
-            })
-
-        return state
-
-    def _section_division_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:脚本段落划分(脚本理解)"""
-        logger.info("=== 执行节点:脚本段落划分 ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "脚本段落划分"):
-            return state
-
-        try:
-            # 初始化Agent
-            if not self.section_agent.is_initialized:
-                self.section_agent.initialize()
-
-            # 执行Agent
-            result = self.section_agent.process(state)
-
-            # 更新状态
-            state.update(result)
-
-            # 检查Agent返回结果中是否包含关键错误(段落划分如果没有视频文件会返回空结果,不算致命错误)
-            sections = result.get("段落列表", [])
-            content_category = result.get("内容品类", "未知")
-            logger.info(f"脚本段落划分完成 - 内容品类: {content_category}, 段落数: {len(sections)}")
-
-        except Exception as e:
-            logger.error(f"脚本段落划分失败: {e}", exc_info=True)
-            state["workflow_failed"] = True
-            state["workflow_error"] = f"脚本段落划分异常: {str(e)}"
-            state.update({
-                "内容品类": "未知品类",
-                "段落列表": []
-            })
-
-        return state
-
-    def _substance_extraction_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:实质元素提取(脚本理解)"""
-        logger.info("=== 执行节点:实质元素提取 ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "实质元素提取"):
-            return state
-
-        try:
-            # 初始化Agent
-            if not self.substance_agent.is_initialized:
-                self.substance_agent.initialize()
-
-            # 准备状态:将段落列表包装到section_division字段中
-            sections = state.get("段落列表", [])
-            state["section_division"] = {"段落列表": sections}
-
-            # 验证三点解构信息是否可用
-            inspiration_points = state.get("inspiration_points", [])
-            purpose_point = state.get("purpose_point", {})
-            key_points = state.get("key_points", {})
-            
-            # 统计三点解构信息
-            inspiration_count = len(inspiration_points) if isinstance(inspiration_points, list) else 0
-            if not inspiration_count and isinstance(inspiration_points, dict):
-                inspiration_count = len(inspiration_points.get("points", []))
-            
-            purpose_count = 0
-            if isinstance(purpose_point, dict):
-                purpose_count = len(purpose_point.get("purposes", []))
-            elif isinstance(purpose_point, list):
-                purpose_count = len(purpose_point)
-            
-            key_points_count = 0
-            if isinstance(key_points, dict):
-                key_points_list = key_points.get("key_points", [])
-                key_points_count = len(key_points_list) if isinstance(key_points_list, list) else 0
-            elif isinstance(key_points, list):
-                key_points_count = len(key_points)
-            
-            logger.info(
-                f"实质提取节点 - 三点解构信息检查: "
-                f"灵感点={inspiration_count}, 目的点={purpose_count}, 关键点={key_points_count}"
-            )
-
-            # 执行Agent
-            result = self.substance_agent.process(state)
-
-            # 更新状态
-            state.update(result)
-
-            # 检查Agent返回结果中是否包含关键错误
-            if self._check_agent_result_for_errors(result, "实质元素提取Agent"):
-                error_msg = "实质元素提取失败:无法获取视频文件"
-                state["workflow_failed"] = True
-                state["workflow_error"] = error_msg
-                return state
-
-            final_elements = result.get("substance_final_elements", [])
-            logger.info(f"实质元素提取完成 - 最终元素数: {len(final_elements)}")
-
-        except Exception as e:
-            logger.error(f"实质元素提取失败: {e}", exc_info=True)
-            state["workflow_failed"] = True
-            state["workflow_error"] = f"实质元素提取异常: {str(e)}"
-            state.update({
-                "concrete_elements": [],
-                "concrete_concepts": [],
-                "abstract_concepts": [],
-                "substance_elements": [],
-                "substance_analyzed_result": [],
-                "substance_scored_result": {},
-                "substance_filtered_ids": [],
-                "substance_categorized_result": {},
-                "substance_final_elements": []
-            })
-
-        return state
-
-    def _form_extraction_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:形式元素提取(脚本理解)"""
-        logger.info("=== 执行节点:形式元素提取 ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "形式元素提取"):
-            return state
-
-        try:
-            # 初始化Agent
-            if not self.form_agent.is_initialized:
-                self.form_agent.initialize()
-
-            # 验证三点解构信息是否可用
-            inspiration_points = state.get("inspiration_points", [])
-            purpose_point = state.get("purpose_point", {})
-            key_points = state.get("key_points", {})
-            
-            # 统计三点解构信息
-            inspiration_count = len(inspiration_points) if isinstance(inspiration_points, list) else 0
-            if not inspiration_count and isinstance(inspiration_points, dict):
-                inspiration_count = len(inspiration_points.get("points", []))
-            
-            purpose_count = 0
-            if isinstance(purpose_point, dict):
-                purpose_count = len(purpose_point.get("purposes", []))
-            elif isinstance(purpose_point, list):
-                purpose_count = len(purpose_point)
-            
-            key_points_count = 0
-            if isinstance(key_points, dict):
-                key_points_list = key_points.get("key_points", [])
-                key_points_count = len(key_points_list) if isinstance(key_points_list, list) else 0
-            elif isinstance(key_points, list):
-                key_points_count = len(key_points)
-            
-            logger.info(
-                f"形式提取节点 - 三点解构信息检查: "
-                f"灵感点={inspiration_count}, 目的点={purpose_count}, 关键点={key_points_count}"
-            )
-
-            # 执行Agent(依赖实质元素)
-            result = self.form_agent.process(state)
-
-            # 更新状态
-            state.update(result)
-
-            # 检查Agent返回结果中是否包含关键错误
-            if self._check_agent_result_for_errors(result, "形式元素提取Agent"):
-                error_msg = "形式元素提取失败:无法获取视频文件"
-                state["workflow_failed"] = True
-                state["workflow_error"] = error_msg
-                return state
-
-            final_elements = result.get("form_final_elements", [])
-            logger.info(f"形式元素提取完成 - 最终元素数: {len(final_elements)}")
-
-        except Exception as e:
-            logger.error(f"形式元素提取失败: {e}", exc_info=True)
-            state["workflow_failed"] = True
-            state["workflow_error"] = f"形式元素提取异常: {str(e)}"
-            state.update({
-                "concrete_element_forms": [],
-                "concrete_concept_forms": [],
-                "overall_forms": [],
-                "form_elements": [],
-                "form_analyzed_result": [],
-                "form_scored_result": {},
-                "form_weighted_result": {},
-                "form_filtered_ids": [],
-                "form_categorized_result": {},
-                "form_final_elements": []
-            })
-
-        return state
-
-    def _merge_all_results_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:分离实质和形式结果(脚本理解)"""
-        logger.info("=== 执行节点:分离实质和形式结果 ===")
-
-        try:
-            # 获取实质和形式的最终元素
-            substance_final_elements = state.get("substance_final_elements", [])
-            form_final_elements = state.get("form_final_elements", [])
-
-            # 分别存储实质列表和形式列表
-            state["实质列表"] = substance_final_elements
-            state["形式列表"] = form_final_elements
-
-            logger.info(f"分离完成 - 实质元素: {len(substance_final_elements)}, 形式元素: {len(form_final_elements)}")
-
-        except Exception as e:
-            logger.error(f"分离结果失败: {e}", exc_info=True)
-            state["实质列表"] = []
-            state["形式列表"] = []
-
-        return state
-
-    def _search_keywords_extraction_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:搜索关键词提取(分支1的最后一步)"""
-        logger.info("=== 执行节点:搜索关键词提取 ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "搜索关键词提取"):
-            return state
-
-        try:
-            # 初始化Agent
-            if not self.search_keyword_agent.is_initialized:
-                self.search_keyword_agent.initialize()
-
-            # 执行Agent(带重试机制)
-            max_retries = 2
-            result = None
-            last_error = None
-            
-            def _is_result_failed(result: Dict[str, Any]) -> tuple[bool, str]:
-                """检查结果是否失败
-                
-                Returns:
-                    (是否失败, 错误信息)
-                """
-                if not result:
-                    return True, "结果为空"
-                
-                # 检查关键错误(如无法获取视频文件)
-                if self._check_agent_result_for_errors(result, "搜索关键词提取Agent"):
-                    return True, "搜索关键词提取失败:无法获取视频文件"
-                
-                # 检查search_keywords中的错误字段
-                search_keywords = result.get("search_keywords", {})
-                if isinstance(search_keywords, dict):
-                    # 检查是否有错误字段
-                    if "错误" in search_keywords or "error" in search_keywords:
-                        error_msg = search_keywords.get("错误") or search_keywords.get("error", "未知错误")
-                        return True, f"搜索关键词提取失败:{error_msg}"
-                    
-                    # 检查搜索词数量是否为0(可能是解析失败的表现)
-                    keyword_count = search_keywords.get("总数", 0)
-                    if keyword_count == 0:
-                        # 如果总数为0,可能是解析失败,需要重试
-                        return True, "搜索关键词提取失败:未提取到搜索词(可能为解析失败)"
-                
-                return False, ""
-            
-            for attempt in range(max_retries):
-                try:
-                    # 执行Agent
-                    result = self.search_keyword_agent.process(state)
-                    
-                    # 检查结果是否失败
-                    is_failed, error_msg = _is_result_failed(result)
-                    
-                    if is_failed:
-                        if attempt < max_retries - 1:
-                            logger.warning(f"搜索关键词提取失败,准备重试 (尝试 {attempt + 1}/{max_retries}): {error_msg}")
-                            last_error = error_msg
-                            continue
-                        else:
-                            # 最后一次尝试也失败
-                            state["workflow_failed"] = True
-                            state["workflow_error"] = error_msg
-                            return state
-                    else:
-                        # 成功,跳出重试循环
-                        break
-                        
-                except Exception as e:
-                    if attempt < max_retries - 1:
-                        logger.warning(f"搜索关键词提取异常,准备重试 (尝试 {attempt + 1}/{max_retries}): {e}")
-                        last_error = str(e)
-                        continue
-                    else:
-                        # 最后一次尝试也失败,抛出异常让外层catch处理
-                        raise
-
-            # 更新状态
-            if result:
-                state.update(result)
-            else:
-                # 如果result为None,使用last_error
-                error_msg = last_error or "搜索关键词提取失败:未知错误"
-                state["workflow_failed"] = True
-                state["workflow_error"] = error_msg
-                return state
-
-            # 再次检查(双重保险)
-            if self._check_agent_result_for_errors(result, "搜索关键词提取Agent"):
-                error_msg = "搜索关键词提取失败:无法获取视频文件"
-                state["workflow_failed"] = True
-                state["workflow_error"] = error_msg
-                return state
-
-            # 获取搜索关键词数量
-            search_keywords = result.get("search_keywords", {})
-            keyword_count = search_keywords.get("总数", 0) if isinstance(search_keywords, dict) else 0
-            logger.info(f"搜索关键词提取完成 - 共 {keyword_count} 个搜索词")
-            
-            # 标记分支1完成
-            state["branch1_completed"] = True
-
-        except Exception as e:
-            logger.error(f"搜索关键词提取失败: {e}", exc_info=True)
-            state["workflow_failed"] = True
-            state["workflow_error"] = f"搜索关键词提取异常: {str(e)}"
-            state.update({
-                "search_keywords": {
-                    "搜索词列表": [],
-                    "总数": 0,
-                    "error": str(e)
-                }
-            })
-            state["branch1_completed"] = True  # 即使失败也标记为完成,避免阻塞
-
-        return state
-
-    def _structure_analysis_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:结构化内容库分析(ScriptWorkflowV2分支)"""
-        logger.info("=== 执行节点:结构化内容库分析(分支2) ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "结构化内容库分析"):
-            return state
-
-        try:
-            if not self.structure_agent.is_initialized:
-                self.structure_agent.initialize()
-
-            result = self.structure_agent.process(state)
-            
-            # 将结果存入 state 的 topic 字段
-            structure_data = result.get("structure_data", {})
-            state["topic"] = structure_data
-
-            logger.info("结构化内容库分析完成")
-            if isinstance(structure_data, dict):
-                topic_info = structure_data.get("选题信息表", {})
-                macro_topic = topic_info.get("宏观母题", "") if isinstance(topic_info, dict) else ""
-                if macro_topic:
-                    logger.info(f"宏观母题: {macro_topic}")
-        except Exception as e:
-            logger.error(f"结构化内容库分析失败: {e}", exc_info=True)
-            state["topic"] = {
-                "错误": str(e),
-            }
-
-        return state
-
-    def _content_unit_split_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:L3 内容单元拆分(ScriptWorkflowV2分支)"""
-        logger.info("=== 执行节点:L3 内容单元拆分(分支2) ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "L3 内容单元拆分"):
-            return state
-
-        try:
-            if not self.content_unit_split_agent.is_initialized:
-                self.content_unit_split_agent.initialize()
-
-            result = self.content_unit_split_agent.process(state)
-            state.update(result)
-
-            analysis = result.get("content_unit_analysis", {})
-            logger.info(
-                f"L3 单元拆分完成,单元数量: {len(analysis.get('单元列表', [])) if isinstance(analysis, dict) else 0}"
-            )
-        except Exception as e:
-            logger.error(f"L3 内容单元拆分失败: {e}", exc_info=True)
-            state["content_unit_analysis"] = {
-                "error": str(e),
-                "单元列表": [],
-            }
-
-        return state
-
-    def _content_unit_understand_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:整体结构理解(ScriptWorkflowV2分支)"""
-        logger.info("=== 执行节点:整体结构理解(分支2) ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "整体结构理解"):
-            return state
-
-        try:
-            if not self.content_unit_understand_agent.is_initialized:
-                self.content_unit_understand_agent.initialize()
-
-            result = self.content_unit_understand_agent.process(state)
-            state.update(result)
-
-            understanding = result.get("content_unit_understanding", {})
-            logger.info(
-                f"整体结构理解完成,段落数量: {len(understanding.get('段落解构', [])) if isinstance(understanding, dict) else 0}"
-            )
-        except Exception as e:
-            logger.error(f"整体结构理解失败: {e}", exc_info=True)
-            state["content_unit_understanding"] = {
-                "error": str(e),
-                "整体解构": {},
-                "段落解构": [],
-                "单元解构": {},
-            }
-
-        return state
-
-    def _keyword_extraction_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:金句提取(ScriptWorkflowV2分支)"""
-        logger.info("=== 执行节点:金句提取(分支2) ===")
-
-        # 检查关键错误
-        if self._check_critical_error(state, "金句提取"):
-            return state
-
-        try:
-            if not self.script_keyword_agent.is_initialized:
-                self.script_keyword_agent.initialize()
-
-            result = self.script_keyword_agent.process(state)
-            state.update(result)
-
-            script_keywords = result.get("script_keywords", {})
-            logger.info("金句提取完成")
-            if isinstance(script_keywords, dict):
-                hooks_count = len(script_keywords.get("hooks", []))
-                golden_sentences_count = len(script_keywords.get("golden_sentences", []))
-                logger.info(f"提取钩子数量: {hooks_count}, 金句数量: {golden_sentences_count}")
-            
-            # 标记分支2完成
-            state["branch2_completed"] = True
-        except Exception as e:
-            logger.error(f"金句提取失败: {e}", exc_info=True)
-            state["script_keywords"] = {
-                "error": str(e),
-            }
-            state["branch2_completed"] = True  # 即使失败也标记为完成,避免阻塞
-
-        return state
-
-    def _merge_branches_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:合并两个分支的结果
-        
-        检查分支2是否完成,如果没完成则执行它
-        """
-        logger.info("=== 执行节点:合并分支结果 ===")
-
-        try:
-            # 检查分支2是否完成
-            branch2_completed = state.get("branch2_completed", False)
-            
-            if not branch2_completed:
-                logger.info("分支2还未完成,现在执行分支2(ScriptWorkflowV2流程)")
-                try:
-                    # 执行分支2的所有节点
-                    if not self.structure_agent.is_initialized:
-                        self.structure_agent.initialize()
-                    structure_result = self.structure_agent.process(state)
-                    state["topic"] = structure_result.get("structure_data", {})
-                    
-                    if not self.content_unit_split_agent.is_initialized:
-                        self.content_unit_split_agent.initialize()
-                    split_result = self.content_unit_split_agent.process(state)
-                    state.update(split_result)
-                    
-                    if not self.content_unit_understand_agent.is_initialized:
-                        self.content_unit_understand_agent.initialize()
-                    understand_result = self.content_unit_understand_agent.process(state)
-                    state.update(understand_result)
-                    
-                    if not self.script_keyword_agent.is_initialized:
-                        self.script_keyword_agent.initialize()
-                    keyword_result = self.script_keyword_agent.process(state)
-                    state.update(keyword_result)
-                    
-                    state["branch2_completed"] = True
-                    logger.info("分支2(ScriptWorkflowV2流程)执行完成")
-                except Exception as e:
-                    logger.error(f"分支2执行失败: {e}", exc_info=True)
-                    state["branch2_completed"] = True  # 即使失败也标记为完成,避免阻塞
-            else:
-                logger.info("分支2已完成,直接合并结果")
-            
-            # 标记已合并
-            state["branches_merged"] = True
-            logger.info("分支结果合并完成,准备进入结果汇总")
-
-        except Exception as e:
-            logger.error(f"合并分支结果失败: {e}", exc_info=True)
-            state["branches_merged"] = True  # 即使失败也标记为已合并,避免阻塞
-
-        return state
-
-    def _result_aggregation_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:结果汇总(包含两个分支的结果)"""
-        logger.info("=== 执行节点:结果汇总 ===")
-
-        try:
-            # 初始化Function
-            if not self.result_aggregation_func.is_initialized:
-                self.result_aggregation_func.initialize()
-
-            # 执行Function(获取分支1的结果)
-            final_result = self.result_aggregation_func.execute(state)
-
-            # 添加分支2的结果(ScriptWorkflowV2的结果)
-            topic = state.get("topic", {})
-            content_unit_analysis = state.get("content_unit_analysis", {})
-            content_unit_understanding = state.get("content_unit_understanding", {})
-            script_keywords = state.get("script_keywords", {})
-
-            # 将分支2的结果添加到最终结果中
-            final_result["脚本解构V2"] = {
-                "结构化内容库": topic,
-                "L3单元解构": content_unit_analysis,
-                "整体结构理解": content_unit_understanding,
-                "金句提取": script_keywords,
-            }
-
-            # 更新状态
-            state["final_result"] = final_result
-
-            logger.info("结果汇总完成(包含两个分支的结果)")
-
-        except Exception as e:
-            logger.error(f"结果汇总失败: {e}", exc_info=True)
-            state["final_result"] = {
-                "视频信息": {},
-                "三点解构": {
-                    "灵感点": [],
-                    "目的点": {},
-                    "关键点": {}
-                },
-                "选题理解": {},
-                "脚本理解": {
-                    "内容品类": "未知",
-                    "段落列表": [],
-                    "实质列表": [],
-                    "形式列表": []
-                },
-                "脚本解构V2": {
-                    "结构化内容库": {},
-                    "L3单元解构": {},
-                    "整体结构理解": {},
-                    "金句提取": {}
-                },
-                "错误": f"汇总失败: {str(e)}"
-            }
-
-        return state
-
-    def _update_db_record_after_workflow(
-        self, 
-        state: Dict[str, Any], 
-        success: bool, 
-        final_result: Dict[str, Any] = None,
-        error_msg: str = None
-    ):
-        """工作流执行完毕后更新数据库记录
-        
-        Args:
-            state: 工作流执行后的状态
-            success: 是否成功
-            final_result: 最终结果(成功时使用)
-            error_msg: 错误信息(失败时使用)
-        """
-        try:
-            task_id = state.get("db_task_id")
-            if not task_id:
-                logger.warning("未找到 db_task_id,跳过数据库记录更新")
-                return
-            
-            db = next(get_db())
-            try:
-                record = db.query(DecodeVideo).filter_by(task_id=task_id).first()
-                if not record:
-                    logger.warning(f"未找到 task_id={task_id} 的数据库记录,跳过更新")
-                    return
-                
-                if success:
-                    # 更新为成功状态
-                    import json
-                    result_json = json.dumps(final_result, ensure_ascii=False) if final_result else None
-                    record.update_status(DecodeStatus.SUCCESS)
-                    record.update_result(result_json)
-                    
-                    # 提取分支2的结果(脚本解构V2)并存储到 decode_result_v2 字段
-                    if final_result and isinstance(final_result, dict):
-                        script_v2_result = final_result.get("脚本解构V2")
-                        if script_v2_result:
-                            result_v2_json = json.dumps(script_v2_result, ensure_ascii=False)
-                            record.update_result_v2(result_v2_json)
-                            logger.info(f"更新数据库记录的分支2结果: task_id={task_id}")
-                    
-                    # 提取搜索关键词并存储到 search_keywords 字段(字符串数组的JSON格式)
-                    search_keywords_data = state.get("search_keywords", {})
-                    if search_keywords_data and isinstance(search_keywords_data, dict):
-                        keyword_list = search_keywords_data.get("搜索词列表", [])
-                        if keyword_list:
-                            # 提取每个搜索词的"搜索词"字段,组成字符串数组
-                            keyword_strings = []
-                            for item in keyword_list:
-                                if isinstance(item, dict):
-                                    keyword = item.get("搜索词", "")
-                                    if keyword:
-                                        keyword_strings.append(keyword)
-                            
-                            # 转换为JSON字符串数组格式
-                            if keyword_strings:
-                                keywords_json = json.dumps(keyword_strings, ensure_ascii=False)
-                                record.update_search_keywords(keywords_json)
-                                logger.info(f"更新数据库记录的搜索关键词: task_id={task_id}, 关键词数量={len(keyword_strings)}")
-                    
-                    logger.info(f"更新数据库记录为成功: task_id={task_id}")
-                else:
-                    # 更新为失败状态
-                    record.update_status(DecodeStatus.FAILED, error_reason=error_msg)
-                    logger.info(f"更新数据库记录为失败: task_id={task_id}, error={error_msg}")
-                
-                db.commit()
-                
-            except Exception as e:
-                logger.error(f"更新数据库记录失败: {e}", exc_info=True)
-                db.rollback()
-            finally:
-                db.close()
-                
-        except Exception as e:
-            logger.error(f"更新数据库记录节点执行失败: {e}", exc_info=True)
-
-    def invoke(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
-        """执行工作流(公共接口)- 视频分析版本
-
-        Returns:
-            最终解码结果
-        """
-        logger.info("=== 开始执行解码工作流(视频分析) ===")
-        logger.info(f"input_data: {input_data}")
-        # 确保工作流已初始化
-        if not self.is_initialized:
-            self.initialize()
-
-        # 验证输入参数
-        video_url = input_data.get("video_url", "")
-        if not video_url:
-            error_msg = "未提供视频URL,无法执行工作流"
-            logger.error(error_msg)
-            return {
-                "error": error_msg,
-                "workflow_status": "failed",
-                "input_data": input_data
-            }
-
-        # 初始化状态(包含视频信息,供视频上传和后续Agent使用)
-        initial_state = {
-            "video": video_url,
-            "video_id": input_data.get("video_id", ""),
-            "title": input_data.get("title", ""),
-            "current_depth": 0,
-            "max_depth": self.max_depth,
-            "task_id": input_data.get("task_id", ""),
-        }
-
-        # 执行工作流
-        result = None
-        try:
-            result = self.compiled_graph.invoke(initial_state)
-        except Exception as e:
-            error_msg = f"工作流执行异常: {str(e)}"
-            logger.error(error_msg, exc_info=True)
-            # 更新数据库记录为失败状态(使用 initial_state 作为 fallback)
-            if result is None:
-                result = initial_state
-            self._update_db_record_after_workflow(result, success=False, error_msg=error_msg)
-            return {
-                "status": 3,
-                "error": error_msg,
-                "workflow_status": "failed",
-                "exception_type": type(e).__name__
-            }
-
-        # 检查是否因为错误而终止
-        if result.get("workflow_failed"):
-            error_msg = result.get("workflow_error", "工作流执行失败")
-            logger.error(f"工作流因错误而终止: {error_msg}")
-            # 更新数据库记录为失败状态
-            self._update_db_record_after_workflow(result, success=False, error_msg=error_msg)
-            return {
-                "error": error_msg,
-                "video_upload_error": result.get("video_upload_error"),
-                "workflow_status": "failed",
-                "failed_at": result.get("failed_at", "unknown"),
-                "status": 3
-            }
-
-        # 检查是否有最终结果
-        final_result = result.get("final_result")
-        if not final_result:
-            # 如果没有最终结果,检查是否有错误信息
-            if result.get("workflow_error"):
-                error_msg = result.get("workflow_error", "工作流执行失败,未生成结果")
-                logger.error(f"工作流执行失败: {error_msg}")
-                # 更新数据库记录为失败状态
-                self._update_db_record_after_workflow(result, success=False, error_msg=error_msg)
-                return {
-                    "status": 3,
-                    "error": error_msg,
-                    "workflow_status": "failed"
-                }
-            else:
-                logger.warning("工作流执行完成,但未生成最终结果")
-                # 更新数据库记录为失败状态
-                self._update_db_record_after_workflow(result, success=False, error_msg="工作流执行完成,但未生成最终结果")
-                return {
-                    "status": 3,
-                    "error": "工作流执行完成,但未生成最终结果",
-                    "workflow_status": "incomplete",
-                    "state": result
-                }
-
-        # 工作流执行成功,更新数据库记录
-        self._update_db_record_after_workflow(result, success=True, final_result=final_result)
-
-        logger.info("=== 解码工作流执行完成(视频分析) ===")
-
-        # 添加status字段(2表示成功)
-        if isinstance(final_result, dict):
-            final_result["status"] = 2
-
-        return final_result
-

+ 0 - 430
src/workflows/evaluate_workflow.py

@@ -1,430 +0,0 @@
-"""
-Evaluate Workflow.
-
-评估工作流:针对视频的筛选
-流程:查询原视频信息 → 评估筛选视频 → 更新数据库
-"""
-
-from typing import Dict, Any, Optional
-import json
-from langgraph.graph import StateGraph, END
-
-from src.components.agents.base import BaseGraphAgent
-from src.components.agents.evaluate_agent import EvaluateAgent
-from src.models import get_db, DecodeVideo, EvaluateRecord, EvaluateStatus
-from src.utils.logger import get_logger
-from utils.general import get_now_ts
-
-logger = get_logger(__name__)
-
-
-class EvaluateWorkflow(BaseGraphAgent):
-    """评估工作流
-
-    功能:
-    - 针对视频的筛选
-    - 流程:查询原视频信息 → 评估筛选视频 → 更新数据库
-    - 管理状态传递
-
-    实现方式:BaseGraphAgent (LangGraph)
-    """
-
-    def __init__(
-        self,
-        name: str = "evaluate_workflow",
-        description: str = "评估工作流 - 针对视频的筛选",
-        model_provider: str = "google_genai"
-    ):
-        super().__init__(
-            name=name,
-            description=description,
-            state_class=dict
-        )
-
-        self.model_provider = model_provider
-
-        # 初始化评估Agent
-        self.evaluate_agent = EvaluateAgent(
-            model_provider=model_provider
-        )
-
-    def _build_graph(self) -> StateGraph:
-        """构建工作流图
-        
-        流程:
-        1. 查询原视频信息(根据task_id查询decode_videos表)
-        2. 更新评估状态为执行中(根据evaluate_id更新evaluate_record表status为1)
-        3. 评估筛选视频(使用evaluate_agent)
-        4. 更新数据库(根据evaluate_id更新evaluate_record表)
-        """
-        workflow = StateGraph(dict)
-
-        # 添加节点
-        workflow.add_node("query_original_video", self._query_original_video_node)
-        workflow.add_node("update_evaluate_status", self._update_evaluate_status_node)
-        workflow.add_node("evaluate_videos", self._evaluate_videos_node)
-        workflow.add_node("update_database", self._update_database_node)
-
-        # 设置入口点
-        workflow.set_entry_point("query_original_video")
-
-        # 添加边
-        workflow.add_edge("query_original_video", "update_evaluate_status")
-        workflow.add_edge("update_evaluate_status", "evaluate_videos")
-        workflow.add_edge("evaluate_videos", "update_database")
-        workflow.add_edge("update_database", END)
-
-        logger.info("评估工作流图构建完成 - 流程:查询原视频信息 → 更新评估状态 → 评估筛选视频 → 更新数据库")
-
-        return workflow
-
-    def _query_original_video_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:查询原视频信息
-        
-        根据task_id查询decode_videos表,获取原视频的标题和解构内容
-        
-        Args:
-            state: 状态字典,包含:
-                - task_id: 任务ID
-                - evaluate_id: 评估ID
-                
-        Returns:
-            更新后的状态,包含:
-                - original_video_title: 原视频标题
-                - original_video_content: 原视频解构内容
-        """
-        logger.info("=== 执行节点:查询原视频信息 ===")
-
-        try:
-            task_id = state.get("task_id")
-            if not task_id:
-                logger.error("task_id为空,无法查询原视频信息")
-                state["workflow_error"] = "task_id为空"
-                return state
-
-            # 查询数据库
-            db = next(get_db())
-            try:
-                decode_video = db.query(DecodeVideo).filter_by(task_id=task_id).first()
-                
-                if not decode_video:
-                    logger.error(f"未找到task_id={task_id}的解构记录")
-                    state["workflow_error"] = f"未找到task_id={task_id}的解构记录"
-                    return state
-
-                # 提取标题(从result或decode_result_v2中提取)
-                title = ""
-                original_content = {}
-
-                # 优先使用decode_result_v2(ScriptWorkflowV2的结果)
-                if decode_video.decode_result_v2:
-                    try:
-                        decode_result_v2 = json.loads(decode_video.decode_result_v2)
-                        original_content["decode_result_v2"] = decode_result_v2
-                        # 尝试从结果中提取标题
-                        if isinstance(decode_result_v2, dict):
-                            # 尝试多种可能的路径
-                            title = (
-                                decode_result_v2.get("视频信息", {}).get("标题", "") or
-                                decode_result_v2.get("title", "") or
-                                decode_result_v2.get("视频标题", "") or
-                                ""
-                            )
-                    except json.JSONDecodeError as e:
-                        logger.warning(f"解析decode_result_v2失败: {e}")
-
-                # 使用result(原有结果)
-                if decode_video.result:
-                    try:
-                        result = json.loads(decode_video.result)
-                        original_content["result"] = result
-                        # 如果还没有标题,尝试从result中提取
-                        if not title and isinstance(result, dict):
-                            title = (
-                                result.get("视频信息", {}).get("标题", "") or
-                                result.get("title", "") or
-                                result.get("视频标题", "") or
-                                ""
-                            )
-                    except json.JSONDecodeError as e:
-                        logger.warning(f"解析result失败: {e}")
-
-                # 如果还是没有标题,使用video_id作为标题
-                if not title and decode_video.video_id:
-                    title = f"视频 {decode_video.video_id}"
-
-                logger.info(f"查询到原视频信息: task_id={task_id}, title={title}")
-
-                # 更新状态
-                state["original_video_title"] = title
-                state["original_video_content"] = original_content
-
-            finally:
-                db.close()
-
-        except Exception as e:
-            logger.error(f"查询原视频信息失败: {e}", exc_info=True)
-            state["workflow_error"] = f"查询原视频信息失败: {str(e)}"
-
-        return state
-
-    def _update_evaluate_status_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:更新评估状态为执行中
-        
-        根据evaluate_id更新evaluate_record表中的status为1(EXECUTING)
-        
-        Args:
-            state: 状态字典,包含:
-                - evaluate_id: 评估ID
-                
-        Returns:
-            更新后的状态
-        """
-        logger.info("=== 执行节点:更新评估状态为执行中 ===")
-
-        try:
-            evaluate_id = state.get("evaluate_id")
-            if not evaluate_id:
-                logger.warning("evaluate_id为空,跳过状态更新")
-                return state
-
-            db = next(get_db())
-            try:
-                evaluate_record = db.query(EvaluateRecord).filter_by(evaluate_id=evaluate_id).first()
-                if evaluate_record:
-                    evaluate_record.update_status(EvaluateStatus.EXECUTING)
-                    db.commit()
-                    logger.info(f"更新评估记录状态为执行中: evaluate_id={evaluate_id}")
-                else:
-                    logger.warning(f"未找到evaluate_id={evaluate_id}的评估记录")
-            finally:
-                db.close()
-
-        except Exception as e:
-            logger.error(f"更新评估状态失败: {e}", exc_info=True)
-            # 状态更新失败不影响工作流继续执行
-            state["status_update_error"] = f"更新评估状态失败: {str(e)}"
-
-        return state
-
-    def _evaluate_videos_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:评估筛选视频
-        
-        使用evaluate_agent从视频列表中筛选出和原视频最匹配的内容
-        
-        Args:
-            state: 状态字典,包含:
-                - original_video_title: 原视频标题
-                - original_video_content: 原视频解构内容
-                - search_result: 待评估的视频列表
-                
-        Returns:
-            更新后的状态,包含:
-                - evaluate_result: 评估结果列表
-        """
-        logger.info("=== 执行节点:评估筛选视频 ===")
-
-        try:
-            # 检查是否有错误
-            if state.get("workflow_error"):
-                logger.warning("存在错误,跳过评估步骤")
-                return state
-
-            # 初始化Agent
-            if not self.evaluate_agent.is_initialized:
-                self.evaluate_agent.initialize()
-
-            # 执行评估
-            evaluate_result = self.evaluate_agent.process(state)
-
-            # 更新状态
-            state.update(evaluate_result)
-
-            logger.info("视频评估完成")
-
-        except Exception as e:
-            logger.error(f"评估筛选视频失败: {e}", exc_info=True)
-            state["workflow_error"] = f"评估筛选视频失败: {str(e)}"
-            # 即使失败,也设置一个空的评估结果
-            state["evaluate_result"] = []
-
-        return state
-
-    def _update_database_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:更新数据库
-        
-        根据evaluate_id更新evaluate_record表中的evaluate_result、status和updated_at
-        
-        Args:
-            state: 状态字典,包含:
-                - evaluate_id: 评估ID
-                - evaluate_result: 评估结果列表
-                
-        Returns:
-            更新后的状态
-        """
-        logger.info("=== 执行节点:更新数据库 ===")
-
-        try:
-            evaluate_id = state.get("evaluate_id")
-            if not evaluate_id:
-                logger.error("evaluate_id为空,无法更新数据库")
-                state["workflow_error"] = "evaluate_id为空"
-                return state
-
-            evaluate_result = state.get("evaluate_result", [])
-
-            # 将评估结果转换为JSON字符串
-            result_json = json.dumps(evaluate_result, ensure_ascii=False, indent=2)
-
-            # 更新数据库
-            db = next(get_db())
-            try:
-                evaluate_record = db.query(EvaluateRecord).filter_by(evaluate_id=evaluate_id).first()
-                
-                if not evaluate_record:
-                    logger.error(f"未找到evaluate_id={evaluate_id}的评估记录")
-                    state["workflow_error"] = f"未找到evaluate_id={evaluate_id}的评估记录"
-                    return state
-
-                # 更新评估结果
-                evaluate_record.evaluate_result = result_json
-
-                # 更新状态
-                if state.get("workflow_error"):
-                    evaluate_record.update_status(EvaluateStatus.FAILED)
-                else:
-                    evaluate_record.update_status(EvaluateStatus.SUCCESS)
-
-                # 更新updated_at(通过SQL的onupdate自动更新,但也可以手动设置)
-                from sqlalchemy.sql import func
-                evaluate_record.updated_at = func.now()
-
-                db.commit()
-
-                logger.info(f"数据库更新成功: evaluate_id={evaluate_id}")
-
-            finally:
-                db.close()
-
-        except Exception as e:
-            logger.error(f"更新数据库失败: {e}", exc_info=True)
-            state["workflow_error"] = f"更新数据库失败: {str(e)}"
-            
-            # 尝试更新数据库状态为失败
-            try:
-                evaluate_id = state.get("evaluate_id")
-                if evaluate_id:
-                    db = next(get_db())
-                    try:
-                        evaluate_record = db.query(EvaluateRecord).filter_by(evaluate_id=evaluate_id).first()
-                        if evaluate_record:
-                            evaluate_record.update_status(EvaluateStatus.FAILED)
-                            db.commit()
-                    finally:
-                        db.close()
-            except Exception as db_error:
-                logger.error(f"更新数据库状态为失败时出错: {db_error}", exc_info=True)
-
-        return state
-
-    def invoke(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
-        """执行工作流(公共接口)
-        
-        Args:
-            input_data: 输入数据,包含:
-                - task_id: 任务ID(必需)
-                - evaluate_id: 评估ID(必需)
-                - search_result: 待评估的视频列表(必需)
-                
-        Returns:
-            执行结果,包含:
-                - evaluate_result: 评估结果列表
-                - workflow_status: 工作流状态(success/failed)
-        """
-        logger.info("=== 开始执行评估工作流 ===")
-        logger.info(f"input_data keys: {list(input_data.keys())}")
-
-        # 确保工作流已初始化
-        if not self.is_initialized:
-            self.initialize()
-
-        # 验证输入参数
-        task_id = input_data.get("task_id")
-        evaluate_id = input_data.get("evaluate_id")
-        search_result = input_data.get("search_result", [])
-
-        if not task_id:
-            error_msg = "未提供task_id,无法执行工作流"
-            logger.error(error_msg)
-            return {
-                "error": error_msg,
-                "workflow_status": "failed",
-                "input_data": input_data
-            }
-
-        if not evaluate_id:
-            error_msg = "未提供evaluate_id,无法执行工作流"
-            logger.error(error_msg)
-            return {
-                "error": error_msg,
-                "workflow_status": "failed",
-                "input_data": input_data
-            }
-
-        if not search_result:
-            error_msg = "未提供search_result,无法执行工作流"
-            logger.error(error_msg)
-            return {
-                "error": error_msg,
-                "workflow_status": "failed",
-                "input_data": input_data
-            }
-
-        # 初始化状态
-        initial_state = {
-            "task_id": task_id,
-            "evaluate_id": evaluate_id,
-            "search_result": search_result,
-        }
-
-        # 执行工作流
-        result = None
-        try:
-            result = self.compiled_graph.invoke(initial_state)
-        except Exception as e:
-            error_msg = f"工作流执行异常: {str(e)}"
-            logger.error(error_msg, exc_info=True)
-            # 更新数据库记录为失败状态
-            if result is None:
-                result = initial_state
-            result["workflow_error"] = error_msg
-            # 触发更新数据库节点以更新状态
-            try:
-                self._update_database_node(result)
-            except Exception as db_error:
-                logger.error(f"更新数据库状态为失败时出错: {db_error}", exc_info=True)
-            return {
-                "status": 3,
-                "error": error_msg,
-                "workflow_status": "failed",
-                "exception_type": type(e).__name__
-            }
-
-        # 检查是否有错误
-        if result.get("workflow_error"):
-            logger.error(f"工作流执行失败: {result.get('workflow_error')}")
-            return {
-                "status": 3,
-                "error": result.get("workflow_error"),
-                "workflow_status": "failed",
-                "evaluate_result": result.get("evaluate_result", [])
-            }
-
-        logger.info("=== 评估工作流执行完成 ===")
-        return {
-            "status": 2,
-            "workflow_status": "success",
-            "evaluate_result": result.get("evaluate_result", [])
-        }
-

+ 0 - 283
src/workflows/script_workflow_v2.py

@@ -1,283 +0,0 @@
-"""
-Script Workflow V2.
-兆恒的脚本解构工作流V2:基于视频直接进行L3单元拆分和整体理解
-
-视频脚本解构工作流(简化版):
-- 步骤:视频上传 → L3 单元拆分 → L1/L2 整体理解
-- 只依赖视频和 L3 解构结果,不再引入多余的输入字段。
-"""
-
-from typing import Dict, Any
-from langgraph.graph import StateGraph, END
-
-from src.components.agents.base import BaseGraphAgent
-from src.components.functions.video_upload_function import VideoUploadFunction
-from src.components.agents.structure_agent import StructureAgent
-from src.components.agents.content_unit_split_agent import ContentUnitSplitAgent
-from src.components.agents.content_unit_understand import ContentUnitUnderstandAgent
-from src.components.agents.script_keyword_agent import ScriptKeywordAgent
-from src.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-class ScriptWorkflowV2(BaseGraphAgent):
-    """脚本理解工作流 V2(仅视频 + L3 解构 + 整体理解)
-
-    流程:
-    START → 视频上传 → L3 单元拆分 → 整体结构理解 → 结果汇总 → END
-    """
-
-    def __init__(
-        self,
-        name: str = "script_workflow_v2",
-        description: str = "脚本理解工作流 V2(视频 → L3 单元 → L1/L2 整体解构)",
-        model_provider: str = "google_genai",
-    ):
-        super().__init__(
-            name=name,
-            description=description,
-            state_class=dict,  # 直接使用 dict 作为状态类型
-        )
-
-        self.model_provider = model_provider
-
-        # 初始化视频上传 Function
-        self.video_upload_func = VideoUploadFunction()
-
-        # 初始化结构化内容库 Agent
-        self.structure_agent = StructureAgent(
-            model_provider=model_provider
-        )
-
-        # 初始化 L3 单元拆分 Agent
-        self.content_unit_split_agent = ContentUnitSplitAgent(
-            model_provider=model_provider
-        )
-
-        # 初始化 L1/L2 整体理解 Agent
-        self.content_unit_understand_agent = ContentUnitUnderstandAgent(
-            model_provider=model_provider
-        )
-
-        # 初始化金句提取 Agent
-        self.script_keyword_agent = ScriptKeywordAgent(
-            model_provider=model_provider
-        )
-
-        logger.info(f"ScriptWorkflowV2 初始化完成,model_provider: {model_provider}")
-
-    def _build_graph(self) -> StateGraph:
-        """构建工作流图(简化版)"""
-        workflow = StateGraph(dict)
-
-        # 添加节点
-        workflow.add_node("video_upload", self._video_upload_node)
-        workflow.add_node("structure_analysis", self._structure_analysis_node)
-        workflow.add_node("content_unit_split", self._content_unit_split_node)
-        workflow.add_node("content_unit_understand", self._content_unit_understand_node)
-        workflow.add_node("keyword_extraction", self._keyword_extraction_node)
-        workflow.add_node("result_aggregation", self._result_aggregation_node)
-
-        # 定义边
-        workflow.set_entry_point("video_upload")
-        workflow.add_edge("video_upload", "structure_analysis")
-        workflow.add_edge("structure_analysis", "content_unit_split")
-        workflow.add_edge("content_unit_split", "content_unit_understand")
-        workflow.add_edge("content_unit_understand", "keyword_extraction")
-        workflow.add_edge("keyword_extraction", "result_aggregation")
-        workflow.add_edge("result_aggregation", END)
-
-        logger.info("ScriptWorkflowV2 图构建完成 - 流程:视频上传 → 结构化分析 → L3 单元拆分 → 整体理解 → 金句提取 → 结果汇总")
-
-        return workflow
-
-    def _video_upload_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:视频上传 - 下载视频并上传至 Gemini"""
-        logger.info("=== ScriptWorkflowV2:执行节点 video_upload ===")
-
-        try:
-            if not self.video_upload_func.is_initialized:
-                self.video_upload_func.initialize()
-
-            result = self.video_upload_func.execute(state)
-            state.update(result)
-
-            video_uri = result.get("video_uploaded_uri")
-            if video_uri:
-                logger.info(f"视频上传完成 - URI: {video_uri}")
-            else:
-                error = result.get("video_upload_error", "未知错误")
-                logger.warning(f"视频上传失败: {error}")
-        except Exception as e:
-            logger.error(f"视频上传失败: {e}", exc_info=True)
-            state.update(
-                {
-                    "video_uploaded_uri": None,
-                    "video_upload_error": str(e),
-                }
-            )
-
-        return state
-
-    def _structure_analysis_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:结构化内容库分析 - 基于视频内容进行结构化解构"""
-        logger.info("=== ScriptWorkflowV2:执行节点 structure_analysis ===")
-
-        try:
-            if not self.structure_agent.is_initialized:
-                self.structure_agent.initialize()
-
-            result = self.structure_agent.process(state)
-            
-            # 将结果存入 state 的 topic 字段
-            structure_data = result.get("structure_data", {})
-            state["topic"] = structure_data
-
-            logger.info("结构化内容库分析完成")
-            if isinstance(structure_data, dict):
-                topic_info = structure_data.get("选题信息表", {})
-                macro_topic = topic_info.get("宏观母题", "") if isinstance(topic_info, dict) else ""
-                if macro_topic:
-                    logger.info(f"宏观母题: {macro_topic}")
-        except Exception as e:
-            logger.error(f"结构化内容库分析失败: {e}", exc_info=True)
-            state["topic"] = {
-                "错误": str(e),
-            }
-
-        return state
-
-    def _content_unit_split_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:L3 内容单元拆分"""
-        logger.info("=== ScriptWorkflowV2:执行节点 content_unit_split ===")
-
-        try:
-            if not self.content_unit_split_agent.is_initialized:
-                self.content_unit_split_agent.initialize()
-
-            result = self.content_unit_split_agent.process(state)
-            state.update(result)
-
-            analysis = result.get("content_unit_analysis", {})
-            logger.info(
-                f"L3 单元拆分完成,单元数量: {len(analysis.get('单元列表', [])) if isinstance(analysis, dict) else 0}"
-            )
-        except Exception as e:
-            logger.error(f"L3 内容单元拆分失败: {e}", exc_info=True)
-            state["content_unit_analysis"] = {
-                "error": str(e),
-                "单元列表": [],
-            }
-
-        return state
-
-    def _content_unit_understand_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:整体结构理解(L1/L2 基于视频 + L3 解构)"""
-        logger.info("=== ScriptWorkflowV2:执行节点 content_unit_understand ===")
-
-        try:
-            if not self.content_unit_understand_agent.is_initialized:
-                self.content_unit_understand_agent.initialize()
-
-            result = self.content_unit_understand_agent.process(state)
-            state.update(result)
-
-            understanding = result.get("content_unit_understanding", {})
-            logger.info(
-                f"整体结构理解完成,段落数量: {len(understanding.get('段落解构', [])) if isinstance(understanding, dict) else 0}"
-            )
-        except Exception as e:
-            logger.error(f"整体结构理解失败: {e}", exc_info=True)
-            state["content_unit_understanding"] = {
-                "error": str(e),
-                "整体解构": {},
-                "段落解构": [],
-                "单元解构": {},
-            }
-
-        return state
-
-    def _keyword_extraction_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:金句提取 - 基于视频内容提取钩子和金句"""
-        logger.info("=== ScriptWorkflowV2:执行节点 keyword_extraction ===")
-
-        try:
-            if not self.script_keyword_agent.is_initialized:
-                self.script_keyword_agent.initialize()
-
-            result = self.script_keyword_agent.process(state)
-            state.update(result)
-
-            script_keywords = result.get("script_keywords", {})
-            logger.info("金句提取完成")
-            if isinstance(script_keywords, dict):
-                hooks_count = len(script_keywords.get("hooks", []))
-                golden_sentences_count = len(script_keywords.get("golden_sentences", []))
-                logger.info(f"提取钩子数量: {hooks_count}, 金句数量: {golden_sentences_count}")
-        except Exception as e:
-            logger.error(f"金句提取失败: {e}", exc_info=True)
-            state["script_keywords"] = {
-                "error": str(e),
-            }
-
-        return state
-
-    def _result_aggregation_node(self, state: Dict[str, Any]) -> Dict[str, Any]:
-        """节点:结果汇总 - 组装最终结果"""
-        logger.info("=== ScriptWorkflowV2:执行节点 result_aggregation ===")
-
-        try:
-            # 从 state 中提取结果
-            topic = state.get("topic", {})
-            content_unit_analysis = state.get("content_unit_analysis", {})
-            content_unit_understanding = state.get("content_unit_understanding", {})
-            script_keywords = state.get("script_keywords", {})
-
-            # 组装最终结果
-            final_result = {
-                "结构化内容库": topic,
-                "L3单元解构": content_unit_analysis,
-                "整体结构理解": content_unit_understanding,
-                "金句提取": script_keywords,
-            }
-
-            # 更新状态
-            state["final_result"] = final_result
-
-            logger.info("结果汇总完成")
-        except Exception as e:
-            logger.error(f"结果汇总失败: {e}", exc_info=True)
-            state["final_result"] = {
-                "error": f"汇总失败: {str(e)}",
-                "结构化内容库": {},
-                "L3单元解构": {},
-                "整体结构理解": {},
-                "金句提取": {},
-            }
-
-        return state
-
-    def invoke(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
-        """执行工作流(公共接口)- 简化版
-
-        只需要传入:
-        - video: 原始视频地址或本地路径(由 VideoUploadFunction 处理)
-        - video_id: 可选,外部业务 ID(兼容旧字段名 channel_content_id)
-        """
-        logger.info("=== 开始执行 ScriptWorkflowV2(视频 → L3 单元 → L1/L2 整体解构) ===")
-
-        if not self.is_initialized:
-            self.initialize()
-
-        # 初始化最小状态(仅保留必须字段)
-        initial_state = {
-            "video": input_data.get("video", ""),
-            "video_id": input_data.get("video_id", "") or input_data.get("channel_content_id", ""),  # 兼容旧字段名
-        }
-
-        result_state = self.compiled_graph.invoke(initial_state)
-
-        logger.info("=== ScriptWorkflowV2 执行完成 ===")
-
-        return result_state.get("final_result", {})

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio