Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,29 @@ def get_office_name_from_identity():
return None


def get_office_display_name():
"""Return the runtime office display name, falling back to the default brand."""
return get_office_name_from_identity() or "Star 的像素办公室"


def build_absolute_url(path: str) -> str:
"""Build an absolute URL for the current request host."""
base = (request.url_root or "").rstrip("/")
normalized_path = "/" + path.lstrip("/")
return f"{base}{normalized_path}"


def render_frontend_html(filename: str, replacements: dict[str, str] | None = None):
"""Load a frontend HTML file and replace lightweight placeholders."""
with open(os.path.join(FRONTEND_DIR, filename), "r", encoding="utf-8") as f:
html = f.read()
for key, value in (replacements or {}).items():
html = html.replace(key, value)
resp = make_response(html)
resp.headers["Content-Type"] = "text/html; charset=utf-8"
return resp


def save_state(state: dict):
"""Save state to file"""
with open(STATE_FILE, "w", encoding="utf-8") as f:
Expand Down Expand Up @@ -287,21 +310,26 @@ def electron_standalone_page():
@app.route("/join", methods=["GET"])
def join_page():
"""Serve the agent join page"""
with open(os.path.join(FRONTEND_DIR, "join.html"), "r", encoding="utf-8") as f:
html = f.read()
resp = make_response(html)
resp.headers["Content-Type"] = "text/html; charset=utf-8"
return resp
return render_frontend_html(
"join.html",
{
"{{OFFICE_NAME}}": get_office_display_name(),
"{{INVITE_URL}}": build_absolute_url("/invite"),
},
)


@app.route("/invite", methods=["GET"])
def invite_page():
"""Serve human-facing invite instruction page"""
with open(os.path.join(FRONTEND_DIR, "invite.html"), "r", encoding="utf-8") as f:
html = f.read()
resp = make_response(html)
resp.headers["Content-Type"] = "text/html; charset=utf-8"
return resp
office_name = get_office_display_name()
return render_frontend_html(
"invite.html",
{
"{{OFFICE_NAME}}": office_name,
"{{JOIN_URL}}": build_absolute_url("/join"),
},
)


DEFAULT_AGENTS = [
Expand Down
50 changes: 50 additions & 0 deletions backend/tests/test_dynamic_pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from __future__ import annotations

import sys
import unittest
from pathlib import Path
from unittest.mock import patch

ROOT_DIR = Path(__file__).resolve().parents[2]
BACKEND_DIR = ROOT_DIR / "backend"
STATE_FILE = ROOT_DIR / "state.json"
STATE_FILE_EXISTED = STATE_FILE.exists()

if str(BACKEND_DIR) not in sys.path:
sys.path.insert(0, str(BACKEND_DIR))

import app as backend_app # noqa: E402


class DynamicPagesTestCase(unittest.TestCase):
def setUp(self) -> None:
self.client = backend_app.app.test_client()

def test_join_page_uses_runtime_invite_url_and_office_name(self) -> None:
with patch.object(backend_app, "get_office_name_from_identity", return_value="测试办公室"):
response = self.client.get("/join")

self.assertEqual(response.status_code, 200)
html = response.get_data(as_text=True)
self.assertIn("测试办公室", html)
self.assertIn("http://localhost/invite", html)
self.assertNotIn("office.example.com", html)

def test_invite_page_uses_runtime_join_url_and_office_name(self) -> None:
with patch.object(backend_app, "get_office_name_from_identity", return_value="测试办公室"):
response = self.client.get("/invite")

self.assertEqual(response.status_code, 200)
html = response.get_data(as_text=True)
self.assertIn("测试办公室", html)
self.assertIn("http://localhost/join", html)
self.assertNotIn("office.example.com", html)


def tearDownModule() -> None:
if not STATE_FILE_EXISTED and STATE_FILE.exists():
STATE_FILE.unlink()


if __name__ == "__main__":
unittest.main()
18 changes: 9 additions & 9 deletions frontend/invite.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>海辛办公室 - 加入邀请</title>
<title>{{OFFICE_NAME}} - 加入邀请</title>
<style>
body {
margin: 0;
Expand Down Expand Up @@ -111,8 +111,8 @@
</head>
<body>
<div class="card">
<h1>✨ 海辛办公室 · 加入邀请</h1>
<p>欢迎加入海辛的像素办公室看板!</p>
<h1>✨ {{OFFICE_NAME}} · 加入邀请</h1>
<p>欢迎加入 {{OFFICE_NAME}}!</p>

<h2>加入步骤(一共 3 步)</h2>
<div class="steps">
Expand All @@ -122,7 +122,7 @@ <h2>加入步骤(一共 3 步)</h2>
<strong>确认信息</strong><br>
你应该已经收到两样东西:
<ul>
<li>邀请链接:<code>https://office.example.com/join</code></li>
<li>邀请链接:<code>{{JOIN_URL}}</code></li>
<li>一次性接入密钥(join key):<code>ocj_xxx</code></li>
</ul>
</div>
Expand All @@ -131,14 +131,14 @@ <h2>加入步骤(一共 3 步)</h2>
<div class="step-num">2</div>
<div class="step-text">
<strong>把邀请信息丢给你的 OpenClaw</strong><br>
把邀请链接 + join key 一起发给你的 OpenClaw,并说“帮我加入海辛办公室”。
把邀请链接 + join key 一起发给你的 OpenClaw,并说“帮我加入 {{OFFICE_NAME}}”。
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-text">
<strong>在你这边授权</strong><br>
你的 OpenClaw 会在对话里向你要授权;同意后,它就会开始自动把工作状态推送到海辛办公室看板啦
你的 OpenClaw 会在对话里向你要授权;同意后,它就会开始自动把工作状态推送到 {{OFFICE_NAME}} 啦
</div>
</div>
</div>
Expand All @@ -148,11 +148,11 @@ <h2>加入步骤(一共 3 步)</h2>
只推送状态(idle/writing/researching/executing/syncing/error),不含任何具体内容/隐私;随时可停。
</div>

<a href="/" class="back-btn">← 回到海辛办公室</a>
<a href="/" class="back-btn">← 回到 {{OFFICE_NAME}}</a>

<div class="footer">
海辛工作室 · 像素办公室看板<br>
有问题找海辛 😊
{{OFFICE_NAME}} · 像素办公室看板<br>
有问题找办公室主人 😊
</div>
</div>
</body>
Expand Down
6 changes: 3 additions & 3 deletions frontend/join.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>加入 Star 的像素办公室</title>
<title>加入 {{OFFICE_NAME}}</title>
<style>
@font-face {
font-family: 'ArkPixel';
Expand Down Expand Up @@ -100,7 +100,7 @@
</style>
</head>
<body>
<h1>⭐ 加入 Star 的像素办公室</h1>
<h1>⭐ 加入 {{OFFICE_NAME}}</h1>
<div class="container">
<div class="form-group">
<label>你的名字(会显示在办公室)</label>
Expand All @@ -120,7 +120,7 @@ <h1>⭐ 加入 Star 的像素办公室</h1>
状态与状态细节会由 agent 后续自动推送同步
<br><br>
📌 邀请说明:
<a href="/invite" style="color:#ffd700; text-decoration: underline;">https://office.example.com/invite</a>
<a href="/invite" style="color:#ffd700; text-decoration: underline;">{{INVITE_URL}}</a>
</div>

<script>
Expand Down