浏览代码

19维度拆解

alair 3 月之前
父节点
当前提交
18abbc31ea
共有 38 个文件被更改,包括 1988 次插入184 次删除
  1. 104 0
      doc/prompt/互动任务设计维度_vlm_text.md
  2. 49 0
      doc/prompt/五大领域分析_vlm_text.md
  3. 72 0
      doc/prompt/传播记忆点设计维度_vlm_text.md
  4. 110 0
      doc/prompt/创作背景_vlm_text.md
  5. 67 0
      doc/prompt/因果逻辑与世界观_vlm_text.md
  6. 138 0
      doc/prompt/图书基础信息_vlm_text.md
  7. 70 0
      doc/prompt/对话与交互行为_vlm_text.md
  8. 33 0
      doc/prompt/情绪色调分析_vlm_text.md
  9. 63 0
      doc/prompt/成长能力匹配_vlm_text.md
  10. 56 0
      doc/prompt/核心价值观提炼_vlm_text.md
  11. 140 0
      doc/prompt/知识实体与百科拆解_vlm_text.md
  12. 99 0
      doc/prompt/童趣元素提炼维度_vlm_text.md
  13. 155 0
      doc/prompt/角色人设建立_vlm_text.md
  14. 80 0
      doc/prompt/语言难度分级_vlm_text.md
  15. 76 0
      doc/prompt/适配媒介转化维度_vlm_text.md
  16. 114 0
      doc/prompt/阅读效果反馈_vlm_text.md
  17. 0 0
      doc/sql/api_keys.sql
  18. 132 0
      doc/sql/book_page.sql
  19. 0 0
      doc/sql/init.sql
  20. 0 0
      doc/sql/prompt_schema.sql
  21. 5 5
      main.py
  22. 1 1
      src/api/db/services/prompt_service.py
  23. 5 9
      src/api/db/services/vector_search_service.py
  24. 4 4
      src/conf/settings.py
  25. 25 14
      src/datasets/parser/nodes/ragflow_nodes.py
  26. 108 52
      src/datasets/parser/nodes/vectorize_node.py
  27. 1 1
      src/datasets/parser/pdf_parser/pdf_splitter.py
  28. 3 1
      src/datasets/parser/states/parser_states.py
  29. 8 4
      src/datasets/parser/workflow_nodes/dimension_skill_node.py
  30. 14 0
      src/datasets/parser/workflows/dynamic_dimension_workflow.py
  31. 1 1
      src/datasets/parser/workflows/pdf_workflow.py
  32. 38 31
      src/job/chunk_update_job.py
  33. 2 2
      src/model/multimodal_embedding.py
  34. 0 3
      src/utils/http_client.py
  35. 36 0
      src/utils/ragflow/ragflow_user_service.py
  36. 104 51
      src/utils/vector_db/elasticsearch_adapter.py
  37. 31 5
      src/utils/vector_db/infinity_adapter.py
  38. 44 0
      src/utils/vector_db/result_util.py

+ 104 - 0
doc/prompt/互动任务设计维度_vlm_text.md

@@ -0,0 +1,104 @@
+# 视觉分析任务: 互动任务设计维度
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **互动任务设计维度**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+提取 / 设计书中可互动的任务(比如 "帮小熊找蜂蜜""数苹果数量")即亲子互动点;
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "任务类型": [
+    {{
+      "内容": "描述图片中关于'任务类型'的内容",
+      "分析": "解释它是如何符合以下标准的: 1. **动手操作类**<br />- **寻找与发现:**  在画面中找出指定的人、物、颜色或形状..."
+    }}
+  ],
+  "亲子互动点": [
+    {{
+      "内容": "描述图片中关于'亲子互动点'的内容",
+      "分析": "解释它是如何符合以下标准的: **1. 家长引导话术(标准模板)** <br />- **启动话术(引发兴趣):**  "宝贝,快..."
+    }}
+  ],
+  "硬件适配": [
+    {{
+      "内容": "描述图片中关于'硬件适配'的内容",
+      "分析": "解释它是如何符合以下标准的: **1. 点读笔指令设计**<br />- **触发区域:**  明确可点击的"热区"(角色、物品、..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像儿童游戏设计师一样看书
+**目标:**  提取 / 设计书中可互动的任务(比如 "帮小熊找蜂蜜""数苹果数量")即亲子互动点;
+**对应场景:**  孩子读的时候需要 "动手参与"(比如点读笔的互动游戏);可用于亲子共读引导、互动硬件设计;
+**应用:**  AI 自动生成互动题、AI 伴读时的互动提问、硬件的游戏化阅读。
+
+**拆解颗粒度:**
+
+任务类型(动手操作类、语言互动类、动作模仿类)
+亲子互动点(家长引导话术、互动时长建议)
+硬件适配(点读笔指令、屏幕交互设计)
+
+
+### 拆解项 (文本描述)
+
+#### 任务类型
+1. **动手操作类**
+- **寻找与发现:**  在画面中找出指定的人、物、颜色或形状。
+- **点数与计量:**  数清特定物品的数量,或比较大小、多少。
+- **路径与选择:**  通过触摸或者划线,为角色选择道路、完成连接。
+- **模拟操作:**  在模拟翻页、开门、擦洗、喂食等动作。
+2. **语言互动类**
+- **预测与提问:**  在翻页前猜测情节发展,或回答事实性问题。
+- **补充与描述:**  让孩子补充句子,或描述画面细节、角色情绪。
+- **模仿与发声:**  在模拟翻页、开门、擦洗、喂食等动作。
+3. **动作模仿类**
+- **角色扮演:**  模仿角色的标志性动作或姿势。
+- **情绪与律动:**  根据情节做出表情(开心、惊讶),或跟着节奏拍手、跺脚。
+- **指令响应:**  响应简单指令,如"拍拍手"、"指指月亮"。
+
+#### 亲子互动点
+**1. 家长引导话术(标准模板)** 
+- **启动话术(引发兴趣):**  "宝贝,快看!这一页好像需要你的帮忙呢!我们一起来......"
+- **任务指令(清晰具体):**  "你能帮小猪找到它丢的三颗橡果吗?用手指指给妈妈看。"
+- **鼓励与反馈(积极强化):** 
+- **成功时:**  "太棒了!你的眼睛真亮!我们一起为小熊鼓掌!"
+- **未成功/需要提示时:**  "没关系,我们看看树洞后面藏着什么?对了!就在这里!"
+- **延伸与联想(深化理解):**  "毛毛虫吃了这么多,你的肚子饿了吗?你最喜欢吃里面的哪种食物呀?"
+**2. 互动时长建议**
+- **微互动(15-30秒):**  适用于快速寻找、点数、模仿声音等。保持节奏轻快,不打断故事流。
+- **标准互动(30-60秒):**  适用于需要观察、描述或简单动作模仿的任务。是互动的主干部分。
+- **深度互动(1-3分钟):**  适用于角色扮演、预测讨论或复杂的多步骤操作。建议在故事转折点或结尾处进行,作为一个小高潮或总结。
+
+#### 硬件适配
+**1. 点读笔指令设计**
+- **触发区域:**  明确可点击的"热区"(角色、物品、背景元素)。
+- **层级反馈设计:** 
+- **第一下(点击):**  播放标准音效或名称(如"这是小兔子")。
+- **第二下(双击):**  触发互动任务或趣味对话(如"小兔子的耳朵不见了,你能帮我找找吗?")。
+- **长按:**  播放背景音乐或环境音。
+- **任务闭环:**  当孩子完成指令(如找到了所有物品),点读笔播放统一的成功音乐和鼓励语。
+**2. 屏幕交互设计(平板/智能绘本机)** 
+- **交互视觉提示:** 
+- **可操作元素:**  采用轻微的呼吸动画、光泽流动或柔和高亮来提示。
+- **任务区域:**  用非侵入性的图标(如一个小放大镜)表示此处有"寻找任务"。
+- **交互逻辑与反馈:** 
+- **拖拽/喂食:**  孩子可将食物拖到角色嘴边,角色会有咀嚼动画和音效。
+- **划线/路径:**  手指划过,路径上会亮起星光或留下彩色轨迹。
+- **擦除/发现:**  用手指涂抹雾气覆盖的区域,会显现隐藏的图案。
+- **自适应难度:**  根据孩子年龄或表现,可调整寻找物品的数量、缩小目标大小或增加干扰项。
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 49 - 0
doc/prompt/五大领域分析_vlm_text.md

@@ -0,0 +1,49 @@
+# 视觉分析任务: 五大领域分析
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **五大领域分析**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+明确对儿童综合能力的培养
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "领域名称": [
+    {{
+      "内容": "描述图片中关于'领域名称'的内容",
+      "分析": "解释它是如何符合以下标准的: 1. 内容比重(预估百分比)<br />2. 归纳能力模块:在每一领域下,概括出2-4个核心要培养的..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像幼教工作者一样看书
+**目标:**  明确对儿童综合能力的培养
+**应用:**  快速了解这本书能提升孩子的哪些能力
+
+**拆解颗粒度:**
+
+按照五大领域(健康、语言、社会、科学、艺术)比重从高到低依次解析,每个领域为一个独立单元。
+
+
+### 拆解项 (文本描述)
+
+#### 领域名称
+1. 内容比重(预估百分比)
+2. 归纳能力模块:在每一领域下,概括出2-4个核心要培养的能力或要传递的知识板块。
+3. 撰写目标描述:为每个能力模块撰写1-2条具体的知识性、技能性或情感性目标。
+4. 关联内容锚点:在目标描述中,尽可能自然地融入具体的书中情节、元素或任务作为例子。
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 72 - 0
doc/prompt/传播记忆点设计维度_vlm_text.md

@@ -0,0 +1,72 @@
+# 视觉分析任务: 传播记忆点设计维度
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **传播记忆点设计维度**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+提炼内容里能让小朋友 / 家长记住的 "slogan 式短语""标志性动作"(比如 "小熊的'抱抱拳'""'分享最快乐'的口头禅");
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "语言记忆点核心slogan重复口头禅": [
+    {{
+      "内容": "描述图片中关于'语言记忆点(核心 slogan、重复口头禅)'的内容",
+      "分析": "解释它是如何符合以下标准的: 1. **儿童本位**:使用孩子能理解、能说出的词汇和句式,避免复杂抽象。<br />2. **正面..."
+    }}
+  ],
+  "动作记忆点角色标志性动作互动标志性动作": [
+    {{
+      "内容": "描述图片中关于'动作记忆点(角色标志性动作、互动标志性动作)'的内容",
+      "分析": "解释它是如何符合以下标准的: 1. **绝对安全第一**:避免任何可能引发儿童模仿危险的动作(如后仰、勒脖、蒙眼)。<br />2..."
+    }}
+  ],
+  "视觉记忆点标志性画面核心符号": [
+    {{
+      "内容": "描述图片中关于'视觉记忆点(标志性画面、核心符号)'的内容",
+      "分析": "解释它是如何符合以下标准的: 1. **极简与可画性**:核心符号必须简单到让3岁孩子能大致描画出来。<br />2. **色彩定..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像品牌营销策划师一样看书
+**目标:**  提炼内容里能让小朋友 / 家长记住的 "slogan 式短语""标志性动作"(比如 "小熊的'抱抱拳'""'分享最快乐'的口头禅");
+**对应场景:**  推广视频需要 "洗脑又友好的记忆点",漫剧需要 "让小朋友能模仿的标志性元素";
+**应用:**  营销视频的核心口号提取、漫剧的角色标志性动作设计。
+
+**拆解颗粒度:**
+
+
+### 拆解项 (文本描述)
+
+#### 语言记忆点(核心 slogan、重复口头禅)
+1. **儿童本位**:使用孩子能理解、能说出的词汇和句式,避免复杂抽象。
+2. **正面引导**:传递积极、友好、鼓励的情绪和价值观。
+3. **可动作化**:最好能与一个简单的标志性动作绑定,形成"语言-动作"条件反射。
+4. **高频重复**:在故事或视频中,有设计地重复出现(通常不少于3次),强化记忆。
+
+#### 动作记忆点(角色标志性动作、互动标志性动作)
+1. **绝对安全第一**:避免任何可能引发儿童模仿危险的动作(如后仰、勒脖、蒙眼)。
+2. **极简与重复**:3秒内可完成,并在多集中重复出现,形成肌肉记忆。
+3. **情绪化与卡通化**:动作可以比现实更夸张,用身体语言把情绪"说"出来。
+4. **与语言/声音绑定**:最佳效果是"声音+动作"组合,如喊出口令"魔法变变变!"的同时完成画圈动作。
+
+#### 视觉记忆点(标志性画面、核心符号)
+1. **极简与可画性**:核心符号必须简单到让3岁孩子能大致描画出来。
+2. **色彩定调**:为每个核心符号或画面绑定1-2种主题色(如"魔法金"、"守护蓝"),形成色彩资产。
+3. **一致性重复**:标志性画面和道具必须在系列中稳定出现,成为观众期待的"彩蛋"。
+4. **情感承载**:每个视觉记忆点都应关联一种积极情感(如"安全"、"惊喜"、"温暖"),形成情感反射。
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 110 - 0
doc/prompt/创作背景_vlm_text.md

@@ -0,0 +1,110 @@
+# 视觉分析任务: 创作背景
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **创作背景**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+标注作者背景、获奖信息、创作动机
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "作者绘者背景": [
+    {{
+      "内容": "描述图片中关于'作者 / 绘者背景'的内容",
+      "分析": "解释它是如何符合以下标准的: -文字作者:<br />1. **姓名**<br />2. **国籍/地区**<br />3. **..."
+    }}
+  ],
+  "获奖信息": [
+    {{
+      "内容": "描述图片中关于'获奖信息'的内容",
+      "分析": "解释它是如何符合以下标准的: -作品荣誉:<br />1. **奖项全称**<br />2. **获奖年份**<br />3. *..."
+    }}
+  ],
+  "创作动机": [
+    {{
+      "内容": "描述图片中关于'创作动机'的内容",
+      "分析": "解释它是如何符合以下标准的: -解决的问题:<br />1. **社会/教育问题**(明确指向,如"自然缺失症")<br />2...."
+    }}
+  ],
+  "权威认证": [
+    {{
+      "内容": "描述图片中关于'权威认证'的内容",
+      "分析": "解释它是如何符合以下标准的: -专家审订与推荐:<br />1. **专家姓名**<br />2. **专家头衔/所在机构**<b..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像文学评论家一样看书
+**目标:**  标注作者背景、获奖信息、创作动机
+**应用:**  家长选书权威背书、AI 创作风格参考
+
+**拆解颗粒度:**
+
+
+### 拆解项 (文本描述)
+
+#### 作者 / 绘者背景
+-文字作者:
+1. **姓名**
+2. **国籍/地区**
+3. **专业背景与定位**(如:深耕领域、核心创作理念)
+4. **代表作品**(列举最具知名度的1-2个作品或系列)
+-绘者:
+1. **姓名**
+2. **国籍/地区**
+3. **艺术风格与专长**(如:专注主题、画风特点)
+4. **代表作品**(列举最具知名度的1-2个作品或系列)
+-译者:
+1. **姓名**
+2. **专业背景**(如:翻译经验年限、领域)
+3. **翻译风格/理念**
+4. **代表译作**(列举最具知名度的1-2个系列或作品)
+
+#### 获奖信息
+-作品荣誉:
+1. **奖项全称**
+2. **获奖年份**
+3. **颁奖机构/赛事名称**
+4. **获奖评语或核心授奖理由**(简述)
+-创作者荣誉:
+1. **奖项全称**
+2. **获奖团队/个人名称**(需与本书创作者关联)
+3. **关联的代表作**(用以佐证团队实力)
+
+#### 创作动机
+-解决的问题:
+1. **社会/教育问题**(明确指向,如"自然缺失症")
+2. **市场/内容缺口**(如"低幼科普读物匮乏")
+3. **儿童发展需求**(如"社交情感教育需求")
+-灵感来源:
+1. **核心理念**(如"慢成长"、"寓教于乐")
+2. **灵感触发点**(如"儿童普遍的好奇心")
+3. **跨文化/跨领域借鉴**
+
+#### 权威认证
+-专家审订与推荐:
+1. **专家姓名**
+2. **专家头衔/所在机构**
+3. **具体贡献**(如"科学审订"、"撰写荐语")
+-机构认证:
+1. **机构名称**(如出版社、行业协会、教育机构)
+2. **认可形式**(如"策划出版"、"入选案例库"、"官方推荐")
+-市场认可:
+1. **发行范围**(如"全渠道发行")
+2. **受众反馈**(如"获广泛好评")
+3. **可量化的市场表现**(如有,如销售数据、再版次数)
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 67 - 0
doc/prompt/因果逻辑与世界观_vlm_text.md

@@ -0,0 +1,67 @@
+# 视觉分析任务: 因果逻辑与世界观
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **因果逻辑与世界观**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+提取世界运行规则
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "明确的因果链条": [
+    {{
+      "内容": "描述图片中关于'明确的因果链条'的内容",
+      "分析": "解释它是如何符合以下标准的: 1. **事件粒度**:以角色的**离散动作或状态变化**为单位,如"离开家""吃掉苹果""房子倒塌..."
+    }}
+  ],
+  "世界观设定": [
+    {{
+      "内容": "描述图片中关于'世界观设定'的内容",
+      "分析": "解释它是如何符合以下标准的: 1. **规则类型**:<br />a. **物理规则**:重力、时间流逝、物体交互方式(如"石头永..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像逻辑学家一样看书
+**目标:**  提取世界运行规则
+**应用:**  逻辑严谨的 AI 生成
+
+**拆解颗粒度:**
+
+
+### 拆解项 (文本描述)
+
+#### 明确的因果链条
+1. **事件粒度**:以角色的**离散动作或状态变化**为单位,如"离开家""吃掉苹果""房子倒塌"。避免使用抽象或内部状态(如"感到害怕")作为原因,除非绘本明确将其与动作关联。
+2. **确定性**:仅提取**必定发生**的因果链,排除概率性或模糊关系(如"可能""有时")。若故事中存在例外,需单独注明。
+3. **条件明确**:
+a. 前提条件应尽可能具体,包含所有必要因素(如时间、地点、角色属性)。
+b. 可包含复合条件(如"如果A且B,则C"),但需确保所有条件在故事中同时满足。
+4. **直接因果**:优先提取直接因果关系,避免长链推理(除非故事明确展示多步因果)。例如:"如果大灰狼吹气,那么稻草屋倒塌"是直接因果;而"如果小猪懒惰,那么最终被狼追赶"需要中间步骤,需拆解为多个直接因果。
+
+#### 世界观设定
+1. **规则类型**:
+a. **物理规则**:重力、时间流逝、物体交互方式(如"石头永远沉入水底")。
+b. **生物特性**:角色能力(如"动物会说话但不会使用手机")、生命周期、饮食需求。
+c. **社会规范**:角色之间的固定关系(如"猫和狗是天敌")、礼仪、法律。
+d. **超自然规则**:魔法使用条件(如"魔法只能在晚上使用")、神奇物品的限制。
+2. **适用范围**:
+a. 标注规则是**全局有效**还是**局部有效**(如"只在森林中生效")。
+b. 注明**例外情况**(如"除了圣诞夜,魔法全天可用")。
+3. **具体性**:设定应尽可能具体、可检验。避免模糊表述(如"善良终有回报"),转而提取具体机制(如"帮助他人的角色会获得一枚魔法金币")。
+4. **一致性检查**:确保不同规则之间无矛盾。若存在矛盾(如不同绘本的设定冲突),需按单个绘本独立处理并标注来源。
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 138 - 0
doc/prompt/图书基础信息_vlm_text.md

@@ -0,0 +1,138 @@
+# 视觉分析任务: 图书基础信息
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **图书基础信息**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+记录书籍核心基础信息,为各场景(选书、创作、硬件适配)提供底层数据支撑
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "书号": [
+    {{
+      "内容": "描述图片中关于'书号'的内容",
+      "分析": "解释它是如何符合以下标准的: 具体的ISBN号..."
+    }}
+  ],
+  "适读年龄": [
+    {{
+      "内容": "描述图片中关于'适读年龄'的内容",
+      "分析": "解释它是如何符合以下标准的: 核心目标读者年龄段..."
+    }}
+  ],
+  "作品综述书籍简介": [
+    {{
+      "内容": "描述图片中关于'作品综述(书籍简介)'的内容",
+      "分析": "解释它是如何符合以下标准的: 1. **系列规模**:共多少**册**。<br />2. **故事背景**<br />3. **核..."
+    }}
+  ],
+  "出版机构": [
+    {{
+      "内容": "描述图片中关于'出版机构'的内容",
+      "分析": "解释它是如何符合以下标准的: **策划/品牌方、合作出版社**..."
+    }}
+  ],
+  "出版时间": [
+    {{
+      "内容": "描述图片中关于'出版时间'的内容",
+      "分析": "解释它是如何符合以下标准的: 首次出版..."
+    }}
+  ],
+  "版权归属": [
+    {{
+      "内容": "描述图片中关于'版权归属'的内容",
+      "分析": "解释它是如何符合以下标准的: **版权方,授权范围**..."
+    }}
+  ],
+  "创作团队": [
+    {{
+      "内容": "描述图片中关于'创作团队'的内容",
+      "分析": "解释它是如何符合以下标准的: 文字作者、插图作者、译者..."
+    }}
+  ],
+  "装帧类型": [
+    {{
+      "内容": "描述图片中关于'装帧类型'的内容",
+      "分析": "解释它是如何符合以下标准的: 单册/套装、平装/精装、胶装或其他..."
+    }}
+  ],
+  "页数": [
+    {{
+      "内容": "描述图片中关于'页数'的内容",
+      "分析": "解释它是如何符合以下标准的: **单册页数、全套总页数**..."
+    }}
+  ],
+  "字数": [
+    {{
+      "内容": "描述图片中关于'字数'的内容",
+      "分析": "解释它是如何符合以下标准的: **单册字数、全套总字数**..."
+    }}
+  ],
+  "开本": [
+    {{
+      "内容": "描述图片中关于'开本'的内容",
+      "分析": "解释它是如何符合以下标准的: 书籍尺寸规格..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角** /
+**目标:**  记录书籍核心基础信息,为各场景(选书、创作、硬件适配)提供底层数据支撑
+**应用:**  家长选书筛选、员工内容管理、开发者硬件适配、合作企业版权确认
+
+**拆解颗粒度:**
+
+
+### 拆解项 (文本描述)
+
+#### 书号
+具体的ISBN号
+
+#### 适读年龄
+核心目标读者年龄段
+
+#### 作品综述(书籍简介)
+1. **系列规模**:共多少**册**。
+2. **故事背景**
+3. **核心角色**
+4. **内容结构**:如**每册1个自然话题**,以故事引入,结尾设**图文科普专栏**。
+5. **核心功能**:兼具**趣味性与知识性**;传递感受自然、成长智慧、友情、协作、抗挫折等**正向价值观**。
+
+#### 出版机构
+**策划/品牌方、合作出版社**
+
+#### 出版时间
+首次出版
+
+#### 版权归属
+**版权方,授权范围**
+
+#### 创作团队
+文字作者、插图作者、译者
+
+#### 装帧类型
+单册/套装、平装/精装、胶装或其他
+
+#### 页数
+**单册页数、全套总页数**
+
+#### 字数
+**单册字数、全套总字数**
+
+#### 开本
+书籍尺寸规格
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 70 - 0
doc/prompt/对话与交互行为_vlm_text.md

@@ -0,0 +1,70 @@
+# 视觉分析任务: 对话与交互行为
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **对话与交互行为**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+分离 "说的" 和 "做的"
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "纯对话文本": [
+    {{
+      "内容": "描述图片中关于'纯对话文本'的内容",
+      "分析": "解释它是如何符合以下标准的: - **精确到每个话轮**:谁对谁说话,内容是什么。<br />- **包含**:所有直接引语("...."
+    }}
+  ],
+  "对话时的动作描述": [
+    {{
+      "内容": "描述图片中关于'对话时的动作描述'的内容",
+      "分析": "解释它是如何符合以下标准的: - **层级一:核心肢体动作**:跑、跳、拿、递、藏、摔倒、拥抱等推动情节的具体行为。<br />-..."
+    }}
+  ],
+  "对话发生的场景环境": [
+    {{
+      "内容": "描述图片中关于'对话发生的场景环境'的内容",
+      "分析": "解释它是如何符合以下标准的: - **宏观设定**:森林、城堡、太空、海边、房间。这是故事的"舞台背景"。<br />- **关键..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像话剧导演一样看书
+**目标:**  分离 "说的" 和 "做的"
+**应用:**  互动剧本生成
+
+**拆解颗粒度:**
+
+
+### 拆解项 (文本描述)
+
+#### 纯对话文本
+- **精确到每个话轮**:谁对谁说话,内容是什么。
+- **包含**:所有直接引语("......"),包括感叹词、疑问句。
+- **排除**:一切描述说话方式的状语(如"生气地说")、心理活动、叙述性语言。
+- **特殊处理**:自言自语、旁白(如果以角色口吻)也视为对话。
+
+#### 对话时的动作描述
+- **层级一:核心肢体动作**:跑、跳、拿、递、藏、摔倒、拥抱等推动情节的具体行为。
+- **层级二:神态与微表情**:笑、皱眉、瞪大眼睛、撇嘴、流泪、做鬼脸等传达情绪的表情变化。
+- **层级三:伴随性动作与姿态**:手舞足蹈、跺脚、蜷缩、背手、东张西望等体现状态的习惯性动作。
+
+#### 对话发生的场景环境
+- **宏观设定**:森林、城堡、太空、海边、房间。这是故事的"舞台背景"。
+- **关键道具**:一棵可以爬的树、一张藏着钥匙的桌子、一扇需要打开的门、一个会发光的水晶。这些是互动的**关键支点**。
+- **氛围与状态**:昏暗的、杂乱无章的、正在下雨的、布满星星的。这决定了灯效和音效的基调。
+- **空间关系**:角色之间的距离、角色的方位(在树上、在床下)。这关系到舞台调度。
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 33 - 0
doc/prompt/情绪色调分析_vlm_text.md

@@ -0,0 +1,33 @@
+# 视觉分析任务: 情绪色调分析
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **情绪色调分析**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+根据提供的维度定义分析图像。
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "分析结果": {
+    "描述": "请根据定义对发现的内容进行分类。"
+  }
+}}
+```
+
+## 维度定义 (详细标准)
+
+
+### 拆解项 (文本描述)
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 63 - 0
doc/prompt/成长能力匹配_vlm_text.md

@@ -0,0 +1,63 @@
+# 视觉分析任务: 成长能力匹配
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **成长能力匹配**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+把内容与成长能力对应,标注进阶衔接点
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "年龄段": "3-4岁/小班",
+  "对应能力认知能力技能能力素养能力": [
+    {{
+      "内容": "描述图片中关于'对应能力(认知能力、技能能力、素养能力)'的内容",
+      "分析": "解释它是如何符合以下标准的: 认知能力:处理信息、建构知识的心智能力。..."
+    }}
+  ],
+  "进阶衔接当前内容对应的下一阶段能力衔接推荐": [
+    {{
+      "内容": "描述图片中关于'进阶衔接(当前内容对应的下一阶段能力、衔接推荐)'的内容",
+      "分析": "解释它是如何符合以下标准的: 认知进阶:知识体系的深化、复杂化或系统化。..."
+    }}
+  ],
+  "分龄适配细化同一本书或同一主题内容在不同年龄段的差异化发展目标实现一材多用的精准引导": [
+    {{
+      "内容": "描述图片中关于'分龄适配(细化同一本书或同一主题内容,在不同年龄段的差异化发展目标,实现"一材多用"的精准引导。)'的内容",
+      "分析": "解释它是如何符合以下标准的: **年龄段划分:**  需明确年龄段或学段(如3-4岁/小班)。..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像儿童发展测评师一样看书
+**目标:**  把内容与成长能力对应,标注进阶衔接点
+**应用:**  按能力需求推荐书籍、硬件成长路径推送。(如:家长想知道 "这本书能锻炼孩子什么能力",需要把知识点、价值观关联成长能力;同时标注内容的 "进阶衔接点"(比如学会 "数 1-5" 后,接下来应衔接 "数的分解")
+
+**拆解颗粒度:**
+
+
+### 拆解项 (文本描述)
+
+#### 对应能力(认知能力、技能能力、素养能力)
+认知能力:处理信息、建构知识的心智能力。
+
+#### 进阶衔接(当前内容对应的下一阶段能力、衔接推荐)
+认知进阶:知识体系的深化、复杂化或系统化。
+
+#### 分龄适配(细化同一本书或同一主题内容,在不同年龄段的差异化发展目标,实现"一材多用"的精准引导。)
+**年龄段划分:**  需明确年龄段或学段(如3-4岁/小班)。
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 56 - 0
doc/prompt/核心价值观提炼_vlm_text.md

@@ -0,0 +1,56 @@
+# 视觉分析任务: 核心价值观提炼
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **核心价值观提炼**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+提取书里隐藏的"道理"
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "主题标签": [
+    {{
+      "内容": "描述图片中关于'主题标签'的内容",
+      "分析": "解释它是如何符合以下标准的: 这12类主题归为一个体系,是基于对**儿童人格全面发展**、**家长真实养育场景**以及**绘本教育..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像德育老师一样看书
+**目标:**  提取书里隐藏的"道理"
+**应用:**  家长痛点搜索
+
+**拆解颗粒度:**
+
+
+### 拆解项 (文本描述)
+
+#### 主题标签
+这12类主题归为一个体系,是基于对**儿童人格全面发展**、**家长真实养育场景**以及**绘本教育功能**的系统性思考。
+**1、品格培养**(勇气、坚持、诚实、责任、善良、乐观、感恩等基础美德。这是家长普遍关注的核心,常与"行为习惯"紧密相连。)
+**2、情绪管理**(识别情绪(喜怒哀惧)、接纳情绪、表达情绪、自我平静、抗挫折能力等。直指孩子发脾气、哭闹、胆小、抗压能力差等痛点。)
+**3、人际交往**(分享、合作、尊重、同理心(换位思考)、解决冲突、表达关爱等。解决孩子"不合群""自私""打人""不懂礼貌"等问题。)
+**4、习惯养成**(生活自理(刷牙、如厕)、作息规律、整理收纳、健康饮食、专注力培养等。是低龄段家长最实际的日常困扰。)
+**5、生命教育**(认识生命、接纳自我、珍惜亲情、理解衰老、面对死亡与分离等。帮助孩子和家长处理关于生命、离别等深刻话题)
+**6、社会适应**(缓解分离焦虑、熟悉规则、适应新环境(如入园入学)、建立安全感等。尤其针对即将或刚进入幼儿园的孩子家庭。)
+**7、中华美德与传统文化**(仁爱、孝道、诚信、谦让、爱国、家国情怀等。符合家长希望孩子扎根传统文化的需求。)
+**8、认知与思维发展**(科学启蒙、逻辑思维、观察力、想象力、创造力、解决问题能力等。对应家长对孩子智力发展的期待。)
+**9、环保意识**(热爱自然、保护动物、节约资源、垃圾分类等。回应现代家长对培养孩子地球公民意识的关注。)
+**10、艺术审美启蒙**(感受色彩、形状、音乐之美,激发艺术兴趣。满足家长对孩子美育培养的诉求。)
+**11、财商启蒙**(认识金钱、学会储蓄、理解交易、建立正确的物质观等)
+**12、反霸凌 / 自我保护**(建立身体边界、识别危险、勇敢说"不"、寻求帮助等。是家长安全感痛点的直接体现。)
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 140 - 0
doc/prompt/知识实体与百科拆解_vlm_text.md

@@ -0,0 +1,140 @@
+# 视觉分析任务: 知识实体与百科拆解
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **知识实体与百科拆解**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+提取硬核知识点
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "自然科学": [
+    {{
+      "内容": "描述图片中关于'自然科学'的内容",
+      "分析": "解释它是如何符合以下标准的: 动植物相关..."
+    }}
+  ],
+  "社会常识交通规则职业认知": [
+    {{
+      "内容": "描述图片中关于'社会常识(交通规则、职业认知)'的内容",
+      "分析": "解释它是如何符合以下标准的: 1 职业认知、<br />2 社区功能:认识学校、医院、邮局、超市、图书馆等场所的作用<br />3..."
+    }}
+  ],
+  "历史文化": [
+    {{
+      "内容": "描述图片中关于'历史文化'的内容",
+      "分析": "解释它是如何符合以下标准的: 传统节日、成语典故、历史事件..."
+    }}
+  ],
+  "社会情感与品德教育": [
+    {{
+      "内容": "描述图片中关于'社会情感与品德教育'的内容",
+      "分析": "解释它是如何符合以下标准的: 情绪管理、社交技能、品德发展、自我认知..."
+    }}
+  ],
+  "生活健康与安全": [
+    {{
+      "内容": "描述图片中关于'生活健康与安全'的内容",
+      "分析": "解释它是如何符合以下标准的: 身体卫生、自理能力、安全常识..."
+    }}
+  ],
+  "健康医学": [
+    {{
+      "内容": "描述图片中关于'健康医学'的内容",
+      "分析": "解释它是如何符合以下标准的: 身体认知、疾病常识、医疗过程、公共卫生..."
+    }}
+  ],
+  "生命教育": [
+    {{
+      "内容": "描述图片中关于'生命教育'的内容",
+      "分析": "解释它是如何符合以下标准的: 理解出生、成长、衰老、死亡;面对离别、失去时的情感..."
+    }}
+  ],
+  "品德教育": [
+    {{
+      "内容": "描述图片中关于'品德教育'的内容",
+      "分析": "解释它是如何符合以下标准的: 乐、分享、合作、关心与照顾、助人、给予、尊重、勇气、感恩、智慧等..."
+    }}
+  ],
+  "人文艺术": [
+    {{
+      "内容": "描述图片中关于'人文艺术'的内容",
+      "分析": "解释它是如何符合以下标准的: 绘画、音乐、舞蹈、戏剧......"
+    }}
+  ],
+  "物理与物质科学": [
+    {{
+      "内容": "描述图片中关于'物理与物质科学'的内容",
+      "分析": "解释它是如何符合以下标准的: 物质特性、力与运动、声光电磁、物态变化..."
+    }}
+  ],
+  "地球与环境科学": [
+    {{
+      "内容": "描述图片中关于'地球与环境科学'的内容",
+      "分析": "解释它是如何符合以下标准的: 1 天文地理:日月星辰、昼夜四季、天气现象(雨、雪、彩虹)。<br />2 自然环境:山脉、河流、海..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像博物学家一样看书
+**目标:**  提取硬核知识点
+**应用:**  AI 百科助手
+
+**拆解颗粒度:**
+
+
+### 拆解项 (文本描述)
+
+#### 自然科学
+动植物相关
+
+#### 社会常识(交通规则、职业认知)
+1 职业认知、
+2 社区功能:认识学校、医院、邮局、超市、图书馆等场所的作用
+3 规则意识:理解排队、轮流、遵守公共规则(如交通规则)的必要性。
+4 经济启蒙:对货币、买卖、储蓄、生产与消费的初级概念
+
+#### 历史文化
+传统节日、成语典故、历史事件
+
+#### 社会情感与品德教育
+情绪管理、社交技能、品德发展、自我认知
+
+#### 生活健康与安全
+身体卫生、自理能力、安全常识
+
+#### 健康医学
+身体认知、疾病常识、医疗过程、公共卫生
+
+#### 生命教育
+理解出生、成长、衰老、死亡;面对离别、失去时的情感
+
+#### 品德教育
+乐、分享、合作、关心与照顾、助人、给予、尊重、勇气、感恩、智慧等
+
+#### 人文艺术
+绘画、音乐、舞蹈、戏剧...
+
+#### 物理与物质科学
+物质特性、力与运动、声光电磁、物态变化
+
+#### 地球与环境科学
+1 天文地理:日月星辰、昼夜四季、天气现象(雨、雪、彩虹)。
+2 自然环境:山脉、河流、海洋、森林、沙漠等不同地貌。
+3 资源认知:水、土壤、动植物资源的珍贵性与有限性。
+4 环保行为:节约用水、垃圾分类、爱护动植物、减少污染。
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 99 - 0
doc/prompt/童趣元素提炼维度_vlm_text.md

@@ -0,0 +1,99 @@
+# 视觉分析任务: 童趣元素提炼维度
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **童趣元素提炼维度**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+提取书中适合儿童的趣味点(比如 "动物拟人化动作""重复押韵的台词""夸张的表情")\\叙事结构的韵律性、语气节奏的适配性;
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "趣味点类型": [
+    {{
+      "内容": "描述图片中关于'趣味点类型'的内容",
+      "分析": "解释它是如何符合以下标准的: - **拟人化与反差萌**<br />- **动物/物品拟人化**:动物穿衣服、刷牙、开车;茶杯会叹..."
+    }}
+  ],
+  "语言特征": [
+    {{
+      "内容": "描述图片中关于'语言特征'的内容",
+      "分析": "解释它是如何符合以下标准的: - **韵律性**<br />- **句式的重复结构**:"一会儿......一会儿......"、..."
+    }}
+  ],
+  "视觉趣味": [
+    {{
+      "内容": "描述图片中关于'视觉趣味'的内容",
+      "分析": "解释它是如何符合以下标准的: - **夸张元素**<br />- **比例的夸张**:巨大的梨子、像房子一样大的鞋子、芝麻小的国王..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像儿童文学作家一样看书
+**目标:**  提取书中适合儿童的趣味点(比如 "动物拟人化动作""重复押韵的台词""夸张的表情")\\叙事结构的韵律性、语气节奏的适配性;
+**对应场景:**  做动画 / 漫剧时,需要放大这些童趣点(比如把 "兔子跳" 改成 "兔子蹦着转圈圈"),但现有维度没专门标记 "哪些内容是小朋友觉得好玩的";
+**应用:**  AI 自动强化童趣细节(续写时加入重复儿歌句式)、动画的夸张化镜头设计\\故事续写的风格匹配。
+
+**拆解颗粒度:**
+
+
+### 拆解项 (文本描述)
+
+#### 趣味点类型
+- **拟人化与反差萌**
+- **动物/物品拟人化**:动物穿衣服、刷牙、开车;茶杯会叹气、云朵有脚丫。
+- **行为与身份的反差**:威严的狮子怕老鼠、笨重的大象跳芭蕾、小蚂蚁扛起大蛋糕。
+- **童言童语式思维**:给太阳盖被子、给月亮讲故事、认为影子是自己的好朋友。
+- **语言与声音的游戏**
+- **重复与押韵的台词**:结构性重复("跑啊跑,跳啊跳")、句式押韵("小汽车,嘀嘀嘀,带着大家去旅行")。
+- **夸张的拟声词**:不是简单的"砰",而是"乒铃乓啷!""咕噜咕噜哐当!"
+- **无厘头口号与咒语**:"变变变,变大象!""呼啦啦,飞起来!"
+- **表情与动作的夸张**
+- **面部表情的极致化**:眼睛瞪得如铜铃、嘴巴咧到耳根、惊讶时头发竖起。
+- **身体动作的卡通化**:"蹦着转圈圈"、"吓得一蹦三尺高"、"高兴得手舞足蹈到模糊"。
+- **互动与意外**
+- **打破第四面墙**:角色直接对读者说话、提问、邀请帮忙翻页。
+- **意想不到的转折**:以为是大怪兽,结果是猫咪的影子;寻找的宝藏是一朵小花。
+- **异想天开的设定**
+- **奇特的交通工具**:用勺子划船、骑着蒲公英飞行、坐着拖鞋赛车。
+- **非常规的问题解决**:用蜂蜜粘起破碎的星星、用呼噜声吓走乌云。
+
+#### 语言特征
+- **韵律性**
+- **句式的重复结构**:"一会儿......一会儿......"、"有的......有的......还有的......"
+- **段落间的循环呼应**:每遇到一个新朋友,都加入一段相同的歌曲或动作描述。
+- **关键词的回环**:故事开头和结尾用同一句话,形成闭环。
+- **语气风格**
+- **亲密感旁白**:像坐在孩子身边讲故事,多用"你看"、"猜猜怎么着"。
+- **角色语气鲜明化**:小老鼠尖细快速,大狗熊低沉缓慢,用文字暗示声音表演方向。
+- **悬念与期待营造**:"然后......最最奇怪的事情发生了!""你绝对想不到他看到了什么!"
+- **句式结构**
+- **短句为主,长短交错**:用于控制叙事节奏。长句描述场景,短句制造紧张或欢乐("快跑!快!")。
+- **排比与列举**:营造丰盈感和趣味性。"他找遍了床底下、橱柜里、窗帘后,甚至花瓶里!"
+
+#### 视觉趣味
+- **夸张元素**
+- **比例的夸张**:巨大的梨子、像房子一样大的鞋子、芝麻小的国王。
+- **速度线与动态线**:表现角色快速移动或情绪爆发。
+- **变形与弹性**:角色惊吓时变成弹簧状、得意时身体膨胀。
+- **隐藏细节(&quot;寻宝游戏&quot;)** 
+- **每页的固定彩蛋**:一只总在背景里偷东西的小地精、一本会改变书名的书。
+- **前后页的线索呼应**:后页出现的角色在前页背景中悄悄登场。
+- **图画中的额外叙事**:主故事之外,角落里小昆虫的迷你冒险。
+- **色彩与纹理的情绪化**
+- **情绪色彩**:生气时画面变红、悲伤时变蓝、开心时充满亮黄和粉色的光晕。
+- **趣味性纹理**:云朵是棉花糖质感、大山是巧克力蛋糕质感。
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 155 - 0
doc/prompt/角色人设建立_vlm_text.md

@@ -0,0 +1,155 @@
+# 视觉分析任务: 角色人设建立
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **角色人设建立**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+建立立体人物档案
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "角色的口头禅": [
+    {{
+      "内容": "描述图片中关于'角色的口头禅'的内容",
+      "分析": "解释它是如何符合以下标准的: - 高频重复句式(例:"我敢打赌......")<br />- 情绪化感叹词(例:"天哪!又搞砸了!..."
+    }}
+  ],
+  "性格弱点": [
+    {{
+      "内容": "描述图片中关于'性格弱点'的内容",
+      "分析": "解释它是如何符合以下标准的: - **表面弱点**:贪吃、粗心、拖延(例:小熊总弄丢蜂蜜罐)<br />- **深层弱点**:恐惧..."
+    }}
+  ],
+  "爱好": [
+    {{
+      "内容": "描述图片中关于'爱好'的内容",
+      "分析": "解释它是如何符合以下标准的: - **主动技能**:涂鸦、数蚂蚁、收集玻璃珠<br />- **被动倾向**:爱窝在窗边发呆、偷听..."
+    }}
+  ],
+  "角色定位": [
+    {{
+      "内容": "描述图片中关于'角色定位'的内容",
+      "分析": "解释它是如何符合以下标准的: - **功能标签**:<br />催化剂型(推动剧情) / 镜子型(反射主角成长) / 锚定型(代表..."
+    }}
+  ],
+  "性格与行为信息": [
+    {{
+      "内容": "描述图片中关于'性格与行为信息'的内容",
+      "分析": "解释它是如何符合以下标准的: - **行为密码**:<br />- 压力反应:咬指甲/狂吃东西/冷笑话攻击<br />- 决策风格..."
+    }}
+  ],
+  "情绪表达特点": [
+    {{
+      "内容": "描述图片中关于'情绪表达特点'的内容",
+      "分析": "解释它是如何符合以下标准的: - **外显层**:<br />愤怒时→反话讽刺 / 悲伤时→躲进衣柜讲故事<br />- **内隐..."
+    }}
+  ],
+  "关系与互动信息": [
+    {{
+      "内容": "描述图片中关于'关系与互动信息'的内容",
+      "分析": "解释它是如何符合以下标准的: - **权力动态**:<br />仰视-俯视(如:对强者模仿 vs 对弱者保护)<br />- **..."
+    }}
+  ],
+  "关键事件与转变": [
+    {{
+      "内容": "描述图片中关于'关键事件与转变'的内容",
+      "分析": "解释它是如何符合以下标准的: - **转折三阶**:<br />- 信念动摇事件(例:发现"英雄"也会撒谎)<br />- 主动选..."
+    }}
+  ],
+  "成长轨迹": [
+    {{
+      "内容": "描述图片中关于'成长轨迹'的内容",
+      "分析": "解释它是如何符合以下标准的: - **能力轴**:<br />从"误打误撞成功"到"有策略解决问题"<br />- **认知轴**..."
+    }}
+  ],
+  "主题映射": [
+    {{
+      "内容": "描述图片中关于'主题映射'的内容",
+      "分析": "解释它是如何符合以下标准的: - **显性主题**:友谊、勇气、探索<br />- **隐性主题**:<br />- 残缺与完整(..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像相声演员背贯口一样看书
+**目标:**  建立立体人物档案
+**应用:**  AI 角色扮演,建立立体的人物档案
+
+**拆解颗粒度:**
+
+
+### 拆解项 (文本描述)
+
+#### 角色的口头禅
+- 高频重复句式(例:"我敢打赌......")
+- 情绪化感叹词(例:"天哪!又搞砸了!")
+- 标志性提问(例:"你说,云朵是棉花糖做的吗?")
+- **禁忌**:避免通用语(如"你好"),需绑定性格或剧情(比如胆小角色的"要不...再等等?")
+
+#### 性格弱点
+- **表面弱点**:贪吃、粗心、拖延(例:小熊总弄丢蜂蜜罐)
+- **深层弱点**:恐惧被否定、过度讨好他人、伪装自信(例:耀眼孔雀怕独处)
+- **关键**:弱点需在剧情中被"针对",促成成长或冲突
+
+#### 爱好
+- **主动技能**:涂鸦、数蚂蚁、收集玻璃珠
+- **被动倾向**:爱窝在窗边发呆、偷听大人讲故事
+- **隐喻设计**:爱好可暗示内心需求(例:囤积闪亮物\=渴望关注)
+
+#### 角色定位
+- **功能标签**:
+催化剂型(推动剧情) / 镜子型(反射主角成长) / 锚定型(代表稳定价值观)
+- **象征标签**:
+"破碎的智慧老者"、"野性的引路人"、"被误解的守门人"
+
+#### 性格与行为信息
+- **行为密码**:
+- 压力反应:咬指甲/狂吃东西/冷笑话攻击
+- 决策风格:掷硬币决定/列清单分析/跟着直觉走
+- **细节库**:随身物品(破旧玩偶)、小动作(捻头发)、秘密习惯(给月亮起名字)
+
+#### 情绪表达特点
+- **外显层**:
+愤怒时→反话讽刺 / 悲伤时→躲进衣柜讲故事
+- **内隐层**:
+开心反而流泪 / 用整理物品掩饰焦虑
+
+#### 关系与互动信息
+- **权力动态**:
+仰视-俯视(如:对强者模仿 vs 对弱者保护)
+- **互动仪式**:
+专属打招呼方式(勾小指)、冲突后和解信号(分享糖果)
+
+#### 关键事件与转变
+- **转折三阶**:
+- 信念动摇事件(例:发现"英雄"也会撒谎)
+- 主动选择时刻(例:放弃奖品换取朋友安全)
+- 新身份确认(例:被群体赋予"守护者"称号)
+
+#### 成长轨迹
+- **能力轴**:
+从"误打误撞成功"到"有策略解决问题"
+- **认知轴**:
+理解"世界不是非黑即白"
+- **关系轴**:
+从"索取关注"到"提供情感支持"
+
+#### 主题映射
+- **显性主题**:友谊、勇气、探索
+- **隐性主题**:
+- 残缺与完整(例:独腿玩具的自我接纳)
+- 记忆与遗忘(例:祖父失忆促发家族故事传承)
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 80 - 0
doc/prompt/语言难度分级_vlm_text.md

@@ -0,0 +1,80 @@
+# 视觉分析任务: 语言难度分级
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **语言难度分级**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+量化阅读难度
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "图画": [
+    {{
+      "内容": "描述图片中关于'图画'的内容",
+      "分析": "解释它是如何符合以下标准的: 参考3-8岁儿童分集阅读指导文件..."
+    }}
+  ],
+  "文字": [
+    {{
+      "内容": "描述图片中关于'文字'的内容",
+      "分析": "解释它是如何符合以下标准的: /..."
+    }}
+  ],
+  "主题": [
+    {{
+      "内容": "描述图片中关于'主题'的内容",
+      "分析": "解释它是如何符合以下标准的: /..."
+    }}
+  ],
+  "角色": [
+    {{
+      "内容": "描述图片中关于'角色'的内容",
+      "分析": "解释它是如何符合以下标准的: /..."
+    }}
+  ],
+  "故事": [
+    {{
+      "内容": "描述图片中关于'故事'的内容",
+      "分析": "解释它是如何符合以下标准的: /..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像语言老师一样看书
+**目标:**  量化阅读难度
+**应用:**  精准分级阅读
+
+**拆解颗粒度:参考文件**
+
+
+### 拆解项 (文本描述)
+
+#### 图画
+参考3-8岁儿童分集阅读指导文件
+
+#### 文字
+/
+
+#### 主题
+/
+
+#### 角色
+/
+
+#### 故事
+/
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 76 - 0
doc/prompt/适配媒介转化维度_vlm_text.md

@@ -0,0 +1,76 @@
+# 视觉分析任务: 适配媒介转化维度
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **适配媒介转化维度**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+标注内容适配的媒介形式(比如 "这段对话适合做动画台词""这个场景适合做 30 秒短视频片段");
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "媒介类型适配br精准识别书中内容最适合转化为哪种媒介形式并明确其核心价值与改编方向": [
+    {{
+      "内容": "描述图片中关于'媒介类型适配<br />(精准识别书中内容最适合转化为哪种媒介形式,并明确其核心价值与改编方向。)'的内容",
+      "分析": "解释它是如何符合以下标准的: **动画:** <br />1. **强动态场景**:物体的移动、变形、生长过程(如:种子飞舞、植物..."
+    }}
+  ],
+  "转化要求br为每种媒介转化设定具体的技术与内容标准确保成品质量与体验": [
+    {{
+      "内容": "描述图片中关于'转化要求<br />(为每种媒介转化设定具体的技术与内容标准,确保成品质量与体验。)'的内容",
+      "分析": "解释它是如何符合以下标准的: **时长:** <br />• **动画短片**:5-15分钟(完整叙事)。<br />• **短视..."
+    }}
+  ],
+  "渠道适配br根据不同传播渠道的特性匹配最合适的媒介形式与内容风格实现精准投放": [
+    {{
+      "内容": "描述图片中关于'渠道适配<br />(根据不同传播渠道的特性,匹配最合适的媒介形式与内容风格,实现精准投放。)'的内容",
+      "分析": "解释它是如何符合以下标准的: **抖音 / 快手**<br />**短视频**(15-60秒)为主,**轻互动游戏**(H5)为辅..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像跨媒介内容制作人一样看书
+**目标:**  标注内容适配的媒介形式(比如 "这段对话适合做动画台词""这个场景适合做 30 秒短视频片段");
+**对应场景:**  把书改成动画片时,得知道 "哪些内容能直接转成视频分镜",做推广视频时得快速抓 "15 秒能讲清楚的亮点";
+**应用:**  AI 自动拆分动画分镜脚本、推广视频的核心片段截取。
+
+**拆解颗粒度:**
+
+媒介类型适配(动画、短视频、音频、互动游戏)
+转化要求(时长、画面完整性、音质)
+渠道适配(抖音、小红书、线下活动)
+
+
+### 拆解项 (文本描述)
+
+#### 媒介类型适配<br />(精准识别书中内容最适合转化为哪种媒介形式,并明确其核心价值与改编方向。)
+**动画:** 
+1. **强动态场景**:物体的移动、变形、生长过程(如:种子飞舞、植物发芽)。
+2. **角色互动段落**:富含表情、动作的对话或协作情节。
+3. **原理可视化段落**:抽象概念或科学过程的直观演示(如:血液流动、地球自转)。
+4. **情绪高潮或冲突**:能通过镜头语言(特写、蒙太奇)强化的情节。
+
+#### 转化要求<br />(为每种媒介转化设定具体的技术与内容标准,确保成品质量与体验。)
+**时长:** 
+• **动画短片**:5-15分钟(完整叙事)。
+• **短视频**:15秒(兴趣钩子)、30秒(情节亮点)、60秒(知识微单元)。
+• **音频故事**:3-8分钟(主线清晰,适合单次收听)。
+• **互动游戏**:单关卡3-5分钟,总体验时长建议15-30分钟。
+
+#### 渠道适配<br />(根据不同传播渠道的特性,匹配最合适的媒介形式与内容风格,实现精准投放。)
+**抖音 / 快手**
+**短视频**(15-60秒)为主,**轻互动游戏**(H5)为辅。
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 114 - 0
doc/prompt/阅读效果反馈_vlm_text.md

@@ -0,0 +1,114 @@
+# 视觉分析任务: 阅读效果反馈
+
+**角色:** 你是一位专业的儿童绘本和教育材料内容分析专家, 专注于 **阅读效果反馈**。
+你的目标是根据该维度的具体标准从图像中提取信息。
+
+## 用途
+标注可检测孩子阅读效果的指标及评估方法
+
+## 输出格式要求
+请按照以下 **JSON** 格式输出你的分析结果。
+键名对应维度文本中提供的"拆解项" (Breakdown Items) 定义。
+
+```json
+{{
+  "页码": {page_number},
+  "知识掌握br核心指标": [
+    {{
+      "内容": "描述图片中关于'知识掌握<br />(核心指标)'的内容",
+      "分析": "解释它是如何符合以下标准的: 对书中传递的**事实、概念、情节**的记忆与理解程度。<br />• **基础认知**:能**指认*..."
+    }}
+  ],
+  "互动完成度br过程指标": [
+    {{
+      "内容": "描述图片中关于'互动完成度<br />(过程指标)'的内容",
+      "分析": "解释它是如何符合以下标准的: 在阅读**过程中及之后**的参与、投入与延伸行为。<br />• **任务参与**:能**独立或在引..."
+    }}
+  ],
+  "能力提升br结果指标": [
+    {{
+      "内容": "描述图片中关于'能力提升<br />(结果指标)'的内容",
+      "分析": "解释它是如何符合以下标准的: 通过阅读在**思维、表达、品格、情绪情感**层面表现出的内化与应用迹象。<br />• **认知应用..."
+    }}
+  ],
+  "评估方法知识类提问": [
+    {{
+      "内容": "描述图片中关于'评估方法 -知识类提问'的内容",
+      "分析": "解释它是如何符合以下标准的: 检验**记忆与基础理解**,对应"知识掌握"指标。<br />• **原则**:问题直接源于书中**..."
+    }}
+  ],
+  "评估方法互动类邀请": [
+    {{
+      "内容": "描述图片中关于'评估方法 -互动类邀请'的内容",
+      "分析": "解释它是如何符合以下标准的: 引导**行为参与与延伸探索**,对应"互动完成度"指标。<br />• **原则**:邀请基于书中*..."
+    }}
+  ],
+  "评估方法能力类提问": [
+    {{
+      "内容": "描述图片中关于'评估方法 -能力类提问'的内容",
+      "分析": "解释它是如何符合以下标准的: 探查**理解深度与迁移应用**,对应"能力提升"指标。<br />• **原则**:问题需要儿童进行..."
+    }}
+  ]
+}}
+```
+
+## 维度定义 (详细标准)
+**视角:**  像教育评估师一样看书
+**目标:**  标注可检测孩子阅读效果的指标及评估方法
+**应用:**  硬件生成 "阅读报告"、家长评估清单
+
+**内容拆解点:**  效果指标(知识掌握、互动完成度、能力提升)、评估方法(家长提问清单)
+
+
+### 拆解项 (文本描述)
+
+#### 知识掌握<br />(核心指标)
+对书中传递的**事实、概念、情节**的记忆与理解程度。
+• **基础认知**:能**指认**(用手指/说出名称)书中**3-5个**核心事物/角色。
+• **概念复述**:能用自己的话,说出**1-2个**核心知识点或概念(如故事主题、科学原理)。
+• **细节记忆**:能准确回忆**1-2个**推动情节发展的**关键事件或对话**。
+
+#### 互动完成度<br />(过程指标)
+在阅读**过程中及之后**的参与、投入与延伸行为。
+• **任务参与**:能**独立或在引导下**,完成书中设计或由成人发起的**1项**主要互动(如贴纸、模仿动作、回答问题),完成度达**基础要求**(如贴对位置、做出关键步骤)。
+• **问答响应**:在阅读交流中,能**回应**成人**半数以上**的引导性问题,或能**主动提出1个**与书中内容相关的疑问。
+• **延伸意愿**:表现出对**1项**与书中主题相关的延伸活动(如户外观察、手工、角色扮演)的兴趣或尝试意愿。
+
+#### 能力提升<br />(结果指标)
+通过阅读在**思维、表达、品格、情绪情感**层面表现出的内化与应用迹象。
+• **认知应用**:能将书中知识/逻辑进行**简单迁移**(如:比较两个事物的异同、推测简单后续、联系自身经验举例)。
+• **语言输出**:能在表达中使用从书中习得的**1-2个新词汇或句式**,并组织成一句**语义完整**的话。
+• **素养显现**:在模拟情境或真实小事中,能表现出与书中传递的**价值观或积极态度**相一致的行为倾向
+
+#### 评估方法 -知识类提问
+检验**记忆与基础理解**,对应"知识掌握"指标。
+• **原则**:问题直接源于书中**明确呈现**的内容。使用 **&quot;是什么&quot;&quot;在哪里&quot;&quot;怎么了&quot;**  等提问词。
+• **示例范围**:
+- **指认类**:"指一指书里的XX在哪里?"
+- **命名/复述类**:"这个东西叫什么?""这个故事讲了谁和什么事?"
+- **细节回忆类**:"XX角色当时做了什么?"
+
+#### 评估方法 -互动类邀请
+引导**行为参与与延伸探索**,对应"互动完成度"指标。
+• **原则**:邀请基于书中**情节或元素** 进行**模拟、创造或实践**。使用 **&quot;我们一起来...&quot;&quot;你可以试着...&quot;**  等引导语。
+• **示例范围**:
+- **任务模拟类**:"我们来像XX一样做这个动作好吗?"
+- **创意表达类**:"你想画一画/搭一个故事里的XX吗?"
+- **延伸探索类**:"我们去公园里找找有没有像书中一样的XX?
+
+#### 评估方法 -能力类提问
+探查**理解深度与迁移应用**,对应"能力提升"指标。
+• **原则**:问题需要儿童进行**简单的联想、比较、解释或价值判断**。使用 **&quot;为什么&quot;&quot;如果...会...&quot;&quot;你觉得...&quot;**  等提问词。
+• **示例范围**:
+- **因果解释类**:"为什么会这样呢?"
+- **假设推理类**:"如果换了XX,事情会有什么不同?"
+- **联系自身类**:"如果你是TA,你会怎么做?""你有没有过类似的感觉?"
+- **价值评价类**:"你觉得他这样做对吗?为什么?"
+
+
+## 指令
+1. **分析:** 仔细阅读上方的"维度定义",理解每一项的具体标准。
+2. **扫描:** 观察图像,识别符合这些标准的元素。
+3. **提取:** 对于JSON中的每一个类别,提供在图像中发现的内容。
+   - 如果图像中不存在该类别的内容,可以将列表留空或设为null。
+   - 请提供描述性的值。

+ 0 - 0
doc/api_keys.sql → doc/sql/api_keys.sql


文件差异内容过多而无法显示
+ 132 - 0
doc/sql/book_page.sql


+ 0 - 0
doc/init.sql → doc/sql/init.sql


+ 0 - 0
sql/prompt_schema.sql → doc/sql/prompt_schema.sql


+ 5 - 5
main.py

@@ -52,15 +52,15 @@ async def main_lifespan(app: FastAPI):
     logger.info("✅ 提示词维度向量数据库表/索引已初始化")
 
     # 5. 启动Chunk更新定时任务
-    # from src.job.chunk_update_job import start_scheduler, shutdown_scheduler
-    # start_scheduler()
-    # logger.info("✅ Chunk update scheduler started")
+    from src.job.chunk_update_job import start_scheduler, shutdown_scheduler
+    start_scheduler()
+    logger.info("✅ Chunk update scheduler started")
     
     yield
 
     # 1. 关闭Chunk更新定时任务
-    # shutdown_scheduler()
-    # logger.info("✅ Chunk update scheduler shutdown")
+    shutdown_scheduler()
+    logger.info("✅ Chunk update scheduler shutdown")
 
     # 2. 关闭全局线程池
     from src.utils.async_utils import ThreadPoolManager

+ 1 - 1
src/api/db/services/prompt_service.py

@@ -187,7 +187,7 @@ class PromptService:
         from src.utils.ragflow.ragflow_service import RAGFlowService
         from src.conf.rag_parser_config import RagParserDefaults
         
-        ragflow_service = RAGFlowService(api_key="ragflow-XelVBvv8Uc6dZLNb1aBIKdbsupucEjESotOPTZZBrG4")
+        ragflow_service = RAGFlowService(api_key="ragflow-tP77uZTCoz81pnXhw5p9I5fWBxAiFjNXcEZ_Vm1L3QU")
         logger.info(f"开始创建 RAGFlow 数据集: {name}")
         
         dataset = ragflow_service.create_dataset(

+ 5 - 9
src/api/db/services/vector_search_service.py

@@ -5,11 +5,10 @@
 """
 
 from typing import Dict, Any, List, Optional
-from src.conf.settings import vector_db_settings
+from src.conf.settings import vector_db_settings, model_settings
 from src.utils.vector_db import get_vector_db_client, VectorDBClient
 from src.utils.file.image_util import image_util
 from src.model.multimodal_embedding import get_embedding_model
-from src.utils.infinity.result_util import convert_to_json
 from src.api.db.models import SearchRequest
 from src.common.logging_config import get_logger
 
@@ -60,7 +59,7 @@ class VectorSearchService:
         self.vector_field = vector_field or "dense_vector_1024"
         self.match_field = match_field or "content"
         self.match_type = match_type or "cosine"
-        self.table_name = table_name or vector_db_settings.infinity_table_name
+        self.table_name = table_name or "ragbook_1_4981b64cf8d611f095d95a49fdb98e7b"
 
     @property
     def client(self) -> VectorDBClient:
@@ -80,8 +79,7 @@ class VectorSearchService:
         try:
             search_query = self._convert_search_request_to_search_query(request)
             result = self._client.search(self.table_name, self.output_fields, search_query)
-            result_dict = result.to_result()
-            return convert_to_json(result_dict)
+            return result.to_result()
         except Exception as e:
             logger.error(f"搜索失败: {str(e)}")
             raise Exception(f"搜索失败: {str(e)}")
@@ -99,8 +97,7 @@ class VectorSearchService:
         try:
             search_query = self._convert_search_request_to_search_query(request)
             result = self._client.vector_search(self.table_name, self.output_fields, search_query)
-            result_dict = result.to_result()
-            return convert_to_json(result_dict)
+            return result.to_result()
         except Exception as e:
             logger.error(f"向量搜索失败: {str(e)}")
             raise Exception(f"向量搜索失败: {str(e)}")
@@ -118,8 +115,7 @@ class VectorSearchService:
         try:
             search_query = self._convert_search_request_to_search_query(request)
             result = self._client.hybrid_search(self.table_name, self.output_fields, search_query)
-            result_dict = result.to_result()
-            return convert_to_json(result_dict)
+            return result.to_result()
         except Exception as e:
             logger.error(f"混合搜索失败: {str(e)}")
             raise Exception(f"混合搜索失败: {str(e)}")

+ 4 - 4
src/conf/settings.py

@@ -22,10 +22,10 @@ class ModelSettings(BaseSettings):
     vl_model_name: str = Field(default="Qwen/Qwen3-VL-8B-Instruct", alias="VL_MODEL_NAME")
     chat_model_name: str = Field(default="deepseek-ai/DeepSeek-V3.2", alias="CHAT_MODEL_NAME")
     embedding_model_name: str = Field(default="Qwen/Qwen3-Embedding-0.6B", alias="EMBEDDING_MODEL_NAME")
+    multimodal_embedding_model_name: str = Field(default="qwen2.5-vl-embedding", alias="MULTIMODAL_EMBEDDING_MODEL_NAME")
+    rank_model_name: str = Field(default="Qwen/Qwen3-Reranker-0.6B", alias="RANK_MODEL_NAME")
     base_url: str = Field(default="https://api.openai.com/v1", alias="BASE_URL")
     api_key: str = Field(default="", alias="API_KEY")
-    rank_model_name: str = Field(default="Qwen/Qwen3-Reranker-0.6B", alias="RANK_MODEL_NAME")
-    multimodal_embedding_model_name: str = Field(default="qwen2.5-vl-embedding", alias="MULTIMODAL_EMBEDDING_MODEL_NAME")
     dashscope_api_key: str = Field(default="", alias="DASHSCOPE_API_KEY")
     
     model_config = SettingsConfigDict(
@@ -39,8 +39,8 @@ class RagflowSettings(BaseSettings):
     """RAGFLOW配置类"""
     ragflow_api_url: str = Field(default="http://192.168.16.134/", alias="RAGFLOW_API_URL")
     ragflow_api_key: str = Field(default="", alias="RAGFLOW_API_KEY")
-    ragflow_dataset_prefix: str = Field(default="ragflow_", alias="RAGFLOW_DATASET_PREFIX")
-    custom_dataset_prefix: str = Field(default="ragbook_", alias="CUSTOM_DATASET_PREFIX")
+    ragflow_dataset_prefix: str = Field(default="ragflow", alias="RAGFLOW_DATASET_PREFIX")
+    custom_dataset_prefix: str = Field(default="ragbook", alias="CUSTOM_DATASET_PREFIX")
     dataset_id: str = Field(default="", alias="DATASET_ID")
     ragflow_user_name: str = Field(default="", alias="RAGFLOW_USER_NAME")
     ragflow_passwd: str = Field(default="", alias="RAGFLOW_PASSWD")

+ 25 - 14
src/datasets/parser/nodes/ragflow_nodes.py

@@ -9,6 +9,7 @@ from src.datasets.parser.core.base import BaseNode, ConditionalNode, BaseState
 from src.datasets.parser.core.registry import register_node
 from src.utils.ragflow.ragflow_service import RAGFlowService
 from src.conf.rag_parser_config import RagParserDefaults
+from src.conf.settings import ragflow_settings
 from src.common.logging_config import get_logger
 
 logger = get_logger(__name__)
@@ -116,14 +117,16 @@ class RAGFlowDocumentUploadNode(BaseNode):
         - document_id: 文档ID
     """
     
-    def __init__(self):
+    def __init__(self, api_key: Optional[str] = None):
         """
         初始化文档上传节点
         
         Args:
             target_field: 存储文档ID的目标字段名
         """
-        self.ragflow_service = RAGFlowService()
+
+        self.api_key = api_key or ragflow_settings.ragflow_api_key
+        self.ragflow_service = RAGFlowService(api_key=self.api_key)
     
     @property
     def name(self) -> str:
@@ -225,8 +228,15 @@ class RAGFlowChunkNode(BaseNode):
         - parsed_results: 解析结果列表
     """
     
-    def __init__(self):
-        self.ragflow_service = RAGFlowService()
+    def __init__(self, api_key: Optional[str] = None):
+        """
+        初始化RAGFlow Chunk节点
+        
+        Args:
+            api_key: RAGFlow API密钥,如果未提供则从配置中获取
+        """
+        self.api_key = api_key or ragflow_settings.ragflow_api_key
+        self.ragflow_service = RAGFlowService(api_key=self.api_key)
     
     @property
     def name(self) -> str:
@@ -250,6 +260,7 @@ class RAGFlowChunkNode(BaseNode):
         page_document_id = getattr(state, 'document_id', '')
         parsed_results = getattr(state, 'parsed_results', [])
         split_pages = getattr(state, 'split_pages', [])
+        ragflow_id = getattr(state, 'ragflow_id', '')
         
         logger.info(f"开始创建Chunks,共 {len(parsed_results)} 页")
         
@@ -258,7 +269,7 @@ class RAGFlowChunkNode(BaseNode):
             text = parsed_result.get("content", "")
             image_path = split_pages[i].get("image_path", "") if i < len(split_pages) else ""
             
-            # img_id = f"{page_dataset_id}-{os.path.basename(image_path).split('.')[0]}.png" if image_path else ""
+            img_id = f"bookpage-{os.path.basename(image_path).split('.')[0]}.png" if image_path else ""
             
             chunk = self.ragflow_service.create_chunk(
                 dataset_id=page_dataset_id,
@@ -269,15 +280,15 @@ class RAGFlowChunkNode(BaseNode):
             parsed_result["chunk_id"] = chunk_id
             logger.debug(f"创建第 {page_number} 页Chunk,ID: {chunk_id}")
             
-            # # 记录到定时任务表
-            # if img_id:
-            #     get_chunk_record_service().record_chunk_add(
-            #         database_name=vector_db_settings.infinity_ragflow_database,
-            #         table_name=vector_db_settings.infinity_page_table_name,
-            #         chunk_id=chunk_id,
-            #         cond=f"id = '{chunk_id}'",
-            #         data={"img_id": img_id}
-            #     )
+            # 记录到定时任务表
+            if img_id:
+                get_chunk_record_service().record_chunk_add(
+                    database_name=vector_db_settings.infinity_ragflow_database,
+                    table_name=ragflow_settings.ragflow_dataset_prefix + "_" + ragflow_id,
+                    chunk_id=chunk_id,
+                    cond=f"_id = '{chunk_id}'",
+                    data={"img_id": img_id}
+                )
         
         logger.info(f"Chunks创建完成")
         return {}

+ 108 - 52
src/datasets/parser/nodes/vectorize_node.py

@@ -1,16 +1,18 @@
 """
 向量化节点
 
-将解析结果向量化并存入Infinity数据库
+将解析结果向量化并存入向量数据库(根据全局配置支持 Infinity 或 Elasticsearch)
 """
 
 import os
+import concurrent.futures
 from typing import Dict, Any, List, Optional
 from src.datasets.parser.core.base import BaseNode, BaseState
 from src.datasets.parser.core.registry import register_node
 from src.model.multimodal_embedding import Embedding
 from src.conf.settings import model_settings, vector_db_settings
-from src.utils.infinity import get_client
+from src.utils.vector_db import get_vector_db_client
+from src.utils.async_utils import ThreadPoolManager
 from src.common.logging_config import get_logger
 
 logger = get_logger(__name__)
@@ -21,7 +23,7 @@ class VectorizeNode(BaseNode):
     """
     向量化入库节点
     
-    将解析结果向量化并存入Infinity向量数据库
+    将解析结果向量化并存入向量数据库(根据全局配置自动选择 Infinity 或 Elasticsearch)
     
     需要的状态字段:
         - parsed_results: 解析结果列表
@@ -44,9 +46,8 @@ class VectorizeNode(BaseNode):
         初始化向量化节点
         
         Args:
-            table_name: Infinity表名
-            database_name: Infinity数据库名
-            embedding_model_name: 嵌入模型名称
+            database_name: 向量数据库名(可选,默认使用配置中的值)
+            embedding_model_name: 嵌入模型名称(可选,默认使用配置中的值)
         """
         self.database_name = database_name or vector_db_settings.infinity_database
         self.embedding_model_name = embedding_model_name or model_settings.multimodal_embedding_model_name
@@ -66,6 +67,71 @@ class VectorizeNode(BaseNode):
             )
         return self._embedding_model
     
+    def _vectorize_single_document(
+        self,
+        parsed_result: Dict[str, Any],
+        index: int,
+        split_pages: List[Dict[str, Any]],
+        document_id: str,
+        dataset_id: str,
+        file_name: str,
+        file_page_count: int
+    ) -> Optional[Dict[str, Any]]:
+        """
+        向量化单个文档
+        
+        Args:
+            parsed_result: 解析结果字典
+            index: 索引位置
+            split_pages: 包含图片信息的页面列表
+            document_id: 文档ID
+            dataset_id: 数据集ID
+            file_name: 文件名
+            file_page_count: 总页数
+            
+        Returns:
+            向量化后的文档字典,失败时返回None
+        """
+        try:
+            page_number = parsed_result.get("page_number", index + 1)
+            text = parsed_result.get("content", "")
+            image = split_pages[index].get("image") if index < len(split_pages) else None
+            image_path = split_pages[index].get("image_path", "") if index < len(split_pages) else ""
+            chunk_id = parsed_result.get("chunk_id", "")
+            
+            # 获取多模态嵌入向量
+            logger.debug(f"正在生成第 {page_number} 页的多模态嵌入...")
+            
+            if image:
+                embedding = self.embedding_model.get_multimodal_embedding(text, image)
+            else:
+                embedding = self.embedding_model.get_text_embedding(text)
+            
+            # 生成1024维稠密向量
+            dense_vector_1024 = embedding[:1024] if len(embedding) >= 1024 else embedding
+            
+            # 创建文档
+            document = {
+                "id": f"{document_id}_{page_number}",
+                "file_name": file_name,
+                "page_number": page_number,
+                "content": text,
+                "image_path": image_path,
+                "dense_vector_1024": dense_vector_1024,
+                "dataset_id": dataset_id,
+                "document_id": document_id,
+                "chunk_id": chunk_id,
+                "metadata": {
+                    "file_page_count": file_page_count,
+                }
+            }
+            
+            logger.debug(f"第 {page_number} 页向量化完成")
+            return document
+        except Exception as e:
+            logger.error(f"第 {index + 1} 页向量化失败: {str(e)}")
+            return None
+    
     def execute(self, state: BaseState) -> Dict[str, Any]:
         """
         执行向量化入库
@@ -81,70 +147,60 @@ class VectorizeNode(BaseNode):
         document_id = getattr(state, 'document_id', '')
         dataset_id = getattr(state, 'dataset_id', '')
         pdf_path = getattr(state, 'pdf_path', '')
+        table_name = getattr(state, 'table_name', '')
         
         if not parsed_results:
             logger.warning("没有待向量化的解析结果")
             return {"vectorized_results": [], "vectorized_pages": 0}
         
-        logger.info(f"开始向量化入库,共 {len(parsed_results)} 页")
-        
-        # 准备要入库的文档列表
-        documents_to_store = []
+        logger.info(f"开始并行向量化入库,共 {len(parsed_results)} 页")
         
         # 获取文件名和总页数
         file_name = os.path.basename(pdf_path) if pdf_path else ''
         file_page_count = len(split_pages)
         
-        # 遍历所有解析结果,生成向量化文档
-        for i, parsed_result in enumerate(parsed_results):
+        # 使用全局线程池并行处理向量化
+        pool = ThreadPoolManager.get_pool("parser")
+        future_to_index = {
+            pool.submit(
+                self._vectorize_single_document,
+                parsed_result,
+                i,
+                split_pages,
+                document_id,
+                dataset_id,
+                file_name,
+                file_page_count
+            ): i
+            for i, parsed_result in enumerate(parsed_results)
+        }
+        
+        # 收集向量化结果
+        documents_to_store = []
+        for future in concurrent.futures.as_completed(future_to_index):
             try:
-                page_number = parsed_result.get("page_number", i + 1)
-                text = parsed_result.get("content", "")
-                image = split_pages[i].get("image") if i < len(split_pages) else None
-                image_path = split_pages[i].get("image_path", "") if i < len(split_pages) else ""
-                chunk_id = parsed_result.get("chunk_id", "")
-                
-                # 获取多模态嵌入向量
-                logger.debug(f"正在生成第 {page_number} 页的多模态嵌入...")
-                
-                if image:
-                    embedding = self.embedding_model.get_multimodal_embedding(text, image)
-                else:
-                    embedding = self.embedding_model.get_text_embedding(text)
-                
-                # 生成1024维稠密向量
-                dense_vector_1024 = embedding[:1024] if len(embedding) >= 1024 else embedding
-                
-                # 创建文档
-                document = {
-                    "id": f"{document_id}_{page_number}",
-                    "file_name": file_name,
-                    "page_number": page_number,
-                    "content": text,
-                    "image_path": image_path,
-                    "dense_vector_1024": dense_vector_1024,
-                    "dataset_id": dataset_id,
-                    "document_id": document_id,
-                    "chunk_id": chunk_id,
-                    "metadata": {
-                        "file_page_count": file_page_count,
-                    }
-                }
-                
-                documents_to_store.append(document)
-                logger.debug(f"第 {page_number} 页向量化完成")
+                document = future.result()
+                if document:
+                    documents_to_store.append(document)
             except Exception as e:
-                logger.error(f"第 {i+1} 页向量化失败: {str(e)}")
+                index = future_to_index[future]
+                logger.error(f"第 {index + 1} 页向量化任务执行失败: {str(e)}")
+        
+        # 按页码排序,确保顺序一致
+        documents_to_store.sort(key=lambda x: x.get("page_number", 0))
         
         # 批量入库
         if documents_to_store:
-            logger.info(f"开始入库,共 {len(documents_to_store)} 个文档")
-            result = get_client().insert(
-                table_name=state.table_name,
+            logger.info(f"开始入库,共 {len(documents_to_store)} 个文档,表名: {table_name}")
+            
+            # 使用全局配置的向量数据库客户端
+            vector_db_client = get_vector_db_client(database=self.database_name)
+            result = vector_db_client.insert(
+                table_name=table_name,
                 documents=documents_to_store,
                 database_name=self.database_name
             )
-            logger.info(f"入库完成")
+            logger.info(f"入库完成,数据库类型: {vector_db_settings.vector_db_type}")
         
         return {
             "vectorized_results": documents_to_store,

+ 1 - 1
src/datasets/parser/pdf_parser/pdf_splitter.py

@@ -64,6 +64,7 @@ class PDFSplitter:
         else:
             return {
                 "page_number": page_number,
+                "chunk_id": "",
                 "image": image,
                 "image_bytes": None,
                 "image_path": None
@@ -137,7 +138,6 @@ class PDFSplitter:
         except Exception as e:
             raise Exception(f"PDF拆分失败: {str(e)}")
         finally:
-            ThreadPoolManager.shutdown_all()
             # 确保PDF文件总是被关闭
             if pdf_document is not None:
                 try:

+ 3 - 1
src/datasets/parser/states/parser_states.py

@@ -122,12 +122,14 @@ class DynamicDimensionState(BaseState):
     dimension_prompt: str = Field(default="", description="维度提示词")
     # dataset_name: str = Field(default="", description="数据集名称")
     document_id: str = Field(default="", description="文档ID")
-    
+    ragflow_id: str = Field(default="", description="RagFlow用户ID")
+    rag_flow_api_key: str = Field(default="", description="RAGFlow API密钥")
     # # RAGFlow 相关
     # ragflow_api_url: str = Field(default="", description="RAGFlow API URL")
     # rag_flow_api_key: str = Field(default="", description="RAGFlow API密钥")
     
     # 中间状态
+    table_name: str = Field(default="", description="向量表名(由 TableNameGenerationNode 生成)")
     split_pages: List[Dict[str, Any]] = Field(default_factory=list, description="拆分后的页面列表")
     parsed_results: List[Dict[str, Any]] = Field(default_factory=list, description="解析结果列表")
     

+ 8 - 4
src/datasets/parser/workflow_nodes/dimension_skill_node.py

@@ -57,14 +57,18 @@ class DimensionSkillNode(BaseNode):
         """节点名称,格式: skill_dim_{id}"""
         return f"skill_dim_{self.dimension_id}"
     
-    def _build_sub_workflow(self):
+    def _build_sub_workflow(self, state):
         """
         构建子工作流
         
+        Args:
+            state: 状态
+            
         Returns:
             编译后的 LangGraph 工作流
         """
         logger.info(f"[Skill-{self.dimension_id}] 开始构建子工作流")
+        rag_flow_api_key = getattr(state, 'rag_flow_api_key', '')
 
 
         # 创建工作流构建器
@@ -72,13 +76,13 @@ class DimensionSkillNode(BaseNode):
         
         # 创建节点
         prompt_node = PromptRetrievalNode(self.dimension_id)
-        document_upload_node = RAGFlowDocumentUploadNode()
+        document_upload_node = RAGFlowDocumentUploadNode(api_key=rag_flow_api_key)
         table_name_node = TableNameGenerationNode(self.dimension_id)
         parse_node = ImageParseNode(
             model_name=self.model_name,
             max_workers=self.max_workers
         )
-        chunk_node = RAGFlowChunkNode()
+        chunk_node = RAGFlowChunkNode(api_key=rag_flow_api_key)
         vectorize_node = VectorizeNode()
         result_node = DimensionResultNode(self.dimension_id)
         
@@ -122,7 +126,7 @@ class DimensionSkillNode(BaseNode):
         logger.info(f"[Skill-{self.dimension_id}] 开始执行维度技能")
         
         # 构建子工作流
-        workflow = self._build_sub_workflow()
+        workflow = self._build_sub_workflow(state)
         
         # 执行子工作流
         result = workflow.invoke(state)

+ 14 - 0
src/datasets/parser/workflows/dynamic_dimension_workflow.py

@@ -19,6 +19,7 @@ from src.datasets.parser.nodes import (
     CompleteNode
 )
 from src.datasets.parser.workflow_nodes import DimensionSkillNode
+from src.utils.ragflow.ragflow_user_service import get_ragflow_user_service
 from src.common.logging_config import get_logger
 
 logger = get_logger(__name__)
@@ -136,6 +137,17 @@ class DynamicDimensionWorkflow:
         
         logger.info(f"开始运行动态多维度解析: {pdf_path}")
         logger.info(f"维度执行顺序: {dimension_ids}")
+
+        # 查询维度知识库对应的user_id 和 api-key
+        ragflow_user = get_ragflow_user_service().get_ragflow_id_and_api_key(3)
+        if not ragflow_user:
+            logger.error("未找到维度知识库对应的user_id和api-key")
+            return {"success": False, "error": "ragflow_user_not_found"}
+        ragflow_id = ragflow_user.get("ragflow_id")
+        ragflow_api_key = ragflow_user.get("api_key")
+        if not ragflow_id or not ragflow_api_key:
+            logger.error("未找到维度知识库对应的user_id和api-key")
+            return {"success": False, "error": "ragflow_user_not_found"}
         
         # 1. 根据维度列表动态构建工作流
         workflow = self._build_workflow_for_dimensions(dimension_ids)
@@ -144,6 +156,8 @@ class DynamicDimensionWorkflow:
         initial_state = DynamicDimensionState(
             pdf_path=pdf_path,
             dimension_ids=dimension_ids,
+            ragflow_id=ragflow_id,
+            rag_flow_api_key=ragflow_api_key,
             # dataset_name=dataset_name or pdf_path.split("/")[-1].split("\\")[-1].replace(".pdf", ""),
             # ragflow_api_url=ragflow_api_url,
             # rag_flow_api_key=rag_flow_api_key,

+ 1 - 1
src/datasets/parser/workflows/pdf_workflow.py

@@ -67,7 +67,7 @@ class PDFParsingWorkflowV2:
         split_node = PDFSplitNode()
         image_parse_node = ImageParseNode(model_name=self.model_name)
         vectorize_node = VectorizeNode()
-        chunk_node = RAGFlowChunkNode()
+        chunk_node = RAGFlowChunkNode(api_key="ragflow-jpVLczgZ7_WkXbve59p1TS_wm3BvNXrcyhmoBFCAjR0")
         complete_node = CompleteNode(message_template="PDF解析工作流完成")
         
         # 使用WorkflowBuilder构建

+ 38 - 31
src/job/chunk_update_job.py

@@ -5,8 +5,9 @@ Chunk 更新定时任务
 - 定期查询到期的任务
 - 执行任务逻辑
 - 更新任务状态
+
+使用与 main.py 全局生命周期相同的向量数据库配置(Infinity 或 Elasticsearch)。
 """
-import time
 import json
 from datetime import datetime
 from apscheduler.schedulers.background import BackgroundScheduler
@@ -22,6 +23,22 @@ logger = get_logger(__name__)
 # 初始化调度器
 _scheduler = None
 
+
+def _get_vector_db_client_for_job():
+    """
+    获取向量数据库客户端,与 main.py 全局配置保持一致。
+    - Infinity: database="book_image_db"
+    - ES: 默认数据库
+    """
+    db_type = vector_db_settings.vector_db_type
+    if db_type == "infinity":
+        return get_vector_db_client(database="book_image_db")
+    if db_type == "es":
+        return get_vector_db_client()
+    logger.warning("未知的向量数据库类型: %s,使用默认 get_vector_db_client()", db_type)
+    return get_vector_db_client()
+
+
 class ChunkUpdateJob:
     """Chunk 更新定时任务服务"""
     
@@ -29,11 +46,11 @@ class ChunkUpdateJob:
         """初始化定时任务服务"""
         self.mysql_client = get_global_mysql_client()
         self.vector_db_type = vector_db_settings.vector_db_type
-        
-        # 使用 FastAPI 全局生命周期管理的向量数据库客户端
-        self.vector_db_client = get_vector_db_client()
-        logger.info("Using global vector_db_client for ChunkUpdateJob (type=%s)", 
-                   self.vector_db_type)
+        self.vector_db_client = _get_vector_db_client_for_job()
+        logger.info(
+            "ChunkUpdateJob 使用全局配置的向量数据库 (type=%s)",
+            self.vector_db_type
+        )
     
     def process_due_tasks(self):
         """处理到期的任务"""
@@ -80,44 +97,34 @@ class ChunkUpdateJob:
     def _execute_task(self, database_name: str, table_name: str, chunk_id: str, 
                       cond: str, data: dict) -> None:
         """
-        执行具体的任务逻辑
+        使用全局配置的向量数据库客户端执行 update 操作。
         
         Args:
             database_name: 数据库名称
             table_name: 表名称 (ES 模式下作为 index_name)
             chunk_id: Chunk ID (ES 模式下作为 document_id)
-            cond: 条件字符串
+            cond: 条件字符串 (Infinity 使用;ES 使用 chunk_id)
             data: 更新的数据字典
         """
         try:
-            # 解析数据
             update_data = json.loads(data) if isinstance(data, str) else data
             
             if self.vector_db_client is None:
-                logger.warning("No vector database client available (vector_db_type=%s)", 
-                              self.vector_db_type)
+                logger.warning(
+                    "向量数据库客户端不可用 (vector_db_type=%s),跳过任务",
+                    self.vector_db_type
+                )
                 return
             
-            # 使用统一的 vector_db_client.update() 接口
-            # ES 模式下:cond 作为 document_id 使用
-            # Infinity 模式下:cond 作为条件字符串使用
-            if self.vector_db_type == "es":
-                # ES 模式:使用 chunk_id 作为 cond(document_id)
-                self.vector_db_client.update(
-                    table_name=table_name or es_settings.es_index_name,
-                    cond=chunk_id,
-                    data=update_data,
-                    database_name=database_name
-                )
-            else:
-                # Infinity 模式:使用原始的 cond
-                self.vector_db_client.update(
-                    table_name=table_name,
-                    cond=cond,
-                    data=update_data,
-                    database_name=database_name
-                )
-                
+            # 根据全局配置的向量库类型调用 update(与 main 初始化一致)
+            self.vector_db_client.update(
+                table_name=table_name,
+                cond=cond,
+                data=update_data,
+                database_name=database_name
+            )
+
+            
             logger.info(f"Updated chunk {chunk_id} in {database_name}.{table_name}")
         except Exception as e:
             raise Exception(f"Failed to update chunk {chunk_id}: {e}")

+ 2 - 2
src/model/multimodal_embedding.py

@@ -22,10 +22,10 @@ class Embedding:
         # 获取模型配置
         self.model_provider = model_settings.model_provider
         self.embedding_model_name = model_name or model_settings.embedding_model_name
-        self.multi_embedding_model_name = model_name or model_settings.multimodal_embedding_model_name
+        self.multi_embedding_model_name = model_settings.multimodal_embedding_model_name
         self.base_url = model_settings.base_url
         self.api_key = api_key or model_settings.api_key
-        self.dashscope_api_key = api_key or model_settings.dashscope_api_key
+        self.dashscope_api_key =  model_settings.dashscope_api_key
     
     @observe(name="text_embedding", as_type="embedding")
     def get_text_embedding(self, text: str) -> List[float]:

+ 0 - 3
src/utils/http_client.py

@@ -91,9 +91,6 @@ class HTTPClient:
             "headers": headers,
             "files": files is not None  # 不记录文件内容,只记录是否有文件
         }
-        # 将请求报文写入D:\project\work\ragflow_plugs\book\output\temp下的request.txt文件
-        with open(r"D:\project\work\ragflow_plugs\book\output\temp\request.txt", "w", encoding="utf-8") as f:
-            f.write(str(request_info))
         
         logger.info(f"Sending request: {request_info}")
         

+ 36 - 0
src/utils/ragflow/ragflow_user_service.py

@@ -0,0 +1,36 @@
+"""
+RagFlow 用户关联服务
+
+根据 ragflow_user 表查询 RagFlow 用户 ID 与 API 密钥,供调用 RAGFlow API 时使用。
+"""
+from typing import Dict, Any, Optional
+
+from src.utils.mysql import get_global_mysql_client
+
+
+class RagflowUserService:
+    """RagFlow 用户关联服务"""
+
+    def __init__(self):
+        """初始化,使用全局 MySQL 客户端"""
+        self._db = get_global_mysql_client()
+
+    def get_ragflow_id_and_api_key(self, id: int) -> Optional[Dict[str, Any]]:
+        """根据主键 id 获取 ragflow_id(user_id)和 api_key"""
+        sql = """
+            SELECT ragflow_id, api_key FROM ragflow_user WHERE id = %s
+        """
+        result = self._db.fetch_one(sql, [id])
+        return result if result else None
+
+
+# 全局实例
+_ragflow_user_service: Optional[RagflowUserService] = None
+
+
+def get_ragflow_user_service() -> RagflowUserService:
+    """获取 RagflowUserService 实例(单例)"""
+    global _ragflow_user_service
+    if _ragflow_user_service is None:
+        _ragflow_user_service = RagflowUserService()
+    return _ragflow_user_service

+ 104 - 51
src/utils/vector_db/elasticsearch_adapter.py

@@ -4,9 +4,11 @@ Elasticsearch 向量数据库适配器
 实现 VectorDBClient 接口,提供与 Infinity 兼容的 ES 操作。
 """
 
+import re
 import threading
-from typing import Dict, Any, List, Optional
+from typing import Dict, Any, List, Optional, Tuple
 from .base import VectorDBClient
+from .result_util import UnifiedSearchResult, build_unified_result
 from src.conf.settings import es_settings, vector_db_settings
 from src.common.logging_config import get_logger
 
@@ -94,14 +96,14 @@ class ElasticsearchAdapter(VectorDBClient):
         
         将 Infinity 风格的查询转换为 ES 查询。
         """
-        index_name = self._get_index_name(table_name, database_name)
+        index_name = table_name
+        match_field = query.get("match_field", "content")
+        matching_text = query.get("matching_text") or ""
         
-        # 构建 ES 查询
+        # match 的 query 不能为 None,否则 ES 报 VALUE_NULL
         es_query = {
             "query": {
-                "match": {
-                    query.get("match_field", "content"): query.get("matching_text", "")
-                }
+                "match": {match_field: matching_text}
             },
             "size": query.get("topn", 10),
             "_source": output_fields
@@ -122,7 +124,7 @@ class ElasticsearchAdapter(VectorDBClient):
         
         使用 ES 的 knn 查询进行向量检索。
         """
-        index_name = self._get_index_name(table_name, database_name)
+        index_name = table_name
         vector_field = query.get("vector_field", "dense_vector")
         query_vector = query.get("query_vector", [])
         topn = query.get("topn", 10)
@@ -161,27 +163,34 @@ class ElasticsearchAdapter(VectorDBClient):
         
         使用 ES 的 bool 查询结合 knn 和 match。
         """
-        index_name = self._get_index_name(table_name, database_name)
+        index_name = table_name
         vector_field = query.get("vector_field", "dense_vector")
         query_vector = query.get("query_vector", [])
         match_field = query.get("match_field", "content")
-        matching_text = query.get("matching_text", "")
+        matching_text = query.get("matching_text")
+        if matching_text is not None and not isinstance(matching_text, str):
+            matching_text = str(matching_text)
+        elif matching_text is None:
+            matching_text = ""
+        else:
+            matching_text = matching_text.strip()
         topn = query.get("topn", 10)
         
+        # match 的 query 不能为 None,否则 ES 报 VALUE_NULL。无文案时仅用向量(match_all)
+        if matching_text:
+            text_clause = {
+                "match": {
+                    match_field: {"query": matching_text, "boost": 1.0}
+                }
+            }
+        else:
+            text_clause = {"match_all": {"boost": 1.0}}
+        
         # 构建混合查询
         es_query = {
             "query": {
                 "bool": {
-                    "should": [
-                        {
-                            "match": {
-                                match_field: {
-                                    "query": matching_text,
-                                    "boost": 1.0
-                                }
-                            }
-                        }
-                    ]
+                    "should": [text_clause]
                 }
             },
             "knn": {
@@ -205,7 +214,7 @@ class ElasticsearchAdapter(VectorDBClient):
         database_name: Optional[str] = None
     ) -> Any:
         """插入文档"""
-        index_name = self._get_index_name(table_name, database_name)
+        index_name = table_name
         
         # 批量插入
         operations = []
@@ -218,6 +227,20 @@ class ElasticsearchAdapter(VectorDBClient):
             return result
         return None
     
+    @staticmethod
+    def _parse_cond_for_term(cond: str) -> Optional[Tuple[str, str]]:
+        """
+        解析 cond 为 (field, value),用于构建 term 查询。
+        支持 "field = 'value'" 或 "field = \"value\"" 格式。
+        """
+        if not cond or not isinstance(cond, str):
+            return None
+        cond = cond.strip()
+        m = re.match(r"^(\w+)\s*=\s*['\"]([^'\"]*)['\"]$", cond)
+        if m:
+            return m.group(1), m.group(2)
+        return None
+
     def update(
         self,
         table_name: str,
@@ -228,25 +251,66 @@ class ElasticsearchAdapter(VectorDBClient):
         """
         更新文档
         
-        注意:ES 的更新方式与 Infinity 不同,这里使用 update_by_query。
+        使用 update_by_query,且 conflicts="proceed" 避免版本冲突导致整次操作失败。
+        若 cond 为 "field = 'value'" 形式,则用 term 查询精确匹配;否则回退到 query_string。
+        若 cond 为 "id = 'value'" 或 "_id = 'value'" 形式,则使用更高效的 update API。
         """
-        index_name = self._get_index_name(table_name, database_name)
+        index_name = table_name
         
-        # 将条件字符串解析为 ES 查询
-        # 简单实现:假设 cond 是 "field = 'value'" 格式
+        parsed = self._parse_cond_for_term(cond)
+        if parsed:
+            field, value = parsed
+            # 检查是否是id条件
+            if field in ['id', '_id']:
+                # 使用update API直接更新指定id的文档
+                try:
+                    result = self._client.update(
+                        index=index_name,
+                        id=value,
+                        body={"doc": data}
+                    )
+                    logger.info(f"update 执行: index={index_name}, id={value}, data={data}")
+                    return result
+                except Exception as e:
+                    logger.warning(f"使用update API更新失败,回退到update_by_query: {str(e)}")
+            
+            # 普通字段条件,使用term查询
+            es_query_clause = {"term": {field: value}}
+        else:
+            # 复杂条件,使用query_string
+            es_query_clause = {"query_string": {"query": cond or "*"}}
+        
+        # 构建update_by_query请求
         es_query = {
-            "query": {
-                "query_string": {
-                    "query": cond
-                }
-            },
+            "query": es_query_clause,
             "script": {
                 "source": "; ".join([f"ctx._source.{k} = params.{k}" for k in data.keys()]),
                 "params": data
             }
         }
         
-        result = self._client.update_by_query(index=index_name, body=es_query)
+        result = self._client.update_by_query(
+            index=index_name,
+            body=es_query,
+            conflicts="proceed"
+        )
+        # 打印实际执行的查询
+        logger.info(f"update_by_query 执行: index={index_name}, cond={cond}, data={data}")
+        
+        vc = result.get("version_conflicts", 0)
+        failures = result.get("failures", [])
+        if vc or failures:
+            logger.warning(
+                "update_by_query 存在版本冲突或失败: index=%s, version_conflicts=%s, "
+                "updated=%s, total=%s, failures_count=%s",
+                index_name, vc, result.get("updated", 0), result.get("total", 0), len(failures)
+            )
+            if failures:
+                for f in failures[:5]:
+                    logger.warning("update_by_query failure: %s", f)
+                if len(failures) > 5:
+                    logger.warning("... 及其他 %s 个 failures", len(failures) - 5)
+        
         return result
     
     def get_status(self) -> Dict[str, Any]:
@@ -274,29 +338,18 @@ class ElasticsearchAdapter(VectorDBClient):
     
     def _convert_result(self, es_result: Dict, output_fields: List[str]) -> Any:
         """
-        转换 ES 结果为统一格式
-        
-        返回类似 Infinity 的结果结构
+        转换 ES 结果为统一格式:仅含 output_fields + score 的 JSON 数组。
         """
         hits = es_result.get("hits", {}).get("hits", [])
-        
-        class MockResult:
-            """模拟 Infinity 结果对象"""
-            def __init__(self, data):
-                self._data = data
-            
-            def to_result(self):
-                return self._data
-        
-        # 转换为统一格式
-        results = []
-        for hit in hits:
-            item = hit.get("_source", {})
-            item["_score"] = hit.get("_score", 0)
-            item["_id"] = hit.get("_id", "")
-            results.append(item)
-        
-        return MockResult(results)
+        rows = []
+        for h in hits:
+            row = dict(h.get("_source", {}))
+            row["_score"] = h.get("_score", 0)
+            rows.append(row)
+        results = build_unified_result(
+            rows, output_fields, lambda r: r.get("_score", 0)
+        )
+        return UnifiedSearchResult(results)
     
     # ========== ES 特有方法(扩展) ==========
     

+ 31 - 5
src/utils/vector_db/infinity_adapter.py

@@ -2,11 +2,14 @@
 Infinity 向量数据库适配器
 
 封装现有的 InfinityClient,实现 VectorDBClient 接口。
+返回与 ES 统一的格式:仅含 output_fields + score 的 JSON 数组。
 """
 
 from typing import Dict, Any, List, Optional
 from .base import VectorDBClient
+from .result_util import UnifiedSearchResult, build_unified_result
 from src.utils.infinity import InfinityClient, get_client as get_infinity_client, close_client as close_infinity_client
+from src.utils.infinity.result_util import convert_to_basic_types
 from src.conf.settings import vector_db_settings
 
 
@@ -55,6 +58,26 @@ class InfinityAdapter(VectorDBClient):
         """获取底层 InfinityClient"""
         return self._client
     
+    def _to_unified(
+        self,
+        raw_result: Any,
+        output_fields: List[str],
+    ) -> UnifiedSearchResult:
+        """将 Infinity 原始结果为统一格式:output_fields + score."""
+        data = convert_to_basic_types(raw_result.to_result())
+        if not data or not isinstance(data, (list, tuple)):
+            return UnifiedSearchResult([])
+        rows = data[0] if (len(data) > 0 and isinstance(data[0], (list, tuple))) else data
+        if not isinstance(rows, (list, tuple)):
+            return UnifiedSearchResult([])
+        rows = [r for r in rows if isinstance(r, dict)]
+
+        def get_score(r: Dict) -> float:
+            return r.get("score", r.get("_score", r.get("distance", 0.0)))
+
+        results = build_unified_result(rows, output_fields, get_score)
+        return UnifiedSearchResult(results)
+
     def search(
         self,
         table_name: str,
@@ -63,8 +86,9 @@ class InfinityAdapter(VectorDBClient):
         database_name: Optional[str] = None
     ) -> Any:
         """全文搜索"""
-        return self._client.search(table_name, output_fields, query, database_name)
-    
+        raw = self._client.search(table_name, output_fields, query, database_name)
+        return self._to_unified(raw, output_fields)
+
     def vector_search(
         self,
         table_name: str,
@@ -73,8 +97,9 @@ class InfinityAdapter(VectorDBClient):
         database_name: Optional[str] = None
     ) -> Any:
         """向量搜索"""
-        return self._client.vector_search(table_name, output_fields, query, database_name)
-    
+        raw = self._client.vector_search(table_name, output_fields, query, database_name)
+        return self._to_unified(raw, output_fields)
+
     def hybrid_search(
         self,
         table_name: str,
@@ -83,7 +108,8 @@ class InfinityAdapter(VectorDBClient):
         database_name: Optional[str] = None
     ) -> Any:
         """混合搜索"""
-        return self._client.hybrid_search(table_name, output_fields, query, database_name)
+        raw = self._client.hybrid_search(table_name, output_fields, query, database_name)
+        return self._to_unified(raw, output_fields)
     
     def insert(
         self,

+ 44 - 0
src/utils/vector_db/result_util.py

@@ -0,0 +1,44 @@
+"""
+向量库统一结果格式
+
+ES / Infinity 等适配器将搜索结果转换为同一结构:
+仅包含 output_fields + score 的 JSON 数组。
+"""
+
+from typing import Dict, Any, List, Callable
+
+
+class UnifiedSearchResult:
+    """统一搜索结果,to_result() 返回 output_fields + score 的列表"""
+
+    def __init__(self, data: List[Dict[str, Any]]):
+        self._data = data
+
+    def to_result(self) -> List[Dict[str, Any]]:
+        return self._data
+
+
+def build_unified_result(
+    rows: List[Any],
+    output_fields: List[str],
+    get_score_fn: Callable[[Dict[str, Any]], float],
+) -> List[Dict[str, Any]]:
+    """
+    将原始行列表转为统一格式:每项仅含 output_fields + score。
+
+    Args:
+        rows: 原始行列表(每行 dict 或可 get 的对象)
+        output_fields: 查询字段列表
+        get_score_fn: (row) -> float,从行中取分
+
+    Returns:
+        [{"f1": v1, "f2": v2, ..., "score": s}, ...]
+    """
+    results = []
+    for row in rows:
+        if not isinstance(row, dict):
+            continue
+        item = {f: row.get(f) for f in output_fields}
+        item["score"] = get_score_fn(row)
+        results.append(item)
+    return results

部分文件因为文件数量过多而无法显示