linke-sdufe 0.1.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.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 学校适配器注册表。核心命令框架只依赖本文件暴露的接口,
3
+ * 不 import 任何学校特有模块——新增学校 = 新增包内 src/<id>/ 目录
4
+ * 并在此注册(T3 验收 7:多学校扩展地基)。
5
+ *
6
+ * 适配器是环境注入产物(createSdufeAdapter(env)),因此注册表不预置
7
+ * 实例:宿主启动时显式 initSdufe(env)(CLI 用 nodeEnv(),uni-app 用
8
+ * uni.request 垫片 env)。getAdapter 在未初始化时给出明确指引。
9
+ */
10
+ import { createSdufeAdapter } from './adapter.js'
11
+ import { nodeEnv } from './env.js'
12
+
13
+ const adapters = new Map()
14
+
15
+ /** 创建 sdufe 适配器(默认 nodeEnv)并注册;幂等(重复调用覆盖同 id) */
16
+ export function initSdufe(env = nodeEnv()) {
17
+ const adapter = createSdufeAdapter(env)
18
+ adapters.set(adapter.id, adapter)
19
+ return adapter
20
+ }
21
+
22
+ /**
23
+ * 适配器接口契约(核心框架只调用这些成员):
24
+ * id, name, baseUrl
25
+ * login({ userId, password, recognizeCaptcha }, { maxRetries, onProgress })
26
+ * → { cookie, userInfo }
27
+ * probeSession(cookie) → userInfo(过期抛 err.isJwLoginExpired)
28
+ * fetchCurrentTerm(cookie) → string | null
29
+ * fetchSchedule(cookie, { term, week }) → { weeks, remark? }
30
+ * fetchScores(cookie, { term }) → rows[]
31
+ */
32
+ export function getAdapter(schoolId) {
33
+ const adapter = adapters.get(schoolId)
34
+ if (!adapter) {
35
+ throw new Error(
36
+ `适配器 ${schoolId} 未初始化或未知(当前已注册: ${Array.from(adapters.keys()).join(', ') || '无'})。` +
37
+ `宿主启动时需先 initSdufe(env)——Node 宿主可省参(默认 nodeEnv),uni-app 宿主需传 uni.request 垫片 env`
38
+ )
39
+ }
40
+ return adapter
41
+ }
42
+
43
+ /** 注册适配器(新学校接入入口;测试也用它注入 fake 适配器) */
44
+ export function registerAdapter(adapter) {
45
+ if (!adapter || !adapter.id) throw new Error('适配器缺少 id')
46
+ adapters.set(adapter.id, adapter)
47
+ }
48
+
49
+ export function listAdapters() {
50
+ return Array.from(adapters.values()).map((a) => ({ id: a.id, name: a.name, baseUrl: a.baseUrl }))
51
+ }
package/src/util.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 教务 HTML 预处理与 cookie 提取(环境无关纯函数)。
3
+ * 口径与 App 端 linke_App/repositories/jwHttp.js、utils/jwLoginExpired.js 保持同源。
4
+ */
5
+
6
+ /**
7
+ * 从响应对象提取 "k=v; k2=v2" 形式的 Cookie 头。
8
+ * 接受带 getSetCookie() 的标准 fetch Response(Node >= 18.14),
9
+ * 也接受宿主垫片构造的同形对象(uni.request 等)。
10
+ */
11
+ export function extractCookieHeader(response) {
12
+ const headers = response && response.headers ? response.headers : {}
13
+ const list = headers.getSetCookie ? headers.getSetCookie() : []
14
+ const pairs = []
15
+ for (const raw of list) {
16
+ const pair = String(raw).split(';')[0]
17
+ if (pair) pairs.push(pair.trim())
18
+ }
19
+ return pairs.join('; ')
20
+ }
21
+
22
+ /**
23
+ * 教务 HTML 预处理:删除空格(含全角)与换行。
24
+ * PHP 端与 App 端解析前都做同一处理,正则依赖此口径(<td align= 会变 <tdalign=)。
25
+ */
26
+ export function stripSpaces(html) {
27
+ return String(html ?? '').replace(/[ \u3000\t\n\r]/g, '')
28
+ }
29
+
30
+ /**
31
+ * 教务「登录过期」判定。
32
+ * 判定序(T12 修订:短页阈值曾误伤 cjcx_avg 这类 <5KB 小业务页):
33
+ * 1. 登录页表单特征(RANDOMCODE + 账号/密码输入框)——最准;
34
+ * 2. 「用户登录 / 请先登录」文案;
35
+ * 3. 短 html + </html> 且不含 <table>——登录页/错误页兜底
36
+ * (业务页必有数据表,小页面如 gpa 页含表即放行)。
37
+ */
38
+ export function isJwLoginExpired(html) {
39
+ if (!html || typeof html !== 'string') return false
40
+ const trimmed = html.trim()
41
+ if (
42
+ trimmed.includes('RANDOMCODE') &&
43
+ (trimmed.includes('userAccount') || trimmed.includes('userPassword'))
44
+ ) {
45
+ return true
46
+ }
47
+ if (trimmed.includes('用户登录') || trimmed.includes('请先登录')) return true
48
+ if (trimmed.includes('</html>') && trimmed.length < 5000 && !/<table[\s>]/i.test(trimmed)) {
49
+ return true
50
+ }
51
+ return false
52
+ }