feat: extract address tool into standalone project
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.log
|
||||||
|
*.bak*
|
||||||
|
.wrangler/
|
||||||
|
.DS_Store
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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/[email protected]"></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>
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user