dsh-plugin-lookatstudy 0.23.0 → 0.24.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/lib/client.js +144 -15
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +274 -39
- package/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -263,6 +263,10 @@ const ZH = {
|
|
|
263
263
|
"rail.due.start": "开始复习",
|
|
264
264
|
"rail.due.none": "暂时没有到期的复习,保持节奏!",
|
|
265
265
|
"chat.reasoning": "思考过程({n} 字)",
|
|
266
|
+
"chat.think": "思考",
|
|
267
|
+
"langview.original": "原文",
|
|
268
|
+
"langview.bilingual": "对照",
|
|
269
|
+
"langview.translation": "译文",
|
|
266
270
|
"msg.speak": "朗读",
|
|
267
271
|
"note.pin": "置顶笔记",
|
|
268
272
|
"note.unpin": "取消置顶",
|
|
@@ -630,6 +634,10 @@ const EN = {
|
|
|
630
634
|
"rail.due.start": "Start reviewing",
|
|
631
635
|
"rail.due.none": "Nothing due right now — keep the streak!",
|
|
632
636
|
"chat.reasoning": "Thinking ({n} chars)",
|
|
637
|
+
"chat.think": "Think",
|
|
638
|
+
"langview.original": "Original",
|
|
639
|
+
"langview.bilingual": "Both",
|
|
640
|
+
"langview.translation": "Translation",
|
|
633
641
|
"msg.speak": "Speak",
|
|
634
642
|
"note.pin": "Pin note",
|
|
635
643
|
"note.unpin": "Unpin",
|
|
@@ -3113,9 +3121,14 @@ function workbenchState(state, now) {
|
|
|
3113
3121
|
quote: n.quote,
|
|
3114
3122
|
pinned: n.pinned === true
|
|
3115
3123
|
})),
|
|
3116
|
-
html: renderMarkdown(normalizeMathNotation(ref.lesson.
|
|
3124
|
+
html: renderMarkdown(normalizeMathNotation(ref.lesson.body)),
|
|
3117
3125
|
markdown: ref.lesson.body,
|
|
3118
|
-
speechText: normalizeSpeechText(normalizeMathNotation(ref.lesson.body))
|
|
3126
|
+
speechText: normalizeSpeechText(normalizeMathNotation(ref.lesson.body)),
|
|
3127
|
+
...ref.lesson.translation !== void 0 ? {
|
|
3128
|
+
bilingualHtml: renderMarkdown(normalizeMathNotation(renderBilingual(ref.lesson.body, ref.lesson.translation))),
|
|
3129
|
+
translationHtml: renderMarkdown(normalizeMathNotation(ref.lesson.translation)),
|
|
3130
|
+
translationLang: ref.lesson.translationLang ?? ""
|
|
3131
|
+
} : {}
|
|
3119
3132
|
};
|
|
3120
3133
|
} catch {
|
|
3121
3134
|
lesson = null;
|
|
@@ -3360,6 +3373,7 @@ function registerDashboard(webServer, deps) {
|
|
|
3360
3373
|
}
|
|
3361
3374
|
try {
|
|
3362
3375
|
const ref = findLesson(deps.store.get(), body.lessonId);
|
|
3376
|
+
if (ref.lesson.kind !== "exam" && ref.lesson.status === "available") attemptLesson(deps.store.get(), ref.lesson.id, /* @__PURE__ */ new Date());
|
|
3363
3377
|
deps.store.get().focus = { lessonId: ref.lesson.id };
|
|
3364
3378
|
deps.store.save();
|
|
3365
3379
|
sendJson(res, 200, { ok: true });
|
|
@@ -3893,6 +3907,26 @@ function registerDashboard(webServer, deps) {
|
|
|
3893
3907
|
});
|
|
3894
3908
|
return;
|
|
3895
3909
|
}
|
|
3910
|
+
if (req.method === "POST" && pathname === "/lookatstudy/api/prefs") {
|
|
3911
|
+
const body = await readJsonBodySafe(req, res);
|
|
3912
|
+
if (body === void 0) return;
|
|
3913
|
+
if (typeof body.interfaceLang !== "string" || body.interfaceLang === "" || body.interfaceLang.length > 35) {
|
|
3914
|
+
sendJson(res, 400, {
|
|
3915
|
+
ok: false,
|
|
3916
|
+
error: "interfaceLang (non-empty string, max 35 chars) required"
|
|
3917
|
+
});
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
if (deps.store.get().interfaceLang !== body.interfaceLang) {
|
|
3921
|
+
deps.store.get().interfaceLang = body.interfaceLang;
|
|
3922
|
+
deps.store.save();
|
|
3923
|
+
}
|
|
3924
|
+
sendJson(res, 200, {
|
|
3925
|
+
ok: true,
|
|
3926
|
+
interfaceLang: body.interfaceLang
|
|
3927
|
+
});
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3896
3930
|
if (req.method === "POST" && pathname === "/lookatstudy/api/attachment") {
|
|
3897
3931
|
const body = await readJsonBodySafe(req, res, 29360128);
|
|
3898
3932
|
if (body === void 0) return;
|
|
@@ -4977,6 +5011,104 @@ async function fetchRepoFileTree(owner, repo, branch, _fetchFn, signal) {
|
|
|
4977
5011
|
}
|
|
4978
5012
|
/** 文件数上限(防爆,和 IPC handler 一致) */
|
|
4979
5013
|
const MAX_FILES = 500;
|
|
5014
|
+
/** 从 markdown 链接中提取翻译语言列表 */
|
|
5015
|
+
function extractLanguagesFromReadme(readmeMd) {
|
|
5016
|
+
const pattern = /\[([^\]]+)\]\(\.?\/?translations\/([^/)]+)\/README\.md\)/g;
|
|
5017
|
+
const langs = [];
|
|
5018
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5019
|
+
let m;
|
|
5020
|
+
while ((m = pattern.exec(readmeMd)) !== null) {
|
|
5021
|
+
const name = m[1].trim();
|
|
5022
|
+
const code = m[2].trim();
|
|
5023
|
+
if (!seen.has(code)) {
|
|
5024
|
+
seen.add(code);
|
|
5025
|
+
langs.push({
|
|
5026
|
+
code,
|
|
5027
|
+
name
|
|
5028
|
+
});
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
return langs;
|
|
5032
|
+
}
|
|
5033
|
+
/**
|
|
5034
|
+
* 净化翻译版 markdown —— 翻译内容是 CDN 原样拉取的,未经原文管道的
|
|
5035
|
+
* code-fence-aware parser 处理,可能含畸形结构导致 react-markdown 崩溃。
|
|
5036
|
+
*
|
|
5037
|
+
* 处理:
|
|
5038
|
+
* 1. 未闭合代码围栏:统计 ``` 和 ~~~ 数量,奇数则补一个闭合围栏
|
|
5039
|
+
* 2. 去除 <script>/<style>/<iframe> 等危险 HTML(防 XSS + 防渲染崩溃)
|
|
5040
|
+
* 3. 去 BOM、统一换行
|
|
5041
|
+
*
|
|
5042
|
+
* 这是确定性规则处理(高置信度),不是 LLM 判断 —— 格式修复是规则擅长的。
|
|
5043
|
+
*/
|
|
5044
|
+
function sanitizeTranslatedMarkdown(md) {
|
|
5045
|
+
let s = md.replace(/^\uFEFF/, "");
|
|
5046
|
+
s = s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
5047
|
+
const lines = s.split("\n");
|
|
5048
|
+
let fence = null;
|
|
5049
|
+
for (const line of lines) {
|
|
5050
|
+
const m = line.match(/^\s*(```|~~~)/);
|
|
5051
|
+
if (m) fence = fence ? null : m[1];
|
|
5052
|
+
}
|
|
5053
|
+
if (fence) s = s + "\n" + fence + "\n";
|
|
5054
|
+
s = s.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
|
5055
|
+
s = s.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "");
|
|
5056
|
+
s = s.replace(/<\/?(iframe|object|embed)\b[^>]*>/gi, "");
|
|
5057
|
+
return s.trim();
|
|
5058
|
+
}
|
|
5059
|
+
/**
|
|
5060
|
+
* 拉取翻译版课程内容。
|
|
5061
|
+
*
|
|
5062
|
+
* 策略:不依赖翻译版 README 的链接(很多翻译只翻译了大纲,README 里没有 lesson 链接,
|
|
5063
|
+
* 或链接指向其他语言翻译)。直接用原文课程的文件路径,在前面加 translations/<code>/
|
|
5064
|
+
* 前缀去探测翻译版是否存在。5 并发拉取,404 跳过(该课无翻译)。
|
|
5065
|
+
*
|
|
5066
|
+
* @returns Map<originalPath, { title, content }> — key 是原文路径
|
|
5067
|
+
*/
|
|
5068
|
+
async function fetchTranslatedContent(owner, repo, branch, langCode, originalFiles, fetchFn, onProgress) {
|
|
5069
|
+
const send = (msg) => onProgress?.(msg);
|
|
5070
|
+
const result = /* @__PURE__ */ new Map();
|
|
5071
|
+
const transReadmeUrl = cdnUrl(owner, repo, branch, `translations/${langCode}/README.md`);
|
|
5072
|
+
send(`检查翻译版 README (${langCode})…`);
|
|
5073
|
+
try {
|
|
5074
|
+
const r = await fetchFn(transReadmeUrl);
|
|
5075
|
+
if (!r.ok) {
|
|
5076
|
+
send(`翻译版不存在 (${r.status}),跳过`);
|
|
5077
|
+
return result;
|
|
5078
|
+
}
|
|
5079
|
+
} catch {
|
|
5080
|
+
send("翻译版检查失败,跳过");
|
|
5081
|
+
return result;
|
|
5082
|
+
}
|
|
5083
|
+
const mdFiles = originalFiles.filter((f) => !f.path.toLowerCase().endsWith(".ipynb"));
|
|
5084
|
+
send(`探测 ${mdFiles.length} 个文件的翻译版(${langCode})…`);
|
|
5085
|
+
const transPrefix = `translations/${langCode}/`;
|
|
5086
|
+
const CONCURRENCY = 5;
|
|
5087
|
+
let done = 0;
|
|
5088
|
+
for (let i = 0; i < mdFiles.length; i += CONCURRENCY) {
|
|
5089
|
+
const batch = mdFiles.slice(i, i + CONCURRENCY);
|
|
5090
|
+
const results = await Promise.allSettled(batch.map(async (file) => {
|
|
5091
|
+
const r = await fetchFn(cdnUrl(owner, repo, branch, transPrefix + file.path));
|
|
5092
|
+
if (!r.ok) return null;
|
|
5093
|
+
const md = await r.text();
|
|
5094
|
+
return {
|
|
5095
|
+
originalPath: file.path,
|
|
5096
|
+
title: file.title,
|
|
5097
|
+
content: sanitizeTranslatedMarkdown(md)
|
|
5098
|
+
};
|
|
5099
|
+
}));
|
|
5100
|
+
for (const res of results) {
|
|
5101
|
+
done++;
|
|
5102
|
+
if (res.status === "fulfilled" && res.value) result.set(res.value.originalPath, {
|
|
5103
|
+
title: res.value.title,
|
|
5104
|
+
content: res.value.content
|
|
5105
|
+
});
|
|
5106
|
+
}
|
|
5107
|
+
if (done % 10 === 0 || done === mdFiles.length) send(`翻译探测 ${done}/${mdFiles.length}(命中 ${result.size})`);
|
|
5108
|
+
}
|
|
5109
|
+
send(`翻译版拉取完成: ${result.size}/${mdFiles.length} 文件有翻译`);
|
|
5110
|
+
return result;
|
|
5111
|
+
}
|
|
4980
5112
|
/**
|
|
4981
5113
|
* Step 1: 拉取仓库清单 —— README 全文 + 完整目录树 + 课程文件列表。
|
|
4982
5114
|
*
|
|
@@ -6072,6 +6204,42 @@ function estimateTokens(text) {
|
|
|
6072
6204
|
return Math.ceil(cjk * 11 / 10 + other / 4);
|
|
6073
6205
|
}
|
|
6074
6206
|
/**
|
|
6207
|
+
* Match the learner's interface language against the repo's declared
|
|
6208
|
+
* translation languages (0.24.0): exact code first (case-insensitive), then
|
|
6209
|
+
* the base subtag family — a zh-CN host matches a zh-cn mirror, pt-PT still
|
|
6210
|
+
* finds pt-br. No family match (or nothing recorded) returns null and the
|
|
6211
|
+
* import stays original-language. Pure.
|
|
6212
|
+
*/
|
|
6213
|
+
function matchTranslationLang(want, available) {
|
|
6214
|
+
if (want === void 0 || want.trim() === "" || available.length === 0) return null;
|
|
6215
|
+
const norm = (code) => code.trim().toLowerCase();
|
|
6216
|
+
const exact = available.find((a) => norm(a.code) === norm(want));
|
|
6217
|
+
if (exact !== void 0) return exact;
|
|
6218
|
+
const base = (code) => norm(code).split("-")[0];
|
|
6219
|
+
const wantBase = base(want);
|
|
6220
|
+
return available.find((a) => base(a.code) === wantBase) ?? null;
|
|
6221
|
+
}
|
|
6222
|
+
/**
|
|
6223
|
+
* 0.23.1 issue 5: the pacing gate's hard cap (the brief's 3000-8000 target's
|
|
6224
|
+
* upper edge). A sliced lesson over this WITH splittable in-span headings is
|
|
6225
|
+
* rejected — the brief has always carried the rule, but nothing enforced it,
|
|
6226
|
+
* so a flash-tier tutor's 1:1 file→lesson design shipped 45k-char lessons.
|
|
6227
|
+
*/
|
|
6228
|
+
const LESSON_MAX_CHARS = 8e3;
|
|
6229
|
+
/**
|
|
6230
|
+
* Count the in-span headings a lesson could be split by: direct children for
|
|
6231
|
+
* an anchored lesson (an H2 anchor's span ends at the next ≤-level heading,
|
|
6232
|
+
* so its H3s are the split unit; an H3 anchor is the terminal unit — nothing
|
|
6233
|
+
* deeper exists to split by), any H2/H3 for a whole-file lesson.
|
|
6234
|
+
*/
|
|
6235
|
+
function splittableHeadingCount(headings, titleIndex) {
|
|
6236
|
+
if (titleIndex < 0) return headings.filter((h) => h.level >= 2).length;
|
|
6237
|
+
const anchorLevel = headings[titleIndex].level;
|
|
6238
|
+
let count = 0;
|
|
6239
|
+
for (let i = titleIndex + 1; i < headings.length && headings[i].level > anchorLevel; i++) if (headings[i].level === anchorLevel + 1) count++;
|
|
6240
|
+
return count;
|
|
6241
|
+
}
|
|
6242
|
+
/**
|
|
6075
6243
|
* Build the pending design from Step 1 (inventory) + Step 3 (outlines).
|
|
6076
6244
|
* @param url - the GitHub URL the learner asked to import.
|
|
6077
6245
|
* @param owner - repo owner.
|
|
@@ -6244,6 +6412,7 @@ function renderDesignBrief(pending, part = 1) {
|
|
|
6244
6412
|
}
|
|
6245
6413
|
lines.push("");
|
|
6246
6414
|
lines.push("### Design rules (from the LookatStudy import pipeline)");
|
|
6415
|
+
if (pending.availableLangs !== void 0 && pending.availableLangs.length > 0) lines.push(`Translations available in this repo: ${pending.availableLangs.map((l) => `${l.name} (${l.code})`).join(", ")}. study_apply_design automatically pairs the learner's interface-language mirror (translations/<code>/ file copies) with each lesson — no action needed from you; mention the availability to the learner.`);
|
|
6247
6416
|
lines.push("Classify every lesson as exactly one of:");
|
|
6248
6417
|
lines.push("- **study**: explanation/theory/tutorial content — the learning-world spine, its own lesson.");
|
|
6249
6418
|
lines.push("- **practice**: Exercise / Lab / notebook — its own lesson.");
|
|
@@ -6257,7 +6426,7 @@ function renderDesignBrief(pending, part = 1) {
|
|
|
6257
6426
|
lines.push("Other rules:");
|
|
6258
6427
|
lines.push("- role hints are references, not verdicts — README tables usually mark real roles (Lesson link = study, Notebook/Lab = practice).");
|
|
6259
6428
|
lines.push("- if the directory layout is already clear (e.g. lessons/N-Topic/), keep its sections; do not over-reorganize.");
|
|
6260
|
-
lines.push(`- anchor is the full heading text used to slice the body; omit it for whole-file lessons.${shown.length > 80 ? " This part is large: design at file granularity (omit anchors, one lesson per file) to keep the JSON manageable." : ""}`);
|
|
6429
|
+
lines.push(`- anchor is the full heading text used to slice the body; omit it for whole-file lessons.${shown.length > 80 ? " This part is large: design at file granularity (omit anchors, one lesson per file) to keep the JSON manageable — EXCEPT files over 8000 chars: those still need anchor splitting (anchor = an H2/H3 heading) into 3000-8000-char lessons; study_apply_design rejects over-long lessons whose file has splittable headings." : ""}`);
|
|
6261
6430
|
lines.push("");
|
|
6262
6431
|
lines.push("### Language-course check (decide while designing)");
|
|
6263
6432
|
lines.push("Is this course teaching a language ITSELF (vocabulary, grammar, reading/writing/listening of English/Japanese/Korean/Chinese/…), rather than a knowledge/tech course that merely happens to be written in some language? Typical tells: vocab lists, grammar-point explanations, dialogues/readings, sentence-translation pairs, level markers (N5/N1, TOEFL/IELTS, HSK, JLPT), pronunciation content.");
|
|
@@ -6444,42 +6613,56 @@ function buildCourseFromDesign(courseTitle, validated, contents, translations) {
|
|
|
6444
6613
|
return cached;
|
|
6445
6614
|
};
|
|
6446
6615
|
const firstLessonSeen = /* @__PURE__ */ new Set();
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
const
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
world: lesson.world,
|
|
6470
|
-
...translation !== void 0 ? {
|
|
6471
|
-
translation,
|
|
6472
|
-
translationLang: pair.lang
|
|
6473
|
-
} : {}
|
|
6474
|
-
};
|
|
6616
|
+
const pacingViolations = [];
|
|
6617
|
+
const sections = validated.sections.map((section) => {
|
|
6618
|
+
const lessons = section.lessons.map((lesson) => {
|
|
6619
|
+
const content = contents.get(lesson.file);
|
|
6620
|
+
if (content === void 0) throw new Error(`lookatstudy-plugin: no fetched content for designed file ${JSON.stringify(lesson.file)} — fetch failed earlier; retry study_apply_design`);
|
|
6621
|
+
const headings = headingsOf(lesson.file);
|
|
6622
|
+
const titleIndex = lesson.anchor === null ? -1 : findTitleIndex(headings, lesson.anchor);
|
|
6623
|
+
const isFirst = !firstLessonSeen.has(lesson.file);
|
|
6624
|
+
firstLessonSeen.add(lesson.file);
|
|
6625
|
+
const pair = translations?.get(lesson.file);
|
|
6626
|
+
let translation;
|
|
6627
|
+
if (pair !== void 0) {
|
|
6628
|
+
const tHeadings = extractHeadings(pair.content);
|
|
6629
|
+
const tIndex = lesson.anchor === null ? -1 : findTitleIndex(tHeadings, lesson.anchor);
|
|
6630
|
+
translation = sliceLessonBody(pair.content, tHeadings, tIndex, isFirst);
|
|
6631
|
+
}
|
|
6632
|
+
const body = sliceLessonBody(content, headings, titleIndex, isFirst);
|
|
6633
|
+
if (body.length > 8e3 && splittableHeadingCount(headings, titleIndex) > 0) pacingViolations.push({
|
|
6634
|
+
title: lesson.title,
|
|
6635
|
+
file: lesson.file,
|
|
6636
|
+
chars: body.length,
|
|
6637
|
+
splittable: splittableHeadingCount(headings, titleIndex)
|
|
6475
6638
|
});
|
|
6476
6639
|
return {
|
|
6477
|
-
title:
|
|
6478
|
-
anchor: slugAnchor(
|
|
6479
|
-
|
|
6480
|
-
|
|
6640
|
+
title: lesson.title,
|
|
6641
|
+
anchor: slugAnchor(lesson.title),
|
|
6642
|
+
body,
|
|
6643
|
+
sourceFilePath: lesson.file,
|
|
6644
|
+
world: lesson.world,
|
|
6645
|
+
...translation !== void 0 ? {
|
|
6646
|
+
translation,
|
|
6647
|
+
translationLang: pair.lang
|
|
6648
|
+
} : {}
|
|
6481
6649
|
};
|
|
6482
|
-
})
|
|
6650
|
+
});
|
|
6651
|
+
return {
|
|
6652
|
+
title: section.title,
|
|
6653
|
+
anchor: slugAnchor(section.title),
|
|
6654
|
+
world: lessons.every((l) => l.world === "practice") ? "practice" : "study",
|
|
6655
|
+
lessons
|
|
6656
|
+
};
|
|
6657
|
+
});
|
|
6658
|
+
if (pacingViolations.length > 0) {
|
|
6659
|
+
const listed = pacingViolations.slice(0, 5).map((v) => `「${v.title}」 ${v.file} (${v.chars} chars, ${v.splittable} splittable heading${v.splittable === 1 ? "" : "s"})`).join("; ");
|
|
6660
|
+
const more = pacingViolations.length > 5 ? ` (+${pacingViolations.length - 5} more)` : "";
|
|
6661
|
+
throw new Error(`lookatstudy-plugin: pacing gate — ${pacingViolations.length} lesson${pacingViolations.length === 1 ? "" : "s"} over ${LESSON_MAX_CHARS} chars while the file has splittable H2/H3 sections (design rule: 3000-8000 chars per lesson): ${listed}${more}. Split each into one lesson per H2/H3 (anchor = the heading's full title, merge fragments under 1000 chars), then re-submit study_apply_design`);
|
|
6662
|
+
}
|
|
6663
|
+
return {
|
|
6664
|
+
title: courseTitle,
|
|
6665
|
+
sections
|
|
6483
6666
|
};
|
|
6484
6667
|
}
|
|
6485
6668
|
//#endregion
|
|
@@ -7142,6 +7325,26 @@ function studyTools(store, deps = {}) {
|
|
|
7142
7325
|
type: "integer",
|
|
7143
7326
|
description: "Pending-design sequence (audit C29): renders detect stale briefs."
|
|
7144
7327
|
},
|
|
7328
|
+
availableLangs: {
|
|
7329
|
+
type: "array",
|
|
7330
|
+
description: "Translation languages the repo README declares (translations/<code>/ mirrors) — apply pairs the learner's interface language automatically.",
|
|
7331
|
+
items: {
|
|
7332
|
+
type: "object",
|
|
7333
|
+
additionalProperties: false,
|
|
7334
|
+
properties: {
|
|
7335
|
+
code: {
|
|
7336
|
+
type: "string",
|
|
7337
|
+
required: true,
|
|
7338
|
+
description: "BCP-47-ish code as it appears in the translations/ path."
|
|
7339
|
+
},
|
|
7340
|
+
name: {
|
|
7341
|
+
type: "string",
|
|
7342
|
+
required: true,
|
|
7343
|
+
description: "Display name from the README link text."
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
}
|
|
7347
|
+
},
|
|
7145
7348
|
part: {
|
|
7146
7349
|
type: "integer",
|
|
7147
7350
|
description: "Brief part this call rendered (context-budget batching)."
|
|
@@ -7245,6 +7448,10 @@ function studyTools(store, deps = {}) {
|
|
|
7245
7448
|
courseTitle: partCount > 1 ? `${pd.courseTitle} (part ${part})` : pd.courseTitle,
|
|
7246
7449
|
fileCount: planBriefParts(pd.files)[Math.min(part, partCount) - 1].length,
|
|
7247
7450
|
fullTreeCount: pd.fullTreeCount,
|
|
7451
|
+
...pd.availableLangs !== void 0 && pd.availableLangs.length > 0 ? { availableLangs: pd.availableLangs.map((l) => ({
|
|
7452
|
+
code: l.code,
|
|
7453
|
+
name: l.name
|
|
7454
|
+
})) } : {},
|
|
7248
7455
|
...partCount > 1 ? {
|
|
7249
7456
|
part,
|
|
7250
7457
|
partCount
|
|
@@ -7265,6 +7472,8 @@ function studyTools(store, deps = {}) {
|
|
|
7265
7472
|
};
|
|
7266
7473
|
const inventory = await fetchRepoInventory(owner, repo, resolvedBranch, fetchFn, void 0, exec.signal);
|
|
7267
7474
|
const pd = buildPendingDesign(canonicalRef, owner, repo, inventory, await fetchFileOutlines(inventory.fileList.map((f) => f.path), owner, repo, inventory.branch, fetchFn, void 0, exec.signal));
|
|
7475
|
+
const availableLangs = extractLanguagesFromReadme(inventory.readmeMd);
|
|
7476
|
+
if (availableLangs.length > 0) pd.availableLangs = availableLangs;
|
|
7268
7477
|
setPendingDesign(pd);
|
|
7269
7478
|
if (pd.files.length === 0) throw new Error("lookatstudy-plugin: course files were discovered but no outlines could be fetched (CDN unreachable?)");
|
|
7270
7479
|
pendingPart = part;
|
|
@@ -7401,7 +7610,11 @@ ${p.text}`));
|
|
|
7401
7610
|
parameters: {
|
|
7402
7611
|
languageTarget: {
|
|
7403
7612
|
type: "string",
|
|
7404
|
-
description: "Set ONLY when this course teaches a language ITSELF (vocabulary, grammar, reading/writing of English/Japanese/Chinese/…) rather than being a knowledge/tech course that merely happens to be written in some language: the taught language as a BCP-47 tag (\"en\", \"ja\", \"zh-CN\", …). The writing language is NOT the taught language — a Chinese-written English-textbook repo has languageTarget \"en\". Typical tells: vocab lists, grammar points, dialogues/readings, sentence-translation pairs, level markers (N5/N1, TOEFL, HSK, JLPT), pronunciation content. When unsure, omit it — a normal course misflagged as a language course is worse than the reverse."
|
|
7613
|
+
description: "Set ONLY when this course teaches a language ITSELF (vocabulary, grammar, reading/writing of English/Japanese/Chinese/…) rather than being a knowledge/tech course that merely happens to be written in some language: the taught language as a BCP-47 tag (\"en\", \"ja\", \"zh-CN\", …). The writing language is NOT the taught language — a Chinese-written English-textbook repo has languageTarget \"en\". Typical tells: vocab lists, grammar points, dialogues/readings, sentence-translation pairs, level markers (N5/N1, TOEFL/IELTS, HSK, JLPT), pronunciation content. When unsure, omit it — a normal course misflagged as a language course is worse than the reverse."
|
|
7614
|
+
},
|
|
7615
|
+
translationLang: {
|
|
7616
|
+
type: "string",
|
|
7617
|
+
description: "Override the translation mirror language (0.24.0). Default: the learner's interface language, matched against the repo's declared translations (zh-CN matches zh-cn, en-US falls back to en). Set this only when the LEARNER explicitly asks for a different language than their interface."
|
|
7405
7618
|
},
|
|
7406
7619
|
sections: {
|
|
7407
7620
|
type: "array",
|
|
@@ -7481,6 +7694,11 @@ ${p.text}`));
|
|
|
7481
7694
|
type: "integer",
|
|
7482
7695
|
required: true
|
|
7483
7696
|
},
|
|
7697
|
+
translations: {
|
|
7698
|
+
type: "integer",
|
|
7699
|
+
required: true,
|
|
7700
|
+
description: "Lessons that landed with a paired translation (the interface-language mirror existed and its anchor matched) — their teach tab offers 原文/对照/译文."
|
|
7701
|
+
},
|
|
7484
7702
|
created: {
|
|
7485
7703
|
type: "boolean",
|
|
7486
7704
|
required: true,
|
|
@@ -7494,7 +7712,7 @@ ${p.text}`));
|
|
|
7494
7712
|
},
|
|
7495
7713
|
render: (_args, value) => [{
|
|
7496
7714
|
type: "text",
|
|
7497
|
-
text: value.created === false ? `“${value.title}” (id ${value.courseId}) was ALREADY imported — the design did not create a new course. Continue with the existing course, or delete it first (study_delete_course) to rebuild from this design.` : `Imported designed course “${value.title}” (${value.sections} sections, ${value.lessons} lessons${value.droppedLessons > 0 ? `, ${value.droppedLessons} hallucinated lesson(s) dropped` : ""})${typeof value.languageTarget === "string" ? ` — language course (${value.languageTarget}): keep reading passages, example sentences, and quiz language material in the target language` : ""}. First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}). Present the course map to the learner.`
|
|
7715
|
+
text: value.created === false ? `“${value.title}” (id ${value.courseId}) was ALREADY imported — the design did not create a new course. Continue with the existing course, or delete it first (study_delete_course) to rebuild from this design.` : `Imported designed course “${value.title}” (${value.sections} sections, ${value.lessons} lessons${value.droppedLessons > 0 ? `, ${value.droppedLessons} hallucinated lesson(s) dropped` : ""})${typeof value.translations === "number" && value.translations > 0 ? ` — ${value.translations} lesson(s) carry the learner's-language translation (原文/对照/译文 in the teach tab)` : ""}${typeof value.languageTarget === "string" ? ` — language course (${value.languageTarget}): keep reading passages, example sentences, and quiz language material in the target language` : ""}. First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}). Present the course map to the learner.`
|
|
7498
7716
|
}]
|
|
7499
7717
|
},
|
|
7500
7718
|
async execute(args, exec) {
|
|
@@ -7521,7 +7739,22 @@ ${p.text}`));
|
|
|
7521
7739
|
if (contents.size === 0) throw new Error(`lookatstudy-plugin: every designed file failed to fetch (${failed.length}) — the CDN path is unreachable; retry or re-import`);
|
|
7522
7740
|
if (failed.length > 0) throw new Error(`lookatstudy-plugin: ${failed.length} designed file(s) failed to fetch: ${failed.join(", ")} — drop or fix them and call study_apply_design again`);
|
|
7523
7741
|
}
|
|
7524
|
-
|
|
7742
|
+
let translations = pd.translations;
|
|
7743
|
+
if (pd.localContents === void 0 && pd.availableLangs !== void 0 && pd.availableLangs.length > 0) {
|
|
7744
|
+
const match = matchTranslationLang(typeof args.translationLang === "string" && args.translationLang.trim() !== "" ? args.translationLang : store.get().interfaceLang, pd.availableLangs);
|
|
7745
|
+
if (match !== null && !exec.signal.aborted) {
|
|
7746
|
+
const fetched = await fetchTranslatedContent(pd.owner, pd.repo, pd.branch, match.code, uniqueFiles.map((f) => ({
|
|
7747
|
+
path: f,
|
|
7748
|
+
title: f,
|
|
7749
|
+
md: ""
|
|
7750
|
+
})), signalFetch(exec.signal, baseFetch));
|
|
7751
|
+
if (fetched.size > 0) translations = new Map([...fetched].map(([path, v]) => [path, {
|
|
7752
|
+
lang: match.code,
|
|
7753
|
+
content: v.content
|
|
7754
|
+
}]));
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7757
|
+
const parsed = buildCourseFromDesign(pd.part !== void 0 && pd.partCount !== void 0 && pd.partCount > 1 ? `${pd.courseTitle} (part ${pd.part})` : pd.courseTitle, validated, contents, translations);
|
|
7525
7758
|
requireParsedLessons(parsed);
|
|
7526
7759
|
const languageTarget = typeof args.languageTarget === "string" && args.languageTarget.trim() !== "" ? args.languageTarget.trim().slice(0, 35) : null;
|
|
7527
7760
|
const value = mutate((state) => {
|
|
@@ -7533,9 +7766,11 @@ ${p.text}`));
|
|
|
7533
7766
|
});
|
|
7534
7767
|
setPendingDesign(null);
|
|
7535
7768
|
pendingPart = 1;
|
|
7769
|
+
const translatedLessons = parsed.sections.reduce((n, s) => n + s.lessons.filter((l) => l.translation !== void 0).length, 0);
|
|
7536
7770
|
return {
|
|
7537
7771
|
...value,
|
|
7538
7772
|
droppedLessons: validated.droppedLessons,
|
|
7773
|
+
translations: translatedLessons,
|
|
7539
7774
|
...languageTarget !== null ? { languageTarget } : {}
|
|
7540
7775
|
};
|
|
7541
7776
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-lookatstudy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"packageManager": "pnpm@11.7.0",
|
|
5
5
|
"description": "Turn any markdown, local folder, or GitHub learning repo into a guided course inside DeepSeek Harness: gated skill-tree progression, BKT mastery tracking, SM-2 spaced repetition.",
|
|
6
6
|
"type": "module",
|