create-ccc-tutor 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/bin/cli.js +70 -9
- package/package.json +1 -1
- package/template/.claude/commands/exam.md +13 -0
- package/template/.claude/commands/slide.md +24 -5
- package/template/.claude-plugin/plugin.json +13 -26
- package/template/.codex/skills/exam/SKILL.md +13 -0
- package/template/.codex/skills/slide/SKILL.md +27 -3
- package/template/.harness/scripts/pdf-rag.sh +40 -0
- package/template/.harness/scripts/pdf_rag.py +485 -0
- package/template/.harness/scripts/requirements-pdf.txt +6 -0
- package/template/.harness/scripts/tests/test_pdf_rag.py +228 -0
- package/template/.harness/state/install.json +1 -1
- package/template/constitution.md +1 -1
- package/template/course/README.md +1 -1
- package/template/docs/features/pdf-vision-implementation.md +109 -0
- package/template/docs/features/pdf-vision.md +226 -0
- package/template/docs/features/slide-query-implementation.md +2 -2
- package/template/docs/features/slide-query.md +2 -0
- package/template/gitignore +4 -0
package/README.md
CHANGED
|
@@ -23,12 +23,25 @@ course/
|
|
|
23
23
|
|
|
24
24
|
`course/course_code(example)/` 是带两个示例课件的样板,复制改名即可开始。
|
|
25
25
|
|
|
26
|
+
## 更新已安装的环境
|
|
27
|
+
|
|
28
|
+
已经装过、想升级到新版(拿到新功能/修复,如 PDF 看图与检索引擎)时,在项目目录里跑:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
npx create-ccc-tutor@latest --update
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`--update` 会更新框架、技能与功能文件,但**保护你的用户数据**:`course/` 课件、`.harness/state/install.json` 配置、`constitution.md` 项目身份都不会被覆盖。
|
|
35
|
+
|
|
36
|
+
> 不加 `--update` 直接重装会跳过所有已存在文件(改过的技能不会更新,等于半更新);检测到老环境时安装器会提示你改用 `--update`。
|
|
37
|
+
|
|
26
38
|
## 选项
|
|
27
39
|
|
|
28
40
|
| 选项 | 作用 |
|
|
29
41
|
|---|---|
|
|
30
42
|
| `--dry-run` | 只预览要写哪些文件,不实际写入 |
|
|
31
|
-
| `--
|
|
43
|
+
| `--update` | 更新已装环境:覆盖框架/技能/功能,但保护 `course/`、`install.json`、`constitution.md` |
|
|
44
|
+
| `--force` | 全量覆盖所有已存在文件(含用户配置,慎用) |
|
|
32
45
|
|
|
33
46
|
## 要求
|
|
34
47
|
|
package/bin/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require("path");
|
|
|
6
6
|
const argv = process.argv.slice(2);
|
|
7
7
|
const dryRun = argv.includes("--dry-run");
|
|
8
8
|
const force = argv.includes("--force");
|
|
9
|
+
const update = argv.includes("--update");
|
|
9
10
|
|
|
10
11
|
const SRC = path.join(__dirname, "..", "template");
|
|
11
12
|
const DEST = process.cwd();
|
|
@@ -15,8 +16,28 @@ function destName(name) {
|
|
|
15
16
|
return name === "gitignore" ? ".gitignore" : name;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
// --update 模式下「保护」的用户数据/配置:即使已存在也绝不覆盖(除非 --force 全量重置)。
|
|
20
|
+
// 这样老环境能更新框架(技能/脚本/文档/引擎),又不会冲掉用户的课件和项目身份。
|
|
21
|
+
const PROTECTED_FILES = [
|
|
22
|
+
".harness/state/install.json", // 用户的科目/配置(/init 结果)
|
|
23
|
+
"constitution.md", // 项目身份(/init 填的槽位)
|
|
24
|
+
];
|
|
25
|
+
const PROTECTED_DIRS = [
|
|
26
|
+
"course", // 用户的课件与题目,整棵子树都不动
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function relPosix(p) {
|
|
30
|
+
return path.relative(DEST, p).split(path.sep).join("/");
|
|
31
|
+
}
|
|
32
|
+
function isProtected(absDest) {
|
|
33
|
+
const rel = relPosix(absDest);
|
|
34
|
+
if (PROTECTED_FILES.includes(rel)) return true;
|
|
35
|
+
return PROTECTED_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
let written = 0;
|
|
19
39
|
let skipped = 0;
|
|
40
|
+
let protectedCount = 0;
|
|
20
41
|
|
|
21
42
|
function copyDir(src, dest) {
|
|
22
43
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
@@ -26,21 +47,38 @@ function copyDir(src, dest) {
|
|
|
26
47
|
if (!dryRun) fs.mkdirSync(d, { recursive: true });
|
|
27
48
|
copyDir(s, d);
|
|
28
49
|
} else {
|
|
29
|
-
const rel =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
const rel = relPosix(d);
|
|
51
|
+
const exists = fs.existsSync(d);
|
|
52
|
+
if (exists && !force) {
|
|
53
|
+
if (update) {
|
|
54
|
+
// 更新模式:保护用户数据,其余框架文件覆盖
|
|
55
|
+
if (isProtected(d)) {
|
|
56
|
+
console.log(` 保护(不覆盖用户文件): ${rel}`);
|
|
57
|
+
protectedCount++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
console.log(` ${dryRun ? "[预览] " : ""}更新: ${rel}`);
|
|
61
|
+
} else {
|
|
62
|
+
// 默认模式(全新安装):已存在就跳过,避免误覆盖
|
|
63
|
+
console.log(` 跳过(已存在;更新用 --update,全量覆盖用 --force): ${rel}`);
|
|
64
|
+
skipped++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
console.log(` ${dryRun ? "[预览] " : ""}写入: ${rel}`);
|
|
34
69
|
}
|
|
35
|
-
console.log(` ${dryRun ? "[预览] " : ""}写入: ${rel}`);
|
|
36
70
|
if (!dryRun) fs.copyFileSync(s, d);
|
|
37
71
|
written++;
|
|
38
72
|
}
|
|
39
73
|
}
|
|
40
74
|
}
|
|
41
75
|
|
|
76
|
+
const hasExistingInstall = fs.existsSync(
|
|
77
|
+
path.join(DEST, ".harness", "state", "install.json")
|
|
78
|
+
);
|
|
79
|
+
const mode = force ? "全量覆盖 (--force)" : update ? "更新 (--update)" : "全新安装";
|
|
42
80
|
console.log(
|
|
43
|
-
`\nCCC-tutor
|
|
81
|
+
`\nCCC-tutor ${mode} → ${DEST}${dryRun ? " (--dry-run 预览,不写文件)" : ""}\n`
|
|
44
82
|
);
|
|
45
83
|
|
|
46
84
|
if (!fs.existsSync(SRC)) {
|
|
@@ -48,6 +86,16 @@ if (!fs.existsSync(SRC)) {
|
|
|
48
86
|
process.exit(1);
|
|
49
87
|
}
|
|
50
88
|
|
|
89
|
+
// 检测到老环境却没带 --update/--force:提示用户,避免「只新增、改过的技能被跳过」的半更新。
|
|
90
|
+
if (hasExistingInstall && !update && !force) {
|
|
91
|
+
console.log(
|
|
92
|
+
"⚠️ 检测到这里已经装过 CCC-tutor。直接安装会跳过所有已存在文件(改过的技能不会更新)。\n" +
|
|
93
|
+
" 想更新到新版请改用: npx create-ccc-tutor@latest --update\n" +
|
|
94
|
+
" (--update 会更新框架与功能,但保护你的 course/ 课件、install.json、constitution.md)\n" +
|
|
95
|
+
" 仍要按全新安装继续会跳过已存在文件。\n"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
51
99
|
copyDir(SRC, DEST);
|
|
52
100
|
|
|
53
101
|
// 给 shell 脚本加执行权限
|
|
@@ -62,8 +110,20 @@ if (!dryRun) {
|
|
|
62
110
|
if (fs.existsSync(codexInstall)) fs.chmodSync(codexInstall, 0o755);
|
|
63
111
|
}
|
|
64
112
|
|
|
65
|
-
|
|
66
|
-
|
|
113
|
+
const tail = [];
|
|
114
|
+
if (skipped) tail.push(`跳过 ${skipped} 个已存在文件(加 --update 更新 / --force 全量覆盖)`);
|
|
115
|
+
if (protectedCount) tail.push(`保护 ${protectedCount} 个用户文件未动`);
|
|
116
|
+
|
|
117
|
+
if (update) {
|
|
118
|
+
console.log(`
|
|
119
|
+
✓ 更新完成!更新 ${written} 个文件${tail.length ? "," + tail.join(",") : ""}。
|
|
120
|
+
|
|
121
|
+
已保护你的 course/ 课件、install.json 配置、constitution.md 项目身份。
|
|
122
|
+
框架、技能与功能(含 PDF 看图/检索引擎)已更新到本版。
|
|
123
|
+
`);
|
|
124
|
+
} else {
|
|
125
|
+
console.log(`
|
|
126
|
+
✓ 完成!写入 ${written} 个文件${tail.length ? "," + tail.join(",") : ""}。
|
|
67
127
|
|
|
68
128
|
接下来:
|
|
69
129
|
1. 把课件放进 course/<科目>/slide/ ,题目放进 course/<科目>/exam/
|
|
@@ -74,3 +134,4 @@ console.log(`
|
|
|
74
134
|
|
|
75
135
|
已预填 install.json,首次启动无需走 /init 配置。
|
|
76
136
|
`);
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -42,6 +42,19 @@ $ARGUMENTS
|
|
|
42
42
|
- 定位不到(题号超出、描述对不上)→ 告诉用户在该文件里没找到对应题目,并简要列出文件里实际有哪些题,请用户确认。
|
|
43
43
|
- **先把题目原文复述出来**给用户核对,再开始解答(确保找对了题)。
|
|
44
44
|
|
|
45
|
+
# 第二步半:用检索引擎找解题依据(课件)
|
|
46
|
+
|
|
47
|
+
解题依据从 slide 课件找时,用 `pdf-vision` 引擎检索定位,不要逐个翻 PDF:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
.harness/scripts/pdf-rag.sh query --subject <科目> -q "<这道题考的知识点/方法>" -k 5
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- 对命中页用 Read 读(Claude 原生看图);命中页 `visual_flag=true` 或题目涉及图/表时务必看图。出处用 `source_file_exact` + 页码。
|
|
54
|
+
- `miss=true` → 该步课件没有依据,按解题铁律 2 标注「课件没有直接依据」。
|
|
55
|
+
- `mode=keyword` / 引擎失败 → 告知「语义检索未启用」或回退直接 Read;**看图能力不可用而必须看图的步骤**,明说不可答、不要纯文字冒充。
|
|
56
|
+
- 看图取值:图上标注数字=精确;按刻度=估算(标「非精确」);看不清=只给相对大小;图未读出=标注并退回文字;文图冲突=两列、各标出处、不替课件裁决。
|
|
57
|
+
|
|
45
58
|
# 解题铁律(不可违反)
|
|
46
59
|
|
|
47
60
|
1. **先依据课件解。** 解题方法、公式、判定标准优先引用同科目 slide 课件,能引的步骤标出处 `(Lecture N, 第M页)`。
|
|
@@ -18,7 +18,7 @@ $ARGUMENTS
|
|
|
18
18
|
1. 用 Bash 列出有哪些科目:`ls -d course/*/ 2>/dev/null`。
|
|
19
19
|
2. 当前会话科目记在 `.harness/state/current-subject.txt`(可能不存在)。先读它。
|
|
20
20
|
3. 决定本次用哪个科目:
|
|
21
|
-
- **用户这次输入里点名了某个科目**(文字里出现某个科目文件夹名,如 `
|
|
21
|
+
- **用户这次输入里点名了某个科目**(文字里出现某个科目文件夹名,如 `ECON-10005`、`COMP-5990`,或明显对应的说法)→ 用它,并把它写进 `.harness/state/current-subject.txt`(覆盖)。
|
|
22
22
|
- **没点名、但 state 文件里有当前科目** → 沿用该科目,不必再问。
|
|
23
23
|
- **没点名、state 也没有**:
|
|
24
24
|
- 如果 `course/` 下**只有一个科目** → 直接用它,并写入 state,回答开头一句话说明"当前科目:X"。
|
|
@@ -51,10 +51,29 @@ $ARGUMENTS
|
|
|
51
51
|
|
|
52
52
|
# 第三步:找答案
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
本项目用 `pdf-vision` 引擎做「快速检索 + 看图」:**先检索定位,再读页**,不要再逐个翻所有 PDF。
|
|
55
|
+
|
|
56
|
+
1. 用 Bash 调检索(首次会建档;可能联网下载一次嵌入模型、稍等;之后很快):
|
|
57
|
+
```bash
|
|
58
|
+
.harness/scripts/pdf-rag.sh query --subject <科目> -q "<用户的问题>" -k 5
|
|
59
|
+
```
|
|
60
|
+
输出 JSON:`results` 是命中页 `[{source_file_exact, lecture, page, score, visual_flag, png_path, text_path}]`;`miss=true` 表示没命中;`mode` 为 `semantic`(语义)或 `keyword`(降级)。
|
|
61
|
+
2. 对每个命中页:用 Read 工具读该页(按 `source_file_exact` + `page`,可用 `pages` 参数)。**Claude 的 Read 对 PDF 是视觉渲染——你能直接看到该页的图、表、图表。** 命中页 `visual_flag=true` 或问题涉及图/表时,务必看图,不只看文字。
|
|
62
|
+
3. `miss=true` → 按第四步「情况 E」:先说课件里没有,再问用户要不要外部补充;**绝不**把低相关结果当权威答案。
|
|
63
|
+
4. 标出处用 `source_file_exact`(确切文件名)+ 页码。
|
|
64
|
+
|
|
65
|
+
**降级与诚实(铁律延伸):**
|
|
66
|
+
- `mode=keyword`(语义检索未启用,常因首次模型下载失败/离线)→ 文字类问题照答,但要告诉用户「语义检索未启用、本次用关键词查找」。
|
|
67
|
+
- `pdf-rag.sh` 整体失败(退出码非 0)→ 回退到直接用 Read 读相关 PDF(旧路径),照常标出处。
|
|
68
|
+
- **看图能力不可用、而问题必须看图才能答**(如只能从图读的数值)→ 明确说「这个需要看图、当前看图不可用」,**不要**用纯文字答案冒充答了图。
|
|
69
|
+
|
|
70
|
+
**看图取值规则(命中忠实性):**
|
|
71
|
+
- 图上直接标注的数字 → 照读,标「精确」。
|
|
72
|
+
- 只能按坐标轴/刻度估算 → 标「估算、非精确读数」。
|
|
73
|
+
- 看不清/被遮挡 → 标「看不清」,只给相对大小(更高/更低/约等)。
|
|
74
|
+
- 图未读出 → 标「该页图未读出」,退回只用文字。
|
|
75
|
+
- 文字层与图内容冲突 → 两个都列、各标出处、指出不一致,不替课件裁决。
|
|
76
|
+
- 每条视觉结论绑定到具体页(`Lecture N, 第M页`)。
|
|
58
77
|
|
|
59
78
|
# 第四步:组织回答
|
|
60
79
|
|
|
@@ -1,41 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "ccc-
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
2
|
+
"name": "ccc-tutor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "多科目 AI 复习助手(基于 CCC-MAGI harness):slide 依据课件查知识点、exam 解题目,严格依据课件、标出处、不编造。Multi-subject AI study assistant: slide answers strictly from course materials with citations, exam locates and solves problems from the exam folder.",
|
|
5
5
|
"author": {
|
|
6
|
-
"name": "
|
|
7
|
-
"email": "Haizhou0807@gmail.com"
|
|
6
|
+
"name": "Ericcccccc777"
|
|
8
7
|
},
|
|
9
|
-
"homepage": "https://github.com/Ericcccccc777/CCC-
|
|
8
|
+
"homepage": "https://github.com/Ericcccccc777/CCC-tutor",
|
|
10
9
|
"repository": {
|
|
11
10
|
"type": "git",
|
|
12
|
-
"url": "https://github.com/Ericcccccc777/CCC-
|
|
11
|
+
"url": "https://github.com/Ericcccccc777/CCC-tutor.git"
|
|
13
12
|
},
|
|
14
13
|
"license": "Apache-2.0",
|
|
15
14
|
"keywords": [
|
|
16
|
-
"
|
|
15
|
+
"ccc-tutor",
|
|
16
|
+
"ai-tutor",
|
|
17
17
|
"claude-code",
|
|
18
18
|
"codex",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"ccc-magi"
|
|
23
|
-
"constitution-driven",
|
|
24
|
-
"persistent-memory",
|
|
25
|
-
"magi-system",
|
|
26
|
-
"magi-verdict",
|
|
27
|
-
"magi-archivist",
|
|
28
|
-
"session-resume",
|
|
29
|
-
"workflow-checkpoint",
|
|
30
|
-
"decision-log",
|
|
31
|
-
"simple-onboarding",
|
|
32
|
-
"team-collaboration",
|
|
33
|
-
"ears-notation",
|
|
34
|
-
"i18n-aware",
|
|
35
|
-
"tier-1-tested-claude-codex"
|
|
19
|
+
"slide",
|
|
20
|
+
"exam",
|
|
21
|
+
"course-review",
|
|
22
|
+
"ccc-magi"
|
|
36
23
|
],
|
|
37
24
|
"engines": {
|
|
38
25
|
"claude-code": ">=2.0.0"
|
|
39
26
|
},
|
|
40
|
-
"_note": "
|
|
27
|
+
"_note": "Plugin-only install ships skills + commands; the full tutor (course/ structure, pre-filled constitution + install.json) comes from `npx create-ccc-tutor`."
|
|
41
28
|
}
|
|
@@ -44,6 +44,19 @@ pdftotext -layout "course/<科目>/exam/<文件名>.pdf" - 2>/dev/null \
|
|
|
44
44
|
- 按用户说的"第几题 / 哪道题 / 描述"定位到具体题目;定位不到就告知没找到,列出文件里实际有哪些题请用户确认。
|
|
45
45
|
- **先把题目原文复述出来**给用户核对,再开始解答。
|
|
46
46
|
|
|
47
|
+
# 第二步半:用检索引擎找解题依据(课件,pdf-vision)
|
|
48
|
+
|
|
49
|
+
解题依据从 slide 课件找时,用引擎检索定位,不要逐个翻 PDF:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
.harness/scripts/pdf-rag.sh query --subject <科目> -q "<这道题考的知识点/方法>" -k 5
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- 读命中页文字 `cat "<text_path>"`;命中页 `visual_flag=true` 或题目涉及图/表时,用 `codex exec -i "<png_path>" "只描述图里看得见的,不猜"` 看图。出处用 `source_file_exact` + 页码。
|
|
56
|
+
- `miss=true` → 该步课件无依据,按解题铁律 2 标「课件没有直接依据」。
|
|
57
|
+
- `mode=keyword` / 引擎失败(退出码非 0)→ 告知「语义检索未启用」或回退 `pdftotext`;**看图不可用而必须看图的步骤**明说不可答、不纯文字冒充。
|
|
58
|
+
- 看图取值:标注数字=精确;按刻度=估算(标「非精确」);看不清=只给相对大小;图未读出=标注退回文字;文图冲突=两列各标出处、不替课件裁决。
|
|
59
|
+
|
|
47
60
|
# 解题铁律(不可违反,与 Claude 版一致)
|
|
48
61
|
|
|
49
62
|
1. **先依据课件解。** 方法/公式/判定标准优先引用同科目 slide 课件,能引的步骤标 `(Lecture N, 第M页)`。
|
|
@@ -18,7 +18,7 @@ metadata:
|
|
|
18
18
|
1. 列出科目:`ls -d course/*/ 2>/dev/null`。
|
|
19
19
|
2. 读当前会话科目:`cat .harness/state/current-subject.txt 2>/dev/null`(可能不存在)。
|
|
20
20
|
3. 决定科目:
|
|
21
|
-
- 用户这次点名了科目(出现某个科目文件夹名,如 `
|
|
21
|
+
- 用户这次点名了科目(出现某个科目文件夹名,如 `ECON-10005`、`COMP-5990`)→ 用它,并写入:`printf '%s' '<科目>' > .harness/state/current-subject.txt`。
|
|
22
22
|
- 没点名、state 有 → 沿用。
|
|
23
23
|
- 没点名、state 没有:只有一个科目 → 直接用并写入 state(开头说明"当前科目:X");多个科目 → **停下来问用户**是哪个科目(列出可选),等回答。不要猜。
|
|
24
24
|
- 用户说换科目 → 更新 state。
|
|
@@ -39,9 +39,33 @@ metadata:
|
|
|
39
39
|
- 除了科目外没有具体问题(只触发了技能 / 只说了科目名)→ 请用户说清楚要问什么,然后停止。
|
|
40
40
|
- 问题太宽泛/模糊/依赖上文 → **不要猜**,请用户把问题说完整;可附明确标注的相关内容,但要写"这是相关内容、非直接回答"。
|
|
41
41
|
|
|
42
|
-
#
|
|
42
|
+
# 第二步:用检索引擎定位 + 看图(pdf-vision)
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
本项目用 `pdf-vision` 引擎做「快速检索 + 看图」,先检索定位再读页:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
ls "course/<科目>/slide/" # 为空 → 提示用户先放 PDF 并停止
|
|
48
|
+
.harness/scripts/pdf-rag.sh query --subject <科目> -q "<用户的问题>" -k 5
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
输出 JSON:`results`=命中页 `[{source_file_exact, lecture, page, score, visual_flag, png_path, text_path}]`;`miss=true`=没命中;`mode`=`semantic`/`keyword`。
|
|
52
|
+
|
|
53
|
+
1. 读命中页文字:`cat "<text_path>"`(引擎已抽好、含扫描页 OCR 文字,免重抽)。
|
|
54
|
+
2. **看图(Codex 关键)**:命中页 `visual_flag=true` 或问题涉及图/表时,用 `png_path` 让 Codex 看图:
|
|
55
|
+
```bash
|
|
56
|
+
codex exec -i "<png_path>" "只描述这页图/表里看得见的:标注数字、坐标轴、图例、趋势;看不清就说看不清,不要猜。"
|
|
57
|
+
```
|
|
58
|
+
把描述并入回答,套下面取值规则。
|
|
59
|
+
3. `miss=true` → 走情况 E(先说没有、再问用户要不要外部补充),绝不拿低相关结果当权威答案。
|
|
60
|
+
4. 出处用 `source_file_exact` + 页码。
|
|
61
|
+
|
|
62
|
+
**降级与诚实:** `mode=keyword` → 文字问题照答,告知「语义检索未启用、用关键词查找」;`pdf-rag.sh` 失败(退出码非 0)→ 回退到下面的 shell 抽文字;看图不可用(`png_path` 空 / `codex exec -i` 失败)而问题必须看图 → 明说「需要看图、当前看图不可用、无法从课件作答」,不要纯文字冒充。
|
|
63
|
+
|
|
64
|
+
**看图取值规则:** 图上标注数字=精确;按刻度=估算(标「非精确」);看不清=只给相对大小;图未读出=标注退回文字;文图冲突=两列各标出处、不替课件裁决;每条视觉结论绑定具体页。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
**后备路径**(仅当 `pdf-rag.sh` 不可用时)——用 shell 把 PDF 转文字再读:
|
|
45
69
|
|
|
46
70
|
```bash
|
|
47
71
|
ls "course/<科目>/slide/" # 看有哪些课件;为空则提示用户先放 PDF 并停止
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pdf-rag.sh — pdf_rag.py 的依赖自举包装。Claude/Codex 技能统一调它。
|
|
3
|
+
#
|
|
4
|
+
# .harness/scripts/pdf-rag.sh index --subject ECON-10005
|
|
5
|
+
# .harness/scripts/pdf-rag.sh query --subject ECON-10005 -q "问题" -k 5
|
|
6
|
+
# .harness/scripts/pdf-rag.sh render --pdf "course/.../x.pdf" --page 9 --subject ECON-10005
|
|
7
|
+
#
|
|
8
|
+
# 隔离 venv 在 .harness/.venv-pdf(gitignored)。首次会装 pymupdf/fastembed/numpy/pypdf,
|
|
9
|
+
# 并触发多语言嵌入模型首次下载(约 400–470MB)。装不上时退出码 3 + JSON 错误,
|
|
10
|
+
# 技能据此降级(只用既有 pdftotext 文字路径)。
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
ROOT="$(cd "$HERE/../.." && pwd)"
|
|
15
|
+
VENV="$ROOT/.harness/.venv-pdf"
|
|
16
|
+
REQ="$HERE/requirements-pdf.txt"
|
|
17
|
+
PY="$VENV/bin/python"
|
|
18
|
+
STAMP="$VENV/.installed"
|
|
19
|
+
|
|
20
|
+
cd "$ROOT"
|
|
21
|
+
|
|
22
|
+
fail_json() { printf '{"ok":false,"error":"%s"}\n' "$1"; exit 3; }
|
|
23
|
+
|
|
24
|
+
if [ ! -x "$PY" ]; then
|
|
25
|
+
PYBIN="$(command -v python3 || true)"
|
|
26
|
+
[ -n "$PYBIN" ] || fail_json "python3-not-found"
|
|
27
|
+
"$PYBIN" -m venv "$VENV" >&2 || fail_json "venv-create-failed"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# 依赖只装一次(按 requirements 内容哈希做戳记,requirements 变了会重装)
|
|
31
|
+
REQ_HASH="$("$PY" -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$REQ" 2>/dev/null || echo none)"
|
|
32
|
+
if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP" 2>/dev/null)" != "$REQ_HASH" ]; then
|
|
33
|
+
if "$PY" -m pip install --quiet --disable-pip-version-check -r "$REQ" >&2; then
|
|
34
|
+
printf '%s' "$REQ_HASH" > "$STAMP"
|
|
35
|
+
else
|
|
36
|
+
fail_json "dependency-install-failed"
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
exec "$PY" "$HERE/pdf_rag.py" "$@"
|