feat: extract address tool into standalone project

This commit is contained in:
kory
2026-10-12 08:21:40 +08:00
commit 2fb416ac33
6 changed files with 1158 additions and 0 deletions
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env python3
import http.server
import json
import os
import random
import re
import socketserver
import string
import threading
import time
import urllib.error
import urllib.request
PORT = int(os.getenv("ADDRESS_TOOL_PORT", "18990"))
WEB_DIR = os.path.dirname(os.path.abspath(__file__))
UPSTREAM_API = "https://www.meiguodizhi.com/api/v1/dz"
FNG_API_KEY = os.getenv("FNG_API_KEY", "").strip()
FNG_BASE_URL = "https://www.fakenamegenerator.com/api/v1"
FIRST_NAMES = [
"James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Charles",
"Christopher", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven", "Paul", "Andrew", "Joshua",
"Kenneth", "Kevin", "Brian", "George", "Edward", "Ronald", "Timothy", "Jason", "Jeffrey", "Ryan",
"Jacob", "Gary", "Nicholas", "Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon",
"Mary", "Patricia", "Jennifer", "Linda", "Elizabeth", "Barbara", "Susan", "Jessica", "Sarah", "Karen",
"Lisa", "Nancy", "Betty", "Margaret", "Sandra", "Ashley", "Kimberly", "Emily", "Donna", "Michelle",
"Carol", "Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", "Laura", "Cynthia", "Kathleen"
]
LAST_NAMES = [
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez",
"Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin",
"Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson",
"Walker", "Young", "Allen", "King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores"
]
US_STATE_CODES = {
"alabama": "AL", "alaska": "AK", "arizona": "AZ", "arkansas": "AR", "california": "CA",
"colorado": "CO", "connecticut": "CT", "delaware": "DE", "florida": "FL", "georgia": "GA",
"hawaii": "HI", "idaho": "ID", "illinois": "IL", "indiana": "IN", "iowa": "IA",
"kansas": "KS", "kentucky": "KY", "louisiana": "LA", "maine": "ME", "maryland": "MD",
"massachusetts": "MA", "michigan": "MI", "minnesota": "MN", "mississippi": "MS", "missouri": "MO",
"montana": "MT", "nebraska": "NE", "nevada": "NV", "new hampshire": "NH", "new jersey": "NJ",
"new mexico": "NM", "new york": "NY", "north carolina": "NC", "north dakota": "ND", "ohio": "OH",
"oklahoma": "OK", "oregon": "OR", "pennsylvania": "PA", "rhode island": "RI", "south carolina": "SC",
"south dakota": "SD", "tennessee": "TN", "texas": "TX", "utah": "UT", "vermont": "VT",
"virginia": "VA", "washington": "WA", "west virginia": "WV", "wisconsin": "WI", "wyoming": "WY",
"district of columbia": "DC"
}
MIN_COOLDOWN_SECONDS = 0.5
REQUEST_TIMEOUT = 10
RATE_LIMIT_COOLDOWN = 15
_lock = threading.Lock()
_is_requesting = False
_last_request_time = 0.0
_cooldown_until = 0.0
def generate_secure_password(length=14):
upper = random.choice(string.ascii_uppercase)
lower = random.choice(string.ascii_lowercase)
digit = random.choice(string.digits)
chars = string.ascii_letters + string.digits
rest = "".join(random.choices(chars, k=length - 3))
pwd_list = list(upper + lower + digit + rest)
random.shuffle(pwd_list)
return "".join(pwd_list)
def resolve_us_state(city_str):
if not city_str:
return ""
c_lower = city_str.strip().lower()
if len(c_lower) == 2 and c_lower.upper() in US_STATE_CODES.values():
return c_lower.upper()
for name, code in US_STATE_CODES.items():
if name in c_lower or c_lower == name:
return code
return ""
def transform_fng_to_dz(item):
name_obj = item.get("name", {})
addr_obj = item.get("address", {})
card_obj = item.get("credit_card", {})
bday_obj = item.get("birthday", {})
height_obj = item.get("height", {})
weight_obj = item.get("weight", {})
first = name_obj.get("first", "")
last = name_obj.get("last", "")
full = name_obj.get("full", f"{first} {last}".strip())
# 格式化过期时间 YYYY-MM -> MM/YYYY
expires_raw = card_obj.get("expires", "")
expires = expires_raw
if "-" in expires_raw:
parts = expires_raw.split("-")
if len(parts) == 2:
expires = f"{parts[1]}/{parts[0]}"
height_str = ""
if height_obj.get("in") and height_obj.get("cm"):
height_str = f"{height_obj.get('in')}\" ({height_obj.get('cm')}cm)"
weight_str = ""
if weight_obj.get("lb") and weight_obj.get("kg"):
weight_str = f"{weight_obj.get('lb')} lbs ({weight_obj.get('kg')}kg)"
transformed = {
"Address": addr_obj.get("street", ""),
"Telephone": item.get("phone", {}).get("number", ""),
"Address_Alias": "",
"Trans_Address": "",
"Trans_Cn_Address": "",
"Fax": "",
"City": addr_obj.get("city", ""),
"Zip_Code": addr_obj.get("postal_code", ""),
"rowkey": item.get("id", ""),
"State": addr_obj.get("state", ""),
"State_Full": addr_obj.get("state", ""),
"Expires": expires,
"Credit_Card_Type": card_obj.get("network", "Visa"),
"Credit_Card_Number": card_obj.get("number", ""),
"CVV2": str(card_obj.get("cvv", "")),
"Full_Name": full,
"Gender": (item.get("gender") or "Male").capitalize(),
"Full_Name_Tran": "",
"Title": name_obj.get("title", "Mr."),
"Birthday": bday_obj.get("date", ""),
"Username": item.get("username", ""),
"Password": item.get("password", ""),
"Height": height_str,
"Weight": weight_str,
"Temporary_mail": item.get("email", ""),
"System": "Windows 11",
"GUID": item.get("id", ""),
"Blood_Type": item.get("blood_type", "O+"),
"Browser_User_Agent": item.get("user_agent", ""),
"Occupation": item.get("occupation", ""),
"Company_Name": item.get("company", ""),
"Social_Security_Number": item.get("national_id", {}).get("value", "") if item.get("national_id") else "",
"First_Name": first,
"Last_Name": last,
"Generated_Password": generate_secure_password(14),
"_source": "fakenamegenerator.com"
}
return transformed
def fetch_from_fng(city_or_state):
url = f"{FNG_BASE_URL}/identities?count=1&country=us"
state_code = resolve_us_state(city_or_state)
if state_code:
url += f"&state={state_code}"
req = urllib.request.Request(
url,
headers={
"Authorization": f"Bearer {FNG_API_KEY}",
"User-Agent": "OpenClaw-AddressTool/2.0"
}
)
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as response:
if response.status == 200:
res_data = json.loads(response.read().decode("utf-8"))
if res_data.get("data") and len(res_data["data"]) > 0:
addr_transformed = transform_fng_to_dz(res_data["data"][0])
return {"status": "ok", "address": addr_transformed}
return None
class ProxyHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=WEB_DIR, **kwargs)
def send_standard_json(self, status_code, data):
self.send_response(status_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("utf-8"))
def do_POST(self):
global _is_requesting, _last_request_time, _cooldown_until
if self.path == "/api/v1/dz" or self.path == "/api/generate":
content_length = int(self.headers.get("Content-Length", 0))
post_data = self.rfile.read(content_length)
now = time.time()
with _lock:
if _is_requesting:
self.send_standard_json(429, {
"status": "busy",
"error_code": "ERR_CONCURRENT_REQUEST",
"message": "正在处理上一条生成请求,请稍候",
"retry_after": 1
})
return
_is_requesting = True
try:
payload = {}
try:
payload = json.loads(post_data.decode("utf-8"))
except Exception:
pass
city = payload.get("city", "")
# 优先调用官方 FakeNameGenerator API
if FNG_API_KEY:
try:
fng_result = fetch_from_fng(city)
if fng_result:
self.send_standard_json(200, fng_result)
return
except Exception as e:
print(f"[WARN] FNG API failed, falling back to upstream: {e}")
# 降级备用:原有的 meiguodizhi.com 接口
req = urllib.request.Request(
UPSTREAM_API,
data=post_data,
headers={
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"Referer": "https://www.meiguodizhi.com/"
}
)
try:
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as response:
raw_body = response.read()
data = json.loads(raw_body.decode("utf-8"))
if data.get("status") == "ok" and "address" in data:
addr = data["address"]
orig_fn = addr.get("Full_Name", "")
# 格式化
cleaned = re.sub(r"[^a-zA-Z\s]", "", orig_fn).strip()
words = [w.capitalize() for w in cleaned.split() if w]
if len(words) >= 2:
fn = f"{words[0]} {words[-1]}"
addr["First_Name"] = words[0]
addr["Last_Name"] = words[-1]
else:
fn = f"{random.choice(FIRST_NAMES)} {random.choice(LAST_NAMES)}"
p = fn.split()
addr["First_Name"] = p[0]
addr["Last_Name"] = p[1]
addr["Full_Name"] = fn
addr["Generated_Password"] = generate_secure_password(14)
addr["_source"] = "meiguodizhi.com"
self.send_standard_json(200, data)
except Exception as e:
self.send_standard_json(500, {
"status": "error",
"message": f"生成失败: {str(e)}"
})
finally:
with _lock:
_is_requesting = False
_last_request_time = time.time()
else:
self.send_error(404, "Not Found")
def do_OPTIONS(self):
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
if __name__ == "__main__":
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("0.0.0.0", PORT), ProxyHandler) as httpd:
print(f"Address tool server running on port {PORT}")
httpd.serve_forever()