@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,288 @@
|
|
|
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.registerAnnouncementsCommand = 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 token_js_1 = require("../lib/token.js");
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
function registerAnnouncementsCommand(program) {
|
|
17
|
+
const announcementsCmd = program.command("announcements");
|
|
18
|
+
announcementsCmd.description("Announcement 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
|
|
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
|
+
announcementsCmd
|
|
90
|
+
.command("list-all")
|
|
91
|
+
.description("List all announcements across all courses")
|
|
92
|
+
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
93
|
+
.option("--unread-only", "Show only unread announcements")
|
|
94
|
+
.option("--limit <n>", "Maximum number of announcements to show", "20")
|
|
95
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
96
|
+
.action(async (options, command) => {
|
|
97
|
+
const limit = parseInt(options.limit, 10);
|
|
98
|
+
// Try pure API mode (no browser, fast!)
|
|
99
|
+
const apiContext = await createApiContext(options, command);
|
|
100
|
+
if (apiContext) {
|
|
101
|
+
try {
|
|
102
|
+
// Get site info to retrieve userid
|
|
103
|
+
const siteInfo = await (0, moodle_js_1.getSiteInfoApi)(apiContext.session);
|
|
104
|
+
// Get messages for the current user
|
|
105
|
+
const messages = await (0, moodle_js_1.getMessagesApi)(apiContext.session, siteInfo.userid, {
|
|
106
|
+
limitnum: limit,
|
|
107
|
+
});
|
|
108
|
+
// Convert messages to announcement format
|
|
109
|
+
const allAnnouncements = messages.map(m => ({
|
|
110
|
+
course_id: 0,
|
|
111
|
+
course_name: "Notifications",
|
|
112
|
+
id: m.id,
|
|
113
|
+
subject: m.subject,
|
|
114
|
+
author: `User ${m.useridfrom}`,
|
|
115
|
+
authorId: m.useridfrom,
|
|
116
|
+
createdAt: m.timecreated,
|
|
117
|
+
modifiedAt: m.timecreated,
|
|
118
|
+
unread: false,
|
|
119
|
+
forumId: 0,
|
|
120
|
+
}));
|
|
121
|
+
// Sort by created date (newest first)
|
|
122
|
+
allAnnouncements.sort((a, b) => b.createdAt - a.createdAt);
|
|
123
|
+
// Apply limit
|
|
124
|
+
let filteredAnnouncements = allAnnouncements.slice(0, limit);
|
|
125
|
+
const output = {
|
|
126
|
+
status: "success",
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
level: options.level,
|
|
129
|
+
announcements: filteredAnnouncements.map(a => ({
|
|
130
|
+
course_id: a.course_id,
|
|
131
|
+
course_name: a.course_name,
|
|
132
|
+
id: a.id,
|
|
133
|
+
subject: a.subject,
|
|
134
|
+
author: a.author,
|
|
135
|
+
author_id: a.authorId,
|
|
136
|
+
created_at: new Date(a.createdAt * 1000).toISOString(),
|
|
137
|
+
modified_at: new Date(a.modifiedAt * 1000).toISOString(),
|
|
138
|
+
unread: a.unread,
|
|
139
|
+
})),
|
|
140
|
+
summary: {
|
|
141
|
+
total_announcements: allAnnouncements.length,
|
|
142
|
+
shown: filteredAnnouncements.length,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
console.log(JSON.stringify(output));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
// API failed, fall through to browser mode
|
|
150
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
151
|
+
console.error(`// API mode failed: ${msg}, trying browser mode...`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Fallback to browser mode
|
|
155
|
+
const context = await createSessionContext(options, command);
|
|
156
|
+
if (!context) {
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const { log, page, session, browser, context: browserContext } = context;
|
|
161
|
+
try {
|
|
162
|
+
const classification = options.level === "all" ? undefined : "inprogress";
|
|
163
|
+
const courses = await (0, moodle_js_1.getEnrolledCourses)(page, session, log, { classification });
|
|
164
|
+
const allAnnouncements = [];
|
|
165
|
+
for (const course of courses) {
|
|
166
|
+
const forums = await (0, moodle_js_1.getForumsInCourse)(page, session, course.id, log);
|
|
167
|
+
// Find news/announcement forums (usually named "news" or "Announcements")
|
|
168
|
+
const announcementForums = forums.filter(f => f.forumType === "news" ||
|
|
169
|
+
f.name.toLowerCase().includes("news") ||
|
|
170
|
+
f.name.toLowerCase().includes("announcement") ||
|
|
171
|
+
f.name.toLowerCase().includes("公告"));
|
|
172
|
+
for (const forum of announcementForums) {
|
|
173
|
+
try {
|
|
174
|
+
const discussions = await (0, moodle_js_1.getForumDiscussions)(page, session, parseInt(forum.cmid, 10));
|
|
175
|
+
for (const discussion of discussions) {
|
|
176
|
+
// Get the first post to get author info
|
|
177
|
+
let author = "Unknown";
|
|
178
|
+
let authorId = 0;
|
|
179
|
+
let createdAt = discussion.timeModified;
|
|
180
|
+
try {
|
|
181
|
+
const posts = await (0, moodle_js_1.getDiscussionPosts)(page, session, discussion.id);
|
|
182
|
+
if (posts.length > 0) {
|
|
183
|
+
const firstPost = posts[0];
|
|
184
|
+
author = firstPost.author;
|
|
185
|
+
authorId = firstPost.authorId || 0;
|
|
186
|
+
createdAt = firstPost.created;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Ignore errors fetching posts
|
|
191
|
+
}
|
|
192
|
+
allAnnouncements.push({
|
|
193
|
+
course_id: course.id,
|
|
194
|
+
course_name: course.fullname,
|
|
195
|
+
id: discussion.id,
|
|
196
|
+
subject: discussion.name,
|
|
197
|
+
author,
|
|
198
|
+
authorId,
|
|
199
|
+
createdAt,
|
|
200
|
+
modifiedAt: discussion.timeModified,
|
|
201
|
+
unread: discussion.unread,
|
|
202
|
+
forumId: parseInt(forum.cmid, 10),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
log.debug(`Failed to fetch announcements for ${course.fullname}: ${err}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Sort by created date (newest first)
|
|
212
|
+
allAnnouncements.sort((a, b) => b.createdAt - a.createdAt);
|
|
213
|
+
// Apply limit
|
|
214
|
+
let filteredAnnouncements = allAnnouncements.slice(0, limit);
|
|
215
|
+
// Filter unread only if requested
|
|
216
|
+
if (options.unreadOnly) {
|
|
217
|
+
filteredAnnouncements = filteredAnnouncements.filter(a => a.unread);
|
|
218
|
+
}
|
|
219
|
+
const output = {
|
|
220
|
+
status: "success",
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
announcements: filteredAnnouncements.map(a => ({
|
|
223
|
+
course_id: a.course_id,
|
|
224
|
+
course_name: a.course_name,
|
|
225
|
+
id: a.id,
|
|
226
|
+
subject: a.subject,
|
|
227
|
+
author: a.author,
|
|
228
|
+
author_id: a.authorId,
|
|
229
|
+
created_at: new Date(a.createdAt * 1000).toISOString(),
|
|
230
|
+
modified_at: new Date(a.modifiedAt * 1000).toISOString(),
|
|
231
|
+
unread: a.unread,
|
|
232
|
+
})),
|
|
233
|
+
summary: {
|
|
234
|
+
total_announcements: allAnnouncements.length,
|
|
235
|
+
unread: allAnnouncements.filter(a => a.unread).length,
|
|
236
|
+
shown: filteredAnnouncements.length,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
console.log(JSON.stringify(output));
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
log.error(`Error: ${err}`);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
announcementsCmd
|
|
249
|
+
.command("read")
|
|
250
|
+
.description("Read a specific announcement (shows full content)")
|
|
251
|
+
.argument("<announcement-id>", "Discussion ID of the announcement")
|
|
252
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
253
|
+
.action(async (announcementId, options, command) => {
|
|
254
|
+
const context = await createSessionContext(options, command);
|
|
255
|
+
if (!context) {
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const { log, page, session, browser, context: browserContext } = context;
|
|
260
|
+
try {
|
|
261
|
+
const posts = await (0, moodle_js_1.getDiscussionPosts)(page, session, parseInt(announcementId, 10));
|
|
262
|
+
if (posts.length === 0) {
|
|
263
|
+
log.error(`Announcement not found: ${announcementId}`);
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const firstPost = posts[0];
|
|
268
|
+
const output = {
|
|
269
|
+
status: "success",
|
|
270
|
+
timestamp: new Date().toISOString(),
|
|
271
|
+
announcement: {
|
|
272
|
+
id: announcementId,
|
|
273
|
+
subject: firstPost.subject,
|
|
274
|
+
author: firstPost.author,
|
|
275
|
+
author_id: firstPost.authorId,
|
|
276
|
+
created_at: new Date(firstPost.created * 1000).toISOString(),
|
|
277
|
+
modified_at: new Date(firstPost.modified * 1000).toISOString(),
|
|
278
|
+
message: firstPost.message,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
console.log(JSON.stringify(output));
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
exports.registerAnnouncementsCommand = registerAnnouncementsCommand;
|
|
@@ -0,0 +1,238 @@
|
|
|
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.registerAuthCommand = void 0;
|
|
7
|
+
const utils_js_1 = require("../lib/utils.js");
|
|
8
|
+
const playwright_core_1 = require("playwright-core");
|
|
9
|
+
const logger_js_1 = require("../lib/logger.js");
|
|
10
|
+
const auth_js_1 = require("../lib/auth.js");
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
function registerAuthCommand(program) {
|
|
14
|
+
const authCmd = program.command("auth");
|
|
15
|
+
authCmd.description("Authentication commands");
|
|
16
|
+
// Register login directly on program (not under auth subcommand)
|
|
17
|
+
program
|
|
18
|
+
.command("login")
|
|
19
|
+
.description("Login to iLearning manually and save session")
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
const log = (0, logger_js_1.createLogger)(false);
|
|
22
|
+
// Determine session storage path
|
|
23
|
+
const baseDir = (0, utils_js_1.getBaseDir)();
|
|
24
|
+
const sessionDir = path_1.default.resolve(baseDir, ".auth");
|
|
25
|
+
const sessionPath = path_1.default.resolve(sessionDir, "storage-state.json");
|
|
26
|
+
// Ensure session directory exists
|
|
27
|
+
if (!fs_1.default.existsSync(sessionDir)) {
|
|
28
|
+
fs_1.default.mkdirSync(sessionDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
const edgePath = (0, auth_js_1.findEdgePath)();
|
|
31
|
+
const browser = await playwright_core_1.chromium.launch({
|
|
32
|
+
executablePath: edgePath,
|
|
33
|
+
headless: false,
|
|
34
|
+
slowMo: 0,
|
|
35
|
+
});
|
|
36
|
+
let context;
|
|
37
|
+
let page;
|
|
38
|
+
if (fs_1.default.existsSync(sessionPath)) {
|
|
39
|
+
log.info(`找到已有 session: ${sessionPath}`);
|
|
40
|
+
log.info("正在驗證 session...");
|
|
41
|
+
try {
|
|
42
|
+
context = await browser.newContext({ storageState: sessionPath });
|
|
43
|
+
page = await context.newPage();
|
|
44
|
+
await page.goto("https://ilearning.cycu.edu.tw/my/", {
|
|
45
|
+
waitUntil: "domcontentloaded",
|
|
46
|
+
timeout: 15000,
|
|
47
|
+
});
|
|
48
|
+
const url = page.url();
|
|
49
|
+
if (url.includes("login") || url.includes("microsoftonline")) {
|
|
50
|
+
log.warn("Session 已過期,請重新登入。");
|
|
51
|
+
await context.close();
|
|
52
|
+
context = await browser.newContext();
|
|
53
|
+
page = await context.newPage();
|
|
54
|
+
await page.goto("https://ilearning.cycu.edu.tw/login/index.php", {
|
|
55
|
+
waitUntil: "domcontentloaded",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Session is still valid, close browser and exit
|
|
60
|
+
try {
|
|
61
|
+
if (context)
|
|
62
|
+
await context.close().catch(() => { });
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
try {
|
|
66
|
+
await browser.close().catch(() => { });
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
// Wait a bit for browser to fully close
|
|
70
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
71
|
+
const result = {
|
|
72
|
+
status: "success",
|
|
73
|
+
message: "Session still valid",
|
|
74
|
+
session_path: sessionPath,
|
|
75
|
+
updated: false
|
|
76
|
+
};
|
|
77
|
+
console.log(JSON.stringify(result));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
log.warn("無法恢復 session,請重新登入。");
|
|
83
|
+
// context might not have been initialized if the error occurred during newContext
|
|
84
|
+
if (context) {
|
|
85
|
+
await context.close();
|
|
86
|
+
}
|
|
87
|
+
context = await browser.newContext();
|
|
88
|
+
page = await context.newPage();
|
|
89
|
+
await page.goto("https://ilearning.cycu.edu.tw/login/index.php", {
|
|
90
|
+
waitUntil: "domcontentloaded",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
log.info("首次登入,請在瀏覽器中完成登入流程。");
|
|
96
|
+
context = await browser.newContext();
|
|
97
|
+
page = await context.newPage();
|
|
98
|
+
await page.goto("https://ilearning.cycu.edu.tw/login/index.php", {
|
|
99
|
+
waitUntil: "domcontentloaded",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
log.info("\n請在瀏覽器中完成登入,登入成功後將自動儲存 session...\n");
|
|
103
|
+
try {
|
|
104
|
+
const startTime = Date.now();
|
|
105
|
+
const timeout = 300000;
|
|
106
|
+
let loggedIn = false;
|
|
107
|
+
while (Date.now() - startTime < timeout) {
|
|
108
|
+
await page.waitForTimeout(1000);
|
|
109
|
+
const currentUrl = page.url();
|
|
110
|
+
if (currentUrl.includes("ilearning.cycu.edu.tw") &&
|
|
111
|
+
!currentUrl.includes("login") &&
|
|
112
|
+
!currentUrl.includes("microsoftonline")) {
|
|
113
|
+
await page.waitForTimeout(2000);
|
|
114
|
+
const finalUrl = page.url();
|
|
115
|
+
if (finalUrl.includes("ilearning.cycu.edu.tw") &&
|
|
116
|
+
!finalUrl.includes("login") &&
|
|
117
|
+
!finalUrl.includes("microsoftonline")) {
|
|
118
|
+
loggedIn = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (loggedIn) {
|
|
124
|
+
await context.storageState({ path: sessionPath });
|
|
125
|
+
const stats = fs_1.default.statSync(sessionPath);
|
|
126
|
+
const result = {
|
|
127
|
+
status: "success",
|
|
128
|
+
message: "Login successful",
|
|
129
|
+
session_path: sessionPath,
|
|
130
|
+
session_size: stats.size,
|
|
131
|
+
updated: true
|
|
132
|
+
};
|
|
133
|
+
console.log(JSON.stringify(result, null, 2));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
throw new Error("TimeoutError");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const errorResult = {
|
|
141
|
+
status: "error",
|
|
142
|
+
error: err instanceof Error ? err.message : String(err),
|
|
143
|
+
session_path: sessionPath
|
|
144
|
+
};
|
|
145
|
+
console.log(JSON.stringify(errorResult));
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
// Safely close browser with error handling
|
|
149
|
+
try {
|
|
150
|
+
if (context)
|
|
151
|
+
await context.close().catch(() => { });
|
|
152
|
+
}
|
|
153
|
+
catch { }
|
|
154
|
+
try {
|
|
155
|
+
await browser.close().catch(() => { });
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
// Wait for browser process to fully terminate
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// auth subcommand group (for status and other auth-related commands)
|
|
163
|
+
authCmd
|
|
164
|
+
.command("status")
|
|
165
|
+
.description("Check session status")
|
|
166
|
+
.option("--session <path>", "Session file path", ".auth/storage-state.json")
|
|
167
|
+
.action(async (options) => {
|
|
168
|
+
const baseDir = (0, utils_js_1.getBaseDir)();
|
|
169
|
+
const sessionPath = path_1.default.resolve(baseDir, options.session);
|
|
170
|
+
if (fs_1.default.existsSync(sessionPath)) {
|
|
171
|
+
const stats = fs_1.default.statSync(sessionPath);
|
|
172
|
+
// Try to read and validate the session
|
|
173
|
+
try {
|
|
174
|
+
const content = fs_1.default.readFileSync(sessionPath, "utf8");
|
|
175
|
+
const state = JSON.parse(content);
|
|
176
|
+
const cookies = state.cookies || [];
|
|
177
|
+
const moodleSession = cookies.find((c) => c.name === "MoodleSession");
|
|
178
|
+
const result = {
|
|
179
|
+
status: "success",
|
|
180
|
+
session_path: sessionPath,
|
|
181
|
+
exists: true,
|
|
182
|
+
modified: new Date(stats.mtime).toISOString(),
|
|
183
|
+
size: stats.size,
|
|
184
|
+
moodle_session: moodleSession ? {
|
|
185
|
+
exists: true,
|
|
186
|
+
expires: new Date(moodleSession.expires * 1000).toISOString()
|
|
187
|
+
} : {
|
|
188
|
+
exists: false
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
console.log(JSON.stringify(result, null, 2));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
const result = {
|
|
195
|
+
status: "error",
|
|
196
|
+
error: "Session file is corrupted",
|
|
197
|
+
session_path: sessionPath
|
|
198
|
+
};
|
|
199
|
+
console.log(JSON.stringify(result, null, 2));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const result = {
|
|
204
|
+
status: "error",
|
|
205
|
+
error: "Session not found",
|
|
206
|
+
session_path: sessionPath,
|
|
207
|
+
hint: "Run 'openape auth login' first"
|
|
208
|
+
};
|
|
209
|
+
console.log(JSON.stringify(result, null, 2));
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
authCmd
|
|
213
|
+
.command("logout")
|
|
214
|
+
.description("Remove saved session")
|
|
215
|
+
.option("--session <path>", "Session file path", ".auth/storage-state.json")
|
|
216
|
+
.action(async (options) => {
|
|
217
|
+
const baseDir = (0, utils_js_1.getBaseDir)();
|
|
218
|
+
const sessionPath = path_1.default.resolve(baseDir, options.session);
|
|
219
|
+
if (fs_1.default.existsSync(sessionPath)) {
|
|
220
|
+
fs_1.default.unlinkSync(sessionPath);
|
|
221
|
+
const result = {
|
|
222
|
+
status: "success",
|
|
223
|
+
message: "Session removed",
|
|
224
|
+
session_path: sessionPath
|
|
225
|
+
};
|
|
226
|
+
console.log(JSON.stringify(result, null, 2));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const result = {
|
|
230
|
+
status: "error",
|
|
231
|
+
error: "Session not found",
|
|
232
|
+
session_path: sessionPath
|
|
233
|
+
};
|
|
234
|
+
console.log(JSON.stringify(result, null, 2));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
exports.registerAuthCommand = registerAuthCommand;
|