Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ type ConfigEnvKeys =
| 'ZSXQ_ACCESS_TOKEN'
| 'SMZDM_COOKIE'
| 'REMOTE_CONFIG'
| 'REMOTE_CONFIG_AUTH';
| 'REMOTE_CONFIG_AUTH'
| 'JAAuthCookie';
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Environment variable names in this codebase follow SCREAMING_SNAKE_CASE convention (e.g., 'NCM_COOKIES', 'SKEB_BEARER_TOKEN', 'SPOTIFY_CLIENT_ID'). The 'JAAuthCookie' variable uses mixed camelCase which is inconsistent. It should be 'SJTU_JA_AUTH_COOKIE' or 'JA_AUTH_COOKIE' to match the established pattern. This affects both the ConfigEnvKeys type and the usage in calculateValue.

Copilot uses AI. Check for mistakes.

export type ConfigEnv = Partial<Record<ConfigEnvKeys, string | undefined>>;

Expand Down Expand Up @@ -570,6 +571,9 @@ export type Config = {
sis001: {
baseUrl?: string;
};
sjtu: {
JAAuthCookie?: string;
};
skeb: {
bearerToken?: string;
};
Expand Down Expand Up @@ -1048,6 +1052,9 @@ const calculateValue = () => {
sis001: {
baseUrl: envs.SIS001_BASE_URL || 'https://sis001.com',
},
sjtu: {
JAAuthCookie: envs.JAAuthCookie,
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The property name should use consistent casing with other config properties. Looking at similar authentication configs in this codebase (e.g., 'bearerToken' at line 1059, 'bearertoken' at line 1070, 'accessToken' at line 984), authentication tokens typically use camelCase. 'JAAuthCookie' should be 'jaAuthCookie' to follow this pattern.

Copilot uses AI. Check for mistakes.
},
skeb: {
bearerToken: envs.SKEB_BEARER_TOKEN,
},
Expand Down
95 changes: 95 additions & 0 deletions lib/routes/sjtu/publicoa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { CookieJar } from 'tough-cookie';

import { config } from '@/config';
import type { Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
import ConfigNotFoundError from '@/errors/types/config-not-found';

const urlRoot = 'https://publicoa.sjtu.edu.cn';

export const route: Route = {
path: '/publicoa',
categories: ['university'],
example: '/sjtu/publicoa',
parameters: {},
features: {
requireConfig: [
{
name: 'JAAuthCookie',
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The requireConfig 'name' field should specify the environment variable name, not the config object property name. Based on established patterns (e.g., lib/routes/smzdm/ranking.ts:209 uses 'SMZDM_COOKIE' while accessing 'config.smzdm.cookie'), this should be 'SJTU_JA_AUTH_COOKIE' or similar, matching the environment variable convention. The current 'JAAuthCookie' doesn't follow the SCREAMING_SNAKE_CASE pattern used for environment variables.

Copilot uses AI. Check for mistakes.
description: 'JAAuthCookie, 登陆后提取自jaccount.sjtu.edu.cn',
},
],
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '上海交通大学公文系统',
maintainers: ['dzx-dzx'],
handler,
description: `需要用户认证`,
};

const cookieJar = new CookieJar();

async function handler() {
Comment on lines +36 to +38
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global cookieJar instance may cause issues in concurrent requests since it's shared across all handler invocations. Consider creating a new CookieJar instance inside the handler function, similar to the pattern used in lib/routes/gdut/oa-news.ts (line 98) where new CookieJar() is created within the handler function scope.

Suggested change
const cookieJar = new CookieJar();
async function handler() {
async function handler() {
const cookieJar = new CookieJar();

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +38
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module-level cookieJar should be avoided as it creates a shared state between requests in a concurrent environment. Each request handler should create its own cookieJar instance to avoid cookie leakage between different users or concurrent requests. Move the cookieJar instantiation into the handler function.

Suggested change
const cookieJar = new CookieJar();
async function handler() {
async function handler() {
const cookieJar = new CookieJar();

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handler functions in RSSHub routes conventionally accept a 'ctx' parameter containing request context, even if unused. This is seen in lib/routes/sjtu/gs.ts:49 and lib/routes/sjtu/jwc.ts:55. While not strictly necessary when no parameters are needed, omitting it breaks the established pattern and could cause issues if the route metadata is extended to include parameters later.

Copilot uses AI. Check for mistakes.
if (!config.sjtu?.JAAuthCookie) {
throw new ConfigNotFoundError('JAAuthCookie needs to be set to use this route.');
}

cookieJar.setCookieSync(`JAAuthCookie=${config.sjtu.JAAuthCookie}; Domain=.jaccount.sjtu.edu.cn; Path=/`, 'https://jaccount.sjtu.edu.cn');
async function getPublicOAList() {
return await ofetch(`${urlRoot}/api/doc/list`, {
headers: {
cookie: (await cookieJar.getCookieString(urlRoot)) as string,
},
});
}
const list: any = await new Promise((resolve) => {
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type annotation 'any' is too permissive and defeats TypeScript's type checking benefits. Based on the usage, this should have a proper type that includes an 'entities' array property with items that have 'title', 'doccode', 'qfdate', and 'pdfpath' fields. Define an interface or type for the API response structure.

Copilot uses AI. Check for mistakes.
resolve(
getPublicOAList().catch(async (error) => {

Check warning

Code scanning / oxlint

github(no-then) Warning

Prefer async/await to Promise.catch()
if (error.response?.status === 401) {
let requestUrl = urlRoot;
while (true) {
// eslint-disable-next-line no-await-in-loop
const res = await ofetch.raw(requestUrl, {
headers: {
// eslint-disable-next-line no-await-in-loop
cookie: (await cookieJar.getCookieString(requestUrl)) as string,

Check failure

Code scanning / ESLint

Disallow `await` inside of loops

Unexpected `await` inside a loop.
},
redirect: 'manual',
});
Comment on lines +58 to +64

Check failure

Code scanning / ESLint

Disallow `await` inside of loops

Unexpected `await` inside a loop.
const setCookies = res.headers.getSetCookie();
for (const c of setCookies) {
cookieJar.setCookieSync(c, requestUrl);
}

if (res.status >= 300 && res.status < 400) {
const location = res.headers.get('location');
if (typeof location === 'string') {
requestUrl = new URL(location, requestUrl).href;
}
} else {
break;
}
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error response handling in this redirect loop doesn't handle cases where the authentication flow fails completely or where redirects might be malicious. The loop should include a maximum iteration count to prevent infinite loops and should validate redirect URLs before following them.

Copilot uses AI. Check for mistakes.
return await getPublicOAList();
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the initial API request at line 44 doesn't return a 401 error but fails in some other way (network error, timeout, 500 error, etc.), the promise will resolve with undefined. This will cause a runtime error at line 86 when trying to access list.entities. The error handling should be expanded to handle all failure cases, not just 401 authentication errors.

Suggested change
}
}
throw error;

Copilot uses AI. Check for mistakes.
})
);
});

return {
title: '上海交通大学公文系统',
item: list.entities.map((item) => ({
title: item.title,
author: item.doccode,
pubDate: timezone(parseDate(item.qfdate), +8),
link: item.pdfpath,
})),
link: urlRoot,
};
}
Loading