Revert "fix: resolve hardcoded paths, port mismatch, XSS, and UX issues"

This commit is contained in:
ringhyacinth 2026-03-03 07:44:48 +08:00 committed by GitHub
parent 474811083b
commit cf76f8556a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 48 additions and 179 deletions

View file

@ -31,7 +31,7 @@ cd backend
python3 app.py
```
打开:**http://127.0.0.1:19000**
打开:**http://127.0.0.1:18791**
切状态试试(在项目根目录执行):
```bash
@ -105,7 +105,7 @@ cd backend
python3 app.py
```
打开:`http://127.0.0.1:19000`
打开:`http://127.0.0.1:18791`
### 4) 切换主 Agent 状态(示例)
@ -254,7 +254,7 @@ cd backend
python3 app.py
```
Open: **http://127.0.0.1:19000**
Open: **http://127.0.0.1:18791**
Try changing states (run from project root):
```bash
@ -328,7 +328,7 @@ cd backend
python3 app.py
```
Open: `http://127.0.0.1:19000`
Open: `http://127.0.0.1:18791`
### 4) Switch main Agent status (example)

View file

@ -39,7 +39,7 @@ python3 app.py
```
然后告诉主人:
> 好了,你现在打开 http://127.0.0.1:19000 就能看到像素办公室了!
> 好了,你现在打开 http://127.0.0.1:18791 就能看到像素办公室了!
---
@ -70,7 +70,7 @@ python3 set_state.py idle "待命中,随时准备为你服务"
如果你这台机器有 `cloudflared`,直接跑:
```bash
cloudflared tunnel --url http://127.0.0.1:19000
cloudflared tunnel --url http://127.0.0.1:18791
```
会得到一个 `https://xxx.trycloudflare.com` 链接,发给主人即可。

View file

@ -3,14 +3,10 @@
from flask import Flask, jsonify, send_from_directory, make_response, request
from datetime import datetime, timedelta
from urllib.parse import urlparse
import hmac
import html as html_mod
import json
import os
import re
import threading
import uuid
# Paths (project-relative, no hardcoded absolute paths)
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -20,12 +16,6 @@ STATE_FILE = os.path.join(ROOT_DIR, "state.json")
AGENTS_STATE_FILE = os.path.join(ROOT_DIR, "agents-state.json")
JOIN_KEYS_FILE = os.path.join(ROOT_DIR, "join-keys.json")
# Security/validation knobs
OFFICE_SET_STATE_TOKEN = (os.environ.get("OFFICE_SET_STATE_TOKEN") or os.environ.get("OFFICE_STATUS_SYNC_TOKEN") or "").strip()
MAX_DETAIL_LEN = int(os.environ.get("OFFICE_MAX_DETAIL_LEN", "200"))
MAX_NAME_LEN = int(os.environ.get("OFFICE_MAX_NAME_LEN", "50"))
CONTROL_CHAR_RE = re.compile(r"[\x00-\x1f\x7f]")
def get_yesterday_date_str():
"""获取昨天的日期字符串 YYYY-MM-DD"""
@ -33,92 +23,25 @@ def get_yesterday_date_str():
return yesterday.strftime("%Y-%m-%d")
def clean_text(value, max_len=200):
"""输入清洗去控制字符、去首尾空格、限长、转义HTML实体。"""
if value is None:
return ""
text = str(value)
text = CONTROL_CHAR_RE.sub("", text).strip()
text = html_mod.escape(text)
if len(text) > max_len:
text = text[:max_len]
return text
def get_trace_id():
return (request.headers.get("X-Trace-Id") or "").strip() or f"office-{uuid.uuid4().hex[:10]}"
def log_event(level, message, **fields):
payload = {
"ts": datetime.now().isoformat(),
"level": level,
"message": message,
}
payload.update(fields)
try:
print(json.dumps(payload, ensure_ascii=False), flush=True)
except Exception:
print(f"[{level}] {message} {fields}", flush=True)
def is_same_origin_request():
"""Allow browser same-origin requests (e.g. local control panel buttons)."""
req_host = (request.host or "").lower()
origin = request.headers.get("Origin") or request.headers.get("Referer")
if not origin:
return False
try:
parsed = urlparse(origin)
return (parsed.netloc or "").lower() == req_host
except Exception:
return False
def is_loopback_remote():
addr = (request.remote_addr or "").strip()
return addr in {"127.0.0.1", "::1", "localhost"}
def is_set_state_authorized():
"""Authorize set_state: same-origin browser OR token OR local loopback fallback."""
if is_same_origin_request():
return True, "same-origin"
token = (request.headers.get("X-Office-Token") or "").strip()
if OFFICE_SET_STATE_TOKEN and token and hmac.compare_digest(token, OFFICE_SET_STATE_TOKEN):
return True, "token"
# Backward compatibility: keep local loopback calls working even if token is unset.
if not OFFICE_SET_STATE_TOKEN and is_loopback_remote():
return True, "loopback-no-token"
return False, "unauthorized"
def sanitize_content(text):
"""清理内容,保护隐私"""
import re
# 移除 OpenID、User ID 等
text = re.sub(r'ou_[a-f0-9]+', '[用户]', text)
text = re.sub(r'user_id="[^"]+"', 'user_id="[隐藏]"', text)
# 移除具体的人名(如果有的话)
# 这里可以根据需要添加更多规则
# 移除 IP 地址、路径等敏感信息
text = re.sub(r'/root/[^"\s]+', '[路径]', text)
text = re.sub(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', '[IP]', text)
# 移除邮箱
# 移除电话号码、邮箱等
text = re.sub(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', '[邮箱]', text)
# 补充敏感信息类型(身份证 / 银行卡 / JWT
text = re.sub(r'\b\d{17}[\dXx]\b', '[身份证]', text)
text = re.sub(r'\b\d{16,19}\b', '[银行卡]', text)
text = re.sub(r'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+', '[JWT]', text)
# 最后处理手机号,避免误伤身份证串
text = re.sub(r'\b1[3-9]\d{9}\b', '[手机号]', text)
text = re.sub(r'1[3-9]\d{9}', '[手机号]', text)
return text
@ -164,10 +87,8 @@ def extract_memo_from_file(file_path):
"「纸上得来终觉浅,绝知此事要躬行。」"
]
# Use date-based index so the same quote shows all day (no jitter on poll)
today = datetime.now().strftime("%Y%m%d")
quote_index = int(today) % len(wisdom_quotes)
quote = wisdom_quotes[quote_index]
import random
quote = random.choice(wisdom_quotes)
# 组合内容
result = []
@ -288,27 +209,9 @@ def load_state():
def save_state(state: dict):
"""Save state to file and sync main agent in agents-state.json"""
"""Save state to file"""
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False, indent=2)
# Keep agents-state.json in sync for the main agent
_sync_main_agent_state(state)
def _sync_main_agent_state(state: dict):
"""Update the isMain=true entry in agents-state.json to match state.json"""
try:
agents = load_agents_state()
for a in agents:
if a.get("isMain"):
a["state"] = state.get("state", "idle")
a["detail"] = state.get("detail", "")
a["updated_at"] = state.get("updated_at", datetime.now().isoformat())
a["area"] = state_to_area(a["state"])
break
save_agents_state(agents)
except Exception:
pass
# Initialize state
@ -462,7 +365,6 @@ def get_agents():
cleaned_agents = []
keys_data = load_join_keys()
dirty = False # only write to disk if something actually changed
for a in agents:
if a.get("isMain"):
@ -485,7 +387,6 @@ def get_agents():
key_item["usedBy"] = None
key_item["usedByAgentId"] = None
key_item["usedAt"] = None
dirty = True
continue
except Exception:
pass
@ -498,15 +399,13 @@ def get_agents():
age = (now - last_push_at).total_seconds()
if age > 300: # 5分钟无推送自动离线
a["authStatus"] = "offline"
dirty = True
except Exception:
pass
cleaned_agents.append(a)
if dirty:
save_agents_state(cleaned_agents)
save_join_keys(keys_data)
save_agents_state(cleaned_agents)
save_join_keys(keys_data)
return jsonify(cleaned_agents)
@ -581,10 +480,10 @@ def join_agent():
if not isinstance(data, dict) or not data.get("name"):
return jsonify({"ok": False, "msg": "请提供名字"}), 400
name = clean_text(data["name"], MAX_NAME_LEN)
name = data["name"].strip()
state = data.get("state", "idle")
detail = clean_text(data.get("detail", ""), MAX_DETAIL_LEN)
join_key = clean_text(data.get("joinKey", ""), 128)
detail = data.get("detail", "")
join_key = data.get("joinKey", "").strip()
# Normalize state early for compatibility
state = normalize_agent_state(state)
@ -780,16 +679,16 @@ def agent_push():
if not isinstance(data, dict):
return jsonify({"ok": False, "msg": "invalid json"}), 400
trace_id = get_trace_id()
agent_id = clean_text(data.get("agentId"), 128)
join_key = clean_text(data.get("joinKey"), 128)
state = clean_text(data.get("state"), 32)
detail = clean_text(data.get("detail"), MAX_DETAIL_LEN)
name = clean_text(data.get("name"), MAX_NAME_LEN)
agent_id = (data.get("agentId") or "").strip()
join_key = (data.get("joinKey") or "").strip()
state = (data.get("state") or "").strip()
detail = (data.get("detail") or "").strip()
name = (data.get("name") or "").strip()
if not agent_id or not join_key or not state:
return jsonify({"ok": False, "msg": "缺少 agentId/joinKey/state"}), 400
valid_states = {"idle", "writing", "researching", "executing", "syncing", "error"}
state = normalize_agent_state(state)
keys_data = load_join_keys()
@ -828,17 +727,8 @@ def agent_push():
target["lastPushAt"] = datetime.now().isoformat()
save_agents_state(agents)
log_event(
"info",
"agent_push_ok",
traceId=trace_id,
agentId=agent_id,
state=state,
source="remote-openclaw",
)
return jsonify({"ok": True, "agentId": agent_id, "area": target.get("area")})
except Exception as e:
log_event("error", "agent_push_failed", error=str(e))
return jsonify({"ok": False, "msg": str(e)}), 500
@ -897,40 +787,22 @@ def get_yesterday_memo():
@app.route("/set_state", methods=["POST"])
def set_state_endpoint():
"""Set state via POST (for UI control panel)"""
trace_id = get_trace_id()
try:
ok, reason = is_set_state_authorized()
if not ok:
log_event("warn", "set_state_denied", traceId=trace_id, reason=reason, remote=request.remote_addr)
return jsonify({"status": "error", "msg": "unauthorized"}), 401
data = request.get_json()
if not isinstance(data, dict):
return jsonify({"status": "error", "msg": "invalid json"}), 400
state = load_state()
if "state" in data:
s = normalize_agent_state(data["state"])
s = data["state"]
valid_states = {"idle", "writing", "researching", "executing", "syncing", "error"}
if s in valid_states:
state["state"] = s
if "detail" in data:
state["detail"] = clean_text(data["detail"], MAX_DETAIL_LEN)
state["detail"] = data["detail"]
state["updated_at"] = datetime.now().isoformat()
save_state(state)
log_event(
"info",
"set_state_ok",
traceId=trace_id,
auth=reason,
state=state.get("state"),
remote=request.remote_addr,
)
return jsonify({"status": "ok"})
except Exception as e:
log_event("error", "set_state_failed", traceId=trace_id, error=str(e))
return jsonify({"status": "error", "msg": str(e)}), 500
@ -938,9 +810,8 @@ if __name__ == "__main__":
print("=" * 50)
print("Star Office UI - Backend State Service")
print("=" * 50)
OFFICE_PORT = int(os.environ.get("OFFICE_PORT", "19000"))
print(f"State file: {STATE_FILE}")
print(f"Listening on: http://0.0.0.0:{OFFICE_PORT}")
print("Listening on: http://0.0.0.0:18791")
print("=" * 50)
app.run(host="0.0.0.0", port=OFFICE_PORT, debug=False)
app.run(host="0.0.0.0", port=18791, debug=False)

View file

@ -9,8 +9,7 @@ import os
from PIL import Image
# 路径
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
FRONTEND_DIR = os.path.join(ROOT_DIR, "frontend")
FRONTEND_DIR = "/root/.openclaw/workspace/star-office-ui/frontend"
STATIC_DIR = os.path.join(FRONTEND_DIR, "")
# 文件分类配置

View file

@ -508,7 +508,7 @@
if (data.success && data.memo) {
memoDate.textContent = data.date || '';
memoContent.textContent = data.memo;
memoContent.innerHTML = data.memo.replace(/\n/g, '<br>');
} else {
memoContent.innerHTML = '<div id="memo-placeholder">暂无昨日日记</div>';
}
@ -789,7 +789,7 @@
}
list.innerHTML = visitors.map(agent => {
const name = (agent.name || '未命名访客').replace(/[<>"'&]/g, c => ({'<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;','&':'&amp;'}[c]));
const name = agent.name || '未命名访客';
const authStatus = agent.authStatus || 'pending';
const state = agent.state || 'idle';
const statusMap = {

View file

@ -18,14 +18,15 @@
- 下载或复制 `office-agent-push.py`可以访问https://office.example.com/static/office-agent-push.py
- **最简单推荐**:直接运行脚本(已内置 state.json 自动发现)
- 会自动尝试以下路径:
- `脚本同目录/state.json`
- `/root/.openclaw/workspace/star-office-ui/state.json`
- `/root/.openclaw/workspace/state.json`
- `当前工作目录/state.json`
- `脚本同目录/state.json`
- 若你的环境路径特殊,再手动指定:
- `OFFICE_LOCAL_STATE_FILE=/你的/state.json/路径`
- 如果你不方便提供 state 文件,再用 /status 鉴权方式:
- `OFFICE_LOCAL_STATUS_TOKEN=<你的token>`
- (可选)`OFFICE_LOCAL_STATUS_URL=http://127.0.0.1:19000/status`
- (可选)`OFFICE_LOCAL_STATUS_URL=http://127.0.0.1:18791/status`
- 填入配置后运行
3. 脚本会自动:

View file

@ -2,16 +2,14 @@
# Star Office UI Health Check
# Checks if backend is responding, restarts if not
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OFFICE_PORT="${OFFICE_PORT:-19000}"
BACKEND_URL="http://127.0.0.1:${OFFICE_PORT}/health"
LOG_FILE="${SCRIPT_DIR}/healthcheck.log"
BACKEND_URL="http://127.0.0.1:18791/health"
LOG_FILE="/root/.openclaw/workspace/star-office-ui/healthcheck.log"
# Log timestamp
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Health check starting..." >> "$LOG_FILE"
# Check backend
if curl -fsS --max-time 5 "$BACKEND_URL" > /dev/null 2>&1; then
if curl -sS "$BACKEND_URL" > /dev/null 2>&1; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backend is healthy" >> "$LOG_FILE"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backend is NOT healthy - restarting..." >> "$LOG_FILE"

View file

@ -31,15 +31,16 @@ STATE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "office-ag
# 优先读取本机 OpenClaw 工作区的状态文件(更贴合 AGENTS.md 的工作流)
# 支持自动发现,减少对方手动配置成本。
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_STATE_CANDIDATES = [
os.path.join(_SCRIPT_DIR, "state.json"),
"/root/.openclaw/workspace/star-office-ui/state.json",
"/root/.openclaw/workspace/state.json",
os.path.join(os.getcwd(), "state.json"),
os.path.join(os.path.dirname(os.path.abspath(__file__)), "state.json"),
]
# 如果对方本地 /status 需要鉴权,可在这里填写 token或通过环境变量 OFFICE_LOCAL_STATUS_TOKEN
LOCAL_STATUS_TOKEN = os.environ.get("OFFICE_LOCAL_STATUS_TOKEN", "")
LOCAL_STATUS_URL = os.environ.get("OFFICE_LOCAL_STATUS_URL", "http://127.0.0.1:19000/status")
LOCAL_STATUS_URL = os.environ.get("OFFICE_LOCAL_STATUS_URL", "http://127.0.0.1:18791/status")
# 可选:直接指定本地状态文件路径(最简单方案:绕过 /status 鉴权)
LOCAL_STATE_FILE = os.environ.get("OFFICE_LOCAL_STATE_FILE", "")
VERBOSE = os.environ.get("OFFICE_VERBOSE", "0") in {"1", "true", "TRUE", "yes", "YES"}

View file

@ -23,7 +23,7 @@ import math
import os
from PIL import Image
ROOT = os.path.dirname(os.path.abspath(__file__))
ROOT = "/root/.openclaw/workspace/star-office-ui"
IN_PATH = os.path.join(ROOT, "frontend", "star-working-spritesheet.png")
OUT_PATH = os.path.join(ROOT, "frontend", "star-working-spritesheet-grid.png")

View file

@ -6,8 +6,7 @@ import os
import sys
from datetime import datetime
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
STATE_FILE = os.path.join(ROOT_DIR, "state.json")
STATE_FILE = "/root/.openclaw/workspace/star-office-ui/state.json"
VALID_STATES = [
"idle",