@mo7yw4ng/openape 1.0.3 → 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 +30 -5
- 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.map +1 -1
- package/esm/src/commands/auth.js +21 -1
- 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 +125 -33
- package/esm/src/commands/materials.d.ts.map +1 -1
- package/esm/src/commands/materials.js +6 -38
- 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 +5 -4
- package/esm/src/index.d.ts.map +1 -1
- package/esm/src/index.js +12 -1
- 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 +179 -1
- package/esm/src/lib/moodle.d.ts.map +1 -1
- package/esm/src/lib/moodle.js +481 -13
- package/esm/src/lib/types.d.ts +81 -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 +20 -0
- package/esm/src/lib/utils.d.ts.map +1 -1
- package/esm/src/lib/utils.js +48 -1
- 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 +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.map +1 -1
- package/script/src/commands/auth.js +20 -0
- 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 +125 -33
- package/script/src/commands/materials.d.ts.map +1 -1
- package/script/src/commands/materials.js +7 -39
- 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 +5 -4
- package/script/src/index.d.ts.map +1 -1
- package/script/src/index.js +12 -1
- 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 +179 -1
- package/script/src/lib/moodle.d.ts.map +1 -1
- package/script/src/lib/moodle.js +493 -13
- package/script/src/lib/types.d.ts +81 -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 +20 -0
- package/script/src/lib/utils.d.ts.map +1 -1
- package/script/src/lib/utils.js +52 -0
- package/skills/openape/SKILL.md +73 -271
package/README.md
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
## 功能特色
|
|
9
9
|
- 📚 **課程資訊**:列出修課清單、成績、課程大綱與進度。
|
|
10
10
|
- 📺 **影片輔助**:列出/下載影片,甚至自動標記為已觀看。
|
|
11
|
-
- 📝
|
|
12
|
-
- 💬
|
|
11
|
+
- 📝 **測驗與教材**:快速進行測驗、下載教材。
|
|
12
|
+
- 💬 **討論區與公告**:閱讀公告、討論區、發表回覆。
|
|
13
13
|
- 📅 **行事曆**:內建行事曆事件查詢與匯出。
|
|
14
|
+
- ✅ **作業繳交**:查詢作業、檢查繳交狀態、上傳檔案並繳交。
|
|
14
15
|
- 🤖 **Agent Skills**:提供 Claude Code 等 Skills 工作流支援。
|
|
15
16
|
|
|
16
17
|
## 安裝
|
|
@@ -60,27 +61,51 @@ openape videos download <id> # 下載影片
|
|
|
60
61
|
```bash
|
|
61
62
|
openape quizzes list <course-id> # 列出特定課程測驗
|
|
62
63
|
openape quizzes list-all # 列出所有課程測驗
|
|
63
|
-
openape quizzes
|
|
64
|
+
openape quizzes start <quiz-id> # 開始測驗
|
|
65
|
+
openape quizzes info <attempt-id> # 查看測驗題目
|
|
66
|
+
openape quizzes save <attempt-id> '<answers>' # 儲存測驗答案
|
|
64
67
|
openape materials list-all # 列出所有可下載教材
|
|
65
68
|
openape materials download <id> # 下載指定教材
|
|
66
69
|
openape materials download-all # 批次下載教材
|
|
70
|
+
openape materials complete <id> # 標記教材為已完成
|
|
71
|
+
openape materials complete-all # 批次標記教材為已完成
|
|
67
72
|
```
|
|
68
73
|
|
|
69
74
|
### 成績與其他查詢 (Grades, Forums, Calendar)
|
|
70
75
|
```bash
|
|
71
76
|
openape grades summary # 顯示學期成績總覽
|
|
72
77
|
openape grades course <id> # 顯示特定課程成績
|
|
73
|
-
openape forums list
|
|
78
|
+
openape forums list # 列出進行中課程的討論區
|
|
79
|
+
openape forums list-all # 列出所有討論區
|
|
80
|
+
openape forums discussions <forum-id> # 列出討論區中的討論串
|
|
81
|
+
openape forums posts <discussion-id> # 列出討論串中的貼文
|
|
82
|
+
openape forums reply <post-id> <subject> <message> # 回覆貼文
|
|
83
|
+
openape forums post <forum-id> <subject> <message> # 發起新討論
|
|
84
|
+
openape forums delete <post-id> # 刪除討論貼文
|
|
74
85
|
openape announcements list-all # 列出所有公告
|
|
75
86
|
openape announcements read <id> # 閱讀特定公告
|
|
76
87
|
openape calendar events # 查詢行事曆事件
|
|
77
88
|
openape calendar export # 匯出事件
|
|
78
89
|
```
|
|
79
90
|
|
|
91
|
+
### 作業與檔案上傳 (Assignments & Upload)
|
|
92
|
+
```bash
|
|
93
|
+
# 作業查詢與繳交
|
|
94
|
+
openape assignments list <course-id> # 列出課程作業
|
|
95
|
+
openape assignments list-all # 列出所有作業 (支援 --level all)
|
|
96
|
+
openape assignments status <assignment-id> # 檢查作業繳交狀態
|
|
97
|
+
openape assignments submit <assignment-id> # 繳交作業
|
|
98
|
+
--text "內容" # 線上文字繳交
|
|
99
|
+
--file-id <draft-id> # 使用已上傳的檔案 ID
|
|
100
|
+
--file <path> # 直接上傳檔案並繳交
|
|
101
|
+
|
|
102
|
+
# 檔案上傳至草稿區
|
|
103
|
+
openape upload file <path> # 上傳檔案取得 draft ID
|
|
104
|
+
```
|
|
105
|
+
|
|
80
106
|
### Skills
|
|
81
107
|
讓你的 AI Agent 也可以控制 OpenApe。只需一個指令即可安裝:
|
|
82
108
|
```bash
|
|
83
|
-
openape skills list # 查看目前提供的所有 skills
|
|
84
109
|
openape skills install claude # 為 Claude Code 安裝技能 (支援 claude, codex, opencode)
|
|
85
110
|
openape skills install --all # 自動偵測環境並安裝給所有支援的 Agent
|
|
86
111
|
```
|
package/esm/deno.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"announcements.d.ts","sourceRoot":"","sources":["../../../src/src/commands/announcements.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqBpC,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"announcements.d.ts","sourceRoot":"","sources":["../../../src/src/commands/announcements.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqBpC,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoJnE"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBaseDir } from "../lib/utils.js";
|
|
1
|
+
import { getBaseDir, formatTimestamp } from "../lib/utils.js";
|
|
2
2
|
import { getSiteInfoApi, getMessagesApi, getDiscussionPostsApi } from "../lib/moodle.js";
|
|
3
3
|
import { createLogger } from "../lib/logger.js";
|
|
4
4
|
import { loadWsToken } from "../lib/token.js";
|
|
@@ -77,27 +77,26 @@ export function registerAnnouncementsCommand(program) {
|
|
|
77
77
|
allAnnouncements.sort((a, b) => b.createdAt - a.createdAt);
|
|
78
78
|
// Apply limit
|
|
79
79
|
let filteredAnnouncements = allAnnouncements.slice(0, limit);
|
|
80
|
-
|
|
80
|
+
console.log(JSON.stringify({
|
|
81
81
|
status: "success",
|
|
82
82
|
timestamp: new Date().toISOString(),
|
|
83
83
|
level: options.level,
|
|
84
|
-
|
|
84
|
+
total_announcements: allAnnouncements.length,
|
|
85
|
+
shown: filteredAnnouncements.length,
|
|
86
|
+
}));
|
|
87
|
+
for (const a of filteredAnnouncements) {
|
|
88
|
+
console.log(JSON.stringify({
|
|
85
89
|
course_id: a.course_id,
|
|
86
90
|
course_name: a.course_name,
|
|
87
91
|
id: a.id,
|
|
88
92
|
subject: a.subject,
|
|
89
93
|
author: a.author,
|
|
90
94
|
author_id: a.authorId,
|
|
91
|
-
created_at:
|
|
92
|
-
modified_at:
|
|
95
|
+
created_at: formatTimestamp(a.createdAt),
|
|
96
|
+
modified_at: formatTimestamp(a.modifiedAt),
|
|
93
97
|
unread: a.unread,
|
|
94
|
-
}))
|
|
95
|
-
|
|
96
|
-
total_announcements: allAnnouncements.length,
|
|
97
|
-
shown: filteredAnnouncements.length,
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
console.log(JSON.stringify(output));
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
101
100
|
});
|
|
102
101
|
announcementsCmd
|
|
103
102
|
.command("read")
|
|
@@ -125,8 +124,8 @@ export function registerAnnouncementsCommand(program) {
|
|
|
125
124
|
subject: firstPost.subject,
|
|
126
125
|
author: firstPost.author,
|
|
127
126
|
author_id: firstPost.authorId,
|
|
128
|
-
created_at:
|
|
129
|
-
modified_at:
|
|
127
|
+
created_at: formatTimestamp(firstPost.created),
|
|
128
|
+
modified_at: formatTimestamp(firstPost.modified),
|
|
130
129
|
message: firstPost.message,
|
|
131
130
|
},
|
|
132
131
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assignments.d.ts","sourceRoot":"","sources":["../../../src/src/commands/assignments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0PjE"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { getOutputFormat, formatFileSize, formatMoodleDate } from "../lib/utils.js";
|
|
2
|
+
import { getEnrolledCoursesApi, getAssignmentsByCoursesApi, getSubmissionStatusApi, saveSubmissionApi, uploadFileApi } from "../lib/moodle.js";
|
|
3
|
+
import { createApiContext } from "../lib/auth.js";
|
|
4
|
+
import { formatAndOutput } from "../index.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
export function registerAssignmentsCommand(program) {
|
|
8
|
+
const assignmentsCmd = program.command("assignments");
|
|
9
|
+
assignmentsCmd.description("Assignment operations");
|
|
10
|
+
assignmentsCmd
|
|
11
|
+
.command("list")
|
|
12
|
+
.description("List assignments in a course")
|
|
13
|
+
.argument("<course-id>", "Course ID")
|
|
14
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
15
|
+
.action(async (courseId, options, command) => {
|
|
16
|
+
const output = getOutputFormat(command);
|
|
17
|
+
const apiContext = await createApiContext(options, command);
|
|
18
|
+
if (!apiContext) {
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const apiAssignments = await getAssignmentsByCoursesApi(apiContext.session, [parseInt(courseId, 10)]);
|
|
23
|
+
const assignments = apiAssignments.map(a => ({
|
|
24
|
+
id: a.id,
|
|
25
|
+
courseName: courseId,
|
|
26
|
+
name: a.name,
|
|
27
|
+
url: a.url,
|
|
28
|
+
cmid: a.cmid,
|
|
29
|
+
duedate: formatMoodleDate(a.duedate),
|
|
30
|
+
cutoffdate: formatMoodleDate(a.cutoffdate),
|
|
31
|
+
allowSubmissionsFromDate: formatMoodleDate(a.allowSubmissionsFromDate),
|
|
32
|
+
}));
|
|
33
|
+
apiContext.log.info(`\n找到 ${assignments.length} 個作業。`);
|
|
34
|
+
formatAndOutput(assignments, output, apiContext.log);
|
|
35
|
+
});
|
|
36
|
+
assignmentsCmd
|
|
37
|
+
.command("list-all")
|
|
38
|
+
.description("List all assignments across all courses")
|
|
39
|
+
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
40
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
41
|
+
.action(async (options, command) => {
|
|
42
|
+
const output = getOutputFormat(command);
|
|
43
|
+
const apiContext = await createApiContext(options, command);
|
|
44
|
+
if (!apiContext) {
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const classification = options.level === "all" ? undefined : "inprogress";
|
|
49
|
+
const courses = await getEnrolledCoursesApi(apiContext.session, {
|
|
50
|
+
classification,
|
|
51
|
+
});
|
|
52
|
+
// Get assignments via WS API (no browser needed!)
|
|
53
|
+
const courseIds = courses.map(c => c.id);
|
|
54
|
+
const apiAssignments = await getAssignmentsByCoursesApi(apiContext.session, courseIds);
|
|
55
|
+
// Build a map of courseId -> course for quick lookup
|
|
56
|
+
const courseMap = new Map(courses.map(c => [c.id, c]));
|
|
57
|
+
const allAssignments = [];
|
|
58
|
+
for (const a of apiAssignments) {
|
|
59
|
+
const course = courseMap.get(a.courseId);
|
|
60
|
+
if (course) {
|
|
61
|
+
allAssignments.push({
|
|
62
|
+
id: a.id,
|
|
63
|
+
courseName: course.fullname,
|
|
64
|
+
name: a.name,
|
|
65
|
+
url: a.url,
|
|
66
|
+
cmid: a.cmid,
|
|
67
|
+
duedate: formatMoodleDate(a.duedate),
|
|
68
|
+
cutoffdate: formatMoodleDate(a.cutoffdate),
|
|
69
|
+
allowSubmissionsFromDate: formatMoodleDate(a.allowSubmissionsFromDate),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
apiContext.log.info(`\n總計發現 ${allAssignments.length} 個作業。`);
|
|
74
|
+
formatAndOutput(allAssignments, output, apiContext.log);
|
|
75
|
+
});
|
|
76
|
+
// ── Submission Status ───────────────────────────────────────────────────────
|
|
77
|
+
assignmentsCmd
|
|
78
|
+
.command("status")
|
|
79
|
+
.description("Check assignment submission status")
|
|
80
|
+
.argument("<assignment-id>", "Assignment instance ID (from list-all)")
|
|
81
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
82
|
+
.action(async (assignmentId, options, command) => {
|
|
83
|
+
const output = getOutputFormat(command);
|
|
84
|
+
const apiContext = await createApiContext(options, command);
|
|
85
|
+
if (!apiContext) {
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const id = parseInt(assignmentId, 10);
|
|
90
|
+
apiContext.log.info("檢查繳交狀態...");
|
|
91
|
+
const status = await getSubmissionStatusApi(apiContext.session, id);
|
|
92
|
+
// Build status data object
|
|
93
|
+
const statusData = {
|
|
94
|
+
submitted: status.submitted,
|
|
95
|
+
submitted_text: status.submitted ? "已繳交" : "尚未繳交",
|
|
96
|
+
graded: status.graded,
|
|
97
|
+
graded_text: status.graded ? "已評分" : "尚未評分",
|
|
98
|
+
last_modified: status.lastModified ? new Date(status.lastModified * 1000).toISOString() : null,
|
|
99
|
+
last_modified_text: status.lastModified ? new Date(status.lastModified * 1000).toLocaleString("zh-TW") : null,
|
|
100
|
+
grader: status.grader,
|
|
101
|
+
grade: status.grade,
|
|
102
|
+
feedback: status.feedback,
|
|
103
|
+
files: status.extensions.map(f => ({
|
|
104
|
+
filename: f.filename,
|
|
105
|
+
filesize: f.filesize,
|
|
106
|
+
filesize_kb: formatFileSize(f.filesize),
|
|
107
|
+
})),
|
|
108
|
+
};
|
|
109
|
+
formatAndOutput(statusData, output, apiContext.log);
|
|
110
|
+
});
|
|
111
|
+
// ── Submit Assignment ────────────────────────────────────────────────────────
|
|
112
|
+
assignmentsCmd
|
|
113
|
+
.command("submit")
|
|
114
|
+
.description("Submit an assignment (online text or file)")
|
|
115
|
+
.argument("<assignment-id>", "Assignment instance ID (from list-all)")
|
|
116
|
+
.option("--text <content>", "Online text content to submit")
|
|
117
|
+
.option("--file-id <id>", "Draft file ID from file upload")
|
|
118
|
+
.option("--file <path>", "Upload and submit a file directly")
|
|
119
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
120
|
+
.action(async (assignmentId, options, command) => {
|
|
121
|
+
const output = getOutputFormat(command);
|
|
122
|
+
const apiContext = await createApiContext(options, command);
|
|
123
|
+
if (!apiContext) {
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const id = parseInt(assignmentId, 10);
|
|
128
|
+
// Check submission status first
|
|
129
|
+
const status = await getSubmissionStatusApi(apiContext.session, id);
|
|
130
|
+
let fileUploaded;
|
|
131
|
+
let cancelled = false;
|
|
132
|
+
if (status.submitted) {
|
|
133
|
+
const confirm = await promptConfirm("此作業已經繳交!確定要重新繳交嗎?(y/N): ");
|
|
134
|
+
if (!confirm) {
|
|
135
|
+
cancelled = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (cancelled) {
|
|
139
|
+
const cancelResult = {
|
|
140
|
+
success: false,
|
|
141
|
+
cancelled: true,
|
|
142
|
+
message: "Submission cancelled by user",
|
|
143
|
+
};
|
|
144
|
+
formatAndOutput(cancelResult, output, apiContext.log);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Validate options
|
|
148
|
+
if (!options.text && !options.fileId && !options.file) {
|
|
149
|
+
const errorResult = {
|
|
150
|
+
success: false,
|
|
151
|
+
error: "請提供 --text、--file-id 或 --file 選項。",
|
|
152
|
+
};
|
|
153
|
+
formatAndOutput(errorResult, output, apiContext.log);
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let fileId = options.fileId ? parseInt(options.fileId, 10) : undefined;
|
|
158
|
+
// Upload file if --file option is provided
|
|
159
|
+
if (options.file) {
|
|
160
|
+
const resolvedPath = path.resolve(options.file);
|
|
161
|
+
// Check if file exists
|
|
162
|
+
try {
|
|
163
|
+
await fs.access(resolvedPath);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
const errorResult = {
|
|
167
|
+
success: false,
|
|
168
|
+
error: `檔案不存在: ${options.file}`,
|
|
169
|
+
};
|
|
170
|
+
formatAndOutput(errorResult, output, apiContext.log);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const stats = await fs.stat(resolvedPath);
|
|
175
|
+
const fileSizeKB = formatFileSize(stats.size);
|
|
176
|
+
const uploadResult = await uploadFileApi(apiContext.session, resolvedPath);
|
|
177
|
+
if (!uploadResult.success) {
|
|
178
|
+
const errorResult = {
|
|
179
|
+
success: false,
|
|
180
|
+
error: `檔案上傳失敗: ${uploadResult.error}`,
|
|
181
|
+
};
|
|
182
|
+
formatAndOutput(errorResult, output, apiContext.log);
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
fileId = uploadResult.draftId;
|
|
187
|
+
fileUploaded = {
|
|
188
|
+
filename: path.basename(resolvedPath),
|
|
189
|
+
filesize: stats.size,
|
|
190
|
+
filesize_kb: formatFileSize(stats.size),
|
|
191
|
+
draft_id: fileId,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Submit
|
|
195
|
+
const result = await saveSubmissionApi(apiContext.session, id, {
|
|
196
|
+
onlineText: options.text ? { text: options.text } : undefined,
|
|
197
|
+
fileId: fileId,
|
|
198
|
+
});
|
|
199
|
+
const submitResult = {
|
|
200
|
+
success: result.success,
|
|
201
|
+
assignment_id: id,
|
|
202
|
+
submitted: !!result.success,
|
|
203
|
+
online_text: !!options.text,
|
|
204
|
+
file_uploaded: fileUploaded,
|
|
205
|
+
file_id: fileId ?? null,
|
|
206
|
+
error: result.success ? undefined : result.error,
|
|
207
|
+
message: result.success ? "Assignment submitted successfully" : result.error,
|
|
208
|
+
};
|
|
209
|
+
formatAndOutput(submitResult, output, apiContext.log);
|
|
210
|
+
if (!result.success) {
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Prompt user for yes/no confirmation.
|
|
217
|
+
*/
|
|
218
|
+
async function promptConfirm(prompt) {
|
|
219
|
+
const readline = await import("node:readline");
|
|
220
|
+
const rl = readline.createInterface({
|
|
221
|
+
input: process.stdin,
|
|
222
|
+
output: process.stdout,
|
|
223
|
+
});
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
rl.question(prompt, (answer) => {
|
|
226
|
+
rl.close();
|
|
227
|
+
resolve(/^y/i.test(answer));
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/commands/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/commands/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuRtD"}
|
package/esm/src/commands/auth.js
CHANGED
|
@@ -2,7 +2,8 @@ import { getBaseDir } from "../lib/utils.js";
|
|
|
2
2
|
import { chromium } from "playwright-core";
|
|
3
3
|
import { createLogger } from "../lib/logger.js";
|
|
4
4
|
import { findEdgePath } from "../lib/auth.js";
|
|
5
|
-
import { saveSesskey, acquireWsToken, saveWsToken } from "../lib/token.js";
|
|
5
|
+
import { saveSesskey, acquireWsToken, saveWsToken, loadWsToken } from "../lib/token.js";
|
|
6
|
+
import { getSiteInfoApi } from "../lib/moodle.js";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import fs from "node:fs";
|
|
8
9
|
export function registerCommand(program) {
|
|
@@ -211,6 +212,25 @@ export function registerCommand(program) {
|
|
|
211
212
|
exists: false
|
|
212
213
|
}
|
|
213
214
|
};
|
|
215
|
+
// Try to get user info from WS API
|
|
216
|
+
try {
|
|
217
|
+
const wsToken = loadWsToken(sessionPath);
|
|
218
|
+
if (wsToken) {
|
|
219
|
+
const session = {
|
|
220
|
+
wsToken,
|
|
221
|
+
moodleBaseUrl: "https://ilearning.cycu.edu.tw"
|
|
222
|
+
};
|
|
223
|
+
const siteInfo = await getSiteInfoApi(session);
|
|
224
|
+
result.user = {
|
|
225
|
+
userid: siteInfo.userid,
|
|
226
|
+
username: siteInfo.username,
|
|
227
|
+
fullname: siteInfo.fullname
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// WS token might not be available or expired, skip user info
|
|
233
|
+
}
|
|
214
234
|
console.log(JSON.stringify(result, null, 2));
|
|
215
235
|
}
|
|
216
236
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/src/commands/calendar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/src/commands/calendar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmM9D"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBaseDir } from "../lib/utils.js";
|
|
1
|
+
import { getBaseDir, formatTimestamp } from "../lib/utils.js";
|
|
2
2
|
import { getEnrolledCoursesApi, getCalendarEventsApi } from "../lib/moodle.js";
|
|
3
3
|
import { createLogger } from "../lib/logger.js";
|
|
4
4
|
import { loadWsToken } from "../lib/token.js";
|
|
@@ -91,29 +91,28 @@ export function registerCalendarCommand(program) {
|
|
|
91
91
|
if (options.upcoming) {
|
|
92
92
|
filteredEvents = allEvents.filter(e => e.timestart > now);
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
console.log(JSON.stringify({
|
|
95
95
|
status: "success",
|
|
96
96
|
timestamp: new Date().toISOString(),
|
|
97
|
-
|
|
97
|
+
total_events: allEvents.length,
|
|
98
|
+
upcoming: allEvents.filter(e => e.timestart > now).length,
|
|
99
|
+
by_type: allEvents.reduce((acc, e) => {
|
|
100
|
+
acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
|
|
101
|
+
return acc;
|
|
102
|
+
}, {}),
|
|
103
|
+
}));
|
|
104
|
+
for (const e of filteredEvents) {
|
|
105
|
+
console.log(JSON.stringify({
|
|
98
106
|
id: e.id,
|
|
99
107
|
name: e.name,
|
|
100
108
|
description: e.description,
|
|
101
109
|
course_id: e.courseid,
|
|
102
110
|
event_type: e.eventtype,
|
|
103
|
-
start_time:
|
|
104
|
-
end_time: e.timeduration ?
|
|
111
|
+
start_time: formatTimestamp(e.timestart),
|
|
112
|
+
end_time: e.timeduration ? formatTimestamp(e.timestart + Math.floor(e.timeduration / 1000)) : null,
|
|
105
113
|
location: e.location,
|
|
106
|
-
}))
|
|
107
|
-
|
|
108
|
-
total_events: allEvents.length,
|
|
109
|
-
upcoming: allEvents.filter(e => e.timestart > now).length,
|
|
110
|
-
by_type: allEvents.reduce((acc, e) => {
|
|
111
|
-
acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
|
|
112
|
-
return acc;
|
|
113
|
-
}, {}),
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
console.log(JSON.stringify(output));
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
117
116
|
});
|
|
118
117
|
calendarCmd
|
|
119
118
|
.command("export")
|
|
@@ -161,8 +160,8 @@ export function registerCalendarCommand(program) {
|
|
|
161
160
|
description: e.description,
|
|
162
161
|
course_id: e.courseid,
|
|
163
162
|
event_type: e.eventtype,
|
|
164
|
-
start_time:
|
|
165
|
-
end_time: e.timeduration ?
|
|
163
|
+
start_time: formatTimestamp(e.timestart),
|
|
164
|
+
end_time: e.timeduration ? formatTimestamp(e.timestart + Math.floor(e.timeduration / 1000)) : null,
|
|
166
165
|
location: e.location,
|
|
167
166
|
})),
|
|
168
167
|
summary: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBaseDir } from "../lib/utils.js";
|
|
1
|
+
import { getBaseDir, formatTimestamp } from "../lib/utils.js";
|
|
2
2
|
import { getEnrolledCoursesApi } from "../lib/moodle.js";
|
|
3
3
|
import { createLogger } from "../lib/logger.js";
|
|
4
4
|
import { loadWsToken } from "../lib/token.js";
|
|
@@ -111,8 +111,8 @@ export function registerCoursesCommand(program) {
|
|
|
111
111
|
courseId: course.id,
|
|
112
112
|
courseName: course.fullname,
|
|
113
113
|
progress: course.progress ?? 0,
|
|
114
|
-
startDate: course.startdate ?
|
|
115
|
-
endDate: course.enddate ?
|
|
114
|
+
startDate: course.startdate ? formatTimestamp(course.startdate) : null,
|
|
115
|
+
endDate: course.enddate ? formatTimestamp(course.enddate) : null,
|
|
116
116
|
};
|
|
117
117
|
formatAndOutput(progressData, output, apiContext.log);
|
|
118
118
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forums.d.ts","sourceRoot":"","sources":["../../../src/src/commands/forums.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"forums.d.ts","sourceRoot":"","sources":["../../../src/src/commands/forums.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkX5D"}
|