@mo7yw4ng/openape 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -9
- package/esm/deno.js +1 -1
- package/esm/src/commands/announcements.d.ts.map +1 -1
- package/esm/src/commands/announcements.js +13 -14
- package/esm/src/commands/assignments.d.ts +3 -0
- package/esm/src/commands/assignments.d.ts.map +1 -0
- package/esm/src/commands/assignments.js +230 -0
- package/esm/src/commands/auth.d.ts +1 -1
- package/esm/src/commands/auth.d.ts.map +1 -1
- package/esm/src/commands/auth.js +24 -8
- package/esm/src/commands/calendar.d.ts.map +1 -1
- package/esm/src/commands/calendar.js +17 -18
- package/esm/src/commands/courses.js +3 -3
- package/esm/src/commands/forums.d.ts.map +1 -1
- package/esm/src/commands/forums.js +132 -45
- package/esm/src/commands/materials.d.ts.map +1 -1
- package/esm/src/commands/materials.js +176 -48
- package/esm/src/commands/quizzes.d.ts.map +1 -1
- package/esm/src/commands/quizzes.js +165 -65
- package/esm/src/commands/skills.d.ts.map +1 -1
- package/esm/src/commands/skills.js +4 -8
- package/esm/src/commands/upload.d.ts +3 -0
- package/esm/src/commands/upload.d.ts.map +1 -0
- package/esm/src/commands/upload.js +58 -0
- package/esm/src/commands/videos.d.ts.map +1 -1
- package/esm/src/commands/videos.js +113 -79
- package/esm/src/index.d.ts.map +1 -1
- package/esm/src/index.js +14 -3
- package/esm/src/lib/auth.d.ts +23 -1
- package/esm/src/lib/auth.d.ts.map +1 -1
- package/esm/src/lib/auth.js +36 -3
- package/esm/src/lib/moodle.d.ts +226 -2
- package/esm/src/lib/moodle.d.ts.map +1 -1
- package/esm/src/lib/moodle.js +648 -37
- package/esm/src/lib/types.d.ts +82 -164
- package/esm/src/lib/types.d.ts.map +1 -1
- package/esm/src/lib/types.js +1 -0
- package/esm/src/lib/utils.d.ts +40 -0
- package/esm/src/lib/utils.d.ts.map +1 -1
- package/esm/src/lib/utils.js +82 -4
- package/package.json +1 -2
- package/script/deno.js +1 -1
- package/script/src/commands/announcements.d.ts.map +1 -1
- package/script/src/commands/announcements.js +12 -13
- package/script/src/commands/assignments.d.ts +3 -0
- package/script/src/commands/assignments.d.ts.map +1 -0
- package/script/src/commands/assignments.js +269 -0
- package/script/src/commands/auth.d.ts +1 -1
- package/script/src/commands/auth.d.ts.map +1 -1
- package/script/src/commands/auth.js +24 -8
- package/script/src/commands/calendar.d.ts.map +1 -1
- package/script/src/commands/calendar.js +16 -17
- package/script/src/commands/courses.js +2 -2
- package/script/src/commands/forums.d.ts.map +1 -1
- package/script/src/commands/forums.js +132 -45
- package/script/src/commands/materials.d.ts.map +1 -1
- package/script/src/commands/materials.js +177 -49
- package/script/src/commands/quizzes.d.ts.map +1 -1
- package/script/src/commands/quizzes.js +163 -63
- package/script/src/commands/skills.d.ts.map +1 -1
- package/script/src/commands/skills.js +4 -8
- package/script/src/commands/upload.d.ts +3 -0
- package/script/src/commands/upload.d.ts.map +1 -0
- package/script/src/commands/upload.js +64 -0
- package/script/src/commands/videos.d.ts.map +1 -1
- package/script/src/commands/videos.js +114 -80
- package/script/src/index.d.ts.map +1 -1
- package/script/src/index.js +13 -2
- package/script/src/lib/auth.d.ts +23 -1
- package/script/src/lib/auth.d.ts.map +1 -1
- package/script/src/lib/auth.js +70 -3
- package/script/src/lib/moodle.d.ts +226 -2
- package/script/src/lib/moodle.d.ts.map +1 -1
- package/script/src/lib/moodle.js +663 -37
- package/script/src/lib/types.d.ts +82 -164
- package/script/src/lib/types.d.ts.map +1 -1
- package/script/src/lib/types.js +1 -0
- package/script/src/lib/utils.d.ts +40 -0
- package/script/src/lib/utils.d.ts.map +1 -1
- package/script/src/lib/utils.js +89 -3
- package/skills/openape/SKILL.md +73 -291
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getBaseDir } from "../lib/utils.js";
|
|
2
|
-
import {
|
|
1
|
+
import { getBaseDir, getOutputFormat, sanitizeFilename } from "../lib/utils.js";
|
|
2
|
+
import { getEnrolledCoursesApi, getSupervideosInCourse, getSupervideosInCourseApi, getVideoMetadata, completeVideoApi, downloadVideo, getIncompleteVideosApi } from "../lib/moodle.js";
|
|
3
3
|
import { createLogger } from "../lib/logger.js";
|
|
4
4
|
import { launchAuthenticated } from "../lib/auth.js";
|
|
5
5
|
import { extractSessionInfo } from "../lib/session.js";
|
|
@@ -11,15 +11,11 @@ import fs from "node:fs";
|
|
|
11
11
|
export function registerVideosCommand(program) {
|
|
12
12
|
const videosCmd = program.command("videos");
|
|
13
13
|
videosCmd.description("Video progress operations");
|
|
14
|
-
// Helper to get output format from global or local options
|
|
15
|
-
function getOutputFormat(command) {
|
|
16
|
-
const opts = command.optsWithGlobals();
|
|
17
|
-
return opts.output || "json";
|
|
18
|
-
}
|
|
19
14
|
// Pure API context - no browser required (fast!)
|
|
20
15
|
async function createApiContext(options, command) {
|
|
21
16
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
22
|
-
|
|
17
|
+
// Don't silence logs for commands that don't have explicit output format control
|
|
18
|
+
const outputFormat = command && command.optsWithGlobals ? getOutputFormat(command) : "table";
|
|
23
19
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
24
20
|
const log = createLogger(opts.verbose, silent);
|
|
25
21
|
const baseDir = getBaseDir();
|
|
@@ -91,46 +87,61 @@ export function registerVideosCommand(program) {
|
|
|
91
87
|
process.exitCode = 1;
|
|
92
88
|
return;
|
|
93
89
|
}
|
|
94
|
-
|
|
95
|
-
//
|
|
90
|
+
let videos = await getSupervideosInCourseApi(apiContext.session, parseInt(courseId, 10));
|
|
91
|
+
// Filter for incomplete videos if requested
|
|
96
92
|
if (options.incompleteOnly) {
|
|
97
|
-
|
|
93
|
+
videos = videos.filter(v => !v.isComplete);
|
|
98
94
|
}
|
|
99
95
|
formatAndOutput(videos, output, apiContext.log);
|
|
100
96
|
});
|
|
101
97
|
videosCmd
|
|
102
98
|
.command("complete")
|
|
103
|
-
.description("Complete videos in a course (
|
|
99
|
+
.description("Complete videos in a course (uses API for list & completion, browser for metadata)")
|
|
104
100
|
.argument("<course-id>", "Course ID")
|
|
105
101
|
.option("--dry-run", "Discover videos but don't complete them")
|
|
106
102
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
107
103
|
.action(async (courseId, options, command) => {
|
|
104
|
+
const output = getOutputFormat(command);
|
|
105
|
+
// Get API context for getting incomplete videos and completion
|
|
106
|
+
const apiContext = await createApiContext(options, command);
|
|
107
|
+
if (!apiContext) {
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Get incomplete videos via API (fast, no browser needed)
|
|
112
|
+
const incompleteVideos = await getIncompleteVideosApi(apiContext.session, parseInt(courseId, 10));
|
|
113
|
+
if (incompleteVideos.length === 0) {
|
|
114
|
+
apiContext.log.info("所有影片已完成(或無影片)。");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
apiContext.log.info(`找到 ${incompleteVideos.length} 部未完成影片`);
|
|
118
|
+
// Dry-run: show videos without needing browser
|
|
119
|
+
if (options.dryRun) {
|
|
120
|
+
const results = incompleteVideos.map(v => ({ name: v.name, success: true }));
|
|
121
|
+
for (const video of incompleteVideos) {
|
|
122
|
+
apiContext.log.info(` [試執行] ${video.name}`);
|
|
123
|
+
}
|
|
124
|
+
apiContext.log.info(`\n執行結果: ${results.length} 影片將被完成`);
|
|
125
|
+
if (output !== "silent") {
|
|
126
|
+
formatAndOutput(results, output, apiContext.log);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Need browser only for getting viewId and duration (not needed for dry-run)
|
|
108
131
|
const context = await createSessionContext(options, command);
|
|
109
132
|
if (!context) {
|
|
110
133
|
process.exitCode = 1;
|
|
111
134
|
return;
|
|
112
135
|
}
|
|
113
|
-
const { log, page,
|
|
114
|
-
const output = getOutputFormat(command);
|
|
136
|
+
const { log, page, browser, context: browserContext } = context;
|
|
115
137
|
try {
|
|
116
|
-
const videos = await getSupervideosInCourse(page, session, parseInt(courseId, 10), log, {
|
|
117
|
-
incompleteOnly: true, // Only operate on incomplete videos
|
|
118
|
-
});
|
|
119
|
-
if (videos.length === 0) {
|
|
120
|
-
log.info("所有影片已完成(或無影片)。");
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
138
|
const results = [];
|
|
124
|
-
for (const sv of
|
|
139
|
+
for (const sv of incompleteVideos) {
|
|
125
140
|
log.info(`處理中: ${sv.name}`);
|
|
126
141
|
try {
|
|
127
142
|
const video = await getVideoMetadata(page, sv.url, log);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
results.push({ name: sv.name, success: true });
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
const success = await completeVideo(page, session, { ...video, cmid: sv.cmid }, log);
|
|
143
|
+
// Use WS API for completion
|
|
144
|
+
const success = await completeVideoApi(apiContext.session, { ...video, cmid: sv.cmid.toString() });
|
|
134
145
|
if (success) {
|
|
135
146
|
log.success(` 已完成!`);
|
|
136
147
|
results.push({ name: sv.name, success: true });
|
|
@@ -159,65 +170,94 @@ export function registerVideosCommand(program) {
|
|
|
159
170
|
});
|
|
160
171
|
videosCmd
|
|
161
172
|
.command("complete-all")
|
|
162
|
-
.description("Complete all incomplete videos across all courses (
|
|
173
|
+
.description("Complete all incomplete videos across all courses (uses API for list & completion, browser for metadata)")
|
|
163
174
|
.option("--dry-run", "Discover videos but don't complete them")
|
|
164
175
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
165
176
|
.action(async (options, command) => {
|
|
177
|
+
const output = getOutputFormat(command);
|
|
178
|
+
// Get API context for getting incomplete videos and completion
|
|
179
|
+
const apiContext = await createApiContext(options, command);
|
|
180
|
+
if (!apiContext) {
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Get all courses via API
|
|
185
|
+
const classification = undefined; // all courses
|
|
186
|
+
const courses = await getEnrolledCoursesApi(apiContext.session, { classification });
|
|
187
|
+
apiContext.log.info(`掃描 ${courses.length} 個課程...`);
|
|
188
|
+
// Collect all incomplete videos across all courses using flatMap for cleaner code
|
|
189
|
+
const allIncompleteVideos = (await Promise.allSettled(courses.map(async (course) => {
|
|
190
|
+
try {
|
|
191
|
+
const videos = await getIncompleteVideosApi(apiContext.session, course.id);
|
|
192
|
+
return videos.map((video) => ({
|
|
193
|
+
courseId: course.id,
|
|
194
|
+
courseName: course.fullname,
|
|
195
|
+
cmid: video.cmid,
|
|
196
|
+
name: video.name,
|
|
197
|
+
url: video.url,
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
apiContext.log.warn(`無法取得課程 ${course.fullname} 的影片: ${e}`);
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
})))
|
|
205
|
+
.filter((result) => result.status === "fulfilled")
|
|
206
|
+
.flatMap((result) => result.status === "fulfilled" ? result.value : []);
|
|
207
|
+
if (allIncompleteVideos.length === 0) {
|
|
208
|
+
apiContext.log.info("所有影片已完成(或無影片)。");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
apiContext.log.info(`找到 ${allIncompleteVideos.length} 部未完成影片`);
|
|
212
|
+
// Dry-run: show videos without needing browser
|
|
213
|
+
if (options.dryRun) {
|
|
214
|
+
for (const video of allIncompleteVideos) {
|
|
215
|
+
apiContext.log.info(` [試執行] [${video.courseName}] ${video.name}`);
|
|
216
|
+
}
|
|
217
|
+
apiContext.log.info("\n===== 執行結果 =====");
|
|
218
|
+
apiContext.log.info(`掃描課程數: ${courses.length}`);
|
|
219
|
+
apiContext.log.info(`找到未完成影片: ${allIncompleteVideos.length}`);
|
|
220
|
+
apiContext.log.info(`執行影片數: ${allIncompleteVideos.length} (試執行)`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Need browser only for getting viewId and duration (not needed for dry-run)
|
|
166
224
|
const context = await createSessionContext(options, command);
|
|
167
225
|
if (!context) {
|
|
168
226
|
process.exitCode = 1;
|
|
169
227
|
return;
|
|
170
228
|
}
|
|
171
|
-
const { log, page,
|
|
172
|
-
const output = getOutputFormat(command);
|
|
229
|
+
const { log, page, browser, context: browserContext } = context;
|
|
173
230
|
try {
|
|
174
|
-
const courses = await getEnrolledCourses(page, session, log);
|
|
175
231
|
const allResults = [];
|
|
176
|
-
let totalVideos = 0;
|
|
177
232
|
let totalCompleted = 0;
|
|
178
233
|
let totalFailed = 0;
|
|
179
|
-
for (const
|
|
180
|
-
log.info(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
for (const sv of videos) {
|
|
190
|
-
log.info(` 處理中: ${sv.name}`);
|
|
191
|
-
try {
|
|
192
|
-
const video = await getVideoMetadata(page, sv.url, log);
|
|
193
|
-
if (options.dryRun) {
|
|
194
|
-
log.info(` [試執行] viewId=${video.viewId}, duration=${video.duration}s`);
|
|
195
|
-
allResults.push({ courseName: course.fullname, name: sv.name, success: true });
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
const success = await completeVideo(page, session, { ...video, cmid: sv.cmid }, log);
|
|
199
|
-
if (success) {
|
|
200
|
-
log.success(` 已完成!`);
|
|
201
|
-
allResults.push({ courseName: course.fullname, name: sv.name, success: true });
|
|
202
|
-
totalCompleted++;
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
log.error(` 失敗。`);
|
|
206
|
-
allResults.push({ courseName: course.fullname, name: sv.name, success: false, error: "Failed to complete" });
|
|
207
|
-
totalFailed++;
|
|
208
|
-
}
|
|
234
|
+
for (const video of allIncompleteVideos) {
|
|
235
|
+
log.info(`處理中: [${video.courseName}] ${video.name}`);
|
|
236
|
+
try {
|
|
237
|
+
const metadata = await getVideoMetadata(page, video.url, log);
|
|
238
|
+
// Use WS API for completion
|
|
239
|
+
const success = await completeVideoApi(apiContext.session, { ...metadata, cmid: video.cmid.toString() });
|
|
240
|
+
if (success) {
|
|
241
|
+
log.success(` 已完成!`);
|
|
242
|
+
allResults.push({ courseName: video.courseName, name: video.name, success: true });
|
|
243
|
+
totalCompleted++;
|
|
209
244
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
allResults.push({ courseName: course.fullname, name: sv.name, success: false, error: msg });
|
|
245
|
+
else {
|
|
246
|
+
log.error(` 失敗。`);
|
|
247
|
+
allResults.push({ courseName: video.courseName, name: video.name, success: false, error: "Failed to complete" });
|
|
214
248
|
totalFailed++;
|
|
215
249
|
}
|
|
216
250
|
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
253
|
+
log.error(` 錯誤: ${msg}`);
|
|
254
|
+
allResults.push({ courseName: video.courseName, name: video.name, success: false, error: msg });
|
|
255
|
+
totalFailed++;
|
|
256
|
+
}
|
|
217
257
|
}
|
|
218
258
|
log.info("\n===== 執行結果 =====");
|
|
219
259
|
log.info(`掃描課程數: ${courses.length}`);
|
|
220
|
-
log.info(
|
|
260
|
+
log.info(`找到未完成影片: ${allIncompleteVideos.length}`);
|
|
221
261
|
log.info(`執行影片數: ${totalCompleted}`);
|
|
222
262
|
if (totalFailed > 0)
|
|
223
263
|
log.warn(`失敗影片數: ${totalFailed}`);
|
|
@@ -229,13 +269,6 @@ export function registerVideosCommand(program) {
|
|
|
229
269
|
await closeBrowserSafely(browser, browserContext);
|
|
230
270
|
}
|
|
231
271
|
});
|
|
232
|
-
// Helper function to sanitize filename
|
|
233
|
-
function sanitizeFilename(name) {
|
|
234
|
-
return name
|
|
235
|
-
.replace(/[<>:"/\\|?*]/g, "_")
|
|
236
|
-
.replace(/\s+/g, "_")
|
|
237
|
-
.substring(0, 200);
|
|
238
|
-
}
|
|
239
272
|
videosCmd
|
|
240
273
|
.command("download")
|
|
241
274
|
.description("Download videos from a course (requires browser)")
|
|
@@ -283,7 +316,7 @@ export function registerVideosCommand(program) {
|
|
|
283
316
|
const completed = downloaded.filter(d => d.success).length;
|
|
284
317
|
const failed = downloaded.filter(d => !d.success).length;
|
|
285
318
|
log.info(`\n執行結果: ${completed} 成功, ${failed} 失敗`);
|
|
286
|
-
|
|
319
|
+
console.log(JSON.stringify({
|
|
287
320
|
status: "success",
|
|
288
321
|
timestamp: new Date().toISOString(),
|
|
289
322
|
course_id: courseId,
|
|
@@ -291,9 +324,10 @@ export function registerVideosCommand(program) {
|
|
|
291
324
|
total_videos: videos.length,
|
|
292
325
|
downloaded: completed,
|
|
293
326
|
failed,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
327
|
+
}));
|
|
328
|
+
for (const v of downloaded) {
|
|
329
|
+
console.log(JSON.stringify(v));
|
|
330
|
+
}
|
|
297
331
|
}
|
|
298
332
|
finally {
|
|
299
333
|
await closeBrowserSafely(browser, browserContext);
|
package/esm/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":";AACA,OAAO,sBAAsB,CAAC;AAO9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":";AACA,OAAO,sBAAsB,CAAC;AAO9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAwEnF;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GACA,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,iBAAiB,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBhH;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,EACb,MAAM,EAAE,YAAY,EACpB,GAAG,EAAE,MAAM,GACV,IAAI,CAyBN;AA2CD,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,CAAC"}
|
package/esm/src/index.js
CHANGED
|
@@ -11,13 +11,15 @@ import denoJson from "../deno.js";
|
|
|
11
11
|
import { registerCoursesCommand } from "./commands/courses.js";
|
|
12
12
|
import { registerVideosCommand } from "./commands/videos.js";
|
|
13
13
|
import { registerQuizzesCommand } from "./commands/quizzes.js";
|
|
14
|
-
import {
|
|
14
|
+
import { registerCommand } from "./commands/auth.js";
|
|
15
15
|
import { registerMaterialsCommand } from "./commands/materials.js";
|
|
16
16
|
import { registerGradesCommand } from "./commands/grades.js";
|
|
17
17
|
import { registerForumsCommand } from "./commands/forums.js";
|
|
18
18
|
import { registerAnnouncementsCommand } from "./commands/announcements.js";
|
|
19
19
|
import { registerCalendarCommand } from "./commands/calendar.js";
|
|
20
20
|
import { registerSkillsCommand } from "./commands/skills.js";
|
|
21
|
+
import { registerAssignmentsCommand } from "./commands/assignments.js";
|
|
22
|
+
import { registerUploadCommand } from "./commands/upload.js";
|
|
21
23
|
const program = new Command();
|
|
22
24
|
program
|
|
23
25
|
.name("openape")
|
|
@@ -32,7 +34,7 @@ program
|
|
|
32
34
|
.option("--silent", "Suppress all log output (JSON only)")
|
|
33
35
|
.option("--headed", "Run browser in visible mode");
|
|
34
36
|
// Register subcommands
|
|
35
|
-
|
|
37
|
+
registerCommand(program);
|
|
36
38
|
registerCoursesCommand(program);
|
|
37
39
|
registerVideosCommand(program);
|
|
38
40
|
registerQuizzesCommand(program);
|
|
@@ -42,6 +44,8 @@ registerForumsCommand(program);
|
|
|
42
44
|
registerAnnouncementsCommand(program);
|
|
43
45
|
registerCalendarCommand(program);
|
|
44
46
|
registerSkillsCommand(program);
|
|
47
|
+
registerAssignmentsCommand(program);
|
|
48
|
+
registerUploadCommand(program);
|
|
45
49
|
/**
|
|
46
50
|
* Load configuration and authenticate, returning the context for commands.
|
|
47
51
|
*/
|
|
@@ -84,7 +88,14 @@ export async function createSessionContext(options) {
|
|
|
84
88
|
*/
|
|
85
89
|
export function formatAndOutput(data, format, log) {
|
|
86
90
|
if (format === "json") {
|
|
87
|
-
|
|
91
|
+
if (Array.isArray(data)) {
|
|
92
|
+
for (const item of data) {
|
|
93
|
+
console.log(JSON.stringify(item));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log(JSON.stringify(data));
|
|
98
|
+
}
|
|
88
99
|
// Exit immediately for AI agent - no need to wait for browser cleanup
|
|
89
100
|
process.exit(0);
|
|
90
101
|
}
|
package/esm/src/lib/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Browser, type BrowserContext, type Page } from "playwright-core";
|
|
2
|
-
import type { AppConfig, Logger } from "./types.js";
|
|
2
|
+
import type { AppConfig, Logger, OutputFormat } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Find a Chromium-based browser executable on Windows.
|
|
5
5
|
* Priority: Edge → Chrome → Brave
|
|
@@ -20,6 +20,28 @@ export declare function launchAuthenticated(config: AppConfig, log: Logger): Pro
|
|
|
20
20
|
* Safely close browser and context with timeout.
|
|
21
21
|
* Designed for AI agent usage - no human interaction needed.
|
|
22
22
|
* If noWait is true, initiates cleanup but doesn't wait for completion.
|
|
23
|
+
*
|
|
24
|
+
* Note: Closes sequentially (context first, then browser) to avoid libuv
|
|
25
|
+
* assertion failures on Windows when handles are closed concurrently.
|
|
23
26
|
*/
|
|
24
27
|
export declare function closeBrowserSafely(browser: Browser, context?: BrowserContext, timeoutMs?: number, noWait?: boolean): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Create API context for WS token operations (no browser required).
|
|
30
|
+
* Returns null if session is invalid or WS token is missing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createApiContext(options: {
|
|
33
|
+
verbose?: boolean;
|
|
34
|
+
headed?: boolean;
|
|
35
|
+
}, command?: {
|
|
36
|
+
optsWithGlobals(): {
|
|
37
|
+
output?: OutputFormat;
|
|
38
|
+
verbose?: boolean;
|
|
39
|
+
};
|
|
40
|
+
}): Promise<{
|
|
41
|
+
log: Logger;
|
|
42
|
+
session: {
|
|
43
|
+
wsToken: string;
|
|
44
|
+
moodleBaseUrl: string;
|
|
45
|
+
};
|
|
46
|
+
} | null>;
|
|
25
47
|
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/lib/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACzF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/lib/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACzF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAGlE;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAuBrC;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA4DtF;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,cAAc,EACxB,SAAS,GAAE,MAAa,EACxB,MAAM,GAAE,OAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAoHD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAChD,OAAO,CAAC,EAAE;IAAE,eAAe,IAAI;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GAC5E,OAAO,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD,GAAG,IAAI,CAAC,CA0BR"}
|
package/esm/src/lib/auth.js
CHANGED
|
@@ -88,17 +88,20 @@ export async function launchAuthenticated(config, log) {
|
|
|
88
88
|
* Safely close browser and context with timeout.
|
|
89
89
|
* Designed for AI agent usage - no human interaction needed.
|
|
90
90
|
* If noWait is true, initiates cleanup but doesn't wait for completion.
|
|
91
|
+
*
|
|
92
|
+
* Note: Closes sequentially (context first, then browser) to avoid libuv
|
|
93
|
+
* assertion failures on Windows when handles are closed concurrently.
|
|
91
94
|
*/
|
|
92
95
|
export async function closeBrowserSafely(browser, context, timeoutMs = 5000, noWait = false) {
|
|
93
96
|
const closePromises = [];
|
|
94
|
-
// Close context with error handling
|
|
97
|
+
// Close context first with error handling
|
|
95
98
|
if (context) {
|
|
96
99
|
closePromises.push(Promise.race([
|
|
97
100
|
context.close().catch(() => { }),
|
|
98
101
|
new Promise(resolve => setTimeout(() => resolve(), timeoutMs))
|
|
99
102
|
]));
|
|
100
103
|
}
|
|
101
|
-
// Close browser with error handling
|
|
104
|
+
// Close browser after context with error handling
|
|
102
105
|
closePromises.push(Promise.race([
|
|
103
106
|
browser.close().catch(() => { }),
|
|
104
107
|
new Promise(resolve => setTimeout(() => resolve(), timeoutMs))
|
|
@@ -108,7 +111,10 @@ export async function closeBrowserSafely(browser, context, timeoutMs = 5000, noW
|
|
|
108
111
|
Promise.allSettled(closePromises);
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
111
|
-
|
|
114
|
+
// Wait sequentially to avoid libuv issues on Windows
|
|
115
|
+
for (const promise of closePromises) {
|
|
116
|
+
await promise.catch(() => { });
|
|
117
|
+
}
|
|
112
118
|
}
|
|
113
119
|
/**
|
|
114
120
|
* Attempt to restore a session from stored state.
|
|
@@ -192,3 +198,30 @@ async function login(page, config, log) {
|
|
|
192
198
|
}
|
|
193
199
|
log.success("Login completed successfully.");
|
|
194
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Create API context for WS token operations (no browser required).
|
|
203
|
+
* Returns null if session is invalid or WS token is missing.
|
|
204
|
+
*/
|
|
205
|
+
export async function createApiContext(options, command) {
|
|
206
|
+
const { createLogger } = await import("./logger.js");
|
|
207
|
+
const { loadWsToken } = await import("./token.js");
|
|
208
|
+
const { getOutputFormat, getSessionPath } = await import("./utils.js");
|
|
209
|
+
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
210
|
+
const outputFormat = command ? getOutputFormat(command) : "json";
|
|
211
|
+
const silent = outputFormat === "json" && !opts.verbose;
|
|
212
|
+
const log = createLogger(opts.verbose, silent);
|
|
213
|
+
const sessionPath = getSessionPath();
|
|
214
|
+
// Try to load WS token
|
|
215
|
+
const wsToken = loadWsToken(sessionPath);
|
|
216
|
+
if (!wsToken) {
|
|
217
|
+
log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
log,
|
|
222
|
+
session: {
|
|
223
|
+
wsToken,
|
|
224
|
+
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|