Files
xianyan/docs/toolsapi/scripts/generate_and_upload_agreements.py
Developer 27672343b8 520
2026-05-20 01:40:09 +08:00

921 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
闲言APP — 协议HTML生成与上传脚本
创建时间: 2026-05-19
更新时间: 2026-05-19
作用: 从agreement_data.dart提取中英文协议内容生成双语HTML文件上传到服务器
上次更新: 支持中英文双语HTML生成URL参数?lang=en切换语言
"""
import re
import os
import paramiko
HOST = '123.207.67.197'
PORT = 22
USER = 'root'
PASS = '520Kiss123'
BASE_DIR = r'e:\project\flutter\f\xianyan\docs\toolsapi\agreements'
REMOTE_BASE = '/www/wwwroot/tools.wktyl.com/public/agreements/'
DART_FILE = r'e:\project\flutter\f\xianyan\lib\features\agreements\data\agreement_data.dart'
AGREEMENT_MAP = {
'privacyPolicy': {
'filename': 'privacy-policy.html',
'icon': '🔒',
'title': '隐私政策',
'titleEn': 'Privacy Policy',
'subtitle': '我们如何收集、使用、存储和保护您的个人信息',
'subtitleEn': 'How we collect, use, store and protect your personal information',
},
'userServiceAgreement': {
'filename': 'user-service-agreement.html',
'icon': '📋',
'title': '用户服务协议',
'titleEn': 'User Service Agreement',
'subtitle': '使用闲言APP的服务条款与规则',
'subtitleEn': 'Terms and rules for using Xianyan APP',
},
'accountAgreement': {
'filename': 'account-agreement.html',
'icon': '👤',
'title': '账号使用协议',
'titleEn': 'Account Agreement',
'subtitle': '账号注册、安全、注销等相关规定',
'subtitleEn': 'Rules for account registration, security, and deletion',
},
'memberBenefits': {
'filename': 'member-benefits.html',
'icon': '👑',
'title': '会员权益说明',
'titleEn': 'Member Benefits',
'subtitle': '会员等级、权益与使用说明',
'subtitleEn': 'Membership levels, benefits and usage instructions',
},
'disclaimer': {
'filename': 'disclaimer.html',
'icon': '⚖️',
'title': '免责声明及内容版权归属',
'titleEn': 'Disclaimer & Copyright',
'subtitle': '免责条款、内容版权与知识产权声明',
'subtitleEn': 'Disclaimers, content copyright and intellectual property',
},
'childrenPrivacy': {
'filename': 'children-privacy.html',
'icon': '👶',
'title': '儿童隐私政策',
'titleEn': 'Children\'s Privacy Policy',
'subtitle': '儿童及未成年人个人信息保护规则',
'subtitleEn': 'Personal information protection rules for children and minors',
},
'permissionUsage': {
'filename': 'permission-usage.html',
'icon': '🔧',
'title': '软件权限使用说明',
'titleEn': 'Permission Usage',
'subtitle': '软件所需权限及使用目的说明',
'subtitleEn': 'App permissions and their usage purposes',
},
'appIntroduction': {
'filename': 'app-introduction.html',
'icon': '',
'title': '软件介绍',
'titleEn': 'App Introduction',
'subtitle': '闲言APP功能介绍与平台支持',
'subtitleEn': 'Xianyan APP features and platform support',
},
'beginnerGuide': {
'filename': 'beginner-guide.html',
'icon': '📖',
'title': '新手指引',
'titleEn': 'Beginner Guide',
'subtitle': '功能导览与操作指引',
'subtitleEn': 'Feature overview and operation guide',
},
'devTeam': {
'filename': 'dev-team.html',
'icon': '💻',
'title': '开发团队',
'titleEn': 'Development Team',
'subtitle': '开发团队信息与联系方式',
'subtitleEn': 'Development team information and contact',
},
}
def extract_agreements(dart_path):
"""从Dart文件提取中英文协议内容"""
with open(dart_path, 'r', encoding='utf-8') as f:
content = f.read()
agreements_zh = {}
agreements_en = {}
pattern = r"static const (\w+)Content = '''\n(.*?)\n''';"
for match in re.finditer(pattern, content, re.DOTALL):
key = match.group(1)
text = match.group(2)
agreements_zh[key] = text
pattern_en = r"static const (\w+)ContentEn = '''\n(.*?)\n''';"
for match in re.finditer(pattern_en, content, re.DOTALL):
key = match.group(1)
text = match.group(2)
agreements_en[key] = text
return agreements_zh, agreements_en
def markdown_to_html(text, is_en=False):
"""将简易Markdown转换为HTML"""
lines = text.split('\n')
html_lines = []
in_table = False
in_ul = False
for line in lines:
stripped = line.strip()
if not stripped:
if in_ul:
html_lines.append('</ul>')
in_ul = False
if in_table:
html_lines.append('</tbody></table>')
in_table = False
continue
if stripped.startswith('|') and '|' in stripped[1:]:
cells = [c.strip() for c in stripped.split('|')[1:-1]]
if all(set(c) <= set('-: ') for c in cells):
continue
if not in_table:
if in_ul:
html_lines.append('</ul>')
in_ul = False
html_lines.append('<table><thead><tr>')
for c in cells:
html_lines.append(f'<th>{c}</th>')
html_lines.append('</tr></thead><tbody>')
in_table = True
else:
html_lines.append('<tr>')
for c in cells:
html_lines.append(f'<td>{c}</td>')
html_lines.append('</tr>')
continue
else:
if in_table:
html_lines.append('</tbody></table>')
in_table = False
if stripped.startswith(''):
if not in_ul:
html_lines.append('<ul>')
in_ul = True
item_text = stripped[1:].strip()
item_text = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', item_text)
html_lines.append(f'<li>{item_text}</li>')
if '2020SR0421982' in stripped:
html_lines.append('</ul>')
in_ul = False
html_lines.append('<div class="copyright-cert">')
html_lines.append('<img src="rz.png" alt="软件著作权登记证书" class="cert-image" />')
cert_caption = 'Software Copyright Registration Certificate' if is_en else '软件著作权登记证书'
html_lines.append(f'<p class="cert-caption">{cert_caption}</p>')
html_lines.append('</div>')
continue
elif stripped.startswith('-'):
if not in_ul:
html_lines.append('<ul>')
in_ul = True
item_text = stripped[1:].strip()
item_text = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', item_text)
html_lines.append(f'<li>{item_text}</li>')
continue
else:
if in_ul:
html_lines.append('</ul>')
in_ul = False
if stripped.startswith('**') and stripped.endswith('**'):
heading = stripped[2:-2]
html_lines.append(f'<h1>{heading}</h1>')
elif re.match(r'^[一二三四五六七八九十零]+[点五]*、', stripped):
heading = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
html_lines.append(f'<h2>{heading}</h2>')
elif re.match(r'^[IVX]+\.\s', stripped):
heading = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
html_lines.append(f'<h2>{heading}</h2>')
elif re.match(r'^\d+\.\d+', stripped):
heading = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
html_lines.append(f'<h3>{heading}</h3>')
elif stripped.startswith('说明:') or stripped.startswith('说明:'):
text_content = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
html_lines.append(f'<p class="note">{text_content}</p>')
elif stripped.startswith('Note:') or stripped.startswith('Note'):
text_content = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
html_lines.append(f'<p class="note">{text_content}</p>')
else:
text_content = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
text_content = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text_content)
html_lines.append(f'<p>{text_content}</p>')
if in_ul:
html_lines.append('</ul>')
if in_table:
html_lines.append('</tbody></table>')
return '\n'.join(html_lines)
def generate_html(info, content_html_zh, content_html_en):
"""生成带语言切换的双语HTML页面"""
icon = info['icon']
title_zh = info['title']
title_en = info['titleEn']
subtitle_zh = info['subtitle']
subtitle_en = info['subtitleEn']
return f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title_zh} - 闲言APP</title>
<style>
:root {{
--primary: #6C5CE7;
--primary-light: #A29BFE;
--primary-dark: #5A4BD1;
--bg: #F2F2F7;
--bg-card: #FFFFFF;
--text: #1C1C1E;
--text-secondary: #8E8E93;
--text-tertiary: #AEAEB2;
--border: #E5E5EA;
--highlight-bg: rgba(108, 92, 231, 0.08);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06);
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}}
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: var(--font-family);
background: var(--bg);
color: var(--text);
line-height: 1.8;
-webkit-font-smoothing: antialiased;
}}
.lang-switch {{
position: fixed;
top: 16px;
right: 16px;
z-index: 100;
display: flex;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 20px;
padding: 3px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
border: 0.5px solid rgba(108, 92, 231, 0.15);
}}
.lang-btn {{
padding: 6px 14px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
border-radius: 17px;
transition: all 0.25s ease;
font-family: var(--font-family);
}}
.lang-btn.active {{
background: var(--primary);
color: #FFF;
box-shadow: 0 2px 8px rgba(108, 92, 231, 0.3);
}}
.lang-btn:hover:not(.active) {{
background: rgba(108, 92, 231, 0.08);
color: var(--primary);
}}
.header {{
background: linear-gradient(135deg, #6C5CE7 0%, #5A4BD1 50%, #4A3DB5 100%);
padding: 48px 20px 44px;
text-align: center;
position: relative;
overflow: hidden;
}}
.header::before {{
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 30% 50%, rgba(255,255,255,0.1) 0%, transparent 50%);
pointer-events: none;
}}
.header::after {{
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 32px;
background: var(--bg);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
}}
.header-icon {{ font-size: 40px; margin-bottom: 10px; display: block; }}
.header h1 {{ color: #FFF; font-size: 24px; font-weight: 700; letter-spacing: -0.3px; margin-bottom: 4px; }}
.header p {{ color: rgba(255,255,255,0.75); font-size: 14px; }}
.container {{ max-width: 800px; margin: 0 auto; padding: 20px 16px 40px; }}
.content-card {{
background: var(--bg-card);
border-radius: var(--radius-lg);
padding: 28px 24px;
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
}}
h1 {{
font-size: 22px;
font-weight: 700;
color: var(--text);
margin: 28px 0 12px;
}}
h2 {{
font-size: 20px;
font-weight: 700;
color: var(--text);
margin: 32px 0 16px;
padding-bottom: 10px;
border-bottom: 2px solid var(--primary);
display: inline-block;
}}
h3 {{
font-size: 17px;
font-weight: 600;
color: var(--text);
margin: 24px 0 12px;
}}
p {{
font-size: 15px;
color: var(--text);
margin-bottom: 12px;
line-height: 1.8;
}}
p.note {{
background: rgba(108, 92, 231, 0.06);
border-left: 3px solid var(--primary);
padding: 12px 16px;
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
margin: 12px 0;
}}
.highlight {{
color: var(--primary);
font-weight: 600;
background: var(--highlight-bg);
padding: 1px 6px;
border-radius: 4px;
}}
.copyright-cert {{
margin: 20px 0;
text-align: center;
padding: 20px;
background: rgba(108, 92, 231, 0.04);
border-radius: var(--radius-md);
border: 1px solid rgba(108, 92, 231, 0.12);
}}
.cert-image {{
max-width: 100%;
max-height: 500px;
border-radius: var(--radius-sm);
box-shadow: var(--shadow-md);
}}
.cert-caption {{
margin-top: 12px;
font-size: 14px;
color: var(--text-secondary);
font-weight: 500;
}}
ul {{
list-style: none;
padding: 0;
margin: 0 0 16px;
}}
li {{
font-size: 15px;
color: var(--text);
padding: 6px 0 6px 20px;
position: relative;
line-height: 1.7;
}}
li::before {{
content: '\\2022';
color: var(--primary);
font-weight: 700;
position: absolute;
left: 4px;
top: 6px;
}}
table {{
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 14px;
}}
th {{
background: var(--primary);
color: #FFF;
padding: 10px 12px;
text-align: left;
font-weight: 600;
font-size: 13px;
}}
td {{
padding: 10px 12px;
border-bottom: 0.5px solid var(--border);
line-height: 1.6;
}}
tr:hover td {{
background: rgba(108, 92, 231, 0.04);
}}
.back-link {{
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--primary);
text-decoration: none;
font-size: 15px;
font-weight: 500;
padding: 12px 0;
transition: opacity 0.2s;
}}
.back-link:hover {{ opacity: 0.7; }}
.footer {{
text-align: center;
padding: 24px 20px 40px;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.8;
border-top: 0.5px solid var(--border);
margin-top: 20px;
}}
.footer .company {{ font-weight: 500; color: var(--text); margin-bottom: 4px; }}
@media (max-width: 640px) {{
.lang-switch {{ top: 10px; right: 10px; }}
.lang-btn {{ padding: 5px 10px; font-size: 12px; }}
.header {{ padding: 40px 16px 36px; }}
.header h1 {{ font-size: 20px; }}
.container {{ padding: 16px 12px 32px; }}
.content-card {{ padding: 20px 16px; }}
h2 {{ font-size: 18px; }}
h3 {{ font-size: 16px; }}
p, li {{ font-size: 14px; }}
table {{ font-size: 12px; }}
th, td {{ padding: 8px 6px; }}
}}
</style>
</head>
<body>
<div class="lang-switch">
<button class="lang-btn active" onclick="switchLang('zh')" id="btn-zh">中文</button>
<button class="lang-btn" onclick="switchLang('en')" id="btn-en">EN</button>
</div>
<div class="header">
<span class="header-icon">{icon}</span>
<h1 id="header-title">{title_zh}</h1>
<p id="header-subtitle">{subtitle_zh}</p>
</div>
<div class="container">
<div class="content-card">
<div id="content-zh" class="lang-content">
{content_html_zh}
</div>
<div id="content-en" class="lang-content" style="display:none;">
{content_html_en}
</div>
</div>
<a href="index.html" class="back-link" id="back-link">← 返回协议列表</a>
</div>
<div class="footer" id="footer">
<div class="company" id="footer-company">弥勒市朋普镇微风暴网络科技工作室</div>
<div id="footer-contact">📧 21981550@qq.com &nbsp;|&nbsp; 📍 云南省昆明市西山区滇池度假区碧鸡街道车家壁513号</div>
<div style="margin-top: 8px;" id="footer-credit">统一社会信用代码92532526MA6PCX153W</div>
<div style="margin-top: 4px; color: var(--text-tertiary);">© 2026 Xianyan. All rights reserved.</div>
</div>
<script>
const DATA = {{
zh: {{
title: '{title_zh}',
subtitle: '{subtitle_zh}',
backLink: '← 返回协议列表',
company: '弥勒市朋普镇微风暴网络科技工作室',
contact: '📧 21981550@qq.com &nbsp;|&nbsp; 📍 云南省昆明市西山区滇池度假区碧鸡街道车家壁513号',
credit: '统一社会信用代码92532526MA6PCX153W'
}},
en: {{
title: '{title_en}',
subtitle: '{subtitle_en}',
backLink: '← Back to Agreement List',
company: 'Mile City Pengpu Town Weifengbao Network Technology Studio',
contact: '📧 21981550@qq.com &nbsp;|&nbsp; 📍 Dianchi Resort, Xishan District, Kunming, Yunnan, China',
credit: 'Unified Social Credit Code: 92532526MA6PCX153W'
}}
}};
function switchLang(lang) {{
document.getElementById('content-zh').style.display = lang === 'zh' ? 'block' : 'none';
document.getElementById('content-en').style.display = lang === 'en' ? 'block' : 'none';
document.getElementById('header-title').textContent = DATA[lang].title;
document.getElementById('header-subtitle').textContent = DATA[lang].subtitle;
document.getElementById('back-link').textContent = DATA[lang].backLink;
document.getElementById('footer-company').textContent = DATA[lang].company;
document.getElementById('footer-contact').innerHTML = DATA[lang].contact;
document.getElementById('footer-credit').textContent = DATA[lang].credit;
document.getElementById('btn-zh').className = 'lang-btn' + (lang === 'zh' ? ' active' : '');
document.getElementById('btn-en').className = 'lang-btn' + (lang === 'en' ? ' active' : '');
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
const url = new URL(window.location);
if (lang === 'en') {{
url.searchParams.set('lang', 'en');
}} else {{
url.searchParams.delete('lang');
}}
history.replaceState(null, '', url);
}}
(function() {{
const params = new URLSearchParams(window.location.search);
const lang = params.get('lang');
if (lang === 'en') switchLang('en');
}})();
</script>
</body>
</html>'''
def generate_index_html():
"""生成带语言切换的协议列表首页"""
keys = list(AGREEMENT_MAP.keys())
sections = [
{'icon': '🔒', 'titleZh': '隐私与安全', 'titleEn': 'Privacy & Security', 'range': range(0, 3)},
{'icon': '📋', 'titleZh': '服务条款', 'titleEn': 'Service Terms', 'range': range(3, 6)},
{'icon': '⚖️', 'titleZh': '内容与版权', 'titleEn': 'Content & Copyright', 'range': range(6, 7)},
{'icon': '💡', 'titleZh': '了解闲言', 'titleEn': 'About Xianyan', 'range': range(7, 10)},
]
def build_items(key_list, lang='zh'):
html = ''
for key in key_list:
info = AGREEMENT_MAP[key]
if lang == 'zh':
title = info['title']
subtitle = info['subtitle']
href = info['filename']
else:
title = info['titleEn']
subtitle = info['subtitleEn']
href = f"{info['filename']}?lang=en"
html += f'''
<a href="{href}" class="agreement-item">
<span class="item-icon">{info['icon']}</span>
<div class="item-text">
<div class="item-title">{title}</div>
<div class="item-subtitle">{subtitle}</div>
</div>
<span class="item-arrow"></span>
</a>'''
return html
def build_sections(lang='zh'):
html = ''
for sec in sections:
section_title = sec['titleZh'] if lang == 'zh' else sec['titleEn']
section_keys = [keys[i] for i in sec['range'] if i < len(keys)]
html += f'''
<div class="section-title">{sec['icon']} {section_title}</div>
<div class="agreement-list">
{build_items(section_keys, lang)}
</div>'''
return html
return f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>软件协议 - 闲言APP</title>
<style>
:root {{
--primary: #6C5CE7;
--primary-light: #A29BFE;
--primary-dark: #5A4BD1;
--bg: #F2F2F7;
--bg-card: #FFFFFF;
--text: #1C1C1E;
--text-secondary: #8E8E93;
--text-tertiary: #AEAEB2;
--border: #E5E5EA;
--highlight-bg: rgba(108, 92, 231, 0.08);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06);
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}}
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: var(--font-family);
background: var(--bg);
color: var(--text);
line-height: 1.8;
-webkit-font-smoothing: antialiased;
}}
.lang-switch {{
position: fixed;
top: 16px;
right: 16px;
z-index: 100;
display: flex;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 20px;
padding: 3px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
border: 0.5px solid rgba(108, 92, 231, 0.15);
}}
.lang-btn {{
padding: 6px 14px;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
border-radius: 17px;
transition: all 0.25s ease;
font-family: var(--font-family);
}}
.lang-btn.active {{
background: var(--primary);
color: #FFF;
box-shadow: 0 2px 8px rgba(108, 92, 231, 0.3);
}}
.lang-btn:hover:not(.active) {{
background: rgba(108, 92, 231, 0.08);
color: var(--primary);
}}
.header {{
background: linear-gradient(135deg, #6C5CE7 0%, #5A4BD1 50%, #4A3DB5 100%);
padding: 48px 20px 44px;
text-align: center;
position: relative;
overflow: hidden;
}}
.header::before {{
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 30% 50%, rgba(255,255,255,0.1) 0%, transparent 50%);
pointer-events: none;
}}
.header::after {{
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 32px;
background: var(--bg);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
}}
.header-icon {{ font-size: 40px; margin-bottom: 10px; display: block; }}
.header h1 {{ color: #FFF; font-size: 24px; font-weight: 700; letter-spacing: -0.3px; margin-bottom: 4px; }}
.header p {{ color: rgba(255,255,255,0.75); font-size: 14px; }}
.container {{ max-width: 800px; margin: 0 auto; padding: 20px 16px 40px; }}
.section-title {{
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 16px 4px 8px;
}}
.agreement-list {{
background: var(--bg-card);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
margin-bottom: 16px;
}}
.agreement-item {{
display: flex;
align-items: center;
padding: 14px 16px;
text-decoration: none;
color: var(--text);
transition: background 0.15s;
border-bottom: 0.5px solid var(--border);
}}
.agreement-item:last-child {{ border-bottom: none; }}
.agreement-item:hover {{ background: rgba(108, 92, 231, 0.04); }}
.item-icon {{ font-size: 28px; margin-right: 14px; flex-shrink: 0; }}
.item-text {{ flex: 1; min-width: 0; }}
.item-title {{ font-size: 16px; font-weight: 600; color: var(--text); }}
.item-subtitle {{ font-size: 13px; color: var(--text-secondary); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
.item-arrow {{ font-size: 20px; color: var(--text-tertiary); margin-left: 8px; }}
.footer {{
text-align: center;
padding: 24px 20px 40px;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.8;
border-top: 0.5px solid var(--border);
margin-top: 20px;
}}
.footer .company {{ font-weight: 500; color: var(--text); margin-bottom: 4px; }}
@media (max-width: 640px) {{
.lang-switch {{ top: 10px; right: 10px; }}
.lang-btn {{ padding: 5px 10px; font-size: 12px; }}
.header {{ padding: 40px 16px 36px; }}
.header h1 {{ font-size: 20px; }}
.container {{ padding: 16px 12px 32px; }}
.agreement-item {{ padding: 12px 14px; }}
.item-icon {{ font-size: 24px; margin-right: 12px; }}
.item-title {{ font-size: 15px; }}
.item-subtitle {{ font-size: 12px; }}
}}
</style>
</head>
<body>
<div class="lang-switch">
<button class="lang-btn active" onclick="switchLang('zh')" id="btn-zh">中文</button>
<button class="lang-btn" onclick="switchLang('en')" id="btn-en">EN</button>
</div>
<div class="header">
<span class="header-icon">📋</span>
<h1 id="header-title">软件协议与政策</h1>
<p id="header-subtitle">请仔细阅读以下协议与政策,了解您的权利与义务</p>
</div>
<div class="container">
<div id="content-zh" class="lang-content">
{build_sections('zh')}
</div>
<div id="content-en" class="lang-content" style="display:none;">
{build_sections('en')}
</div>
</div>
<div class="footer" id="footer">
<div class="company" id="footer-company">弥勒市朋普镇微风暴网络科技工作室</div>
<div id="footer-contact">📧 21981550@qq.com &nbsp;|&nbsp; 📍 云南省昆明市西山区滇池度假区碧鸡街道车家壁513号</div>
<div style="margin-top: 8px;" id="footer-credit">统一社会信用代码92532526MA6PCX153W</div>
<div style="margin-top: 4px; color: var(--text-tertiary);">© 2026 Xianyan. All rights reserved.</div>
</div>
<script>
const DATA = {{
zh: {{
title: '软件协议与政策',
subtitle: '请仔细阅读以下协议与政策,了解您的权利与义务',
company: '弥勒市朋普镇微风暴网络科技工作室',
contact: '📧 21981550@qq.com &nbsp;|&nbsp; 📍 云南省昆明市西山区滇池度假区碧鸡街道车家壁513号',
credit: '统一社会信用代码92532526MA6PCX153W'
}},
en: {{
title: 'Agreements & Policies',
subtitle: 'Please read the following agreements and policies carefully',
company: 'Mile City Pengpu Town Weifengbao Network Technology Studio',
contact: '📧 21981550@qq.com &nbsp;|&nbsp; 📍 Dianchi Resort, Xishan District, Kunming, Yunnan, China',
credit: 'Unified Social Credit Code: 92532526MA6PCX153W'
}}
}};
function switchLang(lang) {{
document.getElementById('content-zh').style.display = lang === 'zh' ? 'block' : 'none';
document.getElementById('content-en').style.display = lang === 'en' ? 'block' : 'none';
document.getElementById('header-title').textContent = DATA[lang].title;
document.getElementById('header-subtitle').textContent = DATA[lang].subtitle;
document.getElementById('footer-company').textContent = DATA[lang].company;
document.getElementById('footer-contact').innerHTML = DATA[lang].contact;
document.getElementById('footer-credit').textContent = DATA[lang].credit;
document.getElementById('btn-zh').className = 'lang-btn' + (lang === 'zh' ? ' active' : '');
document.getElementById('btn-en').className = 'lang-btn' + (lang === 'en' ? ' active' : '');
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
const url = new URL(window.location);
if (lang === 'en') {{
url.searchParams.set('lang', 'en');
}} else {{
url.searchParams.delete('lang');
}}
history.replaceState(null, '', url);
}}
(function() {{
const params = new URLSearchParams(window.location.search);
const lang = params.get('lang');
if (lang === 'en') switchLang('en');
}})();
</script>
</body>
</html>'''
def main():
print('=' * 60)
print('闲言APP 协议HTML生成与上传工具 (双语版)')
print('=' * 60)
print('\n[1/3] 从Dart文件提取中英文协议内容...')
agreements_zh, agreements_en = extract_agreements(DART_FILE)
print(f' 中文协议: {len(agreements_zh)}')
print(f' 英文协议: {len(agreements_en)}')
print('\n[2/3] 生成双语HTML文件...')
for key, info in AGREEMENT_MAP.items():
content_zh = agreements_zh.get(key)
content_en = agreements_en.get(key)
if not content_zh:
print(f' [SKIP] {key} 中文内容未找到')
continue
content_html_zh = markdown_to_html(content_zh, is_en=False)
content_html_en = markdown_to_html(content_en, is_en=True) if content_en else '<p style="color:var(--text-secondary);">English version coming soon...</p>'
html = generate_html(info, content_html_zh, content_html_en)
filepath = os.path.join(BASE_DIR, info['filename'])
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html)
status = '' if content_en else '⚠️ (EN缺失)'
print(f' [OK] {info["filename"]} {status}')
index_html = generate_index_html()
index_path = os.path.join(BASE_DIR, 'index.html')
with open(index_path, 'w', encoding='utf-8') as f:
f.write(index_html)
print(f' [OK] index.html')
print('\n[3/3] 上传到服务器...')
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, PORT, USER, PASS)
sftp = ssh.open_sftp()
for key, info in AGREEMENT_MAP.items():
local = os.path.join(BASE_DIR, info['filename'])
remote = REMOTE_BASE + info['filename']
if not os.path.exists(local):
print(f' [SKIP] {info["filename"]} 本地文件不存在')
continue
print(f' 上传 {info["filename"]}...')
sftp.put(local, remote)
sftp.chmod(remote, 0o644)
print(f' [OK] {info["filename"]}')
index_local = os.path.join(BASE_DIR, 'index.html')
index_remote = REMOTE_BASE + 'index.html'
if os.path.exists(index_local):
print(' 上传 index.html...')
sftp.put(index_local, index_remote)
sftp.chmod(index_remote, 0o644)
print(' [OK] index.html')
sftp.close()
ssh.close()
print('\n' + '=' * 60)
print('完成所有双语协议HTML已生成并上传到服务器')
print(f'中文版: https://tools.wktyl.com/agreements/index.html')
print(f'英文版: https://tools.wktyl.com/agreements/index.html?lang=en')
print('=' * 60)
if __name__ == '__main__':
main()