Author SHA1 Message Date
admin 7c286f639e admin-ui ng转发域名增加 2026-09-11 00:13:41 +08:00
admin 56131a4662 Merge branch 'feature/home-workflow' of https://gitea.redpowerfuture.com/red-future/admin-ui into feature/home-workflow 2026-09-07 10:45:37 +08:00
admin 7209cd3ba2 admin-ui ng转发域名增加 2026-09-07 10:42:49 +08:00
2910410219andClaude Code a5262941ea 价格管理:媒体价格模板三个输入框增加上方 label
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 17:20:16 +08:00
2910410219andClaude Code 490abd60e9 价格管理:label 中文化、金额保留5位小数、通用配置置顶重排、取消模型类型筛选、去除列表计费方式标签
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 17:15:55 +08:00
2910410219andClaude Code aa7c8856fb 新增:首页工作流失败卡片提供「查看失败原因」按钮(默认收起、点击展开,绑定 error 字段);实时与历史回显均展示详细失败原因
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 15:39:21 +08:00
2910410219andClaude Code 3f9b5e59a8 新增:首页工作流执行必选计费方式(重跑回显上次选择);修复:退出登录整页刷新解决重新登录后动态菜单404
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 15:09:24 +08:00
2910410219andClaude Code fbe2bcfee6 新增:模型计价编辑器适配新计费结构(mediaPrices媒体价格模板、thinking/outputAudio/outputResolution中文展示与下拉)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 15:09:15 +08:00
17 changed files with 668 additions and 407 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# ==================== 第一阶段:构建前端 ====================
FROM docker.m.daocloud.io/node:18-alpine AS builder
FROM node:18-alpine AS builder
WORKDIR /app
# 配置Alpine国内镜像源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
@@ -13,7 +13,7 @@ COPY . .
RUN npm run build
# ==================== 第二阶段:部署到Nginx ====================
FROM docker.m.daocloud.io/nginx:alpine
FROM nginx:alpine
# 复制构建产物
COPY --from=builder /app/dist/ /usr/share/nginx/html/
+2
View File
@@ -24,6 +24,8 @@ export interface VOSessionInfoResult {
totalTokens: number;
totalFee: number;
errorMsg: string;
// 失败详细原因(工作流节点失败详情,如「执行工作流失败: ...节点:生成视频, 失败原因:...」),比 errorMsg 更具体
error?: string;
createdAt: string;
}
+9
View File
@@ -17,6 +17,15 @@ export function getPricingConfig(params: { subjectType: string; subjectId: strin
});
}
/** 计价对象可选计费方式(workflow/business 使用;model 计价方式由配置规则决定,不调用) */
export function getChargeModes(params: { subjectType: string; subjectId: string }) {
return request({
url: '/shop-user-trade/pricing/controller/subjects/charge-modes',
method: 'get',
params,
});
}
/** 新增/更新计价配置(id > 0 更新,0 新增;rules 传费率对象;id 兼容雪花字符串避免精度丢失) */
export function savePricingConfig(data: {
id: string | number;
+4 -3
View File
@@ -203,11 +203,12 @@ export default defineComponent({
}
},
})
.then(async () => {
.then(() => {
// 手动退出登录也只清理登录态缓存,保留主题、语言等本地配置。
Session.clearAuth();
// 显式回到登录页,避免保留之前受保护页面的重定向参数
await router.replace('/login');
// 整页跳转登录页(刷新重建路由实例):SPA 内 router.replace 不会重置已注册的动态路由,
// 重新登录时 setAddRoute 的 hasRoute 判断会跳过新菜单注册,出现"菜单显示但点击 404"(如价格管理页)
window.location.href = import.meta.env.BASE_URL + '#/login';
})
.catch(() => {});
} else if (path === 'wareHouse') {
+89 -7
View File
@@ -33,6 +33,12 @@
<div v-show="!collapsed" class="workflow-form-scroll">
<el-form label-position="top" class="workflow-form">
<!-- 计费方式必选不设置默认值可选项来自计价对象 charge-modes 接口workflow -->
<el-form-item label="计费方式" required>
<el-select v-model="chargeMode" :disabled="isDisabled" placeholder="请选择计费方式" class="w100">
<el-option v-for="opt in chargeModes" :key="opt.mode" :label="opt.name" :value="opt.mode" />
</el-select>
</el-form-item>
<template v-if="detail?.nodeInputParams">
<template v-for="node in detail.nodeInputParams" :key="node.id || node.nodeCode">
<template v-if="hasFormConfig(node)">
@@ -165,19 +171,26 @@
<!-- 底部操作区:editing 提示 / running 执行中 / done·failed 状态标签(执行统一由 InputBar 发送按钮触发) -->
<div class="workflow-form-footer">
<div v-if="isFailed" class="form-error-text">{{ formError }}</div>
<!-- 执行中:节点文本进度推进(后端 node_start/node_complete 事件),无进度时兜底「执行中...」;
停止统一由 InputBar 发送按钮(变停止)触发,卡片内不再提供取消入口 -->
<div v-if="submitting" class="executing-progress">
<span class="exec-spinner" />
<span class="exec-text">{{ progressText }}</span>
</div>
<!-- 已执行卡片(done/failed,含可编辑的最后一张):显示状态标签,参数是否可编辑由 editable 决定 -->
<el-tag v-else-if="formStatus === 'done' || formStatus === 'failed'" :type="isFailed ? 'danger' : 'success'" size="small" effect="light">
{{ isFailed ? '执行失败' : '执行完成' }}
</el-tag>
<!-- 已执行卡片(done/failed,含可编辑的最后一张):状态标签 + 失败原因按钮(默认收起,点击手动展开) -->
<div v-else-if="formStatus === 'done' || formStatus === 'failed'" class="form-status-row">
<el-tag :type="isFailed ? 'danger' : 'success'" size="small" effect="light">
{{ isFailed ? '执行失败' : '执行完成' }}
</el-tag>
<el-button v-if="isFailed && formError" text size="small" class="form-error-toggle" @click="showError = !showError">
{{ showError ? '收起失败原因' : '查看失败原因' }}
<el-icon class="error-arrow" :class="{ 'is-expanded': showError }"><ArrowDown /></el-icon>
</el-button>
</div>
<!-- 编辑态:无执行按钮,提示通过下方发送按钮执行 -->
<span v-else class="form-edit-tip">填写参数后,点击下方发送按钮执行工作流</span>
<!-- 失败原因详情:默认收起,点「查看失败原因」展开(error 字段可能含多行详细原因) -->
<div v-if="isFailed && showError && formError" class="form-error-text">{{ formError }}</div>
</div>
</div>
</template>
@@ -188,6 +201,7 @@ import { ElMessage } from 'element-plus';
import { ArrowDown, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue';
import PatchTemplateEditor from '/@/components/patchTemplate/PatchTemplateEditor.vue';
import { uploadFile } from '/@/api/common/upload';
import { getChargeModes } from '/@/api/trade/pricing';
import { collectHomeFormFields } from '../utils/flowDsl';
import type { WorkflowNodeStep } from '../utils/wsMessage';
@@ -209,7 +223,7 @@ interface Props {
}
interface Emits {
(e: 'submit', payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates: any[] }): void;
(e: 'submit', payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates: any[]; chargeMode: string }): void;
}
const props = withDefaults(defineProps<Props>(), {
@@ -229,11 +243,47 @@ const formFileNames = reactive<Record<string, string | string[]>>({});
const uploadingFields = reactive<Record<string, boolean>>({});
const templates = ref<any[]>([]);
// ===== 计费方式:必选、不设置默认值(新建为空强制选择;回显重跑恢复上次选择) =====
interface ChargeModeOption {
mode: string;
name: string;
}
const chargeMode = ref('');
const chargeModes = ref<ChargeModeOption[]>([]);
/** 计费方式可选项缓存:workflow 计价对象固定(subjectType/subjectId 均 workflow),模块级共享避免每张卡片重复请求 */
let workflowChargeModesCache: ChargeModeOption[] | null = null;
const loadChargeModes = async () => {
if (workflowChargeModesCache) {
chargeModes.value = workflowChargeModesCache;
return;
}
try {
const res: any = await getChargeModes({ subjectType: 'workflow', subjectId: 'workflow' });
const list: ChargeModeOption[] = res?.data?.chargeModes || [];
chargeModes.value = list;
workflowChargeModesCache = list;
} catch {
// 计费方式拉取失败:全局拦截器已提示,下拉留空(必填校验会拦截提交)
chargeModes.value = [];
}
};
// 状态派生:失败 = 携带 formError;禁用编辑 = 定格(done/failed)卡片默认只读,
// 但最后一张已执行卡片(editable)允许改参;执行中(submitting)一律禁用
const isFailed = computed(() => !!props.formError);
const isDisabled = computed(() => (props.readonly && !props.editable) || props.submitting);
// 失败原因默认收起:新失败时重置为不展示,用户点「查看失败原因」才展开
const showError = ref(false);
watch(
() => props.formError,
() => {
showError.value = false;
}
);
// 表单收起/展开:editing(待提交/回显)默认展开;提交(running)自动收起;
// done/failed 保持提交时状态(默认收起,用户可手动展开查看已填值)
const collapsed = ref(false);
@@ -435,6 +485,11 @@ const hasFormFields = computed(() => {
});
const validateFormFields = (): boolean => {
// 计费方式必选、无默认值:未选择时阻止提交(含无表单参数的工作流)
if (!chargeMode.value) {
ElMessage.warning('请选择计费方式');
return false;
}
if (!props.detail?.nodeInputParams) return true;
for (const node of props.detail.nodeInputParams as any[]) {
if (String(node?.nodeCode || '').toLowerCase() !== '__start__') continue;
@@ -462,6 +517,10 @@ watch(
Object.keys(fieldFiles).forEach((key) => delete fieldFiles[key]);
Object.keys(formFileNames).forEach((key) => delete formFileNames[key]);
Object.keys(uploadingFields).forEach((key) => delete uploadingFields[key]);
// 计费方式:新建(handleWorkflowSelect 已清空后端自带 chargeMode)为空强制选择;
// 回显重跑卡片从 flowContent.chargeMode 恢复上次选择
chargeMode.value = detail?.flowContent?.chargeMode || '';
loadChargeModes();
// 尝试从执行详情恢复贴片模板
// 提交路径:templates 写入开启贴片布局的节点内部(patchLayout === true 的 node.templates),顶层已删除;
// 回显时 detail.nodeInputParams 即提交时的 nodes,从该处优先恢复;兼容旧数据(extension / detail / flowContent 顶层)
@@ -543,6 +602,7 @@ const handleSubmit = () => {
formValues: JSON.parse(JSON.stringify(formValues)),
formFileNames: JSON.parse(JSON.stringify(formFileNames)),
templates: JSON.parse(JSON.stringify(templates.value || [])),
chargeMode: chargeMode.value,
});
};
@@ -754,19 +814,41 @@ watch(
/* 底部操作区 */
.workflow-form-footer {
display: flex;
flex-wrap: wrap; /* 失败原因详情换行到状态行下方 */
align-items: center;
gap: 12px;
padding: 12px 20px;
border-top: 1px solid #f1f5f9;
background: #fafbfc;
flex-shrink: 0;
.form-error-text {
.form-status-row {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.form-error-toggle {
padding: 0;
height: auto;
font-size: 12px;
color: #ef4444;
.error-arrow {
transition: transform 0.2s;
&.is-expanded { transform: rotate(180deg); }
}
}
.form-error-text {
flex-basis: 100%; /* 占整行,显示在状态行下方 */
font-size: 12px;
color: #ef4444;
line-height: 1.5;
white-space: pre-wrap; /* 失败详情可能含换行(error 字段多行原因),允许换行展示 */
word-break: break-word;
background: #fef2f2;
border: 1px solid #fee2e2;
border-radius: 6px;
padding: 8px 12px;
}
.form-edit-tip {
flex: 1;
+20 -8
View File
@@ -105,7 +105,7 @@ import { applyHomeFormValues, buildOutputsFromUrls } from './utils/flowDsl';
import type { WorkflowOutput } from './utils/flowDsl';
import { getChatModel, listModelManage } from '/@/api/settings/modelConfigV2';
import { connectSessionSocket, sendAgentStart, sendWorkflowStart, sendCancel } from './utils/wsExecute';
import { parseWsMessage, getDelta, getAnswer, getRecordId, getErrorText, getNodeEvent, getToolCallName, getToolResultText, type WorkflowNodeProgress } from './utils/wsMessage';
import { parseWsMessage, getDelta, getAnswer, getRecordId, getErrorText, getErrorDetail, getNodeEvent, getToolCallName, getToolResultText, type WorkflowNodeProgress } from './utils/wsMessage';
import { getApiErrorMessage } from '/@/utils/request';
import {
getWorkflowDetail,
@@ -690,6 +690,8 @@ const handleWorkflowSelect = async (workflowId: string | null, isTemplate?: bool
removeDraftFormCard();
const res = await getWorkflowDetail(workflowId);
selectedWorkflowDetail.value = res.data || null;
// 新建表单不沿用后端工作流详情自带的 chargeMode(如缺省 per_item):计费方式必选且无默认值,强制用户选择
if (res.data?.flowContent) delete res.data.flowContent.chargeMode;
// 对话卡片化:选中工作流即推送一张可填表单卡片进入消息流
if (res.data) pushFormCard(res.data);
} catch {
@@ -1042,7 +1044,7 @@ const runWorkflow = async (
sid: string,
sessionId: string,
detail: any,
opts: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates?: any[] },
opts: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates?: any[]; chargeMode?: string },
formMsg: ChatMessage
) => {
const curSession = historyList.value.find((h) => h.id === sid);
@@ -1119,6 +1121,8 @@ const runWorkflow = async (
// 构建 flowContenttemplates 不再放最外层(删除 detail.flowContent 可能遗留的顶层 templates,统一走节点)
const updatedFlowContent: any = { ...detail.flowContent, nodes: nodeInputParams };
delete updatedFlowContent.templates;
// 计费方式写入 flowContent(与 version 平级),后端按此计费;表单必填已拦截未选,此处仅兜底
if (opts.chargeMode) updatedFlowContent.chargeMode = opts.chargeMode;
// 3. 工作流模型由后端按 flow 配置决定,无需前端取模型 id(普通对话取模型见 runChat)
@@ -1136,13 +1140,14 @@ const runWorkflow = async (
formMsg.recordType = 'workflow';
}
} else if (msg.type === 'error') {
// 失败:执行中的节点标记为失败,执行过程区展示中断位置
// 失败:执行中的节点标记为失败,执行过程区展示中断位置
// 失败原因绑定 error 字段(节点失败详情),供卡片「查看失败原因」手动展开
if (formMsg.formProgress) {
formMsg.formProgress.nodes.forEach((n) => {
if (n.status === 'running') n.status = 'failed';
});
}
finishExec(false, getErrorText(msg));
finishExec(false, getErrorDetail(msg));
} else if (msg.type === 'flow_complete') {
// flow_complete 为后端新增的工作流完成信号,事件自带产出文件 URL 列表 → 直接渲染产出卡片;
// 以 round_start 首帧填充的 roundRecordId 作 backendId,保证实时产出可删除(与回显路径一致)
@@ -1247,7 +1252,7 @@ const snapshotTemplatesToMsg = (msg: ChatMessage, templates: any[]) => {
const startWorkflowFromCard = async (
msg: ChatMessage,
payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates?: any[] }
payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates?: any[]; chargeMode?: string }
) => {
// 定位卡片所在会话
let sid = '';
@@ -1269,6 +1274,12 @@ const startWorkflowFromCard = async (
if (Array.isArray(payload.templates)) {
snapshotTemplatesToMsg(msg, payload.templates);
}
// 计费方式写回消息对象 flowContent(快照):会话认领(virtual→UUID 触发 MainContent 重建)/历史回显重挂载时,
// WorkflowFormCard 从 flowContent.chargeMode 恢复本次选择,避免重挂载后计费方式被清空
if (payload.chargeMode) {
if (!msg.form.flowContent || typeof msg.form.flowContent !== 'object') msg.form.flowContent = {};
msg.form.flowContent.chargeMode = payload.chargeMode;
}
// 标记本轮发送中:工作流执行期间 InputBar 显示停止按钮(isGenerating 依赖 sendingSessions
sendingSessions[sid] = true;
@@ -1647,8 +1658,8 @@ const loadWorkflowSessionMessages = async (sid: string, results: VOSessionInfoRe
if (q) msgs.push({ id: 'rq-' + r.id, content: String(q), time: r.createdAt || '', isUser: true, recordId: String(r.id), recordType: r.type });
if (r.resultContent) msgs.push({ id: 'ra-' + r.id, content: String(r.resultContent), time: r.createdAt || '', isUser: false, recordId: String(r.id), recordType: r.type });
} else if (r.type === 'workflow') {
// 失败判断: errorMsg 为准(部分失败记录 status 仍是 1,仅凭 status 会误判为成功)
const failed = !!r.errorMsg;
// 失败判断:error / errorMsg 任一有值即失败(部分失败记录 status 仍是 1,仅凭 status 会误判为成功)
const failed = !!(r.errorMsg || r.error);
// 每次执行都渲染带值表单卡片(requestParams.nodes 即该次提交的表单值快照);
// form 补 id=flowId,供失败卡片「重新编辑并执行」时作为启动 flowId
if (Array.isArray(r.requestParams?.nodes)) {
@@ -1665,7 +1676,8 @@ const loadWorkflowSessionMessages = async (sid: string, results: VOSessionInfoRe
flowContent: r.requestParams,
},
formStatus: failed ? 'failed' : 'done',
formError: failed ? r.errorMsg || '执行失败' : '',
// 失败原因绑定 error 字段(节点失败详情);errorMsg(如「用户已终止执行」)仅作兜底
formError: failed ? r.error || r.errorMsg || '执行失败' : '',
});
}
// 结果消息:成功记录直接带产出(session/get 记录自带的 resultFileUrl),不再调 execution/get
+13
View File
@@ -169,3 +169,16 @@ export function getErrorText(msg: WsStreamMessage): string {
}
return '执行失败,请重试';
}
/** error 详细原因(error 字段优先展示失败详情;message 仅在 error 缺失时兜底)——供工作流失败卡片「查看失败原因」使用 */
export function getErrorDetail(msg: WsStreamMessage): string {
if (typeof msg?.error === 'string' && msg.error) return msg.error;
if (typeof msg?.message === 'string' && msg.message) return msg.message;
const d = msg?.data;
if (typeof d === 'string') return d;
if (d && typeof d === 'object') {
const nested = d.message ?? d.error ?? d.msg;
if (typeof nested === 'string' && nested) return nested;
}
return '执行失败,请重试';
}
@@ -1,5 +1,32 @@
<template>
<div class="editor-body">
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<div class="cfg-row">
<div class="cfg-item">
<div class="cfg-label">门禁金额</div>
<el-input-number v-model="minBalance" :min="0" :precision="5" :step="0.01" :controls="false" class="cfg-min-balance" />
</div>
<div class="cfg-item">
<div class="cfg-label">币种</div>
<el-select v-model="currency" class="cfg-currency">
<el-option label="人民币" value="CNY" />
<el-option label="美元" value="USD" />
</el-select>
</div>
<div class="cfg-item">
<div class="cfg-label">启用</div>
<el-switch v-model="enabled" />
</div>
</div>
</div>
</div>
</div>
<div class="editor-col">
<!-- 周期订阅 -->
<div class="card">
@@ -9,58 +36,27 @@
</div>
<div class="card-body">
<el-form label-position="top" size="default">
<el-form-item label="周期 period">
<el-form-item label="周期">
<el-select v-model="period" class="ctrl-select">
<el-option label="year按年" value="year" />
<el-option label="按年" value="year" />
</el-select>
</el-form-item>
<el-form-item label="每周期价格(元)">
<el-input-number
v-model="price"
:min="0"
:precision="2"
:precision="5"
:step="0.01"
:controls="false"
class="ctrl-input"
/>
</el-form-item>
<div class="form-hint">本期支持按年订阅period=year订阅购买 / 续费流程接入另期</div>
<div class="form-hint">本期支持按年订阅订阅购买 / 续费流程接入另期</div>
</el-form>
</div>
</div>
</div>
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<el-form label-position="top" size="default">
<el-form-item label="门禁 min_balance(元)">
<el-input-number
v-model="minBalance"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
class="ctrl-input"
/>
</el-form-item>
<el-form-item label="币种">
<el-select v-model="currency" class="ctrl-select">
<el-option label="人民币 (CNY)" value="CNY" />
<el-option label="美元 (USD)" value="USD" />
</el-select>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="enabled" />
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
@@ -134,14 +130,12 @@ defineExpose({ save });
<style scoped lang="scss">
.editor-body {
display: flex;
gap: 20px;
flex-wrap: wrap;
display: block;
}
.editor-col {
flex: 1;
min-width: 380px;
width: 100%;
min-width: 0;
}
.card {
@@ -182,6 +176,33 @@ defineExpose({ save });
line-height: 1.7;
}
.cfg-row {
display: flex;
align-items: flex-end;
gap: 40px;
flex-wrap: wrap;
}
.cfg-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.cfg-label {
font-size: 13px;
color: #606266;
}
.cfg-min-balance {
width: 200px;
}
.cfg-currency {
width: 200px;
}
.ctrl-input {
width: 220px;
}
+215 -78
View File
@@ -1,5 +1,32 @@
<template>
<div class="editor-body">
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<div class="cfg-row">
<div class="cfg-item">
<div class="cfg-label">门禁金额</div>
<el-input-number v-model="minBalance" :min="0" :precision="5" :step="0.01" :controls="false" class="cfg-min-balance" />
</div>
<div class="cfg-item">
<div class="cfg-label">币种</div>
<el-select v-model="currency" class="cfg-currency">
<el-option label="人民币" value="CNY" />
<el-option label="美元" value="USD" />
</el-select>
</div>
<div class="cfg-item">
<div class="cfg-label">启用</div>
<el-switch v-model="enabled" />
</div>
</div>
</div>
</div>
</div>
<div class="editor-col">
<!-- 基础计费 -->
<div class="card">
@@ -8,12 +35,12 @@
</div>
<div class="card-body">
<el-form label-position="top" size="default">
<el-form-item label="计费单位 unit">
<el-form-item label="计费单位">
<el-select v-model="unit" class="ctrl-select" @change="onUnitChange">
<el-option v-for="o in UNIT_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
<el-form-item label="阶梯分档 tiered">
<el-form-item label="阶梯分档">
<el-switch v-model="tiered" :disabled="!isTokenUnit" />
</el-form-item>
<div class="form-hint">阶梯=按输入 token 分档<b>命中哪档整单按哪档价不累加</b>档位区间相邻不重叠下档 max+1 = 上档 min token 单位per_1K / per_1M可用</div>
@@ -40,66 +67,90 @@
<el-button size="small" type="danger" text @click="removeRule(i)">删除</el-button>
</div>
<div class="rc-sec match">命中条件 match</div>
<div class="rc-sec match">命中条件</div>
<el-form label-position="top" size="small">
<div class="rc-grid">
<el-form-item label="媒体类型">
<el-select v-model="r.mediaType" class="rc-control">
<el-option v-for="o in MEDIA_TYPE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
<template v-if="subject.modelType === 'reason'">
<el-form-item label="thinking">
<el-form-item label="思考模式">
<el-select v-model="r.thinking" class="rc-control">
<el-option v-for="o in BOOL_TRIPLE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
<el-option v-for="o in THINKING_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
<el-form-item label="输入 token 下界">
<el-form-item label="输入长度下界">
<el-input-number v-model="r.inLenMin" :min="0" :precision="0" :controls="false" class="rc-control" />
</el-form-item>
<el-form-item label="输入 token 上界">
<el-form-item label="输入长度上界">
<el-input-number v-model="r.inLenMax" :min="0" :precision="0" :controls="false" class="rc-control" />
</el-form-item>
</template>
<template v-if="subject.modelType === 'video'">
<el-form-item label="outputAudio">
<el-form-item label="输出音频">
<el-select v-model="r.outputAudio" class="rc-control">
<el-option v-for="o in BOOL_TRIPLE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
<el-option v-for="o in OUTPUT_AUDIO_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
<el-form-item label="outputResolution">
<el-input v-model="r.outputResolution" placeholder="480p/720p/1080p/2k/4k" class="rc-control" />
<el-form-item label="输出分辨率">
<el-select v-model="r.outputResolution" class="rc-control">
<el-option v-for="o in RESOLUTION_OPTIONS.video" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
</template>
<template v-if="subject.modelType === 'image'">
<el-form-item label="outputResolution">
<el-input v-model="r.outputResolution" placeholder="512x512/1024x1024/2048x2048" class="rc-control" />
<el-form-item label="输出分辨率">
<el-select v-model="r.outputResolution" class="rc-control">
<el-option v-for="o in RESOLUTION_OPTIONS.image" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
</template>
</div>
</el-form>
<div class="rc-sec price">计价项 price</div>
<div class="rc-sec price">计价项</div>
<el-form label-position="top" size="small">
<div class="rc-grid">
<template v-if="isTokenUnit">
<el-form-item label="输入单价 input">
<el-input-number v-model="r.input" :min="0" :precision="6" :step="0.0001" :controls="false" class="rc-control" />
<el-form-item label="输入单价">
<el-input-number v-model="r.input" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
</el-form-item>
<el-form-item label="输出单价 output">
<el-input-number v-model="r.output" :min="0" :precision="6" :step="0.0001" :controls="false" class="rc-control" />
<el-form-item label="输出单价">
<el-input-number v-model="r.output" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
</el-form-item>
<el-form-item label="缓存命中 cacheHit">
<el-input-number v-model="r.cacheHit" :min="0" :precision="6" :step="0.0001" :controls="false" class="rc-control" />
<el-form-item label="缓存命中">
<el-input-number v-model="r.cacheHit" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="每单位单价 unitPrice">
<el-input-number v-model="r.unitPrice" :min="0" :precision="6" :step="0.0001" :controls="false" class="rc-control" />
<el-form-item label="每单位单价">
<el-input-number v-model="r.unitPrice" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
</el-form-item>
</template>
</div>
</el-form>
<!-- 媒体价格模板 token 单位媒体命中条件已移除改为对特定媒体类型设独立覆盖价 mediaPrices -->
<div v-if="isTokenUnit" class="media-price-section">
<div class="rc-sec price">媒体价格模板可选</div>
<div v-for="(mp, mIdx) in r.mediaPrices" :key="mIdx" class="media-price-row">
<el-select v-model="mp.mediaType" class="mp-media" placeholder="媒体类型">
<el-option v-for="o in getAvailableMediaOptions(r, mIdx)" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
<div class="mp-field">
<span class="mp-label">输入价</span>
<el-input-number v-model="mp.input" :min="0" :precision="5" :step="0.0001" :controls="false" class="mp-num" />
</div>
<div class="mp-field">
<span class="mp-label">输出价</span>
<el-input-number v-model="mp.output" :min="0" :precision="5" :step="0.0001" :controls="false" class="mp-num" />
</div>
<div class="mp-field">
<span class="mp-label">缓存命中价</span>
<el-input-number v-model="mp.cacheHit" :min="0" :precision="5" :step="0.0001" :controls="false" class="mp-num" />
</div>
<el-button type="danger" text size="small" @click="removeMediaPrice(r, mIdx)">删除</el-button>
</div>
<el-button size="small" class="media-add-btn" @click="addMediaPrice(r)">+ 添加媒体价格</el-button>
<div class="mp-tip">基础价input/output/cacheHit为默认价媒体模板仅覆盖指定媒体类型的价未选媒体类型的行保存时丢弃</div>
</div>
</div>
<el-button size="small" class="add-btn" @click="addRule">+ 添加规则</el-button>
@@ -110,51 +161,23 @@
</div>
</div>
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<el-form label-position="top" size="default">
<el-form-item label="门禁 min_balance(元)">
<el-input-number
v-model="minBalance"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
class="ctrl-input"
/>
</el-form-item>
<el-form-item label="币种">
<el-select v-model="currency" class="ctrl-select">
<el-option label="人民币 (CNY)" value="CNY" />
<el-option label="美元 (USD)" value="USD" />
</el-select>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="enabled" />
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import {
BOOL_TRIPLE_OPTIONS,
EXAMPLES,
MEDIA_TYPE_OPTIONS,
MEDIA_PRICE_OPTIONS,
OUTPUT_AUDIO_OPTIONS,
RESOLUTION_OPTIONS,
THINKING_OPTIONS,
UNIT_OPTIONS,
type BuiltModelRule,
type BuiltModelRuleMatch,
type Currency,
type EditorInitConfig,
type MediaPriceRow,
type ModelRule,
type ModelRules,
type Subject,
@@ -193,7 +216,6 @@ const priceNote = computed(() =>
function emptyRule(): ModelRule {
return {
name: '',
mediaType: '',
thinking: '',
outputAudio: '',
outputResolution: '',
@@ -203,6 +225,7 @@ function emptyRule(): ModelRule {
output: 0,
cacheHit: 0,
unitPrice: 0,
mediaPrices: [],
};
}
@@ -216,7 +239,6 @@ function loadExample(index: number) {
if (ex.minBalance != null) minBalance.value = ex.minBalance;
modelRules.value = ex.rules.map((r) => ({
name: r.name,
mediaType: (r.mediaType as ModelRule['mediaType']) || '',
thinking: (r.thinking == null ? '' : String(r.thinking)) as ModelRule['thinking'],
outputAudio: (r.outputAudio == null ? '' : String(r.outputAudio)) as ModelRule['outputAudio'],
outputResolution: r.outputResolution || '',
@@ -226,6 +248,7 @@ function loadExample(index: number) {
output: r.output ?? 0,
cacheHit: r.cacheHit ?? 0,
unitPrice: r.unitPrice ?? 0,
mediaPrices: mediaPricesToRows(r.mediaPrices),
}));
onUnitChange();
}
@@ -248,7 +271,6 @@ function fromBuiltRule(r: BuiltModelRule): ModelRule {
const m = r.match;
return {
name: r.name,
mediaType: (m?.mediaType as ModelRule['mediaType']) || '',
thinking: (m?.thinking == null ? '' : String(m.thinking)) as ModelRule['thinking'],
outputAudio: (m?.outputAudio == null ? '' : String(m.outputAudio)) as ModelRule['outputAudio'],
outputResolution: m?.outputResolution || '',
@@ -258,6 +280,7 @@ function fromBuiltRule(r: BuiltModelRule): ModelRule {
output: r.price.output ?? 0,
cacheHit: r.price.cacheHit ?? 0,
unitPrice: r.price.unitPrice ?? 0,
mediaPrices: mediaPricesToRows(r.mediaPrices),
};
}
@@ -277,15 +300,16 @@ function applyInit(cfg: EditorInitConfig) {
/** 过滤空白规则:规则名或任一命中/价格字段非空才保留 */
function keepRule(r: ModelRule): boolean {
if (r.name.trim()) return true;
if (r.mediaType || r.thinking || r.outputAudio || r.outputResolution) return true;
if (r.thinking || r.outputAudio || r.outputResolution) return true;
if (r.inLenMin != null || r.inLenMax != null) return true;
return r.input > 0 || r.output > 0 || r.cacheHit > 0 || r.unitPrice > 0;
if (r.input > 0 || r.output > 0 || r.cacheHit > 0 || r.unitPrice > 0) return true;
// 仅填写了媒体价格模板也算有效规则
return !!buildMediaPrices(r.mediaPrices);
}
/** 编辑器行态 → 构建态规则(match 只收录非空字段) */
function buildBuiltRule(r: ModelRule): BuiltModelRule {
const match: BuiltModelRuleMatch = {};
if (r.mediaType) match.mediaType = r.mediaType;
if (r.thinking === 'true' || r.thinking === 'false') match.thinking = r.thinking === 'true';
if (r.outputAudio === 'true' || r.outputAudio === 'false') match.outputAudio = r.outputAudio === 'true';
if (r.outputResolution) match.outputResolution = r.outputResolution;
@@ -298,9 +322,54 @@ function buildBuiltRule(r: ModelRule): BuiltModelRule {
name: r.name,
match: Object.keys(match).length ? match : undefined,
price,
mediaPrices: buildMediaPrices(r.mediaPrices),
};
}
/** 媒体价格行 → mediaPrices 对象(仅保留填写了价格的媒体;无覆盖返回 undefined 不写入) */
function buildMediaPrices(rows: MediaPriceRow[]): Record<string, Record<string, number>> | undefined {
if (!rows?.length) return undefined;
const map: Record<string, Record<string, number>> = {};
rows.forEach((row) => {
if (!row.mediaType) return;
const prices: Record<string, number> = {};
if (row.input > 0) prices.input = row.input;
if (row.output > 0) prices.output = row.output;
if (row.cacheHit > 0) prices.cacheHit = row.cacheHit;
if (Object.keys(prices).length) map[row.mediaType] = prices;
});
return Object.keys(map).length ? map : undefined;
}
/** mediaPrices 对象 → 编辑态媒体价格行(兼容后端两种结构:按媒体分组的内层对象,或空/缺失) */
function mediaPricesToRows(mediaPrices?: unknown): MediaPriceRow[] {
if (!mediaPrices || typeof mediaPrices !== 'object') return [];
return Object.entries(mediaPrices as Record<string, Record<string, number | undefined>>)
.filter(([, prices]) => prices && typeof prices === 'object')
.map(([mediaType, prices]) => ({
mediaType,
input: prices.input ?? 0,
output: prices.output ?? 0,
cacheHit: prices.cacheHit ?? 0,
}));
}
/** 添加一条媒体价格行 */
function addMediaPrice(r: ModelRule) {
r.mediaPrices.push({ mediaType: '', input: 0, output: 0, cacheHit: 0 });
}
/** 删除一条媒体价格行 */
function removeMediaPrice(r: ModelRule, index: number) {
r.mediaPrices.splice(index, 1);
}
/** 媒体价格行的可选媒体:排除本规则其他行已选中的媒体,避免同一条规则重复选择同一媒体 */
function getAvailableMediaOptions(r: ModelRule, curIndex: number) {
const selected = new Set(r.mediaPrices.filter((_, i) => i !== curIndex).map((row) => row.mediaType));
return MEDIA_PRICE_OPTIONS.filter((opt) => !selected.has(opt.value) || r.mediaPrices[curIndex]?.mediaType === opt.value);
}
/** 构建模型费率 JSON(对齐后端结构,由父组件 stringify 提交) */
function buildRules(): ModelRules {
return {
@@ -341,14 +410,12 @@ defineExpose({ save });
<style scoped lang="scss">
.editor-body {
display: flex;
gap: 20px;
flex-wrap: wrap;
display: block;
}
.editor-col {
flex: 1;
min-width: 380px;
width: 100%;
min-width: 0;
}
.card {
@@ -397,13 +464,6 @@ defineExpose({ save });
margin-top: 10px;
}
.ctrl-input {
width: 220px;
}
.ctrl-select {
width: 220px;
}
.rule-card {
border: 1px solid #e4e7ed;
@@ -467,6 +527,53 @@ defineExpose({ save });
width: 100%;
}
/* 媒体价格模板区(token 单位):基础价下方的媒体覆盖价动态行 */
.media-price-section {
margin-top: 12px;
border-top: 1px dashed #e4e7ed;
padding-top: 10px;
}
.media-price-row {
display: flex;
align-items: flex-end;
gap: 8px;
margin-bottom: 8px;
.mp-media {
width: 120px;
flex-shrink: 0;
}
.mp-field {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
.mp-label {
font-size: 12px;
color: #606266;
}
.mp-num {
width: 100%;
}
}
}
.media-add-btn {
width: 100%;
}
.mp-tip {
font-size: 12px;
color: #909399;
line-height: 1.7;
margin-top: 8px;
}
.note {
border: 1px solid #e6a23c;
background: #fdf6ec;
@@ -476,4 +583,34 @@ defineExpose({ save });
font-size: 12.5px;
line-height: 1.7;
}
.cfg-row {
display: flex;
align-items: flex-end;
gap: 40px;
flex-wrap: wrap;
}
.cfg-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.cfg-label {
font-size: 13px;
color: #606266;
}
.cfg-min-balance {
width: 200px;
}
.cfg-currency {
width: 200px;
}
.ctrl-select {
width: 220px;
}
</style>
@@ -2,19 +2,6 @@
<div class="subject-list">
<div v-if="loading" class="subject-loading">加载计价对象</div>
<template v-else>
<!-- 模型类型筛选 -->
<div v-if="hasModel" class="filter-bar">
<button
v-for="f in FILTER_OPTIONS"
:key="f.value"
class="filter-chip"
:class="{ active: modelFilter === f.value }"
@click="modelFilter = f.value"
>
{{ f.label }}
</button>
</div>
<!-- 工作流 -->
<div class="grp-head">
<span class="grp-name">工作流</span>
@@ -29,7 +16,6 @@
>
<span class="type-badge t-workflow">工作流</span>
<span class="subj-name" :title="s.name">{{ s.name }}</span>
<span v-for="m in s.modes" :key="m" class="mode-chip">{{ m }}</span>
<span
class="status-dot"
:class="s.hasConfig ? 'on' : 'off'"
@@ -37,13 +23,13 @@
/>
</div>
<!-- 模型支持类型筛选 -->
<!-- 模型 -->
<div class="grp-head">
<span class="grp-name">模型</span>
<span class="grp-count">{{ filteredModels.length }}</span>
<span class="grp-count">{{ modelSubjects.length }}</span>
</div>
<div
v-for="s in filteredModels"
v-for="s in modelSubjects"
:key="`${s.type}:${s.id}`"
class="subj-item"
:class="{ active: `${s.type}:${s.id}` === currentKey }"
@@ -57,7 +43,7 @@
:title="s.hasConfig ? '已配置' : '未配置'"
/>
</div>
<div v-if="!filteredModels.length" class="empty-tip">该类型暂无模型</div>
<div v-if="!modelSubjects.length" class="empty-tip">暂无模型</div>
<!-- 业务模块 -->
<div class="grp-head">
@@ -73,7 +59,6 @@
>
<span class="type-badge t-business">业务</span>
<span class="subj-name" :title="s.name">{{ s.name }}</span>
<span v-for="m in s.modes" :key="m" class="mode-chip">{{ m }}</span>
<span
class="status-dot"
:class="s.hasConfig ? 'on' : 'off'"
@@ -85,12 +70,12 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { ModelType, Subject } from '../mock';
import { computed } from 'vue';
import type { Subject } from '../mock';
/**
* 左侧计价对象枚举列表。
* 分三组渲染(工作流 / 模型 / 业务模块),模型组支持类型筛选
* 分三组渲染(工作流 / 模型 / 业务模块);
* 配置状态以实心(已配置)/ 空心(未配置)圆点呈现。
*/
const props = defineProps<{
@@ -105,57 +90,10 @@ const workflowSubjects = computed(() => props.subjects.filter((s) => s.type ===
const modelSubjects = computed(() => props.subjects.filter((s) => s.type === 'model'));
const businessSubjects = computed(() => props.subjects.filter((s) => s.type === 'business'));
const hasModel = computed(() => modelSubjects.value.length > 0);
/** 模型类型筛选 */
const FILTER_OPTIONS: { value: ModelType | 'all'; label: string }[] = [
{ value: 'all', label: '全部' },
{ value: 'reason', label: '推理' },
{ value: 'video', label: '视频' },
{ value: 'audio', label: '音频' },
{ value: 'image', label: '图片' },
];
const modelFilter = ref<ModelType | 'all'>('all');
const filteredModels = computed(() =>
modelFilter.value === 'all'
? modelSubjects.value
: modelSubjects.value.filter((s) => s.modelType === modelFilter.value),
);
</script>
<style scoped lang="scss">
.subject-list {
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 10px 16px 2px;
.filter-chip {
border: 1px solid #dcdfe6;
background: #fff;
color: #606266;
border-radius: 14px;
padding: 3px 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
line-height: 1.4;
&:hover {
color: #409eff;
border-color: #409eff;
}
&.active {
background: #ecf5ff;
border-color: #409eff;
color: #409eff;
font-weight: 500;
}
}
}
.grp-head {
display: flex;
align-items: center;
@@ -251,14 +189,6 @@ const filteredModels = computed(() =>
}
}
.mode-chip {
font-size: 11px;
background: #f4f4f5;
color: #606266;
border-radius: 3px;
padding: 1px 5px;
flex-shrink: 0;
}
.status-dot {
width: 8px;
@@ -1,5 +1,32 @@
<template>
<div class="editor-body">
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<div class="cfg-row">
<div class="cfg-item">
<div class="cfg-label">门禁金额</div>
<el-input-number v-model="minBalance" :min="0" :precision="5" :step="0.01" :controls="false" class="cfg-min-balance" />
</div>
<div class="cfg-item">
<div class="cfg-label">币种</div>
<el-select v-model="currency" class="cfg-currency">
<el-option label="人民币" value="CNY" />
<el-option label="美元" value="USD" />
</el-select>
</div>
<div class="cfg-item">
<div class="cfg-label">启用</div>
<el-switch v-model="enabled" />
</div>
</div>
</div>
</div>
</div>
<div class="editor-col">
<!-- 计费模式 -->
<div class="card">
@@ -11,12 +38,12 @@
<!-- 按条计费 per_item -->
<div class="mode-item">
<div class="mode-item-head">
<span class="mode-name">按条计费 <span class="mode-code">per_item</span></span>
<span class="mode-name">按条计费</span>
<el-switch v-model="modes.per_item" />
</div>
<div v-if="modes.per_item" class="mode-item-body">
<el-form label-position="top" size="default">
<el-form-item label="档位列表(上不封顶,命中首个 maxSec ≥ 时长 的档)">
<el-form-item label="档位列表(上不封顶,命中首个档位上限 ≥ 时长的档)">
<div class="tier-list">
<div v-for="(tier, i) in tiers" :key="i" class="tier-row">
<el-input-number
@@ -24,13 +51,13 @@
:min="0"
:precision="0"
:controls="false"
placeholder="maxSec"
placeholder="档位上限(秒)"
class="tier-input"
/>
<el-input-number
v-model="tier.price"
:min="0"
:precision="2"
:precision="5"
:controls="false"
placeholder="价格(元)"
class="tier-input"
@@ -44,7 +71,7 @@
<el-input-number
v-model="overflowUnitPrice"
:min="0"
:precision="2"
:precision="5"
:step="0.01"
:controls="false"
class="ctrl-input"
@@ -57,7 +84,7 @@
<!-- 按秒计费 per_second -->
<div class="mode-item">
<div class="mode-item-head">
<span class="mode-name">按秒计费 <span class="mode-code">per_second</span></span>
<span class="mode-name">按秒计费</span>
<el-switch v-model="modes.per_second" />
</div>
<div v-if="modes.per_second" class="mode-item-body">
@@ -66,7 +93,7 @@
<el-input-number
v-model="perSecondUnitPrice"
:min="0"
:precision="2"
:precision="5"
:step="0.01"
:controls="false"
class="ctrl-input"
@@ -79,7 +106,7 @@
<!-- token 计费 per_token -->
<div class="mode-item">
<div class="mode-item-head">
<span class="mode-name"> Token 计费 <span class="mode-code">per_token</span></span>
<span class="mode-name"> Token 计费</span>
<el-switch v-model="modes.per_token" />
</div>
<div v-if="modes.per_token" class="mode-item-body">
@@ -93,37 +120,6 @@
</div>
</div>
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<el-form label-position="top" size="default">
<el-form-item label="门禁 min_balance(元)">
<el-input-number
v-model="minBalance"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
class="ctrl-input"
/>
</el-form-item>
<el-form-item label="币种">
<el-select v-model="currency" class="ctrl-select">
<el-option label="人民币 (CNY)" value="CNY" />
<el-option label="美元 (USD)" value="USD" />
</el-select>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="enabled" />
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
@@ -224,14 +220,12 @@ defineExpose({ save });
<style scoped lang="scss">
.editor-body {
display: flex;
gap: 20px;
flex-wrap: wrap;
display: block;
}
.editor-col {
flex: 1;
min-width: 380px;
width: 100%;
min-width: 0;
}
.card {
@@ -286,19 +280,12 @@ defineExpose({ save });
.mode-name {
font-size: 13px;
font-weight: 600;
.mode-code {
font-size: 12px;
font-weight: 400;
color: #909399;
margin-left: 6px;
}
}
}
.mode-item-body {
padding: 12px;
}
.mode-item-body {
padding: 12px;
}
}
.tier-list {
@@ -328,7 +315,29 @@ defineExpose({ save });
width: 220px;
}
.ctrl-select {
width: 220px;
.cfg-row {
display: flex;
align-items: flex-end;
gap: 40px;
flex-wrap: wrap;
}
.cfg-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.cfg-label {
font-size: 13px;
color: #606266;
}
.cfg-min-balance {
width: 200px;
}
.cfg-currency {
width: 200px;
}
</style>
+3 -3
View File
@@ -141,9 +141,9 @@ const current = computed<Subject | undefined>(() =>
const headerSub = computed(() => {
const s = current.value;
if (!s) return '加载计价对象…';
if (s.type === 'workflow') return `subjectType=workflow · subjectId=${s.id}`;
if (s.type === 'model') return `subjectType=model · subjectId=${s.id}${s.typeLabel}modelType=${s.modelType}`;
return `subjectType=business · subjectId=${s.id}`;
if (s.type === 'workflow') return `工作流对象 · 标识:${s.id}`;
if (s.type === 'model') return `模型对象${s.typeLabel})· 标识:${s.id}`;
return `业务模块对象 · 标识:${s.id}`;
});
/** 详情请求序号,防止快速切换时旧响应覆盖当前对象 */
+81 -36
View File
@@ -4,7 +4,7 @@
* 说明:
* - 计价对象枚举来自后端 GET .../subjects(见 src/api/trade/pricing/index.ts),
* 本文件提供后端项 → 页面 Subject 的映射(中文 modelType/modelTypeCode → 英文枚举等)
* - 计价配置读取后端的 rules JSON 字符串(JSON.parse),保存时 JSON.stringify 提交
* - 计价配置读取/提交后端的 rules 均为对象(config/get 返回对象,save 也传对象)
* - 模型类型编码:reason=100推理 / image=200图片 / audio=300音频 / video=600视频
* - EXAMPLES 仅为编辑器本地"示例填充" UX,不参与后端数据
*/
@@ -37,6 +37,8 @@ export interface ApiSubjectItem {
modelType?: string;
/** 仅 model:模型类型编码(100推理/200图片/300音频/600视频) */
modelTypeCode?: number;
/** 该对象支持的计费模式(workflow/business 由后端 subjects.chargeModes 下发;model 无此字段) */
chargeModes?: string[];
}
/** modelTypeCode → 英文模型类型 */
@@ -47,12 +49,6 @@ export const MODEL_TYPE_CODE_TO_EN: Record<number, ModelType> = {
600: 'video',
};
/** 各 subjectType 固定的计费模式(后端不下发 modes,按类型硬编码展示) */
export const SUBJECT_MODE_MAP: Partial<Record<SubjectType, string[]>> = {
workflow: ['per_item', 'per_second', 'per_token'],
business: ['per_period'],
};
/** 后端枚举项 → 页面 Subject */
export function toSubject(item: ApiSubjectItem): Subject {
if (item.subjectType === 'model') {
@@ -71,7 +67,7 @@ export function toSubject(item: ApiSubjectItem): Subject {
type: item.subjectType,
id: item.subjectId,
name: item.name,
modes: SUBJECT_MODE_MAP[item.subjectType] ?? [],
modes: item.chargeModes ?? [],
hasConfig: false,
};
}
@@ -90,7 +86,7 @@ export interface Subject {
modelTypeCode?: number;
/** 仅 model:模型中文类型(推理/视频/音频/图片) */
typeLabel?: string;
/** workflow/business:支持的计费模式(本地硬编码 */
/** workflow/business:支持的计费模式(后端 subjects.chargeModes 下发 */
modes?: string[];
/** 是否已配置计价(选中后由 config/get 判断) */
hasConfig: boolean;
@@ -122,10 +118,21 @@ export interface WorkflowRules {
/* ================= 模型费率 JSON ================= */
/** 编辑器内部规则行态(thinking/outputAudio 用字符串承载"未设置" */
/** 媒体价格模板行(token 单位):对特定媒体类型设置独立覆盖价(mediaPrices */
export interface MediaPriceRow {
/** 媒体类型:text / audio / video / image */
mediaType: string;
/** 输入单价 */
input: number;
/** 输出单价 */
output: number;
/** 缓存命中输入单价 */
cacheHit: number;
}
/** 编辑器内部规则行态(thinking/outputAudio 用字符串承载"未设置";媒体命中条件已移除,媒体改走价格模板) */
export interface ModelRule {
name: string;
mediaType: '' | 'text' | 'audio' | 'video' | 'image';
thinking: '' | 'true' | 'false';
outputAudio: '' | 'true' | 'false';
outputResolution: string;
@@ -139,12 +146,13 @@ export interface ModelRule {
cacheHit: number;
/** 非 token 单位:每单位单价 */
unitPrice: number;
/** 媒体价格模板行(token 单位;空数组=无媒体覆盖价) */
mediaPrices: MediaPriceRow[];
}
/** 示例数据源规则(thinking/outputAudio 为布尔,由 loadExample 转换) */
export interface ExampleRule {
name: string;
mediaType?: string;
thinking?: boolean;
outputAudio?: boolean;
outputResolution?: string;
@@ -154,11 +162,12 @@ export interface ExampleRule {
output?: number;
cacheHit?: number;
unitPrice?: number;
/** 媒体价格模板示例(可选):媒体类型 → 覆盖价 */
mediaPrices?: Record<string, { input?: number; output?: number; cacheHit?: number }>;
}
/** 构建态:规则命中条件 */
/** 构建态:规则命中条件(媒体类型已改为价格模板,不再作为命中条件) */
export interface BuiltModelRuleMatch {
mediaType?: string;
thinking?: boolean;
outputAudio?: boolean;
outputResolution?: string;
@@ -171,6 +180,8 @@ export interface BuiltModelRule {
name: string;
match?: BuiltModelRuleMatch;
price: Record<string, number>;
/** 媒体价格模板(token 单位):媒体类型 → 该媒体独立覆盖价;无覆盖时省略 */
mediaPrices?: Record<string, Record<string, number>>;
}
/** 模型费率 JSON 结构(对齐后端,含内嵌币种) */
@@ -229,39 +240,65 @@ export const MODEL_TYPE_CODES: Record<ModelType, number> = {
};
export const UNIT_OPTIONS: { value: Unit; label: string }[] = [
{ value: 'per_1M', label: 'per_1M每百万token' },
{ value: 'per_1K', label: 'per_1K(每千token' },
{ value: 'per_1', label: 'per_1(每个/每张图' },
{ value: 'per_second', label: 'per_second每秒' },
{ value: 'per_minute', label: 'per_minute每分钟' },
{ value: 'per_hour', label: 'per_hour每小时' },
{ value: 'per_char', label: 'per_char每字' },
{ value: 'per_1M', label: '每百万 Token' },
{ value: 'per_1K', label: '每千 Token' },
{ value: 'per_1', label: '每个 / 每张图' },
{ value: 'per_second', label: '每秒' },
{ value: 'per_minute', label: '每分钟' },
{ value: 'per_hour', label: '每小时' },
{ value: 'per_char', label: '每字' },
];
export const MEDIA_TYPE_OPTIONS: { value: ModelRule['mediaType']; label: string }[] = [
/** 媒体价格模板可选媒体类型(媒体命中条件已移除,媒体改按模板定价) */
export const MEDIA_PRICE_OPTIONS: { value: 'text' | 'audio' | 'video' | 'image'; label: string }[] = [
{ value: 'text', label: '文本' },
{ value: 'audio', label: '音频' },
{ value: 'video', label: '视频' },
{ value: 'image', label: '图片' },
];
/** outputAudio 命中条件(video):给用户展示中文(有声/无声) */
export const OUTPUT_AUDIO_OPTIONS: { value: '' | 'true' | 'false'; label: string }[] = [
{ value: '', label: '不限制' },
{ value: 'text', label: '文本 text' },
{ value: 'audio', label: '音频 audio' },
{ value: 'video', label: '视频 video' },
{ value: 'image', label: '图片 image' },
{ value: 'true', label: '有声' },
{ value: 'false', label: '无声' },
];
export const BOOL_TRIPLE_OPTIONS: { value: '' | 'true' | 'false'; label: string }[] = [
/** thinking 命中条件(reason):给用户展示中文(是/否) */
export const THINKING_OPTIONS: { value: '' | 'true' | 'false'; label: string }[] = [
{ value: '', label: '不限制' },
{ value: 'true', label: '是' },
{ value: 'false', label: '否' },
];
/** 输出分辨率命中条件下拉选项(按模型类型区分:video 横版 / image 方形) */
export const RESOLUTION_OPTIONS: Record<'video' | 'image', { value: string; label: string }[]> = {
video: [
{ value: '', label: '不限制' },
{ value: '480p', label: '480p' },
{ value: '720p', label: '720p' },
{ value: '1080p', label: '1080p' },
{ value: '2k', label: '2k' },
{ value: '4k', label: '4k' },
],
image: [
{ value: '', label: '不限制' },
{ value: '512x512', label: '512x512' },
{ value: '1024x1024', label: '1024x1024' },
{ value: '2048x2048', label: '2048x2048' },
],
};
/** 模型示例(本地填充 UX,逐字对应原型定价配置) */
export const EXAMPLES: ModelExampleMap = {
video: [
{
label: '视频·按秒(分辨率×有声/无声×输入媒体',
label: '视频·按秒(分辨率×有声/无声)',
unit: 'per_second',
tiered: false,
rules: [
{ name: '无声-720p-输入不含视频', mediaType: 'text', outputAudio: false, outputResolution: '720p', unitPrice: 0.8 },
{ name: '有声-1080p-输入含视频', mediaType: 'video', outputAudio: true, outputResolution: '1080p', unitPrice: 1.5 },
{ name: '无声-720p', outputAudio: false, outputResolution: '720p', unitPrice: 0.8 },
{ name: '有声-1080p', outputAudio: true, outputResolution: '1080p', unitPrice: 1.5 },
],
},
{
@@ -269,20 +306,28 @@ export const EXAMPLES: ModelExampleMap = {
unit: 'per_1M',
tiered: false,
rules: [
{ name: '文本输入-无声-1080p', mediaType: 'text', outputAudio: false, outputResolution: '1080p', input: 0.6, output: 3.6, cacheHit: 0.12 },
{ name: '视频输入-有声-1080p', mediaType: 'video', outputAudio: true, outputResolution: '1080p', input: 9, output: 27, cacheHit: 1.8 },
{ name: '无声-1080p', outputAudio: false, outputResolution: '1080p', input: 0.6, output: 3.6, cacheHit: 0.12 },
{ name: '有声-1080p', outputAudio: true, outputResolution: '1080p', input: 9, output: 27, cacheHit: 1.8 },
],
},
],
reason: [
{
label: '推理·阶梯(思考×输入token档×输入媒体',
label: '推理·阶梯(思考×输入长度档×媒体模板',
unit: 'per_1M',
tiered: true,
rules: [
{ name: '思考-输入≤32000-文本', mediaType: 'text', thinking: true, inLenMax: 32000, input: 0.6, output: 3.6, cacheHit: 0.12 },
{ name: '思考-输入32001~128000-文本', mediaType: 'text', thinking: true, inLenMin: 32001, inLenMax: 128000, input: 0.9, output: 5.4, cacheHit: 0.18 },
{ name: '思考-输入含音频', mediaType: 'audio', thinking: true, input: 9, output: 3.6, cacheHit: 1.8 },
{ name: '思考-输入长度[0,32k]', thinking: true, inLenMax: 32000, input: 0.6, output: 3.6, cacheHit: 0.12 },
{
name: '思考-输入长度(32k,128k]',
thinking: true,
inLenMin: 32001,
inLenMax: 128000,
input: 0.9,
output: 5.4,
cacheHit: 0.18,
mediaPrices: { audio: { input: 9, output: 3.6, cacheHit: 1.8 } },
},
{ name: '非思考-统一价', thinking: false, input: 0.3, output: 1.2, cacheHit: 0.06 },
],
},
+25 -25
View File
@@ -1,27 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
+23 -23
View File
@@ -1,17 +1,17 @@
-----BEGIN CERTIFICATE-----
MIIGJTCCBQ2gAwIBAgIQCXpUlFT9Cue3+TsPpxvnWjANBgkqhkiG9w0BAQsFADBu
MIIGJTCCBQ2gAwIBAgIQA0m9UBf/1I217Y9jcPp0vTANBgkqhkiG9w0BAQsFADBu
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMS0wKwYDVQQDEyRFbmNyeXB0aW9uIEV2ZXJ5d2hlcmUg
RFYgVExTIENBIC0gRzIwHhcNMjYwNjEwMDAwMDAwWhcNMjYwOTA3MjM1OTU5WjAj
RFYgVExTIENBIC0gRzIwHhcNMjYwOTA3MDAwMDAwWhcNMjYxMjA1MjM1OTU5WjAj
MSEwHwYDVQQDExhnaXRlYS5yZWRwb3dlcmZ1dHVyZS5jb20wggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQCqVsfwTHPJbcPzuCUOCNLPoigpaZXWJmHfcMhi
YMXNtoap0FmBbJbhNZsGf2G5UOuCVKGN4gSMB18rcFwcq/j/GH5eMaPOn8bK8qSC
AsYHYi1zdarDGXICGWbu2BTRuZAY+7/4Kci+ayzuUpbH1vkv15hT5OhTev8Y4/iO
4apxRgcvKFZkOPLZ+M7km7j9bRTU5xouGjVJqmf0QVcUiMnlFuKI4XxBWjwTPcRt
X8SxCjBoi+k6iotPUYYFF3qzP8vVMyWLDpQ9wZ35NNoANOHg/FKSVixequjS+y1+
3SSK4ASVqHcfDddkw2JF8ColW+Fzz+ld0DqQfgW5kp/EAXwLAgMBAAGjggMIMIID
BDAfBgNVHSMEGDAWgBR435GQX+7erPbFdevVTFVT7yRKtjAdBgNVHQ4EFgQUOiq0
OCRofHA6BrxoM9do+AWnNgowQQYDVR0RBDowOIIYZ2l0ZWEucmVkcG93ZXJmdXR1
DQEBAQUAA4IBDwAwggEKAoIBAQCUvl+uz6tWWTAea9IYnFT35vJzbfPF2wutwVBL
l4QVTNBUPK7Ptpmpvc1AzJNGgZwuxRhEfJBZkx2x7O7XES6a80v0THUGqHf9SUoM
QFUX/PJWtaGF9RQvl53GWJY2vDCYexfUUbL4xpVXIHzNXvXnrIZy8KrtB8A01Pqq
2W2KiCo7qswpaBP/2F0Bn4U9BonaEUeyDPbRNaAqRLZWRnW2asDHAgjpoUEvKqSS
Y6cMWwjCtbzOQaKctWmq8jqjIceA71UqD5MxgtWyykrCage5IQJFBVvbRvWOk+J+
yybGg9jGRjz1objhWMYO5YNTXW1tTvc9oSdltFnT5zBJAJcVAgMBAAGjggMIMIID
BDAfBgNVHSMEGDAWgBR435GQX+7erPbFdevVTFVT7yRKtjAdBgNVHQ4EFgQUYPN9
3He9hDMC/uFm78xCWWEF4/owQQYDVR0RBDowOIIYZ2l0ZWEucmVkcG93ZXJmdXR1
cmUuY29tghx3d3cuZ2l0ZWEucmVkcG93ZXJmdXR1cmUuY29tMD4GA1UdIAQ3MDUw
MwYGZ4EMAQIBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29t
L0NQUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
@@ -19,19 +19,19 @@ BwMCMIGABggrBgEFBQcBAQR0MHIwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
Z2ljZXJ0LmNvbTBKBggrBgEFBQcwAoY+aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
Y29tL0VuY3J5cHRpb25FdmVyeXdoZXJlRFZUTFNDQS1HMi5jcnQwDAYDVR0TAQH/
BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFnAHUAwjF+V0UZo0XufzjespBB
68fCIVoiv3/Vta12mtkOUs0AAAGesEYy8QAABAMARjBEAiBD7255l5PRI6mZXWxU
iC7txvWxkLlBgVFzAkRrMW7RXQIgZyupslAslfGDqDMk9H948AFZGe0JwB6YiuXK
lx1sox0AdgDYCVU7lE96/8gWGW+UT4WrsPj8XodVJg8V0S5yu0VLFAAAAZ6wRjNI
AAAEAwBHMEUCIBqfIfl4sL0ME60NI0KG6JIHImcJjR5qkLGDjCp3sEhrAiEA4cvy
yFaz3BSjjQa+UspsqgfKxeTjCLJq5pLbDcgBgGQAdgCUTkOH+uzB74HzGSQmqBhl
AcfTXzgCAT9yZ31VNy4Z2AAAAZ6wRjL3AAAEAwBHMEUCIQDIIa8mLpO6BmhgCecw
pI+QNznn4lhIxmtJQyHmZo7qRAIgFPq9xlLLqqe5/+WgcNwoawggpi8skhg8t/yT
JJ5TmV0wDQYJKoZIhvcNAQELBQADggEBAOeiZrRwpNh/bNIuKe+1vWBrLHu2Zj70
cKAVUhT08ZAl8scEYusbuqtSAq+oz9sm595YyPgmrB0wSZEMTd/+pUy5GrrJAXal
kOx1a3TBxCvpK9F4T7KmBaBWA5HuljHfw9833jXi7v9jLYShDjyNRzSWeKl41kdt
Hq4CbDLXp9XVzVPBlOongGZV/6cDrvy4hoJcBq8UANU/QWw0PaJQWxPwHcULekBw
AefTX7JwKbHcZ3gGV4EzJU/eoycBOu9VsYgJu9U+p1nNjYHwS8RQjij+cgs8r5Wx
r4+wYkdN2767iK2+NRRdQD/ZqBMTgSZ6AXCM7gS8oP/ylLwnzyq7ikc=
68fCIVoiv3/Vta12mtkOUs0AAAGgebrk6AAABAMARjBEAiAgFMcnwhg3c0h/LxiP
nZ+YWMLYgXC9ePXRZ+F0CFotEAIgR5H6BlW0mUMwOr5n82ukGvdlme/gkKINeQA2
r33cbtQAdgDYCVU7lE96/8gWGW+UT4WrsPj8XodVJg8V0S5yu0VLFAAAAaB5uuUP
AAAEAwBHMEUCIHMpgTZWPL/owVG92G0tYuZEiCPkElXskLLFygZOqbKkAiEA7RGC
gJ+Gd+XWH5n1kvVyofTnRccp9PVwc7gYUzfNQU0AdgCUTkOH+uzB74HzGSQmqBhl
AcfTXzgCAT9yZ31VNy4Z2AAAAaB5uuT1AAAEAwBHMEUCIAzth4PgMSzYLoUFxNAu
ErCqHKZNRACffuKdr1sKodLJAiEAh67qPx8Pfr87kSR2bnMYnpBcXjJ38S4cz2RL
v6dFDaowDQYJKoZIhvcNAQELBQADggEBABPRvwySa7ou11Mlf5265pizxyR6qhG8
ywlCy0IImyFBZ/mu4YzXrLbBVB9c/KNGiLxCAUBg+rEZsbVmPW//cfo31uUtzenb
DhDOXqVlULG1nwDkhcCw90jMGUOl5X6zKZ2KSXd3Uq++EVTjuqXC3fbheocXLNZr
22DWhQ7wfFvogw6bKCkS15y4WGjWa2ut4Avso9pzCA+HDzJCTMLgJStOZNArRkZQ
u3OhuLjnbG5kWQiwVXU/ixxOaL4JCwZWKueMhumux20Ww6bvdj+wx+l4M5Dg+qXp
D3bLSWOxFnv2SqxFf1AA/7hf5rqomO8XhsFGSBV/ESGoKvAQyo5gnRw=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEqjCCA5KgAwIBAgIQDeD/te5iy2EQn2CMnO1e0zANBgkqhkiG9w0BAQsFADBh
+25 -25
View File
@@ -1,27 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
+24 -24
View File
@@ -1,37 +1,37 @@
-----BEGIN CERTIFICATE-----
MIIGJTCCBQ2gAwIBAgIQAWng8l3t5rBYcCmLpCDdJTANBgkqhkiG9w0BAQsFADBu
MIIGJDCCBQygAwIBAgIQCG68erA4eBvEiFO3I/+fkDANBgkqhkiG9w0BAQsFADBu
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMS0wKwYDVQQDEyRFbmNyeXB0aW9uIEV2ZXJ5d2hlcmUg
RFYgVExTIENBIC0gRzIwHhcNMjYwNjEyMDAwMDAwWhcNMjYwOTA5MjM1OTU5WjAj
RFYgVExTIENBIC0gRzIwHhcNMjYwOTA3MDAwMDAwWhcNMjYxMjA1MjM1OTU5WjAj
MSEwHwYDVQQDExhtaW5pby5yZWRwb3dlcmZ1dHVyZS5jb20wggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQC+gxSvGFHa9gL+3IZmWmWQA2m2YV+pyCWd34RO
TWmkT+0zKsg/EEu+M3UEhxsEWQe2G6/9S391KC1LSFsjnZnVswAJsnlJ08tMFnM2
leAdZdQd/9I6xAHyzuCwJ6RxWXLdN/V6axfFzJ5dDu7z9ms0QAoZnBPfGQC+217T
U2ol5ogx11jWj+euVWGBXoK8O0IAUtFW/DannNz9V6Nz8wsRdXiX2DdUg4N9whsu
xzNSaxP3zM4s7DCBWywJBx2cDXZPVfdTixfhTQ+KfQx7FPkvz8rHekdtqOlAr4gP
RMOmY/hAp/lCQAcnEzunLi7UQc0GjeeSvFY099lNOeFu+jCPAgMBAAGjggMIMIID
BDAfBgNVHSMEGDAWgBR435GQX+7erPbFdevVTFVT7yRKtjAdBgNVHQ4EFgQUm+W1
VEi2C0CdxtuncyBFtX9cS5UwQQYDVR0RBDowOIIYbWluaW8ucmVkcG93ZXJmdXR1
DQEBAQUAA4IBDwAwggEKAoIBAQCkmgtAQ/KZv8wrvYOiih8xozGCaoTWiW5ye/Hf
tnX07Sxt1IaAfFUMHtaoEzsVBLwuTALYINWsCTrolvrgy6i/WvpsTGbZn7mkLjmK
T8db1ToBueW/eePdHXg2Q1Pgi/Cy5/cwkp29WXvxRsbiINP4jqsma+4vuqPuXV6W
o6HfXL3xddPl/ap0ScjCe+aZKB/hWwR3O4QNSmQFLY6jb7tgH3hqwRJzjOnqkQHA
H2Tcdx/gUYYUErPqyyqjhFJW5uDvh1Ha5jcbU2XphOiBgI3wElCkmfAqt4WZGU0z
kIluYkwZDxe3nN7iSZl9SbmgwTmlO1g04rC3T5WRQlk08dVdAgMBAAGjggMHMIID
AzAfBgNVHSMEGDAWgBR435GQX+7erPbFdevVTFVT7yRKtjAdBgNVHQ4EFgQUbMUu
74Ronh+u0yO+sQpoSQ4TbFMwQQYDVR0RBDowOIIYbWluaW8ucmVkcG93ZXJmdXR1
cmUuY29tghx3d3cubWluaW8ucmVkcG93ZXJmdXR1cmUuY29tMD4GA1UdIAQ3MDUw
MwYGZ4EMAQIBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29t
L0NQUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
BwMCMIGABggrBgEFBQcBAQR0MHIwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
Z2ljZXJ0LmNvbTBKBggrBgEFBQcwAoY+aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
Y29tL0VuY3J5cHRpb25FdmVyeXdoZXJlRFZUTFNDQS1HMi5jcnQwDAYDVR0TAQH/
BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFnAHUAwjF+V0UZo0XufzjespBB
68fCIVoiv3/Vta12mtkOUs0AAAGeucefAgAABAMARjBEAiBdD793ndAhFw/xk4gS
Pycc/U4IGSbSFNg1wXMMBkma9gIgSK8LNW9lqHn4wEYFRQrBfmYMUkJhUR51JmOb
rUESBu8AdgDXbX0Q0af1d8LH6V/XAL/5gskzWmXh0LMBcxfAyMVpdwAAAZ65x57r
AAAEAwBHMEUCIALrNS+sHrfHVl72YGHkqSb4quYzH6xZ0JNCTLakfjK4AiEA0jbR
ORUL7BLzYbw5geZfMb8NJmD+7CbgXdHNPWmowdYAdgCUTkOH+uzB74HzGSQmqBhl
AcfTXzgCAT9yZ31VNy4Z2AAAAZ65x58NAAAEAwBHMEUCIQCn753LSIK5ODnAw8za
2BUf7PZQhtwhe1I0fEfrFjwcAAIgPZtnV83cNV8UK9zGQhWso4QX/oK+ziXQMuP5
+lApPpcwDQYJKoZIhvcNAQELBQADggEBAKceFp1JWPgaR5TGrMXQNY0qgRPhY0ZC
MSVLqtbgWPeODl3p+BTpLJ/taGR1v1mlL/2cM+EU/RQVDobYUu91Td0Tibp1T8vv
KKgnGTj8fVTzimTR5YsFHKc0XFqHpbneArZFb9tbMnRLheDjAqJ3Xs5H5t/XNDSi
vasqxdvAYZPP+7JZPRrFqq/fPZR79NRz7kl45YiQX199d/8oPRVFRrq1TGtp41Nk
7rP31tq0YZDpBHXXmpYXmOZalWBHoASnZJDq0+H5bSBzNVHR25eOZe0dt5lWMzrf
bzFMatWnPKI/aLhc3GGZAxyy7PCRA5pJGB+qnaJAakODvB02VvJey/Y=
BAIwADCCAXwGCisGAQQB1nkCBAIEggFsBIIBaAFmAHUAwjF+V0UZo0XufzjespBB
68fCIVoiv3/Vta12mtkOUs0AAAGgebrkkgAABAMARjBEAiA3QXzJe/8VFDnVdr/6
BRerjq1qVsc61eFPJfMdmgbeCAIgTTMUJI0RnOIX7O0umzAWJa6T4lljD2+2ybyw
FNTGoaQAdgDYCVU7lE96/8gWGW+UT4WrsPj8XodVJg8V0S5yu0VLFAAAAaB5uuSb
AAAEAwBHMEUCIDb3e4Fb75I9JRnvfM5q8/iPLgs4u/NPdH4brm//kO9KAiEAujgF
VeUmRxadsYgF1pVRytH2vURymXUvcTtZfWLbk/YAdQCUTkOH+uzB74HzGSQmqBhl
AcfTXzgCAT9yZ31VNy4Z2AAAAaB5uuSbAAAEAwBGMEQCIGyiorlBMD7UARZvFGRz
VsPQH+oqh3cEtHq9t/EdDFH7AiAb7dllHHDE1zJrf41OuXOHQZZrA5VBNPpBu1Lj
avU6kzANBgkqhkiG9w0BAQsFAAOCAQEADDNB+zpouXcPD43XUYYE8dOpqFCHXGpe
MOn44OGPpfU/DvsQgsMZl//bdIcSlmuyHIr/Z5CojN/sCiyCmqnM+vSRMjcjoqxU
y7Ly0df9moxXprB20/XiWhCMSy0+bK051Kk4Lh3O0jWKGe2edEUEzwb1vvyhDylV
skd347/BUVGEf+PGT4NqI2ImWoUBc73C8oZ6eHe/bHAiESOI1RnUefOUw3G8El2o
t5DUflbkT1+Gzi/NgxvnSwbuvsf77mA9DOg8F6wwsPk4je+ekGycrzoVj/hnzy+h
4BT6OiBxzBkRa7TLMblQAPiDTXkbP41ZzLNYnCN8XwpXRvrt29Hf6g==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEqjCCA5KgAwIBAgIQDeD/te5iy2EQn2CMnO1e0zANBgkqhkiG9w0BAQsFADBh