@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.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/esm/_dnt.polyfills.d.ts +24 -0
- package/esm/_dnt.polyfills.js +1 -0
- package/esm/_dnt.shims.d.ts +5 -0
- package/esm/_dnt.shims.js +61 -0
- package/esm/deno.d.ts +23 -0
- package/esm/deno.js +22 -0
- package/esm/package.json +3 -0
- package/package.json +34 -0
- package/script/_dnt.polyfills.d.ts +24 -0
- package/script/_dnt.polyfills.js +2 -0
- package/script/_dnt.shims.d.ts +5 -0
- package/script/_dnt.shims.js +65 -0
- package/script/deno.d.ts +23 -0
- package/script/deno.js +24 -0
- package/script/package.json +3 -0
- package/script/src/commands/announcements.d.ts +2 -0
- package/script/src/commands/announcements.js +288 -0
- package/script/src/commands/auth.d.ts +2 -0
- package/script/src/commands/auth.js +238 -0
- package/script/src/commands/calendar.d.ts +2 -0
- package/script/src/commands/calendar.js +375 -0
- package/script/src/commands/courses.d.ts +2 -0
- package/script/src/commands/courses.js +484 -0
- package/script/src/commands/forums.d.ts +2 -0
- package/script/src/commands/forums.js +403 -0
- package/script/src/commands/grades.d.ts +2 -0
- package/script/src/commands/grades.js +259 -0
- package/script/src/commands/materials.d.ts +2 -0
- package/script/src/commands/materials.js +423 -0
- package/script/src/commands/quizzes.d.ts +2 -0
- package/script/src/commands/quizzes.js +228 -0
- package/script/src/commands/skills.d.ts +2 -0
- package/script/src/commands/skills.js +117 -0
- package/script/src/commands/videos.d.ts +2 -0
- package/script/src/commands/videos.js +334 -0
- package/script/src/index.d.ts +26 -0
- package/script/src/index.js +156 -0
- package/script/src/lib/auth.d.ts +24 -0
- package/script/src/lib/auth.js +203 -0
- package/script/src/lib/config.d.ts +5 -0
- package/script/src/lib/config.js +43 -0
- package/script/src/lib/logger.d.ts +2 -0
- package/script/src/lib/logger.js +28 -0
- package/script/src/lib/moodle.d.ts +234 -0
- package/script/src/lib/moodle.js +966 -0
- package/script/src/lib/session.d.ts +7 -0
- package/script/src/lib/session.js +71 -0
- package/script/src/lib/token.d.ts +27 -0
- package/script/src/lib/token.js +154 -0
- package/script/src/lib/types.d.ts +261 -0
- package/script/src/lib/types.js +2 -0
- package/script/src/lib/utils.d.ts +5 -0
- package/script/src/lib/utils.js +43 -0
- package/skills/openape/SKILL.md +328 -0
|
@@ -0,0 +1,403 @@
|
|
|
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.registerForumsCommand = 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 auth_js_2 = require("../lib/auth.js");
|
|
13
|
+
const index_js_1 = require("../index.js");
|
|
14
|
+
const token_js_1 = require("../lib/token.js");
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
function registerForumsCommand(program) {
|
|
18
|
+
const forumsCmd = program.command("forums");
|
|
19
|
+
forumsCmd.description("Forum operations");
|
|
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
|
|
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, wsToken } = await (0, auth_js_1.launchAuthenticated)(config, log);
|
|
79
|
+
try {
|
|
80
|
+
const session = await (0, session_js_1.extractSessionInfo)(page, config, log, wsToken);
|
|
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
|
+
forumsCmd
|
|
90
|
+
.command("list")
|
|
91
|
+
.description("List forums from in-progress courses")
|
|
92
|
+
.option("--unread-only", "Show only forums with unread discussions")
|
|
93
|
+
.option("--fetch-instance", "Fetch forum instance IDs (slower)")
|
|
94
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
95
|
+
.action(async (options, command) => {
|
|
96
|
+
const output = getOutputFormat(command);
|
|
97
|
+
// Try pure WS API mode (no browser, fast!)
|
|
98
|
+
const apiContext = await createApiContext(options, command);
|
|
99
|
+
if (apiContext) {
|
|
100
|
+
try {
|
|
101
|
+
const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session, {
|
|
102
|
+
classification: "inprogress",
|
|
103
|
+
});
|
|
104
|
+
// Get forums via WS API (no browser needed!)
|
|
105
|
+
const courseIds = courses.map(c => c.id);
|
|
106
|
+
const wsForums = await (0, moodle_js_1.getForumsApi)(apiContext.session, courseIds);
|
|
107
|
+
const allForums = [];
|
|
108
|
+
for (const wsForum of wsForums) {
|
|
109
|
+
const course = courses.find(c => c.id === wsForum.courseid);
|
|
110
|
+
if (course) {
|
|
111
|
+
allForums.push({
|
|
112
|
+
course_id: wsForum.courseid,
|
|
113
|
+
course_name: course.fullname,
|
|
114
|
+
cmid: wsForum.cmid.toString(),
|
|
115
|
+
forum_id: wsForum.id,
|
|
116
|
+
name: wsForum.name,
|
|
117
|
+
url: `https://ilearning.cycu.edu.tw/mod/forum/view.php?id=${wsForum.cmid}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const result = {
|
|
122
|
+
status: "success",
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
forums: allForums,
|
|
125
|
+
summary: {
|
|
126
|
+
total_courses: courses.length,
|
|
127
|
+
total_forums: allForums.length,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
console.log(JSON.stringify(result));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
// API failed, fall through to browser mode
|
|
135
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
136
|
+
console.error(`// API mode failed: ${msg}, trying browser mode...`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Fallback to browser mode
|
|
140
|
+
const context = await createSessionContext(options, command);
|
|
141
|
+
if (!context) {
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const { log, page, session, browser, context: browserContext } = context;
|
|
146
|
+
try {
|
|
147
|
+
const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log, {
|
|
148
|
+
classification: "inprogress",
|
|
149
|
+
});
|
|
150
|
+
const allForums = [];
|
|
151
|
+
for (const course of courses) {
|
|
152
|
+
const forums = await (0, moodle_js_1.getForumsInCourse)(page, session, course.id, log);
|
|
153
|
+
for (const forum of forums) {
|
|
154
|
+
let instance = forum.forumId;
|
|
155
|
+
// Fetch instance ID if requested
|
|
156
|
+
if (options.fetchInstance) {
|
|
157
|
+
log.info(` 正在取得 forum ${forum.cmid} 的 instance ID...`);
|
|
158
|
+
instance = await (0, moodle_js_1.getForumIdFromPage)(page, parseInt(forum.cmid, 10), session) ?? 0;
|
|
159
|
+
}
|
|
160
|
+
allForums.push({
|
|
161
|
+
course_id: course.id,
|
|
162
|
+
course_name: course.fullname,
|
|
163
|
+
cmid: forum.cmid,
|
|
164
|
+
forum_id: instance,
|
|
165
|
+
name: forum.name,
|
|
166
|
+
url: forum.url,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const output = {
|
|
171
|
+
status: "success",
|
|
172
|
+
timestamp: new Date().toISOString(),
|
|
173
|
+
forums: allForums.map(f => ({
|
|
174
|
+
course_id: f.course_id,
|
|
175
|
+
course_name: f.course_name,
|
|
176
|
+
cmid: f.cmid,
|
|
177
|
+
forum_id: f.forum_id,
|
|
178
|
+
name: f.name,
|
|
179
|
+
url: f.url,
|
|
180
|
+
})),
|
|
181
|
+
summary: {
|
|
182
|
+
total_courses: courses.length,
|
|
183
|
+
total_forums: allForums.length,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
console.log(JSON.stringify(output));
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
forumsCmd
|
|
193
|
+
.command("list-all")
|
|
194
|
+
.description("List all forums across all courses")
|
|
195
|
+
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
196
|
+
.option("--unread-only", "Show only forums with unread discussions")
|
|
197
|
+
.option("--fetch-instance", "Fetch forum instance IDs (slower)")
|
|
198
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
199
|
+
.action(async (options, command) => {
|
|
200
|
+
// Try pure WS API mode (no browser, fast!)
|
|
201
|
+
const apiContext = await createApiContext(options, command);
|
|
202
|
+
if (apiContext) {
|
|
203
|
+
try {
|
|
204
|
+
const classification = options.level === "all" ? undefined : "inprogress";
|
|
205
|
+
const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session, {
|
|
206
|
+
classification,
|
|
207
|
+
});
|
|
208
|
+
// Get forums via WS API (no browser needed!)
|
|
209
|
+
const courseIds = courses.map(c => c.id);
|
|
210
|
+
const wsForums = await (0, moodle_js_1.getForumsApi)(apiContext.session, courseIds);
|
|
211
|
+
const allForums = [];
|
|
212
|
+
for (const wsForum of wsForums) {
|
|
213
|
+
const course = courses.find(c => c.id === wsForum.courseid);
|
|
214
|
+
if (course) {
|
|
215
|
+
allForums.push({
|
|
216
|
+
course_id: wsForum.courseid,
|
|
217
|
+
course_name: course.fullname,
|
|
218
|
+
cmid: wsForum.cmid.toString(),
|
|
219
|
+
forum_id: wsForum.id,
|
|
220
|
+
name: wsForum.name,
|
|
221
|
+
url: `https://ilearning.cycu.edu.tw/mod/forum/view.php?id=${wsForum.cmid}`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const result = {
|
|
226
|
+
status: "success",
|
|
227
|
+
timestamp: new Date().toISOString(),
|
|
228
|
+
forums: allForums,
|
|
229
|
+
summary: {
|
|
230
|
+
total_courses: courses.length,
|
|
231
|
+
total_forums: allForums.length,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
console.log(JSON.stringify(result));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
// API failed, fall through to browser mode
|
|
239
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
240
|
+
console.error(`// API mode failed: ${msg}, trying browser mode...`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Fallback to browser mode
|
|
244
|
+
const context = await createSessionContext(options, command);
|
|
245
|
+
if (!context) {
|
|
246
|
+
process.exitCode = 1;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const { log, page, session, browser, context: browserContext } = context;
|
|
250
|
+
try {
|
|
251
|
+
const classification = options.level === "all" ? undefined : "inprogress";
|
|
252
|
+
const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log, { classification });
|
|
253
|
+
const allForums = [];
|
|
254
|
+
for (const course of courses) {
|
|
255
|
+
const forums = await (0, moodle_js_1.getForumsInCourse)(page, session, course.id, log);
|
|
256
|
+
for (const forum of forums) {
|
|
257
|
+
let instance = forum.forumId;
|
|
258
|
+
// Fetch instance ID if requested
|
|
259
|
+
if (options.fetchInstance) {
|
|
260
|
+
log.info(` 正在取得 forum ${forum.cmid} 的 instance ID...`);
|
|
261
|
+
instance = await (0, moodle_js_1.getForumIdFromPage)(page, parseInt(forum.cmid, 10), session) ?? 0;
|
|
262
|
+
}
|
|
263
|
+
allForums.push({
|
|
264
|
+
course_id: course.id,
|
|
265
|
+
course_name: course.fullname,
|
|
266
|
+
cmid: forum.cmid,
|
|
267
|
+
forum_id: instance,
|
|
268
|
+
name: forum.name,
|
|
269
|
+
url: forum.url,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const output = {
|
|
274
|
+
status: "success",
|
|
275
|
+
timestamp: new Date().toISOString(),
|
|
276
|
+
forums: allForums.map(f => ({
|
|
277
|
+
course_id: f.course_id,
|
|
278
|
+
course_name: f.course_name,
|
|
279
|
+
cmid: f.cmid,
|
|
280
|
+
forum_id: f.forum_id,
|
|
281
|
+
name: f.name,
|
|
282
|
+
url: f.url,
|
|
283
|
+
})),
|
|
284
|
+
summary: {
|
|
285
|
+
total_courses: courses.length,
|
|
286
|
+
total_forums: allForums.length,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
console.log(JSON.stringify(output));
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
forumsCmd
|
|
296
|
+
.command("discussions")
|
|
297
|
+
.description("List discussions in a forum (use cmid or instance ID)")
|
|
298
|
+
.argument("<forum-id>", "Forum cmid or instance ID")
|
|
299
|
+
.option("--unread-only", "Show only unread discussions")
|
|
300
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
301
|
+
.action(async (forumId, options, command) => {
|
|
302
|
+
const context = await createSessionContext({ verbose: false }, command);
|
|
303
|
+
if (!context) {
|
|
304
|
+
process.exitCode = 1;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const { log, page, session, browser, context: browserContext } = context;
|
|
308
|
+
try {
|
|
309
|
+
// Find forum by cmid or instance ID
|
|
310
|
+
const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log);
|
|
311
|
+
let targetForum = null;
|
|
312
|
+
for (const course of courses) {
|
|
313
|
+
const forums = await (0, moodle_js_1.getForumsInCourse)(page, session, course.id, log);
|
|
314
|
+
const forum = forums.find(f => f.cmid === forumId || f.forumId === parseInt(forumId, 10));
|
|
315
|
+
if (forum) {
|
|
316
|
+
targetForum = { forumId: forum.forumId, forumName: forum.name };
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!targetForum) {
|
|
321
|
+
console.log(JSON.stringify({ status: "error", error: "Forum not found" }));
|
|
322
|
+
process.exitCode = 1;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Use WS API to get discussions
|
|
326
|
+
const discussions = await (0, moodle_js_1.getForumDiscussions)(page, session, targetForum.forumId);
|
|
327
|
+
const output = {
|
|
328
|
+
status: "success",
|
|
329
|
+
timestamp: new Date().toISOString(),
|
|
330
|
+
forum_id: targetForum.forumId,
|
|
331
|
+
forum_name: targetForum.forumName,
|
|
332
|
+
discussions: discussions.map(d => ({
|
|
333
|
+
id: d.id,
|
|
334
|
+
name: d.name,
|
|
335
|
+
user_id: d.userId,
|
|
336
|
+
time_modified: d.timeModified,
|
|
337
|
+
post_count: d.postCount,
|
|
338
|
+
unread: d.unread,
|
|
339
|
+
})),
|
|
340
|
+
summary: {
|
|
341
|
+
total_discussions: discussions.length,
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
console.log(JSON.stringify(output));
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
forumsCmd
|
|
351
|
+
.command("posts")
|
|
352
|
+
.description("Show posts in a discussion")
|
|
353
|
+
.argument("<discussion-id>", "Discussion ID")
|
|
354
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
355
|
+
.action(async (discussionId, options, command) => {
|
|
356
|
+
const context = await createSessionContext(options, command);
|
|
357
|
+
if (!context) {
|
|
358
|
+
process.exitCode = 1;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const { log, page, session, browser, context: browserContext } = context;
|
|
362
|
+
const output = getOutputFormat(command);
|
|
363
|
+
try {
|
|
364
|
+
const posts = await (0, moodle_js_1.getDiscussionPosts)(page, session, parseInt(discussionId, 10));
|
|
365
|
+
if (output === "json") {
|
|
366
|
+
const result = {
|
|
367
|
+
status: "success",
|
|
368
|
+
timestamp: new Date().toISOString(),
|
|
369
|
+
discussion_id: discussionId,
|
|
370
|
+
posts: posts.map(p => ({
|
|
371
|
+
id: p.id,
|
|
372
|
+
subject: p.subject,
|
|
373
|
+
author: p.author,
|
|
374
|
+
author_id: p.authorId,
|
|
375
|
+
created: new Date(p.created * 1000).toISOString(),
|
|
376
|
+
modified: new Date(p.modified * 1000).toISOString(),
|
|
377
|
+
message: p.message,
|
|
378
|
+
unread: p.unread,
|
|
379
|
+
})),
|
|
380
|
+
summary: {
|
|
381
|
+
total_posts: posts.length,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
console.log(JSON.stringify(result));
|
|
385
|
+
}
|
|
386
|
+
else if (output === "table") {
|
|
387
|
+
console.log(`Discussion ${discussionId} - ${posts.length} posts`);
|
|
388
|
+
console.log("Use --output json to see full post content");
|
|
389
|
+
const tablePosts = posts.map(p => ({
|
|
390
|
+
id: p.id,
|
|
391
|
+
subject: p.subject.substring(0, 50) + (p.subject.length > 50 ? "..." : ""),
|
|
392
|
+
author: p.author,
|
|
393
|
+
created: new Date(p.created * 1000).toLocaleString(),
|
|
394
|
+
}));
|
|
395
|
+
(0, index_js_1.formatAndOutput)(tablePosts, "table", log);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
exports.registerForumsCommand = registerForumsCommand;
|
|
@@ -0,0 +1,259 @@
|
|
|
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.registerGradesCommand = 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 auth_js_2 = require("../lib/auth.js");
|
|
13
|
+
const index_js_1 = require("../index.js");
|
|
14
|
+
const token_js_1 = require("../lib/token.js");
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
function registerGradesCommand(program) {
|
|
18
|
+
const gradesCmd = program.command("grades");
|
|
19
|
+
gradesCmd.description("Grade operations");
|
|
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
|
|
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
|
+
gradesCmd
|
|
90
|
+
.command("summary")
|
|
91
|
+
.description("Show grade summary across all courses")
|
|
92
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
93
|
+
.action(async (options, command) => {
|
|
94
|
+
const output = getOutputFormat(command);
|
|
95
|
+
// Try pure API mode (no browser, fast!)
|
|
96
|
+
const apiContext = await createApiContext(options, command);
|
|
97
|
+
if (apiContext) {
|
|
98
|
+
try {
|
|
99
|
+
const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session);
|
|
100
|
+
const gradeSummaries = [];
|
|
101
|
+
for (const course of courses) {
|
|
102
|
+
const grades = await (0, moodle_js_1.getCourseGradesApi)(apiContext.session, course.id);
|
|
103
|
+
gradeSummaries.push({
|
|
104
|
+
courseId: course.id,
|
|
105
|
+
courseName: course.fullname,
|
|
106
|
+
grade: grades.grade,
|
|
107
|
+
gradeFormatted: grades.gradeFormatted,
|
|
108
|
+
rank: grades.rank,
|
|
109
|
+
totalUsers: grades.totalUsers,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Calculate overall statistics
|
|
113
|
+
const gradedCourses = gradeSummaries.filter(g => g.grade !== undefined && g.grade !== null && g.grade !== "-");
|
|
114
|
+
const averageRank = gradeSummaries
|
|
115
|
+
.filter(g => g.rank !== undefined && g.rank !== null)
|
|
116
|
+
.reduce((sum, g) => sum + (g.rank || 0), 0) /
|
|
117
|
+
(gradeSummaries.filter(g => g.rank !== undefined && g.rank !== null).length || 1);
|
|
118
|
+
const summaryData = {
|
|
119
|
+
total_courses: courses.length,
|
|
120
|
+
graded_courses: gradedCourses.length,
|
|
121
|
+
average_rank: averageRank.toFixed(1),
|
|
122
|
+
grades: gradeSummaries,
|
|
123
|
+
};
|
|
124
|
+
(0, index_js_1.formatAndOutput)(summaryData, output, apiContext.log);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
// API failed, fall through to browser mode
|
|
129
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
130
|
+
console.error(`// API mode failed: ${msg}, trying browser mode...`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Fallback to browser mode
|
|
134
|
+
const context = await createSessionContext(options, command);
|
|
135
|
+
if (!context) {
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const { log, page, session, browser, context: browserContext } = context;
|
|
140
|
+
try {
|
|
141
|
+
const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log);
|
|
142
|
+
const gradeSummaries = [];
|
|
143
|
+
for (const course of courses) {
|
|
144
|
+
const grades = await (0, moodle_js_1.getCourseGrades)(page, session, course.id);
|
|
145
|
+
gradeSummaries.push({
|
|
146
|
+
courseId: course.id,
|
|
147
|
+
courseName: course.fullname,
|
|
148
|
+
grade: grades.grade,
|
|
149
|
+
gradeFormatted: grades.gradeFormatted,
|
|
150
|
+
rank: grades.rank,
|
|
151
|
+
totalUsers: grades.totalUsers,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// Calculate overall statistics
|
|
155
|
+
const gradedCourses = gradeSummaries.filter(g => g.grade !== undefined && g.grade !== null && g.grade !== "-");
|
|
156
|
+
const averageRank = gradeSummaries
|
|
157
|
+
.filter(g => g.rank !== undefined && g.rank !== null)
|
|
158
|
+
.reduce((sum, g) => sum + (g.rank || 0), 0) /
|
|
159
|
+
(gradeSummaries.filter(g => g.rank !== undefined && g.rank !== null).length || 1);
|
|
160
|
+
const summaryData = {
|
|
161
|
+
total_courses: courses.length,
|
|
162
|
+
graded_courses: gradedCourses.length,
|
|
163
|
+
average_rank: averageRank.toFixed(1),
|
|
164
|
+
grades: gradeSummaries,
|
|
165
|
+
};
|
|
166
|
+
(0, index_js_1.formatAndOutput)(summaryData, output, log);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
gradesCmd
|
|
173
|
+
.command("course")
|
|
174
|
+
.description("Show detailed grades for a specific course")
|
|
175
|
+
.argument("<course-id>", "Course ID")
|
|
176
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
177
|
+
.action(async (courseId, options, command) => {
|
|
178
|
+
const output = getOutputFormat(command);
|
|
179
|
+
// Try pure API mode (no browser, fast!)
|
|
180
|
+
const apiContext = await createApiContext(options, command);
|
|
181
|
+
if (apiContext) {
|
|
182
|
+
try {
|
|
183
|
+
const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(apiContext.session);
|
|
184
|
+
const course = courses.find(c => c.id === parseInt(courseId, 10));
|
|
185
|
+
if (!course) {
|
|
186
|
+
apiContext.log.error(`Course not found: ${courseId}`);
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const grades = await (0, moodle_js_1.getCourseGradesApi)(apiContext.session, course.id);
|
|
191
|
+
const gradeData = {
|
|
192
|
+
courseId: grades.courseId,
|
|
193
|
+
courseName: grades.courseName,
|
|
194
|
+
grade: grades.grade,
|
|
195
|
+
gradeFormatted: grades.gradeFormatted,
|
|
196
|
+
rank: grades.rank,
|
|
197
|
+
totalUsers: grades.totalUsers,
|
|
198
|
+
items: grades.items?.map(item => ({
|
|
199
|
+
name: item.name,
|
|
200
|
+
grade: item.grade,
|
|
201
|
+
gradeFormatted: item.gradeFormatted,
|
|
202
|
+
range: item.range,
|
|
203
|
+
percentage: item.percentage,
|
|
204
|
+
weight: item.weight,
|
|
205
|
+
feedback: item.feedback,
|
|
206
|
+
graded: item.graded,
|
|
207
|
+
})),
|
|
208
|
+
};
|
|
209
|
+
(0, index_js_1.formatAndOutput)(gradeData, output, apiContext.log);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
// API failed, fall through to browser mode
|
|
214
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
215
|
+
console.error(`// API mode failed: ${msg}, trying browser mode...`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Fallback to browser mode
|
|
219
|
+
const context = await createSessionContext(options, command);
|
|
220
|
+
if (!context) {
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const { log, page, session, browser, context: browserContext } = context;
|
|
225
|
+
try {
|
|
226
|
+
const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log);
|
|
227
|
+
const course = courses.find(c => c.id === parseInt(courseId, 10));
|
|
228
|
+
if (!course) {
|
|
229
|
+
log.error(`Course not found: ${courseId}`);
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const grades = await (0, moodle_js_1.getCourseGrades)(page, session, course.id);
|
|
234
|
+
const gradeData = {
|
|
235
|
+
courseId: grades.courseId,
|
|
236
|
+
courseName: grades.courseName,
|
|
237
|
+
grade: grades.grade,
|
|
238
|
+
gradeFormatted: grades.gradeFormatted,
|
|
239
|
+
rank: grades.rank,
|
|
240
|
+
totalUsers: grades.totalUsers,
|
|
241
|
+
items: grades.items?.map(item => ({
|
|
242
|
+
name: item.name,
|
|
243
|
+
grade: item.grade,
|
|
244
|
+
gradeFormatted: item.gradeFormatted,
|
|
245
|
+
range: item.range,
|
|
246
|
+
percentage: item.percentage,
|
|
247
|
+
weight: item.weight,
|
|
248
|
+
feedback: item.feedback,
|
|
249
|
+
graded: item.graded,
|
|
250
|
+
})),
|
|
251
|
+
};
|
|
252
|
+
(0, index_js_1.formatAndOutput)(gradeData, output, log);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
exports.registerGradesCommand = registerGradesCommand;
|