@mo7yw4ng/openape 1.0.3 → 1.0.5
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 +16 -17
- 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 +45 -15
- package/esm/src/commands/calendar.d.ts.map +1 -1
- package/esm/src/commands/calendar.js +20 -21
- package/esm/src/commands/courses.js +6 -6
- package/esm/src/commands/forums.d.ts.map +1 -1
- package/esm/src/commands/forums.js +128 -36
- package/esm/src/commands/grades.js +3 -3
- package/esm/src/commands/materials.d.ts.map +1 -1
- package/esm/src/commands/materials.js +115 -224
- package/esm/src/commands/quizzes.d.ts.map +1 -1
- package/esm/src/commands/quizzes.js +179 -68
- 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 +10 -9
- 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/logger.d.ts +1 -1
- package/esm/src/lib/logger.d.ts.map +1 -1
- package/esm/src/lib/logger.js +7 -4
- package/esm/src/lib/moodle.d.ts +183 -1
- package/esm/src/lib/moodle.d.ts.map +1 -1
- package/esm/src/lib/moodle.js +498 -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 +15 -16
- 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 +44 -14
- package/script/src/commands/calendar.d.ts.map +1 -1
- package/script/src/commands/calendar.js +19 -20
- package/script/src/commands/courses.js +5 -5
- package/script/src/commands/forums.d.ts.map +1 -1
- package/script/src/commands/forums.js +128 -36
- package/script/src/commands/grades.js +3 -3
- package/script/src/commands/materials.d.ts.map +1 -1
- package/script/src/commands/materials.js +115 -224
- package/script/src/commands/quizzes.d.ts.map +1 -1
- package/script/src/commands/quizzes.js +177 -66
- 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 +10 -9
- 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/logger.d.ts +1 -1
- package/script/src/lib/logger.d.ts.map +1 -1
- package/script/src/lib/logger.js +7 -4
- package/script/src/lib/moodle.d.ts +183 -1
- package/script/src/lib/moodle.d.ts.map +1 -1
- package/script/src/lib/moodle.js +511 -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 +74 -270
|
@@ -7,13 +7,61 @@ exports.registerQuizzesCommand = registerQuizzesCommand;
|
|
|
7
7
|
const utils_js_1 = require("../lib/utils.js");
|
|
8
8
|
const moodle_js_1 = require("../lib/moodle.js");
|
|
9
9
|
const logger_js_1 = require("../lib/logger.js");
|
|
10
|
-
const auth_js_1 = require("../lib/auth.js");
|
|
11
|
-
const session_js_1 = require("../lib/session.js");
|
|
12
|
-
const auth_js_2 = require("../lib/auth.js");
|
|
13
10
|
const index_js_1 = require("../index.js");
|
|
14
11
|
const token_js_1 = require("../lib/token.js");
|
|
15
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
16
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
function stripHtmlKeepLines(html) {
|
|
15
|
+
return html
|
|
16
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
17
|
+
.replace(/<\/p>/gi, "\n")
|
|
18
|
+
.replace(/<[^>]+>/g, "")
|
|
19
|
+
.replace(/ /g, " ")
|
|
20
|
+
.replace(/&/g, "&")
|
|
21
|
+
.replace(/</g, "<")
|
|
22
|
+
.replace(/>/g, ">")
|
|
23
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
function parseQuestionHtml(html) {
|
|
27
|
+
const qtextMatch = html.match(/<div class="qtext">([\s\S]*?)<\/div>\s*<\/div>/);
|
|
28
|
+
const text = stripHtmlKeepLines(qtextMatch?.[1] ?? "");
|
|
29
|
+
const options = [];
|
|
30
|
+
const optionRegex = /data-region="answer-label">([\s\S]*?)<\/div>\s*<\/div>/g;
|
|
31
|
+
let match;
|
|
32
|
+
while ((match = optionRegex.exec(html)) !== null) {
|
|
33
|
+
options.push(stripHtmlKeepLines(match[1]));
|
|
34
|
+
}
|
|
35
|
+
return { text, options };
|
|
36
|
+
}
|
|
37
|
+
function parseSavedAnswer(html) {
|
|
38
|
+
const radioChecked = html.match(/<input type="radio"[^>]*value="(\d+)"[^>]*checked="checked"/);
|
|
39
|
+
if (radioChecked && radioChecked[1] !== "-1")
|
|
40
|
+
return radioChecked[1];
|
|
41
|
+
const checkboxChecked = [...html.matchAll(/<input type="checkbox"[^>]*name="[^"]*choice(\d+)"[^>]*checked="checked"/g)];
|
|
42
|
+
if (checkboxChecked.length > 0)
|
|
43
|
+
return checkboxChecked.map(m => m[1]);
|
|
44
|
+
// Match <input> with both name="*_answer" and type="text" in any attribute order
|
|
45
|
+
const textMatch = html.match(/<input[^>]*(?:name="[^"]*:_answer"|type="text")[^>]*(?:name="[^"]*:_answer"|type="text")[^>]*value="([^"]*)"/);
|
|
46
|
+
if (textMatch && textMatch[1] !== "")
|
|
47
|
+
return textMatch[1];
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function parseQuizQuestions(questions) {
|
|
51
|
+
return Object.values(questions).map((q) => {
|
|
52
|
+
const parsed = parseQuestionHtml(q.html ?? "");
|
|
53
|
+
const savedAnswer = parseSavedAnswer(q.html ?? "");
|
|
54
|
+
return {
|
|
55
|
+
slot: q.slot,
|
|
56
|
+
type: q.type,
|
|
57
|
+
status: q.status,
|
|
58
|
+
stateclass: q.stateclass,
|
|
59
|
+
savedAnswer,
|
|
60
|
+
question: parsed.text,
|
|
61
|
+
options: parsed.options,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
17
65
|
function registerQuizzesCommand(program) {
|
|
18
66
|
const quizzesCmd = program.command("quizzes");
|
|
19
67
|
quizzesCmd.description("Quiz operations");
|
|
@@ -26,19 +74,19 @@ function registerQuizzesCommand(program) {
|
|
|
26
74
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
27
75
|
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
28
76
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
29
|
-
const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
|
|
77
|
+
const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
|
|
30
78
|
const baseDir = (0, utils_js_1.getBaseDir)();
|
|
31
79
|
const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
|
|
32
80
|
// Check if session exists
|
|
33
81
|
if (!node_fs_1.default.existsSync(sessionPath)) {
|
|
34
|
-
log.error("未找到登入 session。請先執行 'openape
|
|
82
|
+
log.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
35
83
|
log.info(`Session 預期位置: ${sessionPath}`);
|
|
36
84
|
return null;
|
|
37
85
|
}
|
|
38
86
|
// Try to load WS token
|
|
39
87
|
const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
|
|
40
88
|
if (!wsToken) {
|
|
41
|
-
log.error("未找到 WS token。請先執行 'openape
|
|
89
|
+
log.error("未找到 WS token。請先執行 'openape login' 進行登入。");
|
|
42
90
|
return null;
|
|
43
91
|
}
|
|
44
92
|
return {
|
|
@@ -49,45 +97,11 @@ function registerQuizzesCommand(program) {
|
|
|
49
97
|
},
|
|
50
98
|
};
|
|
51
99
|
}
|
|
52
|
-
// Helper function to create session context (for open command only)
|
|
53
|
-
async function createSessionContext(options, command) {
|
|
54
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
55
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
56
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
57
|
-
const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
|
|
58
|
-
const baseDir = (0, utils_js_1.getBaseDir)();
|
|
59
|
-
const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
|
|
60
|
-
if (!node_fs_1.default.existsSync(sessionPath)) {
|
|
61
|
-
log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
const config = {
|
|
65
|
-
username: "",
|
|
66
|
-
password: "",
|
|
67
|
-
courseUrl: "",
|
|
68
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
69
|
-
headless: !options.headed,
|
|
70
|
-
slowMo: 0,
|
|
71
|
-
authStatePath: sessionPath,
|
|
72
|
-
ollamaBaseUrl: "",
|
|
73
|
-
};
|
|
74
|
-
log.info("啟動瀏覽器...");
|
|
75
|
-
const { browser, context, page } = await (0, auth_js_1.launchAuthenticated)(config, log);
|
|
76
|
-
try {
|
|
77
|
-
const session = await (0, session_js_1.extractSessionInfo)(page, config, log);
|
|
78
|
-
return { log, page, session, browser, context };
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
await context.close();
|
|
82
|
-
await browser.close();
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
100
|
quizzesCmd
|
|
87
101
|
.command("list")
|
|
88
|
-
.description("List quizzes in a course")
|
|
102
|
+
.description("List incomplete quizzes in a course")
|
|
89
103
|
.argument("<course-id>", "Course ID")
|
|
90
|
-
.option("--
|
|
104
|
+
.option("--all", "Include completed quizzes")
|
|
91
105
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
92
106
|
.action(async (courseId, options, command) => {
|
|
93
107
|
const output = getOutputFormat(command);
|
|
@@ -97,16 +111,19 @@ function registerQuizzesCommand(program) {
|
|
|
97
111
|
return;
|
|
98
112
|
}
|
|
99
113
|
const quizzes = await (0, moodle_js_1.getQuizzesByCoursesApi)(apiContext.session, [parseInt(courseId, 10)]);
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
// Default: only show incomplete quizzes
|
|
115
|
+
const filtered = options.all ? quizzes : quizzes.filter(q => !q.isComplete);
|
|
116
|
+
const formattedQuizzes = filtered.map(({ courseId, ...q }) => ({
|
|
117
|
+
...q,
|
|
118
|
+
timeClose: q.timeClose ? (0, utils_js_1.formatTimestamp)(q.timeClose) : null,
|
|
119
|
+
}));
|
|
120
|
+
(0, index_js_1.formatAndOutput)(formattedQuizzes, output, apiContext.log);
|
|
105
121
|
});
|
|
106
122
|
quizzesCmd
|
|
107
123
|
.command("list-all")
|
|
108
|
-
.description("List all
|
|
124
|
+
.description("List all incomplete quizzes across all courses")
|
|
109
125
|
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
126
|
+
.option("--all", "Include completed quizzes")
|
|
110
127
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
111
128
|
.action(async (options, command) => {
|
|
112
129
|
const output = getOutputFormat(command);
|
|
@@ -127,13 +144,17 @@ function registerQuizzesCommand(program) {
|
|
|
127
144
|
const allQuizzes = [];
|
|
128
145
|
for (const q of apiQuizzes) {
|
|
129
146
|
const course = courseMap.get(q.courseId);
|
|
130
|
-
if (course) {
|
|
147
|
+
if (course && (options.all || !q.isComplete)) {
|
|
131
148
|
allQuizzes.push({
|
|
132
149
|
courseName: course.fullname,
|
|
150
|
+
courseId: q.courseId,
|
|
133
151
|
name: q.name,
|
|
134
152
|
url: q.url,
|
|
135
|
-
|
|
153
|
+
quizid: q.quizid,
|
|
136
154
|
isComplete: q.isComplete,
|
|
155
|
+
attemptsUsed: q.attemptsUsed,
|
|
156
|
+
maxAttempts: q.maxAttempts,
|
|
157
|
+
timeClose: q.timeClose ? (0, utils_js_1.formatTimestamp)(q.timeClose) : null,
|
|
137
158
|
});
|
|
138
159
|
}
|
|
139
160
|
}
|
|
@@ -141,26 +162,116 @@ function registerQuizzesCommand(program) {
|
|
|
141
162
|
(0, index_js_1.formatAndOutput)(allQuizzes, output, apiContext.log);
|
|
142
163
|
});
|
|
143
164
|
quizzesCmd
|
|
144
|
-
.command("
|
|
145
|
-
.description("
|
|
146
|
-
.argument("<quiz-
|
|
147
|
-
.option("--
|
|
148
|
-
.action(async (
|
|
149
|
-
const
|
|
150
|
-
|
|
165
|
+
.command("start")
|
|
166
|
+
.description("Start a new quiz attempt")
|
|
167
|
+
.argument("<quiz-id>", "Quiz ID")
|
|
168
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
169
|
+
.action(async (quizCmid, options, command) => {
|
|
170
|
+
const output = getOutputFormat(command);
|
|
171
|
+
const apiContext = await createApiContext(options, command);
|
|
172
|
+
if (!apiContext) {
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const result = await (0, moodle_js_1.startQuizAttemptApi)(apiContext.session, quizCmid);
|
|
178
|
+
apiContext.log.success(`Quiz attempt ${result.attempt.attemptid} started.`);
|
|
179
|
+
const attemptId = result.attempt.attemptid;
|
|
180
|
+
const data = await (0, moodle_js_1.getAllQuizAttemptDataApi)(apiContext.session, attemptId);
|
|
181
|
+
const questions = parseQuizQuestions(data.questions);
|
|
182
|
+
const outputData = [{
|
|
183
|
+
attemptId,
|
|
184
|
+
quizId: result.attempt.quizid,
|
|
185
|
+
state: result.attempt.state,
|
|
186
|
+
timeStart: (0, utils_js_1.formatTimestamp)(result.attempt.timestart),
|
|
187
|
+
timeFinish: result.attempt.timefinish
|
|
188
|
+
? (0, utils_js_1.formatTimestamp)(result.attempt.timefinish)
|
|
189
|
+
: null,
|
|
190
|
+
isPreview: result.attempt.preview,
|
|
191
|
+
totalQuestions: questions.length,
|
|
192
|
+
questions,
|
|
193
|
+
}];
|
|
194
|
+
(0, index_js_1.formatAndOutput)(outputData, output, apiContext.log);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
apiContext.log.error(`Failed to start quiz attempt: ${error instanceof Error ? error.message : String(error)}`);
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
quizzesCmd
|
|
202
|
+
.command("info")
|
|
203
|
+
.description("Get quiz attempt data and questions")
|
|
204
|
+
.argument("<attempt-id>", "Quiz attempt ID")
|
|
205
|
+
.option("--page <number>", "Page number (-1 for all pages)", "-1")
|
|
206
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
207
|
+
.action(async (attemptId, options, command) => {
|
|
208
|
+
const output = getOutputFormat(command);
|
|
209
|
+
const apiContext = await createApiContext(options, command);
|
|
210
|
+
if (!apiContext) {
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const pageNumber = parseInt(options.page);
|
|
216
|
+
const data = pageNumber === -1
|
|
217
|
+
? await (0, moodle_js_1.getAllQuizAttemptDataApi)(apiContext.session, parseInt(attemptId))
|
|
218
|
+
: await (0, moodle_js_1.getQuizAttemptDataApi)(apiContext.session, parseInt(attemptId), pageNumber);
|
|
219
|
+
const questions = parseQuizQuestions(data.questions);
|
|
220
|
+
const outputData = [{
|
|
221
|
+
attemptId: data.attempt.attemptid,
|
|
222
|
+
quizId: data.attempt.quizid,
|
|
223
|
+
state: data.attempt.state,
|
|
224
|
+
totalQuestions: questions.length,
|
|
225
|
+
questions,
|
|
226
|
+
}];
|
|
227
|
+
apiContext.log.success(`Retrieved attempt ${data.attempt.attemptid}`);
|
|
228
|
+
(0, index_js_1.formatAndOutput)(outputData, output, apiContext.log);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
apiContext.log.error(`Failed to get attempt data: ${error instanceof Error ? error.message : String(error)}`);
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
quizzesCmd
|
|
236
|
+
.command("save")
|
|
237
|
+
.description("Save answers for a quiz attempt")
|
|
238
|
+
.argument("<attempt-id>", "Quiz attempt ID")
|
|
239
|
+
.argument("<answers>", "Answers JSON: [{slot:1,answer:\"0\"}] multichoice=number, multichoices=\"0,2\", shortanswer=\"text\"")
|
|
240
|
+
.option("--submit", "Submit the attempt after saving")
|
|
241
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
242
|
+
.action(async (attemptId, answersJson, options, command) => {
|
|
243
|
+
const output = getOutputFormat(command);
|
|
244
|
+
const apiContext = await createApiContext(options, command);
|
|
245
|
+
if (!apiContext) {
|
|
151
246
|
process.exitCode = 1;
|
|
152
247
|
return;
|
|
153
248
|
}
|
|
154
|
-
|
|
249
|
+
let answers;
|
|
155
250
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
log.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
251
|
+
answers = JSON.parse(answersJson);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
apiContext.log.error("Invalid answers JSON. Expected format: [{\"slot\":1,\"answer\":\"0\"},...]");
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
// Get attempt data to find uniqueid and sequencecheck values
|
|
260
|
+
const attemptData = await (0, moodle_js_1.getAllQuizAttemptDataApi)(apiContext.session, parseInt(attemptId));
|
|
261
|
+
const uniqueId = attemptData.attempt.uniqueid ?? attemptData.attempt.attemptid;
|
|
262
|
+
const sequenceChecks = new Map();
|
|
263
|
+
for (const q of Object.values(attemptData.questions)) {
|
|
264
|
+
if (q.sequencecheck !== undefined) {
|
|
265
|
+
sequenceChecks.set(q.slot, q.sequencecheck);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const result = await (0, moodle_js_1.processQuizAttemptApi)(apiContext.session, parseInt(attemptId), uniqueId, answers, sequenceChecks, !!options.submit);
|
|
269
|
+
apiContext.log.success(`Attempt ${attemptId} state: ${result.state}`);
|
|
270
|
+
(0, index_js_1.formatAndOutput)([result], output, apiContext.log);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
apiContext.log.error(`Failed to submit attempt: ${error instanceof Error ? error.message : String(error)}`);
|
|
274
|
+
process.exitCode = 1;
|
|
164
275
|
}
|
|
165
276
|
});
|
|
166
277
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../../src/src/commands/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../../src/src/commands/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2CpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuE5D"}
|
|
@@ -29,12 +29,10 @@ async function readSkillContent() {
|
|
|
29
29
|
// When running from source: src/commands/ → ../../skills/openape/SKILL.md
|
|
30
30
|
// When bundled by dnt into build/: esm/commands/ or script/ → ../../skills/openape/SKILL.md
|
|
31
31
|
const localPath = node_path_1.default.resolve(normalized, "..", "..", "skills", SKILL_NAME, "SKILL.md");
|
|
32
|
-
|
|
33
|
-
return node_fs_1.default.readFileSync(localPath, "utf-8");
|
|
34
|
-
}
|
|
32
|
+
return await node_fs_1.default.promises.readFile(localPath, "utf-8");
|
|
35
33
|
}
|
|
36
34
|
catch {
|
|
37
|
-
// import.meta.url may be unavailable in some environments
|
|
35
|
+
// import.meta.url may be unavailable in some environments, or file doesn't exist
|
|
38
36
|
}
|
|
39
37
|
// Fallback: fetch from GitHub
|
|
40
38
|
const res = await fetch(GITHUB_RAW_URL, { headers: { "User-Agent": "openape-cli" } });
|
|
@@ -87,10 +85,8 @@ function registerSkillsCommand(program) {
|
|
|
87
85
|
for (const target of targets) {
|
|
88
86
|
console.log(`Installing to ${target.name} (${target.path})...`);
|
|
89
87
|
const destDir = node_path_1.default.join(target.path, SKILL_NAME);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
node_fs_1.default.writeFileSync(node_path_1.default.join(destDir, "SKILL.md"), content, "utf-8");
|
|
88
|
+
await node_fs_1.default.promises.mkdir(destDir, { recursive: true });
|
|
89
|
+
await node_fs_1.default.promises.writeFile(node_path_1.default.join(destDir, "SKILL.md"), content, "utf-8");
|
|
94
90
|
console.log(` \x1b[32m✔\x1b[0m ${SKILL_NAME} installed!`);
|
|
95
91
|
}
|
|
96
92
|
console.log("\nDone!");
|
|
@@ -0,0 +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,CA2D5D"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerUploadCommand = registerUploadCommand;
|
|
7
|
+
const utils_js_1 = require("../lib/utils.js");
|
|
8
|
+
const moodle_js_1 = require("../lib/moodle.js");
|
|
9
|
+
const auth_js_1 = require("../lib/auth.js");
|
|
10
|
+
const index_js_1 = require("../index.js");
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
13
|
+
function registerUploadCommand(program) {
|
|
14
|
+
const uploadCmd = program.command("upload");
|
|
15
|
+
uploadCmd.description("Upload files to Moodle draft area");
|
|
16
|
+
uploadCmd
|
|
17
|
+
.command("file")
|
|
18
|
+
.description("Upload a file to Moodle draft area")
|
|
19
|
+
.argument("<file-path>", "Path to the file to upload")
|
|
20
|
+
.option("--filename <name>", "Custom filename (default: original filename)")
|
|
21
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
22
|
+
.action(async (filePath, options, command) => {
|
|
23
|
+
const output = (0, utils_js_1.getOutputFormat)(command);
|
|
24
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
25
|
+
if (!apiContext) {
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Resolve file path
|
|
30
|
+
const resolvedPath = node_path_1.default.resolve(filePath);
|
|
31
|
+
// Check if file exists
|
|
32
|
+
try {
|
|
33
|
+
await promises_1.default.access(resolvedPath);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
apiContext.log.error(`檔案不存在: ${filePath}`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Get file size
|
|
41
|
+
const stats = await promises_1.default.stat(resolvedPath);
|
|
42
|
+
const fileSizeKB = (0, utils_js_1.formatFileSize)(stats.size);
|
|
43
|
+
apiContext.log.info(`上傳檔案: ${node_path_1.default.basename(resolvedPath)} (${fileSizeKB} KB)`);
|
|
44
|
+
// Upload file
|
|
45
|
+
const result = await (0, moodle_js_1.uploadFileApi)(apiContext.session, resolvedPath, {
|
|
46
|
+
filename: options.filename,
|
|
47
|
+
});
|
|
48
|
+
if (!result.success) {
|
|
49
|
+
apiContext.log.error(`上傳失敗: ${result.error}`);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
apiContext.log.info(`✓ 上傳成功!Draft ID: ${result.draftId}`);
|
|
54
|
+
const uploadResult = {
|
|
55
|
+
success: true,
|
|
56
|
+
draft_id: result.draftId,
|
|
57
|
+
filename: node_path_1.default.basename(resolvedPath),
|
|
58
|
+
filesize: stats.size,
|
|
59
|
+
filesize_kb: (0, utils_js_1.formatFileSize)(stats.size),
|
|
60
|
+
message: "Use this draft ID for assignment submission or forum posts",
|
|
61
|
+
};
|
|
62
|
+
(0, index_js_1.formatAndOutput)(uploadResult, output, apiContext.log);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -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;AAYpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"videos.d.ts","sourceRoot":"","sources":["../../../src/src/commands/videos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgY5D"}
|
|
@@ -23,19 +23,19 @@ function registerVideosCommand(program) {
|
|
|
23
23
|
// Don't silence logs for commands that don't have explicit output format control
|
|
24
24
|
const outputFormat = command && command.optsWithGlobals ? (0, utils_js_1.getOutputFormat)(command) : "table";
|
|
25
25
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
26
|
-
const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
|
|
26
|
+
const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
|
|
27
27
|
const baseDir = (0, utils_js_1.getBaseDir)();
|
|
28
28
|
const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
|
|
29
29
|
// Check if session exists
|
|
30
30
|
if (!node_fs_1.default.existsSync(sessionPath)) {
|
|
31
|
-
|
|
31
|
+
console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
32
32
|
log.info(`Session 預期位置: ${sessionPath}`);
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
// Try to load WS token
|
|
36
36
|
const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
|
|
37
37
|
if (!wsToken) {
|
|
38
|
-
|
|
38
|
+
console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
41
|
return {
|
|
@@ -51,11 +51,11 @@ function registerVideosCommand(program) {
|
|
|
51
51
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
52
52
|
const outputFormat = (0, utils_js_1.getOutputFormat)(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
53
53
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
54
|
-
const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
|
|
54
|
+
const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
|
|
55
55
|
const baseDir = (0, utils_js_1.getBaseDir)();
|
|
56
56
|
const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
|
|
57
57
|
if (!node_fs_1.default.existsSync(sessionPath)) {
|
|
58
|
-
|
|
58
|
+
console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
const config = {
|
|
@@ -322,7 +322,7 @@ function registerVideosCommand(program) {
|
|
|
322
322
|
const completed = downloaded.filter(d => d.success).length;
|
|
323
323
|
const failed = downloaded.filter(d => !d.success).length;
|
|
324
324
|
log.info(`\n執行結果: ${completed} 成功, ${failed} 失敗`);
|
|
325
|
-
|
|
325
|
+
console.log(JSON.stringify({
|
|
326
326
|
status: "success",
|
|
327
327
|
timestamp: new Date().toISOString(),
|
|
328
328
|
course_id: courseId,
|
|
@@ -330,9 +330,10 @@ function registerVideosCommand(program) {
|
|
|
330
330
|
total_videos: videos.length,
|
|
331
331
|
downloaded: completed,
|
|
332
332
|
failed,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
333
|
+
}));
|
|
334
|
+
for (const v of downloaded) {
|
|
335
|
+
console.log(JSON.stringify(v));
|
|
336
|
+
}
|
|
336
337
|
}
|
|
337
338
|
finally {
|
|
338
339
|
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":";AACA,OAAO,sBAAsB,CAAC;AAO9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":";AACA,OAAO,sBAAsB,CAAC;AAO9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAwEnF;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GACA,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,iBAAiB,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBhH;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,EACb,MAAM,EAAE,YAAY,EACpB,GAAG,EAAE,MAAM,GACV,IAAI,CAyBN;AA2CD,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,CAAC"}
|
package/script/src/index.js
CHANGED
|
@@ -27,6 +27,8 @@ const forums_js_1 = require("./commands/forums.js");
|
|
|
27
27
|
const announcements_js_1 = require("./commands/announcements.js");
|
|
28
28
|
const calendar_js_1 = require("./commands/calendar.js");
|
|
29
29
|
const skills_js_1 = require("./commands/skills.js");
|
|
30
|
+
const assignments_js_1 = require("./commands/assignments.js");
|
|
31
|
+
const upload_js_1 = require("./commands/upload.js");
|
|
30
32
|
const program = new commander_1.Command();
|
|
31
33
|
program
|
|
32
34
|
.name("openape")
|
|
@@ -51,6 +53,8 @@ program
|
|
|
51
53
|
(0, announcements_js_1.registerAnnouncementsCommand)(program);
|
|
52
54
|
(0, calendar_js_1.registerCalendarCommand)(program);
|
|
53
55
|
(0, skills_js_1.registerSkillsCommand)(program);
|
|
56
|
+
(0, assignments_js_1.registerAssignmentsCommand)(program);
|
|
57
|
+
(0, upload_js_1.registerUploadCommand)(program);
|
|
54
58
|
/**
|
|
55
59
|
* Load configuration and authenticate, returning the context for commands.
|
|
56
60
|
*/
|
|
@@ -93,7 +97,14 @@ async function createSessionContext(options) {
|
|
|
93
97
|
*/
|
|
94
98
|
function formatAndOutput(data, format, log) {
|
|
95
99
|
if (format === "json") {
|
|
96
|
-
|
|
100
|
+
if (Array.isArray(data)) {
|
|
101
|
+
for (const item of data) {
|
|
102
|
+
console.log(JSON.stringify(item));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.log(JSON.stringify(data));
|
|
107
|
+
}
|
|
97
108
|
// Exit immediately for AI agent - no need to wait for browser cleanup
|
|
98
109
|
process.exit(0);
|
|
99
110
|
}
|
package/script/src/lib/auth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Browser, type BrowserContext, type Page } from "playwright-core";
|
|
2
|
-
import type { AppConfig, Logger } from "./types.js";
|
|
2
|
+
import type { AppConfig, Logger, OutputFormat } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Find a Chromium-based browser executable on Windows.
|
|
5
5
|
* Priority: Edge → Chrome → Brave
|
|
@@ -20,6 +20,28 @@ export declare function launchAuthenticated(config: AppConfig, log: Logger): Pro
|
|
|
20
20
|
* Safely close browser and context with timeout.
|
|
21
21
|
* Designed for AI agent usage - no human interaction needed.
|
|
22
22
|
* If noWait is true, initiates cleanup but doesn't wait for completion.
|
|
23
|
+
*
|
|
24
|
+
* Note: Closes sequentially (context first, then browser) to avoid libuv
|
|
25
|
+
* assertion failures on Windows when handles are closed concurrently.
|
|
23
26
|
*/
|
|
24
27
|
export declare function closeBrowserSafely(browser: Browser, context?: BrowserContext, timeoutMs?: number, noWait?: boolean): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Create API context for WS token operations (no browser required).
|
|
30
|
+
* Returns null if session is invalid or WS token is missing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createApiContext(options: {
|
|
33
|
+
verbose?: boolean;
|
|
34
|
+
headed?: boolean;
|
|
35
|
+
}, command?: {
|
|
36
|
+
optsWithGlobals(): {
|
|
37
|
+
output?: OutputFormat;
|
|
38
|
+
verbose?: boolean;
|
|
39
|
+
};
|
|
40
|
+
}): Promise<{
|
|
41
|
+
log: Logger;
|
|
42
|
+
session: {
|
|
43
|
+
wsToken: string;
|
|
44
|
+
moodleBaseUrl: string;
|
|
45
|
+
};
|
|
46
|
+
} | null>;
|
|
25
47
|
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/lib/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACzF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/lib/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACzF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAGlE;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAuBrC;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA4DtF;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,cAAc,EACxB,SAAS,GAAE,MAAa,EACxB,MAAM,GAAE,OAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAoHD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAChD,OAAO,CAAC,EAAE;IAAE,eAAe,IAAI;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GAC5E,OAAO,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD,GAAG,IAAI,CAAC,CA0BR"}
|