@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
package/README.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<img src="assets/logo.svg" width="150" /><br/>
|
|
3
|
+
OpenApe CLI (Unofficial)
|
|
4
|
+
</h1>
|
|
2
5
|
|
|
3
6
|
中原大學 [i-Learning](https://ilearning.cycu.edu.tw/) (Moodle) 平台自動化命令列工具 (CLI),幫助你快速查詢課程、觀看影片、下載教材。
|
|
4
7
|
|
|
5
8
|
## 功能特色
|
|
6
9
|
- 📚 **課程資訊**:列出修課清單、成績、課程大綱與進度。
|
|
7
10
|
- 📺 **影片輔助**:列出/下載影片,甚至自動標記為已觀看。
|
|
8
|
-
- 📝
|
|
9
|
-
- 💬
|
|
11
|
+
- 📝 **測驗與教材**:快速進行測驗、下載教材。
|
|
12
|
+
- 💬 **討論區與公告**:閱讀公告、討論區、發表回覆。
|
|
10
13
|
- 📅 **行事曆**:內建行事曆事件查詢與匯出。
|
|
14
|
+
- ✅ **作業繳交**:查詢作業、檢查繳交狀態、上傳檔案並繳交。
|
|
11
15
|
- 🤖 **Agent Skills**:提供 Claude Code 等 Skills 工作流支援。
|
|
12
16
|
|
|
13
17
|
## 安裝
|
|
@@ -33,8 +37,8 @@ deno install -A -g -n openape jsr:@openape/openape
|
|
|
33
37
|
第一次使用需要登入,會開啟瀏覽器讓你手動完成登入,隨後會快取 Session 供未來使用。
|
|
34
38
|
```bash
|
|
35
39
|
openape login # 登入並儲存 session (開啟瀏覽器)
|
|
36
|
-
openape
|
|
37
|
-
openape
|
|
40
|
+
openape status # 檢查當前登入狀態
|
|
41
|
+
openape logout # 登出並清除 session
|
|
38
42
|
```
|
|
39
43
|
|
|
40
44
|
### 課程 (Courses)
|
|
@@ -42,7 +46,7 @@ openape auth logout # 登出並清除 session
|
|
|
42
46
|
openape courses list # 列出所有課程 (支援 --incomplete-only, --level)
|
|
43
47
|
openape courses info <id> # 顯示特定課程的詳細資訊
|
|
44
48
|
openape courses progress <id> # 顯示特定課程的進度
|
|
45
|
-
openape courses syllabus <id> # 顯示課程大綱
|
|
49
|
+
openape courses syllabus <id> # 顯示課程大綱
|
|
46
50
|
```
|
|
47
51
|
|
|
48
52
|
### 影片 (Videos)
|
|
@@ -57,27 +61,51 @@ openape videos download <id> # 下載影片
|
|
|
57
61
|
```bash
|
|
58
62
|
openape quizzes list <course-id> # 列出特定課程測驗
|
|
59
63
|
openape quizzes list-all # 列出所有課程測驗
|
|
60
|
-
openape quizzes
|
|
64
|
+
openape quizzes start <quiz-id> # 開始測驗
|
|
65
|
+
openape quizzes info <attempt-id> # 查看測驗題目
|
|
66
|
+
openape quizzes save <attempt-id> '<answers>' # 儲存測驗答案
|
|
61
67
|
openape materials list-all # 列出所有可下載教材
|
|
62
68
|
openape materials download <id> # 下載指定教材
|
|
63
69
|
openape materials download-all # 批次下載教材
|
|
70
|
+
openape materials complete <id> # 標記教材為已完成
|
|
71
|
+
openape materials complete-all # 批次標記教材為已完成
|
|
64
72
|
```
|
|
65
73
|
|
|
66
74
|
### 成績與其他查詢 (Grades, Forums, Calendar)
|
|
67
75
|
```bash
|
|
68
76
|
openape grades summary # 顯示學期成績總覽
|
|
69
77
|
openape grades course <id> # 顯示特定課程成績
|
|
70
|
-
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> # 刪除討論貼文
|
|
71
85
|
openape announcements list-all # 列出所有公告
|
|
72
86
|
openape announcements read <id> # 閱讀特定公告
|
|
73
87
|
openape calendar events # 查詢行事曆事件
|
|
74
88
|
openape calendar export # 匯出事件
|
|
75
89
|
```
|
|
76
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
|
+
|
|
77
106
|
### Skills
|
|
78
107
|
讓你的 AI Agent 也可以控制 OpenApe。只需一個指令即可安裝:
|
|
79
108
|
```bash
|
|
80
|
-
openape skills list # 查看目前提供的所有 skills
|
|
81
109
|
openape skills install claude # 為 Claude Code 安裝技能 (支援 claude, codex, opencode)
|
|
82
110
|
openape skills install --all # 自動偵測環境並安裝給所有支援的 Agent
|
|
83
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,13 +2,11 @@ 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
|
-
export function
|
|
9
|
-
const authCmd = program.command("auth");
|
|
10
|
-
authCmd.description("Authentication commands");
|
|
11
|
-
// Register login directly on program (not under auth subcommand)
|
|
9
|
+
export function registerCommand(program) {
|
|
12
10
|
program
|
|
13
11
|
.command("login")
|
|
14
12
|
.description("Login to iLearning manually and save session")
|
|
@@ -186,8 +184,7 @@ export function registerAuthCommand(program) {
|
|
|
186
184
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
187
185
|
}
|
|
188
186
|
});
|
|
189
|
-
|
|
190
|
-
authCmd
|
|
187
|
+
program
|
|
191
188
|
.command("status")
|
|
192
189
|
.description("Check session status")
|
|
193
190
|
.option("--session <path>", "Session file path", ".auth/storage-state.json")
|
|
@@ -215,6 +212,25 @@ export function registerAuthCommand(program) {
|
|
|
215
212
|
exists: false
|
|
216
213
|
}
|
|
217
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
|
+
}
|
|
218
234
|
console.log(JSON.stringify(result, null, 2));
|
|
219
235
|
}
|
|
220
236
|
catch {
|
|
@@ -236,7 +252,7 @@ export function registerAuthCommand(program) {
|
|
|
236
252
|
console.log(JSON.stringify(result, null, 2));
|
|
237
253
|
}
|
|
238
254
|
});
|
|
239
|
-
|
|
255
|
+
program
|
|
240
256
|
.command("logout")
|
|
241
257
|
.description("Remove saved session")
|
|
242
258
|
.option("--session <path>", "Session file path", ".auth/storage-state.json")
|
|
@@ -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"}
|