从 JS 数组到 SQLite + REST API — 支持本地维护、第三方开放、ref 元数据返工
当前所有数据硬编码在 site/assets/js/cases-data.js 和 analysis-data.js 两个 JS 文件里。下面是 77 条案例的完整画像。
cat 用短码(mfg/entai)需要字典映射 · ref.url 在 60+ 域名上无元数据需返工 · 长文本/数组结构不统一
基于数据画像,设计 7 张表。主表只存元数据,长文本/数组拆表,字典外键管理。
| 表名 | 用途 | 行级数据 |
|---|---|---|
cases | 主表,一行=一个案例 | id / num / title / industry / region / kpi ... |
refs | 引用表,一对多,★ 返工目标 | url / title / author / published_at / type / language |
demands | 需求列表(数组拆表) | case_id / ord / body |
processes | 流程列表(数组拆表) | case_id / ord / body |
compare_cells | 对比表二维 | case_id / ord / col0 / col1 / col2 |
tags + case_tags | 标签字典 + 多对多 | name / case_id ↔ tag_id |
sources + case_sources | 信源类型字典(信任度) | name / trust 1-5 |
-- 主表
CREATE TABLE cases (
id TEXT PRIMARY KEY, -- 'tampa'
num TEXT NOT NULL, -- '08'
slug TEXT UNIQUE, -- URL 友好
title TEXT NOT NULL,
subtitle TEXT,
industry TEXT NOT NULL, -- '医疗' 显示用
cat TEXT NOT NULL, -- 'med' 短码
scale TEXT NOT NULL,
type TEXT,
region TEXT NOT NULL, -- '亚洲' | '北美' | '欧洲'
country CHAR(2), -- ISO 3166-1 alpha-2
year INTEGER,
month INTEGER,
intro_name TEXT,
intro_desc TEXT, -- 长文本
result TEXT, -- 长文本
cover_css TEXT, -- linear-gradient(...)
cover_img TEXT,
highlight TEXT,
kpi_value REAL,
kpi_unit TEXT,
kpi_label TEXT,
published_at DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
verified_by TEXT
);
CREATE INDEX idx_cases_year ON cases(year);
CREATE INDEX idx_cases_region ON cases(region);
CREATE INDEX idx_cases_cat ON cases(cat);
-- 引用表(返工目标)
CREATE TABLE refs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
case_id TEXT NOT NULL REFERENCES cases(id) ON DELETE CASCADE,
ord INTEGER DEFAULT 0,
url TEXT NOT NULL,
label TEXT,
title TEXT, -- ★ 待返工
author TEXT, -- ★ 待返工
published_at DATE, -- ★ 待返工
publisher TEXT, -- ★ 待返工
type TEXT, -- article | press-release | blog | paper | tweet
language CHAR(2),
fetched_at TIMESTAMP,
verified INTEGER DEFAULT 0
);
/case-detail.html?id=tampa 不友好,API 可用 /cases/tampa-general-hospital-palantir不建议改 Monorepo。静态站和 API 部署频率不同,前端零成本部署到 CDN,API 独立部署。保留 Monorepo 迁移路径:schema/ 目录放共用 TS 类型。
当前 77 条数据 + 13 张图,首屏快、SEO 好。引入 React/Vue = 重新写所有交互 + 构建工具链。数据通过 fetch('/api/v1/cases') 拿,逻辑零改动。
| 环境 | 数据库 | 理由 |
|---|---|---|
| 本地开发 | SQLite 文件 | 单文件零部署,77 条性能绰绰有余 |
| 生产部署 | Cloudflare D1 | 边缘部署,免费 10 万请求/天,与 SQLite SQL 方言 99% 兼容 |
| 备选 | VPS + SQLite | D1 不够用时迁 VPS,迁移成本 1 天 |
你的需求里"抓 ref 元数据"Python 生态最强:trafilatura / metascraper / httpx / beautifulsoup4。
推荐双语言:API 服务用 Node.js + TS,数据返工用 Python。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/cases | 列表,支持 ?region=&year=&cat=&q=&limit=&offset= |
| GET | /api/v1/cases/:id | 单条详情(包含 demands/processes/compare) |
| GET | /api/v1/cases/:id/refs | 该案例的引用列表 |
| GET | /api/v1/categories | 行业字典 |
| GET | /api/v1/regions | 地区字典 |
| GET | /api/v1/stats/overview | 总览 |
| GET | /api/v1/stats/by-region | 地区分布 |
| GET | /api/v1/stats/by-industry | 行业分布 |
| GET | /api/v1/stats/by-month | 月度时间序列 |
| GET | /api/v1/health | 健康检查 |
{
"data": [...],
"meta": { "total": 77, "limit": 20, "offset": 0 },
"links": { "next": "/api/v1/cases?offset=20" }
}
# 列表,筛选 2025 年亚太区的企业 AI 案例
curl 'https://api.fd-go.com/v1/cases?year=2025®ion=%E4%BA%9A%E6%B4%B2&cat=entai'
# 单条详情
curl https://api.fd-go.com/v1/cases/tampa
# 案例的引用
curl https://api.fd-go.com/v1/cases/tampa/refs
Phase 1 完成后,前端 0 行为差异 — 重新生成的 cases-data.js 与原版逐字段对比,完全一致才进入 Phase 2。
| 模块 | 部署目标 | 成本 |
|---|---|---|
| 静态前端 | Cloudflare Pages / GitHub Pages | 免费,CDN 全球 |
| API 服务 | Cloudflare Workers + D1 | 免费 10 万请求/天 |
| SQLite 文件 | Cloudflare R2 / Git LFS | 几美分/月 |
| 数据返工 | 本地 Python | 0 |
若 Cloudflare 不够用:API 迁到 VPS(DigitalOcean / 阿里云)+ Node 进程,SQLite 文件放 VPS,定期备份到 S3。迁移成本 1 天。
77 条引用了 60+ 域名,Top 来源:linkedin.com 10 篇 · openai.com 5 篇 · databricks.com 3 篇 · 中文新闻媒体若干。
# tools/enrich-refs.py
import httpx, metascraper, sqlite3
from datetime import datetime
def enrich_url(url: str) -> dict:
"""抓 URL 元数据 → {title, author, published_at, publisher, language}"""
html = httpx.get(url, follow_redirects=True).text
meta = metascraper.scrape(url=url, html=html)
return {
'title': meta.get('title'),
'author': meta.get('author'),
'published_at': meta.get('published_date'),
'publisher': meta.get('publisher') or meta.get('site_name'),
'language': 'zh' if is_chinese(meta.get('title','')) else 'en',
}
db = sqlite3.connect('data/fde.sqlite')
for ref_id, url in db.execute(
"SELECT id, url FROM refs WHERE title IS NULL"):
try:
meta = enrich_url(url)
db.execute("""UPDATE refs SET
title=?, author=?, published_at=?, publisher=?,
language=?, fetched_at=? WHERE id=?""",
[*meta.values(), datetime.now(), ref_id])
print(f'OK #{ref_id}: {meta["title"][:50]}')
except Exception as e:
print(f'FAIL #{ref_id}: {e}')
db.commit()
verified=0,人工补一次未来 LLM(Claude / GPT / 自定义 Agent)会大量查询我们的数据。两种形式互补,不冲突:md/json 适合快照/RAG,API 适合实时/查询。
Anthropic 推的 MCP 是 LLM-first API 标准,把 API 直接变成 LLM 可调用的工具。一次集成,所有 MCP-兼容 LLM(Claude / Cursor / 自定义)都能用。
| 要求 | 原因 |
|---|---|
/openapi.json 完整 schema | LLM 看 schema 才知道怎么调 |
| 返回字段精简 | 不要把 1000 字 introDesc 塞进列表 API |
支持 ?fields= 过滤 | LLM 只需要 title+highlight 时别返 process[] |
支持 ?include= 展开 | 默认只返元数据,详情才展开 |
| 分页强制 | ?limit=20&cursor=...,防 LLM 一次拉 1w 条 |
| 错误信息具体 | "field year must be 2020-2026" 这种 |
GET /api/v1/cases # 人类用,完整字段
GET /api/v1/llm/cases # LLM 用,精简字段
# ?include=summary,keywords
# ?max_tokens=2000 自动截断
GET /api/v1/llm/cases/:id/summary # LLM 看单条摘要
GET /api/v1/llm/search?q=xxx # LLM 语义检索入口
GET /api/v1/openapi.json # 自动生成,LLM 看 schema
ALTER TABLE cases ADD COLUMN summary TEXT;
-- 一句话摘要(LLM 检索第一眼看这个)
ALTER TABLE cases ADD COLUMN keywords TEXT[];
-- 关键词数组(LLM/RAG 检索用)
ALTER TABLE cases ADD COLUMN embedding vector(1536);
-- pgvector,后续做语义检索
md/json dump 每周一次 + 每次大量更新后。脚本:tools/export-llm-corpus.py,输出到 data/exports/ 目录(R2 公开访问)。
从 77 条到 1 万条(130 倍),很多"轻量"假设会失效。
| 项 | 77 条(当前) | 1 万条 | 改动 |
|---|---|---|---|
| 数据库 | SQLite OK | Postgres 才是答案 | 改 |
| 前端 fetch | 一次性 77 条,合理 | 1 万条 50KB+,SEO 爆 | 改 |
| 部署 | Workers + D1 | D1 5GB 上限不够 | 改 |
| 数据返工 | 一次性脚本 | 必须后台 + 队列 | 改 |
| 检索 | LIKE 模糊 | 全文 + 向量 | 改 |
| 后台 | 手改 JS | 必须有管理后台 | 改 |
SQLite 在 1w 条会出问题:1GB 软上限 + 无并发写 + 无内建全文检索。Postgres / Supabase 解法:
tsvector 全文检索,中文用 zhparserpgvector 后续做语义检索| 阶段 | 方案 | 适用数据量 |
|---|---|---|
| 当前 | 静态 HTML + fetch | < 200 条 |
| 中期 | SSR + ISR(Next.js) | 1 千 ~1 万 |
| 远期 | SSR + 分页 API + 全文检索 | 10 万+ |
| 模块 | 服务 | 成本 |
|---|---|---|
| Postgres | Supabase Pro | $25/月 |
| API + 前端 | Vercel Pro | $20/月 |
| 图床 | Cloudflare R2 | ~$1/月 |
| 总计 | ~$50/月 |
现在 ~ 500 条:保持当前架构 + 引入 Postgres/Supabase + Drizzle schema,前端零改动。 · 500~5000:加 Supabase Studio 后台 + 全文检索 + ISR。 · 5000~1w:迁 Next.js SSR + Refine + pgvector。每个阶段改动量最小。
| 风险 | 应对 |
|---|---|
| 数据迁移丢失 | Phase 1 后 diff 工具对比新旧数据完整性 |
| API 上线后前端崩溃 | 保留 cases-data.js 作为 fallback |
| Cloudflare D1 配额不够 | 备用 VPS,迁移成本 1 天 |
| ref 元数据抓不准 | 留 verified=0 字段,人工兜底 |
| 第三方滥用 API | rate limit + API key + 使用条款 |
data/schema.sqltools/migrate-from-js.js(读 cases-data.js → 写 SQLite)tools/build-cases-data.js(读 SQLite → 重新生成 cases-data.js)
完整 Markdown 文档:docs/architecture/api-architecture-proposal.md