dsh-plugin-lookatstudy 0.21.0 → 0.21.1
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 +53 -12
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +56 -8
- package/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -229,6 +229,7 @@ const ZH = {
|
|
|
229
229
|
"settings.budget.on": "开启历史预算",
|
|
230
230
|
"settings.budget.off": "关闭历史预算",
|
|
231
231
|
"attach.sent": "我发了一个文件:{name}(工作区路径 {path})——请读取它再继续。",
|
|
232
|
+
"attach.remove": "移除待发附件",
|
|
232
233
|
"attach.fail": "附件上传失败",
|
|
233
234
|
"attach.label": "附件(≤20MiB,存入学习工作区)",
|
|
234
235
|
"rail.mastered": "毕业 {mastered}/{total} 课",
|
|
@@ -575,6 +576,7 @@ const EN = {
|
|
|
575
576
|
"settings.budget.on": "Enable history budget",
|
|
576
577
|
"settings.budget.off": "Disable history budget",
|
|
577
578
|
"attach.sent": "I attached a file: {name} (workspace path {path}) — read it before continuing.",
|
|
579
|
+
"attach.remove": "Remove pending attachment",
|
|
578
580
|
"attach.fail": "Attachment upload failed",
|
|
579
581
|
"attach.label": "Attach (≤20MiB, lands in the study workspace)",
|
|
580
582
|
"rail.mastered": "{mastered}/{total} lessons graduated",
|
|
@@ -778,10 +780,13 @@ function escapeHtml(text) {
|
|
|
778
780
|
}
|
|
779
781
|
/** Render inline markup (code, bold, italic, links, images) over escaped text.
|
|
780
782
|
* Image src allowlist: https?:// or data:image/ — folder imports inline local
|
|
781
|
-
* images as data URLs (capped), GitHub imports reference jsDelivr;
|
|
782
|
-
*
|
|
783
|
+
* images as data URLs (capped), GitHub imports reference jsDelivr; the
|
|
784
|
+
* plugin's own attachment route (/lookatstudy/api/attachment/<name>, issue #7
|
|
785
|
+
* — same-origin, server-side validated) renders workspace attachments;
|
|
786
|
+
* anything else stays literal text (no broken relative images, no exotic
|
|
787
|
+
* schemes). */
|
|
783
788
|
function inline(escaped) {
|
|
784
|
-
return escaped.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+|data:image\/[^)\s]+)\)/g, (_m, alt, src) => `<img src="${src}" alt="${alt}" loading="lazy">`).replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noreferrer\">$1</a>");
|
|
789
|
+
return escaped.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+|data:image\/[^)\s]+|\/lookatstudy\/api\/attachment\/[^)\s]+)\)/g, (_m, alt, src) => `<img src="${src}" alt="${alt}" loading="lazy">`).replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noreferrer\">$1</a>");
|
|
785
790
|
}
|
|
786
791
|
/** Interleave one body with its translation, paragraph by paragraph (upstream
|
|
787
792
|
* translation-layout "microsoft" semantics: original paragraph then its
|
|
@@ -1496,9 +1501,11 @@ function freshLesson(title, anchor, body, kind = "study") {
|
|
|
1496
1501
|
* @returns the imported (or pre-existing) course.
|
|
1497
1502
|
*/
|
|
1498
1503
|
function importCourse(state, parsed, source, sourceRef, languageTarget) {
|
|
1499
|
-
const
|
|
1500
|
-
const
|
|
1501
|
-
if (
|
|
1504
|
+
const base = slugify(parsed.title);
|
|
1505
|
+
const sameTitle = state.courses.find((c) => c.id === base && c.title === parsed.title);
|
|
1506
|
+
if (sameTitle !== void 0) return sameTitle;
|
|
1507
|
+
let id = base;
|
|
1508
|
+
for (let n = 2; state.courses.some((c) => c.id === id); n++) id = `${base}-${String(n)}`;
|
|
1502
1509
|
const course = {
|
|
1503
1510
|
id,
|
|
1504
1511
|
title: parsed.title,
|
|
@@ -3188,6 +3195,36 @@ function registerDashboard(webServer, deps) {
|
|
|
3188
3195
|
}
|
|
3189
3196
|
return;
|
|
3190
3197
|
}
|
|
3198
|
+
if (req.method === "GET" && pathname.startsWith("/lookatstudy/api/attachment/")) {
|
|
3199
|
+
let name = "";
|
|
3200
|
+
try {
|
|
3201
|
+
name = decodeURIComponent(pathname.slice(28));
|
|
3202
|
+
} catch {
|
|
3203
|
+
name = "";
|
|
3204
|
+
}
|
|
3205
|
+
if (name === "" || name.includes("..") || /[\\/]/.test(name) || name.length > 250) {
|
|
3206
|
+
sendJson(res, 404, {
|
|
3207
|
+
ok: false,
|
|
3208
|
+
error: "not found"
|
|
3209
|
+
});
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
try {
|
|
3213
|
+
const buf = readFileSync(join(deps.studyAreaPath, "attachments", name));
|
|
3214
|
+
const ext = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
|
|
3215
|
+
const mime = ext === "png" ? "image/png" : ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : ext === "bmp" ? "image/bmp" : "application/octet-stream";
|
|
3216
|
+
res.writeHead(200, {
|
|
3217
|
+
"content-type": mime,
|
|
3218
|
+
"cache-control": "public, max-age=31536000, immutable"
|
|
3219
|
+
}).end(buf);
|
|
3220
|
+
} catch {
|
|
3221
|
+
sendJson(res, 404, {
|
|
3222
|
+
ok: false,
|
|
3223
|
+
error: "not found"
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3191
3228
|
sendJson(res, 404, {
|
|
3192
3229
|
ok: false,
|
|
3193
3230
|
error: "not found"
|
|
@@ -6223,6 +6260,11 @@ function studyTools(store, deps = {}) {
|
|
|
6223
6260
|
type: "integer",
|
|
6224
6261
|
required: true
|
|
6225
6262
|
},
|
|
6263
|
+
created: {
|
|
6264
|
+
type: "boolean",
|
|
6265
|
+
required: true,
|
|
6266
|
+
description: "False when an existing course short-circuited the import (same title) — the design did NOT land as a new course."
|
|
6267
|
+
},
|
|
6226
6268
|
languageTarget: {
|
|
6227
6269
|
type: "string",
|
|
6228
6270
|
description: "Present only when the course teaches a language itself (BCP-47); the tutor then keeps quiz material in the target language."
|
|
@@ -6231,7 +6273,7 @@ function studyTools(store, deps = {}) {
|
|
|
6231
6273
|
},
|
|
6232
6274
|
render: (_args, value) => [{
|
|
6233
6275
|
type: "text",
|
|
6234
|
-
text: `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.`
|
|
6276
|
+
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.`
|
|
6235
6277
|
}]
|
|
6236
6278
|
},
|
|
6237
6279
|
async execute(args, exec) {
|
|
@@ -6261,7 +6303,13 @@ function studyTools(store, deps = {}) {
|
|
|
6261
6303
|
const parsed = buildCourseFromDesign(pd.part !== void 0 && pd.partCount !== void 0 && pd.partCount > 1 ? `${pd.courseTitle} (part ${pd.part})` : pd.courseTitle, validated, contents, pd.translations);
|
|
6262
6304
|
requireParsedLessons(parsed);
|
|
6263
6305
|
const languageTarget = typeof args.languageTarget === "string" && args.languageTarget.trim() !== "" ? args.languageTarget.trim().slice(0, 35) : null;
|
|
6264
|
-
const value = mutate((state) =>
|
|
6306
|
+
const value = mutate((state) => {
|
|
6307
|
+
const before = state.courses.length;
|
|
6308
|
+
return {
|
|
6309
|
+
...toImportValue(importCourse(state, parsed, pd.source, pd.url, languageTarget)),
|
|
6310
|
+
created: state.courses.length > before
|
|
6311
|
+
};
|
|
6312
|
+
});
|
|
6265
6313
|
pendingDesign = null;
|
|
6266
6314
|
pendingPart = 1;
|
|
6267
6315
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-lookatstudy",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1",
|
|
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",
|