From c44d7a4b6336aa1b668d6936c46051d99e01df1e Mon Sep 17 00:00:00 2001 From: zhangyang Date: Thu, 14 May 2026 21:05:25 +0000 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=E3=80=8C/=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 84 +++++++ logo512.png | Bin 0 -> 18071 bytes server.py | 566 ++++++++++++++++++++++++++++++++++++++++++++++ server_full.py | 564 +++++++++++++++++++++++++++++++++++++++++++++ server_updated.py | 161 +++++++++++++ 5 files changed, 1375 insertions(+) create mode 100644 README.md create mode 100644 logo512.png create mode 100644 server.py create mode 100644 server_full.py create mode 100644 server_updated.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..074fc1a --- /dev/null +++ b/README.md @@ -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 运行日志 +``` + +--- + +*展翅高飞 🦜* diff --git a/logo512.png b/logo512.png new file mode 100644 index 0000000000000000000000000000000000000000..409e1b3b5a9b8566e2f586334e5e650fd08dcd6e GIT binary patch literal 18071 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 K&x$0C-u!Q$O$vqp literal 0 HcmV?d00001 diff --git a/server.py b/server.py new file mode 100644 index 0000000..8fc26fd --- /dev/null +++ b/server.py @@ -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) diff --git a/server_full.py b/server_full.py new file mode 100644 index 0000000..23d3207 --- /dev/null +++ b/server_full.py @@ -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} diff --git a/server_updated.py b/server_updated.py new file mode 100644 index 0000000..df14e78 --- /dev/null +++ b/server_updated.py @@ -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)