上传文件至「/」

This commit is contained in:
2026-05-14 21:05:25 +00:00
parent f8ed063b76
commit c44d7a4b63
5 changed files with 1375 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
# 🦜 CiBird 词鸟
> 你的私人英语单词本,住在你自己的 VPS 上,AI 自动造句,专攻推特/游戏英语场景
## 一行命令安装
```bash
curl -sSL https://raw.githubusercontent.com/zhangyang-games/cibird/main/install.sh | bash
```
安装过程会问你:
1. 选 AI 服务商(推荐 Gemini 免费额度 / DeepSeek 便宜好用)
2. 粘贴 API Key
3. 设置登录密码
4. 选端口(默认 8848
完成后脚本会告诉你访问地址,打开浏览器就能用。
---
## 功能
- **添加单词** → AI 自动生成释义、音标、2 个例句(口语/推特/游戏场景)
- 🔊 **发音** → 点击发音按钮,浏览器直接朗读
- 📝 **笔记** → 每个单词可以写自己的理解或记忆方法
-**重新造句** → 一键让 AI 换新例句
- 🔍 **搜索** → 支持按单词、释义、笔记搜索
- 📱 **全平台** → 手机/电脑浏览器均可访问,SPA 单页应用
---
## 支持的 AI 服务商
| 服务商 | 推荐模型 | 备注 |
|--------|----------|------|
| Google Gemini | gemini-2.0-flash | 免费额度大,首选 |
| DeepSeek | deepseek-chat | 国产良心价 |
| Groq | llama-3.1-8b-instant | 速度极快,免费 |
| OpenRouter | 任意 | 一个 Key 用几十种模型 |
| Claude | claude-haiku-4-5 | Anthropic 原生 |
| OpenAI | gpt-4o-mini | ChatGPT 同款 |
---
## 管理命令
```bash
cibird # 查看状态
cibird start # 启动
cibird stop # 停止
cibird restart # 重启
cibird log # 查看日志(排查问题)
```
---
## 常见问题
**Q: 装完打不开网页?**
A: Oracle Cloud 需要在"安全列表"里添加端口规则,腾讯云/阿里云要在安全组开放端口。
**Q: AI 造句失败?**
A: 运行 `cibird log` 看报错,通常是 API Key 填错了。
**Q: 怎么换 AI 服务商?**
A: 编辑 `~/cibird/config.json`,修改 provider / api_key / model,然后 `cibird restart`
---
## 文件结构
```
~/cibird/
├── server.py 后端服务
├── index.html 前端单页应用
├── config.json 配置(API Key 等,权限 600
├── cibird.db 单词数据库(SQLite
├── cibird.pid 进程 ID
└── cibird.log 运行日志
```
---
*展翅高飞 🦜*
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+566
View File
@@ -0,0 +1,566 @@
#!/usr/bin/env python3
"""
CiBird 词鸟 - 后端服务 v2.2
FastAPI + SQLite,支持多 AI 服务商
新增:月/周/动物/食物/职业 静态词库接口
"""
import os, json, sqlite3, secrets, hashlib, time, random
from pathlib import Path
from contextlib import contextmanager
from datetime import datetime, timezone
from fastapi import FastAPI, HTTPException, Depends
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
import httpx
BASE_DIR = Path(__file__).parent
CONFIG_FILE = BASE_DIR / "config.json"
DB_FILE = BASE_DIR / "cibird.db"
HTML_FILE = BASE_DIR / "index.html"
def load_config():
if not CONFIG_FILE.exists():
raise RuntimeError("config.json 不存在")
with open(CONFIG_FILE) as f:
return json.load(f)
# ── DB ────────────────────────────────────────────────────────
def init_db():
with sqlite3.connect(DB_FILE) as conn:
conn.execute("""CREATE TABLE IF NOT EXISTS words (
id INTEGER PRIMARY KEY AUTOINCREMENT,
word TEXT NOT NULL, meaning TEXT, phonetic TEXT, pos TEXT,
examples TEXT DEFAULT '[]', note TEXT DEFAULT '',
created INTEGER DEFAULT (strftime('%s','now')))""")
conn.execute("""CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
created INTEGER DEFAULT (strftime('%s','now')))""")
conn.execute("""CREATE TABLE IF NOT EXISTS punch_cards (
date_str TEXT PRIMARY KEY,
count INTEGER DEFAULT 0)""")
# ── 静态词库 ───────────────────────────────────────────────────
STATIC_DATA = {
"国家": [
{"en":"Afghanistan","zh":"阿富汗","note":"亚洲"},
{"en":"Albania","zh":"阿尔巴尼亚","note":"欧洲"},
{"en":"Algeria","zh":"阿尔及利亚","note":"非洲"},
{"en":"Andorra","zh":"安道尔","note":"欧洲"},
{"en":"Angola","zh":"安哥拉","note":"非洲"},
{"en":"Antigua and Barbuda","zh":"安提瓜和巴布达","note":"北美洲"},
{"en":"Argentina","zh":"阿根廷","note":"南美洲"},
{"en":"Armenia","zh":"亚美尼亚","note":"亚洲"},
{"en":"Australia","zh":"澳大利亚","note":"大洋洲"},
{"en":"Austria","zh":"奥地利","note":"欧洲"},
{"en":"Azerbaijan","zh":"阿塞拜疆","note":"亚洲"},
{"en":"Bahamas","zh":"巴哈马","note":"北美洲"},
{"en":"Bahrain","zh":"巴林","note":"亚洲"},
{"en":"Bangladesh","zh":"孟加拉国","note":"亚洲"},
{"en":"Barbados","zh":"巴巴多斯","note":"北美洲"},
{"en":"Belarus","zh":"白俄罗斯","note":"欧洲"},
{"en":"Belgium","zh":"比利时","note":"欧洲"},
{"en":"Belize","zh":"伯利兹","note":"北美洲"},
{"en":"Benin","zh":"贝宁","note":"非洲"},
{"en":"Bhutan","zh":"不丹","note":"亚洲"},
{"en":"Bolivia","zh":"玻利维亚","note":"南美洲"},
{"en":"Bosnia and Herzegovina","zh":"波斯尼亚和黑塞哥维那","note":"欧洲"},
{"en":"Botswana","zh":"博茨瓦纳","note":"非洲"},
{"en":"Brazil","zh":"巴西","note":"南美洲"},
{"en":"Brunei","zh":"文莱","note":"亚洲"},
{"en":"Bulgaria","zh":"保加利亚","note":"欧洲"},
{"en":"Burkina Faso","zh":"布基纳法索","note":"非洲"},
{"en":"Burundi","zh":"布隆迪","note":"非洲"},
{"en":"Cabo Verde","zh":"佛得角","note":"非洲"},
{"en":"Cambodia","zh":"柬埔寨","note":"亚洲"},
{"en":"Cameroon","zh":"喀麦隆","note":"非洲"},
{"en":"Canada","zh":"加拿大","note":"北美洲"},
{"en":"Central African Republic","zh":"中非共和国","note":"非洲"},
{"en":"Chad","zh":"乍得","note":"非洲"},
{"en":"Chile","zh":"智利","note":"南美洲"},
{"en":"China","zh":"中国","note":"亚洲"},
{"en":"Colombia","zh":"哥伦比亚","note":"南美洲"},
{"en":"Comoros","zh":"科摩罗","note":"非洲"},
{"en":"Congo","zh":"刚果共和国","note":"非洲"},
{"en":"Costa Rica","zh":"哥斯达黎加","note":"北美洲"},
{"en":"Croatia","zh":"克罗地亚","note":"欧洲"},
{"en":"Cuba","zh":"古巴","note":"北美洲"},
{"en":"Cyprus","zh":"塞浦路斯","note":"欧洲"},
{"en":"Czech Republic","zh":"捷克","note":"欧洲"},
{"en":"Denmark","zh":"丹麦","note":"欧洲"},
{"en":"Djibouti","zh":"吉布提","note":"非洲"},
{"en":"Dominica","zh":"多米尼克","note":"北美洲"},
{"en":"Dominican Republic","zh":"多米尼加共和国","note":"北美洲"},
{"en":"DR Congo","zh":"刚果民主共和国","note":"非洲"},
{"en":"Ecuador","zh":"厄瓜多尔","note":"南美洲"},
{"en":"Egypt","zh":"埃及","note":"非洲"},
{"en":"El Salvador","zh":"萨尔瓦多","note":"北美洲"},
{"en":"Equatorial Guinea","zh":"赤道几内亚","note":"非洲"},
{"en":"Eritrea","zh":"厄立特里亚","note":"非洲"},
{"en":"Estonia","zh":"爱沙尼亚","note":"欧洲"},
{"en":"Eswatini","zh":"斯威士兰","note":"非洲"},
{"en":"Ethiopia","zh":"埃塞俄比亚","note":"非洲"},
{"en":"Fiji","zh":"斐济","note":"大洋洲"},
{"en":"Finland","zh":"芬兰","note":"欧洲"},
{"en":"France","zh":"法国","note":"欧洲"},
{"en":"Gabon","zh":"加蓬","note":"非洲"},
{"en":"Gambia","zh":"冈比亚","note":"非洲"},
{"en":"Georgia","zh":"格鲁吉亚","note":"亚洲"},
{"en":"Germany","zh":"德国","note":"欧洲"},
{"en":"Ghana","zh":"加纳","note":"非洲"},
{"en":"Greece","zh":"希腊","note":"欧洲"},
{"en":"Grenada","zh":"格林纳达","note":"北美洲"},
{"en":"Guatemala","zh":"危地马拉","note":"北美洲"},
{"en":"Guinea","zh":"几内亚","note":"非洲"},
{"en":"Guinea-Bissau","zh":"几内亚比绍","note":"非洲"},
{"en":"Guyana","zh":"圭亚那","note":"南美洲"},
{"en":"Haiti","zh":"海地","note":"北美洲"},
{"en":"Honduras","zh":"洪都拉斯","note":"北美洲"},
{"en":"Hungary","zh":"匈牙利","note":"欧洲"},
{"en":"Iceland","zh":"冰岛","note":"欧洲"},
{"en":"India","zh":"印度","note":"亚洲"},
{"en":"Indonesia","zh":"印度尼西亚","note":"亚洲"},
{"en":"Iran","zh":"伊朗","note":"亚洲"},
{"en":"Iraq","zh":"伊拉克","note":"亚洲"},
{"en":"Ireland","zh":"爱尔兰","note":"欧洲"},
{"en":"Israel","zh":"以色列","note":"亚洲"},
{"en":"Italy","zh":"意大利","note":"欧洲"},
{"en":"Ivory Coast","zh":"科特迪瓦","note":"非洲"},
{"en":"Jamaica","zh":"牙买加","note":"北美洲"},
{"en":"Japan","zh":"日本","note":"亚洲"},
{"en":"Jordan","zh":"约旦","note":"亚洲"},
{"en":"Kazakhstan","zh":"哈萨克斯坦","note":"亚洲"},
{"en":"Kenya","zh":"肯尼亚","note":"非洲"},
{"en":"Kiribati","zh":"基里巴斯","note":"大洋洲"},
{"en":"Kuwait","zh":"科威特","note":"亚洲"},
{"en":"Kyrgyzstan","zh":"吉尔吉斯斯坦","note":"亚洲"},
{"en":"Laos","zh":"老挝","note":"亚洲"},
{"en":"Latvia","zh":"拉脱维亚","note":"欧洲"},
{"en":"Lebanon","zh":"黎巴嫩","note":"亚洲"},
{"en":"Lesotho","zh":"莱索托","note":"非洲"},
{"en":"Liberia","zh":"利比里亚","note":"非洲"},
{"en":"Libya","zh":"利比亚","note":"非洲"},
{"en":"Liechtenstein","zh":"列支敦士登","note":"欧洲"},
{"en":"Lithuania","zh":"立陶宛","note":"欧洲"},
{"en":"Luxembourg","zh":"卢森堡","note":"欧洲"},
{"en":"Madagascar","zh":"马达加斯加","note":"非洲"},
{"en":"Malawi","zh":"马拉维","note":"非洲"},
{"en":"Malaysia","zh":"马来西亚","note":"亚洲"},
{"en":"Maldives","zh":"马尔代夫","note":"亚洲"},
{"en":"Mali","zh":"马里","note":"非洲"},
{"en":"Malta","zh":"马耳他","note":"欧洲"},
{"en":"Marshall Islands","zh":"马绍尔群岛","note":"大洋洲"},
{"en":"Mauritania","zh":"毛里塔尼亚","note":"非洲"},
{"en":"Mauritius","zh":"毛里求斯","note":"非洲"},
{"en":"Mexico","zh":"墨西哥","note":"北美洲"},
{"en":"Micronesia","zh":"密克罗尼西亚","note":"大洋洲"},
{"en":"Moldova","zh":"摩尔多瓦","note":"欧洲"},
{"en":"Monaco","zh":"摩纳哥","note":"欧洲"},
{"en":"Mongolia","zh":"蒙古","note":"亚洲"},
{"en":"Montenegro","zh":"黑山","note":"欧洲"},
{"en":"Morocco","zh":"摩洛哥","note":"非洲"},
{"en":"Mozambique","zh":"莫桑比克","note":"非洲"},
{"en":"Myanmar","zh":"缅甸","note":"亚洲"},
{"en":"Namibia","zh":"纳米比亚","note":"非洲"},
{"en":"Nauru","zh":"瑙鲁","note":"大洋洲"},
{"en":"Nepal","zh":"尼泊尔","note":"亚洲"},
{"en":"Netherlands","zh":"荷兰","note":"欧洲"},
{"en":"New Zealand","zh":"新西兰","note":"大洋洲"},
{"en":"Nicaragua","zh":"尼加拉瓜","note":"北美洲"},
{"en":"Niger","zh":"尼日尔","note":"非洲"},
{"en":"Nigeria","zh":"尼日利亚","note":"非洲"},
{"en":"North Korea","zh":"朝鲜","note":"亚洲"},
{"en":"North Macedonia","zh":"北马其顿","note":"欧洲"},
{"en":"Norway","zh":"挪威","note":"欧洲"},
{"en":"Oman","zh":"阿曼","note":"亚洲"},
{"en":"Pakistan","zh":"巴基斯坦","note":"亚洲"},
{"en":"Palau","zh":"帕劳","note":"大洋洲"},
{"en":"Palestine","zh":"巴勒斯坦","note":"亚洲"},
{"en":"Panama","zh":"巴拿马","note":"北美洲"},
{"en":"Papua New Guinea","zh":"巴布亚新几内亚","note":"大洋洲"},
{"en":"Paraguay","zh":"巴拉圭","note":"南美洲"},
{"en":"Peru","zh":"秘鲁","note":"南美洲"},
{"en":"Philippines","zh":"菲律宾","note":"亚洲"},
{"en":"Poland","zh":"波兰","note":"欧洲"},
{"en":"Portugal","zh":"葡萄牙","note":"欧洲"},
{"en":"Qatar","zh":"卡塔尔","note":"亚洲"},
{"en":"Romania","zh":"罗马尼亚","note":"欧洲"},
{"en":"Russia","zh":"俄罗斯","note":"欧洲/亚洲"},
{"en":"Rwanda","zh":"卢旺达","note":"非洲"},
{"en":"Saint Kitts and Nevis","zh":"圣基茨和尼维斯","note":"北美洲"},
{"en":"Saint Lucia","zh":"圣卢西亚","note":"北美洲"},
{"en":"Saint Vincent and the Grenadines","zh":"圣文森特和格林纳丁斯","note":"北美洲"},
{"en":"Samoa","zh":"萨摩亚","note":"大洋洲"},
{"en":"San Marino","zh":"圣马力诺","note":"欧洲"},
{"en":"Sao Tome and Principe","zh":"圣多美和普林西比","note":"非洲"},
{"en":"Saudi Arabia","zh":"沙特阿拉伯","note":"亚洲"},
{"en":"Senegal","zh":"塞内加尔","note":"非洲"},
{"en":"Serbia","zh":"塞尔维亚","note":"欧洲"},
{"en":"Seychelles","zh":"塞舌尔","note":"非洲"},
{"en":"Sierra Leone","zh":"塞拉利昂","note":"非洲"},
{"en":"Singapore","zh":"新加坡","note":"亚洲"},
{"en":"Slovakia","zh":"斯洛伐克","note":"欧洲"},
{"en":"Slovenia","zh":"斯洛文尼亚","note":"欧洲"},
{"en":"Solomon Islands","zh":"所罗门群岛","note":"大洋洲"},
{"en":"Somalia","zh":"索马里","note":"非洲"},
{"en":"South Africa","zh":"南非","note":"非洲"},
{"en":"South Korea","zh":"韩国","note":"亚洲"},
{"en":"South Sudan","zh":"南苏丹","note":"非洲"},
{"en":"Spain","zh":"西班牙","note":"欧洲"},
{"en":"Sri Lanka","zh":"斯里兰卡","note":"亚洲"},
{"en":"Sudan","zh":"苏丹","note":"非洲"},
{"en":"Suriname","zh":"苏里南","note":"南美洲"},
{"en":"Sweden","zh":"瑞典","note":"欧洲"},
{"en":"Switzerland","zh":"瑞士","note":"欧洲"},
{"en":"Syria","zh":"叙利亚","note":"亚洲"},
{"en":"Taiwan","zh":"台湾","note":"亚洲"},
{"en":"Tajikistan","zh":"塔吉克斯坦","note":"亚洲"},
{"en":"Tanzania","zh":"坦桑尼亚","note":"非洲"},
{"en":"Thailand","zh":"泰国","note":"亚洲"},
{"en":"Timor-Leste","zh":"东帝汶","note":"亚洲"},
{"en":"Togo","zh":"多哥","note":"非洲"},
{"en":"Tonga","zh":"汤加","note":"大洋洲"},
{"en":"Trinidad and Tobago","zh":"特立尼达和多巴哥","note":"北美洲"},
{"en":"Tunisia","zh":"突尼斯","note":"非洲"},
{"en":"Turkey","zh":"土耳其","note":"亚洲/欧洲"},
{"en":"Turkmenistan","zh":"土库曼斯坦","note":"亚洲"},
{"en":"Tuvalu","zh":"图瓦卢","note":"大洋洲"},
{"en":"Uganda","zh":"乌干达","note":"非洲"},
{"en":"Ukraine","zh":"乌克兰","note":"欧洲"},
{"en":"United Arab Emirates","zh":"阿联酋","note":"亚洲"},
{"en":"United Kingdom","zh":"英国","note":"欧洲"},
{"en":"United States","zh":"美国","note":"北美洲"},
{"en":"Uruguay","zh":"乌拉圭","note":"南美洲"},
{"en":"Uzbekistan","zh":"乌兹别克斯坦","note":"亚洲"},
{"en":"Vanuatu","zh":"瓦努阿图","note":"大洋洲"},
{"en":"Vatican City","zh":"梵蒂冈","note":"欧洲"},
{"en":"Venezuela","zh":"委内瑞拉","note":"南美洲"},
{"en":"Vietnam","zh":"越南","note":"亚洲"},
{"en":"Yemen","zh":"也门","note":"亚洲"},
{"en":"Zambia","zh":"赞比亚","note":"非洲"},
{"en":"Zimbabwe","zh":"津巴布韦","note":"非洲"},
],
"": [
{"en":"January","zh":"一月","note":"1月 / Jan"},
{"en":"February","zh":"二月","note":"2月 / Feb"},
{"en":"March","zh":"三月","note":"3月 / Mar"},
{"en":"April","zh":"四月","note":"4月 / Apr"},
{"en":"May","zh":"五月","note":"5月 / May"},
{"en":"June","zh":"六月","note":"6月 / Jun"},
{"en":"July","zh":"七月","note":"7月 / Jul"},
{"en":"August","zh":"八月","note":"8月 / Aug"},
{"en":"September","zh":"九月","note":"9月 / Sep"},
{"en":"October","zh":"十月","note":"10月 / Oct"},
{"en":"November","zh":"十一月","note":"11月 / Nov"},
{"en":"December","zh":"十二月","note":"12月 / Dec"},
],
"": [
{"en":"Monday","zh":"星期一","note":"Mon"},
{"en":"Tuesday","zh":"星期二","note":"Tue"},
{"en":"Wednesday","zh":"星期三","note":"Wed"},
{"en":"Thursday","zh":"星期四","note":"Thu"},
{"en":"Friday","zh":"星期五","note":"Fri"},
{"en":"Saturday","zh":"星期六","note":"Sat"},
{"en":"Sunday","zh":"星期日","note":"Sun"},
],
"动物": [
{"en":"dog","zh":"","note":"🐶"},
{"en":"cat","zh":"","note":"🐱"},
{"en":"lion","zh":"狮子","note":"🦁"},
{"en":"tiger","zh":"老虎","note":"🐯"},
{"en":"elephant","zh":"大象","note":"🐘"},
{"en":"bear","zh":"","note":"🐻"},
{"en":"monkey","zh":"猴子","note":"🐵"},
{"en":"giraffe","zh":"长颈鹿","note":"🦒"},
{"en":"zebra","zh":"斑马","note":"🦓"},
{"en":"wolf","zh":"","note":"🐺"},
{"en":"fox","zh":"狐狸","note":"🦊"},
{"en":"rabbit","zh":"兔子","note":"🐰"},
{"en":"horse","zh":"","note":"🐴"},
{"en":"cow","zh":"奶牛","note":"🐮"},
{"en":"pig","zh":"","note":"🐷"},
{"en":"sheep","zh":"","note":"🐑"},
{"en":"chicken","zh":"","note":"🐔"},
{"en":"duck","zh":"鸭子","note":"🦆"},
{"en":"penguin","zh":"企鹅","note":"🐧"},
{"en":"eagle","zh":"老鹰","note":"🦅"},
{"en":"parrot","zh":"鹦鹉","note":"🦜"},
{"en":"snake","zh":"","note":"🐍"},
{"en":"crocodile","zh":"鳄鱼","note":"🐊"},
{"en":"shark","zh":"鲨鱼","note":"🦈"},
{"en":"whale","zh":"鲸鱼","note":"🐋"},
{"en":"dolphin","zh":"海豚","note":"🐬"},
{"en":"frog","zh":"青蛙","note":"🐸"},
{"en":"butterfly","zh":"蝴蝶","note":"🦋"},
{"en":"bee","zh":"蜜蜂","note":"🐝"},
{"en":"spider","zh":"蜘蛛","note":"🕷️"},
],
"食物": [
{"en":"rice","zh":"米饭","note":"🍚"},
{"en":"noodles","zh":"面条","note":"🍜"},
{"en":"bread","zh":"面包","note":"🍞"},
{"en":"pizza","zh":"披萨","note":"🍕"},
{"en":"burger","zh":"汉堡","note":"🍔"},
{"en":"hot dog","zh":"热狗","note":"🌭"},
{"en":"sandwich","zh":"三明治","note":"🥪"},
{"en":"sushi","zh":"寿司","note":"🍣"},
{"en":"steak","zh":"牛排","note":"🥩"},
{"en":"chicken","zh":"鸡肉","note":"🍗"},
{"en":"fish","zh":"","note":"🐟"},
{"en":"egg","zh":"鸡蛋","note":"🥚"},
{"en":"salad","zh":"沙拉","note":"🥗"},
{"en":"soup","zh":"","note":"🍲"},
{"en":"dumpling","zh":"饺子","note":"🥟"},
{"en":"apple","zh":"苹果","note":"🍎"},
{"en":"banana","zh":"香蕉","note":"🍌"},
{"en":"orange","zh":"橙子","note":"🍊"},
{"en":"strawberry","zh":"草莓","note":"🍓"},
{"en":"watermelon","zh":"西瓜","note":"🍉"},
{"en":"grape","zh":"葡萄","note":"🍇"},
{"en":"mango","zh":"芒果","note":"🥭"},
{"en":"potato","zh":"土豆","note":"🥔"},
{"en":"tomato","zh":"西红柿","note":"🍅"},
{"en":"carrot","zh":"胡萝卜","note":"🥕"},
{"en":"cake","zh":"蛋糕","note":"🎂"},
{"en":"ice cream","zh":"冰淇淋","note":"🍦"},
{"en":"chocolate","zh":"巧克力","note":"🍫"},
{"en":"coffee","zh":"咖啡","note":""},
{"en":"tea","zh":"","note":"🍵"},
],
"职业": [
{"en":"doctor","zh":"医生","note":"🏥"},
{"en":"nurse","zh":"护士","note":"👩‍⚕️"},
{"en":"teacher","zh":"老师","note":"👩‍🏫"},
{"en":"engineer","zh":"工程师","note":"👨‍💻"},
{"en":"programmer","zh":"程序员","note":"💻"},
{"en":"designer","zh":"设计师","note":"🎨"},
{"en":"lawyer","zh":"律师","note":"⚖️"},
{"en":"judge","zh":"法官","note":"👨‍⚖️"},
{"en":"police","zh":"警察","note":"👮"},
{"en":"firefighter","zh":"消防员","note":"🚒"},
{"en":"soldier","zh":"士兵","note":"💂"},
{"en":"chef","zh":"厨师","note":"👨‍🍳"},
{"en":"waiter","zh":"服务员","note":"🍽️"},
{"en":"driver","zh":"司机","note":"🚗"},
{"en":"pilot","zh":"飞行员","note":"✈️"},
{"en":"sailor","zh":"水手","note":""},
{"en":"farmer","zh":"农民","note":"👨‍🌾"},
{"en":"scientist","zh":"科学家","note":"🔬"},
{"en":"artist","zh":"艺术家","note":"🎭"},
{"en":"singer","zh":"歌手","note":"🎤"},
{"en":"actor","zh":"演员","note":"🎬"},
{"en":"athlete","zh":"运动员","note":"🏅"},
{"en":"journalist","zh":"记者","note":"📰"},
{"en":"photographer","zh":"摄影师","note":"📷"},
{"en":"accountant","zh":"会计","note":"💰"},
{"en":"manager","zh":"经理","note":"👔"},
{"en":"secretary","zh":"秘书","note":"📋"},
{"en":"salesperson","zh":"销售员","note":"🛍️"},
{"en":"mechanic","zh":"机械师","note":"🔧"},
{"en":"electrician","zh":"电工","note":""},
],
}
# ── AI 分类(需要 AI 生成的) ──────────────────────────────────
AI_CATEGORIES = {
"基础": "Most common 100 English words for beginners.",
"推特": "Common slang and abbreviations used on Twitter/X.",
"游戏": "Essential vocabulary for gamers (UI, chat, mechanics).",
"生存": "Crucial phrases for living abroad (ordering, directions).",
}
app = FastAPI()
auth_scheme = HTTPBearer()
def verify_token(cred: HTTPAuthorizationCredentials = Depends(auth_scheme)):
token = cred.credentials
with sqlite3.connect(DB_FILE) as conn:
row = conn.execute("SELECT token FROM sessions WHERE token=?", (token,)).fetchone()
if not row: raise HTTPException(status_code=401, detail="未登录")
return token
async def ask_ai(system: str, user: str):
cfg = load_config()
provider = cfg.get("provider", "gemini")
api_key = cfg.get("api_key", "")
model = cfg.get("model", "")
timeout = 30.0
headers = {"Content-Type": "application/json"}
if provider in ["gemini", "deepseek", "groq", "openrouter", "openai"]:
endpoints = {
"gemini": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
"deepseek": "https://api.deepseek.com/chat/completions",
"groq": "https://api.groq.com/openai/v1/chat/completions",
"openrouter": "https://openrouter.ai/api/v1/chat/completions",
"openai": "https://api.openai.com/v1/chat/completions",
}
url = endpoints[provider]
headers["Authorization"] = f"Bearer {api_key}"
payload = {"model": model, "messages": [{"role":"system","content":system},{"role":"user","content":user}], "max_tokens": 2000}
async with httpx.AsyncClient(timeout=timeout) as client:
r = await client.post(url, headers=headers, json=payload)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
return "不支持的服务商"
# ── API ───────────────────────────────────────────────────────
@app.get("/")
async def read_index():
return HTMLResponse(content=open(HTML_FILE, encoding='utf-8').read())
@app.get("/Cibird.png")
async def get_icon():
icon_path = BASE_DIR / "Cibird.png"
if not icon_path.exists():
raise HTTPException(status_code=404, detail="图标文件不存在")
return FileResponse(str(icon_path), media_type="image/png")
@app.post("/api/login")
async def login(data: dict):
cfg = load_config()
pw_hash = hashlib.sha256(data.get("password", "").encode()).hexdigest()
if pw_hash != cfg.get("password_hash"):
raise HTTPException(status_code=401, detail="密码错误")
token = secrets.token_hex(16)
with sqlite3.connect(DB_FILE) as conn:
conn.execute("INSERT INTO sessions (token) VALUES (?)", (token,))
return {"token": token}
@app.get("/api/words")
async def list_words(q: str = "", token: str = Depends(verify_token)):
with sqlite3.connect(DB_FILE) as conn:
conn.row_factory = sqlite3.Row
sql = "SELECT * FROM words"
params = []
if q:
sql += " WHERE word LIKE ? OR meaning LIKE ? OR note LIKE ?"
params = [f"%{q}%", f"%{q}%", f"%{q}%"]
sql += " ORDER BY created DESC"
rows = conn.execute(sql, params).fetchall()
return [dict(r) for r in rows]
@app.post("/api/words")
async def add_word(data: dict, token: str = Depends(verify_token)):
word = data.get("word", "").strip()
if not word: return {"error": "Word is empty"}
system = "You are a helpful English teacher. Return ONLY JSON."
prompt = f"""Define '{word}'. Output JSON:
{{'word': '{word}', 'phonetic': '...', 'pos': '...', 'meaning': '...', 'examples': ['English example 1 (context: Twitter/Game)', 'English example 2 (context: Daily/Living)']}}
"""
try:
res = await ask_ai(system, prompt)
res_json = json.loads(res.strip('`').replace('json\n',''))
with sqlite3.connect(DB_FILE) as conn:
conn.execute("INSERT INTO words (word, meaning, phonetic, pos, examples) VALUES (?,?,?,?,?)",
(res_json['word'], res_json['meaning'], res_json['phonetic'], res_json['pos'], json.dumps(res_json['examples'])))
today = datetime.now().strftime('%Y-%m-%d')
conn.execute("INSERT INTO punch_cards(date_str, count) VALUES(?,1) ON CONFLICT(date_str) DO UPDATE SET count=count+1", (today,))
return {"success": True}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# 静态词库接口(月/周/动物/食物/职业)
@app.get("/api/static/{cat}")
async def get_static(cat: str, token: str = Depends(verify_token)):
if cat not in STATIC_DATA:
raise HTTPException(status_code=404, detail="分类不存在")
return {"items": STATIC_DATA[cat]}
# AI 词库接口(国家等需要 AI 的)
@app.get("/api/essentials/{cat}")
async def get_essentials(cat: str, token: str = Depends(verify_token)):
if cat not in AI_CATEGORIES: raise HTTPException(status_code=404)
system = "You are a world geography and language expert. Return ONLY JSON array of objects."
prompt = f"{AI_CATEGORIES[cat]} Output format: {{'items': [{{'en': '...', 'zh': '...', 'note': '...'}}, ...]}}"
try:
res = await ask_ai(system, prompt)
return json.loads(res.strip('`').replace('json\n',''))
except:
return {"items": []}
@app.get("/api/stats")
async def get_stats(token: str = Depends(verify_token)):
with sqlite3.connect(DB_FILE) as conn:
rows = conn.execute("SELECT date_str, count FROM punch_cards ORDER BY date_str DESC LIMIT 100").fetchall()
return {r[0]: r[1] for r in rows}
# ── 补全缺失接口 ──────────────────────────────────────────────
# AI 生成单词详情(添加单词用)
@app.post("/api/generate")
async def generate_word(data: dict, token: str = Depends(verify_token)):
word = data.get("word", "").strip()
if not word: raise HTTPException(status_code=400, detail="Word is empty")
system = "You are a helpful English teacher. Return ONLY valid JSON, no markdown."
prompt = f"""Define '{word}'. Return JSON exactly:
{{"word":"{word}","phonetic":"...","pos":"...","meaning":"中文释义","examples":[{{"en":"example sentence 1 (Twitter/Game context)","zh":"中文翻译1"}},{{"en":"example sentence 2 (Daily/Living context)","zh":"中文翻译2"}}]}}"""
try:
res = await ask_ai(system, prompt)
clean = res.strip().strip('`').removeprefix('json').strip()
return json.loads(clean)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# 更新单词例句
@app.patch("/api/words/{word_id}/examples")
async def update_examples(word_id: int, data: dict, token: str = Depends(verify_token)):
examples = data.get("examples", [])
with sqlite3.connect(DB_FILE) as conn:
conn.execute("UPDATE words SET examples=? WHERE id=?", (json.dumps(examples), word_id))
return {"success": True}
# 更新单词笔记
@app.patch("/api/words/{word_id}/note")
async def update_note(word_id: int, data: dict, token: str = Depends(verify_token)):
note = data.get("note", "")
with sqlite3.connect(DB_FILE) as conn:
conn.execute("UPDATE words SET note=? WHERE id=?", (note, word_id))
return {"success": True}
# 删除单词
@app.delete("/api/words/{word_id}")
async def delete_word(word_id: int, token: str = Depends(verify_token)):
with sqlite3.connect(DB_FILE) as conn:
conn.execute("DELETE FROM words WHERE id=?", (word_id,))
return {"success": True}
# 打卡统计
@app.get("/api/checkins")
async def get_checkins(token: str = Depends(verify_token)):
with sqlite3.connect(DB_FILE) as conn:
total = conn.execute("SELECT COUNT(*) FROM words").fetchone()[0]
rows = conn.execute("SELECT date_str, count FROM punch_cards ORDER BY date_str DESC LIMIT 100").fetchall()
today = datetime.now().strftime('%Y-%m-%d')
records = [{"date": r[0], "count": r[1]} for r in rows]
return {"total_words": total, "today": today, "records": records}
# 今日金句(从词库随机取一个词的例句)
@app.get("/api/daily-quote")
async def daily_quote(token: str = Depends(verify_token)):
today = datetime.now().strftime('%Y-%m-%d')
with sqlite3.connect(DB_FILE) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute("SELECT * FROM words WHERE examples != '[]' ORDER BY RANDOM() LIMIT 10").fetchall()
if not rows:
return {"word": "CiBird", "sentence_en": "Keep learning every day!", "sentence_zh": "每天坚持学习!", "date": today}
# 用日期做种子,同一天返回同一个词
seed = sum(ord(c) for c in today)
w = rows[seed % len(rows)]
exs = json.loads(w["examples"] or "[]")
ex = exs[0] if exs else {}
if isinstance(ex, str):
return {"word": w["word"], "sentence_en": ex, "sentence_zh": "", "date": today}
return {"word": w["word"], "sentence_en": ex.get("en",""), "sentence_zh": ex.get("zh",""), "date": today}
if __name__ == "__main__":
import uvicorn
init_db()
uvicorn.run(app, host="0.0.0.0", port=8848)
+564
View File
@@ -0,0 +1,564 @@
#!/usr/bin/env python3
"""
CiBird 词鸟 - 后端服务 v2.2
FastAPI + SQLite,支持多 AI 服务商
新增:月/周/动物/食物/职业 静态词库接口
"""
import os, json, sqlite3, secrets, hashlib, time, random
from pathlib import Path
from contextlib import contextmanager
from datetime import datetime, timezone
from fastapi import FastAPI, HTTPException, Depends
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
import httpx
BASE_DIR = Path(__file__).parent
CONFIG_FILE = BASE_DIR / "config.json"
DB_FILE = BASE_DIR / "cibird.db"
HTML_FILE = BASE_DIR / "index.html"
def load_config():
if not CONFIG_FILE.exists():
raise RuntimeError("config.json 不存在")
with open(CONFIG_FILE) as f:
return json.load(f)
# ── DB ────────────────────────────────────────────────────────
def init_db():
with sqlite3.connect(DB_FILE) as conn:
conn.execute("""CREATE TABLE IF NOT EXISTS words (
id INTEGER PRIMARY KEY AUTOINCREMENT,
word TEXT NOT NULL, meaning TEXT, phonetic TEXT, pos TEXT,
examples TEXT DEFAULT '[]', note TEXT DEFAULT '',
created INTEGER DEFAULT (strftime('%s','now')))""")
conn.execute("""CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
created INTEGER DEFAULT (strftime('%s','now')))""")
conn.execute("""CREATE TABLE IF NOT EXISTS punch_cards (
date_str TEXT PRIMARY KEY,
count INTEGER DEFAULT 0)""")
# ── 静态词库 ───────────────────────────────────────────────────
STATIC_DATA = {
"国家": [
{"en":"Afghanistan","zh":"阿富汗","note":"亚洲"},
{"en":"Albania","zh":"阿尔巴尼亚","note":"欧洲"},
{"en":"Algeria","zh":"阿尔及利亚","note":"非洲"},
{"en":"Andorra","zh":"安道尔","note":"欧洲"},
{"en":"Angola","zh":"安哥拉","note":"非洲"},
{"en":"Antigua and Barbuda","zh":"安提瓜和巴布达","note":"北美洲"},
{"en":"Argentina","zh":"阿根廷","note":"南美洲"},
{"en":"Armenia","zh":"亚美尼亚","note":"亚洲"},
{"en":"Australia","zh":"澳大利亚","note":"大洋洲"},
{"en":"Austria","zh":"奥地利","note":"欧洲"},
{"en":"Azerbaijan","zh":"阿塞拜疆","note":"亚洲"},
{"en":"Bahamas","zh":"巴哈马","note":"北美洲"},
{"en":"Bahrain","zh":"巴林","note":"亚洲"},
{"en":"Bangladesh","zh":"孟加拉国","note":"亚洲"},
{"en":"Barbados","zh":"巴巴多斯","note":"北美洲"},
{"en":"Belarus","zh":"白俄罗斯","note":"欧洲"},
{"en":"Belgium","zh":"比利时","note":"欧洲"},
{"en":"Belize","zh":"伯利兹","note":"北美洲"},
{"en":"Benin","zh":"贝宁","note":"非洲"},
{"en":"Bhutan","zh":"不丹","note":"亚洲"},
{"en":"Bolivia","zh":"玻利维亚","note":"南美洲"},
{"en":"Bosnia and Herzegovina","zh":"波斯尼亚和黑塞哥维那","note":"欧洲"},
{"en":"Botswana","zh":"博茨瓦纳","note":"非洲"},
{"en":"Brazil","zh":"巴西","note":"南美洲"},
{"en":"Brunei","zh":"文莱","note":"亚洲"},
{"en":"Bulgaria","zh":"保加利亚","note":"欧洲"},
{"en":"Burkina Faso","zh":"布基纳法索","note":"非洲"},
{"en":"Burundi","zh":"布隆迪","note":"非洲"},
{"en":"Cabo Verde","zh":"佛得角","note":"非洲"},
{"en":"Cambodia","zh":"柬埔寨","note":"亚洲"},
{"en":"Cameroon","zh":"喀麦隆","note":"非洲"},
{"en":"Canada","zh":"加拿大","note":"北美洲"},
{"en":"Central African Republic","zh":"中非共和国","note":"非洲"},
{"en":"Chad","zh":"乍得","note":"非洲"},
{"en":"Chile","zh":"智利","note":"南美洲"},
{"en":"China","zh":"中国","note":"亚洲"},
{"en":"Colombia","zh":"哥伦比亚","note":"南美洲"},
{"en":"Comoros","zh":"科摩罗","note":"非洲"},
{"en":"Congo","zh":"刚果共和国","note":"非洲"},
{"en":"Costa Rica","zh":"哥斯达黎加","note":"北美洲"},
{"en":"Croatia","zh":"克罗地亚","note":"欧洲"},
{"en":"Cuba","zh":"古巴","note":"北美洲"},
{"en":"Cyprus","zh":"塞浦路斯","note":"欧洲"},
{"en":"Czech Republic","zh":"捷克","note":"欧洲"},
{"en":"Denmark","zh":"丹麦","note":"欧洲"},
{"en":"Djibouti","zh":"吉布提","note":"非洲"},
{"en":"Dominica","zh":"多米尼克","note":"北美洲"},
{"en":"Dominican Republic","zh":"多米尼加共和国","note":"北美洲"},
{"en":"DR Congo","zh":"刚果民主共和国","note":"非洲"},
{"en":"Ecuador","zh":"厄瓜多尔","note":"南美洲"},
{"en":"Egypt","zh":"埃及","note":"非洲"},
{"en":"El Salvador","zh":"萨尔瓦多","note":"北美洲"},
{"en":"Equatorial Guinea","zh":"赤道几内亚","note":"非洲"},
{"en":"Eritrea","zh":"厄立特里亚","note":"非洲"},
{"en":"Estonia","zh":"爱沙尼亚","note":"欧洲"},
{"en":"Eswatini","zh":"斯威士兰","note":"非洲"},
{"en":"Ethiopia","zh":"埃塞俄比亚","note":"非洲"},
{"en":"Fiji","zh":"斐济","note":"大洋洲"},
{"en":"Finland","zh":"芬兰","note":"欧洲"},
{"en":"France","zh":"法国","note":"欧洲"},
{"en":"Gabon","zh":"加蓬","note":"非洲"},
{"en":"Gambia","zh":"冈比亚","note":"非洲"},
{"en":"Georgia","zh":"格鲁吉亚","note":"亚洲"},
{"en":"Germany","zh":"德国","note":"欧洲"},
{"en":"Ghana","zh":"加纳","note":"非洲"},
{"en":"Greece","zh":"希腊","note":"欧洲"},
{"en":"Grenada","zh":"格林纳达","note":"北美洲"},
{"en":"Guatemala","zh":"危地马拉","note":"北美洲"},
{"en":"Guinea","zh":"几内亚","note":"非洲"},
{"en":"Guinea-Bissau","zh":"几内亚比绍","note":"非洲"},
{"en":"Guyana","zh":"圭亚那","note":"南美洲"},
{"en":"Haiti","zh":"海地","note":"北美洲"},
{"en":"Honduras","zh":"洪都拉斯","note":"北美洲"},
{"en":"Hungary","zh":"匈牙利","note":"欧洲"},
{"en":"Iceland","zh":"冰岛","note":"欧洲"},
{"en":"India","zh":"印度","note":"亚洲"},
{"en":"Indonesia","zh":"印度尼西亚","note":"亚洲"},
{"en":"Iran","zh":"伊朗","note":"亚洲"},
{"en":"Iraq","zh":"伊拉克","note":"亚洲"},
{"en":"Ireland","zh":"爱尔兰","note":"欧洲"},
{"en":"Israel","zh":"以色列","note":"亚洲"},
{"en":"Italy","zh":"意大利","note":"欧洲"},
{"en":"Ivory Coast","zh":"科特迪瓦","note":"非洲"},
{"en":"Jamaica","zh":"牙买加","note":"北美洲"},
{"en":"Japan","zh":"日本","note":"亚洲"},
{"en":"Jordan","zh":"约旦","note":"亚洲"},
{"en":"Kazakhstan","zh":"哈萨克斯坦","note":"亚洲"},
{"en":"Kenya","zh":"肯尼亚","note":"非洲"},
{"en":"Kiribati","zh":"基里巴斯","note":"大洋洲"},
{"en":"Kuwait","zh":"科威特","note":"亚洲"},
{"en":"Kyrgyzstan","zh":"吉尔吉斯斯坦","note":"亚洲"},
{"en":"Laos","zh":"老挝","note":"亚洲"},
{"en":"Latvia","zh":"拉脱维亚","note":"欧洲"},
{"en":"Lebanon","zh":"黎巴嫩","note":"亚洲"},
{"en":"Lesotho","zh":"莱索托","note":"非洲"},
{"en":"Liberia","zh":"利比里亚","note":"非洲"},
{"en":"Libya","zh":"利比亚","note":"非洲"},
{"en":"Liechtenstein","zh":"列支敦士登","note":"欧洲"},
{"en":"Lithuania","zh":"立陶宛","note":"欧洲"},
{"en":"Luxembourg","zh":"卢森堡","note":"欧洲"},
{"en":"Madagascar","zh":"马达加斯加","note":"非洲"},
{"en":"Malawi","zh":"马拉维","note":"非洲"},
{"en":"Malaysia","zh":"马来西亚","note":"亚洲"},
{"en":"Maldives","zh":"马尔代夫","note":"亚洲"},
{"en":"Mali","zh":"马里","note":"非洲"},
{"en":"Malta","zh":"马耳他","note":"欧洲"},
{"en":"Marshall Islands","zh":"马绍尔群岛","note":"大洋洲"},
{"en":"Mauritania","zh":"毛里塔尼亚","note":"非洲"},
{"en":"Mauritius","zh":"毛里求斯","note":"非洲"},
{"en":"Mexico","zh":"墨西哥","note":"北美洲"},
{"en":"Micronesia","zh":"密克罗尼西亚","note":"大洋洲"},
{"en":"Moldova","zh":"摩尔多瓦","note":"欧洲"},
{"en":"Monaco","zh":"摩纳哥","note":"欧洲"},
{"en":"Mongolia","zh":"蒙古","note":"亚洲"},
{"en":"Montenegro","zh":"黑山","note":"欧洲"},
{"en":"Morocco","zh":"摩洛哥","note":"非洲"},
{"en":"Mozambique","zh":"莫桑比克","note":"非洲"},
{"en":"Myanmar","zh":"缅甸","note":"亚洲"},
{"en":"Namibia","zh":"纳米比亚","note":"非洲"},
{"en":"Nauru","zh":"瑙鲁","note":"大洋洲"},
{"en":"Nepal","zh":"尼泊尔","note":"亚洲"},
{"en":"Netherlands","zh":"荷兰","note":"欧洲"},
{"en":"New Zealand","zh":"新西兰","note":"大洋洲"},
{"en":"Nicaragua","zh":"尼加拉瓜","note":"北美洲"},
{"en":"Niger","zh":"尼日尔","note":"非洲"},
{"en":"Nigeria","zh":"尼日利亚","note":"非洲"},
{"en":"North Korea","zh":"朝鲜","note":"亚洲"},
{"en":"North Macedonia","zh":"北马其顿","note":"欧洲"},
{"en":"Norway","zh":"挪威","note":"欧洲"},
{"en":"Oman","zh":"阿曼","note":"亚洲"},
{"en":"Pakistan","zh":"巴基斯坦","note":"亚洲"},
{"en":"Palau","zh":"帕劳","note":"大洋洲"},
{"en":"Palestine","zh":"巴勒斯坦","note":"亚洲"},
{"en":"Panama","zh":"巴拿马","note":"北美洲"},
{"en":"Papua New Guinea","zh":"巴布亚新几内亚","note":"大洋洲"},
{"en":"Paraguay","zh":"巴拉圭","note":"南美洲"},
{"en":"Peru","zh":"秘鲁","note":"南美洲"},
{"en":"Philippines","zh":"菲律宾","note":"亚洲"},
{"en":"Poland","zh":"波兰","note":"欧洲"},
{"en":"Portugal","zh":"葡萄牙","note":"欧洲"},
{"en":"Qatar","zh":"卡塔尔","note":"亚洲"},
{"en":"Romania","zh":"罗马尼亚","note":"欧洲"},
{"en":"Russia","zh":"俄罗斯","note":"欧洲/亚洲"},
{"en":"Rwanda","zh":"卢旺达","note":"非洲"},
{"en":"Saint Kitts and Nevis","zh":"圣基茨和尼维斯","note":"北美洲"},
{"en":"Saint Lucia","zh":"圣卢西亚","note":"北美洲"},
{"en":"Saint Vincent and the Grenadines","zh":"圣文森特和格林纳丁斯","note":"北美洲"},
{"en":"Samoa","zh":"萨摩亚","note":"大洋洲"},
{"en":"San Marino","zh":"圣马力诺","note":"欧洲"},
{"en":"Sao Tome and Principe","zh":"圣多美和普林西比","note":"非洲"},
{"en":"Saudi Arabia","zh":"沙特阿拉伯","note":"亚洲"},
{"en":"Senegal","zh":"塞内加尔","note":"非洲"},
{"en":"Serbia","zh":"塞尔维亚","note":"欧洲"},
{"en":"Seychelles","zh":"塞舌尔","note":"非洲"},
{"en":"Sierra Leone","zh":"塞拉利昂","note":"非洲"},
{"en":"Singapore","zh":"新加坡","note":"亚洲"},
{"en":"Slovakia","zh":"斯洛伐克","note":"欧洲"},
{"en":"Slovenia","zh":"斯洛文尼亚","note":"欧洲"},
{"en":"Solomon Islands","zh":"所罗门群岛","note":"大洋洲"},
{"en":"Somalia","zh":"索马里","note":"非洲"},
{"en":"South Africa","zh":"南非","note":"非洲"},
{"en":"South Korea","zh":"韩国","note":"亚洲"},
{"en":"South Sudan","zh":"南苏丹","note":"非洲"},
{"en":"Spain","zh":"西班牙","note":"欧洲"},
{"en":"Sri Lanka","zh":"斯里兰卡","note":"亚洲"},
{"en":"Sudan","zh":"苏丹","note":"非洲"},
{"en":"Suriname","zh":"苏里南","note":"南美洲"},
{"en":"Sweden","zh":"瑞典","note":"欧洲"},
{"en":"Switzerland","zh":"瑞士","note":"欧洲"},
{"en":"Syria","zh":"叙利亚","note":"亚洲"},
{"en":"Taiwan","zh":"台湾","note":"亚洲"},
{"en":"Tajikistan","zh":"塔吉克斯坦","note":"亚洲"},
{"en":"Tanzania","zh":"坦桑尼亚","note":"非洲"},
{"en":"Thailand","zh":"泰国","note":"亚洲"},
{"en":"Timor-Leste","zh":"东帝汶","note":"亚洲"},
{"en":"Togo","zh":"多哥","note":"非洲"},
{"en":"Tonga","zh":"汤加","note":"大洋洲"},
{"en":"Trinidad and Tobago","zh":"特立尼达和多巴哥","note":"北美洲"},
{"en":"Tunisia","zh":"突尼斯","note":"非洲"},
{"en":"Turkey","zh":"土耳其","note":"亚洲/欧洲"},
{"en":"Turkmenistan","zh":"土库曼斯坦","note":"亚洲"},
{"en":"Tuvalu","zh":"图瓦卢","note":"大洋洲"},
{"en":"Uganda","zh":"乌干达","note":"非洲"},
{"en":"Ukraine","zh":"乌克兰","note":"欧洲"},
{"en":"United Arab Emirates","zh":"阿联酋","note":"亚洲"},
{"en":"United Kingdom","zh":"英国","note":"欧洲"},
{"en":"United States","zh":"美国","note":"北美洲"},
{"en":"Uruguay","zh":"乌拉圭","note":"南美洲"},
{"en":"Uzbekistan","zh":"乌兹别克斯坦","note":"亚洲"},
{"en":"Vanuatu","zh":"瓦努阿图","note":"大洋洲"},
{"en":"Vatican City","zh":"梵蒂冈","note":"欧洲"},
{"en":"Venezuela","zh":"委内瑞拉","note":"南美洲"},
{"en":"Vietnam","zh":"越南","note":"亚洲"},
{"en":"Yemen","zh":"也门","note":"亚洲"},
{"en":"Zambia","zh":"赞比亚","note":"非洲"},
{"en":"Zimbabwe","zh":"津巴布韦","note":"非洲"},
],
"": [
{"en":"January","zh":"一月","note":"1月 / Jan"},
{"en":"February","zh":"二月","note":"2月 / Feb"},
{"en":"March","zh":"三月","note":"3月 / Mar"},
{"en":"April","zh":"四月","note":"4月 / Apr"},
{"en":"May","zh":"五月","note":"5月 / May"},
{"en":"June","zh":"六月","note":"6月 / Jun"},
{"en":"July","zh":"七月","note":"7月 / Jul"},
{"en":"August","zh":"八月","note":"8月 / Aug"},
{"en":"September","zh":"九月","note":"9月 / Sep"},
{"en":"October","zh":"十月","note":"10月 / Oct"},
{"en":"November","zh":"十一月","note":"11月 / Nov"},
{"en":"December","zh":"十二月","note":"12月 / Dec"},
],
"": [
{"en":"Monday","zh":"星期一","note":"Mon"},
{"en":"Tuesday","zh":"星期二","note":"Tue"},
{"en":"Wednesday","zh":"星期三","note":"Wed"},
{"en":"Thursday","zh":"星期四","note":"Thu"},
{"en":"Friday","zh":"星期五","note":"Fri"},
{"en":"Saturday","zh":"星期六","note":"Sat"},
{"en":"Sunday","zh":"星期日","note":"Sun"},
],
"动物": [
{"en":"dog","zh":"","note":"🐶"},
{"en":"cat","zh":"","note":"🐱"},
{"en":"lion","zh":"狮子","note":"🦁"},
{"en":"tiger","zh":"老虎","note":"🐯"},
{"en":"elephant","zh":"大象","note":"🐘"},
{"en":"bear","zh":"","note":"🐻"},
{"en":"monkey","zh":"猴子","note":"🐵"},
{"en":"giraffe","zh":"长颈鹿","note":"🦒"},
{"en":"zebra","zh":"斑马","note":"🦓"},
{"en":"wolf","zh":"","note":"🐺"},
{"en":"fox","zh":"狐狸","note":"🦊"},
{"en":"rabbit","zh":"兔子","note":"🐰"},
{"en":"horse","zh":"","note":"🐴"},
{"en":"cow","zh":"奶牛","note":"🐮"},
{"en":"pig","zh":"","note":"🐷"},
{"en":"sheep","zh":"","note":"🐑"},
{"en":"chicken","zh":"","note":"🐔"},
{"en":"duck","zh":"鸭子","note":"🦆"},
{"en":"penguin","zh":"企鹅","note":"🐧"},
{"en":"eagle","zh":"老鹰","note":"🦅"},
{"en":"parrot","zh":"鹦鹉","note":"🦜"},
{"en":"snake","zh":"","note":"🐍"},
{"en":"crocodile","zh":"鳄鱼","note":"🐊"},
{"en":"shark","zh":"鲨鱼","note":"🦈"},
{"en":"whale","zh":"鲸鱼","note":"🐋"},
{"en":"dolphin","zh":"海豚","note":"🐬"},
{"en":"frog","zh":"青蛙","note":"🐸"},
{"en":"butterfly","zh":"蝴蝶","note":"🦋"},
{"en":"bee","zh":"蜜蜂","note":"🐝"},
{"en":"spider","zh":"蜘蛛","note":"🕷️"},
],
"食物": [
{"en":"rice","zh":"米饭","note":"🍚"},
{"en":"noodles","zh":"面条","note":"🍜"},
{"en":"bread","zh":"面包","note":"🍞"},
{"en":"pizza","zh":"披萨","note":"🍕"},
{"en":"burger","zh":"汉堡","note":"🍔"},
{"en":"hot dog","zh":"热狗","note":"🌭"},
{"en":"sandwich","zh":"三明治","note":"🥪"},
{"en":"sushi","zh":"寿司","note":"🍣"},
{"en":"steak","zh":"牛排","note":"🥩"},
{"en":"chicken","zh":"鸡肉","note":"🍗"},
{"en":"fish","zh":"","note":"🐟"},
{"en":"egg","zh":"鸡蛋","note":"🥚"},
{"en":"salad","zh":"沙拉","note":"🥗"},
{"en":"soup","zh":"","note":"🍲"},
{"en":"dumpling","zh":"饺子","note":"🥟"},
{"en":"apple","zh":"苹果","note":"🍎"},
{"en":"banana","zh":"香蕉","note":"🍌"},
{"en":"orange","zh":"橙子","note":"🍊"},
{"en":"strawberry","zh":"草莓","note":"🍓"},
{"en":"watermelon","zh":"西瓜","note":"🍉"},
{"en":"grape","zh":"葡萄","note":"🍇"},
{"en":"mango","zh":"芒果","note":"🥭"},
{"en":"potato","zh":"土豆","note":"🥔"},
{"en":"tomato","zh":"西红柿","note":"🍅"},
{"en":"carrot","zh":"胡萝卜","note":"🥕"},
{"en":"cake","zh":"蛋糕","note":"🎂"},
{"en":"ice cream","zh":"冰淇淋","note":"🍦"},
{"en":"chocolate","zh":"巧克力","note":"🍫"},
{"en":"coffee","zh":"咖啡","note":""},
{"en":"tea","zh":"","note":"🍵"},
],
"职业": [
{"en":"doctor","zh":"医生","note":"🏥"},
{"en":"nurse","zh":"护士","note":"👩‍⚕️"},
{"en":"teacher","zh":"老师","note":"👩‍🏫"},
{"en":"engineer","zh":"工程师","note":"👨‍💻"},
{"en":"programmer","zh":"程序员","note":"💻"},
{"en":"designer","zh":"设计师","note":"🎨"},
{"en":"lawyer","zh":"律师","note":"⚖️"},
{"en":"judge","zh":"法官","note":"👨‍⚖️"},
{"en":"police","zh":"警察","note":"👮"},
{"en":"firefighter","zh":"消防员","note":"🚒"},
{"en":"soldier","zh":"士兵","note":"💂"},
{"en":"chef","zh":"厨师","note":"👨‍🍳"},
{"en":"waiter","zh":"服务员","note":"🍽️"},
{"en":"driver","zh":"司机","note":"🚗"},
{"en":"pilot","zh":"飞行员","note":"✈️"},
{"en":"sailor","zh":"水手","note":""},
{"en":"farmer","zh":"农民","note":"👨‍🌾"},
{"en":"scientist","zh":"科学家","note":"🔬"},
{"en":"artist","zh":"艺术家","note":"🎭"},
{"en":"singer","zh":"歌手","note":"🎤"},
{"en":"actor","zh":"演员","note":"🎬"},
{"en":"athlete","zh":"运动员","note":"🏅"},
{"en":"journalist","zh":"记者","note":"📰"},
{"en":"photographer","zh":"摄影师","note":"📷"},
{"en":"accountant","zh":"会计","note":"💰"},
{"en":"manager","zh":"经理","note":"👔"},
{"en":"secretary","zh":"秘书","note":"📋"},
{"en":"salesperson","zh":"销售员","note":"🛍️"},
{"en":"mechanic","zh":"机械师","note":"🔧"},
{"en":"electrician","zh":"电工","note":""},
],
}
# ── AI 分类(需要 AI 生成的) ──────────────────────────────────
AI_CATEGORIES = {
"基础": "Most common 100 English words for beginners.",
"推特": "Common slang and abbreviations used on Twitter/X.",
"游戏": "Essential vocabulary for gamers (UI, chat, mechanics).",
"生存": "Crucial phrases for living abroad (ordering, directions).",
}
app = FastAPI()
auth_scheme = HTTPBearer()
def verify_token(cred: HTTPAuthorizationCredentials = Depends(auth_scheme)):
token = cred.credentials
with sqlite3.connect(DB_FILE) as conn:
row = conn.execute("SELECT token FROM sessions WHERE token=?", (token,)).fetchone()
if not row: raise HTTPException(status_code=401, detail="未登录")
return token
async def ask_ai(system: str, user: str):
cfg = load_config()
provider = cfg.get("provider", "gemini")
api_key = cfg.get("api_key", "")
model = cfg.get("model", "")
timeout = 30.0
headers = {"Content-Type": "application/json"}
if provider in ["gemini", "deepseek", "groq", "openrouter", "openai"]:
endpoints = {
"gemini": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
"deepseek": "https://api.deepseek.com/chat/completions",
"groq": "https://api.groq.com/openai/v1/chat/completions",
"openrouter": "https://openrouter.ai/api/v1/chat/completions",
"openai": "https://api.openai.com/v1/chat/completions",
}
url = endpoints[provider]
headers["Authorization"] = f"Bearer {api_key}"
payload = {"model": model, "messages": [{"role":"system","content":system},{"role":"user","content":user}], "max_tokens": 2000}
async with httpx.AsyncClient(timeout=timeout) as client:
r = await client.post(url, headers=headers, json=payload)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
return "不支持的服务商"
# ── API ───────────────────────────────────────────────────────
@app.get("/")
async def read_index():
return HTMLResponse(content=open(HTML_FILE, encoding='utf-8').read())
@app.get("/Cibird.png")
async def get_icon():
icon_path = BASE_DIR / "Cibird.png"
if not icon_path.exists():
raise HTTPException(status_code=404, detail="图标文件不存在")
return FileResponse(str(icon_path), media_type="image/png")
@app.post("/api/login")
async def login(data: dict):
cfg = load_config()
pw_hash = hashlib.sha256(data.get("password", "").encode()).hexdigest()
if pw_hash != cfg.get("password_hash"):
raise HTTPException(status_code=401, detail="密码错误")
token = secrets.token_hex(16)
with sqlite3.connect(DB_FILE) as conn:
conn.execute("INSERT INTO sessions (token) VALUES (?)", (token,))
return {"token": token}
@app.get("/api/words")
async def list_words(q: str = "", token: str = Depends(verify_token)):
with sqlite3.connect(DB_FILE) as conn:
conn.row_factory = sqlite3.Row
sql = "SELECT * FROM words"
params = []
if q:
sql += " WHERE word LIKE ? OR meaning LIKE ? OR note LIKE ?"
params = [f"%{q}%", f"%{q}%", f"%{q}%"]
sql += " ORDER BY created DESC"
rows = conn.execute(sql, params).fetchall()
return [dict(r) for r in rows]
@app.post("/api/words")
async def add_word(data: dict, token: str = Depends(verify_token)):
word = data.get("word", "").strip()
if not word: return {"error": "Word is empty"}
system = "You are a helpful English teacher. Return ONLY JSON."
prompt = f"""Define '{word}'. Output JSON:
{{'word': '{word}', 'phonetic': '...', 'pos': '...', 'meaning': '...', 'examples': ['English example 1 (context: Twitter/Game)', 'English example 2 (context: Daily/Living)']}}
"""
try:
res = await ask_ai(system, prompt)
res_json = json.loads(res.strip('`').replace('json\n',''))
with sqlite3.connect(DB_FILE) as conn:
conn.execute("INSERT INTO words (word, meaning, phonetic, pos, examples) VALUES (?,?,?,?,?)",
(res_json['word'], res_json['meaning'], res_json['phonetic'], res_json['pos'], json.dumps(res_json['examples'])))
today = datetime.now().strftime('%Y-%m-%d')
conn.execute("INSERT INTO punch_cards(date_str, count) VALUES(?,1) ON CONFLICT(date_str) DO UPDATE SET count=count+1", (today,))
return {"success": True}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# 静态词库接口(月/周/动物/食物/职业)
@app.get("/api/static/{cat}")
async def get_static(cat: str, token: str = Depends(verify_token)):
if cat not in STATIC_DATA:
raise HTTPException(status_code=404, detail="分类不存在")
return {"items": STATIC_DATA[cat]}
# AI 词库接口(国家等需要 AI 的)
@app.get("/api/essentials/{cat}")
async def get_essentials(cat: str, token: str = Depends(verify_token)):
if cat not in AI_CATEGORIES: raise HTTPException(status_code=404)
system = "You are a world geography and language expert. Return ONLY JSON array of objects."
prompt = f"{AI_CATEGORIES[cat]} Output format: {{'items': [{{'en': '...', 'zh': '...', 'note': '...'}}, ...]}}"
try:
res = await ask_ai(system, prompt)
return json.loads(res.strip('`').replace('json\n',''))
except:
return {"items": []}
@app.get("/api/stats")
async def get_stats(token: str = Depends(verify_token)):
with sqlite3.connect(DB_FILE) as conn:
rows = conn.execute("SELECT date_str, count FROM punch_cards ORDER BY date_str DESC LIMIT 100").fetchall()
return {r[0]: r[1] for r in rows}
if __name__ == "__main__":
import uvicorn
init_db()
uvicorn.run(app, host="0.0.0.0", port=8848)
# ── 补全缺失接口 ──────────────────────────────────────────────
# AI 生成单词详情(添加单词用)
@app.post("/api/generate")
async def generate_word(data: dict, token: str = Depends(verify_token)):
word = data.get("word", "").strip()
if not word: raise HTTPException(status_code=400, detail="Word is empty")
system = "You are a helpful English teacher. Return ONLY valid JSON, no markdown."
prompt = f"""Define '{word}'. Return JSON exactly:
{{"word":"{word}","phonetic":"...","pos":"...","meaning":"中文释义","examples":[{{"en":"example sentence 1 (Twitter/Game context)","zh":"中文翻译1"}},{{"en":"example sentence 2 (Daily/Living context)","zh":"中文翻译2"}}]}}"""
try:
res = await ask_ai(system, prompt)
clean = res.strip().strip('`').removeprefix('json').strip()
return json.loads(clean)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# 更新单词例句
@app.patch("/api/words/{word_id}/examples")
async def update_examples(word_id: int, data: dict, token: str = Depends(verify_token)):
examples = data.get("examples", [])
with sqlite3.connect(DB_FILE) as conn:
conn.execute("UPDATE words SET examples=? WHERE id=?", (json.dumps(examples), word_id))
return {"success": True}
# 更新单词笔记
@app.patch("/api/words/{word_id}/note")
async def update_note(word_id: int, data: dict, token: str = Depends(verify_token)):
note = data.get("note", "")
with sqlite3.connect(DB_FILE) as conn:
conn.execute("UPDATE words SET note=? WHERE id=?", (note, word_id))
return {"success": True}
# 删除单词
@app.delete("/api/words/{word_id}")
async def delete_word(word_id: int, token: str = Depends(verify_token)):
with sqlite3.connect(DB_FILE) as conn:
conn.execute("DELETE FROM words WHERE id=?", (word_id,))
return {"success": True}
# 打卡统计
@app.get("/api/checkins")
async def get_checkins(token: str = Depends(verify_token)):
with sqlite3.connect(DB_FILE) as conn:
total = conn.execute("SELECT COUNT(*) FROM words").fetchone()[0]
rows = conn.execute("SELECT date_str, count FROM punch_cards ORDER BY date_str DESC LIMIT 100").fetchall()
today = datetime.now().strftime('%Y-%m-%d')
records = [{"date": r[0], "count": r[1]} for r in rows]
return {"total_words": total, "today": today, "records": records}
# 今日金句(从词库随机取一个词的例句)
@app.get("/api/daily-quote")
async def daily_quote(token: str = Depends(verify_token)):
today = datetime.now().strftime('%Y-%m-%d')
with sqlite3.connect(DB_FILE) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute("SELECT * FROM words WHERE examples != '[]' ORDER BY RANDOM() LIMIT 10").fetchall()
if not rows:
return {"word": "CiBird", "sentence_en": "Keep learning every day!", "sentence_zh": "每天坚持学习!", "date": today}
# 用日期做种子,同一天返回同一个词
seed = sum(ord(c) for c in today)
w = rows[seed % len(rows)]
exs = json.loads(w["examples"] or "[]")
ex = exs[0] if exs else {}
if isinstance(ex, str):
return {"word": w["word"], "sentence_en": ex, "sentence_zh": "", "date": today}
return {"word": w["word"], "sentence_en": ex.get("en",""), "sentence_zh": ex.get("zh",""), "date": today}
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
CiBird 词鸟 - 后端服务 v2.1
FastAPI + SQLite,支持多 AI 服务商
新增:国家练习模块
"""
import os, json, sqlite3, secrets, hashlib, time, random
from pathlib import Path
from contextlib import contextmanager
from datetime import datetime, timezone
from fastapi import FastAPI, HTTPException, Depends
from fastapi.responses import HTMLResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
import httpx
BASE_DIR = Path(__file__).parent
CONFIG_FILE = BASE_DIR / "config.json"
DB_FILE = BASE_DIR / "cibird.db"
HTML_FILE = BASE_DIR / "index.html"
def load_config():
if not CONFIG_FILE.exists():
raise RuntimeError("config.json 不存在")
with open(CONFIG_FILE) as f:
return json.load(f)
# ── DB ────────────────────────────────────────────────────────
def init_db():
with sqlite3.connect(DB_FILE) as conn:
conn.execute("""CREATE TABLE IF NOT EXISTS words (
id INTEGER PRIMARY KEY AUTOINCREMENT,
word TEXT NOT NULL, meaning TEXT, phonetic TEXT, pos TEXT,
examples TEXT DEFAULT '[]', note TEXT DEFAULT '',
created INTEGER DEFAULT (strftime('%s','now')))""")
conn.execute("""CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
created INTEGER DEFAULT (strftime('%s','now')))""")
conn.execute("""CREATE TABLE IF NOT EXISTS punch_cards (
date_str TEXT PRIMARY KEY,
count INTEGER DEFAULT 0)""")
# ── AI ────────────────────────────────────────────────────────
CATEGORIES = {
"基础": "Most common 100 English words for beginners.",
"推特": "Common slang and abbreviations used on Twitter/X.",
"游戏": "Essential vocabulary for gamers (UI, chat, mechanics).",
"生存": "Crucial phrases for living abroad (ordering, directions).",
"国家": "List of 195 countries in the world. Each item must follow format: {'en': 'Country Name', 'zh': '中文国名', 'note': 'Capital/Continent'}. Keep it accurate."
}
app = FastAPI()
auth_scheme = HTTPBearer()
def verify_token(cred: HTTPAuthorizationCredentials = Depends(auth_scheme)):
token = cred.credentials
with sqlite3.connect(DB_FILE) as conn:
row = conn.execute("SELECT token FROM sessions WHERE token=?", (token,)).fetchone()
if not row: raise HTTPException(status_code=401, detail="未登录")
return token
async def ask_ai(system: str, user: str):
cfg = load_config()
provider = cfg.get("provider", "gemini")
api_key = cfg.get("api_key", "")
model = cfg.get("model", "")
timeout = 30.0
headers = {"Content-Type": "application/json"}
if provider in ["gemini", "deepseek", "groq", "openrouter", "openai"]:
endpoints = {
"gemini": f"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
"deepseek": "https://api.deepseek.com/chat/completions",
"groq": "https://api.groq.com/openai/v1/chat/completions",
"openrouter": "https://openrouter.ai/api/v1/chat/completions",
"openai": "https://api.openai.com/v1/chat/completions",
}
url = endpoints[provider]
headers["Authorization"] = f"Bearer {api_key}"
payload = {"model": model, "messages": [{"role":"system","content":system},{"role":"user","content":user}], "max_tokens": 2000}
async with httpx.AsyncClient(timeout=timeout) as client:
r = await client.post(url, headers=headers, json=payload)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
return "不支持的服务商"
# ── API ───────────────────────────────────────────────────────
@app.get("/")
async def read_index():
return HTMLResponse(content=open(HTML_FILE, encoding='utf-8').read())
@app.post("/api/login")
async def login(data: dict):
cfg = load_config()
pw_hash = hashlib.sha256(data.get("password", "").encode()).hexdigest()
if pw_hash != cfg.get("password_hash"):
raise HTTPException(status_code=401, detail="密码错误")
token = secrets.token_hex(16)
with sqlite3.connect(DB_FILE) as conn:
conn.execute("INSERT INTO sessions (token) VALUES (?)", (token,))
return {"token": token}
@app.get("/api/words")
async def list_words(q: str = "", token: str = Depends(verify_token)):
with sqlite3.connect(DB_FILE) as conn:
conn.row_factory = sqlite3.Row
sql = "SELECT * FROM words"
params = []
if q:
sql += " WHERE word LIKE ? OR meaning LIKE ? OR note LIKE ?"
params = [f"%{q}%", f"%{q}%", f"%{q}%"]
sql += " ORDER BY created DESC"
rows = conn.execute(sql, params).fetchall()
return [dict(r) for r in rows]
@app.post("/api/words")
async def add_word(data: dict, token: str = Depends(verify_token)):
word = data.get("word", "").strip()
if not word: return {"error": "Word is empty"}
system = "You are a helpful English teacher. Return ONLY JSON."
prompt = f"""Define '{word}'. Output JSON:
{{'word': '{word}', 'phonetic': '...', 'pos': '...', 'meaning': '...', 'examples': ['English example 1 (context: Twitter/Game)', 'English example 2 (context: Daily/Living)']}}
"""
try:
res = await ask_ai(system, prompt)
res_json = json.loads(res.strip('`').replace('json\n',''))
with sqlite3.connect(DB_FILE) as conn:
conn.execute("INSERT INTO words (word, meaning, phonetic, pos, examples) VALUES (?,?,?,?,?)",
(res_json['word'], res_json['meaning'], res_json['phonetic'], res_json['pos'], json.dumps(res_json['examples'])))
# 打卡
today = datetime.now().strftime('%Y-%m-%d')
conn.execute("INSERT INTO punch_cards(date_str, count) VALUES(?,1) ON CONFLICT(date_str) DO UPDATE SET count=count+1", (today,))
return {"success": True}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/essentials/{cat}")
async def get_essentials(cat: str, token: str = Depends(verify_token)):
if cat not in CATEGORIES: raise HTTPException(status_code=404)
system = "You are a world geography and language expert. Return ONLY JSON array of objects."
prompt = f"{CATEGORIES[cat]} Output format: {{'items': [{{'en': '...', 'zh': '...', 'note': '...'}}, ...]}}"
try:
res = await ask_ai(system, prompt)
return json.loads(res.strip('`').replace('json\n',''))
except:
return {"items": []}
@app.get("/api/stats")
async def get_stats(token: str = Depends(verify_token)):
with sqlite3.connect(DB_FILE) as conn:
rows = conn.execute("SELECT date_str, count FROM punch_cards ORDER BY date_str DESC LIMIT 100").fetchall()
return {r[0]: r[1] for r in rows}
if __name__ == "__main__":
import uvicorn
init_db()
uvicorn.run(app, host="0.0.0.0", port=8848)