diff --git a/index.html b/index.html
index 2e8d897..74a0a27 100644
--- a/index.html
+++ b/index.html
@@ -27,19 +27,20 @@ body{
.sub{font-size:14px;color:rgba(0,0,0,0.4);margin-bottom:40px;letter-spacing:1px}
.links{display:flex;flex-direction:column;gap:14px;width:100%;max-width:360px}
.link-card{
- display:flex;align-items:center;gap:14px;padding:18px 20px;
+ display:flex;align-items:center;gap:14px;padding:28px 26px;
background:#fff;border-radius:14px;text-decoration:none;
box-shadow:0 2px 10px rgba(0,0,0,0.04);transition:transform 0.15s;
}
.link-card:active{transform:scale(0.97)}
-.link-card .li{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0}
-.link-card .lt{font-size:16px;font-weight:600;color:#333}
-.link-card .ld{font-size:12px;color:#999;margin-top:2px}
+.link-card .li{width:62px;height:62px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:32px;flex-shrink:0}
+.link-card .lt{font-size:24px;font-weight:600;color:#333}
+.link-card .ld{font-size:16px;color:#999;margin-top:2px}
.c1 .li{background:#FFECEC;color:var(--pri)}
.c2 .li{background:#E6F0FF;color:#4A90E2}
.c3 .li{background:#E6FFE8;color:#5CB85C}
.c4 .li{background:#FFF8E6;color:#E8985A}
.c5 .li{background:#FFF5EC;color:#C8923A}
+.c6 .li{background:#F3E8FF;color:#8B5CF6}
@@ -68,6 +69,15 @@ body{
).*?(?=
)' | sed 's/<[^>]*>//g' | head -1 | python3 -c "import sys; print(json.dumps(sys.stdin.read().strip()[:500], ensure_ascii=False))" 2>/dev/null || echo '""')
+
+ # Write entry
+ [ "$first" = true ] && first=false || echo ',' >> "$OUT"
+ cat >> "$OUT" << ENTRY
+ {
+ "slug": "$slug",
+ "name": $(echo "$name" | python3 -c "import sys; print(json.dumps(sys.stdin.read().strip(), ensure_ascii=False))"),
+ "category": "$cat",
+ "url": "$url",
+ "description": $desc,
+ "question_count": $qcount,
+ "questions": $qjson,
+ "options": $ojson
+ }
+ENTRY
+
+ count=$((count+1))
+ echo " ✓ ($qcount题)"
+done
+
+echo ']' >> "$OUT"
+echo "Done! $count scales saved to $OUT"
diff --git a/server.py b/server.py
index 34c4fee..d0c9401 100644
--- a/server.py
+++ b/server.py
@@ -1,80 +1,615 @@
#!/usr/bin/env python3
-"""Minimal HTTP server for digital-psychology — serves static files + mindmap persistence."""
-import json, os, sys
+"""Digital Psychology server — static files + mindmap + scale library API."""
+import json, os, sys, sqlite3, time, uuid
from http.server import HTTPServer, SimpleHTTPRequestHandler
+from urllib.parse import urlparse, parse_qs
ROOT = os.path.dirname(os.path.abspath(__file__))
MINDMAP_FILE = os.path.join(ROOT, "mindmap_data.json")
+SCALES_FILE = os.path.join(ROOT, "scales_data.json")
+RESULTS_DB = os.path.join(ROOT, "scale_results.db")
+
+# ---- SQLite setup ----
+def get_db():
+ db = sqlite3.connect(RESULTS_DB)
+ db.row_factory = sqlite3.Row
+ return db
+
+def init_db():
+ db = get_db()
+ db.execute("""
+ CREATE TABLE IF NOT EXISTS results (
+ id TEXT PRIMARY KEY,
+ scale_slug TEXT NOT NULL,
+ scale_name TEXT NOT NULL,
+ answers TEXT NOT NULL,
+ scores TEXT,
+ created_at TEXT NOT NULL,
+ user_agent TEXT,
+ ip TEXT
+ )
+ """)
+ db.commit()
+ db.close()
+
+init_db()
+
+# ============================================================
+# Scoring engine — known rules for major scales
+# ============================================================
+SCORE_SCALE = {
+ # (option_start, reverse_items, transform, rating_thresholds, factor_map)
+ # option_start: 0=0-indexed (PHQ/GAD), 1=1-indexed (SDS/SAS/SCL-90)
+ # reverse_items: 1-indexed question numbers to reverse-score
+ # transform: (multiplier, round) applied to raw score, or None
+ # rating_thresholds: [(max_score, label), ...]
+ # factor_map: {factor_name: [1-indexed question numbers]}
+}
+
+SCORING = {
+ # === 抑郁症测试 ===
+ "yyzcs": { # SDS 抑郁自评量表
+ "name": "抑郁自评量表(SDS)",
+ "option_base": 1, # 1-4
+ "reverse": [2, 5, 6, 11, 12, 14, 16, 17, 18, 20],
+ "transform": (1.25, 0), # std = raw * 1.25, rounded
+ "ratings": [
+ (52, "可能没有抑郁"),
+ (62, "可能轻度抑郁"),
+ (72, "可能中度抑郁"),
+ (100, "可能重度抑郁"),
+ ],
+ "factors": {
+ "精神性情感症状": [1, 3],
+ "躯体性障碍": [2, 4, 5, 6, 7, 8, 9, 10],
+ "精神运动性障碍": [12, 13],
+ "抑郁的心理障碍": [11, 14, 15, 16, 17, 18, 19, 20],
+ },
+ "use_std": True,
+ },
+ "yyz": { # BDI-II 贝克抑郁量表
+ "name": "贝克抑郁量表(BDI-II)",
+ "option_base": 0, # 0-3
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (13, "无抑郁或极轻微"),
+ (19, "轻度抑郁"),
+ (28, "中度抑郁"),
+ (63, "重度抑郁"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "yy9": { # 中学生抑郁自评量表
+ "name": "中学生抑郁自评量表",
+ "option_base": 1,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (39, "可能没有抑郁"),
+ (47, "可能轻度抑郁"),
+ (55, "可能中度抑郁"),
+ (80, "可能重度抑郁"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "yy4": { # CES-D 流调用抑郁量表
+ "name": "流调用抑郁量表(CES-D)",
+ "option_base": 0,
+ "reverse": [4, 8, 12, 16],
+ "transform": None,
+ "ratings": [
+ (15, "无抑郁症状"),
+ (19, "可能有抑郁倾向"),
+ (24, "可能有抑郁症状"),
+ (60, "可能有严重抑郁症状"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "yy6": { # PHQ-9
+ "name": "抑郁症筛查量表(PHQ-9)",
+ "option_base": 0,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (4, "无抑郁"),
+ (9, "轻度抑郁"),
+ (14, "中度抑郁"),
+ (19, "中重度抑郁"),
+ (27, "重度抑郁"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "yy7": { # CDI 儿童青少年抑郁量表
+ "name": "儿童青少年抑郁量表(CDI)",
+ "option_base": 0,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (19, "无抑郁"),
+ (29, "轻度抑郁"),
+ (39, "中度抑郁"),
+ (54, "重度抑郁"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "yy8": { # DSRSC 儿童抑郁障碍自评
+ "name": "儿童抑郁障碍自评量表(DSRSC)",
+ "option_base": 0,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (13, "正常"),
+ (17, "可能有抑郁"),
+ (36, "很可能有抑郁"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "yy10": { # GDS 老年抑郁量表
+ "name": "老年抑郁量表(GDS)",
+ "option_base": 0,
+ "reverse": [1, 5, 7, 9, 15, 19, 21, 27, 29, 30],
+ "transform": None,
+ "ratings": [
+ (10, "正常"),
+ (19, "轻度抑郁"),
+ (30, "重度抑郁"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "yy11": { # EPDS 爱丁堡产后抑郁
+ "name": "爱丁堡产后抑郁量表(EPDS)",
+ "option_base": 0,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (9, "正常"),
+ (12, "可能存在抑郁"),
+ (30, "很可能存在抑郁"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "yy12": { # HAD 焦虑抑郁量表
+ "name": "焦虑抑郁量表(HAD)",
+ "option_base": 0,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (7, "正常"),
+ (10, "临界"),
+ (21, "异常"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "yy13": { # HAMD 汉密尔顿抑郁
+ "name": "汉密尔顿抑郁量表(HAMD)",
+ "option_base": 0,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (7, "正常"),
+ (17, "可能有轻中度抑郁"),
+ (24, "可能有重度抑郁"),
+ (72, "可能有极重度抑郁"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ # === 焦虑症测试 ===
+ "jl1": { # SAS 焦虑自评量表
+ "name": "焦虑自评量表(SAS)",
+ "option_base": 1,
+ "reverse": [5, 9, 13, 17, 19],
+ "transform": (1.25, 0),
+ "ratings": [
+ (49, "可能没有焦虑"),
+ (59, "可能轻度焦虑"),
+ (69, "可能中度焦虑"),
+ (100, "可能重度焦虑"),
+ ],
+ "factors": {},
+ "use_std": True,
+ },
+ "jlz": { # BAI 贝克焦虑量表
+ "name": "贝克焦虑测试量表(BAI)",
+ "option_base": 0,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (7, "无焦虑"),
+ (15, "轻度焦虑"),
+ (25, "中度焦虑"),
+ (63, "重度焦虑"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "jl5": { # GAD-7
+ "name": "焦虑症筛查量表(GAD-7)",
+ "option_base": 0,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (4, "无焦虑"),
+ (9, "轻度焦虑"),
+ (14, "中度焦虑"),
+ (21, "重度焦虑"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "jl6": { # TAS 考试焦虑
+ "name": "考试焦虑量表(TAS)",
+ "option_base": 0,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (12, "较低考试焦虑"),
+ (20, "中等考试焦虑"),
+ (37, "较高考试焦虑"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ "jl8": { # 中学生焦虑自评
+ "name": "中学生焦虑自评量表",
+ "option_base": 1,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (39, "可能没有焦虑"),
+ (48, "可能轻度焦虑"),
+ (56, "可能中度焦虑"),
+ (80, "可能重度焦虑"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ # === 强迫症测试 ===
+ "qp1": { # YBOCS
+ "name": "耶鲁布朗强迫标准量表(YBOCS)",
+ "option_base": 0,
+ "reverse": [],
+ "transform": None,
+ "ratings": [
+ (7, "亚临床"),
+ (15, "轻度"),
+ (23, "中度"),
+ (31, "重度"),
+ (40, "极重度"),
+ ],
+ "factors": {},
+ "use_std": False,
+ },
+ # === 人格测试 ===
+ "xl16": { # NEO-FFI 大五人格
+ "name": "大五人格测试(NEO-FFI)",
+ "option_base": 1,
+ "reverse": [], # varies by factor
+ "transform": None,
+ "ratings": [], # factor-based
+ "factors": {
+ "神经质": [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56], # approximate
+ "外向性": [2, 7, 12, 17, 22, 27, 32, 37, 42, 47, 52, 57],
+ "开放性": [3, 8, 13, 18, 23, 28, 33, 38, 43, 48, 53, 58],
+ "宜人性": [4, 9, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59],
+ "尽责性": [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60],
+ },
+ "use_std": False,
+ },
+ # === 智商/天赋测试 ===
+ "duoyuan80": {
+ "name": "加德纳多元智能测试(MI-80)",
+ "option_base": 1, # 1-5
+ "num_options": 5,
+ "reverse": [],
+ "transform": None,
+ "ratings": [], # factor-based, no global rating
+ "factors": {
+ "语言智能": [1,2,3,4,5,6,7,8,9,10],
+ "逻辑数学智能": [11,12,13,14,15,16,17,18,19,20],
+ "空间智能": [21,22,23,24,25,26,27,28,29,30],
+ "身体运动智能": [31,32,33,34,35,36,37,38,39,40],
+ "音乐智能": [41,42,43,44,45,46,47,48,49,50],
+ "自然观察智能": [51,52,53,54,55,56,57,58,59,60],
+ "人际智能": [61,62,63,64,65,66,67,68,69,70],
+ "内省智能": [71,72,73,74,75,76,77,78,79,80],
+ },
+ "use_std": False,
+ },
+}
+
+
+def calculate_score(slug, answers):
+ """Calculate scores with known rules, fall back to raw sum."""
+ scoring = SCORING.get(slug)
+ num_options = scoring.get("num_options", 4) if scoring else 4
+
+ if scoring:
+ base = scoring["option_base"]
+ reverse_items = set(scoring["reverse"])
+ raw = 0
+ for i, ans in enumerate(answers):
+ if ans is None:
+ continue
+ q_num = i + 1 # 1-indexed question number
+ opt_idx = int(ans)
+ if q_num in reverse_items:
+ # Reverse: last option → first score
+ raw += base + (num_options - 1 - opt_idx)
+ else:
+ raw += base + opt_idx
+
+ # Factor scores
+ factors = {}
+ if scoring.get("factors"):
+ for fname, qnums in scoring["factors"].items():
+ f_raw = 0
+ f_count = 0
+ for qn in qnums:
+ idx = qn - 1
+ if idx < len(answers) and answers[idx] is not None:
+ opt_idx = int(answers[idx])
+ if qn in reverse_items:
+ f_raw += base + (num_options - 1 - opt_idx)
+ else:
+ f_raw += base + opt_idx
+ f_count += 1
+ if f_count > 0:
+ factors[fname] = f_raw
+
+ # Transform (e.g. SDS std = raw * 1.25)
+ std_score = None
+ if scoring.get("transform"):
+ mult, rnd = scoring["transform"]
+ std_score = round(raw * mult) if rnd == 0 else int(raw * mult)
+
+ used_score = std_score if (scoring.get("use_std") and std_score is not None) else raw
+
+ # Rating
+ rating = ""
+ for threshold, label in scoring.get("ratings", []):
+ if used_score <= threshold:
+ rating = label
+ break
+ if not rating and scoring.get("ratings"):
+ rating = scoring["ratings"][-1][1]
+
+ max_raw = len([a for a in answers if a is not None]) * (base + num_options - 1)
+
+ # Max per factor (for front-end bar charts)
+ factor_max = {}
+ if scoring.get("factors"):
+ for fname, qnums in scoring["factors"].items():
+ factor_max[fname] = len(qnums) * (base + num_options - 1)
+
+ # Per-question scores (1-indexed options: 选项A=1分, B=2分, ...)
+ question_scores = []
+ for i, ans in enumerate(answers):
+ if ans is None:
+ question_scores.append(None)
+ elif (i + 1) in reverse_items:
+ question_scores.append(base + (num_options - 1 - int(ans)))
+ else:
+ question_scores.append(base + int(ans))
+
+ return {
+ "raw_score": raw,
+ "std_score": std_score,
+ "max_score": max_raw,
+ "rating": rating,
+ "factors": factors,
+ "factor_max": factor_max,
+ "answer_count": len([a for a in answers if a is not None]),
+ "question_scores": question_scores,
+ }
+ else:
+ # Generic: option index + 1
+ raw = sum(int(a) + 1 for a in answers if a is not None)
+ question_scores = [(int(a) + 1) if a is not None else None for a in answers]
+ return {
+ "raw_score": raw,
+ "std_score": None,
+ "max_score": len([a for a in answers if a is not None]) * num_options,
+ "rating": "",
+ "factors": {},
+ "factor_max": {},
+ "answer_count": len([a for a in answers if a is not None]),
+ "question_scores": question_scores,
+ }
+
class Handler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=ROOT, **kwargs)
def end_headers(self):
- # no-cache for HTML and JSON endpoints
- if self.path == '/' or self.path.endswith('.html') or self.path == '/load':
+ no_cache_paths = ['/', '/load', '/api/']
+ if any(self.path == p or self.path.startswith(p) for p in no_cache_paths):
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.send_header("Pragma", "no-cache")
self.send_header("Expires", "0")
super().end_headers()
+ def _json(self, data, code=200):
+ self.send_response(code)
+ self.send_header("Content-Type", "application/json; charset=utf-8")
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.end_headers()
+ self.wfile.write(json.dumps(data, ensure_ascii=False).encode())
+
+ def _read_body(self):
+ length = int(self.headers.get("Content-Length", 0))
+ return self.rfile.read(length) if length else b""
+
+ # ==== API routing ====
def do_GET(self):
- if self.path == "/load":
- if os.path.exists(MINDMAP_FILE):
- with open(MINDMAP_FILE, "r", encoding="utf-8") as f:
- data = f.read()
- self.send_response(200)
- self.send_header("Content-Type", "application/json")
- self.end_headers()
- self.wfile.write(data.encode())
- else:
- self.send_response(200)
- self.send_header("Content-Type", "application/json")
- self.end_headers()
- self.wfile.write(b"{}")
- else:
- super().do_GET()
+ path = urlparse(self.path).path
+
+ # Scale API
+ if path == "/api/scales":
+ return self.api_scales_list()
+ if path.startswith("/api/scales/") and not path.startswith("/api/scales/result"):
+ slug = path.split("/api/scales/")[1]
+ return self.api_scale_detail(slug)
+ if path == "/api/scales/results":
+ return self.api_results_list()
+ if path.startswith("/api/scales/results/"):
+ rid = path.split("/api/scales/results/")[1]
+ return self.api_result_detail(rid)
+
+ # Mindmap
+ if path == "/load":
+ return self.mindmap_load()
+
+ return super().do_GET()
def do_POST(self):
- if self.path == "/save":
- length = int(self.headers.get("Content-Length", 0))
- data = self.rfile.read(length)
- try:
- json.loads(data)
- except json.JSONDecodeError:
- self.send_error(400, "Invalid JSON")
- return
- with open(MINDMAP_FILE, "w", encoding="utf-8") as f:
- f.write(data.decode())
+ path = urlparse(self.path).path
+
+ # Scale result submission
+ if path == "/api/scales/result":
+ return self.api_submit_result()
+
+ # Mindmap save
+ if path == "/save":
+ return self.mindmap_save()
+
+ return super().do_POST()
+
+ def do_OPTIONS(self):
+ self.send_response(204)
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+ self.end_headers()
+
+ # ---- Scale APIs ----
+ def api_scales_list(self):
+ if not os.path.exists(SCALES_FILE):
+ return self._json({"error": "No scales data"}, 404)
+ with open(SCALES_FILE, "r", encoding="utf-8") as f:
+ scales = json.load(f)
+ # Return lightweight list (no questions)
+ result = []
+ for s in scales:
+ result.append({
+ "slug": s["slug"], "name": s["name"], "category": s["category"],
+ "question_count": s["question_count"], "description": s.get("description", ""),
+ })
+ self._json(result)
+
+ def api_scale_detail(self, slug):
+ if not os.path.exists(SCALES_FILE):
+ return self._json({"error": "No scales data"}, 404)
+ with open(SCALES_FILE, "r", encoding="utf-8") as f:
+ scales = json.load(f)
+ for s in scales:
+ if s["slug"] == slug:
+ return self._json(s)
+ self._json({"error": "Scale not found"}, 404)
+
+ def api_submit_result(self):
+ body = self._read_body()
+ try:
+ data = json.loads(body)
+ except json.JSONDecodeError:
+ return self._json({"error": "Invalid JSON"}, 400)
+
+ slug = data.get("scale_slug", "")
+ scale_name = data.get("scale_name", "")
+ answers = data.get("answers", []) # list of option indices
+
+ if not slug or not answers:
+ return self._json({"error": "Missing scale_slug or answers"}, 400)
+
+ rid = str(uuid.uuid4())[:8]
+ created = time.strftime("%Y-%m-%d %H:%M:%S")
+
+ # Calculate score using known rules
+ scores = calculate_score(slug, answers)
+ # Get the scale config for display name
+ scale_config = SCORING.get(slug, {})
+ scores["scale_name"] = scale_name
+ scores["scale_slug"] = slug
+
+ db = get_db()
+ db.execute(
+ "INSERT INTO results (id, scale_slug, scale_name, answers, scores, created_at, user_agent, ip) VALUES (?,?,?,?,?,?,?,?)",
+ (rid, slug, scale_name, json.dumps(answers), json.dumps(scores), created,
+ self.headers.get("User-Agent", ""), self.client_address[0])
+ )
+ db.commit()
+ db.close()
+
+ self._json({"id": rid, "scores": scores, "created_at": created})
+
+ def api_results_list(self):
+ db = get_db()
+ rows = db.execute("SELECT id, scale_slug, scale_name, scores, created_at FROM results ORDER BY created_at DESC LIMIT 50").fetchall()
+ db.close()
+ results = []
+ for r in rows:
+ results.append({
+ "id": r["id"], "scale_slug": r["scale_slug"], "scale_name": r["scale_name"],
+ "scores": json.loads(r["scores"]), "created_at": r["created_at"],
+ })
+ self._json(results)
+
+ def api_result_detail(self, rid):
+ db = get_db()
+ r = db.execute("SELECT * FROM results WHERE id = ?", (rid,)).fetchone()
+ db.close()
+ if not r:
+ return self._json({"error": "Not found"}, 404)
+ self._json({
+ "id": r["id"], "scale_slug": r["scale_slug"], "scale_name": r["scale_name"],
+ "answers": json.loads(r["answers"]), "scores": json.loads(r["scores"]),
+ "created_at": r["created_at"],
+ })
+
+ # ---- Mindmap (existing) ----
+ def mindmap_load(self):
+ if os.path.exists(MINDMAP_FILE):
+ with open(MINDMAP_FILE, "r", encoding="utf-8") as f:
+ data = f.read()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
- self.wfile.write(b'{"ok":true}')
- elif self.path == "/load":
- if os.path.exists(MINDMAP_FILE):
- with open(MINDMAP_FILE, "r", encoding="utf-8") as f:
- data = f.read()
- self.send_response(200)
- self.send_header("Content-Type", "application/json")
- self.end_headers()
- self.wfile.write(data.encode())
- else:
- self.send_response(200)
- self.send_header("Content-Type", "application/json")
- self.end_headers()
- self.wfile.write(b"{}")
+ self.wfile.write(data.encode())
else:
- self.send_error(404)
+ self._json({})
+
+ def mindmap_save(self):
+ data = self._read_body()
+ try:
+ json.loads(data)
+ except json.JSONDecodeError:
+ self.send_error(400, "Invalid JSON")
+ return
+ with open(MINDMAP_FILE, "w", encoding="utf-8") as f:
+ f.write(data.decode())
+ self._json({"ok": True})
def log_message(self, format, *args):
- if "/save" in str(args) or "/load" in str(args):
+ if any(x in str(args) for x in ["/save", "/load", "/api/"]):
print(f"[{self.log_date_time_string()}] {args[0]}")
+
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8001
server = HTTPServer(("127.0.0.1", port), Handler)
- server.socket.settimeout(30) # 30s timeout so hung clients don't block the single-thread server
- print(f"Serving {ROOT} on port {port} (save/load enabled)")
+ server.socket.settimeout(30)
+ print(f"愈心谷 server on :{port} (scales API enabled)")
try:
server.serve_forever()
except KeyboardInterrupt:
diff --git a/xingjing.html b/xingjing.html
new file mode 100644
index 0000000..68c72c9
--- /dev/null
+++ b/xingjing.html
@@ -0,0 +1,492 @@
+
+
+
+
+
+
+
+
+
+
星镜 · 动态视频化性格&星盘测试
+
+
+
+
+
+
+
+
+
+
+
+
+
+
愈心谷 · 产品设计档案
+
星 镜
+
动态视频化性格&星盘测试平台
让每一次自我探索,都成为可播放的电影
+
信心指数 · 91%
+
+
+
+
+
产品定位
+
+
+ 年轻人自我探索平台——星盘、性格类型、人格测评,全部支持动态动画与AI生成个人专属短视频报告。
+ 对标「测测」以图文静态报告和图文社区为主,星镜的核心差异化在于:动态可视化星盘、AI数字人视频解读、短视频化测试结果、视频社区、真人短视频咨询,把传统文字报告变成可看、可播放、可二次剪辑分享的视频内容。
+
+
+
+
+
+
+
测测现存痛点
+
+
+ 全部文字长报告 · 阅读门槛高
+ 社区仅图文 · 缺少短视频沉浸感
+ 报告不便于社交传播
+ 解读冰冷 · 缺少情绪氛围感
+
+
+
+
+
+
+
模块一 王牌
+
测试完成 → 自动生成个人动态视频报告
+
+
01
+
🎬
+
个人专属短视频报告
+
+ 用户输入生辰、地点做星盘,或完成性格测试后,输出两套结果:传统文字报告 + 自动生成专属短视频。
+ • 动态星盘动画:行星转动、相位连线、宫位切换特效,星空背景流转
+ • AI数字人出镜讲解:根据本命盘/测试结果,数字人主播口述性格天赋、情绪短板、感情模式、流年提示
+ • 免费版:30-45秒短视频(带水印);付费完整版:2-4分钟完整解读
+ • 一键保存本地,可加字幕和背景音乐,直接分享社交平台
+
+
对比测测:测测只有图文海报,星镜产出可播放的个性化视频,自带裂变传播属性
+
+
+
+
+
+
模块二
+
动态可视化星盘引擎
+
+
02
+
🌌
+
三维动态星盘
+
+ • 可拖拽旋转星空,行星随真实天象实时缓慢运转
+ • 点击任意行星,弹出动画+语音讲解(非静态图片)
+ • 行运动态对比:播放动画演示行星移动对本命盘相位变化,直观看懂水逆、木星过境
+ • 合盘动态动画:两人星盘叠加演示缘分相位
+
+
降低占星理解门槛,把抽象相位关系变成可视动画
+
+
+
+
+
+
模块三
+
短视频化社区(替代测测微博式图文社区)
+
+
03
+
📱
+
短视频优先信息流
+
+ • 首页信息流优先短视频:占星师、心理博主发布测评解读短视频
+ • 用户可发布个人测试短视频报告到社区,带标签
+ • 支持评论、合拍、二次剪辑
+ • 达人供给侧:占星师、心理成长博主入驻,积攒粉丝,承接付费咨询
+ • 同时保留图文帖子,视频优先、图文为辅
+
+
+
+
+
+
+
模块四
+
咨询体系升级(视频优先,兼容语音/图文)
+
+
04
+
💬
+
视频 + 异步 + 传统,全矩阵
+
+ • 视频一对一咨询:占星师/心理师视频通话解读
+ • 预约预制短视频解读:用户提交问题,达人录制专属短视频回复(低压力、产能更高)
+ • 保留语音、图文咨询,形成完整矩阵
+
+
+
+
+
+
+
模块五
+
互动式动态测试
+
+
05
+
🎮
+
告别静态答题
+
+ • 部分测评加入短情景视频片段作为题干,观看情景后选择反应
+ • 测试潜意识性格、亲密关系模式
+ • 答题过程伴随轻量动画反馈,提升沉浸感
+
+
+
+
+
+
+
模块六
+
AI陪伴动态卡片
+
+
06
+
✨
+
每日动态短视频运势
+
+ • 每日运势不再是一段静态文字
+ • AI生成几十秒动态短视频运势卡片,数字人播报今日提示
+ • 支持桌面小组件播放简易动态星象
+
+
+
+
+
+
+
商业模式
+
沿用行业成熟三层变现,叠加视频新增收入
+
+
+
+
第一层 · 免费流量层
+
拉新钩子
+
+ - 免费基础排盘、基础性格测试
+ - 生成30秒精简版AI数字人短视频报告(带水印,鼓励分享裂变)
+ - 免费每日动态短视频运势;基础社区浏览
+
+
核心策略:短视频报告向外分享到抖音/小红书,带来外部回流下载,实现社交裂变——这是对比测测新增的获客通路
+
+
+
+
+
第二层 · 会员订阅
+
稳定现金流
+
首月 ¥9.9 | 月 ¥24 | 年 ¥168
+
+ - 解锁无水印完整 1-4 分钟个人专属视频报告,不限次数
+ - 高清三维动态星盘全部功能
+ - 无广告,全部深度文字报告解锁
+ - AI数字人多风格切换(理性心理向 / 氛围感占星向)
+ - 会员专属短视频课程库
+
+
+
+
+
+
第三层 · 单次付费
+
小额转化
+
¥9.9 - ¥49.9
+
+ - 专项短视频报告:¥19.9「流年事业视频解读」、¥24.9「感情合盘视频报告」
+ - 专题心理测评包,输出视频结果
+ - 保留传统文字报告,给不爱视频的用户备选
+
+
+
+
+
+
第四层 · 高客单达人服务
+
利润大头
+
平台抽成 35%-45%
+
+ - 视频一对一咨询(主力新形态)、语音、图文咨询
+ - 定制录制专属短视频解读:达人录制 5-15 分钟定制视频
+ - 达人付费短视频专栏课程(占星、性格成长)
+
+
+
+
+
+
新增创新商业化(测测不具备)
+
+ • 视频报告增值模板商店:付费购买不同风格视频模板(星空治愈、极简心理风、酷感潮酷风),几块钱单次渲染
+ • B端对外接口:把「生辰生成个性化视频报告」能力对外输出,给其他心理、社交应用做能力授权
+
+
+
+
+
+
+
用户体验完整闭环
+
+
+
1
+
新用户下载 → 输入生辰 / 做性格测试
+
+
+
2
+
得到结果:查看文字报告 | 播放我的专属视频报告
+
+
+
3
+
免费版短视频 → 提示开通会员解锁完整版无水印视频
+
+
+
4
+
用户把短视频分享小红书/抖音 → 带来新用户回流下载
+
+
+
+
6
+
小额单次报告 → 开通会员 → 预约达人视频一对一咨询
+
+
+
+
+
+
+
优劣势对比
+
+
+
+ | 测测 | 星镜 |
+
+
+ | 报告载体 |
+ 图文+海报,静态 |
+ AI个性化短视频 + 动态三维星盘,保留图文 |
+
+
+ | 社区形态 |
+ 微博式图文为主 |
+ 短视频信息流优先,图文为辅,支持用户发布个人测试视频 |
+
+
+ | 对外裂变 |
+ 只能图片海报传播 |
+ 短视频自带社交平台传播能力,外部引流更强 |
+
+
+ | 咨询形态 |
+ 图文、语音连麦为主 |
+ 视频咨询 + 达人定制短视频回复,同时兼容语音图文 |
+
+
+ | 核心短板 |
+ 文字阅读负担重,传播弱 |
+ AI视频生成有算力成本;需要做好内容风控 |
+
+
+
+
+
+
+
+
核心风险与规避
+
+
内容合规
+
产品定位为心理娱乐、自我探索工具,不做宿命论算命;解读偏向性格分析、行为模式、心理成长,弱化"预测吉凶"表述;明确提示:不替代心理咨询及医院就诊。
+
+
+
成本控制
+
大量用AI数字人 + 模板化动画引擎——把星盘/测试数据映射到大模型输出脚本,再驱动数字人生成视频;鼓励达人上传预制短视频内容承接咨询。
+
+
+
兼容存量用户
+
不完全砍掉图文模式,给用户开关选择「视频版 / 传统文字版」报告,照顾不喜欢视频的人群。
+
+
+
+
+
+
最小可落地版本
+
优先实现,无需一次性做完全部
+
+
+
+
+
← 返回愈心谷首页
+
+
+
diff --git a/xingjing_app.html b/xingjing_app.html
new file mode 100644
index 0000000..9043a16
--- /dev/null
+++ b/xingjing_app.html
@@ -0,0 +1,1537 @@
+
+
+
+
+
+
星镜
+
+
+
+
+
+
+
星镜
+
+
+
+
+
+
+
+
今日星象
+
🔭
+
探索你的星盘
+
发现内在的宇宙
+
✦ 开始星盘测试
+
+
+
+
★★★★☆
+
💛 幸运色:— · 幸运数字:—
+
+
+
+
+
热门解读
+
+
+
+
+
+
← 返回首页
+
+
🔭
+
输入出生信息
+
解锁专属星盘与性格解读
+
+
+
+ 年
+
+
+
+
+
+ 时
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
点击行星查看详情
+
+
+ ◎ 实时天象
+ 拖拽旋转 · 双指缩放 · 点击行星
+
+
+
+
+
+
+
+
+
+
+
+
+
成就徽章
+
+
会员订阅
+
免费版
¥0 /月
✓ 基础排盘 ✓ 30秒带水印报告 ✓ 每日运势 ✓ 社区浏览
+
星镜会员
¥24 /月 首月¥9.9
✓ 无水印完整视频报告 ✓ 高清3D星盘 ✓ AI数字人多风格 ✓ 专属课程库 ✓ 不限次数生成
+
年度会员
¥168 /年 省 ¥120
✓ 包含全部月会员权益 + 年度专属深度报告
+
预约达人咨询
+
+
+
+
+
+
+
+
+
+
← 返回首页
+
+
🧊
+
舒尔特前额叶训练
+
按顺序点击 1→25,训练专注力与反应速度
+
+
+
00:00.0
+
+
+
拖拽旋转 · 点击数字按序消除
+
+
+
+
+
← 返回首页
+
+
💫
+
双人合盘 · 缘分配对
+
输入两人的出生信息,解锁星座契合度分析
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 👤 TA
+
+
+
年
+
+
+
+
+
+
+
+
+
⌂首页
+
◎星盘
+
▶报告
+
▤社区
+
◍我的
+
+
+
+
+
+
+
+
+
⭐
开通星镜会员
+
解锁无水印完整视频报告、高清3D星盘、专属课程等全部权益。
首月仅需 ¥9.9
+
+
+
+
+
+
+
+
diff --git a/yuxingu.html b/yuxingu.html
index a817e47..57109d8 100644
--- a/yuxingu.html
+++ b/yuxingu.html
@@ -12,10 +12,11 @@
-
愈心谷 · 心理健康服务
+
愈心谷
@@ -156,24 +217,119 @@ body{
-
-
专业的心理健康服务平台
+
+
愈心谷数字解构
-
+
+
+ 输入生日,更加了解自己 →
+
+
+
-
-
🔮 愈心解码
-
输入生日,解锁你的身心密码
-
-
-
年
-
-
月
-
-
日
-
+
+ 认识自己
+
+
+
+
+
+
+
+
+
@@ -193,52 +349,32 @@ function goDecode(){
});
-
-
-
- 愈心魔方
- 愈见魔方,遇见自己
-
-
-

-
+
+
+
+ 更多服务
+
-
-
+
+
-
-
-
@@ -251,8 +387,8 @@ function goDecode(){
⌂首页
-
◉心理咨询
-
☰心理测评
+
◉认识自己
+
☰专业测评
✦私域活动
♡健康管理
◍我的