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

This commit is contained in:
ai_member_only 2026-05-23 08:10:01 +08:00
parent 0e059ec6ef
commit b725c13e10
29 changed files with 2114 additions and 265 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -43,3 +43,4 @@
{"type":"memory.recall.recorded","timestamp":"2026-05-21T07:31:52.322Z","query":"L1 L2 句型库 sentence pattern 知识点","resultCount":3,"results":[{"path":"memory/2026-05-20.md","startLine":1,"endLine":31,"score":1},{"path":"memory/2026-05-12.md","startLine":189,"endLine":209,"score":1},{"path":"memory/2026-05-12.md","startLine":92,"endLine":113,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-05-21T07:39:38.958Z","query":"组件生产 回填 G列 互动","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-05-21T09:52:45.581Z","query":"李应瑛 user_id 权限","resultCount":1,"results":[{"path":"memory/2026-05-07.md","startLine":71,"endLine":88,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-05-22T07:04:01.804Z","query":"abilityTag 标准库 45条 tag 标签","resultCount":5,"results":[{"path":"memory/2026-05-12.md","startLine":170,"endLine":193,"score":1},{"path":"memory/2026-05-12.md","startLine":206,"endLine":226,"score":1},{"path":"memory/2026-05-12.md","startLine":92,"endLine":113,"score":1},{"path":"memory/2026-05-12.md","startLine":48,"endLine":68,"score":1},{"path":"memory/2026-05-12.md","startLine":122,"endLine":138,"score":1}]}

View File

@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-05-21T09:52:45.581Z",
"updatedAt": "2026-05-22T07:04:01.804Z",
"entries": {
"memory:memory/2026-05-07.md:57:74": {
"key": "memory:memory/2026-05-07.md:57:74",
@ -378,22 +378,24 @@
"endLine": 226,
"source": "memory",
"snippet": "- **10 条 sentenceMeaningMeaning JSON 修复:** - 根因explanation 中 ASCII `\"` 被用作中文引号 - 修复策略演变:状态机拆分失败 → 正则重建 → 发现Q2丢失 → 从中文列完整重建 - 最终10/10 可正确解析,审校结果同步更新 - 受影响1213004/1213006/1213010/1214008/1215005/1216001/1216004/1216007/1216008/1216010 ### 刘彦江 — 核心互动全题型 JSON 配置标准沉淀17:05 ~ 17:50 - **产出 Skill 1** `skills/bitable-reader/SKILL.md` — 通用 bitable 读取(任何 bitable 通用) - **产出 Skill 2** `skills/core-content-json-standard/SKILL.md` v2.0 — 全题型 JSON 标准393行 - **架构:** 通用字段在前ID/kpInfo/taskData+ 5大题型分类📖阅读2 🎧听力3 🗣口语4 ✏写作5+ 审校规则 + 扩展指南 - **覆盖率:** 14/15 种题型(口语探讨 S0 无数据) - **产出脚本:** `scripts/audit_core_reading_S0.py` — 合作阅读 S0 审校 ## 经验教训 ### bitable 写入需严格流程管控2026-05-12 - 批量更新 JSON写入前完整提取",
"recallCount": 3,
"recallCount": 4,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 3,
"totalScore": 4,
"maxScore": 1,
"firstRecalledAt": "2026-05-13T03:09:54.362Z",
"lastRecalledAt": "2026-05-19T03:08:50.132Z",
"lastRecalledAt": "2026-05-22T07:04:01.804Z",
"queryHashes": [
"f151bc633ad1",
"a0932e0e2749",
"3f9471df9e50"
"3f9471df9e50",
"d13d58b4704a"
],
"recallDays": [
"2026-05-13",
"2026-05-15",
"2026-05-19"
"2026-05-19",
"2026-05-22"
],
"conceptTags": [
"10/10",
@ -665,23 +667,25 @@
"endLine": 113,
"source": "memory",
"snippet": "- L2 B级及以上图片文字需为完整陈述句3-5词+ ### 刘彦江 — L1 配置表审校 + 技能沉淀11:50 ~ 12:10 - **数据源:** 飞书多维表格「互动知识点 - 句子」→「Level 1 配置表」(`Nq3Zb258aae7SRs2QfXcqsQYnxJ` / `tblTxGpf6GQ5c7DZ`) - **ID 编码规则:** 7位 = 前2位(Season) + 中2位(Unit) + 后3位(序列号) - 例: 1214001 = L1S2(12) + U14(14) + 001 - **审校记录 1214001 (You need to...) 发现:** 1. ❌ classificationInfo 值互换: cambridgeLevel=A1 vs cefrLevel=YLE与列字段反了 2. ❌ sentenceStructureSort 题2: 单词库有need无needs三单主语Otis语法错误 3. ⚠️ sentenceStructureClozeWordMcq: \"put your toys\" 省略 away 不够自然 - **技能沉淀:** - 脚本: `scripts/audit_l1_config.py` — 7项自动检查 - SKILL.md: `skills/audit_l1_config/SKILL.md` — 含完整审校流程 + 4项人工审核清单 ### 刘彦江 — 1213001-1216010 批量审校 40 条12:10 ~ 14:00 - **范围:** L1",
"recallCount": 4,
"recallCount": 5,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 4,
"totalScore": 5,
"maxScore": 1,
"firstRecalledAt": "2026-05-13T09:14:31.249Z",
"lastRecalledAt": "2026-05-21T07:31:52.322Z",
"lastRecalledAt": "2026-05-22T07:04:01.804Z",
"queryHashes": [
"4906de25dd2a",
"0e7edf653be7",
"4b36721edd0f",
"03b71fab8fb9"
"03b71fab8fb9",
"d13d58b4704a"
],
"recallDays": [
"2026-05-13",
"2026-05-17",
"2026-05-21"
"2026-05-21",
"2026-05-22"
],
"conceptTags": [
"3-5词",
@ -767,23 +771,25 @@
"endLine": 193,
"source": "memory",
"snippet": "- 每个题型cType + bitable 定位 + JSON 字段表 + 结构特点 + 与同类题型的差异说明 #### 发现 - 写作互动和邮件组句 cType 相同(`core_writing_questionMakeSentence`),通过 textInfo 区分素材 - 口语快答/妙问 JSON 结构几乎相同,差异在 prompt 配置和对话样例内容 - 合作阅读和合作听力的核心差异:`textData.text[]` ↔ `textData.audio[]` - meaning 标签(合作阅读)为开放型自由文本,非受控词表 #### 产出文件 - `skills/bitable-reader/SKILL.md` — 通用 bitable 读取技能164行 - `skills/core-content-json-standard/SKILL.md` — 全题型 JSON 标准 v2.0393行 - `scripts/audit_core_reading_S0.py` — 合作阅读 S0 审校脚本(含审校发现) # 2026-05-12 工作日志 ## 会话记录 ### 刘彦江 — 021301-021801 图片描述修正 + 技能更新09:35 ~ 09:45 - **问题:** 021301-021801 信息匹配题的图片描述缺少 `【Notice Type】` 标签 - **处理:** 批量更新6条记录的图片描述字段全部10个标签验证通过 - **脚本:** `scripts/fix_matchInfo_0213_0218_desc.py`",
"recallCount": 4,
"recallCount": 5,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 4,
"totalScore": 5,
"maxScore": 1,
"firstRecalledAt": "2026-05-14T02:49:02.264Z",
"lastRecalledAt": "2026-05-19T03:08:50.132Z",
"lastRecalledAt": "2026-05-22T07:04:01.804Z",
"queryHashes": [
"d592c9ed5e0a",
"5f858a7d5e0b",
"34bf2e5fbdee",
"3f9471df9e50"
"3f9471df9e50",
"d13d58b4704a"
],
"recallDays": [
"2026-05-14",
"2026-05-17",
"2026-05-19"
"2026-05-19",
"2026-05-22"
],
"conceptTags": [
"口语快答/妙问",
@ -1602,20 +1608,22 @@
"endLine": 68,
"source": "memory",
"snippet": "- **ID 编码规则:** 7位 = 前2位(Season) + 中2位(Unit) + 后3位(序列号) - 例: 1214001 = L1S2(12) + U14(14) + 001 - **审校记录 1214001 (You need to...) 发现:** 1. ❌ classificationInfo 值互换: cambridgeLevel=A1 vs cefrLevel=YLE与列字段反了 2. ❌ sentenceStructureSort 题2: 单词库有need无needs三单主语Otis语法错误 3. ⚠️ sentenceStructureClozeWordMcq: \"put your toys\" 省略 away 不够自然 - **技能沉淀:** - 脚本: `scripts/audit_l1_config.py` — 7项自动检查 - SKILL.md: `skills/audit_l1_config/SKILL.md` — 含完整审校流程 + 4项人工审核清单 ### 刘彦江 — 1213001-1216010 批量审校 40 条12:10 ~ 14:00 - **范围:** L1S2 Units 13-16每单元 10 条,共 40 条 - **方法:** `scripts/audit_l1_config.py` 自动化扫描 → 误报排除 → 分类汇总 → 逐条审校回填 - **脚本修复(过程中):** 1. sentenceStructureSort 单词对比大小写误报 → 添加 normalize首",
"recallCount": 2,
"recallCount": 3,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 2,
"totalScore": 3,
"maxScore": 1,
"firstRecalledAt": "2026-05-20T11:03:34.460Z",
"lastRecalledAt": "2026-05-21T02:47:59.335Z",
"lastRecalledAt": "2026-05-22T07:04:01.804Z",
"queryHashes": [
"81867276810d",
"4b36721edd0f"
"4b36721edd0f",
"d13d58b4704a"
],
"recallDays": [
"2026-05-20",
"2026-05-21"
"2026-05-21",
"2026-05-22"
],
"conceptTags": [
"scripts/audit-l1-config.py",
@ -1876,6 +1884,37 @@
"工作",
"日志"
]
},
"memory:memory/2026-05-12.md:122:138": {
"key": "memory:memory/2026-05-12.md:122:138",
"path": "memory/2026-05-12.md",
"startLine": 122,
"endLine": 138,
"source": "memory",
"snippet": "- **🟢 仅有系统性问题的 23 条** - **回填:** 全部 40 条审校结果已写入 bitable「审校结果」列逐条验证通过 - **脚本回填脚本:** `scripts/audit_batch_1213001_1216010.py` ### 刘彦江 — 审校结果修正 + 解析一致检查 + JSON 修复14:00 ~ 15:38 - **YLE↔A1 误报排除:** 刘彦江确认 YLE 与 A1 等级可互通classificationInfo 值互换不视为错误 - 全部 40 条审校结果已更新,移除 classificationInfo 互换相关告警 - 23 条变为 `✅ 通过`无其他问题17 条保留具体错误 - **解析与题目/答案对应性检查(人工审核项):** - 检查题型:句意选择题 ×30、句子补全题、听句作答题、场景选择题、看图选择题 - 验证方法:提取 explanation 中的英文引句 vs question 中的英文句子、正则提取\"空处应填X\" vs options[answer] - 结果:✅ **0 条实质性不对应** - **10 条 sentenceMeaningMeaning JSON 修复:** - **根因:** explanation 文本中的 ASCII `\"` (U+0022) 被当作中文双引号使用JSON 解析器将其视为字符串终止符 - **修复策略:** 从「句意选择题」中文列提取英文原句和选项 → json.dumps 重建标准 JSON只修格式不修内容",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-22T07:04:01.804Z",
"lastRecalledAt": "2026-05-22T07:04:01.804Z",
"queryHashes": [
"d13d58b4704a"
],
"recallDays": [
"2026-05-22"
],
"conceptTags": [
"解析与题目/答案对应性检查",
"json.dumps",
"系统",
"问题",
"回填",
"全部",
"结果",
"bitable"
]
}
}
}

489
output/component_jsons.json Normal file
View File

@ -0,0 +1,489 @@
{
"1217201": {
"componentType": "图片单选",
"taskTitle": "为包裹找到正确的日期牌子",
"contextIntro": [
{
"character": "Jay",
"line": "Help me find the right place!"
}
],
"interaction": {
"instruction": "Find the \"Days Ago\" sign.",
"audio": true,
"options": [
"00",
"01",
"02"
],
"answers": [
"00"
],
"hint": "days ago 指\"几天前\"。"
},
"feedback": {
"correct": {
"character": "Lin",
"line": "Bingo!"
},
"incorrect": {
"character": "Jay",
"line": "No, look! This package is from 3 days ago!"
}
},
"postDialogue": null
},
"1217202": {
"componentType": "对话朗读",
"taskTitle": "朗读90天前的包裹信息",
"resourceConfig": "图片时机:互动内容",
"contextIntro": null,
"interaction": {
"sentence": "User: It's 90 days ago...",
"audio": true
},
"postDialogue": null
},
"1217203": {
"componentType": "对话朗读",
"taskTitle": "理解天数和月数的换算",
"resourceConfig": "图片时机:互动内容",
"contextIntro": [
{
"character": "User",
"line": "No! 90 days! It is about..."
}
],
"interaction": {
"sentence": "User: 3 months!",
"audio": true
},
"postDialogue": [
{
"character": "User",
"line": "This is from months ago!"
}
]
},
"1217204": {
"componentType": "对话组句",
"taskTitle": "用单词组句描述包裹信息",
"resourceConfig": "图片时机:互动内容",
"contextIntro": null,
"interaction": {
"requirement": "用所给单词或短语组句",
"sentence": "It says 24 months ago.",
"audio": true,
"options": [
{
"index": 1,
"text": "it says"
},
{
"index": 2,
"text": "months ago"
},
{
"index": 3,
"text": "24"
}
],
"answer": "It says 24 months ago.",
"hint": null
},
"feedback": {
"correct": null,
"incorrect": {
"character": "Jay",
"line": "Try again! Read what the package says."
}
},
"postDialogue": null
},
"1217205": {
"componentType": "对话朗读",
"taskTitle": "理解月数和年数的换算",
"resourceConfig": "图片时机:互动内容",
"contextIntro": null,
"interaction": {
"sentence": "User: That's 2 years!",
"audio": true
},
"postDialogue": null
},
"1217206": {
"componentType": "对话选读",
"taskTitle": "选择你想表达的感受",
"resourceConfig": "无",
"contextIntro": null,
"interaction": {
"requirement": "选择一个你想表达的观点",
"audio": true,
"options": [
{
"index": 1,
"text": "That's a long time ago!",
"feedback": {
"character": "Jay",
"line": "You are right."
}
},
{
"index": 2,
"text": "That is long, long ago!",
"feedback": {
"character": "Jay",
"line": "You are right."
}
}
]
},
"postDialogue": null
},
"1217207": {
"componentType": "图片多选",
"taskTitle": "找出写有months ago的包裹",
"contextIntro": null,
"interaction": {
"instruction": "Find the \"months ago\" packages in the picture.",
"audio": true,
"options": [
"00",
"01",
"02"
],
"answers": [
"01",
"02"
],
"hint": "months ago 指\"几个月前\"。"
},
"feedback": {
"correct": {
"character": "User",
"line": "Those two are months ago!"
},
"incorrect": {
"character": "Jay",
"line": "Look again! Which ones say \"months ago\"?"
}
},
"postDialogue": null
},
"1217208": {
"componentType": "图片单选",
"taskTitle": "找出写有一年前的包裹",
"contextIntro": null,
"interaction": {
"instruction": "Find the \"year ago\" package in the picture.",
"audio": true,
"options": [
"00",
"01",
"02"
],
"answers": [
"02"
],
"hint": "a year ago 指\"一年前\"。"
},
"feedback": {
"correct": {
"character": "User",
"line": "This one is a year ago!"
},
"incorrect": {
"character": "Jay",
"line": "No, that's not right. Look again!"
}
},
"postDialogue": null
},
"1217209": {
"componentType": "对话挖空",
"taskTitle": "补全对Sunny说的句子",
"resourceConfig": "无",
"contextIntro": null,
"interaction": {
"sentence": "You must ___ it!",
"audio": true,
"options": [
{
"index": 1,
"text": "be happy with",
"correct": true
},
{
"index": 2,
"text": "happy with",
"correct": false
}
]
},
"feedback": {
"correct": {
"character": "User",
"line": "You must be happy with it!"
},
"incorrect": {
"character": "Sunny",
"line": "That doesn't sound quite right..."
}
},
"postDialogue": null
},
"1217210": {
"componentType": "对话选读",
"taskTitle": "选择帮Grace拿包裹的说法",
"resourceConfig": "无",
"contextIntro": null,
"interaction": {
"requirement": "选择一个你想表达的观点",
"audio": true,
"options": [
{
"index": 1,
"text": "Let me get it!",
"feedback": {
"character": "Grace",
"line": "Thank you, kid."
}
},
{
"index": 2,
"text": "I will get it!",
"feedback": {
"character": "Grace",
"line": "Thank you, kid."
}
}
]
},
"postDialogue": null
},
"1217211": {
"componentType": "对话朗读",
"taskTitle": "对Anna的提醒",
"resourceConfig": "图片时机:无",
"contextIntro": null,
"interaction": {
"sentence": "User: You will not be happy with it.",
"audio": true
},
"postDialogue": null
},
"1217212": {
"componentType": "对话组句",
"taskTitle": "用单词组句主动帮忙",
"resourceConfig": "无",
"contextIntro": null,
"interaction": {
"requirement": "用所给单词或短语组句",
"sentence": "Can I get it for you?",
"audio": true,
"options": [
{
"index": 1,
"text": "for you"
},
{
"index": 2,
"text": "can I"
},
{
"index": 3,
"text": "get it"
}
],
"answer": "Can I get it for you?",
"hint": null
},
"feedback": {
"correct": {
"character": "Jack",
"line": "You are very kind. But I just want to say..."
},
"incorrect": {
"character": "Jack",
"line": "I beg your pardon?"
}
},
"postDialogue": null
},
"1217213": {
"componentType": "对话挖空",
"taskTitle": "补全对Jack说的话",
"resourceConfig": "图片时机:互动内容互动反馈",
"contextIntro": null,
"interaction": {
"sentence": "But this meat is from 2 ___ !",
"audio": true,
"options": [
{
"index": 1,
"text": "years ago",
"correct": true
},
{
"index": 2,
"text": "days ago",
"correct": false
}
]
},
"feedback": {
"correct": {
"character": "Jack",
"line": "Perfect!"
},
"incorrect": {
"character": "Jack",
"line": "No, look at the sign on it!"
}
},
"postDialogue": null
},
"1217214": {
"componentType": "听力拖拽",
"cType": "core_listening_drag",
"taskData": {
"cType": "core_listening_drag",
"cId": "1217214",
"title": "告诉 Lin 你们分发包裹的事迹",
"sceneDesc": "包裹大作战!你和 Jay 热火朝天地干了半天,把很多包裹带给了他们的主人。快来回顾一下你们的战果吧!\n\nLin: Come on, tell me what you did!",
"key": "ago, month, year, get"
},
"dialogList": [
{
"character": "Jay",
"line": "Well, well, well! Listen up!"
},
{
"character": "Jay",
"line": "Tom gets a pen!"
},
{
"character": "User",
"line": "It is from 5 days ago!"
},
{
"character": "Jay",
"line": "And Sunny gets a dress."
},
{
"character": "Jay",
"line": "It is from 6 months ago."
},
{
"character": "Jay",
"line": "Jack gets some meat from 2 years ago!"
}
],
"preDialog": [],
"questionList": [
{
"type": "drag_match",
"optionImages": [
"00",
"01",
"02"
],
"answerImages": [
"01",
"00",
"02"
],
"itemCount": 3
}
],
"learningData": {
"learningPart": [
{
"sentence": "It is from 5 days ago!",
"knowledgePoint": "ago"
},
{
"sentence": "It is from 6 months ago.",
"knowledgePoint": "month"
},
{
"sentence": "Jack gets some meat from 2 years ago!",
"knowledgePoint": "year"
}
],
"closing": ""
},
"audioText": "# 文本 1\nJay: Well, well, well! Listen up!\nJay: Tom gets a pen!\nUser: It is from 5 days ago!\nJay: And Sunny gets a dress.\nJay: It is from 6 months ago. \nJay: Jack gets some meat from 2 years ago!",
"knowledgeSummary": "get v. 收到\nmonth n. 月\nyear n. 年\nago adv. 以前\n... month(s)/year(s) ago."
},
"1217215": {
"componentType": "对话选读",
"taskTitle": "选择表达满意的方式",
"resourceConfig": "无",
"contextIntro": null,
"interaction": {
"requirement": "选择一个你想表达的观点",
"audio": true,
"options": [
{
"index": 1,
"text": "I am happy with it!",
"feedback": {
"character": "Jay",
"line": "That's good!"
}
},
{
"index": 2,
"text": "I am happy with the result!",
"feedback": {
"character": "Jay",
"line": "That's good!"
}
}
]
},
"postDialogue": null
},
"1217216": {
"componentType": "对话挖空",
"taskTitle": "补全User想要包裹的句子",
"resourceConfig": "无",
"contextIntro": null,
"interaction": {
"sentence": "I want to ___ one for myself!",
"audio": true,
"options": [
{
"index": 1,
"text": "get",
"correct": true
},
{
"index": 2,
"text": "get up",
"correct": false
}
]
},
"feedback": {
"correct": null,
"incorrect": {
"character": "Jay",
"line": "Hmm, that's not how we say it. Try again!"
}
},
"postDialogue": null
},
"1217217": {
"componentType": "对话朗读",
"taskTitle": "朗读收到帽子的喜悦",
"resourceConfig": "图片时机:无",
"contextIntro": null,
"interaction": {
"sentence": "User: Now I get my own hat!",
"audio": true
},
"postDialogue": null
}
}

Binary file not shown.

View File

@ -0,0 +1,994 @@
#!/usr/bin/env python3
"""
将剧本表(Q8AyX5)中互动组件的文本配置转为结构化JSON并写回组件配置列
"""
import json, subprocess, sys, re
# --- Bot Token ---
def get_token():
APP_ID = "cli_a931175d41799cc7"
import os
with open(os.path.expanduser('/root/.openclaw/credentials/xiaoyan/config.json')) as f:
cfg = json.load(f)
APP_SECRET = cfg['apps'][0]['appSecret']
r = subprocess.run([
'curl', '-s', '-X', 'POST',
'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
'-H', 'Content-Type: application/json',
'-d', json.dumps({"app_id": APP_ID, "app_secret": APP_SECRET})
], capture_output=True, text=True)
return json.loads(r.stdout)['tenant_access_token']
TOKEN = get_token()
SPREADSHEET_TOKEN = "VBozs8u24h4KgdtSSiFc9vHEnBd"
SHEET_ID = "Q8AyX5"
# ============================================================
# Component configs (raw text from sheet column I)
# ============================================================
CONFIGS = {
"1217201": {
"type": "图片单选",
"text": """【任务标题】
为包裹找到正确的日期牌子
情境引入
Jay : Help me find the right place!
互动内容
Find the "Days Ago" sign.音频
选项
00
01
02
答案
00
辅助信息days ago "几天前"
互动反馈
正确 Lin : Bingo!
错误 Jay : No, look! This package is from 3 days ago!
后置对话
""",
},
"1217202": {
"type": "对话朗读",
"text": """【任务标题】
朗读90天前的包裹信息
资源配置
图片时机互动内容
情境引入
互动内容
User: It's 90 days ago...(朗读)
后置对话
""",
},
"1217203": {
"type": "对话朗读",
"text": """【任务标题】
理解天数和月数的换算
资源配置
图片时机互动内容
情境引入
User: No! 90 days! It is about...
互动内容
User: 3 months!朗读
后置对话
User: This is from months ago!""",
},
"1217204": {
"type": "对话组句",
"text": """【任务标题】
用单词组句描述包裹信息
资源配置
图片时机互动内容
情境引入
互动内容
要求用所给单词或短语组句
It says 24 months ago.音频
选项1it says
选项2months ago
选项324
答案It says 24 months ago.
互动反馈
正确
错误 Jay : Try again! Read what the package says.
后置对话
""",
},
"1217205": {
"type": "对话朗读",
"text": """【任务标题】
理解月数和年数的换算
资源配置
图片时机互动内容
情境引入
互动内容
User: That's 2 years!(朗读)
后置对话
""",
},
"1217206": {
"type": "对话选读",
"text": """【任务标题】
选择你想表达的感受
资源配置
情境引入
互动内容
要求选择一个你想表达的观点
选项音频
选项1That's a long time ago!
- 反馈 Jay: You are right.
选项2That is long, long ago!
- 反馈 Jay: You are right.
后置对话
""",
},
"1217207": {
"type": "图片多选",
"text": """【任务标题】
找出写有months ago的包裹
情境引入
互动内容
Find the "months ago" packages in the picture.音频
选项
00
01
02
答案
01
02
辅助信息months ago "几个月前"
互动反馈
正确 User : Those two are months ago!
错误 Jay : Look again! Which ones say "months ago"?
后置对话
""",
},
"1217208": {
"type": "图片单选",
"text": """【任务标题】
找出写有一年前的包裹
情境引入
互动内容
Find the "year ago" package in the picture.音频
选项
00
01
02
答案
02
辅助信息a year ago "一年前"
互动反馈
正确 User : This one is a year ago!
错误 Jay : No, that's not right. Look again!
后置对话
""",
},
"1217209": {
"type": "对话挖空",
"text": """【任务标题】
补全对Sunny说的句子
资源配置
情境引入
互动内容
You must ___ it!音频
选项1be happy with正确
选项2happy with
互动反馈
正确 User : You must be happy with it!
错误 Sunny : That doesn't sound quite right...
后置对话
""",
},
"1217210": {
"type": "对话选读",
"text": """【任务标题】
选择帮Grace拿包裹的说法
资源配置
情境引入
互动内容
要求选择一个你想表达的观点
选项音频
选项1Let me get it!
- 反馈 Grace: Thank you, kid.
选项2I will get it!
- 反馈 Grace: Thank you, kid.
后置对话
""",
},
"1217211": {
"type": "对话朗读",
"text": """【任务标题】
对Anna的提醒
资源配置
图片时机
情境引入
互动内容
User: You will not be happy with it.朗读
后置对话
""",
},
"1217212": {
"type": "对话组句",
"text": """【任务标题】
用单词组句主动帮忙
资源配置
情境引入
互动内容
要求用所给单词或短语组句
Can I get it for you?音频
选项1for you
选项2can I
选项3get it
答案Can I get it for you?
互动反馈
正确 Jack : You are very kind. But I just want to say...
错误 Jack: I beg your pardon?
后置对话
""",
},
"1217213": {
"type": "对话挖空",
"text": """【任务标题】
补全对Jack说的话
资源配置
图片时机互动内容互动反馈
情境引入
互动内容
But this meat is from 2 ___ !音频
选项1years ago正确
选项2days ago
互动反馈
正确 Jack : Perfect!
错误 Jack : No, look at the sign on it!
后置对话
""",
},
"1217214": {
"type": "听力拖拽",
"text": """【任务标题】
告诉 Lin 你们分发包裹的事迹
任务背景
包裹大作战你和 Jay 热火朝天地干了半天把很多包裹带给了他们的主人快来回顾一下你们的战果吧
通关知识
get v. 收到
month n.
year n.
ago adv. 以前
... month(s)/year(s) ago.
开场语
Lin: Come on, tell me what you did!
听力文本
# 文本 1
Jay: Well, well, well! Listen up!
Jay: Tom gets a pen!
User: It is from 5 days ago!
Jay: And Sunny gets a dress.
Jay: It is from 6 months ago.
Jay: Jack gets some meat from 2 years ago!
题目信息
#单空选择
选项图片编号00,01,02
答案图片编号
01,00,02
学习过程
句子 1
It is from 5 days ago!
ago
句子 2
It is from 6 months ago. month
句子 3
Jack gets some meat from 2 years ago! year""",
},
"1217215": {
"type": "对话选读",
"text": """【任务标题】
选择表达满意的方式
资源配置
情境引入
互动内容
要求选择一个你想表达的观点
选项音频
选项1I am happy with it!
- 反馈 Jay: That's good!
选项2I am happy with the result!
- 反馈 Jay: That's good!
后置对话
""",
},
"1217216": {
"type": "对话挖空",
"text": """【任务标题】
补全User想要包裹的句子
资源配置
情境引入
互动内容
I want to ___ one for myself!音频
选项1get正确
选项2get up
互动反馈
正确
错误 Jay : Hmm, that's not how we say it. Try again!
后置对话
""",
},
"1217217": {
"type": "对话朗读",
"text": """【任务标题】
朗读收到帽子的喜悦
资源配置
图片时机
情境引入
互动内容
User: Now I get my own hat!朗读
后置对话
""",
},
}
# ============================================================
# Parsers for each component type
# ============================================================
def extract_section(text, key):
"""Extract content between 【key】and next section header 【...】."""
# Match 【key】 then capture everything until next 【XXX】 header on its own line or end
pattern = rf'{re.escape(key)}\s*\n?(.*?)(?=\n(?:【[^】]+】)\s*\n|\Z)'
m = re.search(pattern, text, re.DOTALL)
if m:
return m.group(1).strip()
return None
def parse_context(text):
"""Parse context intro: '角色 : 台词' or '' or ''"""
if not text or text in ('', '', ''):
return None
lines = [l.strip() for l in text.strip().split('\n') if l.strip()]
result = []
for line in lines:
if ':' in line:
parts = line.split(':', 1)
result.append({"character": parts[0].strip(), "line": parts[1].strip()})
elif '' in line:
parts = line.split('', 1)
result.append({"character": parts[0].strip(), "line": parts[1].strip()})
else:
result.append(line)
return result if result else None
def parse_feedback(text):
"""Parse feedback: 正确/错误 角色 : 台词"""
if not text or text == '':
return {"correct": None, "incorrect": None}
result = {"correct": None, "incorrect": None}
lines = text.strip().split('\n')
current_type = None
for line in lines:
line = line.strip()
if not line:
continue
if line.startswith('正确'):
current_type = 'correct'
content = line[2:].strip()
if ':' in content:
parts = content.split(':', 1)
char = parts[0].strip()
msg = parts[1].strip()
if msg == '':
result['correct'] = None
else:
result['correct'] = {"character": char, "line": msg}
elif content == '':
result['correct'] = None
elif line.startswith('错误'):
current_type = 'incorrect'
content = line[2:].strip()
if ':' in content:
parts = content.split(':', 1)
result['incorrect'] = {"character": parts[0].strip(), "line": parts[1].strip()}
elif '' in content:
parts = content.split('', 1)
result['incorrect'] = {"character": parts[0].strip(), "line": parts[1].strip()}
return result
def parse_selective_options(text):
"""Parse 对话选读 options: 选项Ntext - 反馈 X: line"""
options = []
lines = text.strip().split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
m = re.match(r'选项(\d+)[:]\s*(.+)', line)
if m:
idx = int(m.group(1))
opt_text = m.group(2).strip()
feedback = None
# Check next line for feedback
if i + 1 < len(lines):
next_line = lines[i + 1].strip()
fm = re.match(r'[-]\s*反馈\s*([^:]+)[:]\s*(.+)', next_line)
if fm:
feedback = {"character": fm.group(1).strip(), "line": fm.group(2).strip()}
i += 1
options.append({"index": idx, "text": opt_text, "feedback": feedback})
i += 1
return options
def parse_image_options(text):
"""Parse image choice options: 00, 01, 02 etc."""
options = []
lines = text.strip().split('\n')
in_options = False
for line in lines:
line = line.strip()
if line == '选项:':
in_options = True
continue
if in_options:
if re.match(r'^\d{2}$', line):
options.append(line)
else:
break
return options
def parse_fill_options(text):
"""Parse fill-in-blanks options: 选项Ntext正确"""
options = []
lines = text.strip().split('\n')
for line in lines:
line = line.strip()
m = re.match(r'选项(\d+)[:]\s*(.+)', line)
if m:
idx = int(m.group(1))
opt_text = m.group(2).strip()
correct = False
if '(正确)' in opt_text:
correct = True
opt_text = opt_text.replace('(正确)', '').strip()
options.append({"index": idx, "text": opt_text, "correct": correct})
return options
def parse_sentence_options(text):
"""Parse sentence building options: 选项Ntext"""
options = []
lines = text.strip().split('\n')
for line in lines:
line = line.strip()
m = re.match(r'选项(\d+)[:]\s*(.+)', line)
if m:
idx = int(m.group(1))
opt_text = m.group(2).strip()
options.append({"index": idx, "text": opt_text})
return options
# ============================================================
# Converters
# ============================================================
def convert_choice_image(cid, text):
"""图片单选 / 图片多选"""
is_multi = CONFIGS[cid]['type'] == '图片多选'
raw_interaction = extract_section(text, '互动内容')
context = extract_section(text, '情境引入')
feedback = extract_section(text, '互动反馈')
post = extract_section(text, '后置对话')
# Parse interaction
interaction_lines = raw_interaction.strip().split('\n')
instruction = interaction_lines[0].strip()
audio = '(音频)' in instruction
instruction = instruction.replace('(音频)', '').strip()
options = []
answers = []
hint = None
in_options = False
in_answer = False
in_hint = False
for line in interaction_lines[1:]:
line = line.strip()
if line == '选项:':
in_options = True
continue
if in_options and re.match(r'^\d{2}$', line):
options.append(line)
continue
if '答案:' in line:
in_options = False
in_answer = True
ans_text = line.replace('答案:', '').strip()
if ans_text:
answers.append(ans_text)
continue
if in_answer:
if re.match(r'^\d{2}$', line):
answers.append(line)
elif '辅助信息' in line:
in_answer = False
in_hint = True
hint = line.replace('辅助信息:', '').strip()
else:
in_answer = False
continue
if in_hint:
continue
if '辅助信息' in line:
hint = line.replace('辅助信息:', '').strip()
continue
result = {
"componentType": CONFIGS[cid]['type'],
"taskTitle": extract_section(text, '任务标题'),
"contextIntro": parse_context(context),
"interaction": {
"instruction": instruction,
"audio": audio,
"options": options,
"answers": answers,
"hint": hint
},
"feedback": parse_feedback(feedback),
"postDialogue": parse_context(post)
}
return result
def convert_reading(cid, text):
"""对话朗读"""
return {
"componentType": "对话朗读",
"taskTitle": extract_section(text, '任务标题'),
"resourceConfig": extract_section(text, '资源配置') or None,
"contextIntro": parse_context(extract_section(text, '情境引入')),
"interaction": {
"sentence": (extract_section(text, '互动内容') or '').replace('(朗读)', '').strip(),
"audio": True
},
"postDialogue": parse_context(extract_section(text, '后置对话'))
}
def convert_sentence_building(cid, text):
"""对话组句"""
raw_interaction = extract_section(text, '互动内容')
resource = extract_section(text, '资源配置') or None
context = extract_section(text, '情境引入')
feedback = extract_section(text, '互动反馈')
post = extract_section(text, '后置对话')
interaction_lines = raw_interaction.strip().split('\n')
requirement = None
sentence = None
audio = True
options = []
answer = None
hint = None
# Parse interaction
i = 0
if interaction_lines[0].startswith('要求:'):
requirement = interaction_lines[0].replace('要求:', '').strip()
i = 1
# Find sentence line (ends with (音频))
for j in range(i, len(interaction_lines)):
line = interaction_lines[j].strip()
if '(音频)' in line:
sentence = line.replace('(音频)', '').strip()
i = j + 1
break
elif re.match(r'选项\d+', line):
# No sentence line found, use requirement as sentence
i = j
break
# Parse options
for j in range(i, len(interaction_lines)):
line = interaction_lines[j].strip()
m = re.match(r'选项(\d+)[:]\s*(.+)', line)
if m:
options.append({"index": int(m.group(1)), "text": m.group(2).strip()})
elif '答案:' in line:
answer = line.replace('答案:', '').strip()
elif '辅助信息:' in line:
hint = line.replace('辅助信息:', '').strip()
return {
"componentType": "对话组句",
"taskTitle": extract_section(text, '任务标题'),
"resourceConfig": resource,
"contextIntro": parse_context(context),
"interaction": {
"requirement": requirement,
"sentence": sentence,
"audio": audio,
"options": options,
"answer": answer,
"hint": hint
},
"feedback": parse_feedback(feedback),
"postDialogue": parse_context(post)
}
def convert_selective_reading(cid, text):
"""对话选读"""
raw_interaction = extract_section(text, '互动内容')
resource = extract_section(text, '资源配置') or None
context = extract_section(text, '情境引入')
post = extract_section(text, '后置对话')
interaction_lines = raw_interaction.strip().split('\n')
requirement = interaction_lines[0].strip()
audio = '(音频)' in raw_interaction
if requirement.startswith('要求:'):
requirement = requirement.replace('要求:', '').strip()
options = parse_selective_options(raw_interaction)
return {
"componentType": "对话选读",
"taskTitle": extract_section(text, '任务标题'),
"resourceConfig": resource,
"contextIntro": parse_context(context),
"interaction": {
"requirement": requirement,
"audio": audio,
"options": options
},
"postDialogue": parse_context(post)
}
def convert_fill_blanks(cid, text):
"""对话挖空"""
raw_interaction = extract_section(text, '互动内容')
resource = extract_section(text, '资源配置') or None
context = extract_section(text, '情境引入')
feedback = extract_section(text, '互动反馈')
post = extract_section(text, '后置对话')
interaction_lines = raw_interaction.strip().split('\n')
sentence = None
options = []
for line in interaction_lines:
line = line.strip()
if '(音频)' in line and not line.startswith('选项'):
sentence = line.replace('(音频)', '').strip()
m = re.match(r'选项(\d+)[:]\s*(.+)', line)
if m:
opt_text = m.group(2).strip()
correct = '(正确)' in opt_text
opt_text = opt_text.replace('(正确)', '').strip()
options.append({"index": int(m.group(1)), "text": opt_text, "correct": correct})
return {
"componentType": "对话挖空",
"taskTitle": extract_section(text, '任务标题'),
"resourceConfig": resource,
"contextIntro": parse_context(context),
"interaction": {
"sentence": sentence,
"audio": True,
"options": options
},
"feedback": parse_feedback(feedback),
"postDialogue": parse_context(post)
}
def convert_listening_drag(cid, text):
"""听力拖拽 → core_listening_drag JSON"""
task_title = extract_section(text, '任务标题')
task_bg = extract_section(text, '任务背景') or ''
knowledge = extract_section(text, '通关知识') or ''
opening = extract_section(text, '开场语') or ''
audio_text = extract_section(text, '听力文本') or ''
question_info = extract_section(text, '题目信息') or ''
learning = extract_section(text, '学习过程') or ''
# Parse audio text into dialogs
dialog_list = []
for line in audio_text.strip().split('\n'):
line = line.strip()
if not line or line.startswith('#'):
continue
if ':' in line or '' in line:
sep = ':' if ':' in line else ''
parts = line.split(sep, 1)
dialog_list.append({"character": parts[0].strip(), "line": parts[1].strip()})
# Parse question info
option_images = []
answer_images = []
for line in question_info.strip().split('\n'):
line = line.strip()
if '选项图片编号' in line:
option_images = [x.strip() for x in line.split('', 1)[1].split(',') if x.strip()]
if '答案图片编号' in line:
# Answer on next line
pass
# Get answer from question info - look for answer after 答案图片编号
q_lines = question_info.strip().split('\n')
for i, line in enumerate(q_lines):
if '答案图片编号' in line and i + 1 < len(q_lines):
answer_images = [x.strip() for x in q_lines[i+1].split(',') if x.strip() and x.strip() != '答案图片编号']
# For 听力拖拽, 【学习过程】 is the LAST section and contains inline 【kp】.
# Extract it manually: everything after 【学习过程】
lp_match = re.search(r'【学习过程】\s*\n(.*)', text, re.DOTALL)
learning = lp_match.group(1).strip() if lp_match else ''
learning_steps = []
# Split by sentence markers
blocks = re.split(r'\n(?=句子\s*\d*)', learning)
for block in blocks:
block = block.strip()
if not block:
continue
# Remove the 句子 N header
block = re.sub(r'^句子\s*\d*\s*\n?', '', block).strip()
if not block:
continue
# Extract kp from 【...】
kp_match = re.search(r'【(.+?)】', block)
kp = kp_match.group(1).strip() if kp_match else ""
# Extract sentence (text before 【 or whole block if no 【)
if kp_match:
sentence = block[:kp_match.start()].strip()
else:
sentence = block.strip()
if sentence or kp:
learning_steps.append({"sentence": sentence, "knowledgePoint": kp})
# Scene description
scene_desc = f"{task_bg}\n\n{opening}"
return {
"componentType": "听力拖拽",
"cType": "core_listening_drag",
"taskData": {
"cType": "core_listening_drag",
"cId": cid,
"title": task_title,
"sceneDesc": scene_desc,
"key": "ago, month, year, get"
},
"dialogList": dialog_list,
"preDialog": [],
"questionList": [
{
"type": "drag_match",
"optionImages": option_images,
"answerImages": answer_images,
"itemCount": len(option_images)
}
],
"learningData": {
"learningPart": learning_steps,
"closing": ""
},
"audioText": audio_text,
"knowledgeSummary": knowledge
}
# ============================================================
# Main conversion
# ============================================================
CONVERTERS = {
"图片单选": convert_choice_image,
"图片多选": convert_choice_image,
"对话朗读": convert_reading,
"对话组句": convert_sentence_building,
"对话选读": convert_selective_reading,
"对话挖空": convert_fill_blanks,
"听力拖拽": convert_listening_drag,
}
# Component row mapping (from sheet data: which row has which component ID)
COMPONENT_ROWS = {
"1217201": 33,
"1217202": 40,
"1217203": 43,
"1217204": 50,
"1217205": 51,
"1217206": 53,
"1217207": 68,
"1217208": 70,
"1217209": 89,
"1217210": 95,
"1217211": 107,
"1217212": 120,
"1217213": 123,
"1217214": 157,
"1217215": 164,
"1217216": 168,
"1217217": 173,
}
def write_cell(row, col_letter, value, token):
"""Write a single cell to the sheet."""
cell_range = f"{SHEET_ID}!{col_letter}{row}:{col_letter}{row}"
payload = {
"valueRange": {
"range": cell_range,
"values": [[value]]
}
}
r = subprocess.run([
'curl', '-s', '-X', 'PUT',
f'https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values',
'-H', f'Authorization: Bearer {token}',
'-H', 'Content-Type: application/json',
'-d', json.dumps(payload, ensure_ascii=False)
], capture_output=True, text=True)
result = json.loads(r.stdout)
if result.get('code') != 0:
print(f" ❌ Write failed: {result}", file=sys.stderr)
return False
return True
def main():
results = {}
for cid, cfg in CONFIGS.items():
ctype = cfg['type']
converter = CONVERTERS.get(ctype)
if not converter:
print(f"⚠️ Unknown type {ctype} for {cid}", file=sys.stderr)
continue
try:
json_data = converter(cid, cfg['text'])
results[cid] = json_data
print(f"{cid} ({ctype}) → JSON OK")
except Exception as e:
print(f"{cid} ({ctype}) → ERROR: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
# Print all JSONs for review
print("\n" + "=" * 60)
print("GENERATED JSONS")
print("=" * 60)
for cid, data in results.items():
print(f"\n--- {cid} ({data['componentType']}) ---")
print(json.dumps(data, ensure_ascii=False, indent=2))
# Save to file for inspection
with open('/root/.openclaw/workspace-xiaoyan/output/component_jsons.json', 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\n📁 Saved to output/component_jsons.json")
# Write back to sheet
print("\n" + "=" * 60)
print("WRITING BACK TO SHEET")
print("=" * 60)
# Re-fetch token (may have expired)
token = get_token()
success = 0
fail = 0
for cid, data in results.items():
row = COMPONENT_ROWS.get(cid)
if not row:
print(f"⚠️ {cid}: no row mapping, skipped")
continue
json_str = json.dumps(data, ensure_ascii=False)
if write_cell(row, 'I', json_str, token):
print(f"{cid} → row {row} written")
success += 1
else:
fail += 1
print(f"\nDone: {success} written, {fail} failed")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Fix 021801 题组2 - replace duplicate questions with new ones based on new images."""
import subprocess, json
APP_TOKEN = "CMHSbUUjka3TrUsaxxEc297ongf"
TABLE_ID = "tblGoWYBmVI0IrvQ"
APP_ID = "cli_a931175d41799cc7"
APP_SECRET = "Iw2vEfbjT6GtV0GhbxbZqfQ4nAPtbR14"
r = subprocess.run(["curl", "-s", "-X", "POST",
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
"-H", "Content-Type: application/json",
"-d", json.dumps({"app_id": APP_ID, "app_secret": APP_SECRET})],
capture_output=True, text=True, timeout=10)
TOKEN = json.loads(r.stdout)["tenant_access_token"]
rid = "recvjz5Hs8gpCJ"
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{rid}"
# GET current
r = subprocess.run(["curl", "-s", "-X", "GET", url, "-H", f"Authorization: Bearer {TOKEN}"],
capture_output=True, text=True, timeout=10)
fields = json.loads(r.stdout)['data']['record']['fields']
jd = json.loads(fields['jsonData'])
# ===== Update 题组2 (second) =====
jd['second']['textDesc'] = "Now look at these pictures again. They show more ways children help and encourage each other. Talk about what you see and share your own experiences."
jd['second']['imageDesc'] = "Four new images about helping and caring for each other: 1st: A child brings homework to help a quiet classmate — desk is open with books and pencils. 2nd: In the school corridor, a child kneels to tie another classmate's shoelaces; the classmate looks up gratefully. 3rd: Two children plant a young tree in the garden — one holds the sapling, the other adds soil, both looking hopeful. 4th: At a school assembly, a child stands on stage and receives an award for helping others; classmates stand up and applaud."
jd['second']['questionSet'] = [
{
"content": "What do you do when a classmate looks shy or alone at school?",
"ability": ["表达观点", "句型组织"],
"explanation": "可以分享主动接近新同学的经历,例如 \"I would go and say hello with a smile. I can ask him to sit with us at lunch so he doesn't feel lonely.\" 也可以说 \"I invite her to play with my friends and show her around the school.\""
},
{
"content": "Do you think small acts of kindness, like tying shoelaces, are as important as big help? Why?",
"ability": ["表达观点", "句型组织"],
"explanation": "可以从日常小事角度回答,例如 \"Yes, because small things can make someone's day. When you tie someone's shoelaces, they know you care about them.\" 也可以对比回答:\"Small help is just as important because everyone needs a hand sometimes, even for little things.\""
},
{
"content": "Tell us about a time when you and a friend worked together on something. How did it go?",
"ability": ["经历描述", "句型组织"],
"explanation": "用过去式描述合作经历,例如 \"Last spring my friend and I planted flowers in the school garden. She dug the holes and I put the seeds in. We felt proud when the flowers bloomed.\" 也可以说 \"We cleaned the classroom together after school and finished much faster than doing it alone.\""
},
{
"content": "How do you feel when someone thanks you or gives you praise for helping?",
"ability": ["表达观点", "句型组织"],
"explanation": "可以表达被认可的温暖感,例如 \"It makes me feel warm and happy. I feel like what I did really mattered to someone.\" 也可以说 \"I feel proud but also a little shy. It encourages me to help more people in the future.\""
},
{
"content": "What kind of person do you want to be — someone who helps others every day? Why?",
"ability": ["表达观点", "句型组织"],
"explanation": "可以展望自己的品格成长,例如 \"I want to be the kind of person who helps others without thinking twice. Helping makes the world a kinder place and I want to be part of that.\" 也可以说 \"Yes, because when I help someone and see them smile, it makes my day better too.\""
}
]
# ===== Update 题目2 display text =====
题目2_lines = [
"【题目描述】",
jd['second']['textDesc'],
"",
"【热词】",
"sick,classmate,help,homework,encourage,goal,dream,support,friend,teamwork",
"",
"【图片描述】",
jd['second']['imageDesc'],
"",
"【题目】",
]
for i, q in enumerate(jd['second']['questionSet'], 1):
exp = q['explanation'].replace('\n', ' ')
题目2_lines.append(f"{i}. {q['content']}")
题目2_lines.append(f"- 解析:{exp}")
题目2_lines.append("")
fields_update = {
"jsonData": json.dumps(jd, ensure_ascii=False),
"题目2": "\n".join(题目2_lines),
"审核结果": "✅ OK | 2026-05-22 小研优化题组2",
}
body = json.dumps({"fields": fields_update}, ensure_ascii=False)
r = subprocess.run(["curl", "-s", "-X", "PUT", url,
"-H", f"Authorization: Bearer {TOKEN}",
"-H", "Content-Type: application/json",
"-d", body], capture_output=True, text=True, timeout=10)
resp = json.loads(r.stdout)
if resp.get("code") == 0:
print("✅ 021801 题组2 已更新")
# Verify
r = subprocess.run(["curl", "-s", "-X", "GET", url, "-H", f"Authorization: Bearer {TOKEN}"],
capture_output=True, text=True, timeout=10)
jd2 = json.loads(json.loads(r.stdout)['data']['record']['fields']['jsonData'])
qs1 = jd2['first']['questionSet']
qs2 = jd2['second']['questionSet']
print("\n题组1 vs 题组2 问题对比:")
for i in range(5):
same = "🟡 相同" if qs1[i]['content'] == qs2[i]['content'] else "🟢 不同"
print(f" Q{i+1}: {same}")
print(f" 题1: {qs1[i]['content']}")
print(f" 题2: {qs2[i]['content']}")
else:
print(f"{resp}")

View File

@ -1,253 +1,62 @@
"""
批量修正单元挑战听力能力标签 + JSON修复
"""
import json, urllib.request, re, sys, os
#!/usr/bin/env python3
"""Fix ability / abilityTag to match standard 45 tags."""
import subprocess, json
APP_TOKEN = "CMHSbUUjka3TrUsaxxEc297ongf"
CRED_FILE = "/root/.openclaw/credentials/xiaoyan/config.json"
TABLE_ID = "tblszuk1TeToofBF"
APP_ID = "cli_a931175d41799cc7"
APP_SECRET = "Iw2vEfbjT6GtV0GhbxbZqfQ4nAPtbR14"
# === 标准听力标签 ===
STD_HEARING_TAGS = {
"显性事实理解|关键词识别",
"显性事实理解|单句信息点抓取",
"基础语境理解|场景/物品/动作识别",
"显性细节理解|数字/时间/地点",
"同义替换识别|词/短语级",
"目的/偏好识别|显性 to/for/like",
"干扰抑制|多信息筛选",
"语用推断|否定与纠错",
"多句保持|信息整合",
"情绪/态度理解",
"长对话理解|主旨+细节",
r = subprocess.run(["curl", "-s", "-X", "POST",
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
"-H", "Content-Type: application/json",
"-d", json.dumps({"app_id": APP_ID, "app_secret": APP_SECRET})],
capture_output=True, text=True, timeout=10)
TOKEN = json.loads(r.stdout)["tenant_access_token"]
# Standard 45 tags for writing P1 email:
# 7. 短消息写作|邮件/便条
# 8. 衔接与连贯|连词使用
# 9. 描述写作|人物/物体/场景
CORE_TAGS = ["短消息写作|邮件/便条", "衔接与连贯|连词使用"]
DESC_TAG = "描述写作|人物/物体/场景"
RECORDS = {
"recvjzbkvoOgvy": {"qid": "022201", "desc": False}, # box - no description
"recvjzblhhNt7Q": {"qid": "022301", "desc": True}, # garden - describes garden
"recvjzblXOJ7Fs": {"qid": "022401", "desc": True}, # hospital - describes Jack
"recvjzXjMKAQ4i": {"qid": "032501", "desc": False}, # encouragement
"recvjzXklYIE05": {"qid": "032601", "desc": False}, # thank you
"recvjzXkYnsQ8r": {"qid": "032701", "desc": True}, # cafe - describes cafe/people
"recvjzXlxz4r3i": {"qid": "032801", "desc": False}, # problem solving
"recvjzXm5yEXBY": {"qid": "032901", "desc": True}, # party - describes party/people
}
# === 标签映射规则(正则) ===
TAG_PATTERNS = [
# P1 style: "显性事实理解(关键词识别)- bag"
(r'显性事实理解(关键词识别)', '显性事实理解|关键词识别'),
(r'显性事实理解(单句信息点抓取)', '显性事实理解|单句信息点抓取'),
(r'显性细节理解(地点)', '显性细节理解|数字/时间/地点'),
(r'显性细节理解(数字)', '显性细节理解|数字/时间/地点'),
(r'显性细节理解(时间)', '显性细节理解|数字/时间/地点'),
(r'显性细节理解(复现)', '显性细节理解|数字/时间/地点'),
(r'干扰抑制(多信息筛选)', '干扰抑制|多信息筛选'),
(r'干扰抑制(多信息筛选:.*?', '干扰抑制|多信息筛选'),
(r'偏好识别(显性\s*like', '目的/偏好识别|显性 to/for/like'),
(r'偏好识别(显性\s*love', '目的/偏好识别|显性 to/for/like'),
(r'偏好识别显性like', '目的/偏好识别|显性 to/for/like'),
(r'目的识别(显性\s*for', '目的/偏好识别|显性 to/for/like'),
(r'目的识别(显性\s*to', '目的/偏好识别|显性 to/for/like'),
(r'问题意图识别', '语用推断|否定与纠错'),
(r'关键细节听辨', '显性细节理解|数字/时间/地点'),
(r'否定与纠错', '语用推断|否定与纠错'),
(r'图像语义对齐', '基础语境理解|场景/物品/动作识别'),
(r'近义改写', '同义替换识别|词/短语级'),
(r'多句保持(信息整合)', '多句保持|信息整合'),
# P2/P4 style
(r'听觉抓取关键信息', '显性事实理解|关键词识别'),
(r'多特征整合', '多句保持|信息整合'),
(r'语用推断', '语用推断|否定与纠错'),
# P7 extras
(r'显性细节理解(地点:.*?', '显性细节理解|数字/时间/地点'),
(r'显性细节理解(数字:.*?', '显性细节理解|数字/时间/地点'),
(r'干扰抑制(多信息筛选:.*?', '干扰抑制|多信息筛选'),
(r'语用推断(句式理解)', '语用推断|否定与纠错'),
(r'语境理解(场景推断)', '基础语境理解|场景/物品/动作识别'),
(r'语用推断(否定与纠错)', '语用推断|否定与纠错'),
(r'语用推断(语气识别)', '语用推断|否定与纠错'),
(r'显性事实理解(单句信息点抓取:.*?', '显性事实理解|单句信息点抓取'),
(r'情绪态度理解', '情绪/态度理解'),
(r'长对话主旨+细节', '长对话理解|主旨+细节'),
]
def map_tag(tag):
"""Map a single old tag to standard format. Returns None if no match."""
tag = tag.strip()
if tag in STD_HEARING_TAGS:
return tag
for pattern, replacement in TAG_PATTERNS:
if re.match(pattern + r'.*$', tag):
return replacement
return None
def map_ability_list(abilities):
"""Map a list of ability tags. Handles '¥¥' delimiters."""
if not abilities:
return []
result = []
for a in abilities:
if not isinstance(a, str):
result.append(a)
continue
# Handle ¥¥ delimiter
parts = a.split('¥¥')
for part in parts:
part = part.strip()
if not part:
continue
mapped = map_tag(part)
if mapped and mapped not in result:
result.append(mapped)
elif mapped is None:
# Try to match anyway
mapped2 = map_tag(part)
if mapped2 and mapped2 not in result:
result.append(mapped2)
return result
def get_token():
with open(CRED_FILE) as f:
cfg = json.load(f)
app_id = cfg['apps'][0]['appId']
app_secret = cfg['apps'][0]['appSecret']
req = urllib.request.Request(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
data=json.dumps({"app_id": app_id, "app_secret": app_secret}).encode(),
headers={"Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(req).read())['tenant_access_token']
def api_get(url):
token = get_token()
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
return json.loads(urllib.request.urlopen(req).read())
def api_put(url, body):
token = get_token()
req = urllib.request.Request(url, data=json.dumps(body).encode(), method="PUT",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(req).read())
def update_record(table_id, record_id, fields):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{table_id}/records/{record_id}"
return api_put(url, {"fields": fields})
def fix_tags_in_json(jd):
"""Fix ability tags in jsonData - handles first/second questionSet and root questionSet"""
changed = False
for rid, spec in RECORDS.items():
qid = spec["qid"]
tags = CORE_TAGS + ([DESC_TAG] if spec["desc"] else [])
# Handle first/second structure
for section in ['first', 'second']:
sect = jd.get(section, {})
if not sect:
continue
qs = sect.get('questionSet', [])
for q in qs:
old_ability = q.get('ability', [])
new_ability = map_ability_list(old_ability)
if new_ability != old_ability:
q['ability'] = new_ability
changed = True
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{rid}"
r = subprocess.run(["curl", "-s", "-X", "GET", url, "-H", f"Authorization: Bearer {TOKEN}"],
capture_output=True, text=True, timeout=10)
fields = json.loads(r.stdout)['data']['record']['fields']
jd = json.loads(fields['jsonData'])
# Handle root questionSet (P6 style)
qs = jd.get('questionSet', [])
for q in qs:
old_ability = q.get('ability', [])
new_ability = map_ability_list(old_ability)
if new_ability != old_ability:
q['ability'] = new_ability
changed = True
jd['first']['ability'] = tags
jd['first']['abilityTag'] = tags
# Handle P5/P7 style: ability at root level
if 'ability' in jd:
old_ability = jd.get('ability', [])
new_ability = map_ability_list(old_ability)
if new_ability != old_ability:
jd['ability'] = new_ability
changed = True
body = json.dumps({"fields": {"jsonData": json.dumps(jd, ensure_ascii=False)}}, ensure_ascii=False)
r = subprocess.run(["curl", "-s", "-X", "PUT", url,
"-H", f"Authorization: Bearer {TOKEN}",
"-H", "Content-Type: application/json",
"-d", body], capture_output=True, text=True, timeout=10)
resp = json.loads(r.stdout)
return jd, changed
def fix_tags_for_record(record_id, table_id, table_name):
"""Fix ability tags for a single record"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{table_id}/records/{record_id}"
data = api_get(url)
record = data['data']['items'][0]
fields = record['fields']
jd_str = fields.get('jsonData', '{}')
if not jd_str or jd_str == 'None':
return False, "empty jsonData"
try:
jd = json.loads(jd_str)
except json.JSONDecodeError as e:
return False, f"JSON parse error: {e}"
jd_fixed, changed = fix_tags_in_json(jd)
if not changed:
return True, "no changes needed"
# Update the record with fixed jsonData
new_jd_str = json.dumps(jd_fixed, ensure_ascii=False)
result = update_record(table_id, record_id, {"jsonData": new_jd_str})
if result.get('code') == 0:
return True, "tags updated"
else:
return False, f"API error: {result}"
# ===== Main =====
targets = {
"听力-P1": "tbliZAhcc9C43B23",
"听力-P2": "tblzTLNH7f13uWQN",
"听力-P4": "tblVmeDtBDKsAEfz",
"听力-P7": "tbly9SvPEa44k3yX",
"听力-P5": "tblDssVmhGzc3UKd",
"听力-P6": "tbloiMcD0sBtGSTq",
}
all_results = []
for name, table_id in targets.items():
# Get all records
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{table_id}/records?page_size=500"
data = api_get(url)
if data.get('code') != 0:
print(f"{name}: API error - {data.get('msg')}")
continue
items = data['data']['items']
print(f"\n{'='*50}")
print(f"{name}: {len(items)} records")
total = 0
skipped = 0
success = 0
failed = 0
for item in items:
fields = item.get('fields', {})
sid = fields.get('题目集合 ID', '') or ''
rid = item['record_id']
total += 1
# Skip 010199
if '010199' in str(sid):
skipped += 1
continue
# Skip empty
jd_str = fields.get('jsonData', '{}')
if not jd_str or jd_str == 'None':
skipped += 1
continue
ok, msg = fix_tags_for_record(rid, table_id, name)
if ok:
success += 1
print(f" {'' if 'updated' in msg else '⏭️'} ID={sid:10s} | {msg}")
else:
failed += 1
print(f" ❌ ID={sid:10s} | {msg}")
all_results.append({
'name': name, 'total': total, 'skipped': skipped,
'success': success, 'failed': failed
})
print(f"\n\n===== 标签修正汇总 =====")
for r in all_results:
print(f"{r['name']:10s}: total={r['total']} skipped={r['skipped']} success={r['success']} failed={r['failed']}")
# Verify
r = subprocess.run(["curl", "-s", "-X", "GET", url, "-H", f"Authorization: Bearer {TOKEN}"],
capture_output=True, text=True, timeout=10)
jd2 = json.loads(json.loads(r.stdout)['data']['record']['fields']['jsonData'])
a = jd2['first']['ability']
at = jd2['first']['abilityTag']
ok = a == tags and at == tags
print(f"[{qid}] ability={a} abilityTag={at} {'' if ok else ''}")

53
scripts/fix_answerset.py Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Fix 3 records with wrong answerSet ordering."""
import subprocess, json
APP_TOKEN = "CMHSbUUjka3TrUsaxxEc297ongf"
TABLE_ID = "tblszuk1TeToofBF"
APP_ID = "cli_a931175d41799cc7"
APP_SECRET = "Iw2vEfbjT6GtV0GhbxbZqfQ4nAPtbR14"
r = subprocess.run([
"curl", "-s", "-X", "POST",
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
"-H", "Content-Type: application/json",
"-d", json.dumps({"app_id": APP_ID, "app_secret": APP_SECRET})
], capture_output=True, text=True)
TOKEN = json.loads(r.stdout)["tenant_access_token"]
def get_jsonData(rid):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{rid}"
r = subprocess.run(["curl", "-s", "-X", "GET", url, "-H", f"Authorization: Bearer {TOKEN}"],
capture_output=True, text=True)
f = json.loads(r.stdout)['data']['record']['fields']
return json.loads(f['jsonData'])
def set_jsonData(rid, jd):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{rid}"
body = json.dumps({"fields": {"jsonData": json.dumps(jd, ensure_ascii=False)}}, ensure_ascii=False)
r = subprocess.run(["curl", "-s", "-X", "PUT", url,
"-H", f"Authorization: Bearer {TOKEN}",
"-H", "Content-Type: application/json",
"-d", body], capture_output=True, text=True)
resp = json.loads(r.stdout)
return resp.get("code") == 0
fixes = [
("recvjzbkvoOgvy", "022201", [0, 2, 3, 5, 1, 4]),
("recvjzblhhNt7Q", "022301", [1, 4, 5, 2, 0, 3]),
("recvjzXlxz4r3i", "032801", [5, 0, 4, 2, 1, 3]),
]
for rid, qid, correct_ans in fixes:
print(f"[{qid}] Fixing answerSet to {correct_ans}...", end=" ")
jd = get_jsonData(rid)
jd['first']['answerSet'] = correct_ans
ok = set_jsonData(rid, jd)
# Verify
jd2 = get_jsonData(rid)
stored = jd2['first']['answerSet']
if stored == correct_ans:
print("✅ Fixed")
else:
print(f"❌ Still wrong: got {stored}")

94
scripts/patch_ability.py Normal file
View File

@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Supplement abilityTag and complete ability/explanation for all 8 records."""
import subprocess, json
APP_TOKEN = "CMHSbUUjka3TrUsaxxEc297ongf"
TABLE_ID = "tblszuk1TeToofBF"
APP_ID = "cli_a931175d41799cc7"
APP_SECRET = "Iw2vEfbjT6GtV0GhbxbZqfQ4nAPtbR14"
r = subprocess.run([
"curl", "-s", "-X", "POST",
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
"-H", "Content-Type: application/json",
"-d", json.dumps({"app_id": APP_ID, "app_secret": APP_SECRET})
], capture_output=True, text=True)
TOKEN = json.loads(r.stdout)["tenant_access_token"]
print(f"Token: {TOKEN[:20]}...")
def get_jsonData(rid):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{rid}"
r = subprocess.run(["curl", "-s", "-X", "GET", url, "-H", f"Authorization: Bearer {TOKEN}"],
capture_output=True, text=True)
return json.loads(r.stdout)['data']['record']['fields']
def update_jsonData(rid, jd):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{rid}"
body = json.dumps({"fields": {"jsonData": json.dumps(jd, ensure_ascii=False)}}, ensure_ascii=False)
r = subprocess.run(["curl", "-s", "-X", "PUT", url,
"-H", f"Authorization: Bearer {TOKEN}",
"-H", "Content-Type: application/json",
"-d", body], capture_output=True, text=True)
return json.loads(r.stdout)
# Per-record ability / abilityTag / explanation definitions
SPECS = {
"recvjzbkvoOgvy": { # 022201
"ability": ["短消息写作|邮件/便条", "衔接与连贯|指代与连词使用", "句型组织|邮件段落排序"],
"abilityTag": ["短消息写作", "衔接与连贯", "句型组织"],
"explanation": '正确的邮件结构称呼Dear Max,)→ 正文第一句告诉Max你听说了旧盒子"I heard you found...")→ 正文第二句(询问盒子里有什么:"What did you find...")→ 正文第三句(建议一起打开:"Let\'s open it...")→ 结尾敬语Take care,)→ 署名Alex。学生需通过指代词it指代the old box和时间提示after school判断句子间的逻辑顺序。',
},
"recvjzblhhNt7Q": { # 022301
"ability": ["短消息写作|邮件/便条", "衔接与连贯|指代与连词使用", "句型组织|邮件段落排序"],
"abilityTag": ["短消息写作", "衔接与连贯", "句型组织"],
"explanation": "正确的邮件结构称呼Dear Emma,)→ 正文第一句(描述今天做了什么:\"We worked hard...\")→ 正文第二句(描述花园现状:\"All the new flowers...\")→ 正文第三句(建议下次行动:\"Next time we should...\")→ 结尾Your friend,)→ 署名Lily。学生需通过时间词this afternoon / next time判断事件先后顺序通过\"And\"连词识别并列描述关系。",
},
"recvjzblXOJ7Fs": { # 022401
"ability": ["短消息写作|邮件/便条", "衔接与连贯|指代与连词使用", "句型组织|邮件段落排序"],
"abilityTag": ["短消息写作", "衔接与连贯", "句型组织"],
"explanation": '正确的邮件结构称呼Dear Sophie,)→ 正文第一句(告知探病事实:"Tom and I went...")→ 正文第二句描述Jack当前状态"He is feeling much better...")→ 正文第三句(建议对方可做的事:"You could send him...")→ 结尾敬语Warmly,)→ 署名Lily。学生需通过人称指代him指代Jack和连词and he smiled when...)判断句子衔接关系。',
},
"recvjzXjMKAQ4i": { # 032501
"ability": ["短消息写作|邮件/便条", "衔接与连贯|指代与连词使用", "情感表达|鼓励与共情", "句型组织|邮件段落排序"],
"abilityTag": ["短消息写作", "衔接与连贯", "情感表达", "句型组织"],
"explanation": '正确的邮件结构称呼Dear Ben,)→ 正文第一句(表达听说了对方的担忧:"I heard you are worried...")→ 正文第二句(共情自己的紧张感:"I feel nervous...but practising helps...")→ 正文第三句(鼓励不要放弃:"Please don\'t give up...")→ 结尾敬语Yours truly,)→ 署名Sam。学生需通过转折连词but和破折号—解释说明"you have already worked so hard")判断句子逻辑关系。',
},
"recvjzXklYIE05": { # 032601
"ability": ["短消息写作|邮件/便条", "衔接与连贯|指代与连词使用", "社交写作|感谢信", "句型组织|邮件段落排序"],
"abilityTag": ["短消息写作", "衔接与连贯", "社交写作", "句型组织"],
"explanation": '正确的邮件结构称呼Dear Mr. Brown,)→ 正文第一句(表达感谢:"Thank you so much...")→ 正文第二句(描述最喜欢的内容:"I enjoyed the planet show...")→ 正文第三句(请求分享照片:"Could you please share...")→ 结尾敬语Warm regards,)→ 署名Lucy。学生需通过破折号—补充说明"really amazing"和指代词the trip回指上文博物馆之旅判断句子衔接顺序。',
},
"recvjzXkYnsQ8r": { # 032701
"ability": ["短消息写作|邮件/便条", "衔接与连贯|指代与连词使用", "描述性写作|地点与人物", "句型组织|邮件段落排序"],
"abilityTag": ["短消息写作", "衔接与连贯", "描述性写作", "句型组织"],
"explanation": '正确的邮件结构称呼Dear Anna,)→ 正文第一句(描述去过咖啡馆:"I visited a lovely little cafe...")→ 正文第二句(描述店员特点:"The people working there...")→ 正文第三句(建议一起去:"We should go there together...")→ 结尾敬语Kind regards,)→ 署名Emma。学生需通过指代词there指代the cafe和破折号—补充解释"you would love it")判断句子间的逻辑顺序。',
},
"recvjzXlxz4r3i": { # 032801
"ability": ["短消息写作|邮件/便条", "衔接与连贯|指代与连词使用", "问题解决|求助与建议", "句型组织|邮件段落排序"],
"abilityTag": ["短消息写作", "衔接与连贯", "问题解决", "句型组织"],
"explanation": '正确的邮件结构称呼Dear Miss Green,)→ 正文第一句(说明项目停止工作:"Our science project has suddenly stopped...")→ 正文第二句(描述已尝试的方案:"We have tried changing the batteries, but...")→ 正文第三句(请求建议:"Could you give us some advice...")→ 结尾敬语Gratefully,)→ 署名Tom。学生需通过转折连词but和因果指代"that did not fix..."中that指代changing batteries判断句子逻辑。',
},
"recvjzXm5yEXBY": { # 032901
"ability": ["短消息写作|邮件/便条", "衔接与连贯|指代与连词使用", "描述性写作|场景与情绪", "句型组织|邮件段落排序"],
"abilityTag": ["短消息写作", "衔接与连贯", "描述性写作", "句型组织"],
"explanation": '正确的邮件结构称呼Dear Sophie,)→ 正文第一句(描述正在派对:"I\'m at my mum\'s birthday party...")→ 正文第二句(描述家人表现:"My little sister is dancing...Dad seems...")→ 正文第三句(表达感受:"I feel so lucky...")→ 结尾敬语With love,)→ 署名Jessica。学生需通过并列连词and和时间提示词right now / tonight判断描述的层次推进。',
},
}
for rid, spec in SPECS.items():
fields = get_jsonData(rid)
jd = json.loads(fields["jsonData"])
qid = jd["first"]["questionSetID"]
jd["first"]["ability"] = spec["ability"]
jd["first"]["abilityTag"] = spec["abilityTag"]
jd["first"]["explanation"] = spec["explanation"]
print(f"[{qid}] updating...", end=" ")
resp = update_jsonData(rid, jd)
if resp.get("code") == 0:
print("")
else:
print(f"{resp.get('msg')}")
print("\nDone.")

View File

@ -0,0 +1,264 @@
#!/usr/bin/env python3
"""Update 8 writing P1 email records in bitable."""
import subprocess, json, sys
APP_TOKEN = "CMHSbUUjka3TrUsaxxEc297ongf"
TABLE_ID = "tblszuk1TeToofBF"
APP_ID = "cli_a931175d41799cc7"
APP_SECRET = "Iw2vEfbjT6GtV0GhbxbZqfQ4nAPtbR14"
# Get token
r = subprocess.run([
"curl", "-s", "-X", "POST",
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
"-H", "Content-Type: application/json",
"-d", json.dumps({"app_id": APP_ID, "app_secret": APP_SECRET})
], capture_output=True, text=True)
TOKEN = json.loads(r.stdout)["tenant_access_token"]
print(f"Token obtained: {TOKEN[:20]}...")
def update_record(record_id, fields):
"""Update a single record via curl."""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{record_id}"
body = json.dumps({"fields": fields}, ensure_ascii=False)
r = subprocess.run([
"curl", "-s", "-X", "PUT", url,
"-H", f"Authorization: Bearer {TOKEN}",
"-H", "Content-Type: application/json",
"-d", body
], capture_output=True, text=True)
resp = json.loads(r.stdout)
if resp.get("code") != 0:
print(f" ❌ FAILED: {resp.get('msg', resp)}")
return False
print(f" ✅ OK")
return True
# ========== Record definitions ==========
records = {
# ---- L2 B级 ----
"recvjzbkvoOgvy": { # 022201
"jsonData": json.dumps({
"first": {
"category": "writing",
"type": "writing_email",
"questionSetID": "022201",
"textDesc": "Your friend Max found an old box in the attic and you are curious about it. Write an email to Max.\n\nIn your email:\n1. tell Max you heard about the old box\n2. ask what he found inside the box\n3. suggest opening the box together",
"optionSetList": [
"Dear Max,",
"Take care,",
"I heard you found an old box in the attic.",
"What did you find inside it?",
"Alex",
"Let's open it together after school."
],
"answerSet": [0, 2, 3, 5, 1, 4],
"ability": ["短消息写作|邮件/便条", "衔接与连贯|连词使用"],
"explanation": "6句邮件排序Dear开头→描述听说的消息→询问盒子内容→建议一起打开→Take care收尾→Alex署名。",
"questionSet": []
},
"second": {}
}, ensure_ascii=False),
"题目1": "【题目描述】\nYour friend Max found an old box in the attic and you are curious about it. Write an email to Max.\n\nIn your email:\n1. tell Max you heard about the old box\n2. ask what he found inside the box\n3. suggest opening the box together\n\n【段落列表】\nA. Dear Max,\nB. Take care,\nC. I heard you found an old box in the attic.\nD. What did you find inside it?\nE. Alex\nF. Let's open it together after school.",
"审校结果": "✅ OK | 2026-05-22 小研优化"
},
"recvjzblhhNt7Q": { # 022301
"jsonData": json.dumps({
"first": {
"category": "writing",
"type": "writing_email",
"questionSetID": "022301",
"textDesc": "You and your friend Emma are working on a community garden. You spent the afternoon there. Write an email to Emma.\n\nIn your email:\n1. tell Emma what you did in the garden today\n2. describe what the garden looks like now\n3. suggest what to do next time",
"optionSetList": [
"Your friend,",
"Dear Emma,",
"Next time we should water the young trees together.",
"Lily",
"We worked hard in the community garden this afternoon.",
"All the new flowers make the garden look really colourful now."
],
"answerSet": [1, 4, 5, 2, 0, 3],
"ability": ["短消息写作|邮件/便条", "衔接与连贯|连词使用"],
"explanation": "6句邮件排序Dear开头→描述今天劳动内容→描述花园现状→建议下次行动→Your friend收尾→Lily署名。",
"questionSet": []
},
"second": {}
}, ensure_ascii=False),
"题目1": "【题目描述】\nYou and your friend Emma are working on a community garden. You spent the afternoon there. Write an email to Emma.\n\nIn your email:\n1. tell Emma what you did in the garden today\n2. describe what the garden looks like now\n3. suggest what to do next time\n\n【段落列表】\nA. Your friend,\nB. Dear Emma,\nC. Next time we should water the young trees together.\nD. Lily\nE. We worked hard in the community garden this afternoon.\nF. All the new flowers make the garden look really colourful now.",
"审校结果": "✅ OK | 2026-05-22 小研优化"
},
"recvjzblXOJ7Fs": { # 022401
"jsonData": json.dumps({
"first": {
"category": "writing",
"type": "writing_email",
"questionSetID": "022401",
"textDesc": "Your friend Jack is in hospital. You visited him today with your friend Tom. Write an email to Sophie.\n\nIn your email:\n1. tell Sophie you visited Jack at the hospital\n2. describe how Jack is feeling now\n3. suggest what Sophie can do to help",
"optionSetList": [
"Warmly,",
"Tom and I went to the hospital to visit Jack today.",
"Dear Sophie,",
"You could send him a card to cheer him up.",
"Lily",
"He is feeling much better and he smiled when he saw us."
],
"answerSet": [2, 1, 5, 3, 0, 4],
"ability": ["短消息写作|邮件/便条", "衔接与连贯|连词使用"],
"explanation": "6句邮件排序Dear开头→告知探病事实→描述Jack状态→建议对方行动→Warmly收尾→Lily署名。",
"questionSet": []
},
"second": {}
}, ensure_ascii=False),
"题目1": "【题目描述】\nYour friend Jack is in hospital. You visited him today with your friend Tom. Write an email to Sophie.\n\nIn your email:\n1. tell Sophie you visited Jack at the hospital\n2. describe how Jack is feeling now\n3. suggest what Sophie can do to help\n\n【段落列表】\nA. Warmly,\nB. Tom and I went to the hospital to visit Jack today.\nC. Dear Sophie,\nD. You could send him a card to cheer him up.\nE. Lily\nF. He is feeling much better and he smiled when he saw us.",
"审校结果": "✅ OK | 2026-05-22 小研优化"
},
# ---- L2 C级 ----
"recvjzXjMKAQ4i": { # 032501
"jsonData": json.dumps({
"first": {
"category": "writing",
"type": "writing_email",
"questionSetID": "032501",
"textDesc": "Your friend Ben is worried about the school concert and wants to give up. Write an email to encourage him.\n\nIn your email:\n1. tell Ben you heard he is worried about the concert\n2. share how you feel about performing too\n3. encourage him not to give up",
"optionSetList": [
"Yours truly,",
"I feel nervous before performing too, but practising helps me feel ready.",
"Dear Ben,",
"Please don't give up — you have already worked so hard for this.",
"Sam",
"I heard you are worried about the school concert."
],
"answerSet": [2, 5, 1, 3, 0, 4],
"ability": ["短消息写作|邮件/便条", "衔接与连贯|连词使用", "情感表达|鼓励与共情"],
"explanation": "6句邮件排序Dear开头→表达听说了对方的担忧→共情自己也有紧张→鼓励不要放弃→Yours truly收尾→Sam署名。",
"questionSet": []
},
"second": {}
}, ensure_ascii=False),
"题目1": "【题目描述】\nYour friend Ben is worried about the school concert and wants to give up. Write an email to encourage him.\n\nIn your email:\n1. tell Ben you heard he is worried about the concert\n2. share how you feel about performing too\n3. encourage him not to give up\n\n【段落列表】\nA. Yours truly,\nB. I feel nervous before performing too, but practising helps me feel ready.\nC. Dear Ben,\nD. Please don't give up — you have already worked so hard for this.\nE. Sam\nF. I heard you are worried about the school concert.",
"审校结果": "✅ OK | 2026-05-22 小研优化"
},
"recvjzXklYIE05": { # 032601
"jsonData": json.dumps({
"first": {
"category": "writing",
"type": "writing_email",
"questionSetID": "032601",
"textDesc": "You just came back from a wonderful school trip to the science museum, organised by your teacher Mr. Brown. Write a thank-you email to him.\n\nIn your email:\n1. thank Mr. Brown for organising the school trip\n2. tell him what you enjoyed most at the museum\n3. ask him to share the photos he took",
"optionSetList": [
"Warm regards,",
"I enjoyed the planet show the most — it was really amazing.",
"Dear Mr. Brown,",
"Could you please share the photos you took during the trip?",
"Thank you so much for the wonderful trip to the science museum.",
"Lucy"
],
"answerSet": [2, 4, 1, 3, 0, 5],
"ability": ["短消息写作|邮件/便条", "衔接与连贯|连词使用", "社交写作|感谢信"],
"explanation": "6句邮件排序Dear开头→表达感谢→描述最喜欢的展览→请求分享照片→Warm regards收尾→Lucy署名。",
"questionSet": []
},
"second": {}
}, ensure_ascii=False),
"题目1": "【题目描述】\nYou just came back from a wonderful school trip to the science museum, organised by your teacher Mr. Brown. Write a thank-you email to him.\n\nIn your email:\n1. thank Mr. Brown for organising the school trip\n2. tell him what you enjoyed most at the museum\n3. ask him to share the photos he took\n\n【段落列表】\nA. Warm regards,\nB. I enjoyed the planet show the most — it was really amazing.\nC. Dear Mr. Brown,\nD. Could you please share the photos you took during the trip?\nE. Thank you so much for the wonderful trip to the science museum.\nF. Lucy",
"审校结果": "✅ OK | 2026-05-22 小研优化"
},
"recvjzXkYnsQ8r": { # 032701
"jsonData": json.dumps({
"first": {
"category": "writing",
"type": "writing_email",
"questionSetID": "032701",
"textDesc": "You visited a lovely little cafe that your cousin found last week. Write an email to your friend Anna to tell her about it.\n\nIn your email:\n1. tell Anna about the cafe you visited\n2. describe the people you met there\n3. suggest visiting the cafe together sometime",
"optionSetList": [
"Kind regards,",
"I visited a lovely little cafe that my cousin found last week.",
"We should go there together one afternoon — you would really love it.",
"The people working there were so friendly and kind to every customer.",
"Emma",
"Dear Anna,"
],
"answerSet": [5, 1, 3, 2, 0, 4],
"ability": ["短消息写作|邮件/便条", "衔接与连贯|连词使用", "描述性写作|地点与人物"],
"explanation": "6句邮件排序Dear开头→描述去过咖啡馆→描述店员友好→建议一起去→Kind regards收尾→Emma署名。",
"questionSet": []
},
"second": {}
}, ensure_ascii=False),
"题目1": "【题目描述】\nYou visited a lovely little cafe that your cousin found last week. Write an email to your friend Anna to tell her about it.\n\nIn your email:\n1. tell Anna about the cafe you visited\n2. describe the people you met there\n3. suggest visiting the cafe together sometime\n\n【段落列表】\nA. Kind regards,\nB. I visited a lovely little cafe that my cousin found last week.\nC. We should go there together one afternoon — you would really love it.\nD. The people working there were so friendly and kind to every customer.\nE. Emma\nF. Dear Anna,",
"审校结果": "✅ OK | 2026-05-22 小研优化"
},
"recvjzXlxz4r3i": { # 032801
"jsonData": json.dumps({
"first": {
"category": "writing",
"type": "writing_email",
"questionSetID": "032801",
"textDesc": "Your group's science project has suddenly stopped working and you need your teacher's help. Write an email to Miss Green.\n\nIn your email:\n1. tell Miss Green your science project has stopped working\n2. describe what you have tried to fix it\n3. ask for her advice on what to do next",
"optionSetList": [
"Our science project has suddenly stopped working and we are not sure why.",
"Gratefully,",
"Could you give us some advice on what we should try next?",
"Tom",
"We have tried changing the batteries, but that did not fix the problem.",
"Dear Miss Green,"
],
"answerSet": [5, 0, 4, 2, 1, 3],
"ability": ["短消息写作|邮件/便条", "衔接与连贯|连词使用", "问题解决|求助与建议"],
"explanation": "6句邮件排序Dear开头→说明项目故障→描述已尝试方案→请求建议→Gratefully收尾→Tom署名。",
"questionSet": []
},
"second": {}
}, ensure_ascii=False),
"题目1": "【题目描述】\nYour group's science project has suddenly stopped working and you need your teacher's help. Write an email to Miss Green.\n\nIn your email:\n1. tell Miss Green your science project has stopped working\n2. describe what you have tried to fix it\n3. ask for her advice on what to do next\n\n【段落列表】\nA. Our science project has suddenly stopped working and we are not sure why.\nB. Gratefully,\nC. Could you give us some advice on what we should try next?\nD. Tom\nE. We have tried changing the batteries, but that did not fix the problem.\nF. Dear Miss Green,",
"审校结果": "✅ OK | 2026-05-22 小研优化"
},
"recvjzXm5yEXBY": { # 032901
"jsonData": json.dumps({
"first": {
"category": "writing",
"type": "writing_email",
"questionSetID": "032901",
"textDesc": "You are at your mum's birthday party and having a wonderful time. Write an email to your friend Sophie to share the happy news.\n\nIn your email:\n1. tell Sophie about your mum's birthday party\n2. describe what everyone is doing at the party\n3. share how you feel about this special day",
"optionSetList": [
"With love,",
"My little sister is dancing happily and Dad seems in a wonderful mood tonight.",
"I'm at my mum's birthday party right now and everyone is having a great time.",
"I feel so lucky to celebrate this special day with my whole family.",
"Dear Sophie,",
"Jessica"
],
"answerSet": [4, 2, 1, 3, 0, 5],
"ability": ["短消息写作|邮件/便条", "衔接与连贯|连词使用", "描述性写作|场景与情绪"],
"explanation": "6句邮件排序Dear开头→描述正在派对→描述大家在做的事→表达感受→With love收尾→Jessica署名。",
"questionSet": []
},
"second": {}
}, ensure_ascii=False),
"题目1": "【题目描述】\nYou are at your mum's birthday party and having a wonderful time. Write an email to your friend Sophie to share the happy news.\n\nIn your email:\n1. tell Sophie about your mum's birthday party\n2. describe what everyone is doing at the party\n3. share how you feel about this special day\n\n【段落列表】\nA. With love,\nB. My little sister is dancing happily and Dad seems in a wonderful mood tonight.\nC. I'm at my mum's birthday party right now and everyone is having a great time.\nD. I feel so lucky to celebrate this special day with my whole family.\nE. Dear Sophie,\nF. Jessica",
"审校结果": "✅ OK | 2026-05-22 小研优化"
},
}
# ========== Execute updates ==========
print(f"\nUpdating {len(records)} records...\n")
success = 0
fail = 0
for rid, fields in records.items():
qid = json.loads(fields["jsonData"])["first"]["questionSetID"]
print(f"[{qid}] {rid} ...", end=" ")
if update_record(rid, fields):
success += 1
else:
fail += 1
print(f"\nDone: {success} OK, {fail} failed")