feat: 完成2026-06-09版本迭代更新

此版本包含:
1. 新增位置消息发送与展示功能
2. 完善多语言本地化文案
3. 新增安卓端管理空间Activity与图标背景
4. 优化摇一摇开关逻辑与深度链接配置
5. 新增信息流平台过滤与A/B测试后台功能
6. 更新签名配置与构建脚本
7. 修复若干已知问题与代码优化
This commit is contained in:
Developer
2026-06-09 23:18:13 +08:00
parent e53cd7f496
commit a4a7e10722
119 changed files with 10758 additions and 3893 deletions

19
Scripts/read_db_config.py Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""读取服务器数据库配置"""
import paramiko
HOST = '123.207.67.197'
PORT = 22
USER = 'root'
PASS = '520Kiss123'
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, port=PORT, username=USER, password=PASS, timeout=15)
# 读取数据库配置
stdin, stdout, stderr = ssh.exec_command('cat /www/wwwroot/tools.wktyl.com/application/database.php')
print(stdout.read().decode())
ssh.close()

View File

@@ -0,0 +1,221 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@name 平台过滤接口测试脚本
@desc 测试后台全平台/全分类关闭后API接口是否正确返回空数据
@created 2026-06-09
@updated 2026-06-09
"""
import requests
import json
import sys
import time
BASE_URL = "https://tools.wktyl.com"
PLATFORM = "android"
results = {"passed": 0, "failed": 0}
def record_pass():
results["passed"] += 1
def record_fail():
results["failed"] += 1
def test_api(name, url, expect_empty_list=False, expect_empty_channels=False, check_platform_field=False, headers=None):
"""测试API接口"""
try:
h = headers or {}
resp = requests.get(url, headers=h, timeout=20)
data = resp.json()
code = data.get("code", 0)
if code != 1:
print(f"{name}: 接口返回错误 code={code}, msg={data.get('msg', '')}")
record_fail()
return data
result = data.get("data", {})
if expect_empty_list:
items = result.get("list", [])
if len(items) > 0:
print(f"{name}: 期望空列表,但返回了 {len(items)} 条数据")
record_fail()
else:
print(f"{name}: 正确返回空列表")
record_pass()
if expect_empty_channels:
channels = result.get("channels", [])
if len(channels) > 0:
print(f"{name}: 期望空频道列表,但返回了 {len(channels)} 个频道")
record_fail()
else:
print(f"{name}: 正确返回空频道列表")
record_pass()
if check_platform_field:
plat = result.get("platform", "")
if plat == PLATFORM:
print(f"{name}: platform字段正确 ({plat})")
record_pass()
else:
print(f"{name}: platform字段不正确 (期望={PLATFORM}, 实际={plat})")
record_fail()
return result
except Exception as e:
print(f"{name}: 请求异常 - {e}")
record_fail()
return None
def test_data_count(name, url, headers=None):
"""测试接口返回的数据条数"""
try:
h = headers or {}
resp = requests.get(url, headers=h, timeout=20)
data = resp.json()
if data.get("code") != 1:
print(f"{name}: 接口返回错误 code={data.get('code')}")
record_fail()
return -1
result = data.get("data", {})
items = result.get("list", [])
count = len(items)
print(f"{name}: 返回 {count} 条数据")
record_pass()
return count
except Exception as e:
print(f"{name}: 请求异常 - {e}")
record_fail()
return -1
def main():
print("=" * 60)
print("平台过滤接口测试")
print(f"测试平台: {PLATFORM}")
print(f"基础URL: {BASE_URL}")
print("=" * 60)
headers_with_platform = {"X-Platform": PLATFORM}
# ============================================================
# 第一步:测试正常状态(有启用分类时)
# ============================================================
print("\n--- 第一步:测试正常状态(有启用分类) ---")
print("\n[1] channels接口 - platform参数")
test_api("channels(platform)",
f"{BASE_URL}/api/feed/channels?platform={PLATFORM}",
check_platform_field=True)
print("\n[2] list接口 - channel=all + platform")
test_data_count("list(all+platform)",
f"{BASE_URL}/api/feed/list?channel=all&platform={PLATFORM}&limit=5")
print("\n[3] mix接口 - platform参数")
test_data_count("mix(+platform)",
f"{BASE_URL}/api/feed/mix?platform={PLATFORM}&limit=5")
print("\n[4] trending接口 - platform参数")
test_data_count("trending(+platform)",
f"{BASE_URL}/api/feed/trending?platform={PLATFORM}&limit=5")
print("\n[5] recommend接口 - platform参数")
test_data_count("recommend(+platform)",
f"{BASE_URL}/api/feed/recommend?platform={PLATFORM}&limit=5")
print("\n[6] random接口 - platform参数")
test_data_count("random(+platform)",
f"{BASE_URL}/api/feed/random?platform={PLATFORM}&limit=5")
print("\n[7] list接口 - 指定频道poetry + platform")
test_data_count("list(poetry+platform)",
f"{BASE_URL}/api/feed/list?channel=poetry&platform={PLATFORM}&limit=5")
# ============================================================
# 第二步测试向后兼容性不传platform参数
# ============================================================
print("\n--- 第二步测试向后兼容性不传platform ---")
print("\n[8] channels接口 - 无platform参数")
test_data_count("channels(无platform)",
f"{BASE_URL}/api/feed/channels")
print("\n[9] list接口 - 无platform参数")
test_data_count("list(无platform)",
f"{BASE_URL}/api/feed/list?channel=all&limit=5")
# ============================================================
# 第三步测试X-Platform请求头
# ============================================================
print("\n--- 第三步测试X-Platform请求头 ---")
test_data_count("channels(X-Platform头)",
f"{BASE_URL}/api/feed/channels",
headers=headers_with_platform)
test_data_count("list(X-Platform头)",
f"{BASE_URL}/api/feed/list?channel=all&limit=5",
headers=headers_with_platform)
# ============================================================
# 第四步测试platform_config接口
# ============================================================
print("\n--- 第四步测试platform_config接口 ---")
try:
resp = requests.get(f"{BASE_URL}/api/feed/platform_config?platform={PLATFORM}", timeout=15)
data = resp.json()
if data.get("code") == 1:
result = data.get("data", {})
enabled = result.get("enabled_count", 0)
disabled = result.get("disabled_count", 0)
print(f" ✓ platform_config: 启用={enabled}, 禁用={disabled}")
record_pass()
else:
print(f" ✗ platform_config: 接口返回错误")
record_fail()
except Exception as e:
print(f" ✗ platform_config: 请求异常 - {e}")
record_fail()
# ============================================================
# 第五步:测试无效平台参数(应忽略,正常返回数据)
# ============================================================
print("\n--- 第五步:测试无效平台参数 ---")
try:
resp = requests.get(f"{BASE_URL}/api/feed/list?channel=all&platform=invalid_platform&limit=5", timeout=15)
data = resp.json()
items = data.get("data", {}).get("list", [])
print(f" ✓ 无效platform参数: 返回 {len(items)} 条数据(应忽略无效平台)")
record_pass()
except Exception as e:
print(f" ✗ 无效platform参数: 请求异常 - {e}")
record_fail()
# ============================================================
# 结果汇总
# ============================================================
print("\n" + "=" * 60)
print(f"测试结果: ✓ 通过={results['passed']}, ✗ 失败={results['failed']}")
print("=" * 60)
if results["failed"] > 0:
print("\n⚠ 有测试失败,请检查!")
sys.exit(1)
else:
print("\n✓ 全部测试通过!")
sys.exit(0)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,267 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@name 平台过滤核心场景测试SSH版
@desc 通过SSH操作数据库关闭所有平台后验证API返回空数据然后恢复
@created 2026-06-09
@updated 2026-06-09
"""
import requests
import json
import sys
import time
import paramiko
BASE_URL = "https://tools.wktyl.com"
PLATFORM = "android"
# 服务器配置
HOST = '123.207.67.197'
PORT = 22
USER = 'root'
PASS = '520Kiss123'
results = {"passed": 0, "failed": 0}
def record_pass():
results["passed"] += 1
def record_fail():
results["failed"] += 1
def ssh_exec(cmd):
"""执行SSH命令"""
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, port=PORT, username=USER, password=PASS, timeout=15)
stdin, stdout, stderr = ssh.exec_command(cmd)
out = stdout.read().decode()
err = stderr.read().decode()
ssh.close()
return out, err
def clear_cache():
"""清除服务器运行时缓存"""
ssh_exec('rm -rf /www/wwwroot/tools.wktyl.com/runtime/cache/*')
def backup_platform_enabled():
"""备份当前platform_enabled字段"""
cmd = """mysql -u tools -p'tools' tools -N -e "SELECT GROUP_CONCAT(CONCAT(id,':',platform_enabled) SEPARATOR '|||') FROM tool_feed_weight_config" """
out, err = ssh_exec(cmd)
return out.strip()
def close_all_platforms():
"""关闭所有平台 - 将所有记录的platform_enabled设为全false"""
cmd = """mysql -u tools -p'tools' tools -e "UPDATE tool_feed_weight_config SET platform_enabled='{\\\"android\\\":false,\\\"ios\\\":false,\\\"harmony\\\":false,\\\"macos\\\":false,\\\"win\\\":false,\\\"web\\\":false,\\\"other\\\":false}'" """
out, err = ssh_exec(cmd)
if err and "ERROR" in err:
print(f" ✗ 关闭所有平台失败: {err[:200]}")
return False
print(" ✓ 所有平台已关闭")
return True
def restore_platform_enabled(backup):
"""恢复platform_enabled字段"""
if not backup or backup == "NULL" or backup == "":
print(" ⚠ 无备份数据,使用全开启恢复")
cmd = """mysql -u tools -p'tools' tools -e "UPDATE tool_feed_weight_config SET platform_enabled='{\\\"android\\\":true,\\\"ios\\\":true,\\\"harmony\\\":true,\\\"macos\\\":true,\\\"win\\\":true,\\\"web\\\":true,\\\"other\\\":true}'" """
out, err = ssh_exec(cmd)
return
# 逐条恢复
pairs = backup.split("|||")
for pair in pairs:
if ":" not in pair:
continue
parts = pair.split(":", 1)
id_val = parts[0].strip()
pe_val = parts[1].strip() if len(parts) > 1 else ""
# 转义单引号
pe_escaped = pe_val.replace("'", "\\'")
if not pe_escaped or pe_escaped == "NULL":
pe_escaped = ""
cmd = f"""mysql -u tools -p'tools' tools -e "UPDATE tool_feed_weight_config SET platform_enabled='{pe_escaped}' WHERE id={id_val}" """
ssh_exec(cmd)
print(" ✓ 平台设置已恢复")
def test_empty_result(name, url):
"""测试接口是否返回空列表"""
try:
resp = requests.get(url, timeout=20)
data = resp.json()
if data.get("code") != 1:
print(f"{name}: 接口返回错误 code={data.get('code')}, msg={data.get('msg', '')}")
record_fail()
return
result = data.get("data", {})
items = result.get("list", result.get("channels", []))
count = len(items)
if count == 0:
print(f"{name}: 正确返回空数据 (0条)")
record_pass()
else:
print(f"{name}: 期望空数据,但返回了 {count}")
# 打印前2条数据的feed_type帮助调试
for i, item in enumerate(items[:2]):
print(f" 样本[{i}]: feed_type={item.get('feed_type', '?')}, title={item.get('title', '?')[:30]}")
record_fail()
except Exception as e:
print(f"{name}: 请求异常 - {e}")
record_fail()
def test_has_data(name, url):
"""测试接口是否返回数据"""
try:
resp = requests.get(url, timeout=20)
data = resp.json()
if data.get("code") != 1:
print(f"{name}: 接口返回错误 code={data.get('code')}")
record_fail()
return
result = data.get("data", {})
items = result.get("list", result.get("channels", []))
count = len(items)
if count > 0:
print(f"{name}: 正确返回数据 ({count}条)")
record_pass()
else:
print(f"{name}: 期望有数据但返回了0条")
record_fail()
except Exception as e:
print(f"{name}: 请求异常 - {e}")
record_fail()
def main():
print("=" * 60)
print("平台过滤核心场景测试SSH版")
print(f"测试场景: 后台全平台关闭 → API应返回空数据")
print(f"测试平台: {PLATFORM}")
print("=" * 60)
# 备份当前设置
print("\n--- 备份当前平台设置 ---")
backup = backup_platform_enabled()
print(f" 备份完成 ({len(backup)} 字符)")
try:
# ============================================================
# 核心测试:全平台关闭
# ============================================================
print("\n--- 核心测试:关闭所有平台 ---")
if not close_all_platforms():
print("关闭平台失败,终止测试")
sys.exit(1)
# 清除缓存
print("\n 清除服务器缓存...")
clear_cache()
time.sleep(2)
print("\n--- 验证全平台关闭后API应返回空数据 ---")
print("\n[1] channels接口 - platform=android")
test_empty_result("channels(全关闭)",
f"{BASE_URL}/api/feed/channels?platform={PLATFORM}")
print("\n[2] list接口 - channel=all + platform=android")
test_empty_result("list(全关闭)",
f"{BASE_URL}/api/feed/list?channel=all&platform={PLATFORM}&limit=5")
print("\n[3] mix接口 - platform=android")
test_empty_result("mix(全关闭)",
f"{BASE_URL}/api/feed/mix?platform={PLATFORM}&limit=5")
print("\n[4] trending接口 - platform=android")
test_empty_result("trending(全关闭)",
f"{BASE_URL}/api/feed/trending?platform={PLATFORM}&limit=5")
print("\n[5] recommend接口 - platform=android")
test_empty_result("recommend(全关闭)",
f"{BASE_URL}/api/feed/recommend?platform={PLATFORM}&limit=5")
print("\n[6] random接口 - platform=android")
test_empty_result("random(全关闭)",
f"{BASE_URL}/api/feed/random?platform={PLATFORM}&limit=5")
print("\n[7] list接口 - 指定频道poetry + platform=android")
test_empty_result("list(poetry+全关闭)",
f"{BASE_URL}/api/feed/list?channel=poetry&platform={PLATFORM}&limit=5")
print("\n[8] platform_config接口 - platform=android")
try:
resp = requests.get(f"{BASE_URL}/api/feed/platform_config?platform={PLATFORM}", timeout=15)
data = resp.json()
if data.get("code") == 1:
result = data.get("data", {})
enabled = result.get("enabled_count", 0)
if enabled == 0:
print(f" ✓ platform_config: 启用分类=0 (正确)")
record_pass()
else:
print(f" ✗ platform_config: 启用分类={enabled} (期望0)")
record_fail()
except Exception as e:
print(f" ✗ platform_config: 请求异常 - {e}")
record_fail()
# ============================================================
# 恢复:恢复平台设置
# ============================================================
print("\n--- 恢复:恢复平台设置 ---")
restore_platform_enabled(backup)
# 清除缓存
print("\n 清除服务器缓存...")
clear_cache()
time.sleep(2)
print("\n--- 验证恢复后API应返回数据 ---")
print("\n[9] channels接口 - 恢复后")
test_has_data("channels(恢复后)",
f"{BASE_URL}/api/feed/channels?platform={PLATFORM}")
print("\n[10] list接口 - 恢复后")
test_has_data("list(恢复后)",
f"{BASE_URL}/api/feed/list?channel=all&platform={PLATFORM}&limit=5")
except Exception as e:
print(f"\n⚠ 测试过程异常: {e}")
# 尝试恢复
print("尝试恢复平台设置...")
restore_platform_enabled(backup)
clear_cache()
# ============================================================
# 结果汇总
# ============================================================
print("\n" + "=" * 60)
print(f"测试结果: ✓ 通过={results['passed']}, ✗ 失败={results['failed']}")
print("=" * 60)
if results["failed"] > 0:
print("\n⚠ 有测试失败,请检查!")
sys.exit(1)
else:
print("\n✓ 全部测试通过!")
sys.exit(0)
if __name__ == '__main__':
main()

72
Scripts/upload_ab_test.py Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""上传A/B测试相关文件到服务器"""
import paramiko
import os
HOST = '123.207.67.197'
USER = 'root'
PASS = '520Kiss123'
LOCAL_BASE = r'e:\project\flutter\f\xianyan\docs\toolsapi\application'
REMOTE_BASE = '/www/wwwroot/tools.wktyl.com/application/'
JS_LOCAL = r'e:\project\flutter\f\xianyan\docs\toolsapi\public\assets\js\backend'
JS_REMOTE = '/www/wwwroot/tools.wktyl.com/public/assets/js/backend/'
FILES = [
('admin/controller/AbTest.php', 'admin/controller/AbTest.php'),
('admin/view/ab_test/index.html', 'admin/view/ab_test/index.html'),
('admin/view/ab_test/add.html', 'admin/view/ab_test/add.html'),
('admin/view/ab_test/edit.html', 'admin/view/ab_test/edit.html'),
('admin/lang/zh-cn/ab_test.php', 'admin/lang/zh-cn/ab_test.php'),
('api/controller/Feed.php', 'api/controller/Feed.php'),
]
JS_FILES = [
('ab_test.js', 'ab_test.js'),
('feed_weight.js', 'feed_weight.js'),
]
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, port=22, username=USER, password=PASS, timeout=15)
sftp = ssh.open_sftp()
success = 0
for local_name, remote_name in FILES:
local_path = os.path.join(LOCAL_BASE, local_name)
remote_path = REMOTE_BASE + remote_name
if not os.path.exists(local_path):
print(f" SKIP: {local_name} (not found)")
continue
# 创建远程目录
remote_dir = os.path.dirname(remote_path).replace('\\', '/')
try:
sftp.stat(remote_dir)
except:
ssh.exec_command(f'mkdir -p {remote_dir}')
import time; time.sleep(0.5)
try:
sftp.put(local_path, remote_path)
print(f" OK: {local_name}")
success += 1
except Exception as e:
print(f" FAIL: {local_name}: {e}")
for local_name, remote_name in JS_FILES:
local_path = os.path.join(JS_LOCAL, local_name)
remote_path = JS_REMOTE + remote_name
if not os.path.exists(local_path):
print(f" SKIP: {local_name} (not found)")
continue
try:
sftp.put(local_path, remote_path)
print(f" OK: JS/{local_name}")
success += 1
except Exception as e:
print(f" FAIL: JS/{local_name}: {e}")
# 清除缓存
ssh.exec_command('rm -rf /www/wwwroot/tools.wktyl.com/runtime/cache/* /www/wwwroot/tools.wktyl.com/runtime/temp/*')
sftp.close()
ssh.close()
print(f"\nDone: {success}/{len(FILES)+len(JS_FILES)}")

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@name 上传Feed控制器到服务器
@desc SFTP上传修复后的Feed.php到服务器
@created 2026-06-09
@updated 2026-06-09
"""
import paramiko
import os
# 服务器配置
HOST = '123.207.67.197'
PORT = 22
USER = 'root'
PASS = '520Kiss123'
# 本地文件 -> 远程路径映射
UPLOAD_MAP = {
r'docs\toolsapi\application\api\controller\Feed.php': '/www/wwwroot/tools.wktyl.com/application/api/controller/Feed.php',
r'docs\toolsapi\application\admin\controller\FeedWeight.php': '/www/wwwroot/tools.wktyl.com/application/admin/controller/FeedWeight.php',
r'docs\toolsapi\application\admin\view\feed_weight\index.html': '/www/wwwroot/tools.wktyl.com/application/admin/view/feed_weight/index.html',
r'docs\toolsapi\public\assets\js\backend\feed_weight.js': '/www/wwwroot/tools.wktyl.com/public/assets/js/backend/feed_weight.js',
}
def upload():
local_base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
print(f"本地项目根目录: {local_base}")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
print(f"连接服务器 {HOST}:{PORT}...")
ssh.connect(HOST, port=PORT, username=USER, password=PASS, timeout=15)
sftp = ssh.open_sftp()
print("连接成功!")
for local_rel, remote_path in UPLOAD_MAP.items():
local_path = os.path.join(local_base, local_rel)
if not os.path.exists(local_path):
print(f"[跳过] 本地文件不存在: {local_path}")
continue
# 确保远程目录存在
remote_dir = os.path.dirname(remote_path)
try:
sftp.stat(remote_dir)
except FileNotFoundError:
print(f" 创建远程目录: {remote_dir}")
print(f"上传: {local_rel} -> {remote_path}")
sftp.put(local_path, remote_path)
print(f" ✓ 上传成功")
sftp.close()
# 清除服务器缓存
print("\n清除服务器运行时缓存...")
stdin, stdout, stderr = ssh.exec_command('rm -rf /www/wwwroot/tools.wktyl.com/runtime/cache/*')
print(f" 缓存清除: {stdout.read().decode()}")
ssh.close()
print("\n✓ 全部上传完成!")
if __name__ == '__main__':
upload()

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""上传平台过滤相关PHP代码到服务器"""
import paramiko
import os
HOST = '123.207.67.197'
PORT = 22
USER = 'root'
PASS = '520Kiss123'
REMOTE_BASE = '/www/wwwroot/tools.wktyl.com/application/'
LOCAL_BASE = os.path.join(os.path.dirname(__file__), '..', 'docs', 'toolsapi', 'application')
UPLOAD_FILES = [
('admin/controller/FeedWeight.php', 'admin/controller/FeedWeight.php'),
('admin/model/FeedWeightConfig.php', 'admin/model/FeedWeightConfig.php'),
('admin/view/feed_weight/index.html', 'admin/view/feed_weight/index.html'),
('admin/view/feed_weight/edit.html', 'admin/view/feed_weight/edit.html'),
('admin/lang/zh-cn/feed_weight.php', 'admin/lang/zh-cn/feed_weight.php'),
('api/controller/Feed.php', 'api/controller/Feed.php'),
]
# JS文件在不同目录
JS_LOCAL_BASE = os.path.join(os.path.dirname(__file__), '..', 'docs', 'toolsapi', 'public', 'assets', 'js', 'backend')
JS_REMOTE_BASE = '/www/wwwroot/tools.wktyl.com/public/assets/js/backend/'
JS_FILES = [
('feed_weight.js', 'feed_weight.js'),
]
def main():
local_base = os.path.abspath(LOCAL_BASE)
print(f"本地代码目录: {local_base}")
print(f"待上传文件: {len(UPLOAD_FILES)}\n")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, port=PORT, username=USER, password=PASS, timeout=15)
sftp = ssh.open_sftp()
success_count = 0
for local_rel, remote_rel in UPLOAD_FILES:
local_path = os.path.join(local_base, local_rel.replace('/', os.sep))
remote_path = REMOTE_BASE + remote_rel
if not os.path.exists(local_path):
print(f" ⚠️ 本地文件不存在: {local_path}")
continue
try:
sftp.put(local_path, remote_path)
print(f"{local_rel}")
success_count += 1
except Exception as e:
print(f"{local_rel}: {e}")
# 上传JS文件
js_local_base = os.path.abspath(JS_LOCAL_BASE)
for local_name, remote_name in JS_FILES:
local_path = os.path.join(js_local_base, local_name)
remote_path = JS_REMOTE_BASE + remote_name
if not os.path.exists(local_path):
print(f" ⚠️ 本地JS文件不存在: {local_path}")
continue
try:
sftp.put(local_path, remote_path)
print(f" ✅ JS: {local_name}")
success_count += 1
except Exception as e:
print(f" ❌ JS: {local_name}: {e}")
sftp.close()
ssh.exec_command('rm -rf /www/wwwroot/tools.wktyl.com/runtime/cache/*')
ssh.close()
print(f"\n✅ 上传完成: {success_count}/{len(UPLOAD_FILES) + len(JS_FILES)}")
if __name__ == '__main__':
main()