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

This commit is contained in:
ai_member_only 2026-06-24 08:10:01 +08:00
parent 08a205c409
commit 60dcd4edf1
12 changed files with 6535 additions and 71 deletions

View File

@ -192,6 +192,7 @@ Skills 按四层架构组织:
| 身份识别 | `lark-identify-sender` | `./skills/lark-identify-sender/SKILL.md` | — |
| 多维表格结构读取 | `bitable-reader` | `./business_production/_shared/bitable-reader/SKILL.md` | Bot |
| L1配置审校 | `audit_l1_config` | `./business_production/_shared/audit_l1_config/SKILL.md` | — |
| L2剧本审校 | `l2-script-review` | `./business_production/_shared/l2-script-review/SKILL.md` | — |
| 知识点掌握度计算 | `knowledge-mastery-calculator` | `./business_production/_shared/knowledge-mastery-calculator/SKILL.md` | — |
### 教研资源速查(业务知识库)
@ -232,6 +233,7 @@ Skills 按四层架构组织:
| `/英文台词`、`英文台词生产` | `kids-english-script-production` | `business_production/英文台词/skills/kids-english-script-production/SKILL.md` |
| 飞书文档表格读写通用I/O层 | `feishu-doc-io` | `business_production/英文台词/skills/feishu-table-translate-fill/SKILL.md` |
| `剧本复核`、`/剧本复核` | `script-post-check` | `business_production/英文台词/skills/script-post-check/SKILL.md` |
| `L2审校`、`剧本审校`、`L2剧本审校` | `l2-script-review` | `business_production/_shared/l2-script-review/SKILL.md` |
> **示例:** 用户发来飞书 wiki 链接说「英文台词生产」→ 调用 `kids-english-script-production`,用 `--feishu-url` 模式读 F 列编剧台词 → 英语思维重构 → 写回 G 列
> **示例:** 用户说「剧本复核 <链接>」→ 调用 `script-post-check`,对 F 列执行长句拆分 + 标点检查等代码规则复核

View File

@ -37,8 +37,7 @@
| 姓名 | 飞书 user_id |
|------|-------------|
| (姓名) | `xxxx` |
| (姓名) | `xxxx` |
| 张幸莹 | `4535af71` |
### 第二级:受限权限用户

View File

@ -0,0 +1,110 @@
# L2 剧本教研规范
### **台词字数**
L2 每课台词范围:**1500\-2000 个中文字**
重剧情关卡:不超过**2200 个中文字**
如果超过 2300 字,学生在 20\-25 分钟体验中会明显变成“听故事为主”,互动和练习空间会被压缩。
> 参考依据Cambridge A2 Key / KET Listening 是 **30 分钟、5 个部分、25 题**要求学生能理解“语速较慢”的口语材料Speaking 是 **8\-10 分钟**,重点是回答和提问简单问题。官方格式见 [Cambridge A2 Key exam format](https://www.cambridgeenglish.org/exams-and-tests/qualifications/key/format/)。
>
> KET Listening 30 分钟对应 25 题,约等于 **1\.2 分钟/题**。L2 一节课 22\-26 个互动,互动密度已经接近 KET Listening但 L2 是教学产品,不是纯测试,所以需要更多停顿和反馈。
>
> 因此,真正能承载剧情对白的空间大约是 **8\-10 分钟有效听读量**。按 A2/KET“较慢、清晰”的语言输入估算英文台词约 **1000\-1300 词**。换成中文,约等于**1500\-2000 个中文。**
>
>
单句词数建议
一般对白: 8\-14
互动句子6\-12
阅读理解句子10\-20
听力句子6\-12
### 台词难度
1\.语言准确,**语法正确**,符合英语**母语表达习惯**,符合上下文逻辑;
2\.所有单词**英式拼写**
3\.**大小写和标点**:符合英式英语正式书面文本使用标准;
4\.**词句难度**剑桥A1\-A2水平不要超过A2
### **互动规则**
L2 每课知识点为:
- 单词 6 个;
- 句型 3 个;
- 每个知识点 2\-3 次互动练习;
- 总互动量 **22\-26 个(包含核心互动)**
- 核心互动: S3/S4\-1个lesson 2个核心互动 读3: 说3: 听2 写2
### 互动选择参考
#### 中互动
物品互动
图片单选
图片多选
图片有序
对话朗读
对话挖空
对话组句
对话表达
对话选读
对话选择
信息拼词
信息补词
信息组句
信息填词
信息填句
#### 核心互动

View File

@ -132,7 +132,54 @@ curl -s -X GET \
### 第四步:写入数据
#### 写入/更新指定行
#### ⚠️ 步骤 4.0:行号连续性强制校验(写入前必须执行,不可跳过)
**问题:** 飞书 Sheets API 的 range 写入按连续区域覆盖。如果 range 指定 `F4:F67`API 会把数据依次写入 Row 4, 5, 6, ..., 67**不管这些行是否对应正确的数据行**。
**典型事故:** 剧本表格中E 列有角色名的行号不连续Row 4 有台词、Row 5 是剧情描述空行、Row 6 又有台词),如果按连续 range 批量写入Row 6 的英文台词会写到 Row 5 上,造成全列偏移。
**强制规则(不可跳过):**
```python
def validate_and_write(spreadsheet_token, sheet_id, column_letter, data: dict, token):
"""
写入前强制校验行号连续性,不连续时自动切换逐行写入。
Args:
data: {行号: "内容"} 字典,如 {118: "They are like kids.", 120: "Enough!", ...}
"""
rows = sorted(data.keys())
# 检查连续性
is_continuous = all(rows[i+1] - rows[i] == 1 for i in range(len(rows)-1))
if is_continuous:
# ✅ 行号连续 → 批量 range 写入(高效)
values = [[data[r]] for r in rows]
range_str = f"{sheet_id}!{column_letter}{rows[0]}:{column_letter}{rows[-1]}"
write_range(spreadsheet_token, range_str, values, token)
print(f"✅ 批量写入: {range_str} ({len(rows)} 行,行号连续)")
else:
# ⚠️ 行号不连续 → 必须逐行写入(安全)
print(f"⚠️ 行号不连续,切换逐行写入(共 {len(rows)} 行)")
gaps = [rows[i+1] - rows[i] for i in range(len(rows)-1) if rows[i+1] - rows[i] > 1]
print(f" 跳空位置: {[(rows[i], rows[i+1]) for i in range(len(rows)-1) if rows[i+1] - rows[i] > 1]}")
for row in rows:
range_str = f"{sheet_id}!{column_letter}{row}:{column_letter}{row}"
write_range(spreadsheet_token, range_str, [[data[row]]], token)
time.sleep(0.15) # 逐行写入间隔,避免频率限制
# 写入后立即执行 E-F 对齐校验(步骤 4.3
verify_ef_alignment(spreadsheet_token, sheet_id, rows, token)
```
**禁止行为:**
- ❌ 直接构造连续 range`F118:F142`)而不检查行号是否真的连续
- ❌ 假设行号连续、跳过校验
- ❌ 写入后不回读验证
#### 步骤 4.1:写入/更新指定行
```bash
curl -s -X PUT \
@ -149,9 +196,10 @@ curl -s -X PUT \
}'
```
#### 批量写入多行
#### 步骤 4.2批量写入多行(仅行号连续时使用)
```bash
# ⚠️ 仅当行号连续时可用!写入前必须先执行步骤 4.0 校验
curl -s -X PUT \
"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/${SPREADSHEET_TOKEN}/values" \
-H "Authorization: Bearer $TOKEN" \
@ -168,6 +216,45 @@ curl -s -X PUT \
}'
```
#### 步骤 4.3:写入后 E-F 对齐验证(强制执行)
写入完成后,必须回读 E 列和 F 列,逐行验证对齐:
```python
def verify_ef_alignment(spreadsheet_token, sheet_id, written_rows, token):
"""
写入后验证E 列有角色名的行F 列必须有对应台词;
E 列为空的行剧情描述行F 列必须为空。
"""
min_row = min(written_rows)
max_row = max(written_rows)
# 回读 E 列和 F 列
range_str = f"{sheet_id}!E{min_row}:F{max_row}"
values = read_range(spreadsheet_token, range_str, token)
errors = []
for i, row_data in enumerate(values):
actual_row = min_row + i
e_val = row_data[0] if len(row_data) > 0 else None # E列角色名
f_val = row_data[1] if len(row_data) > 1 else None # F列英文台词
if e_val and not f_val:
# E 列有角色名但 F 列为空 → 台词缺失
errors.append(f"行{actual_row}: E列有角色[{e_val}]但F列为空台词缺失")
elif not e_val and f_val:
# E 列为空但 F 列有内容 → 位移!台词写到了剧情描述行
errors.append(f"行{actual_row}: E列为空但F列有内容[{f_val[:30]}...],台词位移到非台词行!")
if errors:
print("❌ E-F 对齐校验失败:")
for e in errors:
print(f" {e}")
raise RuntimeError("写入后对齐校验失败,数据可能错位,请检查并修复")
else:
print(f"✅ E-F 对齐校验通过({len(written_rows)} 行)")
```
#### 更新指定单元格
```bash
@ -188,7 +275,7 @@ curl -s -X PUT \
写入空字符串 `""` 即可清空单元格内容。
### 第五步:写入后验证
### 第五步:写入后回读验证
**必须回读验证**,确保写入内容与预期一致:
@ -208,30 +295,7 @@ curl -s -X GET \
6. **数据类型**:写入时,数字直接用 number 类型,文本用 string空值用 null
7. **换行符**:需要在单元格内换行时,在字符串中使用 `\n`
### ⚠️ 批量写入前必须验证行号连续性(强制执行)
**问题:** 飞书 Sheets API 的 range 写入是按连续区域覆盖的。如果 range 指定 `F4:F67`API 会把数据依次写入 Row 4, 5, 6, ..., 67**不管这些行是否对应正确的数据行**。
**场景:** 剧本表格中E 列有内容的行号不连续Row 4 有台词、Row 5 是剧情描述空行、Row 6 又有台词),如果按连续 range 批量写入,会导致 Row 6 的英文台词写到 Row 5 上,造成全列偏移。
**强制规则:**
1. 写入前,先统计目标行号列表,检查是否连续(`rows[i+1] - rows[i] == 1`
2. **行号连续** → 可以用 range 批量写入(如 `F4:F67`
3. **行号不连续** → 必须逐行写入(每个行号单独一个 range`F4:F4`, `F6:F6`, ...),或先清空整列再逐行写入
4. 写入后必须抽样验证 E/F 列行号对齐(至少抽查前 5 行、中间 5 行、末尾 5 行)
```python
# 示例:检查行号连续性
rows = sorted(data.keys())
is_continuous = all(rows[i+1] - rows[i] == 1 for i in range(len(rows)-1))
if is_continuous:
# 安全:批量 range 写入
write_range(f"F{rows[0]}:F{rows[-1]}", values)
else:
# 不安全:逐行写入
for row in rows:
write_range(f"F{row}:F{row}", [data[row]])
```
## 读取后错行/乱行校验(强制执行)

View File

@ -0,0 +1,298 @@
---
name: l2-script-review
version: 1.0.0
description: "L2剧本教研审校技能。对L2级别剧本执行9维度逐项审校输出结构化审校报告。触发方式消息包含'L2审校'或'剧本审校'或'L2剧本审校'。"
---
# L2 剧本教研审校l2-script-review
## 定位
L2 剧本的**全维度教研审校**。基于 `L2 剧本教研规范` 对剧本执行 9 个维度的逐项检查,输出结构化审校报告。审校结果需经人工确认后再执行修改。
## 触发词
| 触发词 | 效果 |
|--------|------|
| `L2审校` + 飞书链接 | 对指定文档执行全部 9 维度审校 |
| `剧本审校` + 飞书链接 | 同上 |
| `L2剧本审校` + 飞书链接 | 同上 |
## 审校流程9 维度)
### 一、台词字数
**规范来源:** `business_knowledge/L2/L2 剧本教研规范 .md`
| 指标 | 规范值 | 重剧情上限 | 超标阈值 |
|------|--------|-----------|----------|
| 中文台词字数 | 15002000 字 | ≤2200 字 | >2300 字 |
| 英文台词词数 | 10001300 词 | — | — |
**检查方法:**
1. 统计剧本表中「中文对白」列的总中文字数
2. 统计「翻译」列的总英文词数
3. 与规范值对比,标注超标幅度
**单句词数检查(逐行):**
| 场景 | 建议范围 | 上限 |
|------|----------|------|
| 一般对白 | 814 词 | 18 词 |
| 互动句子 | 612 词 | 15 词 |
| 阅读理解句子 | 1020 词 | 22 词 |
| 听力句子 | 612 词 | 15 词 |
### 二、互动量
**规范来源:** `business_knowledge/L2/L2 剧本教研规范 .md`
| 指标 | 规范值 |
|------|--------|
| 总互动量 | 2226 个(含核心互动) |
| 核心互动数 | 2 个/lessonS3/S4 |
**检查方法:**
1. 统计剧本表中所有互动行(非 TL 行)
2. 统计核心互动数量
3. 与规范值对比
### 三、知识点覆盖
**规范来源:** `business_knowledge/L2/L2 剧本教研规范 .md`
| 指标 | 规范值 |
|------|--------|
| 单词数 | 6 个 |
| 句型数 | 3 个 |
| 每个知识点互动次数 | 23 次 |
**检查方法:**
1. 从教研知识区提取单词和句型列表
2. 在剧本表中统计每个知识点的互动出现次数
3. 标注超标(>3 次)或不足(<2 的知识点
**词库校验:**
- 单词必须在 `business_knowledge/L2_word_list.json` 范围内
- 句型必须在 `business_knowledge/L2_pattern_list.json` 范围内
### 四、核心互动
**规范来源:** `business_knowledge/L2/L2 剧本教研规范 .md`
| 指标 | 规范值 |
|------|--------|
| 每 lesson 核心互动数 | 2 个 |
| 全 Unit 类型比例 | 读3 : 说3 : 听2 : 写2 |
**检查方法:**
1. 从教研知识区提取核心互动列表
2. 检查每 lesson 是否超过 2 个核心互动
3. 统计全 Unit 核心互动类型分布(读/说/听/写)
4. 与规范比例对比,标注偏差
**核心互动类型映射:**
- 阅读理解 → 读
- 任务对话 / 口语妙问 / 口语独白 → 说
- 合作听力 → 听
- 邮件撰写 / 写作回复 → 写
### 五、拼写/语法
**检查项:**
| 检查项 | 规则 | 修正方式 |
|--------|------|----------|
| 常见拼写错误 | 扫描 `recieve` `seperate` `occured` 等高频错误 | 标注正确拼写 |
| 重复词 | 连续出现两次相同单词 | 标注位置 |
| its/it's 混淆 | `its` 后接形容词/副词 | 标注修正 |
| 主谓一致 | 第三人称单数遗漏 `-s` | 标注修正 |
| 时态一致性 | 同一段落时态跳跃 | 标注修正 |
| 冠词缺失 | 单数可数名词前缺 `a/an/the` | 标注修正 |
**检查方法:**
1. 逐行扫描翻译列英文文本
2. 对每个检查项执行正则匹配
3. 标注位置、原文、修正建议
### 六、词汇难度
**规范来源:** `business_knowledge/L2/L2 剧本教研规范 .md` — 剑桥 A1A2 水平,不超过 A2
**检查方法:**
1. 提取翻译列所有英文词汇
2. 对照 CEFR 等级表,标记 B1 及以上词汇
3. 逐词判断:剧情核心词可保留(标注 🟡),非必要超纲词建议替换(标注 🔴)
**分级判定:**
| CEFR | 判定 | 处理 |
|------|------|------|
| A1A2 | ✅ 合规 | 无需处理 |
| B1剧情核心词 | 🟡 可保留 | 标注但建议确认 |
| B1非核心词 | 🔴 建议替换 | 给出 A2 替代词 |
| B2+ | 🔴 必改 | 给出 A2 替代词 |
**常见 B2+ 超纲词及替换建议:**
| 超纲词 | CEFR | 建议替换 |
|--------|------|----------|
| unforgivable | B2 | very wrong / cannot be forgiven |
| arrogance | B2 | too much pride |
| corrupts | B2 | changes / makes people bad |
| electromagnetic | B2+ | laser |
| bargain (v.) | B2 | argue / discuss |
| negotiate | B2 | talk about / discuss |
| convince | B1 | make someone believe |
### 七、标点符号与价值观
#### 7.1 标点符号
| 检查项 | 规则 | 修正 |
|--------|------|------|
| 波浪号 `` | 中文对白中出现 | 改为 `。``` |
| 双叹号 `!!` | 英文翻译中出现 | 归一为单个 `!` |
| Markdown 标记 `**` | 翻译列中出现 | 清除 |
| 中文混用半角标点 | 中文对白中出现 `!` `?` | 改为全角 `` `` |
| 英文混用全角标点 | 翻译列中出现 `` `` `` | 改为半角 `!` `?` `,` |
| 中英文混排空格 | 中文与英文/数字之间缺空格 | 添加半角空格 |
#### 7.2 价值观/用语
| 检查项 | 规则 | 修正 |
|--------|------|------|
| 侮辱性词汇 | `白痴` `人渣` `废物`(贬义用法) | 替换为中性表达 |
| 暴力隐喻 | `吃人不吐骨` `死得很惨` `只能活一个` | 替换为温和表达 |
| 歧视性比喻 | `猴子们`(贬义) | 替换为 `you guys` |
| 威胁性语言 | `你会后悔的` `让你好看` | 替换为正向表达 |
### 八、结构性问题
**检查项:**
| 检查项 | 说明 |
|--------|------|
| 教研与剧本同步 | 教研知识区的知识点/核心互动是否与剧本表一致 |
| 删除线残留 | 剧本表中 `~~删除线~~` 内容是否已清理 |
| 场景衔接 | 场景切换是否流畅,有无逻辑跳跃 |
| 角色出场合理性 | 角色出场是否符合剧情逻辑 |
| 组件配置完整性 | 互动行的组件配置列是否已填写 |
| 配置信息完整性 | 互动行的配置信息列是否已填写 |
| 知识点标注准确性 | 剧本表中知识点列标注是否与教研知识区一致 |
### 九、总结评分
对前 8 个维度汇总评分:
| 维度 | 状态 | 等级 |
|------|------|------|
| 台词字数 | 超标/偏多/合规 | 🔴/🟡/✅ |
| 互动量 | 超标/合规 | 🔴/✅ |
| 知识点覆盖 | 过量/不足/合规 | 🔴/🟡/✅ |
| 核心互动 | 超标/不一致/合规 | 🔴/🟡/✅ |
| 拼写/语法 | 硬伤数 | 🔴/🟡/✅ |
| 词汇难度 | 超纲数 | 🔴/🟡/✅ |
| 标点/价值观 | 问题数 | 🔴/🟡/✅ |
| 结构性问题 | 问题数 | 🔴/🟡/✅ |
**整体判定:**
- 🔴 不合格需大修≥3 个维度为 🔴
- 🟡 需修改12 个维度为 🔴
- ✅ 合格:无 🔴 维度
## 执行流程
```
用户发送 "L2审校 <飞书链接>"
├── 1. 读取 L2 剧本教研规范
`business_knowledge/L2/L2 剧本教研规范 .md`
├── 2. 解析飞书链接 → 获取文档
│ 使用 `lark_wiki_operate_as_bot` 技能
├── 3. 读取文档完整内容
│ 使用 `lark-cli docs +fetch --as bot`
├── 4. 按 9 维度逐项审校
│ ├── 一、台词字数
│ ├── 二、互动量
│ ├── 三、知识点覆盖
│ ├── 四、核心互动
│ ├── 五、拼写/语法
│ ├── 六、词汇难度
│ ├── 七、标点符号与价值观
│ ├── 八、结构性问题
│ └── 九、总结评分
├── 5. 输出结构化审校报告
└── 6. 等待人工确认后再执行修改
```
## 输出格式
```
📝 L2 剧本审校报告 — <文档标题>
### 一、台词字数
| 指标 | 规范值 | 实际 | 判定 |
...
### 二、互动量
...
### 三、知识点覆盖
...
### 四、核心互动
...
### 五、拼写/语法
| # | 类型 | 原文 | 位置 | 修正 |
...
### 六、词汇难度
| # | 原文 | CEFR | 位置 | 建议替换 | 严重度 |
...
### 七、标点符号与价值观
| # | 类型 | 原文 | 位置 | 建议 | 严重度 |
...
### 八、结构性问题
| # | 问题 | 位置 | 建议 |
...
### 九、总结评分
| 维度 | 状态 | 等级 |
...
**整体判定:** <结论>
```
## 修改执行
审校报告经人工确认后,使用 `lark_docx_update_as_bot` 技能逐条执行修改:
```bash
LARKSUITE_CLI_CONFIG_DIR=/root/.openclaw/credentials/xiaoyan \
lark-cli docs +update --api-version v2 --doc "<doc_token>" --as bot \
--command str_replace \
--pattern "原文" \
--content "修正后文本"
```
**修改原则:**
1. 每次修改后回读验证
2. 优先修改 🔴 必改项
3. 🟡 建议改项需人工逐条确认
4. 修改完成后输出修改清单
## 依赖
- `lark_wiki_operate_as_bot`:读取飞书知识库文档
- `lark_docx_update_as_bot`:修改文档正文
- `business_knowledge/L2/L2 剧本教研规范 .md`:审校规范依据
- `business_knowledge/L2_word_list.json`L2 词库
- `business_knowledge/L2_pattern_list.json`L2 句型库
## 权限要求
Bot 应用需对目标文档有编辑权限。

View File

@ -79,17 +79,17 @@ description: 用英语母语儿童思维生成自然地道的分级英文台词
3. 如果目录下有多个 docx 文档 → 列出标题和 obj_token让用户指定目标
4. 禁止在有多份文档时自行猜测选择
### 1. 写入前必须验证行号连续性
### 1. 写入前必须执行行号连续性强制校验(不可跳过)
**问题:** 飞书 Sheets API 的 range 写入按连续区域覆盖,不关心行号是否对应。剧本表格中 E 列有内容的行号通常不连续(中间有剧情描述空行),连续 range 写入会导致全列偏移。
**规则:**
- 写入前统计目标行号列表,检查连续性
- 行号连续 → 可用 range 批量写入
- 行号不连续 → 必须逐行写入(每个行号单独 range
- 写入后抽样验证 E/F 列行号对齐前5行+中间5行+末尾5行
**规则(已固化为代码级强制校验)**
- 写入前必须调用 `feishu-embedded-sheet` 技能中的 **步骤 4.0 `validate_and_write()`** 函数
- 该函数自动检测行号连续性:连续 → 批量写入,不连续 → 逐行写入
- 写入后自动执行 **步骤 4.3 E-F 对齐验证**,发现错位立即报错
- 禁止跳过步骤 4.0 直接构造连续 range 写入
详见 `feishu-embedded-sheet` 技能中的「批量写入前必须验证行号连续性」章节。
详见 `feishu-embedded-sheet` 技能中的「步骤 4.0:行号连续性强制校验」章节。
### 2. 完成后必须输出结果统计与变更明细(强制执行)

View File

@ -138,3 +138,11 @@
{"type":"memory.recall.recorded","timestamp":"2026-06-18T04:36:16.755Z","query":"L1 每单元 lesson 5课 L1-L5 结构","resultCount":5,"results":[{"path":"memory/2026-05-26.md","startLine":130,"endLine":166,"score":1},{"path":"memory/2026-05-28.md","startLine":46,"endLine":62,"score":1},{"path":"memory/2026-05-25.md","startLine":283,"endLine":302,"score":1},{"path":"memory/2026-05-29.md","startLine":1,"endLine":14,"score":1},{"path":"memory/2026-05-25.md","startLine":118,"endLine":137,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-18T08:09:19.223Z","query":"L1 句子 结构 第三级 知识维度 句型结构 看图组句 句子补全","resultCount":2,"results":[{"path":"memory/2026-05-26.md","startLine":130,"endLine":166,"score":1},{"path":"memory/2026-05-28.md","startLine":46,"endLine":62,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-22T02:56:35.364Z","query":"L1 知识巩固 题型 词义 单词题","resultCount":5,"results":[{"path":"memory/2026-05-26.md","startLine":130,"endLine":166,"score":1},{"path":"memory/2026-05-28.md","startLine":46,"endLine":62,"score":1},{"path":"memory/2026-05-25.md","startLine":283,"endLine":302,"score":1},{"path":"memory/2026-05-29.md","startLine":1,"endLine":14,"score":1},{"path":"memory/2026-05-25.md","startLine":118,"endLine":137,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-23T02:16:16.280Z","query":"巩固题 单词朗读 无音频 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-23T02:16:32.659Z","query":"巩固题 单词朗读 题型 口语 L1 无音频","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-23T02:16:32.659Z","query":"单词知识巩固 word consolidation 朗读 read aloud","resultCount":4,"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}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-23T02:16:57.690Z","query":"巩固题 单词朗读 read aloud 口语 题型 L1 阶段","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-23T02:16:57.691Z","query":"word consolidation 巩固 题型列表 朗读 发音","resultCount":1,"results":[{"path":"memory/2026-05-07.md","startLine":29,"endLine":48,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-23T02:17:17.970Z","query":"巩固题 题型 单词 口语 朗读 read aloud 无音频 发音评测","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-23T02:51:54.388Z","query":"L2剧本教研规范","resultCount":1,"results":[{"path":"memory/2026-04-30.md","startLine":1,"endLine":27,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-06-23T02:51:54.388Z","query":"L2剧本 教研规范 knowledge","resultCount":1,"results":[{"path":"memory/2026-04-30.md","startLine":1,"endLine":27,"score":1}]}

View File

@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-06-22T02:56:35.364Z",
"updatedAt": "2026-06-23T02:51:54.388Z",
"entries": {
"memory:memory/2026-05-07.md:57:74": {
"key": "memory:memory/2026-05-07.md:57:74",
@ -1010,18 +1010,21 @@
"endLine": 27,
"source": "memory",
"snippet": "# 2026-04-30 工作日志 ## 李应瑛 — L1-S2-U15-L5 剧本组件配置生产 ### 文档信息 - **剧本**: 另一个BoBo 副本 - **Wiki**: https://makee-interactive.feishu.cn/wiki/HSSRwUqxSi8p51k6FtIcA7UAn8y - **Spreadsheet Token**: SD3Fs2zv8hM5evtHA5Nc7UjQncd - **Sheet ID**: wMQVyV - **Sheet 列结构**(注意:中间新增了\"配置信息\"列C组件配置列从F移至G: - A: 类型, B: ID, C: 配置信息, D: 剧情描述, E: 角色名, F: 英文台词, G: 组件配置, H: 知识点, I: 编剧台词 ### 已完成组件配置 #### 1. 对话朗读 (6条) — 规则: dialogue-reading-config | ID | 类型 | 任务标题 | 互动内容 | |---|------|---------|---------| | 1 | 对话朗读-配图 | 给May看照片 | I have a photo. | | 2 | 对话朗读-配图 | 指认照片中的房子 | Look at this one. | | 3 | 对话朗读-配图 | 告诉May这是我的房子 | This house is mine. | | 4 | 对话朗读 | 赞叹May有一个农场 | You have a farm! | | 7 | 对话朗读 | 夸May刷漆刷得好 | You",
"recallCount": 1,
"recallCount": 3,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"totalScore": 3,
"maxScore": 1,
"firstRecalledAt": "2026-05-17T05:52:07.611Z",
"lastRecalledAt": "2026-05-17T05:52:07.611Z",
"lastRecalledAt": "2026-06-23T02:51:54.388Z",
"queryHashes": [
"65b539c56ec3"
"65b539c56ec3",
"77cfbe459e1d",
"63eeac46a47c"
],
"recallDays": [
"2026-05-17"
"2026-05-17",
"2026-06-23"
],
"conceptTags": [
"l1-s2-u15-l5",
@ -2752,13 +2755,13 @@
"endLine": 62,
"source": "memory",
"snippet": "- B级规则听力句子 5-8 词(均 7 词),每组 3 张同类物品不同属性图片 - 能力标签:显性事实理解|关键词识别 ×2 + 基础语境理解|场景/物品/动作识别 ## 梁辰user_id: ou_28f02dcada1193913cfbb6310f8daf07— HTML 诊断页面教研规则 JS 文件 - 用户有一个 L1 关卡诊断工作台 HTML 页面(部署在腾讯云空间),当前只做数量级统计,缺少教研规则校验 - 需求:提供可嵌入 HTML 的前端 JS 校验规则文件,使诊断有据可依 - 输出:`output/l1_pedagogy_rules.js`42KB包含 8 个校验维度: 1. 词汇超纲检测(基于 L1/L2 词库) 2. 英式拼写检测color→colour 等 115 组映射) 3. Markdown 标记检测(`**`/`#`/`>` 等) 4. 标点规范检测(全角混入、``、`!!!` 5. 题型-阶段匹配校验20 种题型对应的 L1/L2 阶段映射) 6. 台词质量分析(句子长度、负面评价) 7. 知识点曝光度 8. 句型合规检测 - 接入方式:`<script src=\"l1_pedagogy_rules.js\"></script>` + 调用 `PedagogyRules.validate(summary, level)` - 数据源L1 词库 147 词(过滤 enabled=true、L2 词库 52 词、L1 句型 8 个、L2 句型(从 437MB bitable 导出中提取唯一结",
"recallCount": 16,
"recallCount": 17,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 16,
"totalScore": 17,
"maxScore": 1,
"firstRecalledAt": "2026-05-29T00:23:17.391Z",
"lastRecalledAt": "2026-06-22T02:56:35.364Z",
"lastRecalledAt": "2026-06-23T02:16:32.659Z",
"queryHashes": [
"c1d4076205e1",
"4aa0ef719160",
@ -2775,7 +2778,8 @@
"533765637e65",
"367cd3303e1d",
"fc0ce35c5668",
"fecfc20c077f"
"fecfc20c077f",
"8d35216cc8bc"
],
"recallDays": [
"2026-05-29",
@ -2784,7 +2788,8 @@
"2026-06-10",
"2026-06-17",
"2026-06-18",
"2026-06-22"
"2026-06-22",
"2026-06-23"
],
"conceptTags": [
"5-8",
@ -2993,13 +2998,13 @@
"endLine": 66,
"source": "memory",
"snippet": "8. 句型合规检测 - 接入方式:`<script src=\"l1_pedagogy_rules.js\"></script>` + 调用 `PedagogyRules.validate(summary, level)` - 数据源L1 词库 147 词(过滤 enabled=true、L2 词库 52 词、L1 句型 8 个、L2 句型(从 437MB bitable 导出中提取唯一结构) - 技术注意L2_pattern_list.json 体积 437MB1,082,450 条记录),直接嵌入前端不可行,已提取唯一句型结构后嵌入 - 所有 6 个测试用例通过 - 用户后续想尝试其他对接方式API 模式 / 飞书 Bot 联动 / CI 集成)",
"recallCount": 10,
"recallCount": 11,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 10,
"totalScore": 11,
"maxScore": 1,
"firstRecalledAt": "2026-05-29T02:33:30.526Z",
"lastRecalledAt": "2026-06-18T04:36:05.631Z",
"lastRecalledAt": "2026-06-23T02:16:32.659Z",
"queryHashes": [
"4aa0ef719160",
"e30c130b9d1d",
@ -3010,7 +3015,8 @@
"867f7264cd0e",
"5f16a7426a41",
"7c761b49b948",
"533765637e65"
"533765637e65",
"8d35216cc8bc"
],
"recallDays": [
"2026-05-29",
@ -3018,7 +3024,8 @@
"2026-06-02",
"2026-06-10",
"2026-06-17",
"2026-06-18"
"2026-06-18",
"2026-06-23"
],
"conceptTags": [
"l1-pedagogy-rules.js",
@ -3038,13 +3045,13 @@
"endLine": 14,
"source": "memory",
"snippet": "## [梁晨] L1 Pedagogy Rules v4.0.0 交付 - 在 v3 基础上新增 5 个校验维度(来自 16 个教研 skill 规则提取): 6. 组件合规性12 种题型规则,来自 11 个 dialogue/info config skills 7. 字段完整性7 项自动检查,来自 audit_l1_config 8. 掌握度预测W_i×M_i 加权公式,来自 knowledge-mastery-calculator 9. 格式规范(标题/Markdown/标点/英式拼写/价值观,来自全量文本规范) 10. 语法检测(三单/主谓一致/双重否定,来自 audit_l1_config - 输出:`output/l1_pedagogy_rules_v4.js`59KB - 数据L1 147 词、L2 52 词、L1 8 句型、L2 1 句型、89 组英式拼写映射、12 种题型规则 - 已通过飞书 Bot 发送给梁晨 - API 兼容 v3不加 options 参数即为原行为;加 component/componentExposures 启用新维度 - 能力边界:内容质量诊断(第一层筛选),不是全链路归因(看不到 UX/服务端/学生侧因素)",
"recallCount": 11,
"recallCount": 12,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 11,
"totalScore": 12,
"maxScore": 1,
"firstRecalledAt": "2026-06-02T01:49:17.841Z",
"lastRecalledAt": "2026-06-22T02:56:35.364Z",
"lastRecalledAt": "2026-06-23T02:16:32.659Z",
"queryHashes": [
"7e6aa000abdb",
"094bfb865b29",
@ -3056,14 +3063,16 @@
"341edb569d53",
"533765637e65",
"367cd3303e1d",
"fecfc20c077f"
"fecfc20c077f",
"8d35216cc8bc"
],
"recallDays": [
"2026-06-02",
"2026-06-10",
"2026-06-17",
"2026-06-18",
"2026-06-22"
"2026-06-22",
"2026-06-23"
],
"conceptTags": [
"v4.0.0",
@ -3083,13 +3092,13 @@
"endLine": 49,
"source": "memory",
"snippet": "- 答案设为[2](英语的),与申报 adj. 英文的不符,应为[0] - 🟡 rubber (1218116):释义题例句\"an eraser made of rubber\"解释的是橡胶材料,非橡皮擦工具 - 🟡 Chinese(n.) (1218113):例句\"Chinese food\"中 Chinese 是形容词非名词,与题目义项\"中文\"不符 - 🟢 fun (1218119):解析末尾多余一个 `\"` - 🟢 eraser (1218111)`\"an eraser表示\"` 缺少空格 ## 武钰涵 — 听力-P1-图片选择题生产 - 题目集合 ID: 121601难度 B级L1 第3-4阶段Starters 3 - 知识点验证10个词中仅 run(v.) 在 L1 词库feetfoot 的复数)可视为已有,其余 8 个bus, taxi, trousers, shorts, winter, snow, cold, bee不在 L1/L2 词库 - 用户指示直接生产,已按 SKILL 规范生成 2 个题组各5题含听力文本、图片描述、正确答案、能力标签 - 已按 bitable 配置格式输出完整 jsonData + 题目完整配置 + 图片描述 - B级规则听力句子 5-8 词(均 7 词),每组 3 张同类物品不同属性图片 - 能力标签:显性事实理解|关键词识别 ×2 + 基础语境理解|场景/物品/动作识别 ## 梁辰user_id: ou_28f02dcada1193913cfbb6310f8daf07— HTML 诊断页面",
"recallCount": 7,
"recallCount": 8,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 7,
"totalScore": 8,
"maxScore": 1,
"firstRecalledAt": "2026-06-02T01:49:17.841Z",
"lastRecalledAt": "2026-06-18T04:36:05.631Z",
"lastRecalledAt": "2026-06-23T02:16:32.659Z",
"queryHashes": [
"7e6aa000abdb",
"094bfb865b29",
@ -3097,13 +3106,15 @@
"867f7264cd0e",
"5f16a7426a41",
"7c761b49b948",
"533765637e65"
"533765637e65",
"8d35216cc8bc"
],
"recallDays": [
"2026-06-02",
"2026-06-10",
"2026-06-17",
"2026-06-18"
"2026-06-18",
"2026-06-23"
],
"conceptTags": [
"听力-p1-图片选择题生产",
@ -3154,22 +3165,27 @@
"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": 4,
"recallCount": 8,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 4,
"totalScore": 8,
"maxScore": 1,
"firstRecalledAt": "2026-06-04T03:23:22.968Z",
"lastRecalledAt": "2026-06-17T02:46:25.801Z",
"lastRecalledAt": "2026-06-23T02:17:17.970Z",
"queryHashes": [
"defa0daee9d7",
"92343053bee2",
"302eafc578a7",
"666912b77eb8"
"666912b77eb8",
"8b8c93a101f0",
"a051b817dda7",
"0ae754ed0ea5",
"ec8098bc9476"
],
"recallDays": [
"2026-06-04",
"2026-06-17"
"2026-06-17",
"2026-06-23"
],
"conceptTags": [
"10/10",
@ -3189,22 +3205,27 @@
"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": 4,
"recallCount": 8,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 4,
"totalScore": 8,
"maxScore": 1,
"firstRecalledAt": "2026-06-04T03:23:22.968Z",
"lastRecalledAt": "2026-06-17T02:46:25.801Z",
"lastRecalledAt": "2026-06-23T02:17:17.970Z",
"queryHashes": [
"defa0daee9d7",
"92343053bee2",
"302eafc578a7",
"666912b77eb8"
"666912b77eb8",
"8b8c93a101f0",
"a051b817dda7",
"0ae754ed0ea5",
"ec8098bc9476"
],
"recallDays": [
"2026-06-04",
"2026-06-17"
"2026-06-17",
"2026-06-23"
],
"conceptTags": [
"json.dumps",
@ -4077,6 +4098,37 @@
"dialogue-fill-in-blanks-config",
"对话挖空-配图"
]
},
"memory:memory/2026-05-07.md:29:48": {
"key": "memory:memory/2026-05-07.md:29:48",
"path": "memory/2026-05-07.md",
"startLine": 29,
"endLine": 48,
"source": "memory",
"snippet": "- **时间:** 10:39 ~ 10:49 - **需求:** 对比两份单词表的区别,找出\"表1有但表2没有\"的单词逐个列出。 - **状态:** 已完成对比,结果已交付用户。 - **文件:** - 表1.doc 文件分类主题单词表14个大类~970个单词 - 表2.xlsx 文件LV2词表~1326个单词 - **对比结果:** - 两表共有459个单词 - 表1有但表2没有502个单词基础生活词汇为主L1级别高频词 - 表2有但表1没有867个单词LV2进阶/学术词汇为主) - **输出文件:** `/root/.openclaw/workspace-xiaoyan/output/word_comparison_result.xlsx` - **关键发现:** 表1包含大量L1级别基础生活词汇家庭成员、日期、颜色、食物等表2偏重LV2级别学术词汇两者覆盖范围差异大。表1的502个独有词主要是小学阶段高频基础词。 - **备注:** 李应瑛不在负责人配置中,不在完整权限用户列表中。当前请求属于教研内容分析,已先接单处理,后续如有敏感数据查询需求需按权限规则处理。 # 2026-05-07 工作日志 ## 会话记录 ### 李应瑛 (ou_1bd7317ae2ccfeb57e1132028847279e) — 单词表对比请求(三轮对比) - **时间:** 10:39 ~ 11:18 - **需求:** 对比三份单词表,找出\"表1有但表2和表3都没有\"的单词。",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-06-23T02:16:57.691Z",
"lastRecalledAt": "2026-06-23T02:16:57.691Z",
"queryHashes": [
"01b80ce79584"
],
"recallDays": [
"2026-06-23"
],
"conceptTags": [
"lv2进阶/学术词汇为主",
"时间",
"需求",
"对比",
"单词",
"区别",
"找出",
"没有"
]
}
}
}

87
memory/2026-06-23-1639.md Normal file
View File

@ -0,0 +1,87 @@
# Session: 2026-06-23 16:39:31 GMT+8
- **Session Key**: agent:xiaoyan:feishu:direct:ou_d17da6d91277b56710bf8dc45846fb7b
- **Session ID**: 24ef878c-6940-424e-8bd6-3919414fac79
- **Source**: feishu
## Conversation Summary
assistant: "他们是为了获奖的" 可以翻译为:
**They're in it for the prize.**
或者更直译一些:
**They are doing it to win the prize.**
如果结合你之前的语境(那些东西是用来兑奖/获奖的),更自然的说法是:
**They're trying to win the prize.**
user: [message_id: om_x100b6c94379954a4b1128b65a7e462e]
张幸莹: He ate some fruit.His face goes red from kiwi or pineapple.合并成一句话
assistant: 合并后:
**He ate some fruit, and his face went red from the kiwi or pineapple.**
(注意:原句 "goes" 改为过去式 "went",与前面的 "ate" 时态保持一致。)
如果想强调"过敏反应"的转折感,也可以用:
**He ate some fruit, but his face went red from the kiwi or pineapple.**
user: [message_id: om_x100b6c97a746bcacb207929207e245f]
张幸莹: https://makee-interactive.feishu.cn/wiki/GvXTwO7V5iulQtkDOqPcM8o4nBe?from=from_copylink组件生成
assistant: 开始执行组件生产流程。先读取文档。
assistant: 当前Bot无访问该知识空间权限。这个链接属于 `makee-interactive` 租户的知识库而Bot应用App ID: `cli_a931175d41799cc7`)注册在 Vala 租户下,无法跨租户访问。
需要确认:这个文档是否在 Vala 租户下有副本?或者能否将文档迁移到 Vala 租户下 Bot 可访问的知识空间(如 AI_member、Content Universe 等)?
user: [message_id: om_x100b6c97b4da28acb4c8abb79261422]
张幸莹: 怎么样能获得权限呢
assistant: 跨租户访问的限制,有几种解决方式:
**方案一(推荐):把文档迁移到 Vala 租户**
将文档复制/移动到 Vala 租户下 Bot 可访问的知识空间。目前 Bot 可访问的空间包括:
- **AI_member** — 技术负责人群组空间
- **Content Universe** — 内容宇宙
- **VALA Academy** — 教研学院
你可以在飞书里把文档复制到以上任一空间,然后给我新链接即可。
**方案二:在 makee-interactive 租户安装 Bot 应用**
需要在 makee-interactive 飞书后台安装小研 BotApp ID: `cli_a931175d41799cc7`),但涉及跨租户部署,流程较复杂,建议联系技术负责人李若松处理。
**方案三:手动搬运**
你把文档内容粘贴到 Vala 租户下的新知识库文档中,我来继续处理。
建议先用方案一,最快。你把文档复制到 AI_member 或 Content Universe 空间后把新链接发我就行。
user: [message_id: om_x100b6c907b2284b0b1431824c2a3fd9]
张幸莹: https://makee-interactive.feishu.cn/wiki/GvXTwO7V5iulQtkDOqPcM8o4nBe?from=from_copylink组件生成
assistant: 还是同一个链接,情况没变——这个文档在 `makee-interactive` 租户下,小研 Bot 注册在 Vala 租户,跨租户无法访问。
最快的办法:在飞书里把这个文档**复制到 Vala 租户下的知识空间**(比如 **AI_member****Content Universe**),然后把新链接发给我,我立刻开始处理。
user: [message_id: om_x100b6c907641d8bcb1ff0d133814f5a]
张幸莹: https://makee-interactive.feishu.cn/wiki/GvXTwO7V5iulQtkDOqPcM8o4nBe?from=from_copylink组件生成
assistant: 链接还是同一个,问题没变——这个文档在 `makee-interactive` 租户,小研 Bot 跨不了租户。
**操作步骤:**
1. 在飞书打开这个文档
2. 右上角 `...`**复制到** → 选择 **AI_member****Content Universe** 空间
3. 把复制后的新链接发给我
就这三步,新链接到手我立刻开工。
user: [message_id: om_x100b6c900bec68b4b48c5232fccce09]
张幸莹: https://makee-interactive.feishu.cn/wiki/GvXTwO7V5iulQtkDOqPcM8o4nBe?from=from_copylink 组件生产
assistant: 我再试一种不同的方式访问。
assistant: 试了两种 API 路径wiki + docx都返回 `not found`。结论不变Bot 无法跨租户访问 `makee-interactive` 的文档。
**没有别的办法,只能复制文档。** 在飞书里:
1. 打开这个文档
2. 右上角 `...`**复制到** → 选 **AI_member** 空间
3. 把新链接发我
三秒的事,比反复发同一个链接快 😄

View File

@ -0,0 +1,447 @@
#!/usr/bin/env python3
"""
知识点覆盖率分析 + 回填脚本
用法: python3 knowledge_coverage_analyzer.py <飞书文档ID或URL>
功能:
1. 解析剧本主表区分 TL 输入与组件行输出
2. 读取知识点现状表格中的单词和句型
3. 统计每个知识点在输入/输出中的出现次数
4. 回填到知识点现状表格
5. 输出用法质量检查报告
依赖: lark-cli用于读取文档 markdownPython3用于 API 回填
"""
import json
import re
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
# ── 配置 ──────────────────────────────────────────────
CONFIG_PATH = Path("/root/.openclaw/credentials/xiaoyan/config.json")
LARK_CLI_ENV = {
"LARKSUITE_CLI_CONFIG_DIR": "/root/.openclaw/credentials/xiaoyan",
"PATH": "/root/.nvm/versions/node/v24.14.0/bin:" + "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
}
# ── 工具函数 ──────────────────────────────────────────
def parse_doc_id(url_or_id):
"""从 URL 或直接 ID 中提取文档 ID"""
m = re.search(r"docx/([A-Za-z0-9]+)", url_or_id)
if m:
return m.group(1)
return url_or_id.strip()
def fetch_markdown(doc_id):
"""用 lark-cli 获取文档 markdown"""
result = subprocess.run(
["/root/.nvm/versions/node/v24.14.0/bin/lark-cli", "docs", "+fetch", "--doc", doc_id, "--as", "bot"],
capture_output=True, text=True, env=LARK_CLI_ENV, timeout=30,
)
data = json.loads(result.stdout)
return data["data"]["markdown"]
def get_token():
"""获取 tenant_access_token"""
with open(CONFIG_PATH) as f:
cfg = json.load(f)
app = cfg["apps"][0]
body = json.dumps({"app_id": app["appId"], "app_secret": app["appSecret"]}).encode()
req = urllib.request.Request(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
data=body,
headers={"Content-Type": "application/json"},
)
resp = json.loads(urllib.request.urlopen(req).read())
return resp["tenant_access_token"]
def api_get(token, path):
"""GET 请求飞书 API"""
req = urllib.request.Request(
f"https://open.feishu.cn/open-apis{path}",
headers={"Authorization": f"Bearer {token}"},
)
return json.loads(urllib.request.urlopen(req).read())
def api_patch(token, path, body):
"""PATCH 请求飞书 API"""
data = json.dumps(body).encode()
req = urllib.request.Request(
f"https://open.feishu.cn/open-apis{path}",
data=data,
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
method="PATCH",
)
return json.loads(urllib.request.urlopen(req).read())
def api_get_all_children(token, doc_id, block_id):
"""分页获取 block 的所有子块"""
all_items = []
page_token = None
while True:
path = f"/docx/v1/documents/{doc_id}/blocks/{block_id}/children?page_size=50"
if page_token:
path += f"&page_token={page_token}"
resp = api_get(token, path)
items = resp.get("data", {}).get("items", [])
all_items.extend(items)
if not resp.get("data", {}).get("has_more"):
break
page_token = resp["data"]["page_token"]
return all_items
# ── Markdown 解析 ─────────────────────────────────────
def parse_markdown_table(md, heading_text):
"""找到标题下方的 lark-table返回二维数组 [row][col]"""
idx = md.find(heading_text)
if idx == -1:
return None
table_match = re.search(r"<lark-table.*?</lark-table>", md[idx:], re.DOTALL)
if not table_match:
return None
table_md = table_match.group(0)
rows = re.findall(r"<lark-tr>(.*?)</lark-tr>", table_md, re.DOTALL)
result = []
for row in rows:
cells = re.findall(r"<lark-td[^>]*>(.*?)</lark-td>", row, re.DOTALL)
clean_cells = []
for c in cells:
# 去掉所有 HTML 标签,提取纯文本
text = re.sub(r"<[^>]+>|\*\*|\{[^}]*\}", "", c).strip()
clean_cells.append(text)
result.append(clean_cells)
return result
def find_table_by_first_cell_md(md, first_cell_text):
"""在 markdown 中找到第一列第一行为指定文本的表格"""
tables = re.findall(r"<lark-table.*?</lark-table>", md, re.DOTALL)
for table_md in tables:
rows = re.findall(r"<lark-tr>(.*?)</lark-tr>", table_md, re.DOTALL)
if not rows:
continue
cells = re.findall(r"<lark-td[^>]*>(.*?)</lark-td>", rows[0], re.DOTALL)
if cells:
text = re.sub(r"<[^>]+>|\*\*|\{[^}]*\}", "", cells[0]).strip()
if text == first_cell_text:
return parse_markdown_table_from_string(table_md)
return None
def parse_markdown_table_from_string(table_md):
"""从 lark-table markdown 字符串解析为二维数组"""
rows = re.findall(r"<lark-tr>(.*?)</lark-tr>", table_md, re.DOTALL)
result = []
for row in rows:
cells = re.findall(r"<lark-td[^>]*>(.*?)</lark-td>", row, re.DOTALL)
clean_cells = [re.sub(r"<[^>]+>|\*\*|\{[^}]*\}", "", c).strip() for c in cells]
result.append(clean_cells)
return result
# ── 统计逻辑 ──────────────────────────────────────────
def count_occurrences(word, texts):
"""统计单词在文本列表中的出现次数(不区分大小写,整词匹配)"""
if not word:
return 0
count = 0
pattern = re.compile(r"\b" + re.escape(word) + r"\b", re.IGNORECASE)
for t in texts:
count += len(pattern.findall(t))
return count
def count_pattern_occurrences(pattern_text, texts):
"""统计句型在文本列表中的出现次数(子串匹配,不区分大小写)"""
if not pattern_text:
return 0
key = pattern_text.strip().rstrip(".").rstrip("!").rstrip("?")
key_lower = key.lower()
count = 0
for t in texts:
count += t.lower().count(key_lower)
return count
# ── 回填逻辑(使用 Block API─────────────────────────
def find_table_block_id(token, doc_id, heading_text):
"""找到标题下方的第一个表格 block_id"""
blocks = api_get(token, f"/docx/v1/documents/{doc_id}/blocks?page_size=500")
items = blocks.get("data", {}).get("items", [])
heading_index = -1
for i, b in enumerate(items):
text = ""
for e in b.get("text", {}).get("elements", []):
text += e.get("text_run", {}).get("content", "")
if text == heading_text:
heading_index = i
break
if heading_index == -1:
return None
for b in items[heading_index + 1:]:
if b.get("block_type") == 31:
return b["block_id"]
return None
def get_cell_block_ids(token, doc_id, table_block_id):
"""
获取表格中所有单元格的 block_id返回二维数组 [row][col]
飞书表格结构table > row > cell每个 row 只有一个 cell
需要按列数分组
"""
rows = api_get_all_children(token, doc_id, table_block_id)
all_cells = []
for row in rows:
try:
cell_items = api_get_all_children(token, doc_id, row["block_id"])
except Exception:
continue
if cell_items:
all_cells.append(cell_items[0]["block_id"])
return all_cells
def fill_cells(token, doc_id, cell_ids, col_count, knowledge_items, tl_rows, comp_rows):
"""回填统计结果到表格"""
ok = 0
fail = 0
for item_idx, item in enumerate(knowledge_items):
word = item.get("word", "")
pattern = item.get("pattern", "")
# 计算该行在 cell_ids 中的起始位置
# 跳过表头行
row_start = (item_idx + 1) * col_count
if word:
word_only = word.split()[0] if word else ""
w_in = count_occurrences(word_only, tl_rows)
w_out = count_occurrences(word_only, comp_rows)
# col 1 = 输入(单词)
if row_start + 1 < len(cell_ids):
ok += patch_cell(token, doc_id, cell_ids[row_start + 1], str(w_in))
time.sleep(0.3)
else:
fail += 1
# col 2 = 输出(单词)
if row_start + 2 < len(cell_ids):
ok += patch_cell(token, doc_id, cell_ids[row_start + 2], str(w_out))
time.sleep(0.3)
else:
fail += 1
if pattern:
p_in = count_pattern_occurrences(pattern, tl_rows)
p_out = count_pattern_occurrences(pattern, comp_rows)
# col 4 = 输入(句型)
if row_start + 4 < len(cell_ids):
ok += patch_cell(token, doc_id, cell_ids[row_start + 4], str(p_in))
time.sleep(0.3)
else:
fail += 1
# col 5 = 输出(句型)
if row_start + 5 < len(cell_ids):
ok += patch_cell(token, doc_id, cell_ids[row_start + 5], str(p_out))
time.sleep(0.3)
else:
fail += 1
print(f" 回填完成: {ok} 成功, {fail} 失败")
return ok, fail
def patch_cell(token, doc_id, block_id, content):
"""更新单个文本块的内容"""
body = {
"update_text_elements": {
"elements": [{"text_run": {"content": content}}]
}
}
try:
resp = api_patch(token, f"/docx/v1/documents/{doc_id}/blocks/{block_id}", body)
return resp.get("code") == 0
except Exception as e:
print(f" ❌ 回填失败 {block_id}: {e}")
return False
# ── 报告 ──────────────────────────────────────────────
def print_report(knowledge_items, tl_rows, comp_rows):
"""打印统计报告"""
print("\n" + "=" * 60)
print("📊 知识点覆盖率统计")
print("=" * 60)
print(f"\nTL 行(输入): {len(tl_rows)}")
print(f"组件行(输出): {len(comp_rows)}")
print(f"\n{'知识点':<30} {'输入':>4} {'输出':>4}")
print("-" * 42)
for item in knowledge_items:
word = item.get("word", "")
pattern = item.get("pattern", "")
if word:
word_only = word.split()[0]
w_in = count_occurrences(word_only, tl_rows)
w_out = count_occurrences(word_only, comp_rows)
print(f"{word:<30} {w_in:>4} {w_out:>4}")
if pattern:
p_in = count_pattern_occurrences(pattern, tl_rows)
p_out = count_pattern_occurrences(pattern, comp_rows)
label = pattern[:28] + ".." if len(pattern) > 30 else pattern
print(f" {label:<28} {p_in:>4} {p_out:>4}")
# 质量检查
print("\n" + "=" * 60)
print("🔍 用法质量快速检查")
print("=" * 60)
for item in knowledge_items:
word = item.get("word", "")
if not word:
continue
word_only = word.split()[0]
w_out = count_occurrences(word_only, comp_rows)
w_total = count_occurrences(word_only, tl_rows) + w_out
if w_total < 2:
print(f"\n⚠️ {word} — 仅出现 {w_total} 次,建议增加曝光")
elif w_total > 8:
print(f"\n💡 {word} — 出现 {w_total} 次,可适当精简")
# ── 主入口 ────────────────────────────────────────────
def main():
if len(sys.argv) < 2:
print("用法: python3 knowledge_coverage_analyzer.py <飞书文档ID或URL>")
sys.exit(1)
doc_id = parse_doc_id(sys.argv[1])
print(f"📄 文档 ID: {doc_id}")
# Step 1: 用 lark-cli 获取 markdown
print("📥 获取文档内容...")
md = fetch_markdown(doc_id)
# Step 2: 解析知识点现状表格
print("📋 解析「知识点现状」表格...")
knowledge_data = parse_markdown_table(md, "知识点现状")
if not knowledge_data:
print("❌ 未找到「知识点现状」表格")
sys.exit(1)
# 表头: 单词, 输入, 输出, 句型, 输入, 输出
knowledge_items = []
for row in knowledge_data[1:]: # 跳过表头
if len(row) < 6:
continue
word = row[0]
# 清理 markdown 格式:去掉列表符号
word = re.sub(r'^[-*+]\s+', '', word).strip()
pattern = row[3].strip()
if not word and not pattern:
continue
knowledge_items.append({"word": word, "pattern": pattern})
print(f" 知识点: {len(knowledge_items)}")
for item in knowledge_items:
w = item["word"] or "(无)"
p = item["pattern"] or "(无)"
print(f" - {w} | {p}")
# Step 3: 解析剧本主表
print("\n📋 解析剧本主表...")
script_data = find_table_by_first_cell_md(md, "类型")
if not script_data:
print("❌ 未找到剧本主表(第一列第一行为「类型」)")
sys.exit(1)
tl_rows = []
comp_rows = []
last_was_tl = True
for row in script_data[1:]: # 跳过表头
if not row:
continue
type_text = row[0].strip()
# 排除「知识点」列第6列index 5避免单词自计数
row_text = " ".join(row[1:5] + row[6:] if len(row) > 6 else row[1:5])
# 分类:类型列明确标记为 TL → 输入;
# 类型列包含交互关键词 → 输出;
# 空类型 → 继承上一行
if type_text.startswith("TL"):
tl_rows.append(row_text)
last_was_tl = True
elif not type_text:
if last_was_tl:
tl_rows.append(row_text)
else:
comp_rows.append(row_text)
elif any(kw in type_text for kw in ("对话朗读", "对话挖空", "剧情任务", "对话", "挖空", "互动")):
comp_rows.append(row_text)
last_was_tl = False
else:
# 其他(场景描述等)归为 TL
tl_rows.append(row_text)
last_was_tl = True
print(f" TL 行: {len(tl_rows)}, 组件行: {len(comp_rows)}")
# Step 4: 获取 token + 回填
print("\n🔑 获取 API Token...")
token = get_token()
print("📝 查找表格 block ID...")
knowledge_table_id = find_table_block_id(token, doc_id, "知识点现状")
if not knowledge_table_id:
print("❌ 未找到「知识点现状」表格 block")
sys.exit(1)
cell_ids = get_cell_block_ids(token, doc_id, knowledge_table_id)
col_count = 6 # 知识点现状表格固定6列
print(f" 表格 block: {knowledge_table_id}, 共 {len(cell_ids)} 个单元格")
print("\n📝 回填统计结果...")
fill_cells(token, doc_id, cell_ids, col_count, knowledge_items, tl_rows, comp_rows)
# Step 5: 打印报告
print_report(knowledge_items, tl_rows, comp_rows)
print("\n✅ 完成!")
if __name__ == "__main__":
main()