@mo7yw4ng/openape 1.0.4 → 1.0.6
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 +11 -8
- package/esm/deno.js +1 -1
- package/esm/src/commands/announcements.d.ts.map +1 -1
- package/esm/src/commands/announcements.js +22 -85
- package/esm/src/commands/assignments.d.ts.map +1 -1
- package/esm/src/commands/assignments.js +2 -3
- package/esm/src/commands/auth.d.ts.map +1 -1
- package/esm/src/commands/auth.js +24 -14
- package/esm/src/commands/calendar.d.ts.map +1 -1
- package/esm/src/commands/calendar.js +32 -84
- package/esm/src/commands/courses.d.ts.map +1 -1
- package/esm/src/commands/courses.js +2 -38
- package/esm/src/commands/forums.d.ts.map +1 -1
- package/esm/src/commands/forums.js +47 -175
- package/esm/src/commands/grades.d.ts.map +1 -1
- package/esm/src/commands/grades.js +10 -47
- package/esm/src/commands/materials.d.ts.map +1 -1
- package/esm/src/commands/materials.js +135 -223
- package/esm/src/commands/quizzes.d.ts.map +1 -1
- package/esm/src/commands/quizzes.js +32 -56
- package/esm/src/commands/skills.js +3 -3
- package/esm/src/commands/upload.d.ts.map +1 -1
- package/esm/src/commands/upload.js +2 -5
- package/esm/src/commands/videos.d.ts.map +1 -1
- package/esm/src/commands/videos.js +6 -76
- package/esm/src/index.d.ts +2 -1
- package/esm/src/index.d.ts.map +1 -1
- package/esm/src/index.js +5 -1
- package/esm/src/lib/auth.d.ts +21 -2
- package/esm/src/lib/auth.d.ts.map +1 -1
- package/esm/src/lib/auth.js +79 -20
- package/esm/src/lib/logger.d.ts +2 -2
- package/esm/src/lib/logger.d.ts.map +1 -1
- package/esm/src/lib/logger.js +6 -4
- package/esm/src/lib/moodle.d.ts +18 -0
- package/esm/src/lib/moodle.d.ts.map +1 -1
- package/esm/src/lib/moodle.js +54 -2
- package/esm/src/lib/utils.d.ts +3 -8
- package/esm/src/lib/utils.d.ts.map +1 -1
- package/esm/src/lib/utils.js +3 -10
- package/package.json +1 -1
- package/script/deno.js +1 -1
- package/script/src/commands/announcements.d.ts.map +1 -1
- package/script/src/commands/announcements.js +23 -89
- package/script/src/commands/assignments.d.ts.map +1 -1
- package/script/src/commands/assignments.js +2 -3
- package/script/src/commands/auth.d.ts.map +1 -1
- package/script/src/commands/auth.js +24 -14
- package/script/src/commands/calendar.d.ts.map +1 -1
- package/script/src/commands/calendar.js +33 -85
- package/script/src/commands/courses.d.ts.map +1 -1
- package/script/src/commands/courses.js +9 -48
- package/script/src/commands/forums.d.ts.map +1 -1
- package/script/src/commands/forums.js +50 -181
- package/script/src/commands/grades.d.ts.map +1 -1
- package/script/src/commands/grades.js +14 -54
- package/script/src/commands/materials.d.ts.map +1 -1
- package/script/src/commands/materials.js +132 -220
- package/script/src/commands/quizzes.d.ts.map +1 -1
- package/script/src/commands/quizzes.js +40 -67
- package/script/src/commands/skills.js +3 -3
- package/script/src/commands/upload.d.ts.map +1 -1
- package/script/src/commands/upload.js +2 -5
- package/script/src/commands/videos.d.ts.map +1 -1
- package/script/src/commands/videos.js +11 -81
- package/script/src/index.d.ts +2 -1
- package/script/src/index.d.ts.map +1 -1
- package/script/src/index.js +5 -1
- package/script/src/lib/auth.d.ts +21 -2
- package/script/src/lib/auth.d.ts.map +1 -1
- package/script/src/lib/auth.js +83 -56
- package/script/src/lib/logger.d.ts +2 -2
- package/script/src/lib/logger.d.ts.map +1 -1
- package/script/src/lib/logger.js +6 -4
- package/script/src/lib/moodle.d.ts +18 -0
- package/script/src/lib/moodle.d.ts.map +1 -1
- package/script/src/lib/moodle.js +56 -2
- package/script/src/lib/utils.d.ts +3 -8
- package/script/src/lib/utils.d.ts.map +1 -1
- package/script/src/lib/utils.js +3 -11
- package/skills/openape/SKILL.md +10 -8
|
@@ -6,76 +6,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.registerMaterialsCommand = registerMaterialsCommand;
|
|
7
7
|
const utils_js_1 = require("../lib/utils.js");
|
|
8
8
|
const moodle_js_1 = require("../lib/moodle.js");
|
|
9
|
-
const logger_js_1 = require("../lib/logger.js");
|
|
10
9
|
const auth_js_1 = require("../lib/auth.js");
|
|
11
|
-
const session_js_1 = require("../lib/session.js");
|
|
12
|
-
const auth_js_2 = require("../lib/auth.js");
|
|
13
10
|
const index_js_1 = require("../index.js");
|
|
14
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
13
|
function registerMaterialsCommand(program) {
|
|
17
14
|
const materialsCmd = program.command("materials");
|
|
18
15
|
materialsCmd.description("Material/resource operations");
|
|
19
|
-
// Helper
|
|
20
|
-
async function
|
|
21
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
22
|
-
const outputFormat = (0, utils_js_1.getOutputFormat)(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
23
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
24
|
-
const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
|
|
25
|
-
const sessionPath = (0, utils_js_1.getSessionPath)();
|
|
26
|
-
if (!node_fs_1.default.existsSync(sessionPath)) {
|
|
27
|
-
log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
const config = {
|
|
31
|
-
username: "",
|
|
32
|
-
password: "",
|
|
33
|
-
courseUrl: "",
|
|
34
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
35
|
-
headless: !options.headed,
|
|
36
|
-
slowMo: 0,
|
|
37
|
-
authStatePath: sessionPath,
|
|
38
|
-
ollamaBaseUrl: "",
|
|
39
|
-
};
|
|
40
|
-
log.info("啟動瀏覽器...");
|
|
41
|
-
const { browser, context, page } = await (0, auth_js_1.launchAuthenticated)(config, log);
|
|
42
|
-
try {
|
|
43
|
-
const session = await (0, session_js_1.extractSessionInfo)(page, config, log);
|
|
44
|
-
return { log, page, session, browser, context };
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
await context.close();
|
|
48
|
-
await browser.close();
|
|
49
|
-
throw err;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// Helper to download a single resource
|
|
53
|
-
async function downloadResource(page, resource, outputDir, log) {
|
|
16
|
+
// Helper to download a single resource via HTTP (no browser needed)
|
|
17
|
+
async function downloadResourceHttp(resource, outputDir, log, token) {
|
|
54
18
|
try {
|
|
55
19
|
// Only download resource type (skip url)
|
|
56
20
|
if (resource.modType !== "resource") {
|
|
57
21
|
log.debug(` Skipping ${resource.modType}: ${resource.name}`);
|
|
58
22
|
return null;
|
|
59
23
|
}
|
|
60
|
-
// Create course directory
|
|
61
24
|
const courseDir = node_path_1.default.join(outputDir, (0, utils_js_1.sanitizeFilename)(resource.course_name));
|
|
62
25
|
await node_fs_1.default.promises.mkdir(courseDir, { recursive: true });
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
await page.goto(resource.url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
66
|
-
// Try to find download link on the page
|
|
67
|
-
const downloadLinks = await page.$$eval('a[href*="forcedownload=1"]', (links) => links.map((a) => a.href));
|
|
68
|
-
if (downloadLinks.length === 0) {
|
|
69
|
-
log.warn(` No download link found for: ${resource.name}`);
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
// Download the first available file
|
|
73
|
-
const downloadUrl = downloadLinks[0];
|
|
74
|
-
// Extract filename from URL or use resource name
|
|
75
|
-
const urlObj = new URL(downloadUrl);
|
|
76
|
-
const filenameParam = urlObj.searchParams.get("filename");
|
|
77
|
-
let filename = filenameParam || (0, utils_js_1.sanitizeFilename)(resource.name);
|
|
78
|
-
// Add extension if missing
|
|
26
|
+
// Build filename
|
|
27
|
+
let filename = (0, utils_js_1.sanitizeFilename)(resource.name);
|
|
79
28
|
if (resource.mimetype && !node_path_1.default.extname(filename)) {
|
|
80
29
|
const extMap = {
|
|
81
30
|
"application/pdf": ".pdf",
|
|
@@ -94,33 +43,61 @@ function registerMaterialsCommand(program) {
|
|
|
94
43
|
}
|
|
95
44
|
}
|
|
96
45
|
const outputPath = node_path_1.default.join(courseDir, filename);
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
46
|
+
// Skip if already exists
|
|
47
|
+
if (node_fs_1.default.existsSync(outputPath)) {
|
|
48
|
+
log.debug(` Skipping (exists): ${filename}`);
|
|
49
|
+
const stats = await node_fs_1.default.promises.stat(outputPath);
|
|
50
|
+
return { filename, path: outputPath, size: stats.size, course_id: resource.course_id, course_name: resource.course_name };
|
|
51
|
+
}
|
|
52
|
+
// Download via HTTP with WS token
|
|
53
|
+
const separator = resource.url.includes("?") ? "&" : "?";
|
|
54
|
+
const downloadUrl = `${resource.url}${separator}token=${token}`;
|
|
55
|
+
log.debug(` Downloading: ${resource.name}`);
|
|
56
|
+
const response = await fetch(downloadUrl);
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
log.warn(` Failed to download ${resource.name}: HTTP ${response.status}`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
62
|
+
const data = new Uint8Array(arrayBuffer);
|
|
63
|
+
await node_fs_1.default.promises.writeFile(outputPath, data);
|
|
64
|
+
log.success(` Downloaded: ${filename} (${(0, utils_js_1.formatFileSize)(data.byteLength, 1)} KB)`);
|
|
65
|
+
return { filename, path: outputPath, size: data.byteLength, course_id: resource.course_id, course_name: resource.course_name };
|
|
112
66
|
}
|
|
113
67
|
catch (err) {
|
|
114
68
|
log.warn(` Failed to download ${resource.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
115
69
|
return null;
|
|
116
70
|
}
|
|
117
71
|
}
|
|
72
|
+
// Helper to build material list from API resources
|
|
73
|
+
function buildMaterialsList(courses, apiResources) {
|
|
74
|
+
const courseMap = new Map(courses.map(c => [c.id, c]));
|
|
75
|
+
const materials = [];
|
|
76
|
+
for (const resource of apiResources) {
|
|
77
|
+
const course = courseMap.get(resource.courseId);
|
|
78
|
+
if (course) {
|
|
79
|
+
materials.push({
|
|
80
|
+
course_id: resource.courseId,
|
|
81
|
+
course_name: course.fullname,
|
|
82
|
+
cmid: resource.cmid,
|
|
83
|
+
name: resource.name,
|
|
84
|
+
url: resource.url,
|
|
85
|
+
modType: resource.modType,
|
|
86
|
+
mimetype: resource.mimetype,
|
|
87
|
+
filesize: resource.filesize,
|
|
88
|
+
modified: resource.modified,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return materials;
|
|
93
|
+
}
|
|
118
94
|
materialsCmd
|
|
119
95
|
.command("list-all")
|
|
120
96
|
.description("List all materials/resources across all courses")
|
|
121
97
|
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
122
98
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
123
99
|
.action(async (options, command) => {
|
|
100
|
+
const output = (0, utils_js_1.getOutputFormat)(command);
|
|
124
101
|
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
125
102
|
if (!apiContext) {
|
|
126
103
|
process.exitCode = 1;
|
|
@@ -130,10 +107,8 @@ function registerMaterialsCommand(program) {
|
|
|
130
107
|
const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session, {
|
|
131
108
|
classification,
|
|
132
109
|
});
|
|
133
|
-
// Get materials via WS API (no browser needed!)
|
|
134
110
|
const courseIds = courses.map(c => c.id);
|
|
135
111
|
const apiResources = await (0, moodle_js_1.getResourcesByCoursesApi)(apiContext.session, courseIds);
|
|
136
|
-
// Build a map of courseId -> course for quick lookup
|
|
137
112
|
const courseMap = new Map(courses.map(c => [c.id, c]));
|
|
138
113
|
const allMaterials = [];
|
|
139
114
|
for (const resource of apiResources) {
|
|
@@ -152,178 +127,115 @@ function registerMaterialsCommand(program) {
|
|
|
152
127
|
});
|
|
153
128
|
}
|
|
154
129
|
}
|
|
155
|
-
const
|
|
130
|
+
const items = allMaterials.map(m => ({
|
|
131
|
+
course_id: m.course_id,
|
|
132
|
+
course_name: m.course_name,
|
|
133
|
+
id: m.cmid,
|
|
134
|
+
name: m.name,
|
|
135
|
+
type: m.modType,
|
|
136
|
+
mimetype: m.mimetype,
|
|
137
|
+
filesize: m.filesize,
|
|
138
|
+
modified: m.modified ? new Date(m.modified * 1000).toISOString() : null,
|
|
139
|
+
url: m.url,
|
|
140
|
+
}));
|
|
141
|
+
(0, index_js_1.formatAndOutput)(items, output, apiContext.log, {
|
|
156
142
|
status: "success",
|
|
157
143
|
timestamp: new Date().toISOString(),
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
mimetype: m.mimetype,
|
|
166
|
-
filesize: m.filesize,
|
|
167
|
-
modified: m.modified ? new Date(m.modified * 1000).toISOString() : null,
|
|
168
|
-
url: m.url,
|
|
169
|
-
})),
|
|
170
|
-
summary: {
|
|
171
|
-
total_courses: courses.length,
|
|
172
|
-
total_materials: allMaterials.length,
|
|
173
|
-
by_type: allMaterials.reduce((acc, m) => {
|
|
174
|
-
acc[m.modType] = (acc[m.modType] || 0) + 1;
|
|
175
|
-
return acc;
|
|
176
|
-
}, {}),
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
console.log(JSON.stringify(output));
|
|
144
|
+
total_courses: courses.length,
|
|
145
|
+
total_materials: allMaterials.length,
|
|
146
|
+
by_type: allMaterials.reduce((acc, m) => {
|
|
147
|
+
acc[m.modType] = (acc[m.modType] || 0) + 1;
|
|
148
|
+
return acc;
|
|
149
|
+
}, {}),
|
|
150
|
+
});
|
|
180
151
|
});
|
|
181
152
|
materialsCmd
|
|
182
153
|
.command("download")
|
|
183
|
-
.description("Download all materials from a specific course
|
|
154
|
+
.description("Download all materials from a specific course")
|
|
184
155
|
.argument("<course-id>", "Course ID")
|
|
185
156
|
.option("--output-dir <path>", "Output directory", "./downloads")
|
|
186
157
|
.action(async (courseId, options, command) => {
|
|
187
|
-
const
|
|
188
|
-
if (!
|
|
158
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
159
|
+
if (!apiContext) {
|
|
189
160
|
process.exitCode = 1;
|
|
190
161
|
return;
|
|
191
162
|
}
|
|
192
|
-
const { log,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
// Navigate to course page to find materials
|
|
202
|
-
await page.goto(`https://ilearning.cycu.edu.tw/course/view.php?id=${course.id}`, { waitUntil: "domcontentloaded" });
|
|
203
|
-
// Find all resource links
|
|
204
|
-
const materials = [];
|
|
205
|
-
const resourceLinks = await page.$$eval('a[href*="/mod/resource/view.php"]', (links) => {
|
|
206
|
-
return links.map((a) => ({
|
|
207
|
-
url: a.href,
|
|
208
|
-
name: a.textContent?.trim() || "",
|
|
209
|
-
}));
|
|
210
|
-
});
|
|
211
|
-
for (const link of resourceLinks) {
|
|
212
|
-
const cmidMatch = link.url.match(/id=(\d+)/);
|
|
213
|
-
if (cmidMatch) {
|
|
214
|
-
materials.push({
|
|
215
|
-
course_id: course.id,
|
|
216
|
-
course_name: course.fullname,
|
|
217
|
-
cmid: cmidMatch[1],
|
|
218
|
-
name: link.name,
|
|
219
|
-
url: link.url,
|
|
220
|
-
modType: "resource",
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
log.info(`Found ${materials.length} materials in course: ${course.fullname}`);
|
|
225
|
-
const downloadedFiles = [];
|
|
226
|
-
for (const material of materials) {
|
|
227
|
-
const result = await downloadResource(page, material, options.outputDir, log);
|
|
228
|
-
if (result) {
|
|
229
|
-
downloadedFiles.push(result);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
const summary = {
|
|
233
|
-
total_materials: materials.length,
|
|
234
|
-
downloaded: downloadedFiles.length,
|
|
235
|
-
skipped: materials.length - downloadedFiles.length,
|
|
236
|
-
total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
|
|
237
|
-
};
|
|
238
|
-
const output = {
|
|
239
|
-
status: "success",
|
|
240
|
-
timestamp: new Date().toISOString(),
|
|
241
|
-
downloaded_files: downloadedFiles.map(f => ({
|
|
242
|
-
filename: f.filename,
|
|
243
|
-
path: f.path,
|
|
244
|
-
size: f.size,
|
|
245
|
-
course_id: f.course_id,
|
|
246
|
-
course_name: f.course_name,
|
|
247
|
-
})),
|
|
248
|
-
summary,
|
|
249
|
-
};
|
|
250
|
-
console.log(JSON.stringify(output));
|
|
163
|
+
const { log, session } = apiContext;
|
|
164
|
+
const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(session);
|
|
165
|
+
const course = courses.find((c) => c.id === parseInt(courseId, 10));
|
|
166
|
+
if (!course) {
|
|
167
|
+
log.error(`Course not found: ${courseId}`);
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
return;
|
|
251
170
|
}
|
|
252
|
-
|
|
253
|
-
|
|
171
|
+
const apiResources = await (0, moodle_js_1.getResourcesByCoursesApi)(session, [course.id]);
|
|
172
|
+
const materials = buildMaterialsList(courses, apiResources);
|
|
173
|
+
log.info(`Found ${materials.length} materials in course: ${course.fullname}`);
|
|
174
|
+
const downloadedFiles = [];
|
|
175
|
+
for (const material of materials) {
|
|
176
|
+
const result = await downloadResourceHttp(material, options.outputDir, log, session.wsToken);
|
|
177
|
+
if (result) {
|
|
178
|
+
downloadedFiles.push(result);
|
|
179
|
+
}
|
|
254
180
|
}
|
|
181
|
+
const items = downloadedFiles.map(f => ({
|
|
182
|
+
filename: f.filename,
|
|
183
|
+
path: f.path,
|
|
184
|
+
size: f.size,
|
|
185
|
+
course_id: f.course_id,
|
|
186
|
+
course_name: f.course_name,
|
|
187
|
+
}));
|
|
188
|
+
(0, index_js_1.formatAndOutput)(items, "json", log, {
|
|
189
|
+
status: "success",
|
|
190
|
+
timestamp: new Date().toISOString(),
|
|
191
|
+
total_materials: materials.length,
|
|
192
|
+
downloaded: downloadedFiles.length,
|
|
193
|
+
skipped: materials.length - downloadedFiles.length,
|
|
194
|
+
total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
|
|
195
|
+
});
|
|
255
196
|
});
|
|
256
197
|
materialsCmd
|
|
257
198
|
.command("download-all")
|
|
258
|
-
.description("Download all materials from all courses
|
|
199
|
+
.description("Download all materials from all courses")
|
|
259
200
|
.option("--output-dir <path>", "Output directory", "./downloads")
|
|
260
201
|
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
261
202
|
.action(async (options, command) => {
|
|
262
|
-
const
|
|
263
|
-
if (!
|
|
203
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
204
|
+
if (!apiContext) {
|
|
264
205
|
process.exitCode = 1;
|
|
265
206
|
return;
|
|
266
207
|
}
|
|
267
|
-
const { log,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
});
|
|
281
|
-
for (const link of resourceLinks) {
|
|
282
|
-
const cmidMatch = link.url.match(/id=(\d+)/);
|
|
283
|
-
if (cmidMatch) {
|
|
284
|
-
allMaterials.push({
|
|
285
|
-
course_id: course.id,
|
|
286
|
-
course_name: course.fullname,
|
|
287
|
-
cmid: cmidMatch[1],
|
|
288
|
-
name: link.name,
|
|
289
|
-
url: link.url,
|
|
290
|
-
modType: "resource",
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
log.info(`Found ${allMaterials.length} materials across ${courses.length} courses`);
|
|
296
|
-
const downloadedFiles = [];
|
|
297
|
-
for (const material of allMaterials) {
|
|
298
|
-
const result = await downloadResource(page, material, options.outputDir, log);
|
|
299
|
-
if (result) {
|
|
300
|
-
downloadedFiles.push(result);
|
|
301
|
-
}
|
|
208
|
+
const { log, session } = apiContext;
|
|
209
|
+
const classification = options.level === "all" ? undefined : "inprogress";
|
|
210
|
+
const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(session, { classification });
|
|
211
|
+
log.info(`Scanning ${courses.length} courses for materials...`);
|
|
212
|
+
const courseIds = courses.map((c) => c.id);
|
|
213
|
+
const apiResources = await (0, moodle_js_1.getResourcesByCoursesApi)(session, courseIds);
|
|
214
|
+
const allMaterials = buildMaterialsList(courses, apiResources);
|
|
215
|
+
log.info(`Found ${allMaterials.length} materials across ${courses.length} courses`);
|
|
216
|
+
const downloadedFiles = [];
|
|
217
|
+
for (const material of allMaterials) {
|
|
218
|
+
const result = await downloadResourceHttp(material, options.outputDir, log, session.wsToken);
|
|
219
|
+
if (result) {
|
|
220
|
+
downloadedFiles.push(result);
|
|
302
221
|
}
|
|
303
|
-
const summary = {
|
|
304
|
-
total_courses: courses.length,
|
|
305
|
-
total_materials: allMaterials.length,
|
|
306
|
-
downloaded: downloadedFiles.length,
|
|
307
|
-
skipped: allMaterials.length - downloadedFiles.length,
|
|
308
|
-
total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
|
|
309
|
-
};
|
|
310
|
-
const output = {
|
|
311
|
-
status: "success",
|
|
312
|
-
timestamp: new Date().toISOString(),
|
|
313
|
-
downloaded_files: downloadedFiles.map(f => ({
|
|
314
|
-
filename: f.filename,
|
|
315
|
-
path: f.path,
|
|
316
|
-
size: f.size,
|
|
317
|
-
course_id: f.course_id,
|
|
318
|
-
course_name: f.course_name,
|
|
319
|
-
})),
|
|
320
|
-
summary,
|
|
321
|
-
};
|
|
322
|
-
console.log(JSON.stringify(output));
|
|
323
|
-
}
|
|
324
|
-
finally {
|
|
325
|
-
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
326
222
|
}
|
|
223
|
+
const items = downloadedFiles.map(f => ({
|
|
224
|
+
filename: f.filename,
|
|
225
|
+
path: f.path,
|
|
226
|
+
size: f.size,
|
|
227
|
+
course_id: f.course_id,
|
|
228
|
+
course_name: f.course_name,
|
|
229
|
+
}));
|
|
230
|
+
(0, index_js_1.formatAndOutput)(items, "json", log, {
|
|
231
|
+
status: "success",
|
|
232
|
+
timestamp: new Date().toISOString(),
|
|
233
|
+
total_courses: courses.length,
|
|
234
|
+
total_materials: allMaterials.length,
|
|
235
|
+
downloaded: downloadedFiles.length,
|
|
236
|
+
skipped: allMaterials.length - downloadedFiles.length,
|
|
237
|
+
total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
|
|
238
|
+
});
|
|
327
239
|
});
|
|
328
240
|
materialsCmd
|
|
329
241
|
.command("complete")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quizzes.d.ts","sourceRoot":"","sources":["../../../src/src/commands/quizzes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"quizzes.d.ts","sourceRoot":"","sources":["../../../src/src/commands/quizzes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsEpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2N7D"}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.registerQuizzesCommand = registerQuizzesCommand;
|
|
7
4
|
const utils_js_1 = require("../lib/utils.js");
|
|
8
5
|
const moodle_js_1 = require("../lib/moodle.js");
|
|
9
|
-
const
|
|
6
|
+
const auth_js_1 = require("../lib/auth.js");
|
|
10
7
|
const index_js_1 = require("../index.js");
|
|
11
|
-
const token_js_1 = require("../lib/token.js");
|
|
12
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
8
|
function stripHtmlKeepLines(html) {
|
|
15
9
|
return html
|
|
16
10
|
.replace(/<br\s*\/?>/gi, "\n")
|
|
@@ -47,41 +41,24 @@ function parseSavedAnswer(html) {
|
|
|
47
41
|
return textMatch[1];
|
|
48
42
|
return null;
|
|
49
43
|
}
|
|
44
|
+
function parseQuizQuestions(questions) {
|
|
45
|
+
return Object.values(questions).map((q) => {
|
|
46
|
+
const parsed = parseQuestionHtml(q.html ?? "");
|
|
47
|
+
const savedAnswer = parseSavedAnswer(q.html ?? "");
|
|
48
|
+
return {
|
|
49
|
+
slot: q.slot,
|
|
50
|
+
type: q.type,
|
|
51
|
+
status: q.status,
|
|
52
|
+
stateclass: q.stateclass,
|
|
53
|
+
savedAnswer,
|
|
54
|
+
question: parsed.text,
|
|
55
|
+
options: parsed.options,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
50
59
|
function registerQuizzesCommand(program) {
|
|
51
60
|
const quizzesCmd = program.command("quizzes");
|
|
52
61
|
quizzesCmd.description("Quiz operations");
|
|
53
|
-
function getOutputFormat(command) {
|
|
54
|
-
const opts = command.optsWithGlobals();
|
|
55
|
-
return opts.output || "json";
|
|
56
|
-
}
|
|
57
|
-
// Pure API context - no browser required (fast!)
|
|
58
|
-
async function createApiContext(options, command) {
|
|
59
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
60
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
61
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
62
|
-
const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
|
|
63
|
-
const baseDir = (0, utils_js_1.getBaseDir)();
|
|
64
|
-
const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
|
|
65
|
-
// Check if session exists
|
|
66
|
-
if (!node_fs_1.default.existsSync(sessionPath)) {
|
|
67
|
-
log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
|
|
68
|
-
log.info(`Session 預期位置: ${sessionPath}`);
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
// Try to load WS token
|
|
72
|
-
const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
|
|
73
|
-
if (!wsToken) {
|
|
74
|
-
log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
log,
|
|
79
|
-
session: {
|
|
80
|
-
wsToken,
|
|
81
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
62
|
quizzesCmd
|
|
86
63
|
.command("list")
|
|
87
64
|
.description("List incomplete quizzes in a course")
|
|
@@ -89,8 +66,8 @@ function registerQuizzesCommand(program) {
|
|
|
89
66
|
.option("--all", "Include completed quizzes")
|
|
90
67
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
91
68
|
.action(async (courseId, options, command) => {
|
|
92
|
-
const output = getOutputFormat(command);
|
|
93
|
-
const apiContext = await createApiContext(options, command);
|
|
69
|
+
const output = (0, utils_js_1.getOutputFormat)(command);
|
|
70
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
94
71
|
if (!apiContext) {
|
|
95
72
|
process.exitCode = 1;
|
|
96
73
|
return;
|
|
@@ -111,8 +88,8 @@ function registerQuizzesCommand(program) {
|
|
|
111
88
|
.option("--all", "Include completed quizzes")
|
|
112
89
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
113
90
|
.action(async (options, command) => {
|
|
114
|
-
const output = getOutputFormat(command);
|
|
115
|
-
const apiContext = await createApiContext(options, command);
|
|
91
|
+
const output = (0, utils_js_1.getOutputFormat)(command);
|
|
92
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
116
93
|
if (!apiContext) {
|
|
117
94
|
process.exitCode = 1;
|
|
118
95
|
return;
|
|
@@ -152,16 +129,20 @@ function registerQuizzesCommand(program) {
|
|
|
152
129
|
.argument("<quiz-id>", "Quiz ID")
|
|
153
130
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
154
131
|
.action(async (quizCmid, options, command) => {
|
|
155
|
-
const output = getOutputFormat(command);
|
|
156
|
-
const apiContext = await createApiContext(options, command);
|
|
132
|
+
const output = (0, utils_js_1.getOutputFormat)(command);
|
|
133
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
157
134
|
if (!apiContext) {
|
|
158
135
|
process.exitCode = 1;
|
|
159
136
|
return;
|
|
160
137
|
}
|
|
161
138
|
try {
|
|
162
139
|
const result = await (0, moodle_js_1.startQuizAttemptApi)(apiContext.session, quizCmid);
|
|
140
|
+
apiContext.log.success(`Quiz attempt ${result.attempt.attemptid} started.`);
|
|
141
|
+
const attemptId = result.attempt.attemptid;
|
|
142
|
+
const data = await (0, moodle_js_1.getAllQuizAttemptDataApi)(apiContext.session, attemptId);
|
|
143
|
+
const questions = parseQuizQuestions(data.questions);
|
|
163
144
|
const outputData = [{
|
|
164
|
-
attemptId
|
|
145
|
+
attemptId,
|
|
165
146
|
quizId: result.attempt.quizid,
|
|
166
147
|
state: result.attempt.state,
|
|
167
148
|
timeStart: (0, utils_js_1.formatTimestamp)(result.attempt.timestart),
|
|
@@ -169,8 +150,9 @@ function registerQuizzesCommand(program) {
|
|
|
169
150
|
? (0, utils_js_1.formatTimestamp)(result.attempt.timefinish)
|
|
170
151
|
: null,
|
|
171
152
|
isPreview: result.attempt.preview,
|
|
153
|
+
totalQuestions: questions.length,
|
|
154
|
+
questions,
|
|
172
155
|
}];
|
|
173
|
-
apiContext.log.success(`Quiz attempt ${result.attempt.attemptid} started.`);
|
|
174
156
|
(0, index_js_1.formatAndOutput)(outputData, output, apiContext.log);
|
|
175
157
|
}
|
|
176
158
|
catch (error) {
|
|
@@ -182,30 +164,21 @@ function registerQuizzesCommand(program) {
|
|
|
182
164
|
.command("info")
|
|
183
165
|
.description("Get quiz attempt data and questions")
|
|
184
166
|
.argument("<attempt-id>", "Quiz attempt ID")
|
|
185
|
-
.option("--page <number>", "Page number", "
|
|
167
|
+
.option("--page <number>", "Page number (-1 for all pages)", "-1")
|
|
186
168
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
187
169
|
.action(async (attemptId, options, command) => {
|
|
188
|
-
const output = getOutputFormat(command);
|
|
189
|
-
const apiContext = await createApiContext(options, command);
|
|
170
|
+
const output = (0, utils_js_1.getOutputFormat)(command);
|
|
171
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
190
172
|
if (!apiContext) {
|
|
191
173
|
process.exitCode = 1;
|
|
192
174
|
return;
|
|
193
175
|
}
|
|
194
176
|
try {
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
number: q.questionnumber ?? q.slot,
|
|
201
|
-
type: q.type,
|
|
202
|
-
status: q.status,
|
|
203
|
-
stateclass: q.stateclass,
|
|
204
|
-
savedAnswer,
|
|
205
|
-
question: parsed.text,
|
|
206
|
-
options: parsed.options,
|
|
207
|
-
};
|
|
208
|
-
});
|
|
177
|
+
const pageNumber = parseInt(options.page);
|
|
178
|
+
const data = pageNumber === -1
|
|
179
|
+
? await (0, moodle_js_1.getAllQuizAttemptDataApi)(apiContext.session, parseInt(attemptId))
|
|
180
|
+
: await (0, moodle_js_1.getQuizAttemptDataApi)(apiContext.session, parseInt(attemptId), pageNumber);
|
|
181
|
+
const questions = parseQuizQuestions(data.questions);
|
|
209
182
|
const outputData = [{
|
|
210
183
|
attemptId: data.attempt.attemptid,
|
|
211
184
|
quizId: data.attempt.quizid,
|
|
@@ -229,8 +202,8 @@ function registerQuizzesCommand(program) {
|
|
|
229
202
|
.option("--submit", "Submit the attempt after saving")
|
|
230
203
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
231
204
|
.action(async (attemptId, answersJson, options, command) => {
|
|
232
|
-
const output = getOutputFormat(command);
|
|
233
|
-
const apiContext = await createApiContext(options, command);
|
|
205
|
+
const output = (0, utils_js_1.getOutputFormat)(command);
|
|
206
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
234
207
|
if (!apiContext) {
|
|
235
208
|
process.exitCode = 1;
|
|
236
209
|
return;
|
|
@@ -246,7 +219,7 @@ function registerQuizzesCommand(program) {
|
|
|
246
219
|
}
|
|
247
220
|
try {
|
|
248
221
|
// Get attempt data to find uniqueid and sequencecheck values
|
|
249
|
-
const attemptData = await (0, moodle_js_1.
|
|
222
|
+
const attemptData = await (0, moodle_js_1.getAllQuizAttemptDataApi)(apiContext.session, parseInt(attemptId));
|
|
250
223
|
const uniqueId = attemptData.attempt.uniqueid ?? attemptData.attempt.attemptid;
|
|
251
224
|
const sequenceChecks = new Map();
|
|
252
225
|
for (const q of Object.values(attemptData.questions)) {
|
|
@@ -7,6 +7,7 @@ exports.registerSkillsCommand = registerSkillsCommand;
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const node_url_1 = require("node:url");
|
|
10
11
|
const SKILL_NAME = "openape";
|
|
11
12
|
const GITHUB_RAW_URL = `https://raw.githubusercontent.com/mo7yw4ng/openape/refs/heads/main/skills/${SKILL_NAME}/SKILL.md`;
|
|
12
13
|
/**
|
|
@@ -24,11 +25,10 @@ const PLATFORMS = {
|
|
|
24
25
|
async function readSkillContent() {
|
|
25
26
|
// Try local path first (relative to this file's location)
|
|
26
27
|
try {
|
|
27
|
-
const base = node_path_1.default.dirname(
|
|
28
|
-
const normalized = process.platform === "win32" ? base.replace(/^\//, "") : base;
|
|
28
|
+
const base = node_path_1.default.dirname((0, node_url_1.fileURLToPath)(globalThis[Symbol.for("import-meta-ponyfill-commonjs")](require, module).url));
|
|
29
29
|
// When running from source: src/commands/ → ../../skills/openape/SKILL.md
|
|
30
30
|
// When bundled by dnt into build/: esm/commands/ or script/ → ../../skills/openape/SKILL.md
|
|
31
|
-
const localPath = node_path_1.default.resolve(
|
|
31
|
+
const localPath = node_path_1.default.resolve(base, "..", "..", "skills", SKILL_NAME, "SKILL.md");
|
|
32
32
|
return await node_fs_1.default.promises.readFile(localPath, "utf-8");
|
|
33
33
|
}
|
|
34
34
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/src/commands/upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/src/commands/upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwD5D"}
|