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
+4
View File
@@ -0,0 +1,4 @@
# Optional: FakeNameGenerator API key. Leave empty to use the fallback provider.
FNG_API_KEY=
# HTTP listen port (default 18990).
ADDRESS_TOOL_PORT=18990
+9
View File
@@ -0,0 +1,9 @@
.env
.env.*
!.env.example
__pycache__/
*.py[cod]
*.log
*.bak*
.wrangler/
.DS_Store
+43
View File
@@ -0,0 +1,43 @@
# Address Tool
A small Python standard-library HTTP service with a static web UI for address/profile test-data generation.
## Contents
- `server.py` — HTTP server and API proxy/fallback logic.
- `index.html` — browser UI; calls the local `/api/v1/dz` endpoint.
- `.env.example` — documented optional runtime environment variables.
- `deploy/address-tool.service.example` — example systemd unit for this repository layout.
## Requirements
- Python 3.10+ (standard library only; no `pip install` required).
- Network access to configured upstream services if generation is to use them.
- The page currently loads the Cap.js widget from jsDelivr and uses the CAPTCHA endpoint configured in `index.html`; those remain external services, not bundled code.
## Run locally
```sh
cp .env.example .env
# Optional: edit .env and set FNG_API_KEY.
set -a
. ./.env
set +a
python3 server.py
```
The service listens on `0.0.0.0:${ADDRESS_TOOL_PORT:-18990}`. Open `http://127.0.0.1:18990/` on the same host. Do not expose this unauthenticated service directly to the public internet without an appropriate access-control/reverse-proxy layer.
`FNG_API_KEY` is optional. When absent, the service skips the FakeNameGenerator API path and relies on the existing fallback provider. Never commit `.env` or API credentials.
## Existing deployment
The currently running production unit still points to `/root/.openclaw/workspace/address_tool`. This repository is an isolated extraction and does not change or restart that service. Review and test the new copy before migrating the systemd unit.
## External services
- FakeNameGenerator API (optional, when `FNG_API_KEY` is configured)
- `https://www.meiguodizhi.com/api/v1/dz` fallback API
- Cap.js widget/CDN configured in `index.html`
Check each provider's terms, availability, and privacy requirements before production use.
+22
View File
@@ -0,0 +1,22 @@
[Unit]
Description=Address Tool API Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/address-tool
EnvironmentFile=-/etc/address-tool/address-tool.env
ExecStart=/usr/bin/python3 /opt/address-tool/server.py
Restart=on-failure
RestartSec=3
User=address-tool
Group=address-tool
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/address-tool
[Install]
WantedBy=multi-user.target
+802
View File
@@ -0,0 +1,802 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>快速地址与身份生成器</title>
<style>
:root {
--bg: #090d16;
--card-bg: rgba(22, 30, 49, 0.72);
--card-border: rgba(255, 255, 255, 0.12);
--card-inner-border: rgba(255, 255, 255, 0.08);
--primary: #3b82f6;
--primary-hover: #2563eb;
--btn-disabled-bg: #1e293b;
--btn-disabled-text: #475569;
--text: #f8fafc;
--text-muted: #94a3b8;
--text-secondary: #cbd5e1;
--success: #10b981;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: radial-gradient(circle at 50% 0%, #172554 0%, var(--bg) 100%);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px 16px;
-webkit-font-smoothing: antialiased;
}
.container {
width: 100%;
max-width: 520px;
background: var(--card-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--card-border);
border-radius: 24px;
padding: 28px 22px;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.2),
0 24px 48px -12px rgba(0, 0, 0, 0.5),
inset 0 1px 0 0 rgba(255, 255, 255, 0.15);
transition: max-width 0.3s ease;
}
.container.wide-mode {
max-width: 760px;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header h1 {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 6px;
background: linear-gradient(180deg, #ffffff 0%, #94a3b8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header p {
font-size: 13px;
color: var(--text-muted);
}
/* 模式切换器 */
.mode-switch {
display: flex;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--card-inner-border);
border-radius: 12px;
padding: 4px;
margin-bottom: 16px;
gap: 4px;
}
.mode-btn {
flex: 1;
padding: 8px 12px;
border: none;
background: transparent;
color: var(--text-muted);
font-size: 13px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.mode-btn.active {
background: rgba(59, 130, 246, 0.2);
color: #60a5fa;
border: 1px solid rgba(59, 130, 246, 0.4);
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 16px;
}
.controls input {
flex: 1;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--card-inner-border);
border-radius: 12px;
padding: 12px 14px;
color: var(--text);
font-size: 14px;
outline: none;
transition: all 0.2s;
}
.controls input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.cap-wrap {
display: flex;
justify-content: center;
margin-bottom: 16px;
width: 100%;
}
#capWidget {
width: 100%;
--cap-widget-width: 100%;
--cap-widget-height: 56px;
--cap-border-radius: 14px;
--cap-background: rgba(15, 23, 42, 0.65);
--cap-border-color: rgba(255, 255, 255, 0.14);
--cap-color: #f8fafc;
--cap-checkbox-background: rgba(255, 255, 255, 0.08);
--cap-checkbox-border: 1.5px solid rgba(255, 255, 255, 0.22);
--cap-checkbox-border-radius: 8px;
--cap-spinner-background-color: rgba(255, 255, 255, 0.12);
--cap-spinner-color: #3b82f6;
--cap-focus-ring: rgba(59, 130, 246, 0.45);
--cap-troubleshoot-color: #60a5fa;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
#capWidget:hover {
--cap-border-color: rgba(255, 255, 255, 0.24);
--cap-background: rgba(15, 23, 42, 0.8);
}
.btn-main {
width: 100%;
background: var(--primary);
border: none;
border-radius: 12px;
padding: 13px;
color: #fff;
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.btn-main:hover:not(:disabled) {
background: var(--primary-hover);
transform: translateY(-1px);
}
.btn-main:active:not(:disabled) {
transform: translateY(0);
}
.btn-main:disabled {
background: var(--btn-disabled-bg);
color: var(--btn-disabled-text);
cursor: not-allowed;
box-shadow: none;
}
.result-box {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
/* 完整模式网格 */
.grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
}
}
.icon {
width: 14px;
height: 14px;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
vertical-align: -2px;
flex-shrink: 0;
display: inline-block;
margin-right: 4px;
}
.section-title {
font-size: 12px;
font-weight: 700;
color: #60a5fa;
text-transform: uppercase;
letter-spacing: 0.8px;
margin: 12px 0 4px 2px;
display: flex;
align-items: center;
gap: 6px;
}
.field-card {
background: rgba(15, 23, 42, 0.45);
border: 1px solid var(--card-inner-border);
border-radius: 12px;
padding: 11px 14px;
cursor: pointer;
transition: all 0.15s ease;
position: relative;
}
.field-card:hover {
background: rgba(30, 41, 59, 0.6);
border-color: rgba(255, 255, 255, 0.18);
transform: translateY(-1px);
}
.field-card:active {
transform: translateY(0);
}
.field-card .label {
font-size: 11px;
color: var(--text-muted);
margin-bottom: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.field-card .copy-tip {
font-size: 10px;
color: #3b82f6;
opacity: 0;
transition: opacity 0.15s;
}
.field-card:hover .copy-tip {
opacity: 1;
}
.field-card .val {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
word-break: break-all;
user-select: all;
}
.field-card .val.highlight {
color: #f8fafc;
font-weight: 600;
}
.btn-copy-all {
margin-top: 10px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 12px;
color: var(--text);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-copy-all:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.25);
}
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: #0f172a;
border: 1px solid var(--card-border);
color: var(--text);
padding: 10px 18px;
border-radius: 30px;
font-size: 13px;
font-weight: 500;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
opacity: 0;
pointer-events: none;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 9999;
display: flex;
align-items: center;
gap: 8px;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
display: none;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget@0.1.58"></script>
</head>
<body>
<div class="container" id="mainContainer">
<div class="header">
<h1>快速地址与身份生成</h1>
<p>点击一键生成身份、地址与安全信息</p>
</div>
<!-- 模式切换 Tab -->
<div class="mode-switch">
<button class="mode-btn active" id="tabSimple" onclick="switchMode('simple')">
<svg class="icon" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" fill="currentColor" stroke="none"/></svg><span>精简模式</span>
</button>
<button class="mode-btn" id="tabFull" onclick="switchMode('full')">
<svg class="icon" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg><span>完整档案</span>
</button>
</div>
<div class="controls">
<input type="text" id="cityInput" placeholder="输入地区/州(例如 California, TX, NY)" value="Virginia Beach" />
</div>
<div class="cap-wrap">
<cap-widget id="capWidget" data-cap-lang="zh-cn" data-cap-api-endpoint="https://cap.kory.kdns.fr/0817b51627/" data-cap-onsolve="onCapSolve()" onsolve="onCapSolve()"></cap-widget>
</div>
<button id="genBtn" class="btn-main" disabled>
<div id="spinner" class="spinner"></div>
<span id="btnText">点击一键生成</span>
</button>
<!-- 模式一:精简日常结果容器 -->
<div class="result-box" id="simpleResultBox" style="display: none;">
<div class="field-card" onclick="copyText(document.getElementById('s_fullName').innerText, '全名已复制')">
<div class="label">
<span>全名 (Full Name)</span>
<span class="copy-tip">点击复制</span>
</div>
<div class="val highlight" id="s_fullName">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('s_fullAddr').innerText, '完整地址已复制')">
<div class="label">
<span>完整地址 (Address)</span>
<span class="copy-tip">点击复制</span>
</div>
<div class="val highlight" id="s_fullAddr">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('s_phone').innerText, '电话号码已复制')">
<div class="label">
<span>电话号码 (Phone)</span>
<span class="copy-tip">点击复制</span>
</div>
<div class="val" id="s_phone">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('s_password').innerText, '强随机密码已复制')">
<div class="label">
<span>强随机密码 (Password)</span>
<span class="copy-tip">点击复制</span>
</div>
<div class="val highlight" id="s_password">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('s_birthday').innerText, '生日已复制')">
<div class="label">
<span>生日 (Birthday)</span>
<span class="copy-tip">点击复制</span>
</div>
<div class="val" id="s_birthday">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('s_secAnswer').innerText, '密保答案已复制')">
<div class="label">
<span id="s_secQuestion">密保问题 / 答案</span>
<span class="copy-tip">点击复制答案</span>
</div>
<div class="val" id="s_secAnswer">-</div>
</div>
<button class="btn-copy-all" onclick="copySimpleAll()">一键复制精简内容</button>
</div>
<!-- 模式二:完整档案结果容器 -->
<div class="result-box" id="fullResultBox" style="display: none;">
<div class="section-title"><svg class="icon" viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> 个人身份与账户</div>
<div class="grid-2">
<div class="field-card" onclick="copyText(document.getElementById('f_fullName').innerText, '全名已复制')">
<div class="label"><span>全名 (Full Name)</span><span class="copy-tip">复制</span></div>
<div class="val highlight" id="f_fullName">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_splitName').innerText, '姓/名已复制')">
<div class="label"><span>名 / 姓 (First / Last)</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_splitName">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_genderAge').innerText, '性别与生日已复制')">
<div class="label"><span>性别 / 生日 (Birthday)</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_genderAge">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_ssn').innerText, 'SSN已复制')">
<div class="label"><span>社会安全号 (SSN)</span><span class="copy-tip">复制</span></div>
<div class="val highlight" id="f_ssn">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_email').innerText, '临时邮箱已复制')">
<div class="label"><span>邮箱 (Email)</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_email">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_username').innerText, '用户名已复制')">
<div class="label"><span>推荐用户名 (Username)</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_username">-</div>
</div>
</div>
<div class="section-title"><svg class="icon" viewBox="0 0 24 24"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg> 地址详细拆解</div>
<div class="field-card" onclick="copyText(document.getElementById('f_street').innerText, '街道地址已复制')">
<div class="label"><span>街道地址 (Street Address)</span><span class="copy-tip">复制</span></div>
<div class="val highlight" id="f_street">-</div>
</div>
<div class="grid-2">
<div class="field-card" onclick="copyText(document.getElementById('f_city').innerText, '城市已复制')">
<div class="label"><span>城市 (City)</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_city">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_stateZip').innerText, '州与邮编已复制')">
<div class="label"><span>州 / 邮编 (State / Zip)</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_stateZip">-</div>
</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_fullAddr').innerText, '单行完整地址已复制')">
<div class="label"><span>单行完整地址 (Full Line Address)</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_fullAddr">-</div>
</div>
<div class="section-title"><svg class="icon" viewBox="0 0 24 24"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg> 虚拟支付卡信息</div>
<div class="field-card" onclick="copyText(document.getElementById('f_cardNumber').innerText, '信用卡号已复制')">
<div class="label"><span id="f_cardType">信用卡号 (Card Number)</span><span class="copy-tip">复制</span></div>
<div class="val highlight" id="f_cardNumber">-</div>
</div>
<div class="grid-2">
<div class="field-card" onclick="copyText(document.getElementById('f_cardExp').innerText, '到期时间已复制')">
<div class="label"><span>有效期 (Expires)</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_cardExp">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_cardCvv').innerText, 'CVV安全码已复制')">
<div class="label"><span>安全码 (CVV)</span><span class="copy-tip">复制</span></div>
<div class="val highlight" id="f_cardCvv">-</div>
</div>
</div>
<div class="section-title"><svg class="icon" viewBox="0 0 24 24"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg> 辅助与职业资料</div>
<div class="grid-2">
<div class="field-card" onclick="copyText(document.getElementById('f_job').innerText, '公司与职业已复制')">
<div class="label"><span>职业 / 公司 (Job / Co.)</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_job">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_phone').innerText, '电话已复制')">
<div class="label"><span>电话号码 (Phone)</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_phone">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_password').innerText, '随机强密码已复制')">
<div class="label"><span>强随机密码 (Password)</span><span class="copy-tip">复制</span></div>
<div class="val highlight" id="f_password">-</div>
</div>
<div class="field-card" onclick="copyText(document.getElementById('f_physical').innerText, '体貌特征已复制')">
<div class="label"><span>身高 / 体重 / 血型</span><span class="copy-tip">复制</span></div>
<div class="val" id="f_physical">-</div>
</div>
</div>
<button class="btn-copy-all" onclick="copyFullAll()">一键复制完整档案 (TXT 格式)</button>
</div>
</div>
<div id="toast" class="toast">已复制到剪贴板</div>
<script>
const API_URL = "/api/v1/dz";
let isCapSolved = false;
let currentMode = localStorage.getItem("addr_mode") || "simple";
let currentData = null;
const mainContainer = document.getElementById("mainContainer");
const genBtn = document.getElementById("genBtn");
const btnText = document.getElementById("btnText");
const spinner = document.getElementById("spinner");
const cityInput = document.getElementById("cityInput");
const capWidget = document.getElementById("capWidget");
const toast = document.getElementById("toast");
const simpleResultBox = document.getElementById("simpleResultBox");
const fullResultBox = document.getElementById("fullResultBox");
const tabSimple = document.getElementById("tabSimple");
const tabFull = document.getElementById("tabFull");
// 初始化模式
switchMode(currentMode);
function switchMode(mode) {
currentMode = mode;
localStorage.setItem("addr_mode", mode);
if (mode === "full") {
tabFull.classList.add("active");
tabSimple.classList.remove("active");
mainContainer.classList.add("wide-mode");
if (currentData) {
fullResultBox.style.display = "flex";
simpleResultBox.style.display = "none";
}
} else {
tabSimple.classList.add("active");
tabFull.classList.remove("active");
mainContainer.classList.remove("wide-mode");
if (currentData) {
simpleResultBox.style.display = "flex";
fullResultBox.style.display = "none";
}
}
}
function onCapSolve() {
isCapSolved = true;
genBtn.disabled = false;
}
let toastTimer = null;
function showToast(msg, type = "success") {
toast.innerText = msg;
if (type === "warn") toast.style.borderColor = "#f59e0b";
else if (type === "error") toast.style.borderColor = "#ef4444";
else toast.style.borderColor = "#10b981";
toast.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toast.classList.remove("show");
}, 2000);
}
function copyText(text, successMsg) {
if (!text || text === "-") return;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
showToast(successMsg || "已复制到剪贴板");
}).catch(() => fallbackCopy(text, successMsg));
} else {
fallbackCopy(text, successMsg);
}
}
function fallbackCopy(text, successMsg) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
showToast(successMsg || "已复制到剪贴板");
} catch (err) {
showToast("复制失败,请手动选择复制", "error");
}
document.body.removeChild(textarea);
}
function formatAddress(addr) {
const street = (addr.Address || "").trim();
const city = (addr.City || "").trim();
const state = (addr.State || "").trim();
const zip = (addr.Zip_Code || "").trim();
return `${street}, ${city}, ${state} ${zip}`.replace(/^, /, "").trim();
}
function renderData(addr) {
currentData = addr;
// 1. 渲染精简模式数据
document.getElementById("s_fullName").innerText = addr.Full_Name || "-";
document.getElementById("s_fullAddr").innerText = formatAddress(addr) || "-";
document.getElementById("s_phone").innerText = addr.Telephone || "-";
document.getElementById("s_password").innerText = addr.Generated_Password || addr.Password || "-";
document.getElementById("s_birthday").innerText = addr.Birthday ? `${addr.Birthday} (${addr.Gender || ""})` : "-";
document.getElementById("s_secQuestion").innerText = addr.Security_Question || "Mother's Maiden Name (密保)";
document.getElementById("s_secAnswer").innerText = addr.Security_Answer || "Smith";
// 2. 渲染完整模式数据
document.getElementById("f_fullName").innerText = addr.Full_Name || "-";
document.getElementById("f_splitName").innerText = `${addr.First_Name || ""} / ${addr.Last_Name || ""}`.trim() || "-";
document.getElementById("f_genderAge").innerText = `${addr.Gender || "N/A"}, ${addr.Birthday || "N/A"}`;
document.getElementById("f_ssn").innerText = addr.Social_Security_Number || "N/A";
document.getElementById("f_email").innerText = addr.Temporary_mail || "-";
document.getElementById("f_username").innerText = addr.Username || "-";
document.getElementById("f_street").innerText = addr.Address || "-";
document.getElementById("f_city").innerText = addr.City || "-";
document.getElementById("f_stateZip").innerText = `${addr.State || ""} ${addr.Zip_Code || ""}`.trim() || "-";
document.getElementById("f_fullAddr").innerText = formatAddress(addr) || "-";
document.getElementById("f_cardType").innerText = `信用卡 (${addr.Credit_Card_Type || "Card"})`;
document.getElementById("f_cardNumber").innerText = addr.Credit_Card_Number || "-";
document.getElementById("f_cardExp").innerText = addr.Expires || "-";
document.getElementById("f_cardCvv").innerText = addr.CVV2 || "-";
const co = addr.Company_Name ? ` @ ${addr.Company_Name}` : "";
document.getElementById("f_job").innerText = (addr.Occupation || "N/A") + co;
document.getElementById("f_phone").innerText = addr.Telephone || "-";
document.getElementById("f_password").innerText = addr.Generated_Password || addr.Password || "-";
const phy = [];
if (addr.Height) phy.push(addr.Height);
if (addr.Weight) phy.push(addr.Weight);
if (addr.Blood_Type) phy.push(`Blood: ${addr.Blood_Type}`);
document.getElementById("f_physical").innerText = phy.join(" | ") || "N/A";
// 显示对应盒子
if (currentMode === "full") {
fullResultBox.style.display = "flex";
simpleResultBox.style.display = "none";
} else {
simpleResultBox.style.display = "flex";
fullResultBox.style.display = "none";
}
}
function copySimpleAll() {
if (!currentData) return;
const d = currentData;
const text = [
`全名: ${d.Full_Name || ""}`,
`完整地址: ${formatAddress(d)}`,
`电话号码: ${d.Telephone || ""}`,
`强随机密码: ${d.Generated_Password || d.Password || ""}`,
`生日: ${d.Birthday || ""}`,
`密保问题: ${d.Security_Question || "Mother's Maiden Name"}`,
`密保答案: ${d.Security_Answer || "Smith"}`
].join("\n");
copyText(text, "已复制全部精简信息");
}
function copyFullAll() {
if (!currentData) return;
const d = currentData;
const text = [
`=== 个人身份 ===`,
`全名: ${d.Full_Name || ""}`,
`First Name: ${d.First_Name || ""}`,
`Last Name: ${d.Last_Name || ""}`,
`性别/生日: ${d.Gender || ""}, ${d.Birthday || ""}`,
`SSN: ${d.Social_Security_Number || ""}`,
`邮箱: ${d.Temporary_mail || ""}`,
`用户名: ${d.Username || ""}`,
`密码: ${d.Generated_Password || d.Password || ""}`,
``,
`=== 详细地址 ===`,
`街道: ${d.Address || ""}`,
`城市: ${d.City || ""}`,
`州: ${d.State || ""}`,
`邮编: ${d.Zip_Code || ""}`,
`完整地址: ${formatAddress(d)}`,
``,
`=== 支付信息 ===`,
`卡类型: ${d.Credit_Card_Type || ""}`,
`卡号: ${d.Credit_Card_Number || ""}`,
`到期日: ${d.Expires || ""}`,
`CVV: ${d.CVV2 || ""}`,
``,
`=== 辅助资料 ===`,
`电话: ${d.Telephone || ""}`,
`职业/公司: ${d.Occupation || ""} (${d.Company_Name || ""})`,
`身高/体重: ${d.Height || ""} / ${d.Weight || ""}`,
`血型: ${d.Blood_Type || ""}`
].join("\n");
copyText(text, "已复制完整档案 (TXT 格式)");
}
async function fetchAddress() {
if (!isCapSolved) {
showToast("请先完成验证", "warn");
return;
}
const city = cityInput.value.trim();
genBtn.disabled = true;
spinner.style.display = "inline-block";
btnText.innerText = "生成中...";
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 11000);
const res = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
city: city,
path: "/",
method: "refresh"
}),
signal: controller.signal
});
clearTimeout(timeoutId);
const data = await res.json();
if (res.ok && data && data.status === "ok" && data.address) {
renderData(data.address);
} else {
const errMsg = (data && data.message) ? data.message : "未获取到数据,请重试";
showToast(errMsg, "error");
}
} catch (err) {
console.error(err);
if (err.name === "AbortError") {
showToast("请求超时,请稍后重试", "error");
} else {
showToast("网络连接异常,请重试", "error");
}
} finally {
spinner.style.display = "none";
btnText.innerText = "点击一键生成";
isCapSolved = false;
genBtn.disabled = true;
if (capWidget && typeof capWidget.reset === "function") {
capWidget.reset();
}
}
}
genBtn.addEventListener("click", fetchAddress);
cityInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !genBtn.disabled) fetchAddress();
});
</script>
</body>
</html>
+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()