@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, stripHtmlTags } from "../lib/utils.js";
|
|
2
|
-
import { getEnrolledCoursesApi, getForumsApi, getForumDiscussionsApi, getDiscussionPostsApi } from "../lib/moodle.js";
|
|
1
|
+
import { getBaseDir, stripHtmlTags, getOutputFormat, formatTimestamp } from "../lib/utils.js";
|
|
2
|
+
import { getEnrolledCoursesApi, getForumsApi, getForumDiscussionsApi, getDiscussionPostsApi, addForumDiscussionApi, addForumPostApi, deleteForumPostApi } from "../lib/moodle.js";
|
|
3
3
|
import { createLogger } from "../lib/logger.js";
|
|
4
4
|
import { loadWsToken, loadSesskey } from "../lib/token.js";
|
|
5
5
|
import path from "node:path";
|
|
@@ -7,10 +7,6 @@ import fs from "node:fs";
|
|
|
7
7
|
export function registerForumsCommand(program) {
|
|
8
8
|
const forumsCmd = program.command("forums");
|
|
9
9
|
forumsCmd.description("Forum operations");
|
|
10
|
-
function getOutputFormat(command) {
|
|
11
|
-
const opts = command.optsWithGlobals();
|
|
12
|
-
return opts.output || "json";
|
|
13
|
-
}
|
|
14
10
|
// Pure API context - no browser required (fast!)
|
|
15
11
|
async function createApiContext(options, command) {
|
|
16
12
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
@@ -65,23 +61,24 @@ export function registerForumsCommand(program) {
|
|
|
65
61
|
allForums.push({
|
|
66
62
|
course_id: wsForum.courseid,
|
|
67
63
|
course_name: course.fullname,
|
|
64
|
+
intro: wsForum.intro,
|
|
68
65
|
cmid: wsForum.cmid.toString(),
|
|
69
66
|
forum_id: wsForum.id,
|
|
70
67
|
name: wsForum.name,
|
|
68
|
+
timemodified: wsForum.timemodified,
|
|
71
69
|
// url: `https://ilearning.cycu.edu.tw/mod/forum/view.php?id=${wsForum.cmid}`,
|
|
72
70
|
});
|
|
73
71
|
}
|
|
74
72
|
}
|
|
75
|
-
|
|
73
|
+
console.log(JSON.stringify({
|
|
76
74
|
status: "success",
|
|
77
75
|
timestamp: new Date().toISOString(),
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
console.log(JSON.stringify(result));
|
|
76
|
+
total_courses: courses.length,
|
|
77
|
+
total_forums: allForums.length,
|
|
78
|
+
}));
|
|
79
|
+
for (const forum of allForums) {
|
|
80
|
+
console.log(JSON.stringify(forum));
|
|
81
|
+
}
|
|
85
82
|
});
|
|
86
83
|
forumsCmd
|
|
87
84
|
.command("list-all")
|
|
@@ -108,30 +105,29 @@ export function registerForumsCommand(program) {
|
|
|
108
105
|
allForums.push({
|
|
109
106
|
course_id: wsForum.courseid,
|
|
110
107
|
course_name: course.fullname,
|
|
108
|
+
intro: wsForum.intro,
|
|
111
109
|
cmid: wsForum.cmid.toString(),
|
|
112
110
|
forum_id: wsForum.id,
|
|
113
111
|
name: wsForum.name,
|
|
114
|
-
|
|
112
|
+
timemodified: wsForum.timemodified,
|
|
115
113
|
});
|
|
116
114
|
}
|
|
117
115
|
}
|
|
118
|
-
|
|
116
|
+
console.log(JSON.stringify({
|
|
119
117
|
status: "success",
|
|
120
118
|
timestamp: new Date().toISOString(),
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
console.log(JSON.stringify(result));
|
|
119
|
+
total_courses: courses.length,
|
|
120
|
+
total_forums: allForums.length,
|
|
121
|
+
}));
|
|
122
|
+
for (const forum of allForums) {
|
|
123
|
+
console.log(JSON.stringify(forum));
|
|
124
|
+
}
|
|
128
125
|
});
|
|
129
126
|
forumsCmd
|
|
130
127
|
.command("discussions")
|
|
131
128
|
.description("List discussions in a forum (use forum ID)")
|
|
132
129
|
.argument("<forum-id>", "Forum ID")
|
|
133
130
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
134
|
-
.option("--msg", "Include message content in response")
|
|
135
131
|
.action(async (forumId, options, command) => {
|
|
136
132
|
const apiContext = await createApiContext(options, command);
|
|
137
133
|
if (!apiContext) {
|
|
@@ -155,32 +151,29 @@ export function registerForumsCommand(program) {
|
|
|
155
151
|
const course = courses.find(c => c.id === targetForum.courseid);
|
|
156
152
|
// Get discussions via WS API
|
|
157
153
|
const discussions = await getForumDiscussionsApi(apiContext.session, targetForum.id);
|
|
158
|
-
|
|
154
|
+
// Output NDJSON: one line per discussion entry for stream-friendly parsing
|
|
155
|
+
const meta = {
|
|
159
156
|
status: "success",
|
|
160
157
|
timestamp: new Date().toISOString(),
|
|
161
158
|
forum_id: targetForum.id,
|
|
162
159
|
forum_name: targetForum.name,
|
|
160
|
+
forum_intro: targetForum.intro,
|
|
163
161
|
course_id: course?.id,
|
|
164
162
|
course_name: course?.fullname,
|
|
165
|
-
|
|
166
|
-
const discussion = {
|
|
167
|
-
id: d.id,
|
|
168
|
-
name: d.name,
|
|
169
|
-
user_id: d.userId,
|
|
170
|
-
time_modified: d.timeModified,
|
|
171
|
-
post_count: d.postCount,
|
|
172
|
-
unread: d.unread,
|
|
173
|
-
};
|
|
174
|
-
if (options.msg) {
|
|
175
|
-
discussion.message = stripHtmlTags(d.message || "");
|
|
176
|
-
}
|
|
177
|
-
return discussion;
|
|
178
|
-
}),
|
|
179
|
-
summary: {
|
|
180
|
-
total_discussions: discussions.length,
|
|
181
|
-
},
|
|
163
|
+
total_discussions: discussions.length,
|
|
182
164
|
};
|
|
183
|
-
console.log(JSON.stringify(
|
|
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
|
+
}
|
|
184
177
|
});
|
|
185
178
|
forumsCmd
|
|
186
179
|
.command("posts")
|
|
@@ -205,8 +198,8 @@ export function registerForumsCommand(program) {
|
|
|
205
198
|
subject: p.subject,
|
|
206
199
|
author: p.author,
|
|
207
200
|
author_id: p.authorId,
|
|
208
|
-
created:
|
|
209
|
-
modified:
|
|
201
|
+
created: formatTimestamp(p.created),
|
|
202
|
+
modified: formatTimestamp(p.modified),
|
|
210
203
|
message: p.message,
|
|
211
204
|
unread: p.unread,
|
|
212
205
|
})),
|
|
@@ -228,4 +221,98 @@ export function registerForumsCommand(program) {
|
|
|
228
221
|
console.table(tablePosts);
|
|
229
222
|
}
|
|
230
223
|
});
|
|
224
|
+
forumsCmd
|
|
225
|
+
.command("post")
|
|
226
|
+
.description("Post a new discussion to a forum")
|
|
227
|
+
.argument("<forum-id>", "Forum ID")
|
|
228
|
+
.argument("<subject>", "Discussion subject")
|
|
229
|
+
.argument("<message>", "Discussion message")
|
|
230
|
+
.option("--subscribe", "Subscribe to the discussion", false)
|
|
231
|
+
.option("--pin", "Pin the discussion", false)
|
|
232
|
+
.action(async (forumId, subject, message, options, command) => {
|
|
233
|
+
const apiContext = await createApiContext(options, command);
|
|
234
|
+
if (!apiContext) {
|
|
235
|
+
process.exitCode = 1;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const { log, session } = apiContext;
|
|
239
|
+
// Get courses to find the forum
|
|
240
|
+
const courses = await getEnrolledCoursesApi(session, {
|
|
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) {
|
|
248
|
+
log.error(`Forum not found: ${forumId}`);
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const course = courses.find(c => c.id === targetForum.courseid);
|
|
253
|
+
log.info(`Posting to forum: ${targetForum.name} (${course?.fullname})`);
|
|
254
|
+
const result = await addForumDiscussionApi(session, targetForum.id, subject, message);
|
|
255
|
+
if (result.success) {
|
|
256
|
+
log.success(`✓ Discussion posted successfully!`);
|
|
257
|
+
log.info(` Discussion ID: ${result.discussionId}`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
log.error(`✗ Failed to post discussion: ${result.error}`);
|
|
261
|
+
process.exitCode = 1;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
forumsCmd
|
|
265
|
+
.command("reply")
|
|
266
|
+
.description("Reply to a discussion post")
|
|
267
|
+
.argument("<post-id>", "Parent post ID to reply to")
|
|
268
|
+
.argument("<subject>", "Reply subject")
|
|
269
|
+
.argument("<message>", "Reply message")
|
|
270
|
+
.option("--attachment-id <id>", "Draft file ID for attachment")
|
|
271
|
+
.option("--inline-attachment-id <id>", "Draft file ID for inline attachment")
|
|
272
|
+
.action(async (postId, subject, message, options, command) => {
|
|
273
|
+
const apiContext = await createApiContext(options, command);
|
|
274
|
+
if (!apiContext) {
|
|
275
|
+
process.exitCode = 1;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const { log, session } = apiContext;
|
|
279
|
+
log.info(`Replying to post: ${postId}`);
|
|
280
|
+
log.info(` Subject: ${subject}`);
|
|
281
|
+
log.info(` Message: ${message}`);
|
|
282
|
+
if (options.attachmentId) {
|
|
283
|
+
log.info(` Attachment ID: ${options.attachmentId}`);
|
|
284
|
+
}
|
|
285
|
+
const result = await addForumPostApi(session, parseInt(postId, 10), subject, message, {
|
|
286
|
+
attachmentId: options.attachmentId ? parseInt(options.attachmentId, 10) : undefined,
|
|
287
|
+
inlineAttachmentId: options.inlineAttachmentId ? parseInt(options.inlineAttachmentId, 10) : undefined,
|
|
288
|
+
});
|
|
289
|
+
if (result.success) {
|
|
290
|
+
log.success(`✓ Reply posted successfully!`);
|
|
291
|
+
log.info(` Post ID: ${result.postId}`);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
log.error(`✗ Failed to post reply: ${result.error}`);
|
|
295
|
+
process.exitCode = 1;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
forumsCmd
|
|
299
|
+
.command("delete")
|
|
300
|
+
.description("Delete a forum post or discussion (by post ID)")
|
|
301
|
+
.argument("<post-id>", "Post ID to delete (deletes entire discussion if it's the first post)")
|
|
302
|
+
.action(async (postId, options, command) => {
|
|
303
|
+
const apiContext = await createApiContext(options, command);
|
|
304
|
+
if (!apiContext) {
|
|
305
|
+
process.exitCode = 1;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const { log, session } = apiContext;
|
|
309
|
+
const result = await deleteForumPostApi(session, parseInt(postId, 10));
|
|
310
|
+
if (result.success) {
|
|
311
|
+
log.success(`✓ Post ${postId} deleted successfully!`);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
log.error(`✗ Failed to delete post: ${result.error}`);
|
|
315
|
+
process.exitCode = 1;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
231
318
|
}
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../../src/src/commands/materials.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8iB/D"}
|
|
@@ -1,56 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getEnrolledCourses, getEnrolledCoursesApi, getResourcesByCoursesApi } from "../lib/moodle.js";
|
|
1
|
+
import { getOutputFormat, sanitizeFilename, getSessionPath, formatFileSize } from "../lib/utils.js";
|
|
2
|
+
import { getEnrolledCourses, getEnrolledCoursesApi, getResourcesByCoursesApi, updateActivityCompletionStatusManually, getSiteInfoApi, moodleApiCall } from "../lib/moodle.js";
|
|
3
3
|
import { createLogger } from "../lib/logger.js";
|
|
4
|
-
import { launchAuthenticated } from "../lib/auth.js";
|
|
4
|
+
import { launchAuthenticated, createApiContext } from "../lib/auth.js";
|
|
5
5
|
import { extractSessionInfo } from "../lib/session.js";
|
|
6
6
|
import { closeBrowserSafely } from "../lib/auth.js";
|
|
7
|
-
import {
|
|
7
|
+
import { formatAndOutput } from "../index.js";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
export function registerMaterialsCommand(program) {
|
|
11
11
|
const materialsCmd = program.command("materials");
|
|
12
12
|
materialsCmd.description("Material/resource operations");
|
|
13
|
-
// Helper to get output format from global or local options
|
|
14
|
-
function getOutputFormat(command) {
|
|
15
|
-
const opts = command.optsWithGlobals();
|
|
16
|
-
return opts.output || "json";
|
|
17
|
-
}
|
|
18
|
-
// Pure API context - no browser required (fast!)
|
|
19
|
-
async function createApiContext(options, command) {
|
|
20
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
21
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
22
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
23
|
-
const log = createLogger(opts.verbose, silent);
|
|
24
|
-
const baseDir = getBaseDir();
|
|
25
|
-
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
26
|
-
// Check if session exists
|
|
27
|
-
if (!fs.existsSync(sessionPath)) {
|
|
28
|
-
log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
|
|
29
|
-
log.info(`Session 預期位置: ${sessionPath}`);
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
// Try to load WS token
|
|
33
|
-
const wsToken = loadWsToken(sessionPath);
|
|
34
|
-
if (!wsToken) {
|
|
35
|
-
log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
log,
|
|
40
|
-
session: {
|
|
41
|
-
wsToken,
|
|
42
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
13
|
// Helper function to create session context (for download commands)
|
|
47
14
|
async function createSessionContext(options, command) {
|
|
48
15
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
49
16
|
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
50
17
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
51
18
|
const log = createLogger(opts.verbose, silent);
|
|
52
|
-
const
|
|
53
|
-
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
19
|
+
const sessionPath = getSessionPath();
|
|
54
20
|
if (!fs.existsSync(sessionPath)) {
|
|
55
21
|
log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
|
|
56
22
|
return null;
|
|
@@ -77,10 +43,6 @@ export function registerMaterialsCommand(program) {
|
|
|
77
43
|
throw err;
|
|
78
44
|
}
|
|
79
45
|
}
|
|
80
|
-
// Helper to sanitize filenames
|
|
81
|
-
function sanitizeFilename(name) {
|
|
82
|
-
return name.replace(/[<>:"/\\|?*]/g, "_").replace(/\s+/g, "_");
|
|
83
|
-
}
|
|
84
46
|
// Helper to download a single resource
|
|
85
47
|
async function downloadResource(page, resource, outputDir, log) {
|
|
86
48
|
try {
|
|
@@ -91,9 +53,7 @@ export function registerMaterialsCommand(program) {
|
|
|
91
53
|
}
|
|
92
54
|
// Create course directory
|
|
93
55
|
const courseDir = path.join(outputDir, sanitizeFilename(resource.course_name));
|
|
94
|
-
|
|
95
|
-
fs.mkdirSync(courseDir, { recursive: true });
|
|
96
|
-
}
|
|
56
|
+
await fs.promises.mkdir(courseDir, { recursive: true });
|
|
97
57
|
// Navigate to resource page
|
|
98
58
|
log.debug(` Downloading: ${resource.name}`);
|
|
99
59
|
await page.goto(resource.url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
@@ -134,8 +94,8 @@ export function registerMaterialsCommand(program) {
|
|
|
134
94
|
const download = await downloadPromise;
|
|
135
95
|
// Save file
|
|
136
96
|
await download.saveAs(outputPath);
|
|
137
|
-
const stats = fs.
|
|
138
|
-
log.success(` Downloaded: ${filename} (${(stats.size
|
|
97
|
+
const stats = await fs.promises.stat(outputPath);
|
|
98
|
+
log.success(` Downloaded: ${filename} (${formatFileSize(stats.size, 1)} KB)`);
|
|
139
99
|
return {
|
|
140
100
|
filename,
|
|
141
101
|
path: outputPath,
|
|
@@ -359,4 +319,172 @@ export function registerMaterialsCommand(program) {
|
|
|
359
319
|
await closeBrowserSafely(browser, browserContext);
|
|
360
320
|
}
|
|
361
321
|
});
|
|
322
|
+
materialsCmd
|
|
323
|
+
.command("complete")
|
|
324
|
+
.description("Mark all incomplete resources (non-video) as complete in a course")
|
|
325
|
+
.argument("<course-id>", "Course ID")
|
|
326
|
+
.option("--dry-run", "Show what would be marked complete without doing it")
|
|
327
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
328
|
+
.action(async (courseId, options, command) => {
|
|
329
|
+
const output = getOutputFormat(command);
|
|
330
|
+
const apiContext = await createApiContext(options, command);
|
|
331
|
+
if (!apiContext) {
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const { log, session } = apiContext;
|
|
337
|
+
// Get user ID
|
|
338
|
+
const siteInfo = await getSiteInfoApi(session);
|
|
339
|
+
// Get completion status for all activities in the course
|
|
340
|
+
const completionData = await moodleApiCall(session, "core_completion_get_activities_completion_status", { courseid: parseInt(courseId, 10), userid: siteInfo.userid });
|
|
341
|
+
if (!completionData?.statuses) {
|
|
342
|
+
log.info("No activities found in this course.");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Filter for resources (non-video) that have completion enabled but are not complete
|
|
346
|
+
const incompleteResources = completionData.statuses.filter((status) => {
|
|
347
|
+
// Only resources, not supervideo
|
|
348
|
+
if (status.modname === "supervideo")
|
|
349
|
+
return false;
|
|
350
|
+
// Must have completion enabled
|
|
351
|
+
if (!status.hascompletion)
|
|
352
|
+
return false;
|
|
353
|
+
// Must be incomplete
|
|
354
|
+
if (status.isoverallcomplete)
|
|
355
|
+
return false;
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
if (incompleteResources.length === 0) {
|
|
359
|
+
log.info("All resources are already complete (or no resources with completion tracking).");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
log.info(`Found ${incompleteResources.length} incomplete resources to complete:`);
|
|
363
|
+
for (const resource of incompleteResources) {
|
|
364
|
+
log.info(` - ${resource.name} (cmid: ${resource.cmid})`);
|
|
365
|
+
}
|
|
366
|
+
if (options.dryRun) {
|
|
367
|
+
log.info("\n[Dry run] No changes made.");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// Mark each resource as complete
|
|
371
|
+
const results = [];
|
|
372
|
+
for (const resource of incompleteResources) {
|
|
373
|
+
const success = await updateActivityCompletionStatusManually(session, resource.cmid, true);
|
|
374
|
+
results.push({
|
|
375
|
+
cmid: resource.cmid,
|
|
376
|
+
name: resource.name,
|
|
377
|
+
success,
|
|
378
|
+
});
|
|
379
|
+
if (success) {
|
|
380
|
+
log.success(` ✓ Completed: ${resource.name}`);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
log.error(` ✗ Failed: ${resource.name}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const completed = results.filter(r => r.success).length;
|
|
387
|
+
const failed = results.filter(r => !r.success).length;
|
|
388
|
+
log.info(`\n執行結果: ${completed} 成功, ${failed} 失敗`);
|
|
389
|
+
if (output !== "silent") {
|
|
390
|
+
formatAndOutput(results, output, log);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
apiContext.log.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
395
|
+
process.exitCode = 1;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
materialsCmd
|
|
399
|
+
.command("complete-all")
|
|
400
|
+
.description("Mark all incomplete resources (non-video) as complete across all in-progress courses")
|
|
401
|
+
.option("--dry-run", "Show what would be marked complete without doing it")
|
|
402
|
+
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
403
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
404
|
+
.action(async (options, command) => {
|
|
405
|
+
const output = getOutputFormat(command);
|
|
406
|
+
const apiContext = await createApiContext(options, command);
|
|
407
|
+
if (!apiContext) {
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
const { log, session } = apiContext;
|
|
413
|
+
// Get user ID
|
|
414
|
+
const siteInfo = await getSiteInfoApi(session);
|
|
415
|
+
// Get all courses
|
|
416
|
+
const classification = options.level === "all" ? undefined : "inprogress";
|
|
417
|
+
const courses = await getEnrolledCoursesApi(session, { classification });
|
|
418
|
+
log.info(`Scanning ${courses.length} courses for incomplete resources...`);
|
|
419
|
+
const allResults = [];
|
|
420
|
+
let totalIncomplete = 0;
|
|
421
|
+
for (const course of courses) {
|
|
422
|
+
try {
|
|
423
|
+
// Get completion status for all activities in the course
|
|
424
|
+
const completionData = await moodleApiCall(session, "core_completion_get_activities_completion_status", { courseid: course.id, userid: siteInfo.userid });
|
|
425
|
+
if (!completionData?.statuses)
|
|
426
|
+
continue;
|
|
427
|
+
// Filter for resources (non-video) that have completion enabled but are not complete
|
|
428
|
+
const incompleteResources = completionData.statuses.filter((status) => {
|
|
429
|
+
if (status.modname === "supervideo")
|
|
430
|
+
return false;
|
|
431
|
+
if (!status.hascompletion)
|
|
432
|
+
return false;
|
|
433
|
+
if (status.isoverallcomplete)
|
|
434
|
+
return false;
|
|
435
|
+
return true;
|
|
436
|
+
});
|
|
437
|
+
if (incompleteResources.length > 0) {
|
|
438
|
+
log.info(`\n${course.fullname}: ${incompleteResources.length} incomplete resources`);
|
|
439
|
+
totalIncomplete += incompleteResources.length;
|
|
440
|
+
if (options.dryRun) {
|
|
441
|
+
for (const resource of incompleteResources) {
|
|
442
|
+
log.info(` - ${resource.name} (cmid: ${resource.cmid})`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
for (const resource of incompleteResources) {
|
|
447
|
+
const success = await updateActivityCompletionStatusManually(session, resource.cmid, true);
|
|
448
|
+
allResults.push({
|
|
449
|
+
courseId: course.id,
|
|
450
|
+
courseName: course.fullname,
|
|
451
|
+
cmid: resource.cmid,
|
|
452
|
+
name: resource.name,
|
|
453
|
+
success,
|
|
454
|
+
});
|
|
455
|
+
if (success) {
|
|
456
|
+
log.success(` ✓ Completed: ${resource.name}`);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
log.error(` ✗ Failed: ${resource.name}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
log.warn(`Failed to process course ${course.fullname}: ${e}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (totalIncomplete === 0) {
|
|
470
|
+
log.info("\nAll resources are already complete (or no resources with completion tracking).");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (options.dryRun) {
|
|
474
|
+
log.info(`\n[Dry run] Found ${totalIncomplete} incomplete resources across ${courses.length} courses.`);
|
|
475
|
+
log.info("Run without --dry-run to mark them as complete.");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const completed = allResults.filter(r => r.success).length;
|
|
479
|
+
const failed = allResults.filter(r => !r.success).length;
|
|
480
|
+
log.info(`\n執行結果: ${completed} 成功, ${failed} 失敗`);
|
|
481
|
+
if (output !== "silent") {
|
|
482
|
+
formatAndOutput(allResults, output, log);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch (e) {
|
|
486
|
+
apiContext.log.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
487
|
+
process.exitCode = 1;
|
|
488
|
+
}
|
|
489
|
+
});
|
|
362
490
|
}
|
|
@@ -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;AAwDpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0Q7D"}
|