gaokao-pro 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.
Files changed (66) hide show
  1. package/README.md +109 -0
  2. package/data/datasets/gaoshui-yundongdui-2024.json +33 -0
  3. package/data/datasets/junjing-jingxiao.json +75 -0
  4. package/data/datasets/provinces-specialty-2024.json +139 -0
  5. package/data/datasets/qiangji-2024.json +52 -0
  6. package/data/datasets/schools-adapters-2024.json +123 -0
  7. package/data/datasets/sushe-shitang.json +40 -0
  8. package/data/datasets/tijian-shouxian-zhuanye.json +29 -0
  9. package/data/datasets/xiaoyuzhong-zhaosheng.json +18 -0
  10. package/data/datasets/xueke-pinggu-disculun.json +28 -0
  11. package/data/datasets/zhongwai-hezuo-2024.json +36 -0
  12. package/data/datasets/zonghepingjia-2024.json +53 -0
  13. package/data/school-index.json.gz +0 -0
  14. package/data/yifenyiduan/_ocr-pipeline/extract-pdf.py +117 -0
  15. package/data/yifenyiduan/beijing-2023-combined.json +1 -0
  16. package/data/yifenyiduan/beijing-2024-combined.json +1 -0
  17. package/data/yifenyiduan/beijing-2025-combined.json +1 -0
  18. package/data/yifenyiduan/henan-2024-liberal.json +1 -0
  19. package/data/yifenyiduan/hunan-2024-history.json +1 -0
  20. package/dist/aliases.d.ts +2 -0
  21. package/dist/aliases.js +120 -0
  22. package/dist/chart-check.d.ts +20 -0
  23. package/dist/chart-check.js +99 -0
  24. package/dist/codes.d.ts +162 -0
  25. package/dist/codes.js +59 -0
  26. package/dist/compare.d.ts +39 -0
  27. package/dist/compare.js +112 -0
  28. package/dist/datasets.d.ts +65 -0
  29. package/dist/datasets.js +82 -0
  30. package/dist/find.d.ts +48 -0
  31. package/dist/find.js +87 -0
  32. package/dist/format.d.ts +17 -0
  33. package/dist/format.js +109 -0
  34. package/dist/gaokao-cn.d.ts +122 -0
  35. package/dist/gaokao-cn.js +49 -0
  36. package/dist/index-loader.d.ts +33 -0
  37. package/dist/index-loader.js +59 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +727 -0
  40. package/dist/match.d.ts +40 -0
  41. package/dist/match.js +118 -0
  42. package/dist/mcp.d.ts +1 -0
  43. package/dist/mcp.js +535 -0
  44. package/dist/memory.d.ts +31 -0
  45. package/dist/memory.js +69 -0
  46. package/dist/paiming.d.ts +29 -0
  47. package/dist/paiming.js +66 -0
  48. package/dist/probe.d.ts +1 -0
  49. package/dist/probe.js +73 -0
  50. package/dist/provinces/guangdong.d.ts +27 -0
  51. package/dist/provinces/guangdong.js +68 -0
  52. package/dist/provinces/index.d.ts +1 -0
  53. package/dist/provinces/index.js +17 -0
  54. package/dist/rank-table.d.ts +31 -0
  55. package/dist/rank-table.js +74 -0
  56. package/dist/recommend-major.d.ts +34 -0
  57. package/dist/recommend-major.js +119 -0
  58. package/dist/recommend.d.ts +54 -0
  59. package/dist/recommend.js +147 -0
  60. package/dist/selftest.d.ts +11 -0
  61. package/dist/selftest.js +48 -0
  62. package/dist/top.d.ts +29 -0
  63. package/dist/top.js +66 -0
  64. package/dist/xuanke.d.ts +8 -0
  65. package/dist/xuanke.js +35 -0
  66. package/package.json +40 -0
package/dist/format.js ADDED
@@ -0,0 +1,109 @@
1
+ export function isTty() {
2
+ return Boolean(process.stdout.isTTY);
3
+ }
4
+ function pad(s, w) {
5
+ // 中文 char ≈ 2 columns, ASCII ≈ 1. Width-aware padding.
6
+ let used = 0;
7
+ for (const ch of s) {
8
+ used += ch.charCodeAt(0) > 127 ? 2 : 1;
9
+ }
10
+ return s + " ".repeat(Math.max(0, w - used));
11
+ }
12
+ function colW(rows, col) {
13
+ let w = 0;
14
+ for (const r of rows) {
15
+ const cell = r[col] ?? "";
16
+ let used = 0;
17
+ for (const ch of cell)
18
+ used += ch.charCodeAt(0) > 127 ? 2 : 1;
19
+ if (used > w)
20
+ w = used;
21
+ }
22
+ return w;
23
+ }
24
+ export function renderTable(rows, opts = {}) {
25
+ const gap = opts.gap ?? 2;
26
+ if (rows.length === 0)
27
+ return "(empty)";
28
+ const cols = Math.max(...rows.map((r) => r.length));
29
+ const widths = Array.from({ length: cols }, (_, c) => colW(rows, c));
30
+ const sep = " ".repeat(gap);
31
+ return rows
32
+ .map((r) => r.map((cell, i) => (i === cols - 1 ? cell ?? "" : pad(cell ?? "", widths[i]))).join(sep))
33
+ .join("\n");
34
+ }
35
+ function labelTags(c) {
36
+ const tags = [];
37
+ if (c.is985)
38
+ tags.push("985");
39
+ if (c.is211 && !c.is985)
40
+ tags.push("211");
41
+ if (c.dualClass === "双一流" && !c.is985 && !c.is211)
42
+ tags.push("双一流");
43
+ return tags.join(" ");
44
+ }
45
+ export function formatRecommend(out, opts = {}) {
46
+ const lines = [];
47
+ const q = out.query;
48
+ lines.push(`gaokao-pro recommend score=${q.score} ${q.province.name}(${q.province.reform}) ` +
49
+ `subjects=${q.subjects.join("/")} track=${q.trackName} ` +
50
+ (q.rank !== undefined ? `rank=${q.rank} ` : "") +
51
+ `evaluated=${out.evaluated} skipped=${out.buckets.skipped}`);
52
+ const order = [
53
+ { key: "冲", label: "冲 REACH" },
54
+ { key: "稳", label: "稳 MATCH" },
55
+ { key: "保", label: "保 SAFETY" },
56
+ { key: "out", label: "— out of range" }
57
+ ];
58
+ for (const { key, label } of order) {
59
+ const items = out.buckets[key];
60
+ if (items.length === 0)
61
+ continue;
62
+ lines.push("");
63
+ lines.push(`[${label}] ${items.length} school${items.length === 1 ? "" : "s"}`);
64
+ const rows = [
65
+ [" school", "delta", `min(${items[0]?.baselineYear ?? "—"})`, "city", "tags", "belong"]
66
+ ];
67
+ for (const c of items) {
68
+ rows.push([
69
+ ` ${c.name}`,
70
+ (c.delta >= 0 ? "+" : "") + c.delta,
71
+ String(c.baselineMinScore),
72
+ c.city,
73
+ labelTags(c),
74
+ c.belong
75
+ ]);
76
+ }
77
+ lines.push(renderTable(rows));
78
+ if (opts.explain) {
79
+ for (const c of items.slice(0, 3)) {
80
+ const reason = key === "冲"
81
+ ? `差 ${Math.abs(c.delta)} 分(基线 ${c.baselineMinScore},${c.baselineYear} ${c.baselineTrackName})— 有希望但需要更高发挥`
82
+ : key === "稳"
83
+ ? `贴近基线 ${c.baselineMinScore}(${c.baselineYear} ${c.baselineTrackName}),同分段稳妥选择`
84
+ : key === "保"
85
+ ? `高于基线 ${c.delta} 分,作为兜底足够稳`
86
+ : `分差 ${c.delta} 超出 ±25 分窗口,不建议`;
87
+ lines.push(` · ${c.name}: ${reason}`);
88
+ }
89
+ }
90
+ }
91
+ return lines.join("\n");
92
+ }
93
+ export function formatTop(rows) {
94
+ const out = [
95
+ [" rank", "school", "delta", "min(year)", "city", "tags", "belong"]
96
+ ];
97
+ rows.forEach((r, i) => {
98
+ out.push([
99
+ ` ${String(i + 1).padStart(2)}`,
100
+ r.schoolName,
101
+ (r.delta >= 0 ? "+" : "") + r.delta,
102
+ `${r.baselineMinScore} (${r.baselineYear})`,
103
+ r.city,
104
+ r.tags,
105
+ r.belong
106
+ ]);
107
+ });
108
+ return renderTable(out);
109
+ }
@@ -0,0 +1,122 @@
1
+ export type SchoolInfo = {
2
+ school_id: string;
3
+ name: string;
4
+ zs_code: string;
5
+ belong: string;
6
+ province_name: string;
7
+ city_name: string;
8
+ town_name: string;
9
+ level_name: string;
10
+ type_name: string;
11
+ nature_name: string;
12
+ dual_class_name: string;
13
+ f985: string;
14
+ f211: string;
15
+ rank: Record<string, string>;
16
+ xueke_rank: Record<string, string>;
17
+ xueke_pinggu?: Record<string, string>;
18
+ pro_type_min: Record<string, Array<{
19
+ year: number;
20
+ type: Record<string, string>;
21
+ }>>;
22
+ province_score_year: string;
23
+ content: string;
24
+ site: string;
25
+ phone: string;
26
+ address: string;
27
+ };
28
+ export type AdmissionPlanItem = {
29
+ school_id: string;
30
+ special_id: string;
31
+ province: string;
32
+ year?: string;
33
+ type: string;
34
+ zslx: string;
35
+ zslx_name: string;
36
+ batch: string;
37
+ local_batch_name: string;
38
+ num: number;
39
+ length: string;
40
+ tuition: string;
41
+ spcode: string;
42
+ spname: string;
43
+ sp_name: string;
44
+ info: string;
45
+ remark: string;
46
+ level1_name: string;
47
+ level2_name: string;
48
+ level3_name: string;
49
+ special_group: string;
50
+ sp_xuanke: string;
51
+ sp_fxk: string;
52
+ sp_sxk: string;
53
+ sp_info: string;
54
+ sg_xuanke: string;
55
+ sg_fxk: string;
56
+ sg_sxk: string;
57
+ sg_info: string;
58
+ sg_name: string;
59
+ first_km: string;
60
+ };
61
+ export type AdmissionPlanResponse = {
62
+ [bucket: string]: {
63
+ numFound: number;
64
+ item: AdmissionPlanItem[];
65
+ };
66
+ };
67
+ export type AdmissionScoreItem = {
68
+ school_id: string;
69
+ special_id: string;
70
+ province: string;
71
+ type: string;
72
+ zslx: string;
73
+ zslx_name: string;
74
+ batch: string;
75
+ local_batch_name: string;
76
+ spcode?: string;
77
+ spname: string;
78
+ sp_name: string;
79
+ info: string;
80
+ remark: string;
81
+ level1_name: string;
82
+ level2_name: string;
83
+ level3_name: string;
84
+ special_group: string;
85
+ max: number;
86
+ min: number;
87
+ average: number;
88
+ lq_num: string;
89
+ min_section: string;
90
+ min_range: string;
91
+ min_rank_range: string;
92
+ range_max_rank: string;
93
+ is_score_range: string;
94
+ diff: number;
95
+ first_km: string;
96
+ sp_type: string;
97
+ sp_fxk: string;
98
+ sp_sxk: string;
99
+ sp_info: string;
100
+ sp_xuanke: string;
101
+ sg_fxk: string;
102
+ sg_sxk: string;
103
+ sg_type: string;
104
+ sg_name: string;
105
+ sg_info: string;
106
+ sg_xuanke: string;
107
+ };
108
+ export type AdmissionScoreResponse = {
109
+ [bucket: string]: {
110
+ numFound: number;
111
+ item: AdmissionScoreItem[];
112
+ };
113
+ };
114
+ export declare function getSchoolInfo(schoolId: number | string): Promise<SchoolInfo>;
115
+ export declare function getAdmissionPlan(schoolId: number | string, year: number, provinceId: number | string): Promise<AdmissionPlanItem[]>;
116
+ export declare function getAdmissionScores(schoolId: number | string, year: number, provinceId: number | string): Promise<AdmissionScoreItem[]>;
117
+ export declare function extractHistoricalScores(info: SchoolInfo, provinceId: number | string): Array<{
118
+ year: number;
119
+ track: string;
120
+ trackName: string;
121
+ minScore: number;
122
+ }>;
@@ -0,0 +1,49 @@
1
+ // Client for static-data.gaokao.cn — the 中国教育在线 "掌上高考" static JSON tier.
2
+ // No auth, no sign, no rate limit observed. Treat it like a public CDN.
3
+ const BASE = "https://static-data.gaokao.cn/www/2.0";
4
+ const UA = "gaokao-pro/0.0.1 (+https://github.com/HA7CH/gaokao-pro)";
5
+ async function fetchJson(path) {
6
+ const url = `${BASE}${path}`;
7
+ const res = await fetch(url, { headers: { "User-Agent": UA } });
8
+ if (!res.ok) {
9
+ throw new Error(`gaokao.cn ${res.status} ${res.statusText} for ${url}`);
10
+ }
11
+ const body = (await res.json());
12
+ if (body.code !== "0000") {
13
+ throw new Error(`gaokao.cn returned code=${body.code} message=${body.message} for ${url}`);
14
+ }
15
+ return body.data;
16
+ }
17
+ // ---- Client ----
18
+ export async function getSchoolInfo(schoolId) {
19
+ return fetchJson(`/school/${schoolId}/info.json`);
20
+ }
21
+ export async function getAdmissionPlan(schoolId, year, provinceId) {
22
+ const raw = await fetchJson(`/schoolspecialplan/${schoolId}/${year}/${provinceId}.json`);
23
+ // Flatten all buckets into a single array.
24
+ return Object.values(raw).flatMap((bucket) => bucket?.item ?? []);
25
+ }
26
+ export async function getAdmissionScores(schoolId, year, provinceId) {
27
+ const raw = await fetchJson(`/schoolspecialscore/${schoolId}/${year}/${provinceId}.json`);
28
+ return Object.values(raw).flatMap((bucket) => bucket?.item ?? []);
29
+ }
30
+ // Convenience: historical min-score series for a given (school, province).
31
+ // Returns [{ year, track, minScore }] sorted by year descending.
32
+ export function extractHistoricalScores(info, provinceId) {
33
+ const entries = info.pro_type_min?.[String(provinceId)] ?? [];
34
+ const out = [];
35
+ for (const e of entries) {
36
+ for (const [track, score] of Object.entries(e.type ?? {})) {
37
+ const min = Number(score);
38
+ if (Number.isFinite(min) && min > 0) {
39
+ out.push({
40
+ year: e.year,
41
+ track,
42
+ trackName: track,
43
+ minScore: min
44
+ });
45
+ }
46
+ }
47
+ }
48
+ return out.sort((a, b) => b.year - a.year || a.track.localeCompare(b.track));
49
+ }
@@ -0,0 +1,33 @@
1
+ export type SchoolRow = {
2
+ gaokao_cn_id: number;
3
+ zs_code: string;
4
+ name: string;
5
+ province: string;
6
+ city: string;
7
+ level: string;
8
+ type: string;
9
+ nature: string;
10
+ belong: string;
11
+ f985: boolean;
12
+ f211: boolean;
13
+ dual_class: string;
14
+ pro_type_min: Record<string, Array<{
15
+ year: number;
16
+ type: Record<string, string>;
17
+ }>>;
18
+ };
19
+ export type SchoolIndex = {
20
+ generated_at: string;
21
+ rows: SchoolRow[];
22
+ };
23
+ export declare function loadIndex(): SchoolIndex;
24
+ export type IndexFilter = {
25
+ level?: string;
26
+ type?: string;
27
+ nature?: string;
28
+ belong?: string;
29
+ f985?: boolean;
30
+ f211?: boolean;
31
+ dualClass?: boolean;
32
+ };
33
+ export declare function filterIndex(index: SchoolIndex, f: IndexFilter): SchoolRow[];
@@ -0,0 +1,59 @@
1
+ // Loads cli/data/school-index.json.gz — the local school corpus built by `probe`.
2
+ // Exposes filters by province / labels / level / type / belong.
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import { gunzipSync } from "node:zlib";
5
+ import { resolve, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const SRC_DIR = dirname(__filename);
9
+ // Look for the gzipped index first (npm-shipped), then the legacy uncompressed
10
+ // docs path (dev convenience).
11
+ const CANDIDATE_PATHS = [
12
+ // From src/ via tsx: cli/src/ → cli/data/
13
+ resolve(SRC_DIR, "..", "data", "school-index.json.gz"),
14
+ // From dist/ compiled: cli/dist/ → cli/data/
15
+ resolve(SRC_DIR, "..", "..", "data", "school-index.json.gz"),
16
+ // Legacy uncompressed paths
17
+ resolve(SRC_DIR, "..", "..", "docs", "school-index.json"),
18
+ resolve(SRC_DIR, "..", "..", "..", "docs", "school-index.json")
19
+ ];
20
+ let cached = null;
21
+ export function loadIndex() {
22
+ if (cached)
23
+ return cached;
24
+ let lastErr = null;
25
+ for (const path of CANDIDATE_PATHS) {
26
+ if (!existsSync(path))
27
+ continue;
28
+ try {
29
+ const buf = readFileSync(path);
30
+ const raw = path.endsWith(".gz") ? gunzipSync(buf).toString("utf8") : buf.toString("utf8");
31
+ cached = JSON.parse(raw);
32
+ return cached;
33
+ }
34
+ catch (e) {
35
+ lastErr = e;
36
+ }
37
+ }
38
+ throw new Error(`school-index not found in any of: ${CANDIDATE_PATHS.join(", ")}. ` +
39
+ `Run \`pnpm probe\` to build it. Last error: ${lastErr}`);
40
+ }
41
+ export function filterIndex(index, f) {
42
+ return index.rows.filter((r) => {
43
+ if (f.level !== undefined && r.level !== f.level)
44
+ return false;
45
+ if (f.type !== undefined && r.type !== f.type)
46
+ return false;
47
+ if (f.nature !== undefined && r.nature !== f.nature)
48
+ return false;
49
+ if (f.belong !== undefined && r.belong !== f.belong)
50
+ return false;
51
+ if (f.f985 === true && !r.f985)
52
+ return false;
53
+ if (f.f211 === true && !r.f211)
54
+ return false;
55
+ if (f.dualClass === true && r.dual_class !== "双一流")
56
+ return false;
57
+ return true;
58
+ });
59
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};