医疗知识图谱 — symptom self-check on a medical knowledge graph, one page, three steps, one Worker.
🇺🇸 English · 🇨🇳 简体中文
Fill in sex and age, tap a body part, pick symptoms. Doctor walks a disease ↔ symptom graph, screens out departments that cannot apply, and ranks candidate diseases by how much of your input they explain. Educational self-check only — not a diagnostic device.
| What it does | How it decides |
| Screens | Quick start |
| API | Using the full dataset |
| Deploy | Project layout |
| Cost and limits | License |
- One page, three steps — profile → symptoms → result. No route changes, no full reloads.
- Body map as the primary picker — front and back figures, with hotspots for head, chest, abdomen, waist, pelvis, limbs. Hovering the head expands 眼/鼻/口/牙/耳/眉 in place.
- Symptom panel floats beside the figure — hundreds of symptoms per region, with an A–Z pinyin filter, so the page never jumps around.
- Reasoning graph in a modal — open any result and see your symptoms on the left, the diseases they point to on the right (the current one highlighted), and the edges that connect them.
- Map each picked symptom onto the disease ↔ symptom graph.
- Reverse-walk the edges: which diseases point at these symptoms.
- Screen out departments by sex and age (妇科 / 男科 / 小儿科 / 老年科).
- Rank by matched count, then by
matched / disease.symptomCount + matched × 0.1.
The list is ordered by how many of your symptoms each disease explains; the ring in the UI shows relative strength within the result set, not a probability.
npm install
# 1. local schema
npx wrangler d1 execute doctor --local --file=./migrations/0001_init.sql
# 2. build the demo dataset from the sample dump
node scripts/convert-mysql-dump.mjs db/sample-dump.sql
# 3. load it
node scripts/load-d1.mjs --local
# 4. run
npm run dev # http://127.0.0.1:8788npm test runs the algorithm unit tests, npm run check runs typecheck plus tests.
All endpoints are JSON under /api/. Errors share one shape: { "error": "…", "code": "…" }.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/health |
smoke test + dataset size |
GET |
/api/symptoms/search?q=&limit= |
autocomplete, Chinese or pinyin |
GET |
/api/symptoms?part=&letter=&limit= |
symptoms of a body part, optional A–Z filter |
GET |
/api/parts |
body part tags with counts |
POST |
/api/analyze |
the self-assessment call |
GET |
/api/diseases?q= |
disease search |
GET |
/api/diseases/:id |
full disease record |
GET |
/api/graph/explain?id=&symptoms=&per= |
reasoning graph: symptoms → diseases |
GET |
/api/graph?id= |
attribute graph for one disease |
curl -X POST https://doctor.wordtk.com/api/analyze \
-H 'content-type: application/json' \
-d '{"sex":"female","age":28,"symptoms":["头痛","恶心"]}'Chinese sex values and age ranges are accepted too, e.g.
{"sex":"女","age":[28,35],"symptom":["头痛"]}. Full field reference:
docs/en/api.md.
The repository ships a hand-written demo subset only. The complete knowledge base is distributed separately under a commercial license.
mkdir -p _data && cp /path/to/robot.sql _data/ # 120+ MB mysqldump
node scripts/convert-mysql-dump.mjs # -> build/seed-*.sql
node scripts/load-d1.mjs --local # or --remoteThe converter reads the mysqldump directly: it parses CREATE TABLE for column order, folds the
side tables that store list columns back into JSON arrays, derives sex/age department scopes,
computes pinyin keys, and emits chunked SQL small enough for wrangler d1 execute.
One full import is about 173,000 row writes; the free tier allows 100,000 per day. Split it first, then load one batch per day:
npm run data:days # build/day-1 + build/day-2 + days.json
npm run data:load -- --remote --day 1 # ~86.7k writes
npm run data:load -- --remote --day 2 # the rest, next daydays.json records the row count and the table mix of each batch, so you can see the cost before
running it.
npx wrangler login
npx wrangler d1 create doctor # copy the returned database_id
# paste it into wrangler.jsonc, then:
npm run data:load -- --remote
npm run deployThe custom domain is not declared in wrangler.jsonc: a live origin must not sit in the
repository. Bind it once in Cloudflare Dashboard → Workers & Pages → doctor → Settings →
Domains & Routes; wrangler deploy leaves that binding untouched. The workers.dev subdomain
is disabled so there is only one public entry point.
The commercial-licence mailbox is deployment configuration too. Add it as a Worker variable —
Cloudflare Dashboard → Workers & Pages → doctor → Settings → Variables → CONTACT_EMAIL
(plain text; .dev.vars for local runs). GET /api/site serves it to the page; while it is
unset the contact blocks stay hidden instead of rendering an empty mail link.
src/
index.ts Worker entry, /api/ dispatch, error envelope
http.ts JSON helpers, edge cache wrapper
db.ts D1 helpers, row -> DTO mapping
lib/diagnose.ts scoring + ranking, pure and unit tested
routes/ health, symptoms, parts, analyze, diseases, graph
public/
index.html single page shell (three steps)
assets/app.css layout and theme
assets/app.js step flow, body map, search, d3 reasoning graph
assets/body/ front/back body figures
assets/vendor/ d3 v3 (loaded only when a result is opened)
migrations/ D1 schema
db/sample-dump.sql hand-written demo subset in mysqldump format
scripts/ dump converter, day splitter, D1 loader
tests/ algorithm tests
Static asset requests do not count as Worker invocations, and the API surface is small. As of October 2026 the free plan allows 100,000 Worker requests/day with 10 ms CPU each, and D1 allows 5 million rows read/day and 100,000 rows written/day. Verify against the official limits before relying on them.
The hot path is one indexed join plus in-memory scoring, so CPU is not the constraint — row reads
are. That is why lookups are index seeks instead of LIKE '%…%' scans.
Copyright (C) 2026 onblog.
The code is licensed under AGPL-3.0-only — see LICENSE. You may use, modify, self-host and even sell it, provided that any derivative work, and any network service built on top of it, is released under the same license.
If that does not fit your case (closed-source distribution, or a closed-source hosted service), a commercial license is available: hello@wordtk.com.
The dataset is not covered by AGPL-3.0. It is licensed separately under a commercial agreement, so this repository ships only a small hand-written sample.