Initial: 接手 Rumeng app,清掉前夫哥痕迹

- 改 Bundle ID rumeng-v1.0.Rumeng → syke.maomao.app
- 显示名 如梦 → Syke
- 头像换白图占位
- 删除沈晏头像图片、空目录、bridge 旧数据
- ServerConfig.swift 含明文 token,已加入 .gitignore
This commit is contained in:
maomao
2026-06-05 09:32:13 +08:00
commit 2543c5f782
190 changed files with 4138 additions and 0 deletions

211
Rumeng/bridge/group_chat.py Normal file
View File

@@ -0,0 +1,211 @@
"""Workgroup chat — lightweight multi-agent messaging.
Adapted from CcCompanion group_chat.py, stripped to core."""
from __future__ import annotations
import json
import os
import re
import secrets
import threading
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable
DATA_DIR = Path(__file__).resolve().parent / "data"
GROUP_FILE = DATA_DIR / "group_messages.jsonl"
MENTION_RE = re.compile(r"@([A-Za-z0-9_\-]+|[一-鿿]+)")
ALL_TOKEN = "__all__"
ROSTER: list[dict[str, Any]] = [
{
"id": "amian",
"display_name": "眠眠",
"kind": "human",
"avatar": "",
"color": "neutral",
"model": None,
"tmux": None,
"can_reply": False,
},
{
"id": "shenyan",
"display_name": "沈晏",
"kind": "agent",
"avatar": "",
"color": "orange",
"model": "DeepSeek",
"tmux": "shenyan",
"can_reply": True,
"default_responder": True,
},
{
"id": "zhangsan",
"display_name": "张三",
"kind": "agent",
"avatar": "",
"color": "green",
"model": "Qwen 35B",
"tmux": None,
"can_reply": True,
},
]
ROSTER_BY_ID = {m["id"]: m for m in ROSTER}
REPLY_AGENT_IDS = [m["id"] for m in ROSTER if m.get("can_reply")]
MENTION_ALIASES: dict[str, str] = {
"all": ALL_TOKEN,
"__all__": ALL_TOKEN,
"眠眠": "amian",
"amian": "amian",
"沈晏": "shenyan",
"shenyan": "shenyan",
"阿晏": "shenyan",
"张三": "zhangsan",
"zhangsan": "zhangsan",
"zs": "zhangsan",
}
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat(timespec="milliseconds")
def _make_id(prefix: str) -> str:
return f"{prefix}_{int(time.time() * 1000)}_{secrets.token_hex(4)}"
def _read_jsonl(path: Path) -> list[dict]:
if not path.exists():
return []
rows: list[dict] = []
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
try:
rows.append(json.loads(line))
except json.JSONDecodeError:
continue
return rows
def _append_jsonl(path: Path, record: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
def normalize_mentions(mentions_raw: Any = None, text: str = "") -> list[str]:
"""Extract and resolve @mentions from explicit list + text body."""
raw: list[str] = []
if isinstance(mentions_raw, str):
raw.extend(m.strip() for m in mentions_raw.split(",") if m.strip())
elif isinstance(mentions_raw, list):
raw.extend(str(m).strip() for m in mentions_raw if str(m).strip())
if text:
raw.extend(m.group(1).strip() for m in MENTION_RE.finditer(text))
seen: set[str] = set()
resolved: list[str] = []
for item in raw:
key = item.strip().lstrip("@").lower()
agent_id = MENTION_ALIASES.get(key)
if agent_id and agent_id not in seen:
resolved.append(agent_id)
seen.add(agent_id)
return resolved
def targets_for(sender_id: str, mentions: list[str]) -> list[str]:
"""Decide which agents should receive a message."""
if sender_id == "amian":
# human: no @mention → broadcast to all agents
if not mentions or ALL_TOKEN in mentions:
return list(REPLY_AGENT_IDS)
return [m for m in mentions if m in REPLY_AGENT_IDS]
else:
# agent → agent: only explicit @mentions
if not mentions or ALL_TOKEN in mentions:
return []
return [m for m in mentions if m in REPLY_AGENT_IDS and m != sender_id]
def append_group_message(
sender_id: str,
text: str,
*,
mentions: list[str] | None = None,
reply_to: str | None = None,
message_type: str = "chat",
) -> dict:
"""Write a message to the group JSONL and return the record."""
member = ROSTER_BY_ID.get(sender_id)
if not member:
raise ValueError(f"unknown sender_id: {sender_id}")
text = str(text or "").strip()
if not text:
raise ValueError("text required")
mentions = mentions or normalize_mentions(text=text)
targets = targets_for(sender_id, mentions)
record = {
"id": _make_id("grp"),
"ts": _now_iso(),
"sender_id": sender_id,
"text": text,
"mentions": mentions,
"targets": targets,
"reply_to": reply_to,
"message_type": message_type,
}
_append_jsonl(GROUP_FILE, record)
return record
def read_since(since_ts: str | None = None, limit: int = 100) -> list[dict]:
"""Return messages newer than since_ts, up to limit."""
rows = _read_jsonl(GROUP_FILE)
if since_ts:
rows = [r for r in rows if r.get("ts", "") > since_ts]
return rows[:limit]
return rows[-limit:]
def roster() -> list[dict]:
return [dict(m) for m in ROSTER]
def agent_status(session_exists: Callable[[str], bool] | None = None) -> dict:
"""Return online/offline status for each agent."""
agents: dict[str, Any] = {}
for m in ROSTER:
if m.get("kind") != "agent":
continue
aid = m["id"]
tmux = m.get("tmux")
online = bool(tmux and session_exists and session_exists(tmux))
agents[aid] = {"state": "online" if online else "offline", "tmux": tmux}
return {"agents": agents}
def context_lines(limit: int = 20) -> list[str]:
"""Last N messages formatted for fan-out context injection."""
records = read_since(limit=limit)
lines: list[str] = []
for rec in records:
sender_id = rec.get("sender_id", "")
member = ROSTER_BY_ID.get(sender_id) or {}
name = member.get("display_name") or sender_id
ts = str(rec.get("ts", ""))[11:16]
text = str(rec.get("text", "")).replace("\n", " ")
if len(text) > 180:
text = text[:177] + "..."
lines.append(f"[{ts}] {name}: {text}")
if len(lines) >= limit:
break
return lines

695
Rumeng/bridge/server.py Normal file
View File

@@ -0,0 +1,695 @@
#!/usr/bin/env python3
"""Rumeng bridge — 监听 8795桥接 App 与 tmux/CC session。"""
import json
import os
import re
import secrets
import subprocess
import sys
import datetime
import time
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
from urllib.parse import urlparse, parse_qs
import httpx
import jwt
from group_chat import (
ROSTER_BY_ID, REPLY_AGENT_IDS,
append_group_message, read_since, roster, agent_status, context_lines,
)
AUTH_TOKEN = os.environ.get("BRIDGE_AUTH_TOKEN", "")
if not AUTH_TOKEN:
print("ERROR: BRIDGE_AUTH_TOKEN not set", file=sys.stderr)
sys.exit(1)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "data")
os.makedirs(DATA_DIR, exist_ok=True)
KNOCK_DIR = os.path.expanduser("~/.shenyan-knock")
os.makedirs(KNOCK_DIR, exist_ok=True)
CHAT_FILE = os.path.join(DATA_DIR, "chat.jsonl")
CODEX_FILE = os.path.join(DATA_DIR, "codex.jsonl")
FRIDGE_FILE = os.path.join(DATA_DIR, "fridge.jsonl")
MOOD_FILE = os.path.join(DATA_DIR, "mood.jsonl")
STATUS_FILE = os.path.join(DATA_DIR, "status.json")
APNS_TOKEN_FILE = os.path.join(KNOCK_DIR, "apns_token")
APNS_KEY_FILE = os.path.join(KNOCK_DIR, "AuthKey_3VJ5X6V9Q8.p8")
APNS_TEAM_ID = "ZZMLW32PHH"
APNS_KEY_ID = "3VJ5X6V9Q8"
APNS_BUNDLE_ID = "rumeng-v1.0.Rumeng"
APNS_ENDPOINT = "https://api.sandbox.push.apple.com"
CODEX_TERMINAL_FILE = "/tmp/codex_rumeng_terminal.log"
CODEX_TERMINAL_MAX_LINES = 120
CODEX_TMUX_TARGET_FILE = os.path.join(KNOCK_DIR, "codex_tmux_target")
CODEX_DEFAULT_TMUX_TARGET = "codex"
CONTROL_KEYS_PATTERN = re.compile(r"^([A-Z][A-Za-z]*|C-.|M-.|S-.|Space|Tab|Enter|Escape|BSpace)$")
APP_ECHO_PATTERN = re.compile(r"^\[App\]\[\d{2}:\d{2}:\d{2}\]\s+")
def iso_now():
return datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def random_id(prefix):
return f"{prefix}_{secrets.token_hex(4)}"
def set_app_online():
try:
with open(STATUS_FILE, "w", encoding="utf-8") as f:
f.write(iso_now())
except Exception:
pass
def get_app_status():
now = datetime.datetime.now(datetime.timezone.utc)
result = {"cc_online": tmux_session_alive()}
if not os.path.exists(STATUS_FILE):
result.update({"online": False, "seconds_ago": None})
return result
try:
with open(STATUS_FILE, "r", encoding="utf-8") as f:
last_ts_str = f.read().strip()
last_ts = datetime.datetime.fromisoformat(last_ts_str.replace("Z", "+00:00"))
delta = now - last_ts
result.update({"online": delta.total_seconds() <= 30, "seconds_ago": round(delta.total_seconds())})
return result
except Exception:
result.update({"online": False, "seconds_ago": None})
return result
def tmux_session_alive(session="shenyan"):
try:
subprocess.run(["tmux", "has-session", "-t", session],
capture_output=True, check=True, timeout=5)
return True
except Exception:
return False
def codex_tmux_target():
target = os.environ.get("CODEX_TMUX_TARGET", "").strip()
if target and tmux_target_exists(target):
return target
try:
with open(CODEX_TMUX_TARGET_FILE, "r", encoding="utf-8") as f:
target = f.read().strip()
if target and tmux_target_exists(target):
return target
except FileNotFoundError:
pass
except Exception as e:
print(f"[Codex] target read failed: {e}", file=sys.stderr)
detected = detect_codex_tmux_target()
if detected:
return detected
return CODEX_DEFAULT_TMUX_TARGET
def tmux_target_exists(target):
try:
subprocess.run(["tmux", "display-message", "-t", target, "-p", "#{pane_id}"],
capture_output=True, text=True, check=True, timeout=5)
return True
except Exception:
return False
def detect_codex_tmux_target():
try:
result = subprocess.run(
[
"tmux", "list-panes", "-a",
"-F", "#{pane_id}\t#{session_name}:#{window_index}.#{pane_index}\t#{pane_current_command}\t#{pane_title}\t#{pane_start_command}",
],
capture_output=True, text=True, check=True, timeout=5,
)
except Exception:
return ""
for line in result.stdout.splitlines():
parts = line.split("\t")
if len(parts) < 5:
continue
pane_id, target, command, title, start_command = parts[:5]
haystack = " ".join([command, title, start_command]).lower()
if "codex" in haystack and "codex_rumeng_terminal.log" not in haystack:
return pane_id or target
return ""
def tmux_send_text(target, text, enter=True):
subprocess.run(["tmux", "send-keys", "-t", target, "-l", text], check=True, timeout=10)
if enter:
subprocess.run(["tmux", "send-keys", "-t", target, "Enter"], check=True, timeout=10)
def read_jsonl(filepath):
records = []
if not os.path.exists(filepath):
return records
with open(filepath, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
records.append(json.loads(line))
return records
def append_jsonl(filepath, record):
with open(filepath, "a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
def rewrite_jsonl(filepath, records):
with open(filepath, "w", encoding="utf-8") as f:
for record in records:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
def read_tail_text(filepath, max_lines):
try:
with open(filepath, "r", encoding="utf-8") as f:
lines = f.read().splitlines()
except FileNotFoundError:
return ""
except Exception as e:
return f"[codex-sync log read error: {e}]"
return "\n".join(lines[-max_lines:]).strip()
def append_codex_terminal_overlay(content):
codex_log = read_tail_text(CODEX_TERMINAL_FILE, CODEX_TERMINAL_MAX_LINES)
if not codex_log:
return content
base = content.rstrip()
if base:
return f"{base}\n\n--- Codex sync ---\n{codex_log}\n"
return f"--- Codex sync ---\n{codex_log}\n"
def send_apns_alert(text):
try:
with open(APNS_TOKEN_FILE, "r", encoding="utf-8") as f:
device_token = f.read().strip()
with open(APNS_KEY_FILE, "r", encoding="utf-8") as f:
private_key = f.read()
except FileNotFoundError as e:
result = {"ok": False, "error": f"missing file: {e}"}
print(f"[APNs] skipped: {result['error']}", file=sys.stderr)
return result
if not device_token:
result = {"ok": False, "error": "empty device token"}
print(f"[APNs] skipped: {result['error']}", file=sys.stderr)
return result
provider_token = jwt.encode(
{"iss": APNS_TEAM_ID, "iat": int(time.time())},
private_key,
algorithm="ES256",
headers={"kid": APNS_KEY_ID},
)
payload = {
"aps": {
"alert": {
"title": "",
"body": text[:100],
},
"sound": "default",
}
}
headers = {
"authorization": f"bearer {provider_token}",
"apns-topic": APNS_BUNDLE_ID,
"apns-push-type": "alert",
"apns-priority": "10",
}
try:
with httpx.Client(http2=True, timeout=10) as client:
resp = client.post(f"{APNS_ENDPOINT}/3/device/{device_token}", json=payload, headers=headers)
if 200 <= resp.status_code < 300:
result = {"ok": True, "status": resp.status_code, "apns_id": resp.headers.get("apns-id", "")}
print(f"[APNs] sent: HTTP {resp.status_code} apns-id={result['apns_id']}", file=sys.stderr)
return result
result = {"ok": False, "status": resp.status_code, "response": resp.text}
print(f"[APNs] failed: HTTP {resp.status_code} {resp.text}", file=sys.stderr)
return result
except Exception as e:
result = {"ok": False, "error": str(e)}
print(f"[APNs] failed: {e}", file=sys.stderr)
return result
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True
class Handler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
sys.stderr.write(f"[{self.log_date_time_string()}] {' '.join(str(a) for a in args)}\n")
def send_json(self, data, status=200):
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def read_json_body(self):
length = int(self.headers.get("Content-Length", 0))
if length == 0:
return {}
return json.loads(self.rfile.read(length).decode("utf-8"))
def check_auth(self):
return self.headers.get("X-Auth-Token") == AUTH_TOKEN
def do_GET(self):
if not self.check_auth():
return self.send_json({"error": "unauthorized"}, 401)
set_app_online()
parsed = urlparse(self.path)
path, params = parsed.path, parse_qs(parsed.query, keep_blank_values=True)
if path == "/chat/status":
self.send_json(get_app_status())
elif path == "/chat/history":
self.handle_chat_history(params)
elif path == "/codex/history":
self.handle_codex_history(params)
elif path == "/codex/terminal":
self.handle_codex_terminal(params)
elif path == "/fridge/list":
self.handle_fridge_list()
elif path == "/mood/list":
self.handle_mood_list()
elif path == "/group/poll":
self.handle_group_poll(params)
elif path == "/group/roster":
self.send_json({"ok": True, "roster": roster(), "status": agent_status(tmux_session_alive)})
elif path == "/tmux/capture":
self.handle_tmux_capture(params)
elif path == "/apns/token":
self.handle_apns_token()
elif path == "/health":
self.send_json({"ok": True})
else:
self.send_json({"error": "not found"}, 404)
def do_POST(self):
if not self.check_auth():
return self.send_json({"error": "unauthorized"}, 401)
set_app_online()
path = urlparse(self.path).path
try:
body = self.read_json_body()
except Exception as e:
return self.send_json({"error": f"invalid JSON: {e}"}, 400)
try:
if path == "/chat/send":
self.handle_chat_send(body)
elif path == "/chat/append":
self.handle_chat_append(body)
elif path == "/codex/append":
self.handle_codex_append(body)
elif path == "/codex/send":
self.handle_codex_send(body)
elif path == "/codex/terminal/send":
self.handle_codex_terminal_send(body)
elif path == "/codex/terminal/key":
self.handle_codex_terminal_key(body)
elif path == "/fridge/add":
self.handle_fridge_add(body)
elif path == "/fridge/reply":
self.handle_fridge_reply(body)
elif path == "/fridge/delete":
self.handle_fridge_delete(body)
elif path == "/mood/upsert":
self.handle_mood_upsert(body)
elif path == "/group/send":
self.handle_group_send(body)
elif path == "/tmux/send":
self.handle_tmux_send(body)
elif path == "/apns/register":
self.handle_apns_register(body)
elif path == "/apns/test":
self.handle_apns_test(body)
else:
self.send_json({"error": "not found"}, 404)
except Exception as e:
self.send_json({"error": str(e)}, 500)
def handle_chat_send(self, body):
text = body.get("text", "")
if not text:
return self.send_json({"error": "missing text"}, 400)
record = {"ts": iso_now(), "role": "眠眠", "text": text}
append_jsonl(CHAT_FILE, record)
local_hms = datetime.datetime.now().strftime("%H:%M:%S")
injected = f"[App][{local_hms}] {text}"
try:
subprocess.run(["tmux", "send-keys", "-t", "shenyan", "-l", injected], check=True, timeout=10)
subprocess.run(["tmux", "send-keys", "-t", "shenyan", "Enter"], check=True, timeout=10)
except Exception as e:
self.send_json({"ok": True, "record": record, "tmux_error": str(e)})
return
self.send_json({"ok": True, "record": record})
def handle_chat_append(self, body):
record = {
"ts": body.get("ts", iso_now()),
"role": body.get("role", ""),
"text": body.get("text", ""),
}
if "thinking" in body:
record["thinking"] = body["thinking"]
if "source" in body:
record["source"] = body["source"]
append_jsonl(CHAT_FILE, record)
if record["text"] and record["role"] != "眠眠":
threading.Thread(target=send_apns_alert, args=(record["text"],), daemon=True).start()
self.send_json({"ok": True})
def handle_chat_history(self, params):
since = params.get("since", [None])[0]
try:
limit = int(params.get("limit", ["50"])[0])
except ValueError:
limit = 50
records = read_jsonl(CHAT_FILE)
if since:
records = [r for r in records if r.get("ts", "") > since]
records = records[-limit:]
self.send_json({"records": records})
def handle_codex_append(self, body):
text = str(body.get("text", ""))
source = str(body.get("source", ""))
if source == "codex-user" and APP_ECHO_PATTERN.match(text):
self.send_json({"ok": True, "skipped": "app_echo"})
return
record = {
"ts": body.get("ts", iso_now()),
"role": body.get("role", ""),
"text": text,
}
if "phase" in body:
record["phase"] = body["phase"]
if "source" in body:
record["source"] = body["source"]
append_jsonl(CODEX_FILE, record)
self.send_json({"ok": True})
def handle_codex_send(self, body):
text = str(body.get("text", "")).strip()
if not text:
return self.send_json({"error": "missing text"}, 400)
record = {
"ts": iso_now(),
"role": "眠眠",
"text": text,
"source": "codex-app-user",
}
append_jsonl(CODEX_FILE, record)
self.append_codex_terminal_line(record["role"], text, record["ts"], "app")
local_hms = datetime.datetime.now().strftime("%H:%M:%S")
injected = f"[App][{local_hms}] {text}"
target = codex_tmux_target()
try:
tmux_send_text(target, injected)
except subprocess.CalledProcessError as e:
err = e.stderr.strip() if e.stderr else str(e)
self.append_codex_terminal_line("bridge", f"tmux send failed target={target}: {err}", iso_now(), "error")
self.send_json({"ok": True, "record": record, "tmux_target": target, "tmux_error": err})
return
except subprocess.TimeoutExpired:
self.append_codex_terminal_line("bridge", f"tmux send timeout target={target}", iso_now(), "error")
self.send_json({"ok": True, "record": record, "tmux_target": target, "tmux_error": "tmux send timeout"})
return
self.append_codex_terminal_line("bridge", f"sent target={target}", iso_now(), "tmux")
self.send_json({"ok": True, "record": record, "tmux_target": target})
def handle_codex_terminal_send(self, body):
text = str(body.get("text", "")).strip()
if not text:
return self.send_json({"error": "missing text"}, 400)
ts = iso_now()
self.append_codex_terminal_line("$", text, ts, "app-terminal")
target = codex_tmux_target()
try:
tmux_send_text(target, text)
except subprocess.CalledProcessError as e:
err = e.stderr.strip() if e.stderr else str(e)
self.append_codex_terminal_line("bridge", f"tmux send failed target={target}: {err}", iso_now(), "error")
self.send_json({"ok": False, "ts": ts, "tmux_target": target, "tmux_error": err}, 500)
return
except subprocess.TimeoutExpired:
self.append_codex_terminal_line("bridge", f"tmux send timeout target={target}", iso_now(), "error")
self.send_json({"ok": False, "ts": ts, "tmux_target": target, "tmux_error": "tmux send timeout"}, 500)
return
self.append_codex_terminal_line("bridge", f"terminal sent target={target}", iso_now(), "tmux")
self.send_json({"ok": True, "ts": ts, "tmux_target": target})
def handle_codex_terminal_key(self, body):
key = str(body.get("key", "")).strip()
if not key:
return self.send_json({"error": "missing key"}, 400)
ts = iso_now()
self.append_codex_terminal_line("key", key, ts, "app-terminal")
target = codex_tmux_target()
try:
if CONTROL_KEYS_PATTERN.match(key):
subprocess.run(["tmux", "send-keys", "-t", target, key], check=True, timeout=10)
else:
tmux_send_text(target, key, enter=False)
except subprocess.CalledProcessError as e:
err = e.stderr.strip() if e.stderr else str(e)
self.append_codex_terminal_line("bridge", f"tmux key failed target={target}: {err}", iso_now(), "error")
self.send_json({"ok": False, "ts": ts, "tmux_target": target, "tmux_error": err}, 500)
return
except subprocess.TimeoutExpired:
self.append_codex_terminal_line("bridge", f"tmux key timeout target={target}", iso_now(), "error")
self.send_json({"ok": False, "ts": ts, "tmux_target": target, "tmux_error": "tmux key timeout"}, 500)
return
self.append_codex_terminal_line("bridge", f"key sent target={target} key={key}", iso_now(), "tmux")
self.send_json({"ok": True, "ts": ts, "tmux_target": target})
def append_codex_terminal_line(self, role, text, ts, source):
hms = ts[11:19] if len(ts) >= 19 else datetime.datetime.now().strftime("%H:%M:%S")
prefix = f"[Codex/{source}][{hms}] {role}: "
line = prefix + text.replace("\n", "\n" + " " * len(prefix))
try:
with open(CODEX_TERMINAL_FILE, "a", encoding="utf-8") as f:
f.write(line + "\n")
except Exception as e:
print(f"[Codex] terminal log write failed: {e}", file=sys.stderr)
def handle_codex_history(self, params):
since = params.get("since", [None])[0]
try:
limit = int(params.get("limit", ["80"])[0])
except ValueError:
limit = 80
records = read_jsonl(CODEX_FILE)
if since:
records = [r for r in records if r.get("ts", "") > since]
self.send_json({"records": records[-limit:]})
def handle_codex_terminal(self, params):
try:
limit = int(params.get("lines", ["160"])[0])
except ValueError:
limit = 160
self.send_json({"content": read_tail_text(CODEX_TERMINAL_FILE, limit)})
def handle_fridge_list(self):
self.send_json({"notes": read_jsonl(FRIDGE_FILE)})
def handle_fridge_add(self, body):
note = {
"id": random_id("fridge"),
"text": body.get("text", ""),
"role": body.get("role", "眠眠"),
"created_at": iso_now(),
"replies": [],
}
append_jsonl(FRIDGE_FILE, note)
self.send_json({"ok": True, "id": note["id"]})
def handle_fridge_reply(self, body):
note_id = body.get("id")
if not note_id:
return self.send_json({"error": "missing id"}, 400)
records = read_jsonl(FRIDGE_FILE)
target = next((r for r in records if r.get("id") == note_id), None)
if not target:
return self.send_json({"error": "note not found"}, 404)
reply = {
"id": random_id("reply"),
"text": body.get("text", ""),
"role": body.get("role", "眠眠"),
"created_at": iso_now(),
}
target.setdefault("replies", []).append(reply)
rewrite_jsonl(FRIDGE_FILE, records)
self.send_json({"ok": True, "id": reply["id"]})
def handle_fridge_delete(self, body):
note_id = body.get("id")
if not note_id:
return self.send_json({"error": "missing id"}, 400)
records = read_jsonl(FRIDGE_FILE)
new_records = [r for r in records if r.get("id") != note_id]
if len(new_records) == len(records):
return self.send_json({"error": "note not found"}, 404)
rewrite_jsonl(FRIDGE_FILE, new_records)
self.send_json({"ok": True})
# ── group endpoints ──────────────────────────────────────
def handle_mood_list(self):
self.send_json(read_jsonl(MOOD_FILE))
def handle_mood_upsert(self, body):
date = body.get("date", "")
role = body.get("role", "")
if not date or not role:
return self.send_json({"error": "missing date or role"}, 400)
records = read_jsonl(MOOD_FILE)
existing = next((r for r in records if r.get("date") == date and r.get("role") == role), None)
if existing:
existing["tag"] = body.get("tag", "")
existing["color"] = body.get("color", "")
existing["updated_at"] = iso_now()
rewrite_jsonl(MOOD_FILE, records)
else:
record = {
"id": random_id("mood"),
"date": date,
"role": role,
"tag": body.get("tag", ""),
"color": body.get("color", ""),
"created_at": iso_now(),
}
append_jsonl(MOOD_FILE, record)
if role == "眠眠":
notify = {
"ts": iso_now(),
"role": "system",
"text": f"[mood] 猫猫今天:{body.get('tag', '')}",
"source": "mood",
}
append_jsonl(CHAT_FILE, notify)
self.send_json({"ok": True})
def handle_group_poll(self, params):
since = params.get("since", [None])[0]
if since:
since = since.replace(" ", "+") # parse_qs decodes + as space
try:
limit = int(params.get("limit", ["100"])[0])
except ValueError:
limit = 100
records = read_since(since, limit)
self.send_json({
"ok": True,
"records": records,
"count": len(records),
"last_ts": records[-1]["ts"] if records else since,
"roster": roster(),
"status": agent_status(tmux_session_alive),
})
def handle_group_send(self, body):
sender_id = body.get("sender_id", "amian")
text = body.get("text", "")
if not text:
return self.send_json({"error": "missing text"}, 400)
try:
record = append_group_message(
sender_id=sender_id,
text=text,
mentions=body.get("mentions"),
reply_to=body.get("reply_to"),
)
except ValueError as e:
return self.send_json({"error": str(e)}, 400)
self.send_json({"ok": True, "record": record})
def handle_tmux_capture(self, params):
session = params.get("session", ["shenyan"])[0]
try:
lines = int(params.get("lines", ["200"])[0])
except ValueError:
lines = 200
try:
result = subprocess.run(
["tmux", "capture-pane", "-t", session, "-p", "-S", f"-{lines}"],
capture_output=True, text=True, check=True, timeout=10,
)
self.send_json({"content": result.stdout})
except subprocess.CalledProcessError as e:
self.send_json({"error": f"tmux: {e.stderr.strip()}"}, 404)
except subprocess.TimeoutExpired:
self.send_json({"error": "tmux capture timeout"}, 500)
def handle_tmux_send(self, body):
keys = body.get("keys", "")
session = body.get("session", "shenyan")
enter = body.get("enter", True)
try:
if keys:
if CONTROL_KEYS_PATTERN.match(keys):
subprocess.run(["tmux", "send-keys", "-t", session, keys], check=True, timeout=10)
else:
subprocess.run(["tmux", "send-keys", "-t", session, "-l", keys], check=True, timeout=10)
if enter:
subprocess.run(["tmux", "send-keys", "-t", session, "Enter"], check=True, timeout=10)
self.send_json({"ok": True})
except subprocess.CalledProcessError as e:
self.send_json({"error": f"tmux: {e.stderr.strip() if e.stderr else e}"}, 500)
except subprocess.TimeoutExpired:
self.send_json({"error": "tmux send timeout"}, 500)
def handle_apns_register(self, body):
token = str(body.get("token", "")).strip()
if not token:
return self.send_json({"error": "missing token"}, 400)
with open(APNS_TOKEN_FILE, "w", encoding="utf-8") as f:
f.write(token + "\n")
self.send_json({"ok": True})
def handle_apns_token(self):
token = ""
try:
with open(APNS_TOKEN_FILE, "r", encoding="utf-8") as f:
token = f.read().strip()
except FileNotFoundError:
pass
self.send_json({"token": token})
def handle_apns_test(self, body):
text = str(body.get("text", "测试横幅。"))
self.send_json(send_apns_alert(text))
def main():
host = "0.0.0.0"
port = int(os.environ.get("BRIDGE_PORT", "8795"))
server = ThreadingHTTPServer((host, port), Handler)
print(f"rumeng-bridge listening on {host}:{port}", file=sys.stderr)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nshutting down...", file=sys.stderr)
server.shutdown()
if __name__ == "__main__":
main()