auto backup: 2026-06-05 08:10:01

This commit is contained in:
ai_member_only 2026-06-05 08:10:01 +08:00
parent 01072f4003
commit fa5d896a2c
36 changed files with 91929 additions and 82 deletions

View File

@ -233,7 +233,7 @@ interactive-component-json/
| 造句互动 | mid_sentence_makeSentence | WfHMbzBYPau6hZsmFD0cBwWuncc | | 造句互动 | mid_sentence_makeSentence | WfHMbzBYPau6hZsmFD0cBwWuncc |
| 发音互动 | mid_pron_pron | CeH5bO3FhaG5cNsPZ4Xc72axnrb | | 发音互动 | mid_pron_pron | CeH5bO3FhaG5cNsPZ4Xc72axnrb |
### 核心互动组件已实现12种,其余待定义) ### 核心互动组件已实现13种,其余待定义)
- **合作阅读** `core_reading_order` — 7步LLM调用输出 taskData/textData/sequenceData/learningData - **合作阅读** `core_reading_order` — 7步LLM调用输出 taskData/textData/sequenceData/learningData
- **看图选词** `core_reading_imageDrag` — 5步LLM调用输出 taskData/questionGroupData/studyData - **看图选词** `core_reading_imageDrag` — 5步LLM调用输出 taskData/questionGroupData/studyData
- **口语快答** `core_speaking_reply` — 7步LLM调用输出 taskData/settingData/configData/learningData - **口语快答** `core_speaking_reply` — 7步LLM调用输出 taskData/settingData/configData/learningData
@ -246,8 +246,9 @@ interactive-component-json/
- **看图撰写** `core_writing_imgWrite` — 6步LLM调用输出 taskInfo/textInfo/studyInfo/evalInfo - **看图撰写** `core_writing_imgWrite` — 6步LLM调用输出 taskInfo/textInfo/studyInfo/evalInfo
- **邮件组句** `core_writing_questionMakeSentence` — 6步LLM调用输出 taskInfo/textInfo/studyInfo/evalInfo - **邮件组句** `core_writing_questionMakeSentence` — 6步LLM调用输出 taskInfo/textInfo/studyInfo/evalInfo
- **邮件撰写** `core_writing_questionWrite` — 6步LLM调用输出 taskInfo/textInfo/studyInfo/evalInfo - **邮件撰写** `core_writing_questionWrite` — 6步LLM调用输出 taskInfo/textInfo/studyInfo/evalInfo
- **看图拼词** `core_writing_imgMakeWord` — 7步(Step5规则),输出 taskInfo/preDialog/imageInfo/questionList/studyInfo
其余7种详见 `scripts/match_component.py` 中的 `CORE_INTERACTION_TYPES` 其余6种详见 `scripts/match_component.py` 中的 `CORE_INTERACTION_TYPES`
## LLM 配置 ## LLM 配置

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -110,6 +110,18 @@ bitable数据可用于验证和参考。
- 组句类inputType=sentence(optionList+answer)撰写类inputType=write(answerText+hint) - 组句类inputType=sentence(optionList+answer)撰写类inputType=write(answerText+hint)
- prompt模板: `prompts/core_writing_img_prompts.py`, `prompts/core_writing_question_prompts.py` - prompt模板: `prompts/core_writing_img_prompts.py`, `prompts/core_writing_question_prompts.py`
### v8.1 — 看图拼词1种已完成
| 类型 | cType | LLM步数 | 输出字段 | bitable wiki |
|------|-------|---------|---------|-------------|
| 看图拼词 | core_writing_imgMakeWord | 7 (Step5规则) | taskInfo, preDialog, imageInfo, questionList, studyInfo | QDyhwiY7PiSER8kHGZncDsrinde |
- 输入: 单一教研配置块(【任务标题】【任务背景】【任务描述】【通关知识】【前置对话】【互动问题】)
- 中间步骤: 学习流程配置(LLM生成) + 互动描述(LLM生成)
- Step 5 imageInfo 为规则生成: `{"questionImg": "{cId}_question.png"}`
- 独立7步流水线 `generate_core_writing_imgmakeword()`
- prompt模板: `prompts/core_writing_imgmakeword_prompts.py`
### 其他未实现类型 ### 其他未实现类型
- 口语独白、口语探讨 - 口语独白、口语探讨
- 写作互动 - 写作互动
@ -246,3 +258,26 @@ bitable数据可用于验证和参考。
} }
} }
``` ```
## v14 — 重新生成拆分 + 知识点推送失败判定(已完成)
### 重新生成拆分
将原来的单一"重新生成"按钮拆分为两个独立操作:
- **重新生成组件**: 只重新生成 jsonData/core 字段,保留现有 kpInfo 不变
- **重新生成知识点**: 只重新生成 kpInfo保留现有 jsonData/core 字段不变
实现方式:
- 前端拆分为 `regenerateComponentJson(idx)``regenerateComponentKp(idx)` 两个函数
- 后端 `/api/regenerate` 通过 `mode` 参数区分:`mode='component'` 或 `mode='kp'`
- `mode='component'`: 调用 `generate_component()` 但跳过 kpInfo
- `mode='kp'`: 只调用 `generate_kp_info()`
- `knowledge_text` 嵌入到 HTML 报告的 COMPONENTS 前端数据中,供重新生成知识点使用
### 知识点推送失败判定
- 单个组件"知识推送"按钮: 无知识点或所有 kpId 为空时,标记为推送**失败**(原来算成功/info
- 批量"推送全部知识点": 无知识点或 kpId 为空时,计入 failCount 并显示失败详情(原来静默跳过或算成功)
### 涉及文件
- `scripts/pipeline.py`: report entry 添加 `knowledge_text` 字段
- `scripts/html_report.py`: 前端按钮/面板/JS 逻辑拆分 + 推送失败判定
- `scripts/proxy_server.py`: `_do_regenerate` 按 mode 分流

View File

@ -0,0 +1,340 @@
#!/usr/bin/env python3
"""
看图拼词 (core_writing_imgMakeWord) 多步 LLM prompt 模板
生成流水线7Step 5 为规则生成
Step 1: 学习流程配置 (intermediate text)
Step 2: 互动描述 (intermediate text)
Step 3: taskInfo JSON
Step 4: preDialog JSON
Step 5: imageInfo 规则生成 (不需要 prompt)
Step 6: questionList JSON
Step 7: studyInfo JSON
"""
# ── Step 1: 学习流程配置 ──
STEP1_LEARNING_FLOW = r"""你是一名儿童英语学习助手,负责为学习者生成互动式的巩固练习题。请根据以下信息生成练习内容:
**输入信息**
$任务信息$
**生成要求**
1. 为每个通关知识的知识词汇生成一个小题包含以下结构
```
# 小题 X
## 互动题目
1. 抄写题
Brainy如图所示首先/接着/最后要帮 Sarah 整理的物品是"中文词"该词的英文是"英文词"先来抄写一下这个词吧
2. 听写题
Brainy很好接下来记一下"英文词"的拼写"字母拆分"然后看着图片试试听写出"中文词"这个词吧
Brainy没错"中文词"对应的英文词汇是"英文词"你的拼写完全正确
```
2. 每个知识词汇生成一题顺序与知识词汇列表一致
3. 拼写拆解用字母加"-"表示多个单词间用空格分隔 teddy bear t-e-d-d-y b-e-a-r
4. 最后生成一个"完结反馈"
```
Brainy太棒了你复习了所有的关键知识点拼对了每一个词相信下一次你一定可以做得更好
```
5. 输出中文引号全部使用英文引号""不要使用中文引号"「」"
6. 不输出额外说明或过程只生成完整互动题内容
### 三、示例输出(参考格式)
```
# 小题 1
## 互动题目
1. 抄写题
Brainy如图所示首先要帮 Sally 整理的物品是"照片""照片"的英文是"photo"先来抄写一下这个词吧
2. 听写题
Brainy很好接下来记一下"photo"的拼写"p-h-o-t-o"然后看着图片试试听写出"照片"这个词吧
Brainy没错"照片"对应的英文词汇是"photo"你的拼写完全正确
# 小题 2
## 互动题目
1. 抄写题
Brainy如图所示接着要帮 Sally 整理的物品是"奖牌""奖牌"的英文是"medal"先来抄写一下这个词吧
2. 听写题
Brainy很好接下来记一下"medal"的拼写"m-e-d-a-l"然后看着图片试试听写出"奖牌"这个词吧
Brainy没错"奖牌"对应的英文词汇是"medal"你的拼写完全正确
# 完结反馈
Brainy太棒了你复习了所有的关键知识点拼对了每一个词相信下一次你一定可以做得更好
```
#输入
任务信息
$任务信息$
# 输出
现在请按照上述信息和要求直接输出目标内容"""
# ── Step 2: 互动描述 ──
STEP2_INTERACTION_DESC = r"""# 任务目标
你是一个儿童英语学习内容生成助手现在需要根据给定的任务信息知识词汇和前置对话生成一句符合场景的英文描述句并附上对应中文翻译
# 输入信息
* 任务信息
$任务信息$
* 前置对话
$前置对话$
# 生成要求
只输出
一句完整自然符合儿童英语学习场景的英文描述句
对应中文翻译放在括号中
英文句子要求
必须结合任务名称知识中的词汇
尽量自然使用多个知识词汇
句子简洁口语化符合少儿英语水平
前置对话场景保持一致
中文翻译要求
与英文句子语义完全对应
表达自然通顺
输出格式固定如下禁止输出任何解释
English sentence.中文翻译
# 输出示例
Dear Kate, I'd like to say thank you in this letter. Thank you for taking care of me. I have to go away now. Let's meet in the holidays.亲爱的凯特我想在这封信里向你表达感谢感谢你对我的照顾我现在必须离开了咱们假期见"""
# ── Step 3: taskInfo ──
STEP3_TASK_INFO = r"""# 字段内容要求
{
"cId": "{ID}", // 字符串类型
"cType": "core_writing_imgMakeWord",
"title": {任务信息}中的任务名称,
"sceneDesc": {任务信息}中的任务背景,
"cDesc": "xxx", // {互动描述}的全部内容
"key": [ // {任务信息}中的通关知识
{
"desc": "",
"keyList": [
{
"type": "default", //固定
"content": 知识点内容英文部分,
"desc": 知识点解释中文部分如果是单词前面会带有英文缩写的词性标识
},
... ...
]
},
...
],
"resourceMapping"(抽取出{角色配置}中的全部 name:id映射 可为空id必须为int类型):
{
样例:
"Tom":43,
"Jerry":10
}
}
# 输出格式
- 整个输出必须在一行内完成**不要换行**
- **不要使用 markdown 代码块** ```json包裹输出
样例:
{"cId":"0000999","cType":"core_writing_imgMakeWord","title":"帮助sarah整理物品","sceneDesc":"场景描述,场景描述,看图说话场景描述","cDesc":"xxxxxx","key":[{"desc":"","keyList":[{"type":"default","content":"passport"},{"type":"default","content":"suitcase"},{"type":"default","content":"clothes"},{"type":"default","content":"pack"}]}],"resourceMapping":{"brainy":291,"jack":210}}
# 输入
{ID}:
$ID$
{任务信息}
$任务信息$
{互动描述}
$互动描述$
{角色配置}
$角色配置$
# 输出
现在基于输入字段和格式要求生成***可以直接解析的单行json文本******注意前后不要输出任何额外符号***
如果{任务配置}中没有有效文本则输出"""""
# ── Step 4: preDialog ──
STEP4_PRE_DIALOG = r"""# 字段内容要求
[ // {互动配置}中的前置对话中抽取
{
"type": "npc",
"npcName": "xxx",
"content": "xxxxxxxx"
},
{
"type": "user", // 如果是user则没有npcName
"content": "xxxxxxxx"
},
......
]
# 输出格式
- 整个输出必须在一行内完成**不要换行**
- **不要使用 markdown 代码块** ```json包裹输出
样例:
[{"type":"npc","npcName":"Brainy","content":"OK,let's try to the scene the scene together."},{"type":"npc","npcName":"Amy","content":"OK,let's try to the scene the scene together."},{"type":"npc","npcName":"Brainy","content":"OK,let's try to the scene the scene together."}]
# 输入
{互动配置}
$前置对话$
# 输出
现在基于输入字段和格式要求生成***可以直接解析的单行json文本******注意前后不要输出任何额外符号***
如果{任务配置}中没有有效文本则输出"""""
# ── Step 6: questionList ──
STEP6_QUESTION_LIST = r"""# 字段内容要求
[ // {互动配置}中的内容抽取为下面格式有几个题目则有几个item
{
"desc": "(x)", // 题干中抽取
"imageInfo": {
"questionImg": "{ID}_question_{图片编号}.png" // 图片编号从互动问题中该小题对应的抽取
},
"optionList": [ // 选项中抽取
{
"option": "a"
},
{
"option": "b"
},
......
],
"answerText": "xxxxx" // 答案中抽取
},
......
]
# 输出格式
- 整个输出必须在一行内完成**不要换行**
- **不要使用 markdown 代码块** ```json包裹输出
样例:
[{"desc":"(5)","imageInfo":{"questionImg":"012310_question_00.png"},"optionList":[{"option":"o"},{"option":"h"},{"option":"x"},{"option":"P"},{"option":"t"},{"option":"o"},{"option":"k"}],"answerText":"Photo"},{"desc":"(5)","imageInfo":{"questionImg":"012310_question_01.png"},"optionList":[{"option":"m"},{"option":"d"},{"option":"l"},{"option":"q"},{"option":"e"},{"option":"d"},{"option":"m"},{"option":"a"}],"answerText":"medal"},{"desc":"(6)","imageInfo":{"questionImg":"012310_question_02.png"},"optionList":[{"option":"a"},{"option":"j"},{"option":"k"},{"option":"t"},{"option":"e"},{"option":"v"},{"option":"c"},{"option":"z"},{"option":"a"}],"answerText":"jacket"},{"desc":"(5) (4)","imageInfo":{"questionImg":"012310_question_03.png"},"optionList":[{"option":"b"},{"option":"e"},{"option":"t"},{"option":"r"},{"option":"y"},{"option":"a"},{"option":"d"},{"option":"e"},{"option":"t"},{"option":"d"},{"option":"y"}],"answerText":"teddy bear"}]
# 输入
{互动配置}
$互动问题$
{ID}:
$ID$
# 输出
现在基于输入字段和格式要求生成***可以直接解析的单行json文本******注意前后不要输出任何额外符号***
如果{任务配置}中没有有效文本则输出"""""
# ── Step 7: studyInfo ──
STEP7_STUDY_INFO = r"""# 字段内容要求
{
"learningPart": [ // {学习流程配置}中有几个小题则learningPart中有几个元素
{
"type": "npc",
"npcName": "Brainy",
"content": "xxxx" // {任务信息}通关知识和该小题对应的单词
"image": "{ID}_question_{两位自增索引}.png" // 索引从00开始
"learning": [
{
"question": {
"type": "npc",
"npcName": "Brainy",
"content": "xxx", // 抄写题题干
"desc": "xxx", // {任务信息}通关知识和该小题对应的单词
"answerText": "xxx", // {任务信息}通关知识和该小题对应的单词
},
"optionList": [ // 单词的所有字母
{"option": "a"},
{"option": "b"},
......
]
},
{
"question": {
"type": "npc",
"npcName": "Brainy",
"content": "xxx", // 听写题题干
"desc": "x", // x为{任务信息}通关知识和该小题对应的单词的字母个数
"answerText": "xxx", // {任务信息}通关知识和该小题对应的单词
},
"optionList": [ // 单词的所有字母
{"option": "a"},
{"option": "b"},
......
],
"feedback": {
"type": "npc",
"npcName": "Brainy",
"content": "xxxxxx"
}
},
{
......
"image": "{ID}_question_01.png"
......
},
......
"closing": { //{学习流程配置}中的 完结反馈中提取
"type": "npc",
"npcName": "Brainy",
"content": "xxxxx"
}
}
# 输出格式
- 整个输出必须在一行内完成**不要换行**
- **不要使用 markdown 代码块** ```json包裹输出
样例:
{"learningPart":[{"type":"npc","npcName":"Brainy","content":"Photo","image":"012310_question_00.png","learning":[{"question":{"type":"npc","npcName":"Brainy","content":"如图所示,首先要帮 Sally 整理的物品是「照片」「照片」的英文是「photo」。先来抄写一下这个词吧","desc":"Photo","answerText":"Photo"},"optionList":[{"option":"P"},{"option":"h"},{"option":"o"},{"option":"t"},{"option":"o"}]},{"question":{"type":"npc","npcName":"Brainy","content":"很好。接下来记一下「photo」的拼写是「p-h-o-t-o」。然后看着图片试试听写出「照片」这个词吧","desc":"(5)","answerText":"Photo"},"optionList":[{"option":"P"},{"option":"h"},{"option":"o"},{"option":"t"},{"option":"o"}],"feedback":{"type":"npc","npcName":"Brainy","content":"没错!「照片」对应的英文词汇是「photo」,你的拼写完全正确!"}}]},{"type":"npc","npcName":"Brainy","content":"medal","image":"012310_question_01.png","learning":[{"question":{"type":"npc","npcName":"Brainy","content":" 如图所示,接着要帮 Sally 整理的物品是「奖牌」「奖牌」的英文是「medal」。先来抄写一下这个词吧","desc":"medal","answerText":"medal"},"optionList":[{"option":"m"},{"option":"e"},{"option":"d"},{"option":"a"},{"option":"l"}]},{"question":{"type":"npc","npcName":"Brainy","content":"很好。接下来记一下「medal」的拼写是「m-e-d-a-l」。然后看着图片试试听写出「奖牌」这个词吧","desc":"(5)","answerText":"medal"},"optionList":[{"option":"m"},{"option":"e"},{"option":"d"},{"option":"a"},{"option":"l"}],"feedback":{"type":"npc","npcName":"Brainy","content":"没错「奖牌」对应的英文词汇是「medal」你的拼写完全正确"}}]},{"type":"npc","npcName":"Brainy","content":"jetcak","image":"012310_question_02.png","learning":[{"question":{"type":"npc","npcName":"Brainy","content":" 如图所示,接着要帮 Sally 整理的物品是「奖牌」「奖牌」的英文是「medal」。先来抄写一下这个词吧","desc":"jetcak","answerText":"jetcak"},"optionList":[{"option":"j"},{"option":"e"},{"option":"t"},{"option":"c"},{"option":"a"},{"option":"k"}]},{"question":{"type":"npc","npcName":"Brainy","content":"很好。接下来记一下「medal」的拼写是「m-e-d-a-l」。然后看着图片试试听写出「奖牌」这个词吧","desc":"(6)","answerText":"jetcak"},"optionList":[{"option":"j"},{"option":"e"},{"option":"t"},{"option":"c"},{"option":"a"},{"option":"k"}],"feedback":{"type":"npc","npcName":"Brainy","content":"没错「夹克」对应的英文词汇是「jetcak」你的拼写完全正确"}}]},{"type":"npc","npcName":"Brainy","content":"teddy bear","image":"012310_question_03.png","learning":[{"question":{"type":"npc","npcName":"Brainy","content":"很好。接下来记一下「medal」的拼写是「m-e-d-a-l」。然后看着图片试试听写出「奖牌」这个词吧","desc":"teddy bear","answerText":"teddy bear"},"optionList":[{"option":"t"},{"option":"e"},{"option":"d"},{"option":"d"},{"option":"y"},{"option":"b"},{"option":"e"},{"option":"a"},{"option":"r"}]},{"question":{"type":"npc","npcName":"Brainy","content":"很好。接下来记一下「medal」的拼写是「m-e-d-a-l」。然后看着图片试试听写出「奖牌」这个词吧","desc":"(5) (4)","answerText":"teddy bear"},"optionList":[{"option":"t"},{"option":"e"},{"option":"d"},{"option":"d"},{"option":"y"},{"option":"b"},{"option":"e"},{"option":"a"},{"option":"r"}],"feedback":{"type":"npc","npcName":"Brainy","content":"没错「泰迪熊」对应的英文词汇是「teddy bear」你的拼写完全正确"}}]}],"closing":{"type":"npc","npcName":"Brainy","content":"太棒了!你复习了所有的关键知识点,拼对了每一个词,相信下一次你一定可以做得更好!"}}
# 输入
{学习流程配置}
$学习流程配置$
{任务信息}:
$任务信息$
{ID}:
$ID$
# 输出
现在基于输入字段和格式要求生成***可以直接解析的单行json文本******注意前后不要输出任何额外符号***直接以"{"开头输出一定保持单行如果{学习流程配置}中没有有效文本则输出"""""
# ══════════════════════════════════════════════════════════════════════════════
# Steps Registry & Builder
# ══════════════════════════════════════════════════════════════════════════════
CORE_WRITING_IMGMAKEWORD_STEPS = {
"step1_learning_flow": {"template": STEP1_LEARNING_FLOW, "description": "学习流程配置", "output_type": "text"},
"step2_interaction_desc": {"template": STEP2_INTERACTION_DESC, "description": "互动描述", "output_type": "text"},
"step3_task_info": {"template": STEP3_TASK_INFO, "description": "taskInfo", "output_type": "json"},
"step4_pre_dialog": {"template": STEP4_PRE_DIALOG, "description": "preDialog", "output_type": "json"},
"step6_question_list": {"template": STEP6_QUESTION_LIST, "description": "questionList", "output_type": "json"},
"step7_study_info": {"template": STEP7_STUDY_INFO, "description": "studyInfo", "output_type": "json"},
}
def build_writing_imgmakeword_prompt(step_key, replacements):
"""Build a prompt for a given step by replacing $placeholder$ tokens."""
info = CORE_WRITING_IMGMAKEWORD_STEPS[step_key]
prompt = info["template"]
for key, val in replacements.items():
prompt = prompt.replace(f"${key}$", str(val) if val else "")
return prompt

View File

@ -130,6 +130,11 @@ from prompts.core_writing_question_prompts import (
build_writing_question_prompt, build_writing_question_prompt,
) )
from prompts.core_writing_imgmakeword_prompts import (
CORE_WRITING_IMGMAKEWORD_STEPS,
build_writing_imgmakeword_prompt,
)
# Fallback: 旧版 few-shot 方式(对无 prompt 模板的类型) # Fallback: 旧版 few-shot 方式(对无 prompt 模板的类型)
FALLBACK_SYSTEM_PROMPT = """你是一个互动组件配置JSON生成专家。根据教研配置文本生成对应的 jsonData JSON。 FALLBACK_SYSTEM_PROMPT = """你是一个互动组件配置JSON生成专家。根据教研配置文本生成对应的 jsonData JSON。
@ -382,7 +387,7 @@ def generate_kp_info(cType, cId, knowledge_text, llm_client=None, level=None):
return match_knowledge_points(knowledge_text, cType, cId, level=level, llm_client=llm_client) return match_knowledge_points(knowledge_text, cType, cId, level=level, llm_client=llm_client)
CORE_TYPES = {"core_reading_order", "core_reading_imageDrag", "core_speaking_reply", "core_speaking_inquiry", "core_speaking_image", "core_listening_order", "core_listening_drag", "core_listening_choose", "core_writing_imgMakeSentence", "core_writing_imgWrite", "core_writing_questionMakeSentence", "core_writing_questionWrite"} CORE_TYPES = {"core_reading_order", "core_reading_imageDrag", "core_speaking_reply", "core_speaking_inquiry", "core_speaking_image", "core_listening_order", "core_listening_drag", "core_listening_choose", "core_writing_imgMakeSentence", "core_writing_imgWrite", "core_writing_questionMakeSentence", "core_writing_questionWrite", "core_writing_imgMakeWord"}
def parse_core_reading_fields(teaching_config): def parse_core_reading_fields(teaching_config):
@ -1995,6 +2000,162 @@ def generate_core_writing_question_write(cId, teaching_config, character_map=Non
return _generate_core_writing(cId, "core_writing_questionWrite", teaching_config, character_map, llm_client, level=level) return _generate_core_writing(cId, "core_writing_questionWrite", teaching_config, character_map, llm_client, level=level)
# ============ 看图拼词 (core_writing_imgMakeWord) ============
def parse_core_writing_imgmakeword_fields(teaching_config):
"""
从组件配置文本中提取 core_writing_imgMakeWord 所需的输入字段
教研配置为单一文本块包含 任务标题任务背景任务描述通关知识前置对话互动问题 sections
Returns:
dict: 字段名
"""
fields = parse_teaching_config_fields(teaching_config)
任务标题 = fields.get("任务标题", "")
任务背景 = fields.get("任务背景", "")
任务描述 = fields.get("任务描述", "")
通关知识 = fields.get("通关知识", "")
前置对话 = fields.get("前置对话", "")
互动问题 = fields.get("互动问题", "")
# 构建任务信息composite
任务信息_parts = []
if 任务标题:
任务信息_parts.append(f"【任务标题】\n{任务标题}")
if 任务背景:
任务信息_parts.append(f"【任务背景】\n{任务背景}")
if 任务描述:
任务信息_parts.append(f"【任务描述】\n{任务描述}")
if 通关知识:
任务信息_parts.append(f"【通关知识】\n{通关知识}")
任务信息 = "\n\n".join(任务信息_parts)
return {
"任务标题": 任务标题,
"任务背景": 任务背景,
"任务描述": 任务描述,
"通关知识": 通关知识,
"前置对话": 前置对话,
"互动问题": 互动问题,
"任务信息": 任务信息,
}
def generate_core_writing_imgmakeword(cId, teaching_config, character_map=None, llm_client=None, level=None):
"""
看图拼词core_writing_imgMakeWord7步生成流水线
Step 1: 学习流程配置 中间文本
Step 2: 互动描述 中间文本
Step 3: taskInfo JSON
Step 4: preDialog JSON
Step 5: imageInfo 规则生成
Step 6: questionList JSON
Step 7: studyInfo JSON
Returns:
dict: {
"taskInfo": dict, "preDialog": list,
"imageInfo": dict, "questionList": list,
"studyInfo": dict, "kpInfo": None,
"intermediate": {"学习流程配置": str, "互动描述": str}
}
"""
if llm_client is None:
from llm_client import get_client
llm_client = get_client()
cType = "core_writing_imgMakeWord"
# 解析输入字段
fields = parse_core_writing_imgmakeword_fields(teaching_config)
# 角色配置:优先教研配置,回退到 character_map
角色配置 = ""
if character_map:
角色配置 = "\n".join(f"{k}{v}" for k, v in character_map.items())
logger.info(f"[core_writing_imgMakeWord] 开始7步生成: cId={cId}")
# ── Step 1: 学习流程配置 ──
logger.info(f"[core_writing_imgMakeWord] Step 1/7: 学习流程配置")
prompt1 = build_writing_imgmakeword_prompt("step1_learning_flow", {
"任务信息": fields["任务信息"],
})
学习流程配置, usage1 = llm_client.call("", prompt1, max_tokens=8192, temperature=0.3)
学习流程配置 = 学习流程配置.strip()
logger.info(f" Step 1 完成: {len(学习流程配置)} 字符, tokens={usage1}")
# ── Step 2: 互动描述 ──
logger.info(f"[core_writing_imgMakeWord] Step 2/7: 互动描述")
prompt2 = build_writing_imgmakeword_prompt("step2_interaction_desc", {
"任务信息": fields["任务信息"],
"前置对话": fields["前置对话"],
})
互动描述, usage2 = llm_client.call("", prompt2, max_tokens=4096, temperature=0.3)
互动描述 = 互动描述.strip()
logger.info(f" Step 2 完成: {len(互动描述)} 字符, tokens={usage2}")
# ── Step 3: taskInfo ──
logger.info(f"[core_writing_imgMakeWord] Step 3/7: taskInfo")
prompt3 = build_writing_imgmakeword_prompt("step3_task_info", {
"ID": str(cId),
"任务信息": fields["任务信息"],
"互动描述": 互动描述,
"角色配置": 角色配置,
})
taskInfo, usage3 = llm_client.call_for_json("", prompt3, max_tokens=4096, temperature=0.1)
logger.info(f" Step 3 完成: taskInfo keys={list(taskInfo.keys()) if isinstance(taskInfo, dict) else type(taskInfo)}, tokens={usage3}")
# ── Step 4: preDialog ──
logger.info(f"[core_writing_imgMakeWord] Step 4/7: preDialog")
prompt4 = build_writing_imgmakeword_prompt("step4_pre_dialog", {
"前置对话": fields["前置对话"],
})
preDialog, usage4 = llm_client.call_for_json("", prompt4, max_tokens=4096, temperature=0.1)
logger.info(f" Step 4 完成: preDialog len={len(preDialog) if isinstance(preDialog, list) else 'dict'}, tokens={usage4}")
# ── Step 5: imageInfo (规则生成) ──
logger.info(f"[core_writing_imgMakeWord] Step 5/7: imageInfo (规则生成)")
imageInfo = {"questionImg": f"{cId}_question.png"}
logger.info(f" Step 5 完成: imageInfo={imageInfo}")
# ── Step 6: questionList ──
logger.info(f"[core_writing_imgMakeWord] Step 6/7: questionList")
prompt6 = build_writing_imgmakeword_prompt("step6_question_list", {
"互动问题": fields["互动问题"],
"ID": str(cId),
})
questionList, usage6 = llm_client.call_for_json("", prompt6, max_tokens=8192, temperature=0.1)
logger.info(f" Step 6 完成: questionList len={len(questionList) if isinstance(questionList, list) else 'dict'}, tokens={usage6}")
# ── Step 7: studyInfo ──
logger.info(f"[core_writing_imgMakeWord] Step 7/7: studyInfo")
prompt7 = build_writing_imgmakeword_prompt("step7_study_info", {
"学习流程配置": 学习流程配置,
"任务信息": fields["任务信息"],
"ID": str(cId),
})
studyInfo, usage7 = llm_client.call_for_json("", prompt7, max_tokens=16384, temperature=0.1)
logger.info(f" Step 7 完成: studyInfo keys={list(studyInfo.keys()) if isinstance(studyInfo, dict) else type(studyInfo)}, tokens={usage7}")
logger.info(f"[core_writing_imgMakeWord] 7步生成完成: cId={cId}")
return {
"taskInfo": taskInfo,
"preDialog": preDialog,
"imageInfo": imageInfo,
"questionList": questionList,
"studyInfo": studyInfo,
"kpInfo": None,
"intermediate": {
"学习流程配置": 学习流程配置,
"互动描述": 互动描述,
},
}
def generate_component(component, character_map=None, section_char_map=None, llm_client=None, level=None): def generate_component(component, character_map=None, section_char_map=None, llm_client=None, level=None):
""" """
一站式生成单个组件的 jsonData + kpInfo 一站式生成单个组件的 jsonData + kpInfo
@ -2171,6 +2332,20 @@ def generate_component(component, character_map=None, section_char_map=None, llm
"intermediate": result["intermediate"], "intermediate": result["intermediate"],
} }
elif cType == "core_writing_imgMakeWord":
result = generate_core_writing_imgmakeword(**_core_call_args)
core_out = {
**_core_base,
"jsonData": result["taskInfo"],
"taskInfo": result["taskInfo"],
"preDialog": result["preDialog"],
"imageInfo": result["imageInfo"],
"questionList": result["questionList"],
"studyInfo": result["studyInfo"],
"kpInfo": result["kpInfo"],
"intermediate": result["intermediate"],
}
else: else:
raise ValueError(f"核心互动类型 {cType} 尚未实现生成器") raise ValueError(f"核心互动类型 {cType} 尚未实现生成器")

View File

@ -53,6 +53,7 @@ def _build_component_data(result):
"status": status, "status": status,
"raw_config": result.get("raw_config", ""), "raw_config": result.get("raw_config", ""),
"config_info": result.get("config_info", ""), "config_info": result.get("config_info", ""),
"knowledge_text": result.get("knowledge_text", ""),
} }
if status not in ("generated", "saved"): if status not in ("generated", "saved"):
@ -254,9 +255,12 @@ def _render_component(idx, result):
知识推送<span class="push-icon" id="kp-status-{idx}"></span> 知识推送<span class="push-icon" id="kp-status-{idx}"></span>
</button>''' </button>'''
# Regenerate button (for all components) # Regenerate buttons (split: component + kp)
regen_btn = f'''<button class="push-btn regen-btn" data-idx="{idx}" onclick="event.stopPropagation();toggleRegenPanel({idx})"> regen_btn = f'''<button class="push-btn regen-btn" data-idx="{idx}" onclick="event.stopPropagation();toggleRegenPanel({idx},'component')">
重新生成<span class="push-icon" id="regen-status-{idx}"></span> 重新生成组件<span class="push-icon" id="regen-status-{idx}"></span>
</button>
<button class="push-btn regen-kp-btn" data-idx="{idx}" onclick="event.stopPropagation();toggleRegenPanel({idx},'kp')">
重新生成知识点<span class="push-icon" id="regen-kp-status-{idx}"></span>
</button>''' </button>'''
# Build body # Build body
@ -303,17 +307,25 @@ def _render_component(idx, result):
reason = html.escape(result.get("reason", "")) reason = html.escape(result.get("reason", ""))
body_html = f'<div class="skip-msg">跳过: {reason}</div>' body_html = f'<div class="skip-msg">跳过: {reason}</div>'
# Regenerate panel (editable teaching_config) — for all components # Regenerate panel — split into component regen + kp regen
raw_config = result.get("raw_config", "") raw_config = result.get("raw_config", "")
escaped_config = html.escape(raw_config) escaped_config = html.escape(raw_config)
config_rows = min(max(raw_config.count('\n') + 1, 5), 25) config_rows = min(max(raw_config.count('\n') + 1, 5), 25)
knowledge_text = result.get("knowledge_text", "")
escaped_kp_text = html.escape(knowledge_text)
kp_rows = min(max(knowledge_text.count('\n') + 1, 3), 15)
# For failed/skipped: show config panel expanded; for success: collapsed # For failed/skipped: show config panel expanded; for success: collapsed
regen_display = "block" if status in ("failed", "skipped") else "none" regen_display = "block" if status in ("failed", "skipped") else "none"
regen_panel = f''' regen_panel = f'''
<div class="regen-panel" id="regen-panel-{idx}" style="display:{regen_display};"> <div class="regen-panel" id="regen-panel-{idx}" style="display:none;">
<div class="regen-label">原始组件配置 <span class="regen-hint">(可编辑后点击"执行重新生成")</span></div> <div class="regen-label">原始组件配置 <span class="regen-hint">(可编辑后点击"执行重新生成组件")</span></div>
<textarea class="regen-textarea" id="regen-config-{idx}" rows="{config_rows}">{escaped_config}</textarea> <textarea class="regen-textarea" id="regen-config-{idx}" rows="{config_rows}">{escaped_config}</textarea>
<button class="regen-exec-btn" id="regen-exec-{idx}" onclick="regenerateComponent({idx})">执行重新生成</button> <button class="regen-exec-btn" id="regen-exec-{idx}" onclick="regenerateComponentJson({idx})">执行重新生成组件</button>
</div>
<div class="regen-panel regen-kp-panel" id="regen-kp-panel-{idx}" style="display:none;">
<div class="regen-label regen-kp-label">知识点文本 <span class="regen-hint">(来自sheet知识点列可编辑后点击"执行重新生成知识点")</span></div>
<textarea class="regen-textarea regen-kp-textarea" id="regen-kp-text-{idx}" rows="{kp_rows}">{escaped_kp_text}</textarea>
<button class="regen-exec-btn regen-kp-exec-btn" id="regen-kp-exec-{idx}" onclick="regenerateComponentKp({idx})">执行重新生成知识点</button>
</div>''' </div>'''
return f''' return f'''
@ -465,6 +477,8 @@ body {{ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto San
.kp-btn:hover {{ background:#dcfce7; }} .kp-btn:hover {{ background:#dcfce7; }}
.regen-btn {{ background:#fef3c7; color:#d97706; border-color:#fde68a; }} .regen-btn {{ background:#fef3c7; color:#d97706; border-color:#fde68a; }}
.regen-btn:hover {{ background:#fde68a; }} .regen-btn:hover {{ background:#fde68a; }}
.regen-kp-btn {{ background:#fdf2f8; color:#db2777; border-color:#fbcfe8; }}
.regen-kp-btn:hover {{ background:#fce7f3; }}
.push-btn:disabled {{ opacity:0.5; cursor:not-allowed; }} .push-btn:disabled {{ opacity:0.5; cursor:not-allowed; }}
.push-icon {{ display:inline-block; width:8px; height:8px; border-radius:50%; background:#94a3b8; }} .push-icon {{ display:inline-block; width:8px; height:8px; border-radius:50%; background:#94a3b8; }}
.push-icon.ok {{ background:#10b981; }} .push-icon.ok {{ background:#10b981; }}
@ -485,6 +499,12 @@ body {{ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto San
cursor:pointer; transition:background 0.2s; }} cursor:pointer; transition:background 0.2s; }}
.regen-exec-btn:hover {{ background:#b45309; }} .regen-exec-btn:hover {{ background:#b45309; }}
.regen-exec-btn:disabled {{ opacity:0.5; cursor:not-allowed; }} .regen-exec-btn:disabled {{ opacity:0.5; cursor:not-allowed; }}
.regen-kp-panel {{ background:#fdf2f8; border-color:#fbcfe8; }}
.regen-kp-label {{ color:#9d174d; }}
.regen-kp-textarea {{ border-color:#fbcfe8; }}
.regen-kp-textarea:focus {{ border-color:#db2777; box-shadow:0 0 0 2px rgba(219,39,119,0.15); }}
.regen-kp-exec-btn {{ background:#db2777; }}
.regen-kp-exec-btn:hover {{ background:#9d174d; }}
/* JSON sections */ /* JSON sections */
.json-section {{ margin-bottom:12px; }} .json-section {{ margin-bottom:12px; }}
@ -1009,8 +1029,9 @@ async function pushComponent(idx) {{
async function pushKp(idx) {{ async function pushKp(idx) {{
const body = buildKpPushBody(idx); const body = buildKpPushBody(idx);
if (!body) {{ if (!body) {{
showToast('组件 #' + idx + ' 无知识点数据', 'info'); setStatus('kp-status-' + idx, 'fail');
return true; showToast('组件 #' + idx + ' 知识点推送失败: 无知识点或kpId为空', 'error');
return false;
}} }}
setStatus('kp-status-' + idx, 'loading'); setStatus('kp-status-' + idx, 'loading');
@ -1079,7 +1100,11 @@ async function _runPushAll(mode) {{
const r2 = await doPush(body2); const r2 = await doPush(body2);
if (r2.ok) {{ setStatus('kp-status-' + idx, 'ok'); okCount++; }} if (r2.ok) {{ setStatus('kp-status-' + idx, 'ok'); okCount++; }}
else {{ setStatus('kp-status-' + idx, 'fail'); failCount++; _batchErrors.push('[' + _batchContext + '] 知识点推送失败: ' + r2.error); }} else {{ setStatus('kp-status-' + idx, 'fail'); failCount++; _batchErrors.push('[' + _batchContext + '] 知识点推送失败: ' + r2.error); }}
}} else {{ if (mode === 'both') okCount++; }} }} else {{
setStatus('kp-status-' + idx, 'fail');
failCount++;
_batchErrors.push('[' + _batchContext + '] 知识点推送失败: 无知识点或kpId为空');
}}
done++; updateProgress(); done++; updateProgress();
}} }}
}} }}
@ -1103,18 +1128,34 @@ function pushAllComponents() {{ _runPushAll('config'); }}
function pushAllKpAll() {{ _runPushAll('kp'); }} function pushAllKpAll() {{ _runPushAll('kp'); }}
// ============ Regenerate ============ // ============ Regenerate ============
function toggleRegenPanel(idx) {{ function toggleRegenPanel(idx, mode) {{
const panel = document.getElementById('regen-panel-' + idx); const compPanel = document.getElementById('regen-panel-' + idx);
if (!panel) return; const kpPanel = document.getElementById('regen-kp-panel-' + idx);
const showing = panel.style.display !== 'block';
panel.style.display = showing ? 'block' : 'none'; if (mode === 'kp') {{
if (compPanel) compPanel.style.display = 'none';
if (kpPanel) {{
const showing = kpPanel.style.display !== 'block';
kpPanel.style.display = showing ? 'block' : 'none';
if (showing) {{ if (showing) {{
const card = document.getElementById('card-' + idx); const card = document.getElementById('card-' + idx);
if (card) card.classList.add('open'); if (card) card.classList.add('open');
}} }}
}} }}
}} else {{
if (kpPanel) kpPanel.style.display = 'none';
if (compPanel) {{
const showing = compPanel.style.display !== 'block';
compPanel.style.display = showing ? 'block' : 'none';
if (showing) {{
const card = document.getElementById('card-' + idx);
if (card) card.classList.add('open');
}}
}}
}}
}}
async function regenerateComponent(idx) {{ async function regenerateComponentJson(idx) {{
const comp = COMPONENTS[idx - 1]; const comp = COMPONENTS[idx - 1];
const ta = document.getElementById('regen-config-' + idx); const ta = document.getElementById('regen-config-' + idx);
const btn = document.getElementById('regen-exec-' + idx); const btn = document.getElementById('regen-exec-' + idx);
@ -1137,6 +1178,7 @@ async function regenerateComponent(idx) {{
method: 'POST', method: 'POST',
headers: {{ 'Content-Type': 'application/json' }}, headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ body: JSON.stringify({{
mode: 'component',
teaching_config: teachingConfig, teaching_config: teachingConfig,
type_name: comp.type_name, type_name: comp.type_name,
cId: comp.cId, cId: comp.cId,
@ -1153,7 +1195,7 @@ async function regenerateComponent(idx) {{
throw new Error(data.error || ('HTTP ' + resp.status)); throw new Error(data.error || ('HTTP ' + resp.status));
}} }}
// Update COMPONENTS data // Update COMPONENTS data (only jsonData/core fields, keep kpInfo)
comp.status = 'generated'; comp.status = 'generated';
comp.category = data.category || comp.category; comp.category = data.category || comp.category;
comp.cType = data.cType || comp.cType; comp.cType = data.cType || comp.cType;
@ -1167,13 +1209,15 @@ async function regenerateComponent(idx) {{
}} else {{ }} else {{
if (data.jsonData) comp.jsonData = data.jsonData; if (data.jsonData) comp.jsonData = data.jsonData;
}} }}
if (data.kpInfo) comp.kpInfo = data.kpInfo; // kpInfo not updated in component-only mode
// Rebuild body HTML // Rebuild body HTML (pass existing kpInfo from comp, not from data)
rebuildComponentBody(idx, data); const displayData = {{ ...data }};
if (!displayData.kpInfo && comp.kpInfo) displayData.kpInfo = comp.kpInfo;
rebuildComponentBody(idx, displayData);
setStatus('regen-status-' + idx, 'ok'); setStatus('regen-status-' + idx, 'ok');
btn.textContent = '执行重新生成'; btn.textContent = '执行重新生成组件';
btn.disabled = false; btn.disabled = false;
// Update status label // Update status label
@ -1183,12 +1227,83 @@ async function regenerateComponent(idx) {{
statusEl.textContent = '生成成功'; statusEl.textContent = '生成成功';
}} }}
showToast('组件 #' + idx + ' 重新生成成功', 'success'); showToast('组件 #' + idx + ' 组件配置重新生成成功', 'success');
}} catch(e) {{ }} catch(e) {{
setStatus('regen-status-' + idx, 'fail'); setStatus('regen-status-' + idx, 'fail');
btn.textContent = '执行重新生成'; btn.textContent = '执行重新生成组件';
btn.disabled = false; btn.disabled = false;
showToast('组件 #' + idx + ' 重新生成失败: ' + e.message, 'error'); showToast('组件 #' + idx + ' 组件重新生成失败: ' + e.message, 'error');
}}
}}
async function regenerateComponentKp(idx) {{
const comp = COMPONENTS[idx - 1];
const ta = document.getElementById('regen-kp-text-' + idx);
const btn = document.getElementById('regen-kp-exec-' + idx);
if (!ta || !btn) return;
const knowledgeText = ta.value.trim();
if (!knowledgeText) {{
showToast('知识点文本不能为空', 'error');
return;
}}
btn.disabled = true;
btn.textContent = '生成中...';
setStatus('regen-kp-status-' + idx, 'loading');
const regenUrl = getRegenUrl();
try {{
const resp = await fetch(regenUrl, {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{
mode: 'kp',
type_name: comp.type_name,
cId: comp.cId,
knowledge_text: knowledgeText,
level: LEVEL,
}}),
}});
const data = await resp.json();
if (!resp.ok) {{
throw new Error(data.error || ('HTTP ' + resp.status));
}}
// Update kpInfo only
if (data.kpInfo) {{
comp.kpInfo = data.kpInfo;
}} else {{
comp.kpInfo = null;
}}
comp.knowledge_text = knowledgeText;
// Rebuild body HTML (keep existing jsonData/core fields)
const displayData = {{}};
if (comp.category === 'core') {{
const fields = PUSH_FIELD_MAP[comp.cType] || [];
for (const key of fields) {{
if (comp[key]) displayData[key] = comp[key];
}}
}} else {{
if (comp.jsonData) displayData.jsonData = comp.jsonData;
}}
displayData.kpInfo = comp.kpInfo;
rebuildComponentBody(idx, displayData);
setStatus('regen-kp-status-' + idx, 'ok');
btn.textContent = '执行重新生成知识点';
btn.disabled = false;
showToast('组件 #' + idx + ' 知识点重新生成成功', 'success');
}} catch(e) {{
setStatus('regen-kp-status-' + idx, 'fail');
btn.textContent = '执行重新生成知识点';
btn.disabled = false;
showToast('组件 #' + idx + ' 知识点重新生成失败: ' + e.message, 'error');
}} }}
}} }}
@ -1200,8 +1315,9 @@ function rebuildComponentBody(idx, data) {{
// Clear old content above regen-result (error-msg, json-sections etc) // Clear old content above regen-result (error-msg, json-sections etc)
const body = document.getElementById('body-' + idx); const body = document.getElementById('body-' + idx);
const regenPanel = document.getElementById('regen-panel-' + idx); const regenPanel = document.getElementById('regen-panel-' + idx);
// Remove everything except regen-result and regen-panel const regenKpPanel = document.getElementById('regen-kp-panel-' + idx);
while (body.firstChild && body.firstChild !== resultDiv && body.firstChild !== regenPanel) {{ // Remove everything except regen-result and regen panels
while (body.firstChild && body.firstChild !== resultDiv && body.firstChild !== regenPanel && body.firstChild !== regenKpPanel) {{
body.removeChild(body.firstChild); body.removeChild(body.firstChild);
}} }}
@ -1301,7 +1417,7 @@ function ensurePushButtons(idx) {{
}} }}
// ============ LocalStorage Auto-Save ============ // ============ LocalStorage Auto-Save ============
const SAVE_KEY = 'icj_' + document.title; const SAVE_KEY = 'icj_' + document.title + '_{timestamp}';
function saveEdits() {{ function saveEdits() {{
const state = {{}}; const state = {{}};

View File

@ -267,7 +267,26 @@ def _extract_english_word_only(text):
# ============ 单条知识点匹配 ============ # ============ 单条知识点匹配 ============
def _match_single_kp(kp_text, level=None, context="", llm_client=None): def _filter_candidates_by_unit(candidates, cId):
"""
根据组件ID前4位过滤候选知识点确保知识点属于同一单元
kpid前4位与cId前4位相同才属于同一单元
Args:
candidates: 粗召回的候选列表
cId: 组件ID
Returns:
list: 过滤后的候选列表仅保留同单元的知识点
"""
if not cId or len(str(cId)) < 4:
return candidates
unit_prefix = str(cId)[:4]
filtered = [c for c in candidates if c.get("kp_id") and str(c["kp_id"])[:4] == unit_prefix]
return filtered
def _match_single_kp(kp_text, level=None, context="", llm_client=None, cId=None):
""" """
匹配单个知识点文本到数据库记录 匹配单个知识点文本到数据库记录
@ -309,6 +328,14 @@ def _match_single_kp(kp_text, level=None, context="", llm_client=None):
logger.info(f"未找到匹配: '{kp_text}' (level={level})") logger.info(f"未找到匹配: '{kp_text}' (level={level})")
return default_result return default_result
# 根据组件ID前4位过滤候选确保知识点属于同一单元
if cId:
filtered = _filter_candidates_by_unit(candidates, cId)
if not filtered:
logger.info(f"单元过滤后无匹配: '{kp_text}' (cId前4位={str(cId)[:4]})")
return default_result
candidates = filtered
# 保存候选列表供 HTML 下拉展示 # 保存候选列表供 HTML 下拉展示
candidates_for_ui = [ candidates_for_ui = [
{"kp_id": c["kp_id"], "type": c.get("type", ""), "title": c.get("title", ""), {"kp_id": c["kp_id"], "type": c.get("type", ""), "title": c.get("title", ""),
@ -378,7 +405,7 @@ def match_knowledge_points(knowledge_text, cType, cId, level=None, llm_client=No
logger.debug(f"跳过无英文字符行: '{stripped}'") logger.debug(f"跳过无英文字符行: '{stripped}'")
continue continue
kp_entry = _match_single_kp(stripped, level, context, llm_client) kp_entry = _match_single_kp(stripped, level, context, llm_client, cId=cId)
kp_list.append(kp_entry) kp_list.append(kp_entry)
if not kp_list: if not kp_list:

View File

@ -51,6 +51,7 @@ CORE_INTERACTION_TYPES = {
"合作听力": {"cType": "core_listening_order", "bitable_wiki": "FrxtwNRQDizqiikPkATcBzTCnYe", "db_table": "tblBuFU6wVKdBh8B"}, "合作听力": {"cType": "core_listening_order", "bitable_wiki": "FrxtwNRQDizqiikPkATcBzTCnYe", "db_table": "tblBuFU6wVKdBh8B"},
"听力拖拽": {"cType": "core_listening_drag", "bitable_wiki": "K3QrwQnWqiPBm1krhnNcWDTqnhe", "db_table": "tblOOAsgYoZWsTGJ"}, "听力拖拽": {"cType": "core_listening_drag", "bitable_wiki": "K3QrwQnWqiPBm1krhnNcWDTqnhe", "db_table": "tblOOAsgYoZWsTGJ"},
"听力选择": {"cType": "core_listening_choose", "bitable_wiki": "Kwrcw6A4jip2sxkdLn4czd1knvf", "db_table": "tblH4xNpAmKJ7gEq"}, "听力选择": {"cType": "core_listening_choose", "bitable_wiki": "Kwrcw6A4jip2sxkdLn4czd1knvf", "db_table": "tblH4xNpAmKJ7gEq"},
"看图拼词": {"cType": "core_writing_imgMakeWord", "bitable_wiki": "QDyhwiY7PiSER8kHGZncDsrinde", "db_table": "tblyPBqx9IRYQ3a9"},
"写作互动": {"cType": None, "bitable_wiki": "DVkcwVr2giswTckcgD0cpHwNnv2"}, "写作互动": {"cType": None, "bitable_wiki": "DVkcwVr2giswTckcgD0cpHwNnv2"},
"看图组句": {"cType": "core_writing_imgMakeSentence", "bitable_wiki": "BkmtwUBwMiHd5Ak7VS6ccE9SnHd", "db_table": "tblpILmFc9ec17DY"}, "看图组句": {"cType": "core_writing_imgMakeSentence", "bitable_wiki": "BkmtwUBwMiHd5Ak7VS6ccE9SnHd", "db_table": "tblpILmFc9ec17DY"},
"看图撰写": {"cType": "core_writing_imgWrite", "bitable_wiki": "KwPHwnaqdiWlvNkDm5fcFreDnQh", "db_table": "tblAMaM3g7500Lmm"}, "看图撰写": {"cType": "core_writing_imgWrite", "bitable_wiki": "KwPHwnaqdiWlvNkDm5fcFreDnQh", "db_table": "tblAMaM3g7500Lmm"},

View File

@ -390,6 +390,7 @@ def process_script(wiki_url_or_token, db_path=None, dry_run=False, target_cids=N
"result": result, "result": result,
"raw_config": comp.get("teaching_config", ""), "raw_config": comp.get("teaching_config", ""),
"config_info": comp.get("config_info", ""), "config_info": comp.get("config_info", ""),
"knowledge_text": comp.get("knowledge_text", ""),
} }
if dry_run: if dry_run:
@ -505,6 +506,7 @@ def process_script(wiki_url_or_token, db_path=None, dry_run=False, target_cids=N
"kpInfo": result_data.get("kpInfo"), "kpInfo": result_data.get("kpInfo"),
"raw_config": res.get("raw_config", ""), "raw_config": res.get("raw_config", ""),
"config_info": res.get("config_info", ""), "config_info": res.get("config_info", ""),
"knowledge_text": res.get("knowledge_text", ""),
} }
if status == "saved": if status == "saved":
entry["component_id"] = res.get("component_id") entry["component_id"] = res.get("component_id")

View File

@ -159,35 +159,66 @@ rate_limiter = RateLimiter(CONFIG['rate_limit_per_minute'])
# --- Regenerate Handler --- # --- Regenerate Handler ---
def _do_regenerate(body_data): def _do_regenerate(body_data):
"""调用 generate_component 重新生成单个组件""" """调用 generate_component 或 generate_kp_info 重新生成单个组件/知识点"""
if str(SCRIPTS_DIR) not in sys.path: if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR)) sys.path.insert(0, str(SCRIPTS_DIR))
mode = body_data.get("mode", "component")
type_name = body_data.get("type_name", "")
cId = body_data.get("cId", "")
level = body_data.get("level") or None
if not type_name:
return 400, {"error": "缺少 type_name"}
if mode == "kp":
# 仅重新生成知识点
from generate_json import generate_kp_info
from match_component import match_component_type
from llm_client import LLMClient
knowledge_text = body_data.get("knowledge_text", "")
if not knowledge_text.strip():
return 400, {"error": "缺少 knowledge_text"}
try:
type_info = match_component_type(type_name)
cType = type_info["cType"]
except ValueError as e:
return 400, {"error": str(e)}
try:
llm_client = LLMClient()
kp_info = generate_kp_info(cType, cId, knowledge_text, llm_client, level=level)
return 200, {"kpInfo": kp_info, "cType": cType}
except Exception as e:
logger.error(f"Regenerate KP failed: {traceback.format_exc()}")
return 500, {"error": str(e)}
else:
# 仅重新生成组件配置(不生成 kpInfo
from generate_json import generate_component from generate_json import generate_component
from llm_client import LLMClient from llm_client import LLMClient
teaching_config = body_data.get("teaching_config", "") teaching_config = body_data.get("teaching_config", "")
type_name = body_data.get("type_name", "")
cId = body_data.get("cId", "")
character_map = body_data.get("character_map") or {} character_map = body_data.get("character_map") or {}
section_char_map = body_data.get("section_char_map") or [] section_char_map = body_data.get("section_char_map") or []
level = body_data.get("level") or None
if not teaching_config or not type_name: if not teaching_config:
return 400, {"error": "缺少 teaching_config 或 type_name"} return 400, {"error": "缺少 teaching_config"}
component = { component = {
"type_name": type_name, "type_name": type_name,
"cId": cId, "cId": cId,
"teaching_config": teaching_config, "teaching_config": teaching_config,
"has_image": body_data.get("has_image", False), "has_image": body_data.get("has_image", False),
"knowledge_text": body_data.get("knowledge_text", ""), "knowledge_text": "", # 强制为空,跳过 kpInfo 生成
"config_info": body_data.get("config_info", ""), "config_info": body_data.get("config_info", ""),
} }
try: try:
llm_client = LLMClient() llm_client = LLMClient()
result = generate_component(component, character_map=character_map, section_char_map=section_char_map, llm_client=llm_client, level=level) result = generate_component(component, character_map=character_map, section_char_map=section_char_map, llm_client=llm_client, level=level)
result["kpInfo"] = None # 确保不返回 kpInfo
return 200, result return 200, result
except Exception as e: except Exception as e:
logger.error(f"Regenerate failed: {traceback.format_exc()}") logger.error(f"Regenerate failed: {traceback.format_exc()}")
@ -316,8 +347,8 @@ class ProxyHandler(BaseHTTPRequestHandler):
self._send_json(err[0], err[1]) self._send_json(err[0], err[1])
return return
if not isinstance(body_data, dict) or not body_data.get('teaching_config'): if not isinstance(body_data, dict) or not body_data.get('type_name'):
self._send_json(400, {'error': 'body must contain teaching_config'}) self._send_json(400, {'error': 'body must contain type_name'})
return return
logger.info(f'[{client_ip}] Regenerating: type_name={body_data.get("type_name")}, cId={body_data.get("cId")}') logger.info(f'[{client_ip}] Regenerating: type_name={body_data.get("type_name")}, cId={body_data.get("cId")}')

View File

@ -84,3 +84,11 @@
{"type":"memory.recall.recorded","timestamp":"2026-06-01T10:44:47.489Z","query":"王璐辰 反馈 组件配置 中互动 核心互动 问题","resultCount":2,"results":[{"path":"memory/2026-04-30.md","startLine":50,"endLine":63,"score":1},{"path":"memory/2026-05-07.md","startLine":354,"endLine":368,"score":1}]} {"type":"memory.recall.recorded","timestamp":"2026-06-01T10:44:47.489Z","query":"王璐辰 反馈 组件配置 中互动 核心互动 问题","resultCount":2,"results":[{"path":"memory/2026-04-30.md","startLine":50,"endLine":63,"score":1},{"path":"memory/2026-05-07.md","startLine":354,"endLine":368,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-02T01:49:17.841Z","query":"英文台词生产 L1-S2-U17-L1 摩托怪客","resultCount":6,"results":[{"path":"memory/2026-05-28.md","startLine":46,"endLine":62,"score":1},{"path":"memory/2026-05-28.md","startLine":60,"endLine":66,"score":1},{"path":"memory/2026-05-29.md","startLine":1,"endLine":14,"score":1},{"path":"memory/2026-05-28.md","startLine":35,"endLine":49,"score":1},{"path":"memory/2026-05-18.md","startLine":791,"endLine":810,"score":1},{"path":"memory/2026-05-08.md","startLine":33,"endLine":55,"score":1}]} {"type":"memory.recall.recorded","timestamp":"2026-06-02T01:49:17.841Z","query":"英文台词生产 L1-S2-U17-L1 摩托怪客","resultCount":6,"results":[{"path":"memory/2026-05-28.md","startLine":46,"endLine":62,"score":1},{"path":"memory/2026-05-28.md","startLine":60,"endLine":66,"score":1},{"path":"memory/2026-05-29.md","startLine":1,"endLine":14,"score":1},{"path":"memory/2026-05-28.md","startLine":35,"endLine":49,"score":1},{"path":"memory/2026-05-18.md","startLine":791,"endLine":810,"score":1},{"path":"memory/2026-05-08.md","startLine":33,"endLine":55,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-02T07:02:08.195Z","query":"interactive-component-json 组件配置-json skill","resultCount":1,"results":[{"path":"memory/2026-05-20.md","startLine":40,"endLine":59,"score":1}]} {"type":"memory.recall.recorded","timestamp":"2026-06-02T07:02:08.195Z","query":"interactive-component-json 组件配置-json skill","resultCount":1,"results":[{"path":"memory/2026-05-20.md","startLine":40,"endLine":59,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-04T03:23:22.968Z","query":"主题图片 L1 L2 theme picture","resultCount":2,"results":[{"path":"memory/2026-05-12.md","startLine":141,"endLine":155,"score":1},{"path":"memory/2026-05-12.md","startLine":132,"endLine":143,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-04T03:23:36.292Z","query":"主题 topic theme L1 L2 图片 单元","resultCount":2,"results":[{"path":"memory/2026-05-12.md","startLine":141,"endLine":155,"score":1},{"path":"memory/2026-05-12.md","startLine":132,"endLine":143,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-04T03:24:24.356Z","query":"主题图片 theme image 单元主题 topic picture L1 L2 课程","resultCount":2,"results":[{"path":"memory/2026-05-12.md","startLine":141,"endLine":155,"score":1},{"path":"memory/2026-05-12.md","startLine":132,"endLine":143,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-04T03:43:35.423Z","query":"瓦拉能力体系 Vala 产品能力","resultCount":2,"results":[{"path":"memory/2026-05-18.md","startLine":702,"endLine":726,"score":1},{"path":"memory/2026-05-18.md","startLine":672,"endLine":707,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-04T06:32:47.988Z","query":"组件生产 多维表格 bitable app_token 1112315","resultCount":2,"results":[{"path":"memory/2026-05-07.md","startLine":354,"endLine":368,"score":1},{"path":"memory/2026-04-22.md","startLine":1,"endLine":8,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-04T06:33:43.353Z","query":"组件生产 bitable app_token 多维表格 互动组件","resultCount":2,"results":[{"path":"memory/2026-05-07.md","startLine":354,"endLine":368,"score":1},{"path":"memory/2026-04-22.md","startLine":1,"endLine":8,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-04T06:34:06.417Z","query":"组件生产 多维表格 app_token CMHSbUUjka","resultCount":2,"results":[{"path":"memory/2026-05-07.md","startLine":354,"endLine":368,"score":1},{"path":"memory/2026-04-22.md","startLine":1,"endLine":8,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-04T09:21:44.866Z","query":"儿童英语学习通病 常见错误 不同阶段 学习难点","resultCount":2,"results":[{"path":"memory/2026-05-11.md","startLine":78,"endLine":89,"score":1},{"path":"memory/2026-05-26.md","startLine":161,"endLine":187,"score":1}]}

View File

@ -1,6 +1,6 @@
{ {
"version": 1, "version": 1,
"updatedAt": "2026-06-02T07:02:08.195Z", "updatedAt": "2026-06-04T09:21:44.866Z",
"entries": { "entries": {
"memory:memory/2026-05-07.md:57:74": { "memory:memory/2026-05-07.md:57:74": {
"key": "memory:memory/2026-05-07.md:57:74", "key": "memory:memory/2026-05-07.md:57:74",
@ -876,13 +876,13 @@
"endLine": 368, "endLine": 368,
"source": "memory", "source": "memory",
"snippet": "- **需求:** 将 020102I am...)和 020103I am ready / Thank you两套题合并为一个 `{first:..., second:...}` JSON统一 questionSetID=0000001 - **状态:** ✅ 已完成 - **核心考点分析(用户强调):** 需分析每个句型的核心考点(孩子最容易犯错的地方),挖空对准核心考点 - I am/from 组am系动词第一人称、from介词选择、studenta+名词结构) - Thank you for 组for介词选择非 you、helpingfor+动名词,非 help/to help - **输出文件:** `output/writing_pic_qa_combined.json` ### 刘彦江 — 组件配置-json 请求L1-S2-U13-L4 沙漠之花) - **时间:** 16:45 ~ 17:51 - **文档:** `https://makee-interactive.feishu.cn/wiki/K5E1wzwk7it9t7kXvcbc6Xugnhc` - **状态:** ⚠️ 未完成 — pipeline 识别到 0 组件 - **根因:** 剧本文档的13个组件数据存储在 markdown 内联表格中lark-table5列×36行而非内嵌 Sheet。当前 pipeline 的 parse_script 只从内嵌 Sheet 读取组件数据,不支持 markdown 表格组件解析 - **已识别组件ma", "snippet": "- **需求:** 将 020102I am...)和 020103I am ready / Thank you两套题合并为一个 `{first:..., second:...}` JSON统一 questionSetID=0000001 - **状态:** ✅ 已完成 - **核心考点分析(用户强调):** 需分析每个句型的核心考点(孩子最容易犯错的地方),挖空对准核心考点 - I am/from 组am系动词第一人称、from介词选择、studenta+名词结构) - Thank you for 组for介词选择非 you、helpingfor+动名词,非 help/to help - **输出文件:** `output/writing_pic_qa_combined.json` ### 刘彦江 — 组件配置-json 请求L1-S2-U13-L4 沙漠之花) - **时间:** 16:45 ~ 17:51 - **文档:** `https://makee-interactive.feishu.cn/wiki/K5E1wzwk7it9t7kXvcbc6Xugnhc` - **状态:** ⚠️ 未完成 — pipeline 识别到 0 组件 - **根因:** 剧本文档的13个组件数据存储在 markdown 内联表格中lark-table5列×36行而非内嵌 Sheet。当前 pipeline 的 parse_script 只从内嵌 Sheet 读取组件数据,不支持 markdown 表格组件解析 - **已识别组件ma",
"recallCount": 12, "recallCount": 15,
"dailyCount": 0, "dailyCount": 0,
"groundedCount": 0, "groundedCount": 0,
"totalScore": 12, "totalScore": 15,
"maxScore": 1, "maxScore": 1,
"firstRecalledAt": "2026-05-15T07:13:08.147Z", "firstRecalledAt": "2026-05-15T07:13:08.147Z",
"lastRecalledAt": "2026-06-01T10:44:47.489Z", "lastRecalledAt": "2026-06-04T06:34:06.417Z",
"queryHashes": [ "queryHashes": [
"08364c8746ab", "08364c8746ab",
"4f08741ab4fd", "4f08741ab4fd",
@ -895,7 +895,10 @@
"8742c0bf4e2b", "8742c0bf4e2b",
"eb0902db0156", "eb0902db0156",
"229e5d3943bb", "229e5d3943bb",
"f53c9769e692" "f53c9769e692",
"5d025e291984",
"472b243ec77c",
"fd4c8aca2cb6"
], ],
"recallDays": [ "recallDays": [
"2026-05-15", "2026-05-15",
@ -904,7 +907,8 @@
"2026-05-21", "2026-05-21",
"2026-05-25", "2026-05-25",
"2026-05-28", "2026-05-28",
"2026-06-01" "2026-06-01",
"2026-06-04"
], ],
"conceptTags": [ "conceptTags": [
"am/from", "am/from",
@ -924,13 +928,13 @@
"endLine": 8, "endLine": 8,
"source": "memory", "source": "memory",
"snippet": "[李应瑛 2026-04-22 提出要求] 所有需要包含对话的内容(如剧本、互动组件等)必须要有【后置对话】字段,无后置对话时填写“无”。 [李应瑛 2026-04-22 确认规则] 剧本内嵌表格组件填写位置规则仅当表格第一列A列明确标注为对话类类型对话朗读/对话挖空/对话选读/对话组句等才在同一行的H列【组件】列填写对应的组件内容其他类型行TL/场景/角色/图片/非对话类等)无需填写。 [李应瑛 2026-04-22 确认格式规则] 对话类组件字段换行规则:每个结构单独占一行,格式为: 【任务标题】xxx 【情境引入】xxx 【互动内容】xxx 【后置对话】xxx 单元格内使用\\n作为换行符实现后续所有组件均遵循此格式。", "snippet": "[李应瑛 2026-04-22 提出要求] 所有需要包含对话的内容(如剧本、互动组件等)必须要有【后置对话】字段,无后置对话时填写“无”。 [李应瑛 2026-04-22 确认规则] 剧本内嵌表格组件填写位置规则仅当表格第一列A列明确标注为对话类类型对话朗读/对话挖空/对话选读/对话组句等才在同一行的H列【组件】列填写对应的组件内容其他类型行TL/场景/角色/图片/非对话类等)无需填写。 [李应瑛 2026-04-22 确认格式规则] 对话类组件字段换行规则:每个结构单独占一行,格式为: 【任务标题】xxx 【情境引入】xxx 【互动内容】xxx 【后置对话】xxx 单元格内使用\\n作为换行符实现后续所有组件均遵循此格式。",
"recallCount": 10, "recallCount": 13,
"dailyCount": 0, "dailyCount": 0,
"groundedCount": 0, "groundedCount": 0,
"totalScore": 10, "totalScore": 13,
"maxScore": 1, "maxScore": 1,
"firstRecalledAt": "2026-05-15T07:13:08.147Z", "firstRecalledAt": "2026-05-15T07:13:08.147Z",
"lastRecalledAt": "2026-06-01T10:44:19.887Z", "lastRecalledAt": "2026-06-04T06:34:06.417Z",
"queryHashes": [ "queryHashes": [
"08364c8746ab", "08364c8746ab",
"4f08741ab4fd", "4f08741ab4fd",
@ -941,7 +945,10 @@
"d6a04b711fd9", "d6a04b711fd9",
"8742c0bf4e2b", "8742c0bf4e2b",
"eb0902db0156", "eb0902db0156",
"229e5d3943bb" "229e5d3943bb",
"5d025e291984",
"472b243ec77c",
"fd4c8aca2cb6"
], ],
"recallDays": [ "recallDays": [
"2026-05-15", "2026-05-15",
@ -950,7 +957,8 @@
"2026-05-21", "2026-05-21",
"2026-05-25", "2026-05-25",
"2026-05-28", "2026-05-28",
"2026-06-01" "2026-06-01",
"2026-06-04"
], ],
"conceptTags": [ "conceptTags": [
"对话朗读/对话挖空/对话选读/对话组句等", "对话朗读/对话挖空/对话选读/对话组句等",
@ -1396,19 +1404,21 @@
"endLine": 726, "endLine": 726,
"source": "memory", "source": "memory",
"snippet": "- 颜色题: \"What colour is it?\",物体题: \"What's this?\" - ability: 图文匹配 - explanation: 含回答要点、图片内容、能力、评估标准、回答指导 ### 审校结果 - ✅ 全部27题 jsonData 结构完整、explanation 全中文、题目格式正确 - ✅ 题目集合ID一致、图片编号连续00-10/00-11/00-09 - ✅ 题目配置(人类可读)格式规范 - ⚠️ 口语-P4表无「审校结果」列无法标记审校状态 - 110101题组2 \"colour\"出现2次调色盘+彩虹)— 按规格生成 - 110201 \"mice\"(复数) 使用 \"What are these?\" 区分于 \"mouse\"(单数) 使用 \"What's this?\" - 以下词汇未在L1词库中检索到词库仅160词/声称980词疑似文件不完整jacket, T-shirt, hair, monster, colour, bread, cake, candy, chocolate, ice cream, mice, mouse — 按用户指定词汇生产,未做替换 ### Bitable信息 - App Token: CMHSbUUjka3TrUsaxxEc297ongf - Table ID: tblsD2dxaRpLmkXD - 操作方式: Python subprocess + curl避免shell转义 ## [李应瑛] L1-S2-U17-L3 紧急拜访 组件配置2026-05-18 20:5", "snippet": "- 颜色题: \"What colour is it?\",物体题: \"What's this?\" - ability: 图文匹配 - explanation: 含回答要点、图片内容、能力、评估标准、回答指导 ### 审校结果 - ✅ 全部27题 jsonData 结构完整、explanation 全中文、题目格式正确 - ✅ 题目集合ID一致、图片编号连续00-10/00-11/00-09 - ✅ 题目配置(人类可读)格式规范 - ⚠️ 口语-P4表无「审校结果」列无法标记审校状态 - 110101题组2 \"colour\"出现2次调色盘+彩虹)— 按规格生成 - 110201 \"mice\"(复数) 使用 \"What are these?\" 区分于 \"mouse\"(单数) 使用 \"What's this?\" - 以下词汇未在L1词库中检索到词库仅160词/声称980词疑似文件不完整jacket, T-shirt, hair, monster, colour, bread, cake, candy, chocolate, ice cream, mice, mouse — 按用户指定词汇生产,未做替换 ### Bitable信息 - App Token: CMHSbUUjka3TrUsaxxEc297ongf - Table ID: tblsD2dxaRpLmkXD - 操作方式: Python subprocess + curl避免shell转义 ## [李应瑛] L1-S2-U17-L3 紧急拜访 组件配置2026-05-18 20:5",
"recallCount": 2, "recallCount": 3,
"dailyCount": 0, "dailyCount": 0,
"groundedCount": 0, "groundedCount": 0,
"totalScore": 2, "totalScore": 3,
"maxScore": 1, "maxScore": 1,
"firstRecalledAt": "2026-05-19T03:08:27.099Z", "firstRecalledAt": "2026-05-19T03:08:27.099Z",
"lastRecalledAt": "2026-05-19T03:08:50.132Z", "lastRecalledAt": "2026-06-04T03:43:35.423Z",
"queryHashes": [ "queryHashes": [
"34bf2e5fbdee", "34bf2e5fbdee",
"3f9471df9e50" "3f9471df9e50",
"19f5f93f3465"
], ],
"recallDays": [ "recallDays": [
"2026-05-19" "2026-05-19",
"2026-06-04"
], ],
"conceptTags": [ "conceptTags": [
"00-10/00-11/00-09", "00-10/00-11/00-09",
@ -1428,19 +1438,21 @@
"endLine": 707, "endLine": 707,
"source": "memory", "source": "memory",
"snippet": "- `/tmp/unit_challenge_ability_doc.md` — 对外分享版(已发送文件 + 飞书文档) --- ## L1-S2-U18-L5 双龙会 台词润色审校([王璐辰] ### 审校结果 - 语法/拼写硬错误5处Rows 41, 55, 126, 153, 178 - 表达优化建议4处Rows 77, 84, 63, 207 - 一致性遗漏4行无润色 + dragon/Loong 混用不统一 - D列知识点6个知识点共20次 ### 交互模式收获 - 用户(王璐辰)偏好直接在 bitable 中写入统计表格 → 后续同类需求优先写入原表 ## [刘彦江] 口语-P4-看图识物 内容生产2026-05-18 19:34 ### 生产范围3条记录全部双题组全部回填验证通过 ✅) | ID | record_id | 题组1 | 题组2 | 状态 | |----|-----------|-------|-------|------| | 100001 | recvjYhcXkYXIM | 6题(颜色) | 5题(衣物) | ✅ | | 110101 | recvjYhdvUxDgs | 6题(身体) | 6题(颜色) | ✅ | | 110201 | recvjYhe4opOGm | 6题(食物) | 4题(动物) | ✅ | ### 题型结构 **口语-P4 (speaking_pic_recognize)**: - textTitle: \"Look and answer.\" - 每词1题配1图白底中央展", "snippet": "- `/tmp/unit_challenge_ability_doc.md` — 对外分享版(已发送文件 + 飞书文档) --- ## L1-S2-U18-L5 双龙会 台词润色审校([王璐辰] ### 审校结果 - 语法/拼写硬错误5处Rows 41, 55, 126, 153, 178 - 表达优化建议4处Rows 77, 84, 63, 207 - 一致性遗漏4行无润色 + dragon/Loong 混用不统一 - D列知识点6个知识点共20次 ### 交互模式收获 - 用户(王璐辰)偏好直接在 bitable 中写入统计表格 → 后续同类需求优先写入原表 ## [刘彦江] 口语-P4-看图识物 内容生产2026-05-18 19:34 ### 生产范围3条记录全部双题组全部回填验证通过 ✅) | ID | record_id | 题组1 | 题组2 | 状态 | |----|-----------|-------|-------|------| | 100001 | recvjYhcXkYXIM | 6题(颜色) | 5题(衣物) | ✅ | | 110101 | recvjYhdvUxDgs | 6题(身体) | 6题(颜色) | ✅ | | 110201 | recvjYhe4opOGm | 6题(食物) | 4题(动物) | ✅ | ### 题型结构 **口语-P4 (speaking_pic_recognize)**: - textTitle: \"Look and answer.\" - 每词1题配1图白底中央展",
"recallCount": 2, "recallCount": 3,
"dailyCount": 0, "dailyCount": 0,
"groundedCount": 0, "groundedCount": 0,
"totalScore": 2, "totalScore": 3,
"maxScore": 1, "maxScore": 1,
"firstRecalledAt": "2026-05-19T03:08:27.099Z", "firstRecalledAt": "2026-05-19T03:08:27.099Z",
"lastRecalledAt": "2026-05-19T03:08:50.132Z", "lastRecalledAt": "2026-06-04T03:43:35.423Z",
"queryHashes": [ "queryHashes": [
"34bf2e5fbdee", "34bf2e5fbdee",
"3f9471df9e50" "3f9471df9e50",
"19f5f93f3465"
], ],
"recallDays": [ "recallDays": [
"2026-05-19" "2026-05-19",
"2026-06-04"
], ],
"conceptTags": [ "conceptTags": [
"l1-s2-u18-l5", "l1-s2-u18-l5",
@ -2979,6 +2991,134 @@
"token", "token",
"u1zewhzaaie07tkjmoochurgnih" "u1zewhzaaie07tkjmoochurgnih"
] ]
},
"memory:memory/2026-05-12.md:141:155": {
"key": "memory:memory/2026-05-12.md:141:155",
"path": "memory/2026-05-12.md",
"startLine": 141,
"endLine": 155,
"source": "memory",
"snippet": "- **最终结果10/10 条记录 sentenceMeaningMeaning 均可 json.loads 正确解析,每条含 2 题,无残留垃圾数据** - **受影响记录:** 1213004, 1213006, 1213010, 1214008, 1215005, 1216001, 1216004, 1216007, 1216008, 1216010 - **审校结果同步更新:** 移除所有 10 条的 `❌ JSON 解析失败` 标记 - **注意:** Q1 explanation 在重建时词表述可能微调(内容含义一致),如需精确原始 wording 可从中文列恢复后人工微调 ## 经验教训 ### bitable 写入需严格流程管控2026-05-12 - 批量更新 JSON 字段时必须:写入前完整提取原始数据 → 写入后逐字段 diff 验证 → 发现异常立即回滚 - 本次 JSON 修复中首次写入不完整导致 Q2 丢失,原因:修复脚本未正确处理多题 JSON 结构 - **教训:** 任何涉及 JSON 重建/修复的批量操作,先单条试运行验证通过后再批量执行,不可并行甩出多条 ### sentenceMeaningMeaning 的 explanation 引号规范2026-05-12 - explanation 字段中的中文双引号极易被误用为 ASCII `\"` (U+0022) - 建议生产侧统一使用中文引号 `\"` (U+201C) 和 `\"` (U+201D),或在 explanation 写入前做引号归一化预处理",
"recallCount": 3,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 3,
"maxScore": 1,
"firstRecalledAt": "2026-06-04T03:23:22.968Z",
"lastRecalledAt": "2026-06-04T03:24:24.356Z",
"queryHashes": [
"defa0daee9d7",
"92343053bee2",
"302eafc578a7"
],
"recallDays": [
"2026-06-04"
],
"conceptTags": [
"10/10",
"json.loads",
"重建/修复的批量操作",
"最终",
"结果",
"记录",
"sentencemeaningmeaning",
"均可"
]
},
"memory:memory/2026-05-12.md:132:143": {
"key": "memory:memory/2026-05-12.md:132:143",
"path": "memory/2026-05-12.md",
"startLine": 132,
"endLine": 143,
"source": "memory",
"snippet": "- 检查题型:句意选择题 ×30、句子补全题、听句作答题、场景选择题、看图选择题 - 验证方法:提取 explanation 中的英文引句 vs question 中的英文句子、正则提取\"空处应填X\" vs options[answer] - 结果:✅ **0 条实质性不对应** - **10 条 sentenceMeaningMeaning JSON 修复:** - **根因:** explanation 文本中的 ASCII `\"` (U+0022) 被当作中文双引号使用JSON 解析器将其视为字符串终止符 - **修复策略:** 从「句意选择题」中文列提取英文原句和选项 → json.dumps 重建标准 JSON只修格式不修内容 - **1216007 特殊处理:** 原始 JSON 损坏严重,需从中文列完全重建 - **二次修复Q2 丢失问题):** 首次不完全修复导致 9 条记录的 Q2 被覆盖丢失,且 Q1 被 JSON 垃圾污染 → 从中文列全面重建 Q1+Q2 - **最终结果10/10 条记录 sentenceMeaningMeaning 均可 json.loads 正确解析,每条含 2 题,无残留垃圾数据** - **受影响记录:** 1213004, 1213006, 1213010, 1214008, 1215005, 1216001, 1216004, 1216007, 1216008, 1216010 - **审校结果同步更新:** 移除所有 10 条的 `❌ JSON 解析失败` 标记",
"recallCount": 3,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 3,
"maxScore": 1,
"firstRecalledAt": "2026-06-04T03:23:22.968Z",
"lastRecalledAt": "2026-06-04T03:24:24.356Z",
"queryHashes": [
"defa0daee9d7",
"92343053bee2",
"302eafc578a7"
],
"recallDays": [
"2026-06-04"
],
"conceptTags": [
"json.dumps",
"10/10",
"json.loads",
"检查",
"选择",
"句子",
"作答",
"场景"
]
},
"memory:memory/2026-05-11.md:78:89": {
"key": "memory:memory/2026-05-11.md:78:89",
"path": "memory/2026-05-11.md",
"startLine": 78,
"endLine": 89,
"source": "memory",
"snippet": "- **状态:** ✅ 全部完成(两阶段延迟写入策略,避免自动化覆盖) - **脚本:** `scripts/batch_reading_matchInfo.py` | ID | 题组1 | 题组2 | record_id | |----|-------|-------|-----------| | 021301 | actor/beard/different/brave/neighbour人物 | dinner/cooking/fire/album/diary生活 | recvjjk7oAT1YN | | 021401 | plan/repair/newspaper/backpack/seat日程 | garden/melon/water/plant/summer自然 | recvjjk7TU4ZFs | | 021501 | visitor/welcome/invite/laugh/guest社交 | mistake/instruction/reason/technology/brush学习 | recvjjk8mju8ou | | 021601 | record/event/enter/return/sort管理 | handbag/camera/gate/wheel/entrance场所 | recvjjk8Q5ZMIu | | 021701 | driver/street/market/guidebook/shopping出行 | machine/draw/basketball/bookcase/invention活动 | r",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-06-04T09:21:44.866Z",
"lastRecalledAt": "2026-06-04T09:21:44.866Z",
"queryHashes": [
"3d95a503721c"
],
"recallDays": [
"2026-06-04"
],
"conceptTags": [
"record-id",
"dinner/cooking/fire/album/diary",
"garden/melon/water/plant/summer",
"record/event/enter/return/sort",
"状态",
"全部",
"完成",
"阶段"
]
},
"memory:memory/2026-05-26.md:161:187": {
"key": "memory:memory/2026-05-26.md:161:187",
"path": "memory/2026-05-26.md",
"startLine": 161,
"endLine": 187,
"source": "memory",
"snippet": "- 文档:`https://makee-interactive.feishu.cn/wiki/XoGZwgrRMiGk9vkYNWmcegs9nyh`L1-S2-U19-L1 客厅游戏) - Bot当前无该知识空间权限无法写入 - 已完成 5 个组件配置文本生成,待权限恢复后写入 G 列 ### 修正格式规范(用户指定) - 不含【组件ID】字段 - 【互动内容】顶部加「题目:选择正确的单词补全句子」 - 挖空用 `___`3个下划线 - 选项后加「辅助信息」行(如 `afternoon 意为下午` - 【后置对话】缺省写「空」,不用「无」 - 【互动反馈】正确:「正确:无」;错误:「错误 角色名: 台词」 - 任务标题和反馈需贴合剧情 - 有【教研图】的组件:资源配置需写「图片时机」 ### 5个组件配置摘要 | ID | 句子 | 正确答案 | 干扰项 | 图片时机 | |----|------|---------|--------|---------| | 1219102 | This ___ , we go to space! | afternoon | evening | 无 | | 1219105 | This ___ is the captain's seat! | armchair | sofa | 互动内容 | | 1219110 | Push the ___ , Dad! | armchair | table | 互动内容 | | 1219111 | The ___ is a glowing star! | lamp | armchair |",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-06-04T09:21:44.866Z",
"lastRecalledAt": "2026-06-04T09:21:44.866Z",
"queryHashes": [
"3d95a503721c"
],
"recallDays": [
"2026-06-04"
],
"conceptTags": [
"l1-s2-u19-l1",
"https",
"makee",
"interactive.feishu.cn",
"wiki",
"xogzwgrrmigk9vkynwmcegs9nyh",
"u19",
"客厅"
]
} }
} }
} }

45
memory/2026-06-04-1218.md Normal file
View File

@ -0,0 +1,45 @@
# Session: 2026-06-04 12:18:44 GMT+8
- **Session Key**: agent:xiaoyan:feishu:direct:ou_8b79b7f711e42a4004f3f3c225a1a99c
- **Session ID**: bed05a1e-fc0d-4ea2-b319-ed5f913c4a60
- **Source**: feishu
## Conversation Summary
user: https://makee-interactive.feishu.cn/wiki/PgWrwllW2ikQKgkLTYKcmKFonXc组件配置-json
assistant: 文档是「L1-S2-U18-L1 中国周」的组件配置文档,包含 17 个互动组件。我先读取相关技能和嵌入的 Sheet 数据。
assistant: 文档已读取标题是「L1-S2-U18-L1 中国周」,包含 17 个互动组件。消息含"组件配置-json",需要调用 `interactive-component-json` 技能。
assistant: 匹配到 `interactive-component-json` 技能。文档「L1-S2-U18-L1 中国周」包含 17 个互动组件,启动流水线处理。
assistant: 处理完成。17/17 组件全部成功HTML 报告已发送给你。
**L1-S2-U18-L1 中国周 — 组件配置 JSON 生成结果:**
| # | 组件ID | 类型 | cType |
|---|--------|------|-------|
| 1 | 1218101 | 对话朗读 | mid_dialog_repeat |
| 2 | 1218102 | 图片单选 | mid_image_choose |
| 3 | 1218103 | 对话朗读 | mid_dialog_repeat |
| 4 | 1218104 | 图片单选 | mid_image_choose |
| 5 | 1218105 | 对话挖空 | mid_dialog_fillin |
| 6 | 1218106 | 对话朗读 | mid_dialog_repeat |
| 7 | 1218107 | 图片多选 | mid_image_multiple |
| 8 | 1218108 | 对话挖空 | mid_dialog_fillin |
| 9 | 1218109 | 对话朗读 | mid_dialog_repeat |
| 10 | 1218110 | 对话朗读 | mid_dialog_repeat |
| 11 | 1218111 | 对话选择 | mid_dialog_choose |
| 12 | 1218112 | 对话挖空 | mid_dialog_fillin |
| 13 | 1218113 | 对话组句 | mid_dialog_sentence |
| 14 | 1218114 | 对话朗读 | mid_dialog_repeat |
| 15 | 1218115 | 对话组句 | mid_dialog_sentence |
| 16 | 1218116 | 听力拖拽 | core_listening_drag |
| 17 | 1218117 | 对话选读 | mid_dialog_select |
⚠️ 1 个注意项:组件 1218115对话组句的知识点 `proud of` 在 L1 词库中未匹配到 kpId需人工确认。
HTML 报告内嵌了推送按钮,你可以在浏览器中打开后直接推送配置到远程数据库。