aiclient-2-api / static /potluck-user.html
Jaasomn
Initial deployment
ceb3821
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 大锅饭 - 我的用量</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117;
min-height: 100vh;
color: #e6edf3;
}
/* 顶部导航 */
.navbar {
background: #161b22;
border-bottom: 1px solid #30363d;
position: sticky;
top: 0;
z-index: 100;
}
.navbar-inner {
max-width: 1000px;
margin: 0 auto;
padding: 0 24px;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar-brand {
display: flex;
align-items: center;
gap: 12px;
font-size: 20px;
font-weight: 700;
color: #fff;
}
.navbar-brand .icon { font-size: 28px; }
.navbar-brand .badge {
background: linear-gradient(135deg, #f472b6, #ec4899);
color: #fff;
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.navbar-user {
display: flex;
align-items: center;
gap: 16px;
}
.navbar-user .welcome {
color: #8b949e;
font-size: 14px;
}
.navbar-user .welcome strong { color: #e6edf3; }
.btn-logout {
background: linear-gradient(135deg, #f472b6, #ec4899);
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.btn-logout:hover { opacity: 0.9; transform: translateY(-1px); }
/* 主容器 */
.main-container {
max-width: 1000px;
margin: 0 auto;
padding: 30px 24px;
}
/* Tab 导航 */
.tab-nav {
display: flex;
gap: 8px;
border-bottom: 1px solid #30363d;
margin-bottom: 30px;
justify-content: center;
}
.tab-btn {
background: none;
border: none;
color: #8b949e;
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.tab-btn:hover { color: #e6edf3; }
.tab-btn.active {
color: #e6edf3;
border-bottom-color: #f472b6;
}
.tab-content { display: none; }
.tab-content.active { display: block; }
/* 区块标题 */
.section-title {
font-size: 18px;
font-weight: 600;
color: #e6edf3;
margin-bottom: 20px;
}
/* 统计卡片网格 */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
/* 第一行:3列带边框卡片 */
.stats-row-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.stat-card-bordered {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 20px;
text-align: center;
position: relative;
}
.stat-card-bordered::before {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
height: 3px;
border-radius: 12px 12px 0 0;
}
.stat-card-bordered.purple::before { background: linear-gradient(90deg, #a855f7, #7c3aed); }
.stat-card-bordered.pink::before { background: linear-gradient(90deg, #ec4899, #f472b6); }
.stat-card-bordered.green::before { background: linear-gradient(90deg, #10b981, #34d399); }
.stat-card-bordered.cyan::before { background: linear-gradient(90deg, #06b6d4, #22d3ee); }
.stat-card-bordered.orange::before { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.stat-card-bordered .label {
font-size: 12px;
color: #8b949e;
margin-bottom: 10px;
}
.stat-card-bordered .value {
font-size: 28px;
font-weight: 700;
color: #e6edf3;
}
.stat-card-bordered .value .dim {
font-size: 18px;
color: #8b949e;
font-weight: 400;
}
.stat-card-bordered.purple .value > span:first-child { color: #a855f7; }
.stat-card-bordered.green .value > span:first-child { color: #10b981; }
.stat-card-bordered.cyan .value { color: #22d3ee; }
/* 第二行:2列大卡片 */
.stats-row-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.stat-card-large {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 28px 24px;
text-align: center;
}
.stat-card-large .value {
font-size: 36px;
font-weight: 700;
margin-bottom: 8px;
}
.stat-card-large .value .highlight { color: #22d3ee; }
.stat-card-large .value .highlight.green { color: #10b981; }
.stat-card-large .value .dim { font-size: 24px; color: #8b949e; font-weight: 400; }
.stat-card-large .label { font-size: 13px; color: #8b949e; }
.stat-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 20px;
text-align: center;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
}
.stat-card.purple::before { background: linear-gradient(90deg, #a855f7, #7c3aed); }
.stat-card.pink::before { background: linear-gradient(90deg, #ec4899, #f472b6); }
.stat-card.cyan::before { background: linear-gradient(90deg, #06b6d4, #22d3ee); }
.stat-card.green::before { background: linear-gradient(90deg, #10b981, #34d399); }
.stat-card.orange::before { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.stat-card .label {
font-size: 12px;
color: #8b949e;
margin-bottom: 8px;
}
.stat-card .value {
font-size: 32px;
font-weight: 700;
}
.stat-card.purple .value { color: #a855f7; }
.stat-card.pink .value { color: #ec4899; }
.stat-card.cyan .value { color: #22d3ee; }
.stat-card.green .value { color: #10b981; }
.stat-card.orange .value { color: #f59e0b; }
.stat-card .sub { font-size: 14px; color: #8b949e; }
/* 大统计卡片 */
.stats-large {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-large {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 24px;
text-align: center;
}
.stat-large .value {
font-size: 42px;
font-weight: 700;
margin-bottom: 8px;
}
.stat-large .value .highlight { color: #22d3ee; }
.stat-large .value .dim { color: #8b949e; }
.stat-large .label { font-size: 13px; color: #8b949e; }
/* 操作卡片 */
.action-card {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
border: 1px solid #4c1d95;
border-radius: 12px;
padding: 20px 24px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.action-card-content {
display: flex;
align-items: center;
gap: 16px;
}
.action-card-icon {
width: 48px;
height: 48px;
background: rgba(168, 85, 247, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.action-card-text h4 {
font-size: 15px;
font-weight: 600;
color: #e6edf3;
margin-bottom: 4px;
}
.action-card-text p {
font-size: 13px;
color: #8b949e;
}
.btn-action {
background: linear-gradient(135deg, #7c3aed, #a855f7);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
white-space: nowrap;
}
.btn-action:hover { opacity: 0.9; transform: translateY(-1px); }
/* 凭证列表 */
.credentials-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.credential-item {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 16px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
transition: all 0.2s;
}
.credential-item:hover {
border-color: #8b949e;
}
.credential-info {
display: flex;
align-items: center;
gap: 14px;
flex: 1;
min-width: 0;
}
.credential-icon {
width: 40px;
height: 40px;
background: #21262d;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.credential-details { flex: 1; min-width: 0; }
.credential-name {
font-size: 14px;
font-weight: 600;
color: #e6edf3;
margin-bottom: 4px;
}
.credential-path {
font-size: 12px;
color: #8b949e;
font-family: monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.credential-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
padding: 4px 10px;
border-radius: 20px;
flex-shrink: 0;
}
.credential-status.healthy {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
}
.credential-status.unhealthy {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.credential-status.unknown {
background: rgba(139, 148, 158, 0.15);
color: #8b949e;
}
.credential-status.checking {
background: rgba(251, 191, 36, 0.15);
color: #fbbf24;
}
.credential-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.btn-icon {
background: #21262d;
border: 1px solid #30363d;
color: #8b949e;
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.btn-icon:hover {
background: #30363d;
color: #e6edf3;
}
.btn-icon.danger:hover {
background: rgba(239, 68, 68, 0.15);
border-color: #ef4444;
color: #ef4444;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #8b949e;
}
.empty-state .icon { font-size: 48px; margin-bottom: 16px; }
.empty-state p { font-size: 14px; }
/* 登录页面 */
.login-container {
min-height: calc(100vh - 60px);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.login-box {
background: #161b22;
border: 1px solid #30363d;
border-radius: 16px;
padding: 40px;
width: 100%;
max-width: 400px;
}
.login-box h2 {
font-size: 24px;
font-weight: 700;
color: #e6edf3;
text-align: center;
margin-bottom: 8px;
}
.login-box .subtitle {
text-align: center;
color: #8b949e;
font-size: 14px;
margin-bottom: 30px;
}
.form-group { margin-bottom: 20px; }
.form-group label {
display: block;
font-size: 13px;
color: #8b949e;
margin-bottom: 8px;
}
.form-input {
width: 100%;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 8px;
padding: 12px 16px;
color: #e6edf3;
font-size: 14px;
font-family: monospace;
transition: all 0.2s;
}
.form-input:focus {
outline: none;
border-color: #a855f7;
box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.15);
}
.form-input::placeholder { color: #484f58; }
.form-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #8b949e;
cursor: pointer;
}
.form-checkbox input {
width: 16px;
height: 16px;
accent-color: #a855f7;
}
.btn-login {
width: 100%;
background: linear-gradient(135deg, #7c3aed, #a855f7);
color: #fff;
border: none;
padding: 14px;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-login:hover { opacity: 0.9; }
.btn-login:disabled {
background: #30363d;
color: #484f58;
cursor: not-allowed;
}
/* 模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal-overlay.active { display: flex; }
.modal {
background: #161b22;
border: 1px solid #30363d;
border-radius: 16px;
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 20px 24px;
border-bottom: 1px solid #30363d;
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-header h3 {
font-size: 18px;
font-weight: 600;
color: #e6edf3;
display: flex;
align-items: center;
gap: 10px;
}
.modal-close {
background: none;
border: none;
color: #8b949e;
font-size: 24px;
cursor: pointer;
padding: 4px;
line-height: 1;
}
.modal-close:hover { color: #e6edf3; }
.modal-body {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid #30363d;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.btn-secondary {
background: #21262d;
border: 1px solid #30363d;
color: #e6edf3;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover { background: #30363d; }
.btn-primary {
background: linear-gradient(135deg, #7c3aed, #a855f7);
border: none;
color: #fff;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover { opacity: 0.9; }
.btn-primary:disabled {
background: #30363d;
color: #484f58;
cursor: not-allowed;
}
/* 模式切换 */
.mode-toggle {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.mode-btn {
flex: 1;
background: #21262d;
border: 2px solid #30363d;
color: #8b949e;
padding: 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.mode-btn:hover { border-color: #8b949e; }
.mode-btn.active {
background: rgba(168, 85, 247, 0.15);
border-color: #a855f7;
color: #a855f7;
}
/* 文件上传区域 */
.upload-zone {
border: 2px dashed #30363d;
border-radius: 12px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 16px;
}
.upload-zone:hover {
border-color: #a855f7;
background: rgba(168, 85, 247, 0.05);
}
.upload-zone .icon { font-size: 40px; margin-bottom: 12px; }
.upload-zone p { color: #8b949e; font-size: 14px; }
.upload-zone .hint { font-size: 12px; color: #484f58; margin-top: 8px; }
/* 文件列表 */
.file-list {
background: #0d1117;
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #161b22;
border-radius: 6px;
margin-bottom: 6px;
}
.file-item:last-child { margin-bottom: 0; }
.file-item-info {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
}
.file-item-name {
font-size: 13px;
color: #e6edf3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-item-fields {
font-size: 11px;
color: #8b949e;
}
.file-item-remove {
background: none;
border: none;
color: #8b949e;
cursor: pointer;
padding: 4px;
}
.file-item-remove:hover { color: #ef4444; }
/* 验证结果 */
.validation-result {
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.validation-result.success {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.validation-result.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.validation-result h4 {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.validation-result.success h4 { color: #10b981; }
.validation-result.error h4 { color: #ef4444; }
.validation-result ul {
list-style: none;
font-size: 13px;
}
.validation-result li {
padding: 4px 0;
display: flex;
align-items: center;
gap: 8px;
}
.validation-result .found { color: #10b981; }
.validation-result .missing { color: #ef4444; }
/* JSON 预览 */
.json-preview {
background: #0d1117;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.json-preview-header {
font-size: 13px;
font-weight: 600;
color: #8b949e;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.json-preview pre {
font-family: monospace;
font-size: 12px;
color: #7ee787;
white-space: pre-wrap;
word-break: break-all;
max-height: 150px;
overflow: auto;
}
/* 提示信息 */
.info-box {
background: rgba(168, 85, 247, 0.1);
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 20px;
}
.info-box p {
font-size: 13px;
color: #c4b5fd;
}
.info-box code {
background: rgba(0, 0, 0, 0.3);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
}
/* Toast */
.toast {
position: fixed;
bottom: 24px;
right: 24px;
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 14px 20px;
color: #e6edf3;
font-size: 14px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
transform: translateY(100px);
opacity: 0;
transition: all 0.3s;
z-index: 2000;
display: flex;
align-items: center;
gap: 10px;
}
.toast.show { transform: translateY(0); opacity: 1; }
.toast.success { border-left: 4px solid #10b981; }
.toast.error { border-left: 4px solid #ef4444; }
/* 响应式 */
@media (max-width: 768px) {
.navbar-inner { padding: 0 16px; }
.navbar-user .welcome { display: none; }
.btn-logout span:first-child { display: none; }
.main-container { padding: 20px 16px; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.stats-row-3 { grid-template-columns: repeat(3, 1fr); gap: 8px; }
.stats-row-2 { grid-template-columns: 1fr; gap: 8px; }
.stat-card-bordered .value { font-size: 22px; }
.stat-card-bordered .value .dim { font-size: 14px; }
.stat-card-large .value { font-size: 28px; }
.stat-card-large .value .dim { font-size: 18px; }
.stats-large { grid-template-columns: 1fr; }
.action-card { flex-direction: column; gap: 16px; text-align: center; }
.action-card-content { flex-direction: column; }
.credential-item { flex-direction: column; align-items: flex-start; }
.credential-actions { width: 100%; justify-content: flex-end; }
.tab-nav { overflow-x: auto; }
.tab-btn { white-space: nowrap; padding: 12px 16px; }
}
@media (max-width: 480px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.stats-row-3 { grid-template-columns: 1fr; }
.stat-card-bordered { padding: 16px; }
.stat-card-bordered .value { font-size: 24px; }
.stat-card .value { font-size: 28px; }
.stat-large .value { font-size: 32px; }
.upload-providers-grid { grid-template-columns: 1fr; }
}
/* 提供商上传卡片 */
.upload-providers-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.upload-provider-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
transition: all 0.2s;
}
.upload-provider-card:hover {
border-color: #8b949e;
}
.upload-provider-icon {
font-size: 32px;
margin-bottom: 12px;
}
.upload-provider-info {
margin-bottom: 16px;
}
.upload-provider-name {
font-size: 15px;
font-weight: 600;
color: #e6edf3;
margin-bottom: 4px;
}
.upload-provider-type {
font-size: 11px;
color: #8b949e;
font-family: monospace;
}
.btn-upload {
width: 100%;
background: #21262d;
border: 1px solid #30363d;
color: #e6edf3;
padding: 10px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-upload:hover {
background: #30363d;
border-color: #8b949e;
}
.btn-upload:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.upload-provider-status {
margin-top: 10px;
font-size: 12px;
min-height: 18px;
word-break: break-all;
}
.upload-provider-status.success { color: #10b981; }
.upload-provider-status.error { color: #ef4444; }
</style>
</head>
<body>
<!-- 顶部导航 -->
<nav class="navbar">
<div class="navbar-inner">
<div class="navbar-brand">
<span class="icon">🍲</span>
<span>API 大锅饭</span>
<span class="badge">用户版</span>
</div>
<div class="navbar-user" id="navbarUser" style="display: none;">
<span class="welcome">欢迎, <strong id="navUserName">-</strong></span>
<button class="btn-logout" onclick="logout()">
<span></span> 退出登录
</button>
</div>
</div>
</nav>
<!-- 登录页面 -->
<div class="login-container" id="loginContainer">
<div class="login-box">
<h2>🔑 登录</h2>
<p class="subtitle">使用您的 API Key 登录查看用量</p>
<div class="form-group">
<label>API Key</label>
<input type="text" class="form-input" id="apiKeyInput" placeholder="maki_xxxxxxxx..." autocomplete="off">
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="rememberKey" checked>
<span>记住我的 Key</span>
</label>
</div>
<button class="btn-login" id="loginBtn" onclick="login()">登录</button>
</div>
</div>
<!-- 主内容区 -->
<div class="main-container" id="mainContainer" style="display: none;">
<!-- Tab 导航 -->
<div class="tab-nav">
<button class="tab-btn active" data-tab="stats" onclick="switchTab('stats')">个人统计</button>
<button class="tab-btn" data-tab="credentials" onclick="switchTab('credentials')">凭证管理</button>
<button class="tab-btn" data-tab="apikey" onclick="switchTab('apikey')">API密钥</button>
<button class="tab-btn" data-tab="upload" onclick="switchTab('upload')">上传凭证</button>
</div>
<!-- 个人统计 Tab -->
<div class="tab-content active" id="tabStats">
<h3 class="section-title">个人使用统计</h3>
<!-- 第一行:3个小统计卡片 -->
<div class="stats-row-3">
<div class="stat-card-bordered purple">
<div class="label">每日用量</div>
<div class="value"><span id="statToday">0</span><span class="dim"> / <span id="statLimit">0</span></span></div>
</div>
<div class="stat-card-bordered green">
<div class="label">资源包</div>
<div class="value"><span id="statBonusUsed">0</span><span class="dim"> / <span id="statBonusTotal">0</span></span></div>
</div>
<div class="stat-card-bordered cyan">
<div class="label">累计调用</div>
<div class="value" id="statTotal">0</div>
</div>
</div>
<!-- 第二行:2个大统计卡片 -->
<div class="stats-row-2">
<div class="stat-card-large">
<div class="value">
<span class="highlight" id="usageToday">0</span>
<span class="dim"> / <span id="usageLimit">0</span></span>
</div>
<div class="label">总已使用 / 总配额上限</div>
</div>
<div class="stat-card-large">
<div class="value">
<span class="highlight green" id="credentialCount">0</span>
</div>
<div class="label">有效凭证数</div>
</div>
</div>
<!-- 操作卡片 -->
<div class="action-card">
<div class="action-card-content">
<div class="action-card-icon">🎁</div>
<div class="action-card-text">
<h4>获取凭证,上传使用</h4>
<p>通过 AWS SSO 或 OAuth 授权获取凭证文件</p>
</div>
</div>
<button class="btn-action" onclick="switchTab('upload')">
<span></span> 立即上传
</button>
</div>
</div>
<!-- 凭证管理 Tab -->
<div class="tab-content" id="tabCredentials">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 class="section-title" style="margin-bottom: 0;">我的凭证</h3>
<button class="btn-secondary" onclick="loadCredentials()">🔄 刷新</button>
</div>
<div class="credentials-list" id="credentialsList">
<div class="empty-state">
<div class="icon">📭</div>
<p>暂无凭证,请通过上传功能添加</p>
</div>
</div>
</div>
<!-- API密钥 Tab -->
<div class="tab-content" id="tabApikey">
<h3 class="section-title">API密钥</h3>
<!-- API Key 显示区域 -->
<div style="background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 24px; margin-bottom: 24px;">
<input type="text" class="form-input" id="displayApiKeyFull" readonly
style="font-family: monospace; font-size: 14px; text-align: center; margin-bottom: 16px; background: #0d1117;">
<div style="display: flex; gap: 12px; justify-content: center;">
<button class="btn-action" style="flex: 1; max-width: 200px; background: linear-gradient(135deg, #2563eb, #3b82f6); justify-content: center;" onclick="copyApiKey()">
<span id="copyKeyIcon">📋</span> <span id="copyKeyText">复制</span>
</button>
<button class="btn-action" style="flex: 1; max-width: 200px; background: linear-gradient(135deg, #f59e0b, #fbbf24); justify-content: center;" onclick="showRegenerateModal()">
<span>🔄</span> 更改
</button>
</div>
</div>
<div class="info-box" style="background: rgba(139, 148, 158, 0.1); border-color: rgba(139, 148, 158, 0.3);">
<p style="color: #8b949e; font-size: 13px;">
💡 API Key 用于访问 API 服务。请妥善保管,不要泄露给他人。如果 Key 泄露,请立即更改。
</p>
</div>
</div>
<!-- 上传凭证 Tab -->
<div class="tab-content" id="tabUpload">
<h3 class="section-title">上传授权凭证</h3>
<!-- Kiro AWS 导入卡片 -->
<div class="action-card" style="margin-bottom: 16px;">
<div class="action-card-content">
<div class="action-card-icon">☁️</div>
<div class="action-card-text">
<h4>Kiro AWS 导入</h4>
<p>从 AWS SSO cache 导入 Kiro 凭据</p>
</div>
</div>
<button class="btn-action" onclick="showAwsImportModal()">
<span>📤</span> 导入
</button>
</div>
<!-- Kiro 批量导入卡片 -->
<div class="action-card" style="background: linear-gradient(135deg, #134e4a 0%, #115e59 100%); border-color: #14b8a6; margin-bottom: 24px;">
<div class="action-card-content">
<div class="action-card-icon" style="background: rgba(20, 184, 166, 0.2);">🔄</div>
<div class="action-card-text">
<h4>Kiro RefreshToken 批量导入</h4>
<p>批量导入 Refresh Token</p>
</div>
</div>
<button class="btn-action" style="background: linear-gradient(135deg, #0d9488, #14b8a6);" onclick="showBatchImportModal()">
<span>📋</span> 导入
</button>
</div>
<!-- 授权文件上传 -->
<h3 class="section-title" style="margin-top: 32px;">授权文件上传</h3>
<p style="color: #8b949e; font-size: 13px; margin-bottom: 16px;">上传 OAuth 授权文件到对应的提供商目录</p>
<div class="upload-providers-grid">
<div class="upload-provider-card" data-provider="gemini-cli-oauth">
<div class="upload-provider-icon">🔷</div>
<div class="upload-provider-info">
<div class="upload-provider-name">Gemini CLI</div>
<div class="upload-provider-type">gemini-cli-oauth</div>
</div>
<input type="file" class="provider-file-input" accept=".json,.txt,.key,.pem" style="display:none">
<button class="btn-upload" onclick="triggerProviderUpload(this)">
<span class="btn-upload-text">选择文件</span>
<span class="btn-upload-loading" style="display:none">上传中...</span>
</button>
<div class="upload-provider-status"></div>
</div>
<div class="upload-provider-card" data-provider="gemini-antigravity">
<div class="upload-provider-icon">🌀</div>
<div class="upload-provider-info">
<div class="upload-provider-name">Antigravity</div>
<div class="upload-provider-type">gemini-antigravity</div>
</div>
<input type="file" class="provider-file-input" accept=".json,.txt,.key,.pem" style="display:none">
<button class="btn-upload" onclick="triggerProviderUpload(this)">
<span class="btn-upload-text">选择文件</span>
<span class="btn-upload-loading" style="display:none">上传中...</span>
</button>
<div class="upload-provider-status"></div>
</div>
<div class="upload-provider-card" data-provider="claude-kiro-oauth">
<div class="upload-provider-icon">🤖</div>
<div class="upload-provider-info">
<div class="upload-provider-name">Kiro (Claude)</div>
<div class="upload-provider-type">claude-kiro-oauth</div>
</div>
<input type="file" class="provider-file-input" accept=".json,.txt,.key,.pem" style="display:none">
<button class="btn-upload" onclick="triggerProviderUpload(this)">
<span class="btn-upload-text">选择文件</span>
<span class="btn-upload-loading" style="display:none">上传中...</span>
</button>
<div class="upload-provider-status"></div>
</div>
<div class="upload-provider-card" data-provider="openai-qwen-oauth">
<div class="upload-provider-icon">🌐</div>
<div class="upload-provider-info">
<div class="upload-provider-name">Qwen (OpenAI)</div>
<div class="upload-provider-type">openai-qwen-oauth</div>
</div>
<input type="file" class="provider-file-input" accept=".json,.txt,.key,.pem" style="display:none">
<button class="btn-upload" onclick="triggerProviderUpload(this)">
<span class="btn-upload-text">选择文件</span>
<span class="btn-upload-loading" style="display:none">上传中...</span>
</button>
<div class="upload-provider-status"></div>
</div>
<div class="upload-provider-card" data-provider="openai-iflow">
<div class="upload-provider-icon">🔄</div>
<div class="upload-provider-info">
<div class="upload-provider-name">iFlow (OpenAI)</div>
<div class="upload-provider-type">openai-iflow</div>
</div>
<input type="file" class="provider-file-input" accept=".json,.txt,.key,.pem" style="display:none">
<button class="btn-upload" onclick="triggerProviderUpload(this)">
<span class="btn-upload-text">选择文件</span>
<span class="btn-upload-loading" style="display:none">上传中...</span>
</button>
<div class="upload-provider-status"></div>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div id="toast" class="toast"></div>
<!-- AWS 导入模态框 -->
<div class="modal-overlay" id="awsImportModal">
<div class="modal">
<div class="modal-header">
<h3>☁️ Kiro AWS 导入</h3>
<button class="modal-close" onclick="closeModal('awsImportModal')">&times;</button>
</div>
<div class="modal-body">
<div class="info-box">
<p>从 AWS SSO cache 目录导入凭据文件</p>
<p style="margin-top: 8px;"><code>C:\Users\{username}\.aws\sso\cache</code></p>
</div>
<!-- 模式切换 -->
<div class="mode-toggle">
<button class="mode-btn active" id="modeFileBtn" onclick="switchAwsMode('file')">
📁 文件上传
</button>
<button class="mode-btn" id="modeJsonBtn" onclick="switchAwsMode('json')">
📝 JSON 粘贴
</button>
</div>
<!-- 文件上传模式 -->
<div id="fileModeSection">
<div class="upload-zone" id="uploadZone">
<input type="file" id="awsFilesInput" multiple accept=".json" style="display: none;">
<div class="icon">📤</div>
<p>拖拽 JSON 文件到此处,或点击选择</p>
<p class="hint">支持多文件上传,系统将智能合并</p>
</div>
<div class="file-list" id="fileList" style="display: none;"></div>
</div>
<!-- JSON 输入模式 -->
<div id="jsonModeSection" style="display: none;">
<textarea class="form-input" id="awsJsonInput" rows="10" style="resize: vertical; font-family: monospace;" placeholder='{
"clientId": "...",
"clientSecret": "...",
"accessToken": "...",
"refreshToken": "..."
}'></textarea>
</div>
<!-- 验证结果 -->
<div class="validation-result" id="validationResult" style="display: none;"></div>
<!-- JSON 预览 -->
<div class="json-preview" id="jsonPreview" style="display: none;">
<div class="json-preview-header">👁️ 合并预览(敏感信息已脱敏)</div>
<pre id="jsonPreviewContent"></pre>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeModal('awsImportModal')">取消</button>
<button class="btn-primary" id="awsImportBtn" onclick="startAwsImport()" disabled>导入</button>
</div>
</div>
</div>
<!-- 批量导入模态框 -->
<div class="modal-overlay" id="batchImportModal">
<div class="modal">
<div class="modal-header">
<h3>🔄 Kiro RefreshToken 批量导入</h3>
<button class="modal-close" onclick="closeModal('batchImportModal')">&times;</button>
</div>
<div class="modal-body">
<div class="info-box" style="background: rgba(20, 184, 166, 0.1); border-color: rgba(20, 184, 166, 0.3);">
<p style="color: #5eead4;">请输入 refreshToken,每行一个。系统将自动刷新并生成凭据文件。</p>
</div>
<textarea class="form-input" id="batchTokensInput" rows="10" style="resize: vertical; font-family: monospace;" placeholder="aorAxxxxxxxx
aorAyyyyyyyy"></textarea>
<div id="batchProgress" style="display: none; margin-top: 16px;">
<div style="display: flex; justify-content: space-between; font-size: 12px; color: #8b949e; margin-bottom: 8px;">
<span id="batchProgressText">正在导入...</span>
<span id="batchProgressPercent">0%</span>
</div>
<div style="height: 6px; background: #21262d; border-radius: 3px; overflow: hidden;">
<div id="batchProgressBar" style="height: 100%; width: 0%; background: linear-gradient(90deg, #14b8a6, #5eead4); transition: width 0.3s;"></div>
</div>
<div id="batchResults" style="margin-top: 12px; max-height: 150px; overflow-y: auto; font-size: 12px; font-family: monospace;"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeModal('batchImportModal')">取消</button>
<button class="btn-primary" style="background: linear-gradient(135deg, #0d9488, #14b8a6);" onclick="startBatchImport()">开始导入</button>
</div>
</div>
</div>
<!-- 重置 Key 模态框 -->
<div class="modal-overlay" id="regenerateKeyModal">
<div class="modal" style="max-width: 480px;">
<div class="modal-header">
<h3>🔑 重置 API Key</h3>
<button class="modal-close" onclick="closeModal('regenerateKeyModal')">&times;</button>
</div>
<div class="modal-body">
<!-- 确认阶段 -->
<div id="regenerateConfirmSection">
<div class="info-box" style="background: rgba(239, 68, 68, 0.1); border-color: rgba(239, 68, 68, 0.3);">
<p style="color: #fca5a5;">⚠️ 重置后旧 Key 将立即失效,请确认操作:</p>
</div>
<ul style="color: #8b949e; font-size: 13px; margin: 16px 0; padding-left: 20px;">
<li style="margin-bottom: 8px;">旧 API Key 将无法继续使用</li>
<li style="margin-bottom: 8px;">需要使用新 Key 重新配置客户端</li>
<li>您的凭证数据会自动迁移到新 Key</li>
</ul>
</div>
<!-- 结果阶段 -->
<div id="regenerateResultSection" style="display: none;">
<div class="info-box" style="background: rgba(16, 185, 129, 0.1); border-color: rgba(16, 185, 129, 0.3);">
<p style="color: #6ee7b7;">✅ API Key 重置成功!请妥善保存新 Key。</p>
</div>
<div style="margin-top: 16px;">
<label style="font-size: 12px; color: #8b949e; display: block; margin-bottom: 8px;">新 API Key</label>
<div style="display: flex; gap: 8px;">
<input type="text" class="form-input" id="newKeyDisplay" readonly style="flex: 1; font-family: monospace; font-size: 13px;">
<button class="btn-secondary" onclick="copyNewKey()" style="white-space: nowrap;">
<span id="copyBtnText">📋 复制</span>
</button>
</div>
</div>
<p style="font-size: 12px; color: #f59e0b; margin-top: 12px;">⚠️ 关闭此窗口后将无法再次查看完整 Key</p>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeModal('regenerateKeyModal')" id="regenerateCancelBtn">取消</button>
<button class="btn-primary" style="background: linear-gradient(135deg, #dc2626, #ef4444);" onclick="confirmRegenerateKey()" id="regenerateConfirmBtn">确认重置</button>
</div>
</div>
</div>
<script>
const API_BASE = '/api/potluckuser';
let currentApiKey = '';
let isLoggedIn = false;
// AWS 导入状态
let awsUploadedFiles = [];
let awsMergedCredentials = null;
let awsCurrentMode = 'file';
// 提供商配置
const providerIcons = {
'claude-kiro-oauth': '🤖',
'gemini-cli-oauth': '🔷',
'gemini-antigravity': '🌀',
'openai-qwen-oauth': '🌐',
'openai-iflow': '🔄'
};
const providerNames = {
'claude-kiro-oauth': 'Kiro (Claude)',
'gemini-cli-oauth': 'Gemini CLI',
'gemini-antigravity': 'Antigravity',
'openai-qwen-oauth': 'Qwen (OpenAI)',
'openai-iflow': 'iFlow (OpenAI)'
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
initUploadZone();
initProviderUploads();
const savedKey = localStorage.getItem('potluck_user_key');
if (savedKey) {
document.getElementById('apiKeyInput').value = savedKey;
login();
}
document.getElementById('apiKeyInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') login();
});
document.getElementById('awsJsonInput').addEventListener('input', () => {
if (awsCurrentMode === 'json') validateAwsJsonInput();
});
});
// 初始化提供商上传
function initProviderUploads() {
document.querySelectorAll('.upload-provider-card').forEach(card => {
const input = card.querySelector('.provider-file-input');
const btn = card.querySelector('.btn-upload');
const status = card.querySelector('.upload-provider-status');
const providerType = card.dataset.provider;
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const allowedExtensions = ['.json', '.txt', '.key', '.pem', '.p12', '.pfx'];
const ext = '.' + file.name.split('.').pop().toLowerCase();
if (!allowedExtensions.includes(ext)) {
status.textContent = '不支持的文件类型';
status.className = 'upload-provider-status error';
input.value = '';
return;
}
if (file.size > 5 * 1024 * 1024) {
status.textContent = '文件大小不能超过 5MB';
status.className = 'upload-provider-status error';
input.value = '';
return;
}
btn.disabled = true;
btn.querySelector('.btn-upload-text').style.display = 'none';
btn.querySelector('.btn-upload-loading').style.display = 'inline';
status.textContent = '';
status.className = 'upload-provider-status';
try {
const formData = new FormData();
formData.append('file', file);
formData.append('provider', providerType);
const response = await fetch(`${API_BASE}/upload`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${currentApiKey}` },
body: formData
});
const data = await response.json();
if (response.ok && data.success) {
status.textContent = '✓ ' + (data.filePath || '上传成功');
status.className = 'upload-provider-status success';
showToast('文件上传成功', 'success');
loadCredentials();
} else {
throw new Error(data.error?.message || '上传失败');
}
} catch (error) {
status.textContent = '✗ ' + error.message;
status.className = 'upload-provider-status error';
showToast('上传失败: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.querySelector('.btn-upload-text').style.display = 'inline';
btn.querySelector('.btn-upload-loading').style.display = 'none';
input.value = '';
}
});
});
}
// 触发提供商文件上传
function triggerProviderUpload(btn) {
const card = btn.closest('.upload-provider-card');
const input = card.querySelector('.provider-file-input');
input.click();
}
// 登录
async function login() {
const apiKey = document.getElementById('apiKeyInput').value.trim();
if (!apiKey) {
showToast('请输入 API Key', 'error');
return;
}
if (!apiKey.startsWith('maki_')) {
showToast('API Key 格式不正确', 'error');
return;
}
currentApiKey = apiKey;
document.getElementById('loginBtn').disabled = true;
try {
const response = await fetch(`${API_BASE}/usage`, {
method: 'GET',
headers: { 'Authorization': `Bearer ${apiKey}` }
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error?.message || '登录失败');
}
if (document.getElementById('rememberKey').checked) {
localStorage.setItem('potluck_user_key', apiKey);
}
isLoggedIn = true;
displayUserInfo(data.data);
document.getElementById('loginContainer').style.display = 'none';
document.getElementById('mainContainer').style.display = 'block';
document.getElementById('navbarUser').style.display = 'flex';
loadCredentials();
showToast('登录成功', 'success');
} catch (error) {
showToast(error.message, 'error');
} finally {
document.getElementById('loginBtn').disabled = false;
}
}
// 退出登录
function logout() {
currentApiKey = '';
isLoggedIn = false;
localStorage.removeItem('potluck_user_key');
document.getElementById('loginContainer').style.display = 'flex';
document.getElementById('mainContainer').style.display = 'none';
document.getElementById('navbarUser').style.display = 'none';
document.getElementById('apiKeyInput').value = '';
showToast('已退出登录', 'success');
}
// 显示用户信息
function displayUserInfo(data) {
document.getElementById('navUserName').textContent = data.name || '用户';
// 每日用量: 已用/限额
document.getElementById('statToday').textContent = data.usage.today;
document.getElementById('statLimit').textContent = data.usage.limit;
// 资源包: 已用/总量
const bonusUsed = data.bonusUsed || 0;
const bonusTotal = data.bonusTotal || 0;
document.getElementById('statBonusUsed').textContent = bonusUsed;
document.getElementById('statBonusTotal').textContent = bonusTotal;
// 累计调用
document.getElementById('statTotal').textContent = data.total || 0;
// 总使用 = 每日已用 + 资源包已用
const totalUsed = data.usage.today + bonusUsed;
document.getElementById('usageToday').textContent = totalUsed;
// 总配额上限 = 每日限额 + 资源包总量
const totalQuota = data.usage.limit + bonusTotal;
document.getElementById('usageLimit').textContent = totalQuota;
// 显示完整 API Key(在 API密钥 Tab)
document.getElementById('displayApiKeyFull').value = currentApiKey;
}
// 复制到剪贴板(兼容方案)
function copyToClipboardFallback(text, successMsg = '已复制到剪贴板') {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
textArea.style.top = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showToast(successMsg, 'success');
} catch (err) {
showToast('复制失败,请手动复制', 'error');
}
document.body.removeChild(textArea);
}
// 复制 API Key
function copyApiKey() {
const text = currentApiKey;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
document.getElementById('copyKeyIcon').textContent = '✓';
document.getElementById('copyKeyText').textContent = '已复制';
showToast('已复制到剪贴板', 'success');
setTimeout(() => {
document.getElementById('copyKeyIcon').textContent = '📋';
document.getElementById('copyKeyText').textContent = '复制';
}, 2000);
}).catch(() => copyToClipboardFallback(text));
} else {
copyToClipboardFallback(text);
}
}
// 打开重置 Key 模态框
function showRegenerateModal() {
// 重置模态框状态
document.getElementById('regenerateConfirmSection').style.display = 'block';
document.getElementById('regenerateResultSection').style.display = 'none';
document.getElementById('regenerateConfirmBtn').style.display = 'inline-flex';
document.getElementById('regenerateCancelBtn').textContent = '取消';
document.getElementById('regenerateKeyModal').classList.add('active');
}
// 确认重置 Key
async function confirmRegenerateKey() {
const confirmBtn = document.getElementById('regenerateConfirmBtn');
confirmBtn.disabled = true;
confirmBtn.textContent = '重置中...';
try {
const response = await fetch(`${API_BASE}/regenerate-key`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${currentApiKey}` }
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error?.message || data.error || '重置失败');
}
const newKey = data.data.newKey;
// 更新本地存储和当前 Key
currentApiKey = newKey;
if (localStorage.getItem('potluck_user_key')) {
localStorage.setItem('potluck_user_key', newKey);
}
// 显示结果
document.getElementById('regenerateConfirmSection').style.display = 'none';
document.getElementById('regenerateResultSection').style.display = 'block';
document.getElementById('newKeyDisplay').value = newKey;
document.getElementById('regenerateConfirmBtn').style.display = 'none';
document.getElementById('regenerateCancelBtn').textContent = '关闭';
// 更新 API密钥 Tab 的显示
document.getElementById('displayApiKeyFull').value = newKey;
showToast('API Key 已重置', 'success');
} catch (error) {
showToast('重置失败: ' + error.message, 'error');
confirmBtn.disabled = false;
confirmBtn.textContent = '确认重置';
}
}
// 复制新 Key(模态框中)
function copyNewKey() {
const text = document.getElementById('newKeyDisplay').value;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
const btnText = document.getElementById('copyBtnText');
btnText.textContent = '✓ 已复制';
showToast('已复制到剪贴板', 'success');
setTimeout(() => { btnText.textContent = '📋 复制'; }, 2000);
}).catch(() => copyToClipboardFallback(text));
} else {
copyToClipboardFallback(text);
}
}
// Tab 切换
function switchTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabId);
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.id === `tab${tabId.charAt(0).toUpperCase() + tabId.slice(1)}`);
});
if (tabId === 'credentials') loadCredentials();
}
// 加载凭证列表(自动检查健康状态)
async function loadCredentials(autoCheck = true) {
const listEl = document.getElementById('credentialsList');
listEl.innerHTML = '<div style="text-align: center; color: #8b949e; padding: 40px;">加载中...</div>';
try {
let credentials;
if (autoCheck) {
// 调用批量检查 API,同时获取最新状态
const response = await fetch(`${API_BASE}/credentials/check-all`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${currentApiKey}` }
});
const data = await response.json();
if (!response.ok || !data.success) throw new Error(data.error?.message || '加载失败');
credentials = data.data.credentials || [];
} else {
// 仅获取列表,不检查
const response = await fetch(`${API_BASE}/credentials`, {
headers: { 'Authorization': `Bearer ${currentApiKey}` }
});
const data = await response.json();
if (!response.ok || !data.success) throw new Error(data.error?.message || '加载失败');
credentials = data.data || [];
}
document.getElementById('credentialCount').textContent = credentials.filter(c => c.isHealthy).length;
if (credentials.length === 0) {
listEl.innerHTML = '<div class="empty-state"><div class="icon">📭</div><p>暂无凭证,请通过上传功能添加</p></div>';
return;
}
// 按添加时间倒序排序
credentials.sort((a, b) => new Date(b.addedAt || 0) - new Date(a.addedAt || 0));
listEl.innerHTML = '';
credentials.forEach(cred => {
listEl.appendChild(createCredentialItem(cred));
});
} catch (error) {
listEl.innerHTML = `<div style="text-align: center; color: #ef4444; padding: 40px;">加载失败: ${error.message}</div>`;
}
}
// 创建凭证项
function createCredentialItem(cred) {
const item = document.createElement('div');
item.className = 'credential-item';
const icon = providerIcons[cred.provider] || '📄';
const name = providerNames[cred.provider] || cred.provider;
const shortPath = cred.path.length > 50 ? '...' + cred.path.slice(-47) : cred.path;
// 格式化添加日期
let addedDateStr = '';
if (cred.addedAt) {
const d = new Date(cred.addedAt);
addedDateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
let statusClass = 'unknown', statusText = '未检查', statusIcon = '❓';
if (cred.isHealthy === true) {
statusClass = 'healthy';
statusText = cred.healthMessage || '正常';
statusIcon = '✓';
} else if (cred.isHealthy === false) {
statusClass = 'unhealthy';
statusText = cred.healthMessage || '异常';
statusIcon = '✗';
}
// 资源包信息
let bonusHtml = '';
if (cred.bonus) {
const usedPercent = cred.bonus.total > 0 ? Math.round((cred.bonus.usedCount / cred.bonus.total) * 100) : 0;
const bonusColor = cred.bonus.remaining > 0 ? '#10b981' : '#8b949e';
bonusHtml = `
<div class="credential-bonus" style="display: flex; align-items: center; gap: 8px; margin-left: 12px;">
<span style="font-size: 12px; color: #8b949e;">资源包:</span>
<span style="font-size: 13px; font-weight: 600; color: ${bonusColor};">${cred.bonus.remaining}/${cred.bonus.total}</span>
</div>
`;
}
item.innerHTML = `
<div class="credential-info">
<div class="credential-icon">${icon}</div>
<div class="credential-details">
<div class="credential-name">${name}${addedDateStr ? `<span style="color: #8b949e; font-weight: 400; font-size: 12px; margin-left: 8px;">${addedDateStr}</span>` : ''}</div>
<div class="credential-path" title="${cred.path}">${shortPath}</div>
</div>
</div>
<div style="display: flex; align-items: center;">
${bonusHtml}
<div class="credential-status ${statusClass}" id="status-${cred.id}">
<span>${statusIcon}</span>
<span>${statusText}</span>
</div>
</div>
`;
return item;
}
// 模态框控制
function showAwsImportModal() {
awsUploadedFiles = [];
awsMergedCredentials = null;
awsCurrentMode = 'file';
document.getElementById('awsFilesInput').value = '';
document.getElementById('awsJsonInput').value = '';
document.getElementById('fileList').style.display = 'none';
document.getElementById('fileList').innerHTML = '';
document.getElementById('validationResult').style.display = 'none';
document.getElementById('jsonPreview').style.display = 'none';
document.getElementById('awsImportBtn').disabled = true;
switchAwsMode('file');
document.getElementById('awsImportModal').classList.add('active');
}
function showBatchImportModal() {
document.getElementById('batchTokensInput').value = '';
document.getElementById('batchProgress').style.display = 'none';
document.getElementById('batchResults').innerHTML = '';
document.getElementById('batchImportModal').classList.add('active');
}
function closeModal(id) {
document.getElementById(id).classList.remove('active');
}
// AWS 模式切换
function switchAwsMode(mode) {
awsCurrentMode = mode;
document.getElementById('modeFileBtn').classList.toggle('active', mode === 'file');
document.getElementById('modeJsonBtn').classList.toggle('active', mode === 'json');
document.getElementById('fileModeSection').style.display = mode === 'file' ? 'block' : 'none';
document.getElementById('jsonModeSection').style.display = mode === 'json' ? 'block' : 'none';
if (mode === 'file') validateAwsFiles();
else validateAwsJsonInput();
}
// 初始化上传区域
function initUploadZone() {
const zone = document.getElementById('uploadZone');
const input = document.getElementById('awsFilesInput');
zone.addEventListener('click', () => input.click());
zone.addEventListener('dragover', (e) => {
e.preventDefault();
zone.style.borderColor = '#a855f7';
zone.style.background = 'rgba(168, 85, 247, 0.05)';
});
zone.addEventListener('dragleave', (e) => {
e.preventDefault();
zone.style.borderColor = '#30363d';
zone.style.background = 'transparent';
});
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.style.borderColor = '#30363d';
zone.style.background = 'transparent';
const files = Array.from(e.dataTransfer.files).filter(f => f.name.endsWith('.json'));
if (files.length > 0) processAwsFiles(files);
});
input.addEventListener('change', () => {
const files = Array.from(input.files);
if (files.length > 0) processAwsFiles(files);
});
}
// 处理上传文件
async function processAwsFiles(files) {
for (const file of files) {
const existingIndex = awsUploadedFiles.findIndex(f => f.name === file.name);
try {
const content = await file.text();
const json = JSON.parse(content);
if (existingIndex >= 0) {
awsUploadedFiles[existingIndex] = { name: file.name, content: json };
} else {
awsUploadedFiles.push({ name: file.name, content: json });
}
} catch (error) {
showToast(`解析失败: ${file.name}`, 'error');
}
}
renderFileList();
document.getElementById('awsFilesInput').value = '';
validateAwsFiles();
}
// 渲染文件列表
function renderFileList() {
const listEl = document.getElementById('fileList');
if (awsUploadedFiles.length === 0) {
listEl.style.display = 'none';
return;
}
listEl.style.display = 'block';
listEl.innerHTML = awsUploadedFiles.map(file => {
const fields = Object.keys(file.content).slice(0, 3).join(', ');
return `
<div class="file-item">
<div class="file-item-info">
<span>📄</span>
<div>
<div class="file-item-name">${file.name}</div>
<div class="file-item-fields">${fields}...</div>
</div>
</div>
<button class="file-item-remove" onclick="removeFile('${file.name}')">✕</button>
</div>
`;
}).join('');
}
function removeFile(filename) {
awsUploadedFiles = awsUploadedFiles.filter(f => f.name !== filename);
renderFileList();
validateAwsFiles();
}
// 验证文件
function validateAwsFiles() {
if (awsUploadedFiles.length === 0) {
hideValidation();
return;
}
awsMergedCredentials = {};
let expiresAtFromRefreshTokenFile = null;
for (const file of awsUploadedFiles) {
if (file.content.refreshToken && file.content.expiresAt) {
expiresAtFromRefreshTokenFile = file.content.expiresAt;
}
Object.assign(awsMergedCredentials, file.content);
}
if (expiresAtFromRefreshTokenFile) {
awsMergedCredentials.expiresAt = expiresAtFromRefreshTokenFile;
}
showValidationResult();
}
// 验证 JSON 输入
function validateAwsJsonInput() {
const inputValue = document.getElementById('awsJsonInput').value.trim();
if (!inputValue) {
hideValidation();
return;
}
try {
awsMergedCredentials = JSON.parse(inputValue);
showValidationResult();
} catch (error) {
document.getElementById('validationResult').style.display = 'block';
document.getElementById('validationResult').className = 'validation-result error';
document.getElementById('validationResult').innerHTML = `<h4>❌ JSON 解析错误</h4><p style="font-size: 12px; color: #8b949e;">${error.message}</p>`;
document.getElementById('jsonPreview').style.display = 'none';
document.getElementById('awsImportBtn').disabled = true;
awsMergedCredentials = null;
}
}
function hideValidation() {
document.getElementById('validationResult').style.display = 'none';
document.getElementById('jsonPreview').style.display = 'none';
document.getElementById('awsImportBtn').disabled = true;
awsMergedCredentials = null;
}
function showValidationResult() {
const fields = [
{ key: 'clientId', has: !!awsMergedCredentials.clientId },
{ key: 'clientSecret', has: !!awsMergedCredentials.clientSecret },
{ key: 'accessToken', has: !!awsMergedCredentials.accessToken },
{ key: 'refreshToken', has: !!awsMergedCredentials.refreshToken }
];
const isValid = fields.every(f => f.has);
const resultEl = document.getElementById('validationResult');
resultEl.style.display = 'block';
resultEl.className = `validation-result ${isValid ? 'success' : 'error'}`;
resultEl.innerHTML = `
<h4>${isValid ? '✅ 验证通过' : '❌ 验证失败'}</h4>
<ul>
${fields.map(f => `<li><span class="${f.has ? 'found' : 'missing'}">${f.has ? '✓' : '✗'}</span> ${f.key}</li>`).join('')}
</ul>
`;
document.getElementById('awsImportBtn').disabled = !isValid;
// 显示预览
const previewEl = document.getElementById('jsonPreview');
const contentEl = document.getElementById('jsonPreviewContent');
previewEl.style.display = 'block';
const previewData = { ...awsMergedCredentials };
if (previewData.clientSecret) previewData.clientSecret = previewData.clientSecret.substring(0, 8) + '...' + previewData.clientSecret.slice(-4);
if (previewData.accessToken) previewData.accessToken = previewData.accessToken.substring(0, 20) + '...' + previewData.accessToken.slice(-10);
if (previewData.refreshToken) previewData.refreshToken = previewData.refreshToken.substring(0, 10) + '...' + previewData.refreshToken.slice(-6);
contentEl.textContent = JSON.stringify(previewData, null, 2);
}
// AWS 导入
async function startAwsImport() {
if (!awsMergedCredentials) return;
const btn = document.getElementById('awsImportBtn');
btn.disabled = true;
btn.textContent = '导入中...';
try {
if (!awsMergedCredentials.authMethod) {
awsMergedCredentials.authMethod = 'builder-id';
}
const response = await fetch(`${API_BASE}/kiro/import-aws-credentials`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${currentApiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ credentials: awsMergedCredentials })
});
const data = await response.json();
if (response.ok && data.success) {
showToast('AWS 凭据导入成功', 'success');
closeModal('awsImportModal');
loadCredentials();
} else if (data.error === 'duplicate') {
showToast('凭据已存在', 'error');
} else {
throw new Error(data.error || '导入失败');
}
} catch (error) {
showToast('导入错误: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = '导入';
}
}
// 批量导入
async function startBatchImport() {
const tokens = document.getElementById('batchTokensInput').value.split('\n').filter(t => t.trim());
if (tokens.length === 0) {
showToast('请输入至少一个 refreshToken', 'error');
return;
}
const progressDiv = document.getElementById('batchProgress');
const progressBar = document.getElementById('batchProgressBar');
const progressText = document.getElementById('batchProgressText');
const progressPercent = document.getElementById('batchProgressPercent');
const resultsDiv = document.getElementById('batchResults');
progressDiv.style.display = 'block';
progressBar.style.width = '0%';
resultsDiv.innerHTML = '';
try {
const response = await fetch(`${API_BASE}/kiro/batch-import-tokens`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${currentApiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshTokens: tokens })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6));
if (data.current) {
const percent = Math.round((data.index / data.total) * 100);
progressBar.style.width = percent + '%';
progressPercent.textContent = percent + '%';
progressText.textContent = `正在导入 ${data.index}/${data.total}`;
const color = data.current.success ? '#10b981' : '#ef4444';
const icon = data.current.success ? '✓' : '✗';
const msg = data.current.success ? data.current.path : data.current.error;
resultsDiv.innerHTML += `<div style="color:${color}">${icon} Token ${data.current.index}: ${msg}</div>`;
resultsDiv.scrollTop = resultsDiv.scrollHeight;
}
if (data.successCount !== undefined) {
showToast(`导入完成: 成功 ${data.successCount}, 失败 ${data.failedCount}`, 'success');
loadCredentials();
setTimeout(() => closeModal('batchImportModal'), 2000);
}
} catch (e) {}
}
}
}
} catch (error) {
showToast('导入失败: ' + error.message, 'error');
}
}
// Toast
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast ${type} show`;
setTimeout(() => toast.classList.remove('show'), 3000);
}
</script>
</body>
</html>