This commit is contained in:
Daniel
2026-04-28 19:16:27 +08:00
parent 6de7e782fc
commit c234fe64d6
13 changed files with 229 additions and 31 deletions

View File

@@ -4,6 +4,7 @@ import logging
import math import math
import re import re
import secrets import secrets
import socket
import time import time
import uuid import uuid
from pathlib import Path from pathlib import Path
@@ -175,18 +176,31 @@ def _platform_model_cfg() -> dict:
def _select_model_cfg(user_id: int, prefer_vip: bool = True) -> tuple[dict | None, str]: def _select_model_cfg(user_id: int, prefer_vip: bool = True) -> tuple[dict | None, str]:
vip = users.get_vip_status(user_id) vip = users.get_vip_status(user_id)
now = int(time.time())
active_user_model = users.get_active_ai_model(user_id)
has_user_model = bool(
active_user_model
and (active_user_model.get("api_key") or "").strip()
and (active_user_model.get("model") or "").strip()
)
purchased_cycle_active = bool(int(vip.get("cycle_started_at") or 0) > 0 and int(vip.get("cycle_expires_at") or 0) > now)
if prefer_vip and vip.get("vip_enabled"): if prefer_vip and vip.get("vip_enabled"):
if int(vip.get("total_available_credits") or 0) <= 0: total_available = int(vip.get("total_available_credits") or 0)
if total_available < _min_platform_credits_required():
return None, "vip_empty" return None, "vip_empty"
# 已购买订阅:优先平台模型
# 未购买订阅:若未配置自有模型,则默认走平台模型(可使用赠送额度)
if purchased_cycle_active or not has_user_model:
cfg = _platform_model_cfg() cfg = _platform_model_cfg()
if cfg.get("api_key"): if cfg.get("api_key"):
return cfg, "vip" return cfg, "vip"
cfg = users.get_active_ai_model(user_id) cfg = active_user_model
return cfg, "user" return cfg, "user"
def _quota_detail() -> str: def _quota_detail() -> str:
return "Credits 额度已用完(席位额度+共享加油包),请充值或等待下个计费周期" min_required = _min_platform_credits_required()
return f"Credits 额度不足,平台模型至少需要 {min_required} Credits1张图片或10000Token门槛请订阅或充值"
def _credits_from_cny(amount_cny: float) -> int: def _credits_from_cny(amount_cny: float) -> int:
@@ -222,6 +236,13 @@ def _estimate_image_cost(image_count: int) -> int:
return _credits_from_cny((cnt / pkg_images) * pkg_cny) return _credits_from_cny((cnt / pkg_images) * pkg_cny)
def _min_platform_credits_required() -> int:
one_image_credits = _estimate_image_cost(1)
token_10k_cny = (10_000.0 / 1_000_000.0) * max(0.0, float(settings.credits_token_price_per_million_cny))
token_10k_credits = _credits_from_cny(token_10k_cny)
return max(1, one_image_credits, token_10k_credits)
def _new_order_no() -> str: def _new_order_no() -> str:
return f"RC{time.strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:10].upper()}" return f"RC{time.strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:10].upper()}"
@@ -743,6 +764,31 @@ async def pay_address_suggest(request: Request):
return {"ok": True, "detail": "IP地址解析失败已填入IP信息可手动修改", "ip": ip, "address": f"IP:{ip}"} return {"ok": True, "detail": "IP地址解析失败已填入IP信息可手动修改", "ip": ip, "address": f"IP:{ip}"}
@app.get("/api/tools/server-ip")
async def tools_server_ip(request: Request):
user = _require_user(request)
if not user:
return {"ok": False, "detail": "请先登录"}
public_ip = ""
try:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get("https://api64.ipify.org?format=json")
body = resp.json() if resp.content else {}
if resp.status_code < 400 and isinstance(body, dict):
public_ip = str(body.get("ip") or "").strip()
except Exception:
public_ip = ""
private_ip = ""
try:
private_ip = socket.gethostbyname(socket.gethostname())
except Exception:
private_ip = ""
ip = public_ip or private_ip
if not ip:
return {"ok": False, "detail": "无法识别服务器IP请稍后重试"}
return {"ok": True, "ip": ip, "public_ip": public_ip, "private_ip": private_ip}
@app.post("/api/billing/recharge/notify") @app.post("/api/billing/recharge/notify")
async def billing_recharge_notify(req: BillingRechargeNotifyRequest, request: Request): async def billing_recharge_notify(req: BillingRechargeNotifyRequest, request: Request):
token = (request.headers.get("X-Shop-Token") or "").strip() token = (request.headers.get("X-Shop-Token") or "").strip()
@@ -967,7 +1013,7 @@ async def rewrite(req: RewriteRequest, request: Request):
billed_basis = "usage_tokens" if total_tokens > 0 else "char_estimate" billed_basis = "usage_tokens" if total_tokens > 0 else "char_estimate"
token_cost = _estimate_rewrite_cost(req, result) token_cost = _estimate_rewrite_cost(req, result)
vip_status = users.get_vip_status(user["id"]) vip_status = users.get_vip_status(user["id"])
should_consume = bool(vip_status.get("vip_enabled")) should_consume = model_source == "vip"
if should_consume: if should_consume:
ok_cost, balance = users.consume_tokens( ok_cost, balance = users.consume_tokens(
user["id"], user["id"],

View File

@@ -709,10 +709,10 @@ class UserStore:
cycle_started_at = int(row["cycle_started_at"] or 0) if row else 0 cycle_started_at = int(row["cycle_started_at"] or 0) if row else 0
cycle_expires_at = int(row["cycle_expires_at"] or 0) if row else 0 cycle_expires_at = int(row["cycle_expires_at"] or 0) if row else 0
now = int(time.time()) now = int(time.time())
cycle_active = cycle_expires_at > now if cycle_expires_at > 0 else True # 仅当已开启有效计费周期时,席位额度才可用;新用户未购买时不应显示席位 1500
cycle_active = cycle_started_at > 0 and cycle_expires_at > now
if not cycle_active: if not cycle_active:
seat_remaining = 0 seat_remaining = 0
shared_credits = 0
return { return {
"vip_enabled": bool(int(row["vip_enabled"] or 0)) if row else False, "vip_enabled": bool(int(row["vip_enabled"] or 0)) if row else False,
"token_balance": shared_credits, "token_balance": shared_credits,

36
app/static/guide.js Normal file
View File

@@ -0,0 +1,36 @@
const guideGetServerIpBtn = document.getElementById("getServerIpBtn");
async function showGuideAlert(message, title = "提示") {
if (typeof window.uiAlert === "function") {
await window.uiAlert(message, title);
return;
}
window.alert(message);
}
if (guideGetServerIpBtn) {
guideGetServerIpBtn.addEventListener("click", async () => {
const idleText = "获取服务器IP";
guideGetServerIpBtn.disabled = true;
guideGetServerIpBtn.textContent = "获取中...";
try {
const res = await fetch("/api/tools/server-ip");
const data = await res.json();
if (!res.ok || !data.ok) {
await showGuideAlert((data && data.detail) || "获取服务器IP失败请稍后重试", "获取失败");
return;
}
const ip = String(data.ip || "").trim();
if (!ip) {
await showGuideAlert("服务器IP为空请稍后重试", "获取失败");
return;
}
await showGuideAlert(`当前服务器IP${ip}`, "API IP白名单");
} catch {
await showGuideAlert("获取服务器IP失败请稍后重试", "获取失败");
} finally {
guideGetServerIpBtn.disabled = false;
guideGetServerIpBtn.textContent = idleText;
}
});
}

27
app/static/mode-hint.js Normal file
View File

@@ -0,0 +1,27 @@
(() => {
const badges = Array.from(document.querySelectorAll(".global-mode-hint"));
if (!badges.length) return;
function setText(text) {
badges.forEach((el) => {
el.textContent = text;
});
}
async function run() {
try {
const res = await fetch("/api/auth/me");
const data = await res.json();
if (!res.ok || !data || !data.logged_in) return;
const vip = data.vip || {};
const now = Math.floor(Date.now() / 1000);
const enabled = Boolean(vip.vip_enabled);
const hasActiveSubscription = Number(vip.cycle_started_at || 0) > 0 && Number(vip.cycle_expires_at || 0) > now;
setText(enabled && hasActiveSubscription ? "当前模型模式:平台模型" : "当前模型模式:自由模型");
} catch {
// ignore
}
}
run();
})();

View File

@@ -207,7 +207,7 @@ if (saveModelBtn) {
api_key: ($("apiKey") && $("apiKey").value.trim()) || "", api_key: ($("apiKey") && $("apiKey").value.trim()) || "",
base_url: ($("baseUrl") && $("baseUrl").value.trim()) || "", base_url: ($("baseUrl") && $("baseUrl").value.trim()) || "",
model: ($("modelValue") && $("modelValue").value.trim()) || "", model: ($("modelValue") && $("modelValue").value.trim()) || "",
image_model: "", image_model: ($("imageModelValue") && $("imageModelValue").value.trim()) || "",
timeout_sec: Number((($("timeoutSec") && $("timeoutSec").value) || "120").trim()), timeout_sec: Number((($("timeoutSec") && $("timeoutSec").value) || "120").trim()),
max_output_tokens: Number((($("maxOutputTokens") && $("maxOutputTokens").value) || "8192").trim()), max_output_tokens: Number((($("maxOutputTokens") && $("maxOutputTokens").value) || "8192").trim()),
max_retries: Number((($("maxRetries") && $("maxRetries").value) || "0").trim()), max_retries: Number((($("maxRetries") && $("maxRetries").value) || "0").trim()),
@@ -219,6 +219,7 @@ if (saveModelBtn) {
setStatus("模型配置已保存并设为当前。"); setStatus("模型配置已保存并设为当前。");
if ($("apiKey")) $("apiKey").value = ""; if ($("apiKey")) $("apiKey").value = "";
if ($("modelName")) $("modelName").value = ""; if ($("modelName")) $("modelName").value = "";
if ($("imageModelValue")) $("imageModelValue").value = "";
await refresh(); await refresh();
} catch (e) { } catch (e) {
setStatus(e.message || "模型保存失败", true); setStatus(e.message || "模型保存失败", true);

View File

@@ -215,6 +215,41 @@ a {
flex-wrap: wrap; flex-wrap: wrap;
} }
.upgrade-topbar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
}
.topbar-spacer {
min-height: 1px;
}
.topbar-center {
display: flex;
justify-content: center;
}
.upgrade-topbar .topbar-actions {
justify-self: end;
flex-wrap: nowrap;
}
.mode-badge {
display: inline-flex;
align-items: center;
min-height: var(--control-h);
padding: 0 12px;
border-radius: var(--radius);
border: 1px solid var(--accent);
background: linear-gradient(180deg, #ff922e, #ff6b16);
color: #fff;
font-size: 12px;
font-weight: 850;
white-space: nowrap;
box-shadow: 0 10px 20px rgba(255, 107, 22, 0.2);
}
.wechat-account-switch { .wechat-account-switch {
min-width: 250px; min-width: 250px;
display: grid; display: grid;
@@ -333,12 +368,14 @@ a {
.panel-scroll { .panel-scroll {
min-height: 0; min-height: 0;
overflow: auto; overflow-y: auto;
overflow-x: hidden;
padding: 10px; padding: 10px;
} }
.input-panel .panel-scroll { .input-panel .panel-scroll {
overflow: visible; overflow-y: visible;
overflow-x: hidden;
padding: 8px; padding: 8px;
} }
@@ -783,27 +820,29 @@ button.secondary:hover,
} }
.target-chars-quick { .target-chars-quick {
display: flex; display: grid;
align-items: center; grid-template-columns: repeat(4, minmax(0, 1fr));
align-items: stretch;
gap: 6px; gap: 6px;
width: 100%;
min-width: 0; min-width: 0;
flex-wrap: wrap;
overflow: visible;
} }
.target-char-chip, .target-char-chip,
button.target-char-chip { button.target-char-chip {
width: auto; width: 100%;
min-width: 54px; min-width: 0;
min-height: 30px;
margin-top: 0; margin-top: 0;
padding: 5px 8px; padding: 5px 6px;
flex: 0 0 auto; border-radius: 8px;
border-radius: 999px;
border-color: var(--line-strong); border-color: var(--line-strong);
color: #344054; color: #344054;
background: #fff; background: #fff;
font-size: 12px; font-size: 12px;
line-height: 1.2; line-height: 1.1;
text-align: center;
white-space: nowrap;
box-shadow: none; box-shadow: none;
} }
@@ -1587,7 +1626,7 @@ button.target-char-chip {
.upgrade-grid { .upgrade-grid {
margin-top: 10px; margin-top: 10px;
display: grid; display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(320px, 0.8fr); grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
gap: 10px; gap: 10px;
} }
@@ -2009,6 +2048,12 @@ button.target-char-chip {
border-radius: 10px; border-radius: 10px;
} }
@media (max-width: 1100px) {
.upgrade-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 860px) { @media (max-width: 860px) {
.upgrade-hero { .upgrade-hero {
align-items: flex-start; align-items: flex-start;

View File

@@ -85,6 +85,8 @@ function renderVip(vip) {
const enabled = Boolean(vip.vip_enabled); const enabled = Boolean(vip.vip_enabled);
const cycleStart = Number(vip.cycle_started_at || 0); const cycleStart = Number(vip.cycle_started_at || 0);
const cycleEnd = Number(vip.cycle_expires_at || 0); const cycleEnd = Number(vip.cycle_expires_at || 0);
const now = Math.floor(Date.now() / 1000);
const hasActiveSubscription = cycleStart > 0 && cycleEnd > now;
if ($("upgradeTokenBalance")) $("upgradeTokenBalance").textContent = String(totalAvailable); if ($("upgradeTokenBalance")) $("upgradeTokenBalance").textContent = String(totalAvailable);
if ($("upgradeTokenBalanceHero")) $("upgradeTokenBalanceHero").textContent = String(totalAvailable); if ($("upgradeTokenBalanceHero")) $("upgradeTokenBalanceHero").textContent = String(totalAvailable);
if ($("vipTokenBalance")) $("vipTokenBalance").textContent = String(shared); if ($("vipTokenBalance")) $("vipTokenBalance").textContent = String(shared);
@@ -103,6 +105,15 @@ function renderVip(vip) {
$("vipCycleHint").textContent = "当前未开始月周期,首次支付成功后开始计时。"; $("vipCycleHint").textContent = "当前未开始月周期,首次支付成功后开始计时。";
} }
} }
if ($("vipModeHint")) {
if (enabled && hasActiveSubscription) {
$("vipModeHint").textContent = "当前模型模式:平台模型(订阅有效)";
} else if (enabled && !hasActiveSubscription) {
$("vipModeHint").textContent = "当前模型模式:自由模型(未开通订阅)";
} else {
$("vipModeHint").textContent = "当前模型模式:自由模型";
}
}
if (totalAvailable <= 0) { if (totalAvailable <= 0) {
setStatus("Credits 额度已用完,请充值共享加油包或等待下个计费周期。", true); setStatus("Credits 额度已用完,请充值共享加油包或等待下个计费周期。", true);
} }

View File

@@ -31,6 +31,7 @@
<div class="workspace"> <div class="workspace">
<header class="topbar topbar-compact"> <header class="topbar topbar-compact">
<div class="topbar-actions"> <div class="topbar-actions">
<span class="mode-badge global-mode-hint">当前模型模式:自由模型</span>
<a class="icon-btn" href="/" aria-label="返回工作台" title="返回工作台"></a> <a class="icon-btn" href="/" aria-label="返回工作台" title="返回工作台"></a>
<a class="icon-btn" href="/settings" aria-label="账号与模型设置" title="账号与模型设置"></a> <a class="icon-btn" href="/settings" aria-label="账号与模型设置" title="账号与模型设置"></a>
<a class="icon-btn" href="/upgrade" aria-label="升级" title="升级"></a> <a class="icon-btn" href="/upgrade" aria-label="升级" title="升级"></a>
@@ -95,6 +96,7 @@
</div> </div>
</div> </div>
<script src="/static/ui-dialog.js?v=20260428a"></script> <script src="/static/ui-dialog.js?v=20260428a"></script>
<script src="/static/mode-hint.js?v=20260428a"></script>
<script src="/static/billing.js?v=20260428w"></script> <script src="/static/billing.js?v=20260428w"></script>
</body> </body>
</html> </html>

View File

@@ -31,6 +31,7 @@
<div class="workspace"> <div class="workspace">
<header class="topbar topbar-compact"> <header class="topbar topbar-compact">
<div class="topbar-actions"> <div class="topbar-actions">
<span class="mode-badge global-mode-hint">当前模型模式:自由模型</span>
<a class="icon-btn" href="/" aria-label="返回工作台" title="返回工作台"></a> <a class="icon-btn" href="/" aria-label="返回工作台" title="返回工作台"></a>
<a class="icon-btn" href="/upgrade" aria-label="升级" title="升级"></a> <a class="icon-btn" href="/upgrade" aria-label="升级" title="升级"></a>
<a class="icon-btn" href="/profile" aria-label="个人中心" title="个人中心"></a> <a class="icon-btn" href="/profile" aria-label="个人中心" title="个人中心"></a>
@@ -58,39 +59,50 @@
<div class="guide-step">01</div> <div class="guide-step">01</div>
<h3>准备发布账号</h3> <h3>准备发布账号</h3>
<p>进入账号与模型设置,绑定公众号 AppID 和 Secret。草稿发布、封面上传、段落海报素材都会使用当前选中的发表主体。</p> <p>进入账号与模型设置,绑定公众号 AppID 和 Secret。草稿发布、封面上传、段落海报素材都会使用当前选中的发表主体。</p>
<p class="muted small">
不知道哪里查看 ID可先阅读微信文档
<a href="https://developers.weixin.qq.com/doc/oplatform/developers/product/subscription_service/appid.html" target="_blank" rel="noopener noreferrer">查看 AppID / AppSecret 获取说明</a>
</p>
<a href="/settings" class="guide-link">打开账号设置</a> <a href="/settings" class="guide-link">打开账号设置</a>
</article> </article>
<article class="guide-card"> <article class="guide-card">
<div class="guide-step">02</div> <div class="guide-step">02</div>
<h3>配置 API IP 白名单</h3>
<p>若第三方模型平台开启了 API IP 白名单,请先把当前服务器 IP 加入白名单,避免接口请求被拒绝。</p>
<button id="getServerIpBtn" class="guide-link" type="button">获取服务器IP</button>
</article>
<article class="guide-card">
<div class="guide-step">03</div>
<h3>配置 AI 模型</h3> <h3>配置 AI 模型</h3>
<p>保存模型名称、API Key、Base URL、超时秒数和输出 token 上限。未配置模型时将无法进行 AI 改写,请先完成模型配置。</p> <p>保存模型名称、API Key、Base URL、超时秒数和输出 token 上限。未配置模型时将无法进行 AI 改写,请先完成模型配置。</p>
<a href="/settings#model-settings" class="guide-link">打开模型配置</a> <a href="/settings#model-settings" class="guide-link">打开模型配置</a>
</article> </article>
<article class="guide-card"> <article class="guide-card">
<div class="guide-step">03</div> <div class="guide-step">04</div>
<h3>输入原文与策略</h3> <h3>输入原文与策略</h3>
<p>在内容生产页粘贴原文,补充标题提示、目标读者、语气风格、必须保留观点和避免词汇。目标字数建议先从 500 或 800 开始。</p> <p>在内容生产页粘贴原文,补充标题提示、目标读者、语气风格、必须保留观点和避免词汇。目标字数建议先从 500 或 800 开始。</p>
<a href="/" class="guide-link">进入写作输入</a> <a href="/" class="guide-link">进入写作输入</a>
</article> </article>
<article class="guide-card"> <article class="guide-card">
<div class="guide-step">04</div> <div class="guide-step">05</div>
<h3>生成并人工复核</h3> <h3>生成并人工复核</h3>
<p>点击“改写并排版”后,检查标题、摘要、正文结构和排版预览。涉及事实、数据、引用和品牌表达时,发布前务必人工确认。</p> <p>点击“改写并排版”后,检查标题、摘要、正文结构和排版预览。涉及事实、数据、引用和品牌表达时,发布前务必人工确认。</p>
<a href="/" class="guide-link">查看发布内容</a> <a href="/" class="guide-link">查看发布内容</a>
</article> </article>
<article class="guide-card"> <article class="guide-card">
<div class="guide-step">05</div> <div class="guide-step">06</div>
<h3>补齐封面和海报</h3> <h3>补齐封面和海报</h3>
<p>可按输出标题自动生成 900×383 公众号封面并绑定 thumb_media_id也可以生成段落海报。勾选自动插入后发布草稿时会把正文和海报一起编排。</p> <p>可按输出标题自动生成 900×383 公众号封面并绑定 thumb_media_id也可以生成段落海报。勾选自动插入后发布草稿时会把正文和海报一起编排。</p>
<a href="/" class="guide-link">处理内容素材</a> <a href="/" class="guide-link">处理内容素材</a>
</article> </article>
<article class="guide-card"> <article class="guide-card">
<div class="guide-step">06</div> <div class="guide-step">07</div>
<h3>发布到草稿箱</h3> <h3>发布到草稿箱</h3>
<p>确认发表主体无误后,点击“发布到公众号草稿箱”。需要团队同步时,再点击“发送到 IM”。草稿发布后仍建议在公众号后台最终预览。</p> <p>确认发表主体无误后,点击“发布到公众号草稿箱”。需要团队同步时,再点击“发送到 IM”。草稿发布后仍建议在公众号后台最终预览。</p>
<a href="/" class="guide-link">回到发布动作</a> <a href="/" class="guide-link">回到发布动作</a>
@@ -135,5 +147,8 @@
</main> </main>
</div> </div>
</div> </div>
<script src="/static/ui-dialog.js?v=20260428a"></script>
<script src="/static/mode-hint.js?v=20260428a"></script>
<script src="/static/guide.js?v=20260428a"></script>
</body> </body>
</html> </html>

View File

@@ -31,6 +31,7 @@
<div class="workspace"> <div class="workspace">
<header class="topbar topbar-compact"> <header class="topbar topbar-compact">
<div class="topbar-actions"> <div class="topbar-actions">
<span class="mode-badge global-mode-hint">当前模型模式:自由模型</span>
<div class="wechat-account-switch" title="发布将使用当前账号"> <div class="wechat-account-switch" title="发布将使用当前账号">
<label for="wechatAccountSelect" class="wechat-account-label">发表主体</label> <label for="wechatAccountSelect" class="wechat-account-label">发表主体</label>
<select id="wechatAccountSelect" class="topbar-select" aria-label="切换公众号"></select> <select id="wechatAccountSelect" class="topbar-select" aria-label="切换公众号"></select>
@@ -248,6 +249,7 @@
</div> </div>
</div> </div>
<script src="/static/mode-hint.js?v=20260428a"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="/static/app.js?v=20260428s"></script> <script src="/static/app.js?v=20260428s"></script>
</body> </body>

View File

@@ -31,6 +31,7 @@
<div class="workspace"> <div class="workspace">
<header class="topbar topbar-compact"> <header class="topbar topbar-compact">
<div class="topbar-actions"> <div class="topbar-actions">
<span class="mode-badge global-mode-hint">当前模型模式:自由模型</span>
<a class="icon-btn" href="/" aria-label="返回工作台" title="返回工作台"></a> <a class="icon-btn" href="/" aria-label="返回工作台" title="返回工作台"></a>
<a class="icon-btn" href="/upgrade" aria-label="升级" title="升级"></a> <a class="icon-btn" href="/upgrade" aria-label="升级" title="升级"></a>
<a class="icon-btn" href="/settings" aria-label="账号与模型设置" title="账号与模型设置"></a> <a class="icon-btn" href="/settings" aria-label="账号与模型设置" title="账号与模型设置"></a>
@@ -70,6 +71,7 @@
</main> </main>
</div> </div>
</div> </div>
<script src="/static/mode-hint.js?v=20260428a"></script>
<script src="/static/profile.js?v=20260428a"></script> <script src="/static/profile.js?v=20260428a"></script>
</body> </body>
</html> </html>

View File

@@ -31,6 +31,7 @@
<div class="workspace"> <div class="workspace">
<header class="topbar topbar-compact"> <header class="topbar topbar-compact">
<div class="topbar-actions"> <div class="topbar-actions">
<span class="mode-badge global-mode-hint">当前模型模式:自由模型</span>
<a class="icon-btn" href="/" aria-label="返回工作台" title="返回工作台"></a> <a class="icon-btn" href="/" aria-label="返回工作台" title="返回工作台"></a>
<a class="icon-btn" href="/upgrade" aria-label="升级" title="升级"></a> <a class="icon-btn" href="/upgrade" aria-label="升级" title="升级"></a>
<a class="icon-btn" href="/profile" aria-label="个人中心" title="个人中心"></a> <a class="icon-btn" href="/profile" aria-label="个人中心" title="个人中心"></a>
@@ -91,6 +92,10 @@
<input id="modelValue" type="text" placeholder="如gpt-4.1-mini / qwen-max" /> <input id="modelValue" type="text" placeholder="如gpt-4.1-mini / qwen-max" />
</div> </div>
</div> </div>
<div>
<label>生图模型名</label>
<input id="imageModelValue" type="text" placeholder="如wanx2.0-t2i-turbo / gpt-image-1" />
</div>
<div class="grid2"> <div class="grid2">
<div> <div>
<label>Base URL可选</label> <label>Base URL可选</label>
@@ -154,6 +159,7 @@
</div> </div>
</div> </div>
<script src="/static/ui-dialog.js?v=20260428a"></script> <script src="/static/ui-dialog.js?v=20260428a"></script>
<script src="/static/settings.js?v=20260428q"></script> <script src="/static/mode-hint.js?v=20260428a"></script>
<script src="/static/settings.js?v=20260428r"></script>
</body> </body>
</html> </html>

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ app_name }} - 升级</title> <title>{{ app_name }} - 升级</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=20260428h" /> <link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=20260428h" />
<link rel="stylesheet" href="/static/style.css?v=20260428za" /> <link rel="stylesheet" href="/static/style.css?v=20260428zd" />
</head> </head>
<body> <body>
<div class="product-shell"> <div class="product-shell">
@@ -29,7 +29,11 @@
</aside> </aside>
<div class="workspace"> <div class="workspace">
<header class="topbar topbar-compact"> <header class="topbar topbar-compact upgrade-topbar">
<div class="topbar-spacer" aria-hidden="true"></div>
<div class="topbar-center">
<span id="vipModeHint" class="mode-badge global-mode-hint">当前模型模式:自由模型</span>
</div>
<div class="topbar-actions"> <div class="topbar-actions">
<a class="icon-btn" href="/" aria-label="返回工作台" title="返回工作台"></a> <a class="icon-btn" href="/" aria-label="返回工作台" title="返回工作台"></a>
<a class="icon-btn" href="/settings" aria-label="账号与模型设置" title="账号与模型设置"></a> <a class="icon-btn" href="/settings" aria-label="账号与模型设置" title="账号与模型设置"></a>
@@ -172,6 +176,7 @@
</div> </div>
</div> </div>
<script src="/static/ui-dialog.js?v=20260428a"></script> <script src="/static/ui-dialog.js?v=20260428a"></script>
<script src="/static/upgrade.js?v=20260428ae"></script> <script src="/static/mode-hint.js?v=20260428a"></script>
<script src="/static/upgrade.js?v=20260428ag"></script>
</body> </body>
</html> </html>