neuro-canvas-lms 2.12.0 → 2.13.0
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/AGENTS.md +6 -3
- package/README.md +13 -6
- package/canvas.mjs +61 -16
- package/file-content.mjs +38 -8
- package/find-files.mjs +49 -10
- package/i18n.mjs +93 -22
- package/installer/cli.mjs +43 -13
- package/installer/lib.mjs +59 -4
- package/installer/messages.mjs +50 -12
- package/installer/subdomain.mjs +58 -0
- package/module-file.mjs +8 -4
- package/package.json +7 -2
- package/page-scene.mjs +904 -0
- package/pages.mjs +186 -84
- package/version.mjs +1 -1
- package/worker.mjs +54 -21
- package/zip-content.mjs +45 -14
package/AGENTS.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
This repo, **Neuro-CanvasLMS** (`neuro-canvas-lms`), is a read-only Canvas LMS connector (remote MCP
|
|
4
4
|
server on Cloudflare Workers) that each user deploys to their **own** Cloudflare account. It works
|
|
5
|
-
with any Canvas school; nothing school-specific is in the
|
|
6
|
-
It is a community project provided as is.
|
|
5
|
+
with any Canvas school that lets students create access tokens; nothing school-specific is in the
|
|
6
|
+
code (the school comes from Worker vars). It is a community project provided as is.
|
|
7
7
|
|
|
8
8
|
- To install, update, reconnect, uninstall or troubleshoot: follow
|
|
9
9
|
`.claude/skills/canvas-connector-setup/SKILL.md` (Claude Code loads it as a skill; other agents
|
|
@@ -30,9 +30,12 @@ Code map:
|
|
|
30
30
|
`download-link.mjs` (one-time, minutes-long Canvas storage link so coding agents can curl an original
|
|
31
31
|
file to disk).
|
|
32
32
|
- `owner.mjs`: owner binding, allowed OAuth clients. `i18n.mjs` + `pages.mjs`: web pages.
|
|
33
|
+
`page-scene.mjs`: their one script (allowed by its hash in the CSP; pages stay off the network):
|
|
34
|
+
the black-hole scene and the step-by-step authorization. Everything works without it.
|
|
33
35
|
- `update-check.mjs`: once a day reads the newest version number from npm; when it is newer the AI
|
|
34
36
|
tells the user how to update. Also the `canvas_connector_version` tool.
|
|
35
|
-
- `installer/`: the setup wizard.
|
|
37
|
+
- `installer/`: the setup wizard. On a new Cloudflare account it registers a random workers.dev
|
|
38
|
+
subdomain through Cloudflare's API. Re-running it asks once (y/n), then updates with the saved
|
|
36
39
|
settings (without a terminal it does not ask). `installer/verify.mjs`: the post-deploy online
|
|
37
40
|
check, also run alone by `npx -y neuro-canvas-lms check`.
|
|
38
41
|
- `version.mjs`: shown on the home page and /healthz, compared with npm by update-check.mjs.
|
package/README.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
**English** · [简体中文](docs/README.zh-CN.md) · [Español](docs/README.es.md) · [日本語](docs/README.ja.md) · [हिन्दी](docs/README.hi.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Wire your Canvas LMS straight into Claude and ChatGPT.** Your courses, deadlines, grades and lecture files become part of what your AI knows, in any chat and on any device. It is a read-only remote MCP server that one command deploys to your own free Cloudflare account. Works at any Canvas school that lets students create access tokens ([30-second check](#can-you-use-it)).
|
|
8
|
+
|
|
9
|
+
> "What's due this week?" · "Summarize Lecture 5 in MATH 132." · "What feedback did I get on HW 3?" · "What's in the code ZIP for Lecture 12?" · "Set up the PA2 starter code in this folder."
|
|
8
10
|
|
|
9
11
|
## Why Neuro-CanvasLMS
|
|
10
12
|
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
| 💸 **Free** | It runs on Cloudflare's free plan, which is far more than one student needs. |
|
|
16
18
|
| 🧠 **Your files live in the conversation** | Lecture notes and PDFs the AI reads stay with that chat on your AI provider's servers. Nothing to download. |
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
Local Canvas tools only work while your own computer runs them. Hosted services keep your Canvas token, or a copy of your courses, on their servers. Neuro-CanvasLMS runs in **your own** Cloudflare account: always on, usable from your phone and in ChatGPT, and your token goes only to your own Worker and your school's Canvas.
|
|
19
21
|
|
|
20
22
|
## What you get
|
|
21
23
|
|
|
@@ -23,20 +25,25 @@ Most Canvas integrations for AI are **local** tools: they only work while your o
|
|
|
23
25
|
- **Real file reading:** text from PDF, Word, PowerPoint and Excel, about 90 kinds of code and text files, and Jupyter notebooks (cells and outputs); ZIP archives unpacked on the server (listing plus every code/text file); scanned or handwritten PDFs are recognised and their original pages handed to the AI to look at.
|
|
24
26
|
- **Linked files too:** course files that a module links on Google Docs/Slides/Sheets, Google Drive, Dropbox (files and folders) and GitHub (files and repositories) are read as text, as long as they are shared publicly ("Anyone with the link").
|
|
25
27
|
- **Finds files the course hides:** many courses hide their Files list from students. The connector searches the front page, syllabus, modules, assignments, announcements and the pages they link to, so the AI can still find "the Lecture 12 slides" by name.
|
|
28
|
+
- **Starter code onto your computer:** in Claude Code, Codex and other coding agents, ask "set up PA2 from Canvas". The agent gets a one-time download link that expires in minutes, saves the assignment's starter files in your folder and unpacks them.
|
|
26
29
|
- **Read-only by design:** only fixed `GET` requests. It never submits work, sends messages, starts quizzes or changes anything in Canvas.
|
|
27
30
|
- **Yours only:** deployed in your own Cloudflare account and locked to your own Canvas account. Nobody else, including the authors, ever sees your Canvas token.
|
|
28
31
|
|
|
32
|
+
## Can you use it?
|
|
33
|
+
|
|
34
|
+
Check in 30 seconds: in Canvas, open **Account → Settings** and scroll to **Approved Integrations**. If you see a **+ New Access Token** button, you're set. If it is missing, or creating a token is refused, your school doesn't let students make personal tokens, and this connector can't work there yet.
|
|
35
|
+
|
|
29
36
|
## Install (about 5 minutes)
|
|
30
37
|
|
|
31
|
-
You need [Node.js 22+](https://nodejs.org), a free [Cloudflare account](https://dash.cloudflare.com/sign-up) and your Canvas login. In a terminal, run:
|
|
38
|
+
You need [Node.js 22+](https://nodejs.org), a free [Cloudflare account](https://dash.cloudflare.com/sign-up) (verify its email address) and your Canvas login. In a terminal, run:
|
|
32
39
|
|
|
33
40
|
```bash
|
|
34
41
|
npx -y neuro-canvas-lms
|
|
35
42
|
```
|
|
36
43
|
|
|
37
|
-
The installer signs you in to Cloudflare (a browser page opens; click **Authorize**), asks for your school's Canvas address, deploys the connector to your own account and checks that it works. Then:
|
|
44
|
+
The installer signs you in to Cloudflare (a browser page opens; click **Authorize**), asks for your school's Canvas address, sets up your free workers.dev address if the account is new, deploys the connector to your own account and checks that it works. A new address can take a few minutes to come online; meanwhile the installer opens Canvas so you can create your token. Then:
|
|
38
45
|
|
|
39
|
-
1. **Create a Canvas token
|
|
46
|
+
1. **Create a Canvas token** (if you haven't yet): Canvas → Account → Settings → **+ New Access Token**. Your school may cap its lifetime (30 days at UMass Amherst). Copy it right away; Canvas shows it only once.
|
|
40
47
|
2. **Claude:** Settings → Connectors → **Add custom connector** → paste the URL the installer printed (it is already copied) → **Connect** → paste the Canvas token → Authorize. It then works everywhere you use Claude, including the mobile app and Claude Code.
|
|
41
48
|
3. **ChatGPT:** add a custom connector (developer mode) with the same URL.
|
|
42
49
|
4. Start a new chat and ask: "What assignments do I have this week?"
|
package/canvas.mjs
CHANGED
|
@@ -229,27 +229,55 @@ export async function callCanvas(name, args, token, fetcher = fetch, opts = {})
|
|
|
229
229
|
return out;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// Canvas could not be reached at all: say so (and that the connection is fine), so the AI retries
|
|
233
|
+
// instead of changing its arguments. Our own transport messages already start with "Canvas".
|
|
234
|
+
export function unreachable(e) {
|
|
235
|
+
if (/^Canvas/.test(e?.message ?? '')) return e;
|
|
236
|
+
const timeout = e?.name === 'TimeoutError' || e?.name === 'AbortError';
|
|
237
|
+
return Object.assign(
|
|
238
|
+
new Error(
|
|
239
|
+
timeout
|
|
240
|
+
? 'Canvas did not answer within 20 seconds; it may be slow or down right now. Try again in a minute. The token and the connection are fine: do not reconnect.'
|
|
241
|
+
: `Canvas could not be reached (network error: ${e?.name ?? 'Error'}). Try again in a minute. The token and the connection are fine: do not reconnect.`,
|
|
242
|
+
),
|
|
243
|
+
{ unreachable: true, cause: e },
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
232
247
|
// One GET to the Canvas API: fixed headers, no redirects, Canvas errors turned into safe messages
|
|
233
248
|
// (never the upstream body). `e.status` is set when Canvas refused this item (not a token problem).
|
|
234
249
|
export async function canvasGet(u, token, fetcher = fetch) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
250
|
+
let r;
|
|
251
|
+
try {
|
|
252
|
+
r = await fetcher(u, {
|
|
253
|
+
method: 'GET',
|
|
254
|
+
headers: {
|
|
255
|
+
Authorization: `Bearer ${token}`,
|
|
256
|
+
Accept: 'application/json',
|
|
257
|
+
'User-Agent': 'Neuro-CanvasLMS/2.0 (read-only personal integration)',
|
|
258
|
+
},
|
|
259
|
+
redirect: 'manual',
|
|
260
|
+
signal: AbortSignal.timeout(20000),
|
|
261
|
+
});
|
|
262
|
+
} catch (e) {
|
|
263
|
+
throw unreachable(e);
|
|
264
|
+
}
|
|
245
265
|
if (
|
|
246
266
|
r.status === 406 ||
|
|
247
267
|
(r.status >= 500 && !(r.headers.get('content-type') ?? '').includes('json'))
|
|
248
268
|
) {
|
|
249
|
-
// Canvas answers its API with JSON. An HTML error page means
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
269
|
+
// Canvas answers its API with JSON. An HTML error page means Canvas is down or in maintenance
|
|
270
|
+
// (502/503/504 from the load balancer), a firewall in front of Canvas rejected the request, or
|
|
271
|
+
// Canvas failed on a malformed token (an HTML 500; only possible on the authorization page,
|
|
272
|
+
// since a connected token already passed). The phrase "HTML error page" stops canvas_find_files.
|
|
273
|
+
await r.body?.cancel().catch(() => {});
|
|
274
|
+
throw Object.assign(
|
|
275
|
+
new Error(
|
|
276
|
+
[502, 503, 504].includes(r.status)
|
|
277
|
+
? `Canvas is temporarily unavailable (HTTP ${r.status}, an HTML error page instead of JSON). Try again in a few minutes. The token and the connection are fine: do not reconnect.`
|
|
278
|
+
: `Canvas returned an HTML error page (HTTP ${r.status}) instead of JSON: usually a temporary Canvas problem or a firewall in front of Canvas. Try again in a few minutes; reconnecting does not help.`,
|
|
279
|
+
),
|
|
280
|
+
{ htmlError: r.status },
|
|
253
281
|
);
|
|
254
282
|
}
|
|
255
283
|
if (r.status === 401) {
|
|
@@ -276,7 +304,14 @@ export async function canvasGet(u, token, fetcher = fetch) {
|
|
|
276
304
|
),
|
|
277
305
|
{ status: r.status },
|
|
278
306
|
);
|
|
279
|
-
|
|
307
|
+
let data;
|
|
308
|
+
try {
|
|
309
|
+
data = await r.json();
|
|
310
|
+
} catch {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Canvas answered HTTP ${r.status} but not with JSON (a maintenance or login page in between?). Try again in a few minutes; the token and the connection are fine.`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
280
315
|
const next = nextPage(r.headers.get('link'));
|
|
281
316
|
// api_url is the API request itself (it shows JSON); links for the user are the html_url fields.
|
|
282
317
|
return { data, has_more: next !== null, next_page: next, api_url: u.href };
|
|
@@ -299,12 +334,22 @@ const FEED = /https?:\/\/[^\s"'<>()]*\/feeds\/[^\s"'<>()]+/gi;
|
|
|
299
334
|
const LINK = /(?:https?:\/\/|\/api\/v1\/(?:canvadoc|crocodoc)_session)[^\s"'<>()]*/gi;
|
|
300
335
|
const SIGNED =
|
|
301
336
|
/(?:canvadoc|crocodoc)_session|[?&](?:amp;)?(?:sf_verifier|hmac|x-amz-signature|signature)=|[?&](?:amp;)?token=[\w-]+\.[\w-]+\.[\w-]+/i;
|
|
302
|
-
|
|
337
|
+
// A file's `uuid` is the verifier itself (Canvas accepts ?verifier=<uuid>) and its thumbnail_url
|
|
338
|
+
// carries it in the path; a course's uuid is the key of its calendar feed. Raw results lose them.
|
|
339
|
+
const SIGNED_KEYS = new Set([
|
|
340
|
+
'canvadoc_session_url',
|
|
341
|
+
'crocodoc_session_url',
|
|
342
|
+
'uuid',
|
|
343
|
+
'thumbnail_url',
|
|
344
|
+
]);
|
|
345
|
+
// The same pass URL-encoded inside another link (e.g. a login page's return_to=).
|
|
346
|
+
const ENCODED_VERIFIER = /verifier%3D[^&#"'\s<>()%]*/gi;
|
|
303
347
|
export function withoutVerifiers(value) {
|
|
304
348
|
if (typeof value === 'string') {
|
|
305
349
|
let v = value;
|
|
306
350
|
if (v.includes('verifier='))
|
|
307
351
|
v = v.replace(VERIFIER, (m, before, after) => (after ? before : ''));
|
|
352
|
+
if (/verifier%3D/i.test(v)) v = v.replace(ENCODED_VERIFIER, 'verifier%3D');
|
|
308
353
|
if (v.includes('/feeds/')) v = v.replace(FEED, '[private feed link removed]');
|
|
309
354
|
if (/[?]|_session/.test(v))
|
|
310
355
|
v = v.replace(LINK, link => (SIGNED.test(link) ? '[signed link removed]' : link));
|
package/file-content.mjs
CHANGED
|
@@ -226,6 +226,20 @@ export async function extractBytes(bytes, shown, mime = '', opts = {}) {
|
|
|
226
226
|
'This format is not supported for text extraction. Call canvas_file_original with this file_id to get the original file.',
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
|
+
// extractBytes, but a parser failing on the file itself (damaged, password-protected, or not really
|
|
230
|
+
// a PDF/Office file) becomes a note with a way on instead of an error the AI reads as "check your
|
|
231
|
+
// arguments". Our own messages (limits, a wrong `entry`) start with "Canvas" and go through.
|
|
232
|
+
export async function extractOrExplain(bytes, name, mime, opts, wayOn) {
|
|
233
|
+
try {
|
|
234
|
+
return await extractBytes(bytes, name, mime, opts);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
if (/^Canvas/.test(e?.message ?? '')) throw e;
|
|
237
|
+
return {
|
|
238
|
+
text: null,
|
|
239
|
+
extraction_note: `Could not read the text of this file (${e?.name ?? 'Error'}): it may be damaged, password-protected, or not really the type its name says. ${wayOn}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
229
243
|
export async function readFileContent(meta, args, fetcher = fetch) {
|
|
230
244
|
// `hidden` files are only hidden from the Files list ("only available with link"), which is how
|
|
231
245
|
// many courses share files; students may open them. Only a lock stops a download.
|
|
@@ -237,7 +251,14 @@ export async function readFileContent(meta, args, fetcher = fetch) {
|
|
|
237
251
|
html_url: `https://${CANVAS_HOST}/files/${meta.id}`,
|
|
238
252
|
mime_type: meta['content-type'],
|
|
239
253
|
};
|
|
240
|
-
if (!meta.url
|
|
254
|
+
if (!meta.url)
|
|
255
|
+
return {
|
|
256
|
+
...base,
|
|
257
|
+
text: null,
|
|
258
|
+
extraction_note:
|
|
259
|
+
'Canvas gives no download address for this file (it may only be viewable inside Canvas); open html_url.',
|
|
260
|
+
};
|
|
261
|
+
if (!allowedDownload(meta.url))
|
|
241
262
|
return {
|
|
242
263
|
...base,
|
|
243
264
|
text: null,
|
|
@@ -262,14 +283,22 @@ export async function readFileContent(meta, args, fetcher = fetch) {
|
|
|
262
283
|
throw new Error(
|
|
263
284
|
'Canvas file redirect host is not allowed: ' + [...hops, where(url)].join(' → '),
|
|
264
285
|
);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
286
|
+
try {
|
|
287
|
+
response = await fetcher(url, {
|
|
288
|
+
method: 'GET',
|
|
289
|
+
redirect: 'manual',
|
|
290
|
+
headers: { Accept: '*/*', 'User-Agent': DOWNLOAD_UA },
|
|
291
|
+
signal: AbortSignal.timeout(20000),
|
|
292
|
+
});
|
|
293
|
+
} catch (e) {
|
|
294
|
+
if (/^Canvas/.test(e?.message ?? '')) throw e;
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Canvas file download did not finish (${e?.name === 'TimeoutError' || e?.name === 'AbortError' ? 'no answer within 20 seconds' : 'network error: ' + (e?.name ?? 'Error')}; ${where(url)}). Try again in a minute; the connection is fine.`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
271
299
|
hops.push(response.status + ' ' + where(url));
|
|
272
300
|
if (response.status >= 300 && response.status < 400) {
|
|
301
|
+
await response.body?.cancel().catch(() => {});
|
|
273
302
|
const location = response.headers.get('location');
|
|
274
303
|
if (!location) throw new Error('Canvas download redirect is missing its destination.');
|
|
275
304
|
url = new URL(location, url).href;
|
|
@@ -293,11 +322,12 @@ export async function readFileContent(meta, args, fetcher = fetch) {
|
|
|
293
322
|
(response.headers.get('content-type') ?? 'unknown').split(';')[0] +
|
|
294
323
|
').',
|
|
295
324
|
);
|
|
296
|
-
const extracted = await
|
|
325
|
+
const extracted = await extractOrExplain(
|
|
297
326
|
await boundedBytes(response),
|
|
298
327
|
base.name ?? '',
|
|
299
328
|
base.mime_type ?? response.headers.get('content-type') ?? '',
|
|
300
329
|
{ entry: args.entry, storedName: meta.filename },
|
|
330
|
+
'Call canvas_file_original with this file_id to get the file itself, or open html_url.',
|
|
301
331
|
);
|
|
302
332
|
return paginated(base, extracted, args);
|
|
303
333
|
}
|
package/find-files.mjs
CHANGED
|
@@ -180,14 +180,24 @@ export async function findFiles(args, token, fetcher) {
|
|
|
180
180
|
return canvasGet(u, token, fetcher);
|
|
181
181
|
};
|
|
182
182
|
const searched = {};
|
|
183
|
-
//
|
|
183
|
+
// A source being hidden or not enabled (401/403/404) is normal. A source that failed (timeout,
|
|
184
|
+
// network, rate limit, Canvas error) was not searched: the result is then incomplete, and if
|
|
185
|
+
// nothing could be searched at all the error is passed on instead of an empty "complete" list.
|
|
186
|
+
// An expired token or an HTML error page (Canvas down, a firewall) stops everything.
|
|
187
|
+
const failures = [];
|
|
188
|
+
const hidden = e => [401, 403, 404].includes(e.status);
|
|
184
189
|
const attempt = async (source, run) => {
|
|
185
190
|
try {
|
|
186
191
|
const n = await run();
|
|
187
192
|
searched[source] = n === undefined ? 'ok' : `ok (${n})`;
|
|
188
193
|
} catch (e) {
|
|
189
194
|
if (/authorization expired|HTML error page/.test(e.message ?? '')) throw e;
|
|
190
|
-
searched[source] =
|
|
195
|
+
if (hidden(e)) searched[source] = `not available (HTTP ${e.status})`;
|
|
196
|
+
else {
|
|
197
|
+
failures.push(e);
|
|
198
|
+
complete = false;
|
|
199
|
+
searched[source] = e.status ? `failed (HTTP ${e.status})` : 'failed';
|
|
200
|
+
}
|
|
191
201
|
}
|
|
192
202
|
};
|
|
193
203
|
const pagesOf = async (path, query, maxPages, each) => {
|
|
@@ -337,7 +347,8 @@ export async function findFiles(args, token, fetcher) {
|
|
|
337
347
|
|
|
338
348
|
// Pages linked from everything above, breadth first, never outside this course.
|
|
339
349
|
let pagesRead = 0,
|
|
340
|
-
pagesUnavailable = 0
|
|
350
|
+
pagesUnavailable = 0,
|
|
351
|
+
pagesFailed = 0;
|
|
341
352
|
while (queue.length) {
|
|
342
353
|
const batch = queue.splice(0, PAGE_BATCH).filter(slug => !read.has(slug));
|
|
343
354
|
await Promise.all(
|
|
@@ -355,7 +366,12 @@ export async function findFiles(args, token, fetcher) {
|
|
|
355
366
|
});
|
|
356
367
|
} catch (e) {
|
|
357
368
|
if (/authorization expired|HTML error page/.test(e.message ?? '')) throw e;
|
|
358
|
-
pagesUnavailable++;
|
|
369
|
+
if (hidden(e)) pagesUnavailable++;
|
|
370
|
+
else {
|
|
371
|
+
failures.push(e);
|
|
372
|
+
complete = false;
|
|
373
|
+
pagesFailed++;
|
|
374
|
+
}
|
|
359
375
|
}
|
|
360
376
|
}),
|
|
361
377
|
);
|
|
@@ -365,7 +381,20 @@ export async function findFiles(args, token, fetcher) {
|
|
|
365
381
|
}
|
|
366
382
|
}
|
|
367
383
|
searched.linked_pages =
|
|
368
|
-
`${pagesRead} read` +
|
|
384
|
+
`${pagesRead} read` +
|
|
385
|
+
(pagesUnavailable ? `, ${pagesUnavailable} not available` : '') +
|
|
386
|
+
(pagesFailed ? `, ${pagesFailed} failed` : '');
|
|
387
|
+
const sources = Object.entries(searched).filter(([k]) => k !== 'linked_pages');
|
|
388
|
+
// Nothing could be searched: say why instead of "no files, complete".
|
|
389
|
+
if (failures.length && !pagesRead && !sources.some(([, v]) => v.startsWith('ok'))) {
|
|
390
|
+
const e = failures.find(x => /^Canvas/.test(x.message ?? '')) ?? failures[0];
|
|
391
|
+
throw /^Canvas/.test(e.message ?? '')
|
|
392
|
+
? e
|
|
393
|
+
: new Error(`Canvas could not be searched (${e.name}). Try again in a minute.`);
|
|
394
|
+
}
|
|
395
|
+
// Every source 404: most likely not a course this user is in.
|
|
396
|
+
const wrongCourse =
|
|
397
|
+
!files.size && sources.length && sources.every(([, v]) => v === 'not available (HTTP 404)');
|
|
369
398
|
|
|
370
399
|
let list = [...files.values()];
|
|
371
400
|
let matchedOn = null;
|
|
@@ -393,11 +422,21 @@ export async function findFiles(args, token, fetcher) {
|
|
|
393
422
|
external_links: ext,
|
|
394
423
|
searched,
|
|
395
424
|
complete,
|
|
396
|
-
...(
|
|
397
|
-
? {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
425
|
+
...(failures.length
|
|
426
|
+
? {
|
|
427
|
+
note: `Some places could not be searched (they failed, see searched: ${failures.length} failed request${failures.length > 1 ? 's' : ''}), so a file missing here may still exist. Try again in a minute before telling the user it is not there.`,
|
|
428
|
+
}
|
|
429
|
+
: complete
|
|
430
|
+
? {}
|
|
431
|
+
: {
|
|
432
|
+
note: `Stopped after ${requests} Canvas requests; some linked pages or lists were not searched. Open more pages with canvas_context_page or canvas_module_items if the file is not listed.`,
|
|
433
|
+
}),
|
|
434
|
+
...(wrongCourse
|
|
435
|
+
? {
|
|
436
|
+
course_note:
|
|
437
|
+
'Every part of this course answered 404: check the course_id (canvas_courses lists the user’s courses).',
|
|
438
|
+
}
|
|
439
|
+
: {}),
|
|
401
440
|
how_to_read:
|
|
402
441
|
'Read a file_id with canvas_file_content (text from PDF/Office/code, ZIP listing and contents) or canvas_file_original (original bytes, e.g. scanned PDFs). An external link with read_with (a module item): call canvas_module_file_content with those IDs for its text (Google Docs/Slides/Sheets/Drive, Dropbox, GitHub), or canvas_module_file for the original file. Other external links cannot be read by this connector: give the user the link.',
|
|
403
442
|
};
|
package/i18n.mjs
CHANGED
|
@@ -15,15 +15,12 @@ const S = {
|
|
|
15
15
|
en: {
|
|
16
16
|
product: 'Neuro-CanvasLMS',
|
|
17
17
|
tagline: 'Your Canvas, readable by your own AI assistant.',
|
|
18
|
-
home_lead:
|
|
19
|
-
'A private, read-only bridge between your Canvas account and Claude or ChatGPT. Ask about deadlines, grades, lecture files and announcements in plain language.',
|
|
20
18
|
status_bound: 'Connected to its owner',
|
|
21
19
|
status_unbound: 'Not connected yet',
|
|
22
20
|
home_add_title: 'Add it to your assistant',
|
|
23
|
-
home_add_body: 'In Claude or ChatGPT, add a custom connector with this address:',
|
|
24
|
-
home_safe: 'Read-only. It never submits work, sends messages or changes anything in Canvas.',
|
|
25
21
|
canvas_site: 'Canvas site',
|
|
26
22
|
auth_title: 'Connect your {school} Canvas',
|
|
23
|
+
auth_title_plain: 'Connect your Canvas',
|
|
27
24
|
auth_lead: 'Let your own AI assistant read what you can already see in Canvas.',
|
|
28
25
|
read_only: 'Read-only',
|
|
29
26
|
scopes_title: 'What it can read',
|
|
@@ -34,6 +31,7 @@ const S = {
|
|
|
34
31
|
token_placeholder: 'Paste your token here',
|
|
35
32
|
token_how:
|
|
36
33
|
'Create one in Canvas: Account → Settings → New Access Token. Copy it right away — Canvas shows it only once.',
|
|
34
|
+
token_open: 'Open Canvas settings',
|
|
37
35
|
token_privacy:
|
|
38
36
|
'The token goes only to this Worker (which you deployed) and to Canvas, then is stored encrypted inside the authorization. Your school may limit tokens to 30 days; when it expires, reconnect with a new one.',
|
|
39
37
|
consent: 'I allow this AI assistant to read the Canvas data listed above.',
|
|
@@ -47,23 +45,37 @@ const S = {
|
|
|
47
45
|
'It is already bound to a different Canvas account. Deploy your own copy to use it.',
|
|
48
46
|
client_title: 'Unsupported app',
|
|
49
47
|
client_body: 'Only Claude, ChatGPT and local AI clients can connect to this connector.',
|
|
48
|
+
expired_title: 'This page has expired',
|
|
49
|
+
expired_body:
|
|
50
|
+
'The page was open too long, or was opened again in another tab. Go back and paste your token again.',
|
|
51
|
+
down_title: 'Canvas is not answering right now',
|
|
52
|
+
down_hint:
|
|
53
|
+
'Your token is probably fine: Canvas is slow, down for maintenance, or could not be reached. Wait a few minutes, then go back and try again.',
|
|
50
54
|
footer: 'Open source · read-only · runs on your own Cloudflare account',
|
|
51
55
|
version_label: 'Version',
|
|
52
56
|
not_configured: 'Not configured yet. Run the installer.',
|
|
53
57
|
language: 'Language',
|
|
58
|
+
token_path: 'Account → Settings → + New Access Token',
|
|
59
|
+
step_token: 'Token',
|
|
60
|
+
step_consent: 'Consent',
|
|
61
|
+
step_authorize: 'Authorize',
|
|
62
|
+
next: 'Continue',
|
|
63
|
+
skip: 'Skip',
|
|
64
|
+
token_got: 'Token received',
|
|
65
|
+
thanks_title: 'Thank you',
|
|
66
|
+
thanks_body: 'Connecting your Canvas to your AI…',
|
|
67
|
+
subjects:
|
|
68
|
+
'Calculus|Linear Algebra|Organic Chemistry|Cell Biology|Quantum Mechanics|Macroeconomics|Data Structures|Statistics|Psychology|World History|Philosophy|Literature|Linguistics|Music Theory|Art History|Political Science',
|
|
54
69
|
},
|
|
55
70
|
'zh-CN': {
|
|
56
71
|
product: 'Neuro-CanvasLMS',
|
|
57
72
|
tagline: '让你自己的 AI 助手读懂你的 Canvas。',
|
|
58
|
-
home_lead:
|
|
59
|
-
'在你的 Canvas 账号与 Claude 或 ChatGPT 之间建立一条私人的只读通道。用日常语言询问截止日期、成绩、讲义文件和课程公告。',
|
|
60
73
|
status_bound: '已绑定所有者',
|
|
61
74
|
status_unbound: '还没有连接',
|
|
62
75
|
home_add_title: '添加到你的 AI 助手',
|
|
63
|
-
home_add_body: '在 Claude 或 ChatGPT 中添加自定义连接器,地址填写:',
|
|
64
|
-
home_safe: '只读。不会提交作业、发送消息,也不会修改 Canvas 中的任何内容。',
|
|
65
76
|
canvas_site: 'Canvas 站点',
|
|
66
77
|
auth_title: '连接你的 {school} Canvas',
|
|
78
|
+
auth_title_plain: '连接你的 Canvas',
|
|
67
79
|
auth_lead: '让你自己的 AI 助手读取你在 Canvas 中本来就能看到的内容。',
|
|
68
80
|
read_only: '只读',
|
|
69
81
|
scopes_title: '可读取的内容',
|
|
@@ -73,6 +85,7 @@ const S = {
|
|
|
73
85
|
token_label: 'Canvas 访问令牌',
|
|
74
86
|
token_placeholder: '在此粘贴令牌',
|
|
75
87
|
token_how: '在 Canvas 中创建:账户 → 设置 → 新建访问许可证。请立即复制——Canvas 只显示一次。',
|
|
88
|
+
token_open: '打开 Canvas 设置',
|
|
76
89
|
token_privacy:
|
|
77
90
|
'令牌只会发送到这个由你自己部署的 Worker 和 Canvas,并加密保存在授权记录中。学校可能限制令牌最长 30 天有效,过期后用新令牌重新连接即可。',
|
|
78
91
|
consent: '我允许该 AI 助手读取上面列出的 Canvas 信息。',
|
|
@@ -84,23 +97,35 @@ const S = {
|
|
|
84
97
|
owner_body: '它已绑定到另一个 Canvas 账号。如需使用,请部署你自己的副本。',
|
|
85
98
|
client_title: '不支持的应用',
|
|
86
99
|
client_body: '只有 Claude、ChatGPT 和本地 AI 客户端可以连接此连接器。',
|
|
100
|
+
expired_title: '页面已过期',
|
|
101
|
+
expired_body: '这个页面打开太久了,或者在另一个标签页里又打开过一次。请返回,重新粘贴令牌。',
|
|
102
|
+
down_title: 'Canvas 暂时没有响应',
|
|
103
|
+
down_hint: '令牌多半没有问题:Canvas 现在很慢、正在维护,或者连不上。等几分钟后返回重试。',
|
|
87
104
|
footer: '开源 · 只读 · 运行在你自己的 Cloudflare 账号上',
|
|
88
105
|
version_label: '版本',
|
|
89
106
|
not_configured: '还没配置,请运行安装向导。',
|
|
90
107
|
language: '语言',
|
|
108
|
+
token_path: '账户 → 设置 → 新建访问许可证',
|
|
109
|
+
step_token: '令牌',
|
|
110
|
+
step_consent: '同意',
|
|
111
|
+
step_authorize: '授权',
|
|
112
|
+
next: '继续',
|
|
113
|
+
skip: '跳过',
|
|
114
|
+
token_got: '已收到令牌',
|
|
115
|
+
thanks_title: '谢谢',
|
|
116
|
+
thanks_body: '正在把你的 Canvas 接入你的 AI 助手…',
|
|
117
|
+
subjects:
|
|
118
|
+
'微积分|线性代数|有机化学|细胞生物学|量子力学|宏观经济学|数据结构|统计学|心理学|世界史|哲学|文学|语言学|乐理|艺术史|政治学',
|
|
91
119
|
},
|
|
92
120
|
es: {
|
|
93
121
|
product: 'Neuro-CanvasLMS',
|
|
94
122
|
tagline: 'Tu Canvas, al alcance de tu propio asistente de IA.',
|
|
95
|
-
home_lead:
|
|
96
|
-
'Un puente privado y de solo lectura entre tu cuenta de Canvas y Claude o ChatGPT. Pregunta por fechas de entrega, calificaciones, apuntes y anuncios en lenguaje natural.',
|
|
97
123
|
status_bound: 'Vinculado a su propietario',
|
|
98
124
|
status_unbound: 'Aún no conectado',
|
|
99
125
|
home_add_title: 'Añádelo a tu asistente',
|
|
100
|
-
home_add_body: 'En Claude o ChatGPT, añade un conector personalizado con esta dirección:',
|
|
101
|
-
home_safe: 'Solo lectura. Nunca entrega tareas, envía mensajes ni modifica nada en Canvas.',
|
|
102
126
|
canvas_site: 'Sitio de Canvas',
|
|
103
127
|
auth_title: 'Conecta tu Canvas de {school}',
|
|
128
|
+
auth_title_plain: 'Conecta tu Canvas',
|
|
104
129
|
auth_lead: 'Permite que tu propio asistente de IA lea lo que ya puedes ver en Canvas.',
|
|
105
130
|
read_only: 'Solo lectura',
|
|
106
131
|
scopes_title: 'Qué puede leer',
|
|
@@ -111,6 +136,7 @@ const S = {
|
|
|
111
136
|
token_placeholder: 'Pega tu token aquí',
|
|
112
137
|
token_how:
|
|
113
138
|
'Créalo en Canvas: Cuenta → Configuración → Nuevo token de acceso. Cópialo enseguida: Canvas solo lo muestra una vez.',
|
|
139
|
+
token_open: 'Abrir la configuración de Canvas',
|
|
114
140
|
token_privacy:
|
|
115
141
|
'El token solo se envía a este Worker (que tú desplegaste) y a Canvas, y se guarda cifrado dentro de la autorización. Tu universidad puede limitar los tokens a 30 días; cuando caduque, vuelve a conectar con uno nuevo.',
|
|
116
142
|
consent: 'Permito que este asistente de IA lea los datos de Canvas indicados arriba.',
|
|
@@ -123,24 +149,37 @@ const S = {
|
|
|
123
149
|
owner_body: 'Ya está vinculado a otra cuenta de Canvas. Despliega tu propia copia para usarlo.',
|
|
124
150
|
client_title: 'Aplicación no compatible',
|
|
125
151
|
client_body: 'Solo Claude, ChatGPT y clientes de IA locales pueden conectarse.',
|
|
152
|
+
expired_title: 'La página ha caducado',
|
|
153
|
+
expired_body:
|
|
154
|
+
'La página estuvo abierta demasiado tiempo o se volvió a abrir en otra pestaña. Vuelve y pega tu token de nuevo.',
|
|
155
|
+
down_title: 'Canvas no responde ahora mismo',
|
|
156
|
+
down_hint:
|
|
157
|
+
'Seguramente tu token está bien: Canvas va lento, está en mantenimiento o no se pudo contactar. Espera unos minutos, luego vuelve e inténtalo de nuevo.',
|
|
126
158
|
footer: 'Código abierto · solo lectura · en tu propia cuenta de Cloudflare',
|
|
127
159
|
version_label: 'Versión',
|
|
128
160
|
not_configured: 'Aún no configurado. Ejecuta el instalador.',
|
|
129
161
|
language: 'Idioma',
|
|
162
|
+
token_path: 'Cuenta → Configuración → + Nuevo token de acceso',
|
|
163
|
+
step_token: 'Token',
|
|
164
|
+
step_consent: 'Consentimiento',
|
|
165
|
+
step_authorize: 'Autorizar',
|
|
166
|
+
next: 'Continuar',
|
|
167
|
+
skip: 'Saltar',
|
|
168
|
+
token_got: 'Token recibido',
|
|
169
|
+
thanks_title: 'Gracias',
|
|
170
|
+
thanks_body: 'Conectando tu Canvas con tu IA…',
|
|
171
|
+
subjects:
|
|
172
|
+
'Cálculo|Álgebra lineal|Química orgánica|Biología celular|Mecánica cuántica|Macroeconomía|Estructuras de datos|Estadística|Psicología|Historia universal|Filosofía|Literatura|Lingüística|Teoría musical|Historia del arte|Ciencia política',
|
|
130
173
|
},
|
|
131
174
|
ja: {
|
|
132
175
|
product: 'Neuro-CanvasLMS',
|
|
133
176
|
tagline: 'あなたの Canvas を、あなた自身の AI アシスタントに。',
|
|
134
|
-
home_lead:
|
|
135
|
-
'Canvas アカウントと Claude / ChatGPT をつなぐ、個人用の読み取り専用ブリッジです。締め切り、成績、講義資料、お知らせを普段の言葉で質問できます。',
|
|
136
177
|
status_bound: '所有者と連携済み',
|
|
137
178
|
status_unbound: 'まだ接続されていません',
|
|
138
179
|
home_add_title: 'アシスタントに追加する',
|
|
139
|
-
home_add_body:
|
|
140
|
-
'Claude または ChatGPT で、次のアドレスをカスタムコネクタとして追加してください:',
|
|
141
|
-
home_safe: '読み取り専用です。課題の提出、メッセージ送信、Canvas 上の変更は一切行いません。',
|
|
142
180
|
canvas_site: 'Canvas サイト',
|
|
143
181
|
auth_title: '{school} の Canvas を接続',
|
|
182
|
+
auth_title_plain: 'Canvas を接続',
|
|
144
183
|
auth_lead:
|
|
145
184
|
'Canvas であなたがすでに閲覧できる内容を、あなた自身の AI アシスタントが読めるようにします。',
|
|
146
185
|
read_only: '読み取り専用',
|
|
@@ -152,6 +191,7 @@ const S = {
|
|
|
152
191
|
token_placeholder: 'ここにトークンを貼り付け',
|
|
153
192
|
token_how:
|
|
154
193
|
'Canvas で作成:アカウント → 設定 → 新しいアクセストークン。表示は一度きりなので、すぐにコピーしてください。',
|
|
194
|
+
token_open: 'Canvas の設定を開く',
|
|
155
195
|
token_privacy:
|
|
156
196
|
'トークンはあなたがデプロイしたこの Worker と Canvas にのみ送信され、認可情報の中に暗号化して保存されます。学校によっては有効期限が最長 30 日です。期限切れの際は新しいトークンで再接続してください。',
|
|
157
197
|
consent: '上記の Canvas データをこの AI アシスタントが読み取ることを許可します。',
|
|
@@ -165,24 +205,37 @@ const S = {
|
|
|
165
205
|
'すでに別の Canvas アカウントに紐づいています。利用するにはご自身でデプロイしてください。',
|
|
166
206
|
client_title: '対応していないアプリです',
|
|
167
207
|
client_body: '接続できるのは Claude、ChatGPT、ローカルの AI クライアントのみです。',
|
|
208
|
+
expired_title: 'ページの有効期限が切れました',
|
|
209
|
+
expired_body:
|
|
210
|
+
'ページを長く開いたままにしたか、別のタブで開き直しました。戻って、もう一度トークンを貼り付けてください。',
|
|
211
|
+
down_title: 'Canvas が今は応答していません',
|
|
212
|
+
down_hint:
|
|
213
|
+
'トークンはおそらく問題ありません。Canvas が遅いか、メンテナンス中か、接続できませんでした。数分待ってから、戻ってもう一度お試しください。',
|
|
168
214
|
footer: 'オープンソース · 読み取り専用 · あなたの Cloudflare アカウントで動作',
|
|
169
215
|
version_label: 'バージョン',
|
|
170
216
|
not_configured: '未設定です。インストーラーを実行してください。',
|
|
171
217
|
language: '言語',
|
|
218
|
+
token_path: 'アカウント → 設定 → 新しいアクセストークン',
|
|
219
|
+
step_token: 'トークン',
|
|
220
|
+
step_consent: '同意',
|
|
221
|
+
step_authorize: '認可',
|
|
222
|
+
next: '次へ',
|
|
223
|
+
skip: 'スキップ',
|
|
224
|
+
token_got: 'トークンを受け取りました',
|
|
225
|
+
thanks_title: 'ありがとうございます',
|
|
226
|
+
thanks_body: 'Canvas を AI に接続しています…',
|
|
227
|
+
subjects:
|
|
228
|
+
'微分積分|線形代数|有機化学|細胞生物学|量子力学|マクロ経済学|データ構造|統計学|心理学|世界史|哲学|文学|言語学|音楽理論|美術史|政治学',
|
|
172
229
|
},
|
|
173
230
|
hi: {
|
|
174
231
|
product: 'Neuro-CanvasLMS',
|
|
175
232
|
tagline: 'आपका Canvas, आपके अपने AI सहायक के लिए।',
|
|
176
|
-
home_lead:
|
|
177
|
-
'आपके Canvas खाते और Claude या ChatGPT के बीच एक निजी, केवल-पढ़ने वाला पुल। समय-सीमा, ग्रेड, लेक्चर फ़ाइलों और घोषणाओं के बारे में सामान्य भाषा में पूछें।',
|
|
178
233
|
status_bound: 'अपने स्वामी से जुड़ा हुआ',
|
|
179
234
|
status_unbound: 'अभी तक कनेक्ट नहीं हुआ',
|
|
180
235
|
home_add_title: 'इसे अपने सहायक में जोड़ें',
|
|
181
|
-
home_add_body: 'Claude या ChatGPT में इस पते के साथ एक कस्टम कनेक्टर जोड़ें:',
|
|
182
|
-
home_safe:
|
|
183
|
-
'केवल पढ़ने के लिए। यह कभी असाइनमेंट जमा नहीं करता, संदेश नहीं भेजता और Canvas में कुछ नहीं बदलता।',
|
|
184
236
|
canvas_site: 'Canvas साइट',
|
|
185
237
|
auth_title: 'अपना {school} Canvas जोड़ें',
|
|
238
|
+
auth_title_plain: 'अपना Canvas जोड़ें',
|
|
186
239
|
auth_lead: 'Canvas में जो आप पहले से देख सकते हैं, उसे अपने AI सहायक को पढ़ने दें।',
|
|
187
240
|
read_only: 'केवल पढ़ना',
|
|
188
241
|
scopes_title: 'यह क्या पढ़ सकता है',
|
|
@@ -193,6 +246,7 @@ const S = {
|
|
|
193
246
|
token_placeholder: 'अपना टोकन यहाँ चिपकाएँ',
|
|
194
247
|
token_how:
|
|
195
248
|
'Canvas में बनाएँ: Account → Settings → New Access Token. तुरंत कॉपी करें — Canvas इसे केवल एक बार दिखाता है।',
|
|
249
|
+
token_open: 'Canvas सेटिंग्स खोलें',
|
|
196
250
|
token_privacy:
|
|
197
251
|
'टोकन केवल इस Worker (जिसे आपने डिप्लॉय किया है) और Canvas को भेजा जाता है, फिर प्राधिकरण के अंदर एन्क्रिप्ट करके रखा जाता है। आपका संस्थान टोकन को 30 दिनों तक सीमित कर सकता है; समाप्त होने पर नए टोकन से फिर से जोड़ें।',
|
|
198
252
|
consent: 'मैं इस AI सहायक को ऊपर बताए गए Canvas डेटा को पढ़ने की अनुमति देता/देती हूँ।',
|
|
@@ -206,10 +260,27 @@ const S = {
|
|
|
206
260
|
'यह पहले से किसी दूसरे Canvas खाते से जुड़ा है। इसे इस्तेमाल करने के लिए अपनी खुद की कॉपी डिप्लॉय करें।',
|
|
207
261
|
client_title: 'असमर्थित ऐप',
|
|
208
262
|
client_body: 'केवल Claude, ChatGPT और लोकल AI क्लाइंट ही इस कनेक्टर से जुड़ सकते हैं।',
|
|
263
|
+
expired_title: 'पेज की अवधि समाप्त हो गई',
|
|
264
|
+
expired_body:
|
|
265
|
+
'यह पेज बहुत देर तक खुला रहा, या किसी दूसरे टैब में फिर से खोला गया। वापस जाएँ और अपना टोकन फिर से चिपकाएँ।',
|
|
266
|
+
down_title: 'Canvas अभी जवाब नहीं दे रहा',
|
|
267
|
+
down_hint:
|
|
268
|
+
'आपका टोकन शायद ठीक है: Canvas धीमा है, रखरखाव में है, या उससे संपर्क नहीं हो सका। कुछ मिनट रुकें, फिर वापस जाकर दोबारा कोशिश करें।',
|
|
209
269
|
footer: 'ओपन सोर्स · केवल पढ़ना · आपके अपने Cloudflare खाते पर चलता है',
|
|
210
270
|
version_label: 'संस्करण',
|
|
211
271
|
not_configured: 'अभी कॉन्फ़िगर नहीं है। इंस्टॉलर चलाएँ।',
|
|
212
272
|
language: 'भाषा',
|
|
273
|
+
token_path: 'Account → Settings → + New Access Token',
|
|
274
|
+
step_token: 'टोकन',
|
|
275
|
+
step_consent: 'सहमति',
|
|
276
|
+
step_authorize: 'अनुमति',
|
|
277
|
+
next: 'आगे बढ़ें',
|
|
278
|
+
skip: 'छोड़ें',
|
|
279
|
+
token_got: 'टोकन मिल गया',
|
|
280
|
+
thanks_title: 'धन्यवाद',
|
|
281
|
+
thanks_body: 'आपका Canvas आपके AI से जोड़ा जा रहा है…',
|
|
282
|
+
subjects:
|
|
283
|
+
'कलन|रैखिक बीजगणित|कार्बनिक रसायन|कोशिका जीवविज्ञान|क्वांटम यांत्रिकी|समष्टि अर्थशास्त्र|डेटा संरचनाएँ|सांख्यिकी|मनोविज्ञान|विश्व इतिहास|दर्शनशास्त्र|साहित्य|भाषाविज्ञान|संगीत सिद्धांत|कला इतिहास|राजनीति विज्ञान',
|
|
213
284
|
},
|
|
214
285
|
};
|
|
215
286
|
|