@mo7yw4ng/openape 1.0.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/esm/_dnt.polyfills.d.ts +24 -0
  4. package/esm/_dnt.polyfills.js +1 -0
  5. package/esm/_dnt.shims.d.ts +5 -0
  6. package/esm/_dnt.shims.js +61 -0
  7. package/esm/deno.d.ts +23 -0
  8. package/esm/deno.js +22 -0
  9. package/esm/package.json +3 -0
  10. package/package.json +34 -0
  11. package/script/_dnt.polyfills.d.ts +24 -0
  12. package/script/_dnt.polyfills.js +2 -0
  13. package/script/_dnt.shims.d.ts +5 -0
  14. package/script/_dnt.shims.js +65 -0
  15. package/script/deno.d.ts +23 -0
  16. package/script/deno.js +24 -0
  17. package/script/package.json +3 -0
  18. package/script/src/commands/announcements.d.ts +2 -0
  19. package/script/src/commands/announcements.js +288 -0
  20. package/script/src/commands/auth.d.ts +2 -0
  21. package/script/src/commands/auth.js +238 -0
  22. package/script/src/commands/calendar.d.ts +2 -0
  23. package/script/src/commands/calendar.js +375 -0
  24. package/script/src/commands/courses.d.ts +2 -0
  25. package/script/src/commands/courses.js +484 -0
  26. package/script/src/commands/forums.d.ts +2 -0
  27. package/script/src/commands/forums.js +403 -0
  28. package/script/src/commands/grades.d.ts +2 -0
  29. package/script/src/commands/grades.js +259 -0
  30. package/script/src/commands/materials.d.ts +2 -0
  31. package/script/src/commands/materials.js +423 -0
  32. package/script/src/commands/quizzes.d.ts +2 -0
  33. package/script/src/commands/quizzes.js +228 -0
  34. package/script/src/commands/skills.d.ts +2 -0
  35. package/script/src/commands/skills.js +117 -0
  36. package/script/src/commands/videos.d.ts +2 -0
  37. package/script/src/commands/videos.js +334 -0
  38. package/script/src/index.d.ts +26 -0
  39. package/script/src/index.js +156 -0
  40. package/script/src/lib/auth.d.ts +24 -0
  41. package/script/src/lib/auth.js +203 -0
  42. package/script/src/lib/config.d.ts +5 -0
  43. package/script/src/lib/config.js +43 -0
  44. package/script/src/lib/logger.d.ts +2 -0
  45. package/script/src/lib/logger.js +28 -0
  46. package/script/src/lib/moodle.d.ts +234 -0
  47. package/script/src/lib/moodle.js +966 -0
  48. package/script/src/lib/session.d.ts +7 -0
  49. package/script/src/lib/session.js +71 -0
  50. package/script/src/lib/token.d.ts +27 -0
  51. package/script/src/lib/token.js +154 -0
  52. package/script/src/lib/types.d.ts +261 -0
  53. package/script/src/lib/types.js +2 -0
  54. package/script/src/lib/utils.d.ts +5 -0
  55. package/script/src/lib/utils.js +43 -0
  56. package/skills/openape/SKILL.md +328 -0
@@ -0,0 +1,484 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerCoursesCommand = void 0;
7
+ const utils_js_1 = require("../lib/utils.js");
8
+ const moodle_js_1 = require("../lib/moodle.js");
9
+ const logger_js_1 = require("../lib/logger.js");
10
+ const auth_js_1 = require("../lib/auth.js");
11
+ const session_js_1 = require("../lib/session.js");
12
+ const token_js_1 = require("../lib/token.js");
13
+ const index_js_1 = require("../index.js");
14
+ const path_1 = __importDefault(require("path"));
15
+ const fs_1 = __importDefault(require("fs"));
16
+ function registerCoursesCommand(program) {
17
+ const coursesCmd = program.command("courses");
18
+ coursesCmd.description("Course operations");
19
+ // Helper to get output format from global or local options
20
+ function getOutputFormat(command) {
21
+ const opts = command.optsWithGlobals();
22
+ return opts.output || "json";
23
+ }
24
+ // Pure API context - no browser required (fast!)
25
+ async function createApiContext(options, command) {
26
+ const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
27
+ const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
28
+ const silent = outputFormat === "json" && !opts.verbose;
29
+ const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
30
+ const baseDir = (0, utils_js_1.getBaseDir)();
31
+ const sessionPath = path_1.default.resolve(baseDir, ".auth", "storage-state.json");
32
+ // Check if session exists
33
+ if (!fs_1.default.existsSync(sessionPath)) {
34
+ return null;
35
+ }
36
+ // Try to load WS token
37
+ const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
38
+ if (!wsToken) {
39
+ return null;
40
+ }
41
+ return {
42
+ log,
43
+ session: {
44
+ wsToken,
45
+ moodleBaseUrl: "https://ilearning.cycu.edu.tw",
46
+ },
47
+ };
48
+ }
49
+ // Helper function to create session context (browser mode - fallback)
50
+ async function createSessionContext(options, command) {
51
+ // Get global options if command is provided (for --verbose, --silent flags)
52
+ const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
53
+ // Auto-enable silent mode for JSON output (unless --verbose is also set)
54
+ const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
55
+ const silent = outputFormat === "json" && !opts.verbose;
56
+ const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
57
+ // Determine session path
58
+ const baseDir = (0, utils_js_1.getBaseDir)();
59
+ const sessionPath = path_1.default.resolve(baseDir, ".auth", "storage-state.json");
60
+ // Check if session exists
61
+ if (!fs_1.default.existsSync(sessionPath)) {
62
+ log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
63
+ log.info(`Session 預期位置: ${sessionPath}`);
64
+ return null;
65
+ }
66
+ // Create minimal config
67
+ const config = {
68
+ username: "",
69
+ password: "",
70
+ courseUrl: "",
71
+ moodleBaseUrl: "https://ilearning.cycu.edu.tw",
72
+ headless: !options.headed,
73
+ slowMo: 0,
74
+ authStatePath: sessionPath,
75
+ ollamaBaseUrl: "",
76
+ };
77
+ log.info("啟動瀏覽器...");
78
+ const { browser, context, page } = await (0, auth_js_1.launchAuthenticated)(config, log);
79
+ try {
80
+ const session = await (0, session_js_1.extractSessionInfo)(page, config, log);
81
+ return { log, page, session, browser, context };
82
+ }
83
+ catch (err) {
84
+ await context.close();
85
+ await browser.close();
86
+ throw err;
87
+ }
88
+ }
89
+ coursesCmd
90
+ .command("list")
91
+ .description("List enrolled courses")
92
+ .option("--incomplete-only", "Show only incomplete courses")
93
+ .option("--output <format>", "Output format: json|csv|table|silent")
94
+ .option("--level <type>", "Course level: in_progress (default) | past | future | all", "in_progress")
95
+ .action(async (options, command) => {
96
+ const output = getOutputFormat(command);
97
+ // Map level to classification
98
+ const classification = options.level === "all" ? undefined :
99
+ options.level === "past" ? "past" :
100
+ options.level === "future" ? "future" : "inprogress";
101
+ // Try API mode first (no browser, fast!)
102
+ const apiContext = await createApiContext(options, command);
103
+ if (apiContext) {
104
+ try {
105
+ const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session, {
106
+ classification,
107
+ });
108
+ let filteredCourses = courses;
109
+ if (options.incompleteOnly) {
110
+ filteredCourses = courses.filter(c => (c.progress ?? 0) < 100);
111
+ }
112
+ (0, index_js_1.formatAndOutput)(filteredCourses, output, apiContext.log);
113
+ return;
114
+ }
115
+ catch (e) {
116
+ // API failed, fall through to browser mode
117
+ const msg = e instanceof Error ? e.message : String(e);
118
+ console.error(`// API mode failed: ${msg}, trying browser mode...`);
119
+ }
120
+ }
121
+ // Fallback to browser mode
122
+ const context = await createSessionContext(options, command);
123
+ if (!context) {
124
+ process.exitCode = 1;
125
+ return;
126
+ }
127
+ const { log, page, session, browser, context: browserContext } = context;
128
+ try {
129
+ const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log, {
130
+ classification,
131
+ });
132
+ let filteredCourses = courses;
133
+ if (options.incompleteOnly) {
134
+ filteredCourses = courses.filter(c => (c.progress ?? 0) < 100);
135
+ }
136
+ (0, index_js_1.formatAndOutput)(filteredCourses, output, log);
137
+ }
138
+ finally {
139
+ await (0, auth_js_1.closeBrowserSafely)(browser, browserContext);
140
+ }
141
+ });
142
+ coursesCmd
143
+ .command("info")
144
+ .description("Show detailed course information")
145
+ .argument("<course-id>", "Course ID")
146
+ .option("--output <format>", "Output format: json|csv|table|silent")
147
+ .action(async (courseId, options, command) => {
148
+ const output = getOutputFormat(command);
149
+ // Try pure API mode (no browser, fast!)
150
+ const apiContext = await createApiContext(options, command);
151
+ if (apiContext) {
152
+ try {
153
+ const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session);
154
+ const course = courses.find(c => c.id === parseInt(courseId, 10));
155
+ if (!course) {
156
+ apiContext.log.error(`Course not found: ${courseId}`);
157
+ process.exitCode = 1;
158
+ return;
159
+ }
160
+ (0, index_js_1.formatAndOutput)(course, output, apiContext.log);
161
+ return;
162
+ }
163
+ catch (e) {
164
+ // API failed, fall through to browser mode
165
+ const msg = e instanceof Error ? e.message : String(e);
166
+ console.error(`// API mode failed: ${msg}, trying browser mode...`);
167
+ }
168
+ }
169
+ // Fallback to browser mode
170
+ const context = await createSessionContext(options, command);
171
+ if (!context) {
172
+ process.exitCode = 1;
173
+ return;
174
+ }
175
+ const { log, page, session, browser, context: browserContext } = context;
176
+ try {
177
+ const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log);
178
+ const course = courses.find(c => c.id === parseInt(courseId, 10));
179
+ if (!course) {
180
+ log.error(`Course not found: ${courseId}`);
181
+ process.exitCode = 1;
182
+ return;
183
+ }
184
+ (0, index_js_1.formatAndOutput)(course, output, log);
185
+ }
186
+ finally {
187
+ await (0, auth_js_1.closeBrowserSafely)(browser, browserContext);
188
+ }
189
+ });
190
+ coursesCmd
191
+ .command("progress")
192
+ .description("Show course progress")
193
+ .argument("<course-id>", "Course ID")
194
+ .option("--output <format>", "Output format: json|csv|table|silent")
195
+ .action(async (courseId, options, command) => {
196
+ const output = getOutputFormat(command);
197
+ // Try pure API mode (no browser, fast!)
198
+ const apiContext = await createApiContext(options, command);
199
+ if (apiContext) {
200
+ try {
201
+ const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session);
202
+ const course = courses.find(c => c.id === parseInt(courseId, 10));
203
+ if (!course) {
204
+ apiContext.log.error(`Course not found: ${courseId}`);
205
+ process.exitCode = 1;
206
+ return;
207
+ }
208
+ const progressData = {
209
+ courseId: course.id,
210
+ courseName: course.fullname,
211
+ progress: course.progress ?? 0,
212
+ startDate: course.startdate ? new Date(course.startdate * 1000).toISOString() : null,
213
+ endDate: course.enddate ? new Date(course.enddate * 1000).toISOString() : null,
214
+ };
215
+ (0, index_js_1.formatAndOutput)(progressData, output, apiContext.log);
216
+ return;
217
+ }
218
+ catch (e) {
219
+ // API failed, fall through to browser mode
220
+ const msg = e instanceof Error ? e.message : String(e);
221
+ console.error(`// API mode failed: ${msg}, trying browser mode...`);
222
+ }
223
+ }
224
+ // Fallback to browser mode
225
+ const context = await createSessionContext(options, command);
226
+ if (!context) {
227
+ process.exitCode = 1;
228
+ return;
229
+ }
230
+ const { log, page, session, browser, context: browserContext } = context;
231
+ try {
232
+ const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log);
233
+ const course = courses.find(c => c.id === parseInt(courseId, 10));
234
+ if (!course) {
235
+ log.error(`Course not found: ${courseId}`);
236
+ process.exitCode = 1;
237
+ return;
238
+ }
239
+ const progressData = {
240
+ courseId: course.id,
241
+ courseName: course.fullname,
242
+ progress: course.progress ?? 0,
243
+ startDate: course.startdate ? new Date(course.startdate * 1000).toISOString() : null,
244
+ endDate: course.enddate ? new Date(course.enddate * 1000).toISOString() : null,
245
+ };
246
+ (0, index_js_1.formatAndOutput)(progressData, output, log);
247
+ }
248
+ finally {
249
+ await (0, auth_js_1.closeBrowserSafely)(browser, browserContext);
250
+ }
251
+ });
252
+ // Helper function to fetch syllabus from CMAP using GWT-RPC API
253
+ async function fetchSyllabus(shortname) {
254
+ try {
255
+ const parts = shortname.split("_");
256
+ if (parts.length < 2) {
257
+ return { error: "Invalid course shortname format" };
258
+ }
259
+ const [yearTerm, opCode] = parts;
260
+ // Build GWT-RPC request body
261
+ // Format: 7|0|8|<base_url>|<permutation>|<service>|<method>|<param_types>|<params>...|1|2|3|4|3|5|5|5|6|7|8|
262
+ const gwtBody = `7|0|8|https://cmap.cycu.edu.tw:8443/Syllabus/syllabus/|339796D6E7B561A6465F5E9B5F4943FA|com.sanfong.syllabus.shared.SyllabusClientService|findClassTargetByYearAndOpCode|java.lang.String/2004016611|${yearTerm}|${opCode}|zh_TW|1|2|3|4|3|5|5|5|6|7|8|`;
263
+ const response = await fetch("https://cmap.cycu.edu.tw:8443/Syllabus/syllabus/syllabusClientService", {
264
+ method: "POST",
265
+ headers: {
266
+ "X-GWT-Permutation": "339796D6E7B561A6465F5E9B5F4943FA",
267
+ "Accept": "text/x-gwt-rpc, */*; q=0.01",
268
+ "Content-Type": "text/x-gwt-rpc; charset=UTF-8",
269
+ },
270
+ body: gwtBody,
271
+ });
272
+ if (!response.ok) {
273
+ return { error: `HTTP ${response.status}`, url: "https://cmap.cycu.edu.tw:8443/Syllabus/syllabus/syllabusClientService" };
274
+ }
275
+ const rawText = await response.text();
276
+ // GWT-RPC response format: //OK[...data...]
277
+ if (!rawText.startsWith("//OK")) {
278
+ return { error: "Invalid GWT-RPC response", rawResponse: rawText.slice(0, 200) };
279
+ }
280
+ // Extract the JSON array part from the GWT response
281
+ // Response format: //OK[data1,data2,...]
282
+ const content = rawText.slice(4); // Remove "//OK"
283
+ // Parse the GWT string table - GWT uses a special format where strings are escaped
284
+ // Format: ["string1","string2",...] or [123,"string2",...]
285
+ const stringTable = [];
286
+ // Simple parser for GWT string table
287
+ let current = "";
288
+ let inString = false;
289
+ let escaped = false;
290
+ for (let i = 0; i < content.length; i++) {
291
+ const char = content[i];
292
+ if (escaped) {
293
+ // Handle escape sequences
294
+ switch (char) {
295
+ case 'n':
296
+ current += '\n';
297
+ break;
298
+ case 'r':
299
+ current += '\r';
300
+ break;
301
+ case 't':
302
+ current += '\t';
303
+ break;
304
+ case '"':
305
+ current += '"';
306
+ break;
307
+ case '\\':
308
+ current += '\\';
309
+ break;
310
+ case '0':
311
+ current += '\0';
312
+ break;
313
+ default:
314
+ // Unknown escape, just append the char
315
+ current += char;
316
+ }
317
+ escaped = false;
318
+ continue;
319
+ }
320
+ if (char === "\\") {
321
+ escaped = true;
322
+ continue;
323
+ }
324
+ if (char === '"') {
325
+ inString = !inString;
326
+ if (!inString && current.length > 0) {
327
+ stringTable.push(current);
328
+ current = "";
329
+ }
330
+ continue;
331
+ }
332
+ if (inString) {
333
+ current += char;
334
+ }
335
+ }
336
+ // Parse schedule from string table
337
+ // Strategy: Find week numbers (1-18), extract title (previous field) and date
338
+ const schedule = [];
339
+ const datePattern = /^\d{4}-\d{2}-\d{2}$/;
340
+ // Track processed indices to avoid duplicates
341
+ const processedIndices = new Set();
342
+ for (let i = 0; i < stringTable.length; i++) {
343
+ const s = stringTable[i];
344
+ // Look for week numbers (1-18)
345
+ if (/^[1-9]$|^1[0-8]$/.test(s) && !processedIndices.has(i)) {
346
+ const week = s;
347
+ let date = "";
348
+ let title = "";
349
+ // Previous field is the title
350
+ if (i - 1 >= 0 && !processedIndices.has(i - 1)) {
351
+ title = stringTable[i - 1];
352
+ }
353
+ // For week 1 & 2, find date before the week number
354
+ // For week 18, also look before (last week has no "next week")
355
+ // For other weeks (3-17), next field is next week's date
356
+ if (week === "1" || week === "2" || week === "18") {
357
+ // Look backwards for date pattern (search further back for week 18)
358
+ const maxLookback = week === "18" ? 15 : 6;
359
+ for (let j = i - 1; j >= Math.max(0, i - maxLookback); j--) {
360
+ if (datePattern.test(stringTable[j]) && !processedIndices.has(j)) {
361
+ date = stringTable[j];
362
+ processedIndices.add(j);
363
+ break;
364
+ }
365
+ }
366
+ }
367
+ else {
368
+ // Week 3-17: look for next week number, then get date before it
369
+ for (let j = i + 1; j < Math.min(i + 10, stringTable.length); j++) {
370
+ if (/^[1-9]$|^1[0-8]$/.test(stringTable[j]) && !processedIndices.has(j)) {
371
+ // Found next week, look before it for date
372
+ for (let k = j - 1; k >= Math.max(0, j - 6); k--) {
373
+ if (datePattern.test(stringTable[k]) && !processedIndices.has(k)) {
374
+ date = stringTable[k];
375
+ processedIndices.add(k);
376
+ break;
377
+ }
378
+ }
379
+ break;
380
+ }
381
+ }
382
+ }
383
+ // Clean up title
384
+ title = title.trim()
385
+ .replace(/[\r\n]+/g, ' ')
386
+ .replace(/,+$/, '')
387
+ .trim()
388
+ .slice(0, 200);
389
+ // Only add if we have title (date is optional, will be inferred if missing)
390
+ if (title.length > 1) {
391
+ // If no date found, try to infer from the last added date
392
+ if (date.length === 0 && schedule.length > 0) {
393
+ const lastEntry = schedule[schedule.length - 1];
394
+ const lastDate = new Date(lastEntry.date);
395
+ const nextDate = new Date(lastDate.getTime() + 7 * 24 * 60 * 60 * 1000);
396
+ const year = nextDate.getFullYear();
397
+ const month = String(nextDate.getMonth() + 1).padStart(2, "0");
398
+ const day = String(nextDate.getDate()).padStart(2, "0");
399
+ date = `${year}-${month}-${day}`;
400
+ }
401
+ schedule.push({
402
+ week,
403
+ date,
404
+ title,
405
+ });
406
+ }
407
+ processedIndices.add(i);
408
+ }
409
+ }
410
+ // Sort by date to maintain order
411
+ schedule.sort((a, b) => a.date.localeCompare(b.date));
412
+ // Extract course info from string table
413
+ const result = {
414
+ yearTerm,
415
+ opCode,
416
+ url: `https://cmap.cycu.edu.tw:8443/Syllabus/CoursePreview.html?yearTerm=${yearTerm}&opCode=${opCode}&locale=zh_TW`,
417
+ schedule,
418
+ };
419
+ // Try to find instructor (look for common patterns)
420
+ for (let i = 0; i < stringTable.length; i++) {
421
+ const s = stringTable[i];
422
+ if (s.includes("教授") || s.includes("老師") || s.includes("教師") || s.includes("Instructor")) {
423
+ result.instructor = s;
424
+ break;
425
+ }
426
+ }
427
+ return result;
428
+ }
429
+ catch (e) {
430
+ return { error: e instanceof Error ? e.message : String(e) };
431
+ }
432
+ }
433
+ coursesCmd
434
+ .command("syllabus")
435
+ .description("Show course syllabus (from CMAP)")
436
+ .argument("<course-id>", "Course ID")
437
+ .option("--output <format>", "Output format: json|csv|table|silent")
438
+ .action(async (courseId, options, command) => {
439
+ const output = getOutputFormat(command);
440
+ // First get course info to get shortname
441
+ const apiContext = await createApiContext(options, command);
442
+ if (!apiContext) {
443
+ console.error("// Unable to get course info without session");
444
+ process.exitCode = 1;
445
+ return;
446
+ }
447
+ try {
448
+ const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session);
449
+ const course = courses.find(c => c.id === parseInt(courseId, 10));
450
+ if (!course) {
451
+ apiContext.log.error(`Course not found: ${courseId}`);
452
+ process.exitCode = 1;
453
+ return;
454
+ }
455
+ // Fetch syllabus from CMAP
456
+ const syllabus = await fetchSyllabus(course.shortname);
457
+ if (!syllabus) {
458
+ apiContext.log.warn(`Syllabus not found for course: ${course.shortname}`);
459
+ // Return course info at least
460
+ (0, index_js_1.formatAndOutput)({
461
+ courseId: course.id,
462
+ shortname: course.shortname,
463
+ fullname: course.fullname,
464
+ note: "Syllabus not available from CMAP",
465
+ }, output, apiContext.log);
466
+ return;
467
+ }
468
+ // Combine course info with syllabus
469
+ const result = {
470
+ courseId: course.id,
471
+ shortname: course.shortname,
472
+ fullname: course.fullname,
473
+ ...syllabus,
474
+ };
475
+ (0, index_js_1.formatAndOutput)(result, output, apiContext.log);
476
+ }
477
+ catch (e) {
478
+ const msg = e instanceof Error ? e.message : String(e);
479
+ apiContext.log.error(`Error fetching syllabus: ${msg}`);
480
+ process.exitCode = 1;
481
+ }
482
+ });
483
+ }
484
+ exports.registerCoursesCommand = registerCoursesCommand;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerForumsCommand(program: Command): void;