linke-sdufe 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linke-sdufe",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "山东财经大学强智教务(jw.sdufe.edu.cn)适配器共享包:登录、教务查询与页面解析。linke-cli 与 Linke App 同源引用;教务请求一律从用户设备直发。",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/adapter.js CHANGED
@@ -45,10 +45,52 @@ const REQUEST_TIMEOUT_MS = 20000
45
45
  * 翻页:listPath=栏目首页,page N → pagePath/{N-1}.htm。
46
46
  * 学工部通知栏目实测=info/1093(数据源地图旧记 1090 实测 404)。
47
47
  */
48
+ /**
49
+ * 校园官网公开通知源注册表(T46 起 36 源;数据源地图 v4 全站探测 +
50
+ * 2026-09-04 执行者逐站实勘:列表/栏目/翻页/详情容器逐站 curl 实证)。
51
+ * 翻页:listPath 去掉 .htm 即 pagerPath(page N → {pagerPath}/{N-1}.htm);
52
+ * pagePath 为空=该源翻页形态特殊未实证(如 jinrong),page>1 明确报错。
53
+ * infoPaths:列表条目栏目过滤(防导航混入);详情白名单按 origin(不限栏目)。
54
+ * 不接:zgjjyjy(无通知栏目)、xcb(内容发新闻网)、tuanwei/grads 等
55
+ * 防护壳与登录系统(数据源地图标注排除)。
56
+ */
48
57
  export const PORTAL_NOTICE_SOURCES = {
49
- jwc: { name: '教务处官网', origin: 'https://jwc.sdufe.edu.cn', listPath: '/zxdt/tzgg.htm', pagePath: '/zxdt/tzgg', infoPath: '/info/1043/' },
50
- main: { name: '学校主站', origin: 'https://www.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPath: '/info/1068/' },
51
- xgb: { name: '学工部官网', origin: 'https://student.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPath: '/info/1093/' },
58
+ jwc: { name: '教务处官网', origin: 'https://jwc.sdufe.edu.cn', listPath: '/zxdt/tzgg.htm', pagePath: '/zxdt/tzgg', infoPaths: ['1043'] },
59
+ main: { name: '学校主站', origin: 'https://www.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1068'] },
60
+ xgb: { name: '学工部官网', origin: 'https://student.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1093'] },
61
+ archives: { name: '档案馆', origin: 'https://archives.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1053'] },
62
+ bcf: { name: '区块链金融实验室', origin: 'https://bcf.sdufe.edu.cn', listPath: '/xwzx/zhxw.htm', pagePath: '/xwzx/zhxw', infoPaths: ['1072'] },
63
+ cs: { name: '计算机学院', origin: 'https://cs.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1014'] },
64
+ gkgc: { name: '管科学院', origin: 'https://gkgc.sdufe.edu.cn', listPath: '/tzgg.htm', pagePath: '/tzgg', infoPaths: ['1099'] },
65
+ gonghui: { name: '工会', origin: 'https://gonghui.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1061'] },
66
+ gsglxy: { name: '工商管理学院', origin: 'https://gsglxy.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1193'] },
67
+ hf: { name: '合作发展', origin: 'https://hf.sdufe.edu.cn', listPath: '/fxljy1/tzgg.htm', pagePath: '/fxljy1/tzgg', infoPaths: ['1194'] },
68
+ insurance: { name: '保险学院', origin: 'https://insurance.sdufe.edu.cn', listPath: '/tzgg.htm', pagePath: '/tzgg', infoPaths: ['1059', '1152'] },
69
+ jinrong: { name: '金融学院', origin: 'https://jinrong.sdufe.edu.cn', listPath: '/tzgg.htm', pagePath: '', infoPaths: ['1043', '1160'] },
70
+ jiwei: { name: '纪委', origin: 'https://jiwei.sdufe.edu.cn', listPath: '/tzgg.htm', pagePath: '/tzgg', infoPaths: ['1080'] },
71
+ jrfzyjy: { name: '金融发展研究院', origin: 'https://jrfzyjy.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1013'] },
72
+ jsgzb: { name: '教师工作部', origin: 'https://jsgzb.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1127'] },
73
+ kyc: { name: '科研处', origin: 'https://kyc.sdufe.edu.cn', listPath: '/tzgg.htm', pagePath: '/tzgg', infoPaths: ['1077'] },
74
+ law: { name: '法学院', origin: 'https://law.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1287'] },
75
+ lib: { name: '图书馆', origin: 'https://lib.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1094'] },
76
+ mba: { name: 'MBA学院', origin: 'https://mba.sdufe.edu.cn', listPath: '/tzgg.htm', pagePath: '/tzgg', infoPaths: ['1372', '1376'] },
77
+ mpa: { name: 'MPA中心', origin: 'https://mpa.sdufe.edu.cn', listPath: '/xwtz/tzgg.htm', pagePath: '/xwtz/tzgg', infoPaths: ['1027'] },
78
+ news: { name: '新闻网', origin: 'https://news.sdufe.edu.cn', listPath: '/cdyw.htm', pagePath: '/cdyw', infoPaths: ['1071'] },
79
+ rcb: { name: '人才办', origin: 'https://rcb.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1023'] },
80
+ rsc: { name: '人事处', origin: 'https://rsc.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1047'] },
81
+ sam: { name: '统数学院', origin: 'https://sam.sdufe.edu.cn', listPath: '/tzgg.htm', pagePath: '/tzgg', infoPaths: ['1012', '1051'] },
82
+ sce: { name: '继续教育学院', origin: 'https://sce.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1124', '1125', '1164', '1282', '2241'] },
83
+ sie: { name: '国际教育学院', origin: 'https://sie.sdufe.edu.cn', listPath: '/index/tzgg2.htm', pagePath: '/index/tzgg2', infoPaths: ['1063'] },
84
+ site: { name: '国贸学院', origin: 'https://site.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1009', '1063', '1064'] },
85
+ spft: { name: '财税学院', origin: 'https://spft.sdufe.edu.cn', listPath: '/tzgg1.htm', pagePath: '/tzgg1', infoPaths: ['1010'] },
86
+ spm: { name: '公共管理学院', origin: 'https://spm.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1324'] },
87
+ tzb: { name: '统战部', origin: 'https://tzb.sdufe.edu.cn', listPath: '/tzgg.htm', pagePath: '/tzgg', infoPaths: ['1031'] },
88
+ xkjsbgs: { name: '学科办', origin: 'https://xkjsbgs.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1106'] },
89
+ xxgk: { name: '信息公开', origin: 'https://xxgk.sdufe.edu.cn', listPath: '/ejlm/xwyxk/xwsybfhsyjd.htm', pagePath: '/ejlm/xwyxk/xwsybfhsyjd', infoPaths: ['1115'] },
90
+ yjsy: { name: '研究生院', origin: 'https://yjsy.sdufe.edu.cn', listPath: '/index/tzgg1.htm', pagePath: '/index/tzgg1', infoPaths: ['1027', '1030', '1031', '1032', '1043', '1058'] },
91
+ ys: { name: '燕山学院', origin: 'https://ys.sdufe.edu.cn', listPath: '/xwtz/zhxw.htm', pagePath: '/xwtz/zhxw', infoPaths: ['1018', '1053'] },
92
+ zfjxpj: { name: '政府绩效评价', origin: 'https://zfjxpj.sdufe.edu.cn', listPath: '/zxdt/tzgg.htm', pagePath: '/zxdt/tzgg', infoPaths: ['1010', '1015', '1018', '1037'] },
93
+ zzb: { name: '组织部', origin: 'https://zzb.sdufe.edu.cn', listPath: '/index/tzgg.htm', pagePath: '/index/tzgg', infoPaths: ['1046', '1076'] },
52
94
  }
53
95
 
54
96
  /** 课程属性中文名 → zzdKcSX 表单码(强智 kbxx_kc_ifr 口径) */
@@ -407,17 +449,24 @@ export function createSdufeAdapter(env) {
407
449
  },
408
450
 
409
451
  /**
410
- * 校园官网公开通知三大主源列表(T46;博达 CMS 同构一套解析)。
411
- * site PORTAL_NOTICE_SOURCES:jwc 教务处 / main 主站 / xgb 学工部。
412
- * 翻页口径(逐源实证):page 1=栏目首页 tzgg.htm,page N= tzgg/{N-1}.htm
452
+ * 校园官网公开通知源列表(T46 三源起步,T46 扩展 36 源;博达 CMS 同构
453
+ * 一套解析,逐站实勘配置见 PORTAL_NOTICE_SOURCES)。
454
+ * 翻页口径:page 1=listPath,page N={pagePath}/{N-1}.htm
455
+ * pagePath 为空=该源翻页未实证(page>1 明确报错,不静默回首页)。
413
456
  * 礼貌纪律:只抓请求的页,不做归档批量预取。
414
457
  */
415
458
  async fetchPortalNotices(site, page = 1) {
416
459
  const src = PORTAL_NOTICE_SOURCES[site]
417
460
  if (!src) {
418
- throw new LinkeError('BAD_SOURCE', `未知通知源:${site}(限 ${Object.keys(PORTAL_NOTICE_SOURCES).join('/')})`, {
461
+ throw new LinkeError('BAD_SOURCE', `未知通知源:${site}`, {
462
+ exitCode: EXIT.GENERAL,
463
+ hint: 'source 取源注册表 key(jwc=教务处 / main=主站 / xgb=学工部 / lib=图书馆 / yjsy=研究生院 / news=新闻网 / 各学院与职能部门拼音缩写)',
464
+ })
465
+ }
466
+ if (page > 1 && !src.pagePath) {
467
+ throw new LinkeError('NO_PAGER', `${src.name}暂不支持翻页(该站翻页形态特殊未实证)`, {
419
468
  exitCode: EXIT.GENERAL,
420
- hint: 'source 取 jwc=教务处 / main=学校主站 / xgb=学工部',
469
+ hint: '本源仅可查第一页(page=1)',
421
470
  })
422
471
  }
423
472
  const path = page <= 1 ? src.listPath : `${src.pagePath}/${page - 1}.htm`
@@ -437,11 +486,11 @@ export function createSdufeAdapter(env) {
437
486
  // 栏目过滤:列表页含导航/侧栏的 info 链接(如主站「学校概况」),
438
487
  // 只保留本源通知栏目条目
439
488
  return parsePortalNotices(await response.text(), src.origin)
440
- .filter((x) => !src.infoPath || x.url.includes(src.infoPath))
489
+ .filter((x) => !src.infoPaths || !src.infoPaths.length || src.infoPaths.some((c) => x.url.includes(`/info/${c}/`)))
441
490
  },
442
491
 
443
492
  /**
444
- * 校园官网公开通知详情全文(T46)。url 限三源域名(白名单防抓任意站);
493
+ * 校园官网公开通知详情全文(T46)。url 限源注册表内域名(白名单防抓任意站);
445
494
  * 礼貌纪律:只抓用户点名的这一篇,不做批量预取。
446
495
  */
447
496
  async fetchPortalNoticeDetail(url) {
@@ -456,9 +505,9 @@ export function createSdufeAdapter(env) {
456
505
  }
457
506
  const src = Object.values(PORTAL_NOTICE_SOURCES).find((s) => s.origin === parsed.origin)
458
507
  if (!src || !/^\/info\/\d+\/\d+\.htm$/.test(parsed.pathname)) {
459
- throw new LinkeError('BAD_URL', `URL 不在校内通知三源范围:${url}`, {
508
+ throw new LinkeError('BAD_URL', `URL 不在校内通知源范围:${url}`, {
460
509
  exitCode: EXIT.GENERAL,
461
- hint: `仅支持 ${Object.values(PORTAL_NOTICE_SOURCES).map((s) => s.origin).join(' / ')} 的 info/{栏目}/{id}.htm 详情页`,
510
+ hint: `仅支持通知源注册表内各站(jwc/main/xgb/lib/yjsy/news/各学院等)的 info/{栏目}/{id}.htm 详情页`,
462
511
  })
463
512
  }
464
513
  let response
package/src/parsers.js CHANGED
@@ -876,25 +876,38 @@ function htmlToText(html) {
876
876
  export function parsePortalNotices(html, baseUrl = '') {
877
877
  const list = []
878
878
  const seen = new Set()
879
- const linkRe = /<a\s+href="([^"]*\/info\/\d+\/\d+\.htm)"[^>]*>([^<]+)<\/a>/g
879
+ // 兼容条目形态(逐站实勘判例):
880
+ // - href 属性位不定(news/xxgk:<a target="_blank" href="info/…">)
881
+ // - href 三种相对形态:../info/{栏}/{id}.htm、/info/…、info/…(无前导斜杠)
882
+ // - a 内可嵌日期 span(lib/yjsy/news/cs 等新模板:<a>标题<span class="spans">日期</span></a>)
883
+ const linkRe = /<a\s[^>]*?href="((?:[^"]*?\/)?info\/\d+\/\d+\.htm)"[^>]*>([\s\S]*?)<\/a>/g
880
884
  let m
881
885
  while ((m = linkRe.exec(html))) {
882
886
  let url = m[1]
883
887
  if (!/^https?:/i.test(url)) {
884
- // "../info/x" 相对列表页目录 → 站点根 /info/x(博达栏目均为根级 info/)
885
- url = baseUrl.replace(/\/$/, '') + '/' + url.replace(/^(\.\.\/)+/, '')
888
+ // 相对列表页目录 → 站点根 /info/x(博达栏目均为根级 info/)
889
+ url = baseUrl.replace(/\/$/, '') + '/' + url.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
886
890
  }
887
891
  if (seen.has(url)) continue
888
892
  seen.add(url)
893
+ const inner = decodeEntities(m[2].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim())
894
+ // yjsy/cs 等新模板把日期放 a 内文本开头 → 剥前缀日期当 date
895
+ let title = inner
896
+ let date = ''
897
+ const inline = inner.match(/^(\d{4}-\d{2}-\d{2})\s*(.*)$/)
898
+ const inlineCn = inner.match(/^(\d{4}年\d{1,2}月\d{1,2}日)\s*(.*)$/)
899
+ if (inline) { date = inline[1]; title = inline[2] || inner }
900
+ else if (inlineCn) { date = inlineCn[1]; title = inlineCn[2] || inner }
889
901
  const after = html.slice(m.index + m[0].length, m.index + m[0].length + 600)
890
902
  const before = html.slice(Math.max(0, m.index - 600), m.index)
891
- let date = (after.match(/\d{4}-\d{2}-\d{2}/) || [])[0] || ''
903
+ // 日期其余形态:a span 徽章 a 后窗口 → a 前 day+month 分元素(学工部)
904
+ if (!date) date = (m[2].match(/\d{4}-\d{2}-\d{2}/) || [])[0] || ''
905
+ if (!date) date = (after.match(/\d{4}-\d{2}-\d{2}/) || [])[0] || ''
892
906
  if (!date) {
893
- // 学工部形态:<p class="day">13</p><p class="month">2026-07</p>
894
907
  const dm = before.match(/class="day">\s*(\d{1,2})\s*<\/p>\s*<p class="month">\s*(\d{4}-\d{2})/)
895
908
  if (dm) date = `${dm[2]}-${dm[1].padStart(2, '0')}`
896
909
  }
897
- list.push({ title: decodeEntities(m[2].trim()), url, date })
910
+ list.push({ title, url, date })
898
911
  }
899
912
  return list
900
913
  }