@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.
Files changed (81) hide show
  1. package/README.md +11 -8
  2. package/esm/deno.js +1 -1
  3. package/esm/src/commands/announcements.d.ts.map +1 -1
  4. package/esm/src/commands/announcements.js +22 -85
  5. package/esm/src/commands/assignments.d.ts.map +1 -1
  6. package/esm/src/commands/assignments.js +2 -3
  7. package/esm/src/commands/auth.d.ts.map +1 -1
  8. package/esm/src/commands/auth.js +24 -14
  9. package/esm/src/commands/calendar.d.ts.map +1 -1
  10. package/esm/src/commands/calendar.js +32 -84
  11. package/esm/src/commands/courses.d.ts.map +1 -1
  12. package/esm/src/commands/courses.js +2 -38
  13. package/esm/src/commands/forums.d.ts.map +1 -1
  14. package/esm/src/commands/forums.js +47 -175
  15. package/esm/src/commands/grades.d.ts.map +1 -1
  16. package/esm/src/commands/grades.js +10 -47
  17. package/esm/src/commands/materials.d.ts.map +1 -1
  18. package/esm/src/commands/materials.js +135 -223
  19. package/esm/src/commands/quizzes.d.ts.map +1 -1
  20. package/esm/src/commands/quizzes.js +32 -56
  21. package/esm/src/commands/skills.js +3 -3
  22. package/esm/src/commands/upload.d.ts.map +1 -1
  23. package/esm/src/commands/upload.js +2 -5
  24. package/esm/src/commands/videos.d.ts.map +1 -1
  25. package/esm/src/commands/videos.js +6 -76
  26. package/esm/src/index.d.ts +2 -1
  27. package/esm/src/index.d.ts.map +1 -1
  28. package/esm/src/index.js +5 -1
  29. package/esm/src/lib/auth.d.ts +21 -2
  30. package/esm/src/lib/auth.d.ts.map +1 -1
  31. package/esm/src/lib/auth.js +79 -20
  32. package/esm/src/lib/logger.d.ts +2 -2
  33. package/esm/src/lib/logger.d.ts.map +1 -1
  34. package/esm/src/lib/logger.js +6 -4
  35. package/esm/src/lib/moodle.d.ts +18 -0
  36. package/esm/src/lib/moodle.d.ts.map +1 -1
  37. package/esm/src/lib/moodle.js +54 -2
  38. package/esm/src/lib/utils.d.ts +3 -8
  39. package/esm/src/lib/utils.d.ts.map +1 -1
  40. package/esm/src/lib/utils.js +3 -10
  41. package/package.json +1 -1
  42. package/script/deno.js +1 -1
  43. package/script/src/commands/announcements.d.ts.map +1 -1
  44. package/script/src/commands/announcements.js +23 -89
  45. package/script/src/commands/assignments.d.ts.map +1 -1
  46. package/script/src/commands/assignments.js +2 -3
  47. package/script/src/commands/auth.d.ts.map +1 -1
  48. package/script/src/commands/auth.js +24 -14
  49. package/script/src/commands/calendar.d.ts.map +1 -1
  50. package/script/src/commands/calendar.js +33 -85
  51. package/script/src/commands/courses.d.ts.map +1 -1
  52. package/script/src/commands/courses.js +9 -48
  53. package/script/src/commands/forums.d.ts.map +1 -1
  54. package/script/src/commands/forums.js +50 -181
  55. package/script/src/commands/grades.d.ts.map +1 -1
  56. package/script/src/commands/grades.js +14 -54
  57. package/script/src/commands/materials.d.ts.map +1 -1
  58. package/script/src/commands/materials.js +132 -220
  59. package/script/src/commands/quizzes.d.ts.map +1 -1
  60. package/script/src/commands/quizzes.js +40 -67
  61. package/script/src/commands/skills.js +3 -3
  62. package/script/src/commands/upload.d.ts.map +1 -1
  63. package/script/src/commands/upload.js +2 -5
  64. package/script/src/commands/videos.d.ts.map +1 -1
  65. package/script/src/commands/videos.js +11 -81
  66. package/script/src/index.d.ts +2 -1
  67. package/script/src/index.d.ts.map +1 -1
  68. package/script/src/index.js +5 -1
  69. package/script/src/lib/auth.d.ts +21 -2
  70. package/script/src/lib/auth.d.ts.map +1 -1
  71. package/script/src/lib/auth.js +83 -56
  72. package/script/src/lib/logger.d.ts +2 -2
  73. package/script/src/lib/logger.d.ts.map +1 -1
  74. package/script/src/lib/logger.js +6 -4
  75. package/script/src/lib/moodle.d.ts +18 -0
  76. package/script/src/lib/moodle.d.ts.map +1 -1
  77. package/script/src/lib/moodle.js +56 -2
  78. package/script/src/lib/utils.d.ts +3 -8
  79. package/script/src/lib/utils.d.ts.map +1 -1
  80. package/script/src/lib/utils.js +3 -11
  81. 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 function to create session context (for download commands)
20
- async function createSessionContext(options, command) {
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
- // Navigate to resource page
64
- log.debug(` Downloading: ${resource.name}`);
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
- // Trigger download
98
- const downloadPromise = page.waitForEvent("download", { timeout: 30000 });
99
- await page.goto(downloadUrl, { waitUntil: "domcontentloaded" });
100
- const download = await downloadPromise;
101
- // Save file
102
- await download.saveAs(outputPath);
103
- const stats = await node_fs_1.default.promises.stat(outputPath);
104
- log.success(` Downloaded: ${filename} (${(0, utils_js_1.formatFileSize)(stats.size, 1)} KB)`);
105
- return {
106
- filename,
107
- path: outputPath,
108
- size: stats.size,
109
- course_id: resource.course_id,
110
- course_name: resource.course_name,
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 output = {
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
- level: options.level,
159
- materials: allMaterials.map(m => ({
160
- course_id: m.course_id,
161
- course_name: m.course_name,
162
- id: m.cmid,
163
- name: m.name,
164
- type: m.modType,
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 (requires browser)")
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 context = await createSessionContext(options, command);
188
- if (!context) {
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, page, session, browser, context: browserContext } = context;
193
- try {
194
- const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log);
195
- const course = courses.find(c => c.id === parseInt(courseId, 10));
196
- if (!course) {
197
- log.error(`Course not found: ${courseId}`);
198
- process.exitCode = 1;
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
- finally {
253
- await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
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 (requires browser)")
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 context = await createSessionContext(options, command);
263
- if (!context) {
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, page, session, browser, context: browserContext } = context;
268
- try {
269
- const classification = options.level === "all" ? undefined : "inprogress";
270
- const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log, { classification });
271
- log.info(`Scanning ${courses.length} courses for materials...`);
272
- const allMaterials = [];
273
- for (const course of courses) {
274
- await page.goto(`https://ilearning.cycu.edu.tw/course/view.php?id=${course.id}`, { waitUntil: "domcontentloaded" });
275
- const resourceLinks = await page.$$eval('a[href*="/mod/resource/view.php"]', (links) => {
276
- return links.map((a) => ({
277
- url: a.href,
278
- name: a.textContent?.trim() || "",
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;AAwDpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0Q7D"}
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 logger_js_1 = require("../lib/logger.js");
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: result.attempt.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", "0")
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 data = await (0, moodle_js_1.getQuizAttemptDataApi)(apiContext.session, parseInt(attemptId), parseInt(options.page));
196
- const questions = Object.values(data.questions).map((q) => {
197
- const parsed = parseQuestionHtml(q.html ?? "");
198
- const savedAnswer = parseSavedAnswer(q.html ?? "");
199
- return {
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.getQuizAttemptDataApi)(apiContext.session, parseInt(attemptId), 0);
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(new URL(globalThis[Symbol.for("import-meta-ponyfill-commonjs")](require, module).url).pathname);
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(normalized, "..", "..", "skills", SKILL_NAME, "SKILL.md");
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,CA2D5D"}
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"}