feat: 支持鉴权token

This commit is contained in:
hp0912 2026-08-27 10:52:49 +08:00
parent f4b2c8d51d
commit 43c8637326
15 changed files with 186 additions and 20 deletions

View File

@ -17,6 +17,10 @@ FETCH_API_URL = "https://api.pearapi.ai/api/today_wife"
FALLBACK_TEXT = "今天的美女图片暂时没拿到,等我再找找。"
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def fetch_image_url() -> str | None:
try:
with urllib.request.urlopen(FETCH_API_URL, timeout=10) as response:
@ -52,7 +56,10 @@ def send_image(image_url: str) -> bool:
request = urllib.request.Request(
api_url,
data=body,
headers={"Content-Type": "application/json"},
headers={
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
method="POST",
)
@ -85,4 +92,4 @@ if __name__ == "__main__":
raise
except Exception:
traceback.print_exc(file=sys.stdout)
raise SystemExit(1)
raise SystemExit(1)

View File

@ -31,6 +31,10 @@ def _require_env(name: str) -> str:
return value
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _positive_int_env(name: str) -> int:
raw = _require_env(name)
try:
@ -80,7 +84,10 @@ def _post_json(url: str, body: dict[str, Any]) -> dict[str, Any]:
request = urllib.request.Request(
url,
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json"},
headers={
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
method="POST",
)
try:

View File

@ -43,6 +43,10 @@ class AmbiguousCreateError(RuntimeError):
"""The POST may have reached the server, so retrying could create a duplicate."""
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
class SkillArgumentParser(argparse.ArgumentParser):
def error(self, message: str) -> NoReturn:
raise ValueError(f"参数错误:{message}")
@ -310,7 +314,11 @@ def _pick_unique_member(
def _get_json(url: str, timeout: int = 15) -> dict[str, Any]:
request = urllib.request.Request(url, method="GET")
request = urllib.request.Request(
url,
headers={"X-Private-Token": _client_private_token()},
method="GET",
)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
response_text = response.read().decode("utf-8", errors="replace")
@ -435,7 +443,10 @@ def _post_json(url: str, payload: dict[str, Any], timeout: int = 30) -> dict[str
request = urllib.request.Request(
url,
data=body,
headers={"Content-Type": "application/json"},
headers={
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
method="POST",
)

View File

@ -525,6 +525,10 @@ def _raise_for_client_error(payload: dict[str, Any]) -> None:
raise RuntimeError(message)
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _send_file(client_port: str, chat_room_id: str, path: Path) -> None:
url = (
f"http://127.0.0.1:{client_port}"
@ -536,7 +540,10 @@ def _send_file(client_port: str, chat_room_id: str, path: Path) -> None:
request = urllib.request.Request(
url,
data=body,
headers={"Content-Type": "application/json"},
headers={
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
method="POST",
)
try:

View File

@ -190,6 +190,10 @@ def _client_base_url(client_port: str) -> str:
return f"http://127.0.0.1:{client_port}"
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _build_url(base_url: str, path: str, params: dict[str, object] | None = None) -> str:
url = f"{base_url}{path}"
if not params:
@ -199,8 +203,13 @@ def _build_url(base_url: str, path: str, params: dict[str, object] | None = None
def _http_get_bytes(url: str, timeout: int = 300) -> tuple[bytes, dict[str, str]]:
request = urllib.request.Request(
url,
headers={"X-Private-Token": _client_private_token()},
method="GET",
)
try:
with urllib.request.urlopen(url, timeout=timeout) as resp:
with urllib.request.urlopen(request, timeout=timeout) as resp:
headers = {key.lower(): value for key, value in resp.headers.items()}
return resp.read(), headers
except urllib.error.HTTPError as exc:
@ -240,7 +249,10 @@ def _http_post_multipart(
req = urllib.request.Request(
url,
data=body,
headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"X-Private-Token": _client_private_token(),
},
method="POST",
)
try:

View File

@ -159,6 +159,10 @@ def load_drawing_settings(conn, from_wx_id: str) -> tuple[bool, dict]:
# API callers
# ---------------------------------------------------------------------------
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _http_post_json(url: str, body: dict, headers: dict, timeout: int = 300) -> dict:
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
@ -349,7 +353,15 @@ def _send_image_outputs(client_port: str, from_wx_id: str, image_outputs: list[s
"to_wxid": from_wx_id,
"image_urls": remote_urls,
}
response = _http_post_json(send_url, send_body, {"Content-Type": "application/json"}, timeout=300)
response = _http_post_json(
send_url,
send_body,
{
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
timeout=300,
)
_debug_response("send image url response", response)
for file_path in local_paths:
@ -358,7 +370,15 @@ def _send_image_outputs(client_port: str, from_wx_id: str, image_outputs: list[s
"to_wxid": from_wx_id,
"file_path": file_path,
}
response = _http_post_json(send_url, send_body, {"Content-Type": "application/json"}, timeout=300)
response = _http_post_json(
send_url,
send_body,
{
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
timeout=300,
)
_debug_response("send image local response", response)

View File

@ -62,12 +62,19 @@ EMOJI_MAP: dict[str, dict[str, object]] = {
}
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _http_post_json(url: str, body: dict, timeout: int = 300) -> dict:
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
headers={"Content-Type": "application/json"},
headers={
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as resp:

View File

@ -15,6 +15,10 @@ sys.stderr = sys.stdout
MAX_FILE_SIZE = 25 * 1024 * 1024
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _raise_for_client_error(response: dict) -> None:
if not response:
return
@ -40,7 +44,10 @@ def _http_post_json(url: str, body: dict, timeout: int = 300) -> dict:
req = urllib.request.Request(
url,
data=data,
headers={"Content-Type": "application/json"},
headers={
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as resp:

View File

@ -13,6 +13,10 @@ from pathlib import Path
sys.stderr = sys.stdout
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _raise_for_client_error(response: dict) -> None:
if not response:
return
@ -38,7 +42,10 @@ def _http_post_json(url: str, body: dict, timeout: int = 300) -> dict:
req = urllib.request.Request(
url,
data=data,
headers={"Content-Type": "application/json"},
headers={
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as resp:

View File

@ -14,6 +14,10 @@ from pathlib import Path
sys.stderr = sys.stdout
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _skill_root() -> Path:
return Path(__file__).resolve().parent.parent
@ -94,7 +98,10 @@ def _http_post_json(url: str, body: dict, timeout: int = 300) -> dict:
req = urllib.request.Request(
url,
data=data,
headers={"Content-Type": "application/json"},
headers={
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as resp:

View File

@ -161,6 +161,10 @@ def load_drawing_settings(conn, from_wx_id: str) -> tuple[bool, dict]:
# API callers
# ---------------------------------------------------------------------------
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _http_post_json(url: str, body: dict, headers: dict, timeout: int = 300) -> dict:
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
@ -360,7 +364,15 @@ def _send_image_outputs(client_port: str, from_wx_id: str, image_outputs: list[s
"to_wxid": from_wx_id,
"image_urls": remote_urls,
}
response = _http_post_json(send_url, send_body, {"Content-Type": "application/json"}, timeout=300)
response = _http_post_json(
send_url,
send_body,
{
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
timeout=300,
)
_debug_response("send image url response", response)
for file_path in local_paths:
@ -369,7 +381,15 @@ def _send_image_outputs(client_port: str, from_wx_id: str, image_outputs: list[s
"to_wxid": from_wx_id,
"file_path": file_path,
}
response = _http_post_json(send_url, send_body, {"Content-Type": "application/json"}, timeout=300)
response = _http_post_json(
send_url,
send_body,
{
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
timeout=300,
)
_debug_response("send image local response", response)

View File

@ -30,6 +30,10 @@ DEFAULT_RESOLUTION = "720p"
DEFAULT_DURATION = 5
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _skill_root() -> Path:
script_dir = Path(__file__).resolve().parent
return script_dir.parent
@ -193,7 +197,15 @@ def send_videos(from_wx_id: str, video_urls: list[str]) -> None:
"to_wxid": from_wx_id,
"video_urls": [url for url in video_urls if url],
}
_http_post_json(send_url, send_body, {"Content-Type": "application/json"}, timeout=60)
_http_post_json(
send_url,
send_body,
{
"Content-Type": "application/json",
"X-Private-Token": _client_private_token(),
},
timeout=60,
)
def call_jimeng_video(

View File

@ -59,6 +59,10 @@ MAX_CONTENT_LENGTH = 260
STREAM_END_CODE = 20000000
def _client_private_token() -> str:
return os.environ.get("ROBOT_CLIENT_PRIVATE_TOKEN", "").strip()
def _skill_root() -> Path:
return Path(__file__).resolve().parent.parent
@ -252,7 +256,11 @@ def _download_referenced_voice_clone(message_id: str) -> str:
f"http://127.0.0.1:{client_port}/api/v1/robot/chat/voice/download"
f"?message_id={encoded_message_id}"
)
req = urllib.request.Request(download_url, method="GET")
req = urllib.request.Request(
download_url,
headers={"X-Private-Token": _client_private_token()},
method="GET",
)
try:
with urllib.request.urlopen(req, timeout=60) as response:
wav_data = response.read()
@ -836,7 +844,10 @@ def send_voice(from_wx_id: str, audio_data: bytes, audio_format: str) -> None:
req = urllib.request.Request(
send_url,
data=body,
headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
headers={
"Content-Type": f"multipart/form-data; boundary={boundary}",
"X-Private-Token": _client_private_token(),
},
method="POST",
)
try:
@ -954,4 +965,4 @@ if __name__ == "__main__":
raise
except Exception:
traceback.print_exc(file=sys.stdout)
raise SystemExit(1)
raise SystemExit(1)

View File

@ -1703,6 +1703,9 @@ function postJson(
timeoutMs: number,
): Promise<string> {
return new Promise<string>((resolve, reject) => {
const privateToken = (
process.env.ROBOT_CLIENT_PRIVATE_TOKEN ?? ""
).trim();
const parsed = new URL(url);
const payload = Buffer.from(JSON.stringify(body), "utf8");
const request = http.request(
@ -1714,6 +1717,7 @@ function postJson(
headers: {
"Content-Type": "application/json",
"Content-Length": payload.length,
"X-Private-Token": privateToken,
},
timeout: timeoutMs,
},

View File

@ -0,0 +1,27 @@
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
class PrivateTokenHeaderTests(unittest.TestCase):
def test_every_local_client_api_script_supports_optional_private_token(self) -> None:
client_api_scripts: list[Path] = []
for pattern in ("*.py", "*.ts"):
for script in (REPO_ROOT / "skills").glob(f"*/scripts/{pattern}"):
content = script.read_text(encoding="utf-8")
if "/api/v1/robot" not in content:
continue
client_api_scripts.append(script)
self.assertIn("ROBOT_CLIENT_PRIVATE_TOKEN", content, script)
self.assertIn("X-Private-Token", content, script)
self.assertNotIn(
"环境变量 ROBOT_CLIENT_PRIVATE_TOKEN 未配置", content, script
)
self.assertTrue(client_api_scripts, "no local client API scripts were found")
if __name__ == "__main__":
unittest.main()