@mo7yw4ng/openape 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/esm/deno.js +1 -1
- package/esm/src/commands/announcements.d.ts.map +1 -1
- package/esm/src/commands/announcements.js +22 -85
- package/esm/src/commands/assignments.d.ts.map +1 -1
- package/esm/src/commands/assignments.js +2 -3
- package/esm/src/commands/calendar.d.ts.map +1 -1
- package/esm/src/commands/calendar.js +32 -84
- package/esm/src/commands/courses.d.ts.map +1 -1
- package/esm/src/commands/courses.js +2 -38
- package/esm/src/commands/forums.d.ts.map +1 -1
- package/esm/src/commands/forums.js +47 -175
- package/esm/src/commands/grades.d.ts.map +1 -1
- package/esm/src/commands/grades.js +10 -47
- package/esm/src/commands/materials.d.ts.map +1 -1
- package/esm/src/commands/materials.js +47 -58
- package/esm/src/commands/quizzes.d.ts.map +1 -1
- package/esm/src/commands/quizzes.js +2 -37
- package/esm/src/commands/skills.js +3 -3
- package/esm/src/commands/upload.d.ts.map +1 -1
- package/esm/src/commands/upload.js +2 -5
- package/esm/src/commands/videos.d.ts.map +1 -1
- package/esm/src/commands/videos.js +6 -76
- package/esm/src/index.d.ts +2 -1
- package/esm/src/index.d.ts.map +1 -1
- package/esm/src/index.js +5 -1
- package/esm/src/lib/auth.d.ts +21 -2
- package/esm/src/lib/auth.d.ts.map +1 -1
- package/esm/src/lib/auth.js +78 -19
- package/esm/src/lib/logger.d.ts +2 -2
- package/esm/src/lib/logger.d.ts.map +1 -1
- package/esm/src/lib/logger.js +1 -2
- package/esm/src/lib/moodle.d.ts +14 -0
- package/esm/src/lib/moodle.d.ts.map +1 -1
- package/esm/src/lib/moodle.js +35 -0
- package/esm/src/lib/utils.d.ts +3 -8
- package/esm/src/lib/utils.d.ts.map +1 -1
- package/esm/src/lib/utils.js +3 -10
- package/package.json +1 -1
- package/script/deno.js +1 -1
- package/script/src/commands/announcements.d.ts.map +1 -1
- package/script/src/commands/announcements.js +23 -89
- package/script/src/commands/assignments.d.ts.map +1 -1
- package/script/src/commands/assignments.js +2 -3
- package/script/src/commands/calendar.d.ts.map +1 -1
- package/script/src/commands/calendar.js +33 -85
- package/script/src/commands/courses.d.ts.map +1 -1
- package/script/src/commands/courses.js +9 -48
- package/script/src/commands/forums.d.ts.map +1 -1
- package/script/src/commands/forums.js +50 -181
- package/script/src/commands/grades.d.ts.map +1 -1
- package/script/src/commands/grades.js +14 -54
- package/script/src/commands/materials.d.ts.map +1 -1
- package/script/src/commands/materials.js +47 -58
- package/script/src/commands/quizzes.d.ts.map +1 -1
- package/script/src/commands/quizzes.js +11 -49
- package/script/src/commands/skills.js +3 -3
- package/script/src/commands/upload.d.ts.map +1 -1
- package/script/src/commands/upload.js +2 -5
- package/script/src/commands/videos.d.ts.map +1 -1
- package/script/src/commands/videos.js +11 -81
- package/script/src/index.d.ts +2 -1
- package/script/src/index.d.ts.map +1 -1
- package/script/src/index.js +5 -1
- package/script/src/lib/auth.d.ts +21 -2
- package/script/src/lib/auth.d.ts.map +1 -1
- package/script/src/lib/auth.js +83 -56
- package/script/src/lib/logger.d.ts +2 -2
- package/script/src/lib/logger.d.ts.map +1 -1
- package/script/src/lib/logger.js +1 -2
- package/script/src/lib/moodle.d.ts +14 -0
- package/script/src/lib/moodle.d.ts.map +1 -1
- package/script/src/lib/moodle.js +36 -0
- package/script/src/lib/utils.d.ts +3 -8
- package/script/src/lib/utils.d.ts.map +1 -1
- package/script/src/lib/utils.js +3 -11
- package/skills/openape/SKILL.md +6 -6
|
@@ -1,62 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getEnrolledCoursesApi, getForumsApi, getForumDiscussionsApi, getDiscussionPostsApi, addForumDiscussionApi, addForumPostApi, deleteForumPostApi } from "../lib/moodle.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import fs from "node:fs";
|
|
1
|
+
import { stripHtmlTags, getOutputFormat, formatTimestamp } from "../lib/utils.js";
|
|
2
|
+
import { getEnrolledCoursesApi, getForumsApi, getForumDiscussionsApi, getDiscussionPostsApi, addForumDiscussionApi, addForumPostApi, deleteForumPostApi, resolveForumId } from "../lib/moodle.js";
|
|
3
|
+
import { createApiContext } from "../lib/auth.js";
|
|
4
|
+
import { formatAndOutput } from "../index.js";
|
|
7
5
|
export function registerForumsCommand(program) {
|
|
8
6
|
const forumsCmd = program.command("forums");
|
|
9
7
|
forumsCmd.description("Forum operations");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
13
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
14
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
15
|
-
const log = createLogger(opts.verbose, silent, outputFormat);
|
|
16
|
-
const baseDir = getBaseDir();
|
|
17
|
-
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
18
|
-
// Check if session exists
|
|
19
|
-
if (!fs.existsSync(sessionPath)) {
|
|
20
|
-
console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
21
|
-
log.info(`Session 預期位置: ${sessionPath}`);
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
// Try to load WS token
|
|
25
|
-
const wsToken = loadWsToken(sessionPath);
|
|
26
|
-
if (!wsToken) {
|
|
27
|
-
console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
// Try to load sesskey from cache
|
|
31
|
-
const sesskey = loadSesskey(sessionPath) || undefined;
|
|
32
|
-
return {
|
|
33
|
-
log,
|
|
34
|
-
session: {
|
|
35
|
-
wsToken,
|
|
36
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
37
|
-
sesskey,
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
forumsCmd
|
|
42
|
-
.command("list")
|
|
43
|
-
.description("List forums from in-progress courses")
|
|
44
|
-
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
45
|
-
.action(async (options, command) => {
|
|
46
|
-
const apiContext = await createApiContext(options, command);
|
|
8
|
+
async function listForums(classification) {
|
|
9
|
+
const apiContext = await createApiContext({});
|
|
47
10
|
if (!apiContext) {
|
|
48
11
|
process.exitCode = 1;
|
|
49
12
|
return;
|
|
50
13
|
}
|
|
51
14
|
const courses = await getEnrolledCoursesApi(apiContext.session, {
|
|
52
|
-
classification
|
|
15
|
+
classification,
|
|
53
16
|
});
|
|
54
|
-
// Get forums via WS API (no browser needed!)
|
|
55
17
|
const courseIds = courses.map(c => c.id);
|
|
56
18
|
const wsForums = await getForumsApi(apiContext.session, courseIds);
|
|
19
|
+
const courseMap = new Map(courses.map(c => [c.id, c]));
|
|
57
20
|
const allForums = [];
|
|
58
21
|
for (const wsForum of wsForums) {
|
|
59
|
-
const course =
|
|
22
|
+
const course = courseMap.get(wsForum.courseid);
|
|
60
23
|
if (course) {
|
|
61
24
|
allForums.push({
|
|
62
25
|
course_id: wsForum.courseid,
|
|
@@ -66,62 +29,22 @@ export function registerForumsCommand(program) {
|
|
|
66
29
|
forum_id: wsForum.id,
|
|
67
30
|
name: wsForum.name,
|
|
68
31
|
timemodified: wsForum.timemodified,
|
|
69
|
-
// url: `https://ilearning.cycu.edu.tw/mod/forum/view.php?id=${wsForum.cmid}`,
|
|
70
32
|
});
|
|
71
33
|
}
|
|
72
34
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (const forum of allForums) {
|
|
80
|
-
console.log(JSON.stringify(forum));
|
|
81
|
-
}
|
|
82
|
-
});
|
|
35
|
+
formatAndOutput(allForums, "json", apiContext.log, { status: "success", timestamp: new Date().toISOString(), total_courses: courses.length, total_forums: allForums.length });
|
|
36
|
+
}
|
|
37
|
+
forumsCmd
|
|
38
|
+
.command("list")
|
|
39
|
+
.description("List forums from in-progress courses")
|
|
40
|
+
.action(() => listForums("inprogress"));
|
|
83
41
|
forumsCmd
|
|
84
42
|
.command("list-all")
|
|
85
43
|
.description("List all forums across all courses")
|
|
86
44
|
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
87
|
-
.
|
|
88
|
-
.action(async (options, command) => {
|
|
89
|
-
const apiContext = await createApiContext(options, command);
|
|
90
|
-
if (!apiContext) {
|
|
91
|
-
process.exitCode = 1;
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
45
|
+
.action(async (options) => {
|
|
94
46
|
const classification = options.level === "all" ? undefined : "inprogress";
|
|
95
|
-
|
|
96
|
-
classification,
|
|
97
|
-
});
|
|
98
|
-
// Get forums via WS API (no browser needed!)
|
|
99
|
-
const courseIds = courses.map(c => c.id);
|
|
100
|
-
const wsForums = await getForumsApi(apiContext.session, courseIds);
|
|
101
|
-
const allForums = [];
|
|
102
|
-
for (const wsForum of wsForums) {
|
|
103
|
-
const course = courses.find(c => c.id === wsForum.courseid);
|
|
104
|
-
if (course) {
|
|
105
|
-
allForums.push({
|
|
106
|
-
course_id: wsForum.courseid,
|
|
107
|
-
course_name: course.fullname,
|
|
108
|
-
intro: wsForum.intro,
|
|
109
|
-
cmid: wsForum.cmid.toString(),
|
|
110
|
-
forum_id: wsForum.id,
|
|
111
|
-
name: wsForum.name,
|
|
112
|
-
timemodified: wsForum.timemodified,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
console.log(JSON.stringify({
|
|
117
|
-
status: "success",
|
|
118
|
-
timestamp: new Date().toISOString(),
|
|
119
|
-
total_courses: courses.length,
|
|
120
|
-
total_forums: allForums.length,
|
|
121
|
-
}));
|
|
122
|
-
for (const forum of allForums) {
|
|
123
|
-
console.log(JSON.stringify(forum));
|
|
124
|
-
}
|
|
47
|
+
await listForums(classification);
|
|
125
48
|
});
|
|
126
49
|
forumsCmd
|
|
127
50
|
.command("discussions")
|
|
@@ -129,51 +52,29 @@ export function registerForumsCommand(program) {
|
|
|
129
52
|
.argument("<forum-id>", "Forum ID")
|
|
130
53
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
131
54
|
.action(async (forumId, options, command) => {
|
|
55
|
+
const output = getOutputFormat(command);
|
|
132
56
|
const apiContext = await createApiContext(options, command);
|
|
133
57
|
if (!apiContext) {
|
|
134
58
|
process.exitCode = 1;
|
|
135
59
|
return;
|
|
136
60
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
});
|
|
141
|
-
// Get forums via WS API
|
|
142
|
-
const courseIds = courses.map(c => c.id);
|
|
143
|
-
const wsForums = await getForumsApi(apiContext.session, courseIds);
|
|
144
|
-
// Find forum by cmid or instance ID
|
|
145
|
-
const targetForum = wsForums.find(f => f.cmid.toString() === forumId || f.id === parseInt(forumId, 10));
|
|
146
|
-
if (!targetForum) {
|
|
147
|
-
console.log(JSON.stringify({ status: "error", error: "Forum not found" }));
|
|
61
|
+
const resolved = await resolveForumId(apiContext.session, forumId);
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
apiContext.log.error("Forum not found");
|
|
148
64
|
process.exitCode = 1;
|
|
149
65
|
return;
|
|
150
66
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
course_name: course?.fullname,
|
|
163
|
-
total_discussions: discussions.length,
|
|
164
|
-
};
|
|
165
|
-
console.log(JSON.stringify(meta));
|
|
166
|
-
for (const d of discussions) {
|
|
167
|
-
console.log(JSON.stringify({
|
|
168
|
-
id: d.id,
|
|
169
|
-
name: d.name,
|
|
170
|
-
user_id: d.userId,
|
|
171
|
-
time_modified: d.timeModified,
|
|
172
|
-
post_count: d.postCount,
|
|
173
|
-
unread: d.unread,
|
|
174
|
-
message: stripHtmlTags(d.message || ""),
|
|
175
|
-
}));
|
|
176
|
-
}
|
|
67
|
+
const discussions = await getForumDiscussionsApi(apiContext.session, resolved.forumId);
|
|
68
|
+
const items = discussions.map(d => ({
|
|
69
|
+
id: d.id,
|
|
70
|
+
name: d.name,
|
|
71
|
+
user_id: d.userId,
|
|
72
|
+
time_modified: d.timeModified,
|
|
73
|
+
post_count: d.postCount,
|
|
74
|
+
unread: d.unread,
|
|
75
|
+
message: stripHtmlTags(d.message || ""),
|
|
76
|
+
}));
|
|
77
|
+
formatAndOutput(items, output, apiContext.log, { status: "success", timestamp: new Date().toISOString(), forum_id: resolved.forumId, forum_name: resolved.name ?? null, course_id: resolved.courseid ?? null, total_discussions: discussions.length });
|
|
177
78
|
});
|
|
178
79
|
forumsCmd
|
|
179
80
|
.command("posts")
|
|
@@ -188,38 +89,17 @@ export function registerForumsCommand(program) {
|
|
|
188
89
|
return;
|
|
189
90
|
}
|
|
190
91
|
const posts = await getDiscussionPostsApi(apiContext.session, parseInt(discussionId, 10));
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
modified: formatTimestamp(p.modified),
|
|
203
|
-
message: p.message,
|
|
204
|
-
unread: p.unread,
|
|
205
|
-
})),
|
|
206
|
-
summary: {
|
|
207
|
-
total_posts: posts.length,
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
console.log(JSON.stringify(result));
|
|
211
|
-
}
|
|
212
|
-
else if (output === "table") {
|
|
213
|
-
console.log(`Discussion ${discussionId} - ${posts.length} posts`);
|
|
214
|
-
console.log("Use --output json to see full post content");
|
|
215
|
-
const tablePosts = posts.map(p => ({
|
|
216
|
-
id: p.id,
|
|
217
|
-
subject: p.subject.substring(0, 50) + (p.subject.length > 50 ? "..." : ""),
|
|
218
|
-
author: p.author,
|
|
219
|
-
created: new Date(p.created * 1000).toLocaleString(),
|
|
220
|
-
}));
|
|
221
|
-
console.table(tablePosts);
|
|
222
|
-
}
|
|
92
|
+
const items = posts.map(p => ({
|
|
93
|
+
id: p.id,
|
|
94
|
+
subject: p.subject,
|
|
95
|
+
author: p.author,
|
|
96
|
+
author_id: p.authorId,
|
|
97
|
+
created: formatTimestamp(p.created),
|
|
98
|
+
modified: formatTimestamp(p.modified),
|
|
99
|
+
message: p.message,
|
|
100
|
+
unread: p.unread,
|
|
101
|
+
}));
|
|
102
|
+
formatAndOutput(items, output, apiContext.log, { status: "success", timestamp: new Date().toISOString(), discussion_id: discussionId, total_posts: posts.length });
|
|
223
103
|
});
|
|
224
104
|
forumsCmd
|
|
225
105
|
.command("post")
|
|
@@ -236,22 +116,14 @@ export function registerForumsCommand(program) {
|
|
|
236
116
|
return;
|
|
237
117
|
}
|
|
238
118
|
const { log, session } = apiContext;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
classification: "inprogress",
|
|
242
|
-
});
|
|
243
|
-
const courseIds = courses.map(c => c.id);
|
|
244
|
-
const wsForums = await getForumsApi(session, courseIds);
|
|
245
|
-
// Find forum by cmid or instance ID
|
|
246
|
-
const targetForum = wsForums.find(f => f.cmid.toString() === forumId || f.id === parseInt(forumId, 10));
|
|
247
|
-
if (!targetForum) {
|
|
119
|
+
const resolved = await resolveForumId(session, forumId);
|
|
120
|
+
if (!resolved) {
|
|
248
121
|
log.error(`Forum not found: ${forumId}`);
|
|
249
122
|
process.exitCode = 1;
|
|
250
123
|
return;
|
|
251
124
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const result = await addForumDiscussionApi(session, targetForum.id, subject, message);
|
|
125
|
+
log.info(`Posting to forum: ${resolved.name ?? forumId}`);
|
|
126
|
+
const result = await addForumDiscussionApi(session, resolved.forumId, subject, message);
|
|
255
127
|
if (result.success) {
|
|
256
128
|
log.success(`✓ Discussion posted successfully!`);
|
|
257
129
|
log.info(` Discussion ID: ${result.discussionId}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grades.d.ts","sourceRoot":"","sources":["../../../src/src/commands/grades.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"grades.d.ts","sourceRoot":"","sources":["../../../src/src/commands/grades.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+F5D"}
|
|
@@ -1,45 +1,10 @@
|
|
|
1
|
-
import { getBaseDir } from "../lib/utils.js";
|
|
2
1
|
import { getEnrolledCoursesApi, getCourseGradesApi } from "../lib/moodle.js";
|
|
3
|
-
import {
|
|
4
|
-
import { loadWsToken } from "../lib/token.js";
|
|
2
|
+
import { createApiContext } from "../lib/auth.js";
|
|
5
3
|
import { formatAndOutput } from "../index.js";
|
|
6
|
-
import
|
|
7
|
-
import fs from "node:fs";
|
|
4
|
+
import { getOutputFormat } from "../lib/utils.js";
|
|
8
5
|
export function registerGradesCommand(program) {
|
|
9
6
|
const gradesCmd = program.command("grades");
|
|
10
7
|
gradesCmd.description("Grade operations");
|
|
11
|
-
function getOutputFormat(command) {
|
|
12
|
-
const opts = command.optsWithGlobals();
|
|
13
|
-
return opts.output || "json";
|
|
14
|
-
}
|
|
15
|
-
// Pure API context - no browser required (fast!)
|
|
16
|
-
async function createApiContext(options, command) {
|
|
17
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
18
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
19
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
20
|
-
const log = createLogger(opts.verbose, silent, outputFormat);
|
|
21
|
-
const baseDir = getBaseDir();
|
|
22
|
-
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
23
|
-
// Check if session exists
|
|
24
|
-
if (!fs.existsSync(sessionPath)) {
|
|
25
|
-
console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
26
|
-
log.info(`Session 預期位置: ${sessionPath}`);
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
// Try to load WS token
|
|
30
|
-
const wsToken = loadWsToken(sessionPath);
|
|
31
|
-
if (!wsToken) {
|
|
32
|
-
console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
log,
|
|
37
|
-
session: {
|
|
38
|
-
wsToken,
|
|
39
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
8
|
gradesCmd
|
|
44
9
|
.command("summary")
|
|
45
10
|
.description("Show grade summary across all courses")
|
|
@@ -52,9 +17,13 @@ export function registerGradesCommand(program) {
|
|
|
52
17
|
return;
|
|
53
18
|
}
|
|
54
19
|
const courses = await getEnrolledCoursesApi(apiContext.session);
|
|
20
|
+
const gradeResults = await Promise.allSettled(courses.map(course => getCourseGradesApi(apiContext.session, course.id)
|
|
21
|
+
.then(grades => ({ course, grades }))));
|
|
55
22
|
const gradeSummaries = [];
|
|
56
|
-
for (const
|
|
57
|
-
|
|
23
|
+
for (const result of gradeResults) {
|
|
24
|
+
if (result.status !== "fulfilled")
|
|
25
|
+
continue;
|
|
26
|
+
const { course, grades } = result.value;
|
|
58
27
|
gradeSummaries.push({
|
|
59
28
|
courseId: course.id,
|
|
60
29
|
courseName: course.fullname,
|
|
@@ -64,19 +33,13 @@ export function registerGradesCommand(program) {
|
|
|
64
33
|
totalUsers: grades.totalUsers,
|
|
65
34
|
});
|
|
66
35
|
}
|
|
67
|
-
// Calculate overall statistics
|
|
68
36
|
const gradedCourses = gradeSummaries.filter(g => g.grade !== undefined && g.grade !== null && g.grade !== "-");
|
|
69
37
|
const averageRank = gradeSummaries
|
|
70
38
|
.filter(g => g.rank !== undefined && g.rank !== null)
|
|
71
39
|
.reduce((sum, g) => sum + (g.rank || 0), 0) /
|
|
72
40
|
(gradeSummaries.filter(g => g.rank !== undefined && g.rank !== null).length || 1);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
graded_courses: gradedCourses.length,
|
|
76
|
-
average_rank: averageRank.toFixed(1),
|
|
77
|
-
grades: gradeSummaries,
|
|
78
|
-
};
|
|
79
|
-
formatAndOutput(summaryData, output, apiContext.log);
|
|
41
|
+
apiContext.log.info(`Total: ${courses.length} courses, ${gradedCourses.length} graded, avg rank: ${averageRank.toFixed(1)}`);
|
|
42
|
+
formatAndOutput(gradeSummaries, output, apiContext.log);
|
|
80
43
|
});
|
|
81
44
|
gradesCmd
|
|
82
45
|
.command("course")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../../src/src/commands/materials.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4BpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../../src/src/commands/materials.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4BpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwd/D"}
|
|
@@ -91,6 +91,7 @@ export function registerMaterialsCommand(program) {
|
|
|
91
91
|
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
92
92
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
93
93
|
.action(async (options, command) => {
|
|
94
|
+
const output = getOutputFormat(command);
|
|
94
95
|
const apiContext = await createApiContext(options, command);
|
|
95
96
|
if (!apiContext) {
|
|
96
97
|
process.exitCode = 1;
|
|
@@ -100,10 +101,8 @@ export function registerMaterialsCommand(program) {
|
|
|
100
101
|
const courses = await getEnrolledCoursesApi(apiContext.session, {
|
|
101
102
|
classification,
|
|
102
103
|
});
|
|
103
|
-
// Get materials via WS API (no browser needed!)
|
|
104
104
|
const courseIds = courses.map(c => c.id);
|
|
105
105
|
const apiResources = await getResourcesByCoursesApi(apiContext.session, courseIds);
|
|
106
|
-
// Build a map of courseId -> course for quick lookup
|
|
107
106
|
const courseMap = new Map(courses.map(c => [c.id, c]));
|
|
108
107
|
const allMaterials = [];
|
|
109
108
|
for (const resource of apiResources) {
|
|
@@ -122,31 +121,27 @@ export function registerMaterialsCommand(program) {
|
|
|
122
121
|
});
|
|
123
122
|
}
|
|
124
123
|
}
|
|
125
|
-
const
|
|
124
|
+
const items = allMaterials.map(m => ({
|
|
125
|
+
course_id: m.course_id,
|
|
126
|
+
course_name: m.course_name,
|
|
127
|
+
id: m.cmid,
|
|
128
|
+
name: m.name,
|
|
129
|
+
type: m.modType,
|
|
130
|
+
mimetype: m.mimetype,
|
|
131
|
+
filesize: m.filesize,
|
|
132
|
+
modified: m.modified ? new Date(m.modified * 1000).toISOString() : null,
|
|
133
|
+
url: m.url,
|
|
134
|
+
}));
|
|
135
|
+
formatAndOutput(items, output, apiContext.log, {
|
|
126
136
|
status: "success",
|
|
127
137
|
timestamp: new Date().toISOString(),
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
mimetype: m.mimetype,
|
|
136
|
-
filesize: m.filesize,
|
|
137
|
-
modified: m.modified ? new Date(m.modified * 1000).toISOString() : null,
|
|
138
|
-
url: m.url,
|
|
139
|
-
})),
|
|
140
|
-
summary: {
|
|
141
|
-
total_courses: courses.length,
|
|
142
|
-
total_materials: allMaterials.length,
|
|
143
|
-
by_type: allMaterials.reduce((acc, m) => {
|
|
144
|
-
acc[m.modType] = (acc[m.modType] || 0) + 1;
|
|
145
|
-
return acc;
|
|
146
|
-
}, {}),
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
console.log(JSON.stringify(output));
|
|
138
|
+
total_courses: courses.length,
|
|
139
|
+
total_materials: allMaterials.length,
|
|
140
|
+
by_type: allMaterials.reduce((acc, m) => {
|
|
141
|
+
acc[m.modType] = (acc[m.modType] || 0) + 1;
|
|
142
|
+
return acc;
|
|
143
|
+
}, {}),
|
|
144
|
+
});
|
|
150
145
|
});
|
|
151
146
|
materialsCmd
|
|
152
147
|
.command("download")
|
|
@@ -177,24 +172,21 @@ export function registerMaterialsCommand(program) {
|
|
|
177
172
|
downloadedFiles.push(result);
|
|
178
173
|
}
|
|
179
174
|
}
|
|
180
|
-
const
|
|
175
|
+
const items = downloadedFiles.map(f => ({
|
|
176
|
+
filename: f.filename,
|
|
177
|
+
path: f.path,
|
|
178
|
+
size: f.size,
|
|
179
|
+
course_id: f.course_id,
|
|
180
|
+
course_name: f.course_name,
|
|
181
|
+
}));
|
|
182
|
+
formatAndOutput(items, "json", log, {
|
|
181
183
|
status: "success",
|
|
182
184
|
timestamp: new Date().toISOString(),
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
course_name: f.course_name,
|
|
189
|
-
})),
|
|
190
|
-
summary: {
|
|
191
|
-
total_materials: materials.length,
|
|
192
|
-
downloaded: downloadedFiles.length,
|
|
193
|
-
skipped: materials.length - downloadedFiles.length,
|
|
194
|
-
total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
|
|
195
|
-
},
|
|
196
|
-
};
|
|
197
|
-
console.log(JSON.stringify(output));
|
|
185
|
+
total_materials: materials.length,
|
|
186
|
+
downloaded: downloadedFiles.length,
|
|
187
|
+
skipped: materials.length - downloadedFiles.length,
|
|
188
|
+
total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
|
|
189
|
+
});
|
|
198
190
|
});
|
|
199
191
|
materialsCmd
|
|
200
192
|
.command("download-all")
|
|
@@ -222,25 +214,22 @@ export function registerMaterialsCommand(program) {
|
|
|
222
214
|
downloadedFiles.push(result);
|
|
223
215
|
}
|
|
224
216
|
}
|
|
225
|
-
const
|
|
217
|
+
const items = downloadedFiles.map(f => ({
|
|
218
|
+
filename: f.filename,
|
|
219
|
+
path: f.path,
|
|
220
|
+
size: f.size,
|
|
221
|
+
course_id: f.course_id,
|
|
222
|
+
course_name: f.course_name,
|
|
223
|
+
}));
|
|
224
|
+
formatAndOutput(items, "json", log, {
|
|
226
225
|
status: "success",
|
|
227
226
|
timestamp: new Date().toISOString(),
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
})),
|
|
235
|
-
summary: {
|
|
236
|
-
total_courses: courses.length,
|
|
237
|
-
total_materials: allMaterials.length,
|
|
238
|
-
downloaded: downloadedFiles.length,
|
|
239
|
-
skipped: allMaterials.length - downloadedFiles.length,
|
|
240
|
-
total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
console.log(JSON.stringify(output));
|
|
227
|
+
total_courses: courses.length,
|
|
228
|
+
total_materials: allMaterials.length,
|
|
229
|
+
downloaded: downloadedFiles.length,
|
|
230
|
+
skipped: allMaterials.length - downloadedFiles.length,
|
|
231
|
+
total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
|
|
232
|
+
});
|
|
244
233
|
});
|
|
245
234
|
materialsCmd
|
|
246
235
|
.command("complete")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quizzes.d.ts","sourceRoot":"","sources":["../../../src/src/commands/quizzes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"quizzes.d.ts","sourceRoot":"","sources":["../../../src/src/commands/quizzes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsEpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2N7D"}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatTimestamp, getOutputFormat } from "../lib/utils.js";
|
|
2
2
|
import { getEnrolledCoursesApi, getQuizzesByCoursesApi, startQuizAttemptApi, getQuizAttemptDataApi, getAllQuizAttemptDataApi, processQuizAttemptApi } from "../lib/moodle.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createApiContext } from "../lib/auth.js";
|
|
4
4
|
import { formatAndOutput } from "../index.js";
|
|
5
|
-
import { loadWsToken } from "../lib/token.js";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import fs from "node:fs";
|
|
8
5
|
function stripHtmlKeepLines(html) {
|
|
9
6
|
return html
|
|
10
7
|
.replace(/<br\s*\/?>/gi, "\n")
|
|
@@ -59,38 +56,6 @@ function parseQuizQuestions(questions) {
|
|
|
59
56
|
export function registerQuizzesCommand(program) {
|
|
60
57
|
const quizzesCmd = program.command("quizzes");
|
|
61
58
|
quizzesCmd.description("Quiz operations");
|
|
62
|
-
function getOutputFormat(command) {
|
|
63
|
-
const opts = command.optsWithGlobals();
|
|
64
|
-
return opts.output || "json";
|
|
65
|
-
}
|
|
66
|
-
// Pure API context - no browser required (fast!)
|
|
67
|
-
async function createApiContext(options, command) {
|
|
68
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
69
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
70
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
71
|
-
const log = createLogger(opts.verbose, silent, outputFormat);
|
|
72
|
-
const baseDir = getBaseDir();
|
|
73
|
-
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
74
|
-
// Check if session exists
|
|
75
|
-
if (!fs.existsSync(sessionPath)) {
|
|
76
|
-
log.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
77
|
-
log.info(`Session 預期位置: ${sessionPath}`);
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
// Try to load WS token
|
|
81
|
-
const wsToken = loadWsToken(sessionPath);
|
|
82
|
-
if (!wsToken) {
|
|
83
|
-
log.error("未找到 WS token。請先執行 'openape login' 進行登入。");
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
log,
|
|
88
|
-
session: {
|
|
89
|
-
wsToken,
|
|
90
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
59
|
quizzesCmd
|
|
95
60
|
.command("list")
|
|
96
61
|
.description("List incomplete quizzes in a course")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
const SKILL_NAME = "openape";
|
|
5
6
|
const GITHUB_RAW_URL = `https://raw.githubusercontent.com/mo7yw4ng/openape/refs/heads/main/skills/${SKILL_NAME}/SKILL.md`;
|
|
6
7
|
/**
|
|
@@ -18,11 +19,10 @@ const PLATFORMS = {
|
|
|
18
19
|
async function readSkillContent() {
|
|
19
20
|
// Try local path first (relative to this file's location)
|
|
20
21
|
try {
|
|
21
|
-
const base = path.dirname(
|
|
22
|
-
const normalized = process.platform === "win32" ? base.replace(/^\//, "") : base;
|
|
22
|
+
const base = path.dirname(fileURLToPath(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url));
|
|
23
23
|
// When running from source: src/commands/ → ../../skills/openape/SKILL.md
|
|
24
24
|
// When bundled by dnt into build/: esm/commands/ or script/ → ../../skills/openape/SKILL.md
|
|
25
|
-
const localPath = path.resolve(
|
|
25
|
+
const localPath = path.resolve(base, "..", "..", "skills", SKILL_NAME, "SKILL.md");
|
|
26
26
|
return await fs.promises.readFile(localPath, "utf-8");
|
|
27
27
|
}
|
|
28
28
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/src/commands/upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/src/commands/upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwD5D"}
|
|
@@ -20,19 +20,16 @@ export function registerUploadCommand(program) {
|
|
|
20
20
|
process.exitCode = 1;
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
-
// Resolve file path
|
|
24
23
|
const resolvedPath = path.resolve(filePath);
|
|
25
|
-
|
|
24
|
+
let stats;
|
|
26
25
|
try {
|
|
27
|
-
await fs.
|
|
26
|
+
stats = await fs.stat(resolvedPath);
|
|
28
27
|
}
|
|
29
28
|
catch {
|
|
30
29
|
apiContext.log.error(`檔案不存在: ${filePath}`);
|
|
31
30
|
process.exitCode = 1;
|
|
32
31
|
return;
|
|
33
32
|
}
|
|
34
|
-
// Get file size
|
|
35
|
-
const stats = await fs.stat(resolvedPath);
|
|
36
33
|
const fileSizeKB = formatFileSize(stats.size);
|
|
37
34
|
apiContext.log.info(`上傳檔案: ${path.basename(resolvedPath)} (${fileSizeKB} KB)`);
|
|
38
35
|
// Upload file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"videos.d.ts","sourceRoot":"","sources":["../../../src/src/commands/videos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"videos.d.ts","sourceRoot":"","sources":["../../../src/src/commands/videos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgT5D"}
|