上传文件至「/」
This commit is contained in:
@@ -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
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -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
@@ -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}
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user