Compare commits
2 Commits
a88cfebf8a
...
1dcb4816d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dcb4816d8 | |||
| a610c22f45 |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 877 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.0 MiB |
@@ -327,6 +327,16 @@ export const authApi = {
|
||||
// ---- 计量:余额 / 本月用量(按模型) ----
|
||||
getOpcComputeBalance: () => request<{ id: number; quota: number; used_quota: number }>("/opc/compute/balance"),
|
||||
getOpcComputeUsage: () => request<{ month: string; cost_quota: number; input_tokens: number; output_tokens: number; items: { model_name: string; requests: number; input_tokens: number; output_tokens: number; cost_quota: number }[] }>("/opc/compute/usage"),
|
||||
// ---- 算力充值(微信支付,native 扫码) ----
|
||||
getOpcRechargePackages: () => request<{ enabled: boolean; items: { id: string; amount: number; bonus: number; label: string; quota_micro: number }[]; min_yuan: number; max_yuan: number }>("/opc/compute/recharge/packages"),
|
||||
// client_type="mp":桌面端展示小程序码,微信扫码在小程序内完成支付(与扫码登录同构)
|
||||
createOpcRechargeOrder: (body: { client_type: "mp" | "native" | "jsapi"; package_id?: string; amount_yuan?: number }) =>
|
||||
request<{ order_no: string; amount_fen: number; quota_micro: number; bonus_quota_micro: number; status: string; code_url?: string; qr_image?: string; pay_params?: Record<string, string>; expires_at: string }>("/opc/compute/recharge/orders", { method: "POST", body: JSON.stringify(body) }),
|
||||
getOpcRechargePayParams: (orderNo: string) =>
|
||||
request<{ order: { order_no: string; amount_fen: number; quota_micro: number; status: string }; pay_params: Record<string, string> | null; finished: boolean }>(`/opc/compute/recharge/orders/${orderNo}/pay-params`),
|
||||
getOpcRechargeStatus: (orderNo: string) =>
|
||||
request<{ order_no: string; status: string; paid: boolean; credited: boolean; amount_fen: number; quota_micro: number; transaction_id: string; expires_at: string }>(`/opc/compute/recharge/orders/${orderNo}/status`, { method: "POST", body: "{}" }),
|
||||
getOpcRechargeOrders: () => request<{ items: { order_no: string; amount_fen: number; quota_micro: number; status: string; created_at: string; paid_at: string }[] }>("/opc/compute/recharge/orders"),
|
||||
|
||||
// ---- 各业务端口工作台 ----
|
||||
getEnterpriseDashboard: () => request<PortDashboardData>("/enterprise/dashboard"),
|
||||
|
||||
@@ -364,7 +364,7 @@ export default function Header() {
|
||||
*/}
|
||||
<Slot name="header.logo" kind="replace">
|
||||
<img
|
||||
src="/logo.png"
|
||||
src="/logo-light.svg"
|
||||
alt="PineAgents"
|
||||
className={styles.logoImg}
|
||||
/>
|
||||
|
||||
@@ -3758,6 +3758,17 @@
|
||||
"computeRequests": "请求",
|
||||
"computeCost": "费用",
|
||||
"computeNoUsage": "本月暂无用量,使用模型后显示",
|
||||
"computeRecharge": "充值",
|
||||
"computeRechargeTitle": "算力充值(微信扫码 · 小程序支付)",
|
||||
"computeRechargePackages": "充值套餐",
|
||||
"computeRechargeCustom": "自定义金额(元)",
|
||||
"computeRechargePay": "生成小程序支付码",
|
||||
"computeRechargeScan": "请使用微信扫码,在小程序中确认并完成支付,余额自动到账",
|
||||
"computeRechargeSuccess": "充值成功,算力已到账",
|
||||
"computeRechargeFailed": "下单失败",
|
||||
"computeRechargeDisabled": "支付暂未配置,请联系运营",
|
||||
"computeRechargeDone": "我已完成支付",
|
||||
"computeRechargeBonus": "送 {{bonus}} 元",
|
||||
"profile": "个人资料",
|
||||
"username": "用户名",
|
||||
"nickname": "昵称",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Button, Card, Col, List, Row, Space, Table, Tag, Typography, message } from "antd";
|
||||
import { authApi } from "../../../api/modules/auth";
|
||||
import { RequireRole } from "../../../auth/guards";
|
||||
import RechargeModal from "./RechargeModal";
|
||||
import styles from "./OpcList.module.less";
|
||||
|
||||
interface Token { id: number; name: string; key: string; created_time: number }
|
||||
@@ -19,6 +20,7 @@ export default function OpcCompute() {
|
||||
const [balance, setBalance] = useState<{ quota: number; used_quota: number } | null>(null);
|
||||
const [usage, setUsage] = useState<{ month: string; cost_quota: number; input_tokens: number; output_tokens: number; items: any[] } | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [rechargeOpen, setRechargeOpen] = useState(false);
|
||||
|
||||
const load = useCallback(() => {
|
||||
authApi.getOpcComputeBase().then(setBase).catch(() => {});
|
||||
@@ -86,7 +88,8 @@ export default function OpcCompute() {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title={t("role.opc.computeUsage")} style={{ marginTop: 12 }} size="small">
|
||||
<Card title={t("role.opc.computeUsage")} style={{ marginTop: 12 }} size="small"
|
||||
extra={<Button type="primary" size="small" onClick={() => setRechargeOpen(true)}>{t("role.opc.computeRecharge")}</Button>}>
|
||||
<Space size={24} wrap>
|
||||
<div><Typography.Text type="secondary">{t("role.opc.computeBalance")}</Typography.Text>
|
||||
<div className={styles.itemTitle}>¥{((balance?.quota ?? 0) / 1000000).toFixed(4)}</div></div>
|
||||
@@ -131,6 +134,8 @@ export default function OpcCompute() {
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<RechargeModal open={rechargeOpen} onClose={() => setRechargeOpen(false)} onPaid={load} />
|
||||
</div>
|
||||
</RequireRole>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// 算力充值对话框:套餐/自定义金额 → 展示小程序码 → 微信扫码在小程序内完成支付 → 轮询到账
|
||||
// (与「小程序扫码登录」同构:桌面出码,支付能力统一由小程序 wx.requestPayment 承载)
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Image, InputNumber, Modal, Radio, Space, Tag, Typography, message } from "antd";
|
||||
import { authApi } from "../../../api/modules/auth";
|
||||
|
||||
interface Pkg { id: string; amount: number; bonus: number; label: string; quota_micro: number }
|
||||
interface Packages { enabled: boolean; items: Pkg[]; min_yuan: number; max_yuan: number }
|
||||
|
||||
const MICRO = 1_000_000;
|
||||
|
||||
export default function RechargeModal({ open, onClose, onPaid }: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onPaid: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [pkgs, setPkgs] = useState<Packages | null>(null);
|
||||
const [pkgId, setPkgId] = useState<string>("");
|
||||
const [custom, setCustom] = useState<number | null>(null);
|
||||
const [order, setOrder] = useState<{ order_no: string; qr_image: string; quota_micro: number } | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const timer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const loadPkgs = () => {
|
||||
authApi.getOpcRechargePackages()
|
||||
.then((r) => { setPkgs(r); setPkgId(r.items?.[0]?.id ?? ""); })
|
||||
.catch(() => setPkgs({ enabled: false, items: [], min_yuan: 1, max_yuan: 5000 }));
|
||||
};
|
||||
|
||||
const stopPoll = () => { if (timer.current) { clearInterval(timer.current); timer.current = null; } };
|
||||
|
||||
const poll = (orderNo: string) => {
|
||||
stopPoll();
|
||||
timer.current = setInterval(async () => {
|
||||
try {
|
||||
const st = await authApi.getOpcRechargeStatus(orderNo);
|
||||
if (st.paid) {
|
||||
stopPoll();
|
||||
message.success(t("role.opc.computeRechargeSuccess"));
|
||||
setOrder(null);
|
||||
onPaid();
|
||||
onClose();
|
||||
}
|
||||
} catch { /* 忽略瞬时错误,下一轮重试 */ }
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) { loadPkgs(); } else { stopPoll(); setOrder(null); setCustom(null); }
|
||||
return stopPoll;
|
||||
}, [open]);
|
||||
|
||||
const submit = async () => {
|
||||
if (!pkgId && !(custom && custom > 0)) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const body = pkgId
|
||||
? { client_type: "mp" as const, package_id: pkgId }
|
||||
: { client_type: "mp" as const, amount_yuan: custom ?? 0 };
|
||||
const res = await authApi.createOpcRechargeOrder(body);
|
||||
if (!res.qr_image) throw new Error("小程序码生成失败,请稍后重试");
|
||||
setOrder(res);
|
||||
poll(res.order_no);
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : t("role.opc.computeRechargeFailed"));
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const min = pkgs?.min_yuan ?? 1;
|
||||
const max = pkgs?.max_yuan ?? 5000;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("role.opc.computeRechargeTitle")}
|
||||
open={open}
|
||||
onCancel={() => { stopPoll(); setOrder(null); onClose(); }}
|
||||
footer={order ? [
|
||||
<Button key="done" onClick={() => { stopPoll(); setOrder(null); loadPkgs(); }}>{t("role.opc.computeRechargeDone")}</Button>,
|
||||
] : [
|
||||
<Button key="cancel" onClick={() => onClose()}>{t("common.cancel", "取消")}</Button>,
|
||||
<Button key="pay" type="primary" loading={busy} disabled={!pkgs?.enabled || (!pkgId && !(custom && custom > 0))}
|
||||
onClick={submit}>{t("role.opc.computeRechargePay")}</Button>,
|
||||
]}
|
||||
>
|
||||
{pkgs && !pkgs.enabled && (
|
||||
<Typography.Paragraph type="warning">{t("role.opc.computeRechargeDisabled")}</Typography.Paragraph>
|
||||
)}
|
||||
{!order ? (
|
||||
<>
|
||||
{pkgs?.items?.length ? (
|
||||
<>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>{t("role.opc.computeRechargePackages")}</Typography.Paragraph>
|
||||
<Radio.Group value={pkgId} onChange={(e) => { setPkgId(e.target.value); setCustom(null); }} style={{ width: "100%" }}>
|
||||
<Space wrap>
|
||||
{pkgs.items.map((p) => (
|
||||
<Radio.Button key={p.id} value={p.id}>
|
||||
¥{p.amount}{p.bonus > 0 ? <Tag color="orange" style={{ marginLeft: 6 }}>{t("role.opc.computeRechargeBonus", { bonus: p.bonus })}</Tag> : null}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</>
|
||||
) : null}
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>{t("role.opc.computeRechargeCustom")}({min} ~ {max})</Typography.Paragraph>
|
||||
<InputNumber min={min} max={max} precision={0} value={custom}
|
||||
onChange={(v) => { setCustom(v); setPkgId(""); }} style={{ width: 200 }} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Image src={order.qr_image} width={220} preview={false} />
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: 12 }}>{t("role.opc.computeRechargeScan")}</Typography.Paragraph>
|
||||
<Typography.Text code>{order.order_no}</Typography.Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Typography.Text strong>¥{(order.quota_micro / MICRO).toFixed(2)}</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -291,6 +291,16 @@ async def opc_compute_prices(request: Request):
|
||||
return await forward("GET", "/opc/compute/prices", auth_header=_auth(request))
|
||||
|
||||
|
||||
@router.get("/opc/compute/balance")
|
||||
async def opc_compute_balance(request: Request):
|
||||
return await forward("GET", "/opc/compute/balance", auth_header=_auth(request))
|
||||
|
||||
|
||||
@router.get("/opc/compute/usage")
|
||||
async def opc_compute_usage(request: Request):
|
||||
return await forward("GET", "/opc/compute/usage", auth_header=_auth(request))
|
||||
|
||||
|
||||
@router.get("/opc/compute/tokens")
|
||||
async def opc_compute_tokens(request: Request):
|
||||
return await forward("GET", "/opc/compute/tokens", auth_header=_auth(request))
|
||||
@@ -306,6 +316,34 @@ async def opc_compute_tokens_delete(token_id: int, request: Request):
|
||||
return await forward("DELETE", f"/opc/compute/tokens/{token_id}", auth_header=_auth(request))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 算力充值(转发 server-core /opc/compute/recharge;微信回调不经本地后端,直连公网)
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/opc/compute/recharge/packages")
|
||||
async def opc_compute_recharge_packages(request: Request):
|
||||
return await forward("GET", "/opc/compute/recharge/packages", auth_header=_auth(request))
|
||||
|
||||
|
||||
@router.post("/opc/compute/recharge/orders")
|
||||
async def opc_compute_recharge_create(req: dict, request: Request):
|
||||
return await forward("POST", "/opc/compute/recharge/orders", body=req, auth_header=_auth(request))
|
||||
|
||||
|
||||
@router.get("/opc/compute/recharge/orders/{order_no}/pay-params")
|
||||
async def opc_compute_recharge_pay_params(order_no: str, request: Request):
|
||||
return await forward("GET", f"/opc/compute/recharge/orders/{order_no}/pay-params", auth_header=_auth(request))
|
||||
|
||||
|
||||
@router.post("/opc/compute/recharge/orders/{order_no}/status")
|
||||
async def opc_compute_recharge_status(order_no: str, request: Request):
|
||||
return await forward("POST", f"/opc/compute/recharge/orders/{order_no}/status", body={}, auth_header=_auth(request))
|
||||
|
||||
|
||||
@router.get("/opc/compute/recharge/orders")
|
||||
async def opc_compute_recharge_orders(request: Request):
|
||||
return await forward("GET", "/opc/compute/recharge/orders", auth_header=_auth(request))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 各业务端口工作台
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -164,10 +164,10 @@ SECRET_DIR = (
|
||||
# Env key for overriding the OS keychain account used for the master key.
|
||||
KEYRING_ACCOUNT_ENV = "QWENPAW_KEYRING_ACCOUNT"
|
||||
|
||||
# 演示 FastAPI 服务地址:登录/演示接口由 pineagents-demo 提供,本项目后端转发过去。
|
||||
# 后端服务地址
|
||||
DEMO_API_BASE_URL = EnvVarLoader.get_str(
|
||||
"PINEAGENTS_DEMO_BASE_URL",
|
||||
"http://192.168.1.9:8090",
|
||||
"https://opc.pinesound.cn",
|
||||
)
|
||||
|
||||
PROJECT_NAME = BRAND.project_name
|
||||
|
||||
@@ -1400,6 +1400,9 @@ class ProviderManager: # pylint: disable=too-many-public-methods
|
||||
logger.warning("Failed to migrate legacy providers: %s", e)
|
||||
self._init_from_storage()
|
||||
self._apply_default_annotations()
|
||||
# PineAgents(算力中心)中继按登录用户归户计量:注入本地持久化的服务端 JWT
|
||||
# (置于 _init_from_storage 之后,避免被持久化配置覆盖)。
|
||||
self._refresh_pineagents_auth()
|
||||
|
||||
def _prepare_disk_storage(self):
|
||||
"""Prepare directory structure"""
|
||||
@@ -1454,7 +1457,31 @@ class ProviderManager: # pylint: disable=too-many-public-methods
|
||||
def _add_builtin(self, provider: Provider):
|
||||
self.builtin_providers[provider.id] = provider
|
||||
|
||||
def _refresh_pineagents_auth(self) -> None:
|
||||
"""把本地持久化的服务端 JWT 注入 PineAgents provider 请求头。
|
||||
|
||||
server-core /v1 中继据 JWT 归户到该用户的算力令牌并按其余额计量/拦截;
|
||||
未登录时移除该头(引擎侧拒绝匿名调用)。
|
||||
"""
|
||||
pine = self.builtin_providers.get("pineagents")
|
||||
if pine is None:
|
||||
return
|
||||
try:
|
||||
from ..app.auth_token_store import load_auth_token
|
||||
|
||||
token = load_auth_token()
|
||||
except Exception: # noqa: BLE001
|
||||
token = ""
|
||||
headers = dict(pine.custom_headers or {})
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
else:
|
||||
headers.pop("Authorization", None)
|
||||
pine.custom_headers = headers
|
||||
|
||||
async def list_provider_info(self) -> List[ProviderInfo]:
|
||||
# 登录态可能变化(登录/换发/登出),先刷新 PineAgents 鉴权头再拉模型。
|
||||
self._refresh_pineagents_auth()
|
||||
# PineAgents(算力中心):动态拉取 admin 模型(server-core /v1/models),失败回退静态 DEEPSEEK_MODELS。
|
||||
pine = self.builtin_providers.get("pineagents")
|
||||
if pine is not None and hasattr(pine, "fetch_models"):
|
||||
|
||||
Reference in New Issue
Block a user