<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="UTF-8" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<title>Prompt配置</title>
|
<link rel="stylesheet" href="../../static/vue/element/element.css" />
|
<style>
|
[v-cloak] { display: none; }
|
body {
|
margin: 0;
|
font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
|
background:
|
radial-gradient(1200px 520px at 8% -10%, rgba(24, 118, 210, 0.14), transparent 50%),
|
radial-gradient(900px 480px at 100% 0%, rgba(22, 160, 133, 0.11), transparent 56%),
|
linear-gradient(180deg, #f4f8fb 0%, #f8fbfd 100%);
|
color: #223244;
|
}
|
.page-shell {
|
max-width: 1700px;
|
margin: 14px auto;
|
padding: 0 14px 14px;
|
}
|
.hero {
|
border-radius: 18px;
|
color: #fff;
|
padding: 16px 18px;
|
background: linear-gradient(135deg, #0f4774 0%, #1f6fb2 45%, #249b8f 100%);
|
box-shadow: 0 14px 30px rgba(16, 65, 103, 0.22);
|
}
|
.hero-top {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
flex-wrap: wrap;
|
}
|
.hero-title {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
min-width: 0;
|
}
|
.hero-title .main {
|
font-size: 18px;
|
font-weight: 700;
|
letter-spacing: 0.3px;
|
}
|
.hero-title .sub {
|
margin-top: 3px;
|
font-size: 12px;
|
opacity: 0.92;
|
}
|
.hero-actions {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
flex-wrap: wrap;
|
justify-content: flex-end;
|
}
|
.summary-grid {
|
margin-top: 12px;
|
display: grid;
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
gap: 10px;
|
}
|
.summary-card {
|
border-radius: 12px;
|
padding: 10px 12px;
|
min-height: 62px;
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
background: rgba(255, 255, 255, 0.14);
|
backdrop-filter: blur(4px);
|
}
|
.summary-card .k {
|
font-size: 11px;
|
opacity: 0.88;
|
}
|
.summary-card .v {
|
margin-top: 6px;
|
font-size: 24px;
|
font-weight: 700;
|
line-height: 1.1;
|
}
|
.workspace {
|
margin-top: 12px;
|
display: grid;
|
grid-template-columns: 280px 380px minmax(0, 1fr);
|
gap: 12px;
|
min-height: calc(100vh - 170px);
|
}
|
.panel {
|
display: flex;
|
flex-direction: column;
|
min-height: 0;
|
border-radius: 18px;
|
border: 1px solid #dce6f1;
|
background:
|
radial-gradient(700px 180px at -10% 0%, rgba(45, 120, 200, 0.05), transparent 55%),
|
radial-gradient(640px 200px at 110% 10%, rgba(35, 155, 133, 0.06), transparent 60%),
|
rgba(255, 255, 255, 0.95);
|
box-shadow: 0 14px 30px rgba(28, 53, 84, 0.08);
|
overflow: hidden;
|
}
|
.panel-head {
|
padding: 14px 16px 10px;
|
border-bottom: 1px solid #e4ebf3;
|
background: rgba(249, 251, 255, 0.92);
|
}
|
.panel-head-title {
|
font-size: 15px;
|
font-weight: 700;
|
color: #24384d;
|
}
|
.panel-head-sub {
|
margin-top: 4px;
|
font-size: 12px;
|
color: #7a8ea4;
|
line-height: 1.5;
|
}
|
.panel-body {
|
flex: 1 1 auto;
|
min-height: 0;
|
overflow: auto;
|
padding: 12px;
|
}
|
.scene-card {
|
border-radius: 14px;
|
border: 1px solid #e2eaf4;
|
padding: 12px;
|
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
|
box-shadow: 0 10px 22px rgba(16, 43, 72, 0.06);
|
cursor: pointer;
|
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
}
|
.scene-card + .scene-card {
|
margin-top: 10px;
|
}
|
.scene-card:hover {
|
transform: translateY(-2px);
|
box-shadow: 0 12px 24px rgba(16, 43, 72, 0.10);
|
}
|
.scene-card.active {
|
border-color: #84b5eb;
|
box-shadow: 0 14px 26px rgba(31, 111, 178, 0.15);
|
background: linear-gradient(180deg, #fdfefe 0%, #f4faff 100%);
|
}
|
.scene-head {
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 8px;
|
}
|
.scene-code {
|
margin-top: 4px;
|
font-size: 11px;
|
color: #8da0b5;
|
word-break: break-all;
|
font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
}
|
.scene-stats {
|
margin-top: 10px;
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 8px;
|
}
|
.mini-stat {
|
border-radius: 10px;
|
border: 1px solid #e7eef7;
|
padding: 8px 10px;
|
background: #fff;
|
}
|
.mini-stat .k {
|
font-size: 11px;
|
color: #7f92a8;
|
}
|
.mini-stat .v {
|
margin-top: 4px;
|
font-size: 18px;
|
font-weight: 700;
|
color: #2a3e55;
|
}
|
.version-toolbar {
|
display: flex;
|
gap: 8px;
|
flex-wrap: wrap;
|
margin-bottom: 10px;
|
}
|
.version-card {
|
border-radius: 14px;
|
border: 1px solid #e2eaf4;
|
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
|
box-shadow: 0 10px 20px rgba(18, 44, 74, 0.06);
|
padding: 12px;
|
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
|
}
|
.version-card + .version-card {
|
margin-top: 10px;
|
}
|
.version-card.active {
|
border-color: #77aee9;
|
box-shadow: 0 14px 26px rgba(31, 111, 178, 0.15);
|
transform: translateY(-1px);
|
}
|
.version-card.published {
|
background: linear-gradient(180deg, #fffef8 0%, #fffaf0 100%);
|
border-color: #f0d79f;
|
}
|
.version-head {
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 10px;
|
}
|
.version-title {
|
font-size: 14px;
|
font-weight: 700;
|
color: #25384c;
|
}
|
.version-meta {
|
margin-top: 6px;
|
font-size: 12px;
|
color: #74879d;
|
line-height: 1.7;
|
}
|
.version-actions {
|
margin-top: 10px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
.version-actions-left,
|
.version-actions-right {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
.editor-toolbar {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 8px;
|
flex-wrap: wrap;
|
margin-bottom: 12px;
|
}
|
.editor-toolbar-right {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
.editor-form {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 10px;
|
}
|
.editor-full {
|
grid-column: 1 / -1;
|
}
|
.field-label {
|
margin-bottom: 4px;
|
font-size: 12px;
|
color: #6f859d;
|
}
|
.block-textarea .el-textarea__inner {
|
min-height: 190px !important;
|
font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
font-size: 12px;
|
line-height: 1.7;
|
color: #243447;
|
background: #fbfdff;
|
border-color: #dbe7f3;
|
}
|
.preview-textarea .el-textarea__inner {
|
min-height: 260px !important;
|
font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
font-size: 12px;
|
line-height: 1.75;
|
color: #203246;
|
background: linear-gradient(180deg, #fcfdff 0%, #f7fbff 100%);
|
border-color: #d5e3f1;
|
}
|
.dynamic-context-card {
|
margin-top: 10px;
|
padding: 10px 12px;
|
border-radius: 12px;
|
border: 1px solid #e2ebf5;
|
background: linear-gradient(180deg, #fdfefe 0%, #f5fbfa 100%);
|
color: #5f7288;
|
font-size: 12px;
|
line-height: 1.75;
|
}
|
.editor-hint {
|
margin-top: 10px;
|
padding: 10px 12px;
|
border-radius: 12px;
|
border: 1px solid #e7edf6;
|
background: linear-gradient(180deg, #fcfdff 0%, #f7fbff 100%);
|
color: #5c7087;
|
font-size: 12px;
|
line-height: 1.7;
|
}
|
.editor-stats {
|
margin-top: 10px;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 8px;
|
flex-wrap: wrap;
|
color: #7e91a6;
|
font-size: 12px;
|
}
|
.empty-shell {
|
min-height: 220px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-direction: column;
|
border: 1px dashed #d1dbe8;
|
border-radius: 14px;
|
background: rgba(255, 255, 255, 0.6);
|
color: #7d8ea4;
|
text-align: center;
|
padding: 18px;
|
}
|
.empty-shell strong {
|
font-size: 14px;
|
color: #374b63;
|
margin-bottom: 6px;
|
}
|
@media (max-width: 1380px) {
|
.workspace {
|
grid-template-columns: 260px minmax(320px, 1fr);
|
}
|
.editor-panel {
|
grid-column: 1 / -1;
|
}
|
}
|
@media (max-width: 960px) {
|
.summary-grid {
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
}
|
.workspace {
|
grid-template-columns: 1fr;
|
min-height: auto;
|
}
|
.editor-form {
|
grid-template-columns: 1fr;
|
}
|
}
|
</style>
|
</head>
|
<body>
|
<div id="app" class="page-shell" v-cloak>
|
<div class="hero">
|
<div class="hero-top">
|
<div class="hero-title">
|
<div v-html="headerIcon" style="display:flex;"></div>
|
<div>
|
<div class="main">AI配置 - Prompt中心</div>
|
<div class="sub">按场景管理 Prompt 版本,支持草稿编辑、发布切换和快速复制优化</div>
|
</div>
|
</div>
|
<div class="hero-actions">
|
<el-button size="mini" @click="goLlmConfig">LLM配置</el-button>
|
<el-button size="mini" @click="restoreDefaults">补齐默认Prompt</el-button>
|
<el-button size="mini" @click="reloadAll">刷新</el-button>
|
</div>
|
</div>
|
<div class="summary-grid">
|
<div class="summary-card">
|
<div class="k">场景数</div>
|
<div class="v">{{ summary.sceneCount }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="k">版本总数</div>
|
<div class="v">{{ summary.totalTemplates }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="k">已发布</div>
|
<div class="v">{{ summary.publishedTemplates }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="k">草稿数</div>
|
<div class="v">{{ summary.draftTemplates }}</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="workspace">
|
<div class="panel">
|
<div class="panel-head">
|
<div class="panel-head-title">场景</div>
|
<div class="panel-head-sub">每个场景只会有一个已发布版本,诊断运行时直接读取它。</div>
|
</div>
|
<div class="panel-body" v-loading="loadingScenes">
|
<div v-if="!scenes.length" class="empty-shell">
|
<strong>暂无场景</strong>
|
<div>请先检查场景接口是否可用。</div>
|
</div>
|
<div v-else>
|
<div class="scene-card"
|
:class="{ active: selectedSceneCode === scene.code }"
|
v-for="scene in scenes"
|
:key="scene.code"
|
@click="selectScene(scene.code)">
|
<div class="scene-head">
|
<div>
|
<div style="font-size:14px;font-weight:700;color:#284059;">{{ scene.label }}</div>
|
<div class="scene-code">{{ scene.code }}</div>
|
</div>
|
<el-tag size="mini" :type="publishedTemplate(scene.code) ? 'success' : 'info'">
|
{{ publishedTemplate(scene.code) ? ('v' + publishedTemplate(scene.code).version) : '未发布' }}
|
</el-tag>
|
</div>
|
<div class="scene-stats">
|
<div class="mini-stat">
|
<div class="k">版本数</div>
|
<div class="v">{{ templatesByScene(scene.code).length }}</div>
|
</div>
|
<div class="mini-stat">
|
<div class="k">草稿数</div>
|
<div class="v">{{ draftCount(scene.code) }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="panel">
|
<div class="panel-head">
|
<div class="panel-head-title">版本列表</div>
|
<div class="panel-head-sub">
|
当前场景:{{ selectedSceneLabel }}
|
<span v-if="currentPublishedTemplate">,线上版本 v{{ currentPublishedTemplate.version }}</span>
|
</div>
|
</div>
|
<div class="panel-body" v-loading="loadingTemplates">
|
<div class="version-toolbar">
|
<el-button type="primary" size="mini" @click="createBlankDraft" :disabled="!selectedSceneCode">新建草稿</el-button>
|
<el-button size="mini" @click="createDraftFromPublished" :disabled="!currentPublishedTemplate">基于已发布新建</el-button>
|
</div>
|
<div v-if="!selectedTemplates.length" class="empty-shell">
|
<strong>当前场景还没有版本</strong>
|
<div>你可以直接新建草稿,或者先执行“补齐默认Prompt”。</div>
|
</div>
|
<div v-else>
|
<div class="version-card"
|
:class="{ active: isSelectedRow(row), published: row.published === 1 }"
|
v-for="row in selectedTemplates"
|
:key="rowKey(row)"
|
@click="openTemplate(row)">
|
<div class="version-head">
|
<div>
|
<div class="version-title">{{ displayTemplateTitle(row) }}</div>
|
<div class="version-meta">
|
{{ displayTemplateVersion(row) }}<br />
|
更新时间:{{ formatDateTime(row.updateTime) }}<br />
|
备注:{{ row.memo || '-' }}
|
</div>
|
</div>
|
<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;">
|
<el-tag size="mini" type="info" v-if="row.__unsaved === true">未保存</el-tag>
|
<el-tag size="mini" :type="row.published === 1 ? 'warning' : 'info'">
|
{{ row.published === 1 ? '已发布' : '草稿' }}
|
</el-tag>
|
<el-tag size="mini" :type="row.status === 1 ? 'success' : 'info'">
|
{{ row.status === 1 ? '启用' : '禁用' }}
|
</el-tag>
|
</div>
|
</div>
|
<div class="version-actions">
|
<div class="version-actions-left">
|
<el-button type="text" size="mini" @click.stop="openTemplate(row)">{{ row.__unsaved === true ? '继续编辑' : '查看' }}</el-button>
|
<el-button type="text" size="mini" @click.stop="cloneFromTemplate(row)">复制成草稿</el-button>
|
</div>
|
<div class="version-actions-right">
|
<el-button type="text" size="mini" @click.stop="publishTemplate(row)" :disabled="row.published === 1">发布</el-button>
|
<el-button type="text" size="mini" @click.stop="cancelPublish(row)" v-if="row.published === 1">取消发布</el-button>
|
<el-button type="text" size="mini" style="color:#F56C6C;" @click.stop="deleteTemplate(row)" :disabled="row.published === 1">删除</el-button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="panel editor-panel">
|
<div class="panel-head">
|
<div class="panel-head-title">编辑器</div>
|
<div class="panel-head-sub">{{ editorModeText }}</div>
|
</div>
|
<div class="panel-body" v-loading="saving">
|
<div class="editor-toolbar">
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
<el-tag size="mini" :type="editor.published === 1 ? 'warning' : 'info'">
|
{{ editor.published === 1 ? '来源:已发布版本' : (editor.id ? '来源:草稿' : '来源:新建') }}
|
</el-tag>
|
<el-tag size="mini" :type="editor.status === 1 ? 'success' : 'info'">
|
{{ editor.status === 1 ? '启用' : '禁用' }}
|
</el-tag>
|
</div>
|
<div class="editor-toolbar-right">
|
<el-button size="mini" @click="resetEditor">清空编辑器</el-button>
|
<el-button type="primary" size="mini" @click="saveEditor(false)" :disabled="!editor.sceneCode">保存</el-button>
|
<el-button type="warning" size="mini" @click="publishTemplate()" :disabled="!editor.sceneCode">发布</el-button>
|
<el-button size="mini" @click="cancelPublish()" v-if="editor.published === 1 && editor.id">取消发布</el-button>
|
</div>
|
</div>
|
|
<div class="editor-form">
|
<div>
|
<div class="field-label">场景</div>
|
<el-select v-model="editor.sceneCode" size="mini" style="width:100%;" placeholder="请选择场景">
|
<el-option v-for="scene in scenes" :key="scene.code" :label="scene.label + '(' + scene.code + ')'" :value="scene.code"></el-option>
|
</el-select>
|
</div>
|
<div>
|
<div class="field-label">Prompt名称</div>
|
<el-input v-model="editor.name" size="mini" placeholder="例如:WCS专家问答 v3"></el-input>
|
</div>
|
<div>
|
<div class="field-label">状态</div>
|
<el-switch v-model="editor.status" :active-value="1" :inactive-value="0"></el-switch>
|
</div>
|
<div>
|
<div class="field-label">备注</div>
|
<el-input v-model="editor.memo" size="mini" placeholder="记录这次优化目的或测试结论"></el-input>
|
</div>
|
<div>
|
<div class="field-label">Base Policy</div>
|
<el-input
|
class="block-textarea"
|
type="textarea"
|
v-model="editor.basePolicy"
|
placeholder="角色、底线规则、稳定约束"
|
:autosize="{ minRows: 8, maxRows: 14 }"></el-input>
|
</div>
|
<div>
|
<div class="field-label">Tool Policy</div>
|
<el-input
|
class="block-textarea"
|
type="textarea"
|
v-model="editor.toolPolicy"
|
placeholder="什么时候必须查 MCP、怎么使用工具和引用证据"
|
:autosize="{ minRows: 8, maxRows: 14 }"></el-input>
|
</div>
|
<div class="editor-full">
|
<div class="field-label">Output Contract</div>
|
<el-input
|
class="block-textarea"
|
type="textarea"
|
v-model="editor.outputContract"
|
placeholder="输出格式、结构、禁止事项、工程化要求"
|
:autosize="{ minRows: 7, maxRows: 12 }"></el-input>
|
</div>
|
<div class="editor-full">
|
<div class="field-label">Scene Playbook</div>
|
<el-input
|
class="block-textarea"
|
type="textarea"
|
v-model="editor.scenePlaybook"
|
placeholder="场景策略,比如任务不执行、设备异常、人工问答等"
|
:autosize="{ minRows: 10, maxRows: 16 }"></el-input>
|
</div>
|
<div class="editor-full">
|
<div class="field-label">组装预览</div>
|
<el-input
|
class="preview-textarea"
|
type="textarea"
|
:value="assembledPromptPreview"
|
readonly
|
:autosize="{ minRows: 12, maxRows: 18 }"></el-input>
|
</div>
|
<div class="editor-full">
|
<div class="dynamic-context-card">
|
<strong>Dynamic Context</strong><br />
|
这一层不在这里持久化维护。运行时仍由请求实时注入,比如:用户问题、告警描述、重点设备、日志范围、extraContext,以及 Agent 后续通过 MCP 拉到的实时事实数据。
|
</div>
|
</div>
|
</div>
|
|
<div class="editor-stats">
|
<div>
|
字符数 {{ contentCharCount }} · 行数 {{ contentLineCount }}
|
</div>
|
<div>
|
当前场景线上版本:{{ currentPublishedTemplate ? ('v' + currentPublishedTemplate.version + ' / ' + (currentPublishedTemplate.name || '-')) : '暂无' }}
|
</div>
|
</div>
|
|
<div class="editor-hint">
|
<div v-if="editor.published === 1">
|
当前载入的是已发布版本。现在不允许直接保存修改,请先点击“取消发布”,再保存当前版本。
|
</div>
|
<div v-else-if="editor.id">
|
当前正在编辑草稿 v{{ editor.version || '-' }}。保存只会更新这份草稿,发布后它会替换当前场景的线上版本。
|
</div>
|
<div v-else>
|
当前是新草稿。你现在编辑的是 4 个持久化层:Base Policy、Tool Policy、Output Contract、Scene Playbook;Dynamic Context 继续由运行时注入。
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
|
<script type="text/javascript" src="../../static/vue/element/element.js"></script>
|
<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
|
<script>
|
new Vue({
|
el: '#app',
|
data: function() {
|
return {
|
headerIcon: getAiIconHtml(34, 34),
|
loadingScenes: false,
|
loadingTemplates: false,
|
saving: false,
|
scenes: [],
|
templates: [],
|
selectedSceneCode: '',
|
selectedTemplateId: null,
|
editor: {
|
id: null,
|
name: '',
|
sceneCode: '',
|
version: null,
|
content: '',
|
basePolicy: '',
|
toolPolicy: '',
|
outputContract: '',
|
scenePlaybook: '',
|
status: 1,
|
published: 0,
|
memo: '',
|
publishedTime: null,
|
updateTime: null,
|
__localKey: null,
|
__unsaved: false
|
}
|
};
|
},
|
computed: {
|
summary: function() {
|
var published = 0;
|
var drafts = 0;
|
for (var i = 0; i < this.templates.length; i++) {
|
if (this.templates[i].published === 1) published++;
|
else drafts++;
|
}
|
return {
|
sceneCount: this.scenes.length,
|
totalTemplates: this.templates.length,
|
publishedTemplates: published,
|
draftTemplates: drafts
|
};
|
},
|
selectedSceneLabel: function() {
|
for (var i = 0; i < this.scenes.length; i++) {
|
if (this.scenes[i].code === this.selectedSceneCode) {
|
return this.scenes[i].label;
|
}
|
}
|
return this.selectedSceneCode || '未选择';
|
},
|
selectedTemplates: function() {
|
var list = this.templatesByScene(this.selectedSceneCode).slice().sort(function(a, b) {
|
var av = a && a.version ? a.version : 0;
|
var bv = b && b.version ? b.version : 0;
|
return bv - av;
|
});
|
if (this.hasUnsavedEditorForScene(this.selectedSceneCode)) {
|
list.unshift(this.buildUnsavedTemplateCard());
|
}
|
return list;
|
},
|
currentPublishedTemplate: function() {
|
return this.publishedTemplate(this.selectedSceneCode);
|
},
|
editorModeText: function() {
|
if (!this.editor.sceneCode) {
|
return '先从左侧选择场景,或者直接新建草稿。';
|
}
|
if (this.editor.published === 1) {
|
return '当前是已发布版本,4 层内容只能查看;若要修改,先取消发布。';
|
}
|
if (this.editor.id) {
|
return '正在编辑已有草稿。修改的是 Prompt 的 4 个持久化层,保存不会影响线上版本。';
|
}
|
return '当前是新草稿。你可以分层编辑 Prompt,再通过右侧预览检查最终拼接效果。';
|
},
|
assembledPromptPreview: function() {
|
return this.composePromptPreview(this.editor);
|
},
|
contentCharCount: function() {
|
return this.assembledPromptPreview ? this.assembledPromptPreview.length : 0;
|
},
|
contentLineCount: function() {
|
if (!this.assembledPromptPreview) return 0;
|
return this.assembledPromptPreview.split(/\r?\n/).length;
|
}
|
},
|
methods: {
|
emptyEditor: function() {
|
return {
|
id: null,
|
name: '',
|
sceneCode: '',
|
version: null,
|
content: '',
|
basePolicy: '',
|
toolPolicy: '',
|
outputContract: '',
|
scenePlaybook: '',
|
status: 1,
|
published: 0,
|
memo: '',
|
publishedTime: null,
|
updateTime: null,
|
__localKey: null,
|
__unsaved: false
|
};
|
},
|
authHeaders: function() {
|
return { token: localStorage.getItem('token') };
|
},
|
requestJson: function(url, options) {
|
var self = this;
|
var opts = options || {};
|
opts.headers = Object.assign({}, opts.headers || {}, self.authHeaders());
|
return fetch(url, opts)
|
.then(function(resp) { return resp.json(); })
|
.then(function(res) {
|
if (res && res.code === 403) {
|
top.location.href = baseUrl + '/';
|
throw new Error('未授权');
|
}
|
return res;
|
});
|
},
|
formatDateTime: function(input) {
|
if (!input) return '-';
|
var d = input instanceof Date ? input : new Date(input);
|
if (isNaN(d.getTime())) return String(input);
|
var pad = function(n) { return n < 10 ? ('0' + n) : String(n); };
|
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' '
|
+ pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
|
},
|
normalizeTemplate: function(item) {
|
return {
|
id: item && item.id != null ? item.id : null,
|
name: item && item.name ? item.name : '',
|
sceneCode: item && item.sceneCode ? item.sceneCode : '',
|
version: item && item.version != null ? item.version : null,
|
content: item && item.content ? item.content : '',
|
basePolicy: item && item.basePolicy ? item.basePolicy : '',
|
toolPolicy: item && item.toolPolicy ? item.toolPolicy : '',
|
outputContract: item && item.outputContract ? item.outputContract : '',
|
scenePlaybook: item && item.scenePlaybook ? item.scenePlaybook : '',
|
status: item && item.status != null ? item.status : 1,
|
published: item && item.published != null ? item.published : 0,
|
memo: item && item.memo ? item.memo : '',
|
publishedTime: item ? item.publishedTime : null,
|
updateTime: item ? item.updateTime : null,
|
__localKey: item && item.__localKey ? item.__localKey : null,
|
__unsaved: item && item.__unsaved === true
|
};
|
},
|
rowKey: function(row) {
|
if (!row) return 'row_empty';
|
if (row.id != null) return 'db_' + row.id;
|
return row.__localKey || 'draft_local';
|
},
|
isSelectedRow: function(row) {
|
if (!row) return false;
|
if (row.id != null) {
|
return this.selectedTemplateId === row.id;
|
}
|
return row.__localKey && this.editor && this.editor.__localKey === row.__localKey;
|
},
|
displayTemplateTitle: function(row) {
|
if (!row) return '';
|
if (row.name) return row.name;
|
if (row.version != null) return this.selectedSceneLabel + ' v' + row.version;
|
return this.selectedSceneLabel + ' 未保存草稿';
|
},
|
displayTemplateVersion: function(row) {
|
if (!row) return '-';
|
if (row.version != null) {
|
return '版本 v' + row.version;
|
}
|
return '版本号将在保存后生成';
|
},
|
buildLocalDraftKey: function() {
|
return 'draft_' + Date.now() + '_' + Math.floor(Math.random() * 100000);
|
},
|
composePromptPreview: function(editor) {
|
var sections = [];
|
var append = function(label, content) {
|
var value = content == null ? '' : String(content).trim();
|
if (!value) return;
|
sections.push('【' + label + '】\n' + value);
|
};
|
append('基础策略', editor && editor.basePolicy);
|
append('工具策略', editor && editor.toolPolicy);
|
append('输出约定', editor && editor.outputContract);
|
append('场景策略', editor && editor.scenePlaybook);
|
return sections.join('\n\n');
|
},
|
hasUnsavedEditorForScene: function(sceneCode) {
|
return !!(this.editor
|
&& this.editor.sceneCode
|
&& this.editor.sceneCode === sceneCode
|
&& this.editor.id == null
|
&& this.editor.__unsaved === true);
|
},
|
buildUnsavedTemplateCard: function() {
|
return this.normalizeTemplate(this.editor);
|
},
|
templatesByScene: function(sceneCode) {
|
var code = sceneCode || '';
|
return this.templates.filter(function(x) {
|
return x.sceneCode === code;
|
});
|
},
|
publishedTemplate: function(sceneCode) {
|
var list = this.templatesByScene(sceneCode);
|
for (var i = 0; i < list.length; i++) {
|
if (list[i].published === 1 && list[i].status === 1) {
|
return list[i];
|
}
|
}
|
for (var j = 0; j < list.length; j++) {
|
if (list[j].published === 1) {
|
return list[j];
|
}
|
}
|
return null;
|
},
|
draftCount: function(sceneCode) {
|
var list = this.templatesByScene(sceneCode);
|
var count = 0;
|
for (var i = 0; i < list.length; i++) {
|
if (list[i].published !== 1) count++;
|
}
|
return count;
|
},
|
goLlmConfig: function() {
|
window.location.href = 'llm_config.html';
|
},
|
reloadAll: function() {
|
var self = this;
|
self.loadScenes().then(function() {
|
return self.loadTemplates();
|
});
|
},
|
loadScenes: function() {
|
var self = this;
|
self.loadingScenes = true;
|
return self.requestJson(baseUrl + '/ai/prompt/template/sceneList/auth')
|
.then(function(res) {
|
self.loadingScenes = false;
|
if (!res || res.code !== 200) {
|
self.$message.error((res && res.msg) ? res.msg : '场景加载失败');
|
return;
|
}
|
self.scenes = Array.isArray(res.data) ? res.data : [];
|
if (!self.selectedSceneCode && self.scenes.length) {
|
self.selectedSceneCode = self.scenes[0].code;
|
}
|
})
|
.catch(function() {
|
self.loadingScenes = false;
|
self.$message.error('场景加载失败');
|
});
|
},
|
loadTemplates: function() {
|
var self = this;
|
self.loadingTemplates = true;
|
return self.requestJson(baseUrl + '/ai/prompt/template/list/auth')
|
.then(function(res) {
|
self.loadingTemplates = false;
|
if (!res || res.code !== 200) {
|
self.$message.error((res && res.msg) ? res.msg : '版本加载失败');
|
return;
|
}
|
var rows = Array.isArray(res.data) ? res.data : [];
|
self.templates = rows.map(self.normalizeTemplate);
|
self.syncSelectionAfterReload();
|
})
|
.catch(function() {
|
self.loadingTemplates = false;
|
self.$message.error('版本加载失败');
|
});
|
},
|
syncSelectionAfterReload: function() {
|
if (!this.selectedSceneCode && this.scenes.length) {
|
this.selectedSceneCode = this.scenes[0].code;
|
}
|
if (!this.selectedSceneCode) {
|
this.resetEditor();
|
return;
|
}
|
if (this.selectedTemplateId) {
|
for (var i = 0; i < this.templates.length; i++) {
|
if (this.templates[i].id === this.selectedTemplateId) {
|
this.editor = this.normalizeTemplate(this.templates[i]);
|
return;
|
}
|
}
|
}
|
if (this.editor && !this.editor.id && this.editor.sceneCode === this.selectedSceneCode) {
|
return;
|
}
|
this.resetEditor();
|
},
|
selectScene: function(sceneCode) {
|
this.selectedSceneCode = sceneCode;
|
this.selectedTemplateId = null;
|
this.resetEditor();
|
},
|
openTemplate: function(row) {
|
if (!row) return;
|
if (row.id != null) {
|
this.selectedTemplateId = row.id;
|
this.editor = this.normalizeTemplate(row);
|
return;
|
}
|
this.selectedTemplateId = null;
|
this.editor = this.normalizeTemplate(row);
|
},
|
resetEditor: function() {
|
var editor = this.emptyEditor();
|
editor.sceneCode = this.selectedSceneCode || '';
|
editor.status = 1;
|
this.editor = editor;
|
this.selectedTemplateId = null;
|
},
|
createBlankDraft: function() {
|
if (!this.selectedSceneCode) {
|
this.$message.warning('请先选择场景');
|
return;
|
}
|
this.selectedTemplateId = null;
|
this.resetEditor();
|
this.editor.name = this.selectedSceneLabel + ' 新草稿';
|
this.editor.__localKey = this.buildLocalDraftKey();
|
this.editor.__unsaved = true;
|
this.editor.updateTime = new Date();
|
},
|
cloneFromTemplate: function(row) {
|
if (!row) return;
|
this.selectedSceneCode = row.sceneCode;
|
this.selectedTemplateId = null;
|
this.editor = this.normalizeTemplate(row);
|
this.editor.id = null;
|
this.editor.published = 0;
|
this.editor.version = null;
|
this.editor.name = (row.name || this.selectedSceneLabel) + ' - 草稿';
|
this.editor.__localKey = this.buildLocalDraftKey();
|
this.editor.__unsaved = true;
|
this.editor.updateTime = new Date();
|
},
|
createDraftFromPublished: function() {
|
if (!this.currentPublishedTemplate) {
|
this.$message.warning('当前场景暂无已发布版本');
|
return;
|
}
|
this.cloneFromTemplate(this.currentPublishedTemplate);
|
},
|
buildSavePayload: function() {
|
return {
|
id: this.editor.id,
|
name: this.editor.name,
|
sceneCode: this.editor.sceneCode,
|
basePolicy: this.editor.basePolicy,
|
toolPolicy: this.editor.toolPolicy,
|
outputContract: this.editor.outputContract,
|
scenePlaybook: this.editor.scenePlaybook,
|
status: this.editor.status,
|
memo: this.editor.memo
|
};
|
},
|
saveEditor: function(publishAfterSave) {
|
var self = this;
|
if (self.editor.published === 1) {
|
self.$alert('当前版本已经发布,请先取消发布后再保存。', '提示', {
|
confirmButtonText: '知道了',
|
type: 'warning'
|
});
|
return;
|
}
|
if (!self.editor.sceneCode) {
|
self.$message.warning('请选择场景');
|
return;
|
}
|
if (!self.assembledPromptPreview || !self.assembledPromptPreview.trim()) {
|
self.$message.warning('至少填写一个 Prompt 分段');
|
return;
|
}
|
self.saving = true;
|
self.requestJson(baseUrl + '/ai/prompt/template/save/auth', {
|
method: 'POST',
|
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify(self.buildSavePayload())
|
})
|
.then(function(res) {
|
if (!res || res.code !== 200) {
|
self.saving = false;
|
self.$message.error((res && res.msg) ? res.msg : '保存失败');
|
return;
|
}
|
var saved = self.normalizeTemplate(res.data || {});
|
self.selectedSceneCode = saved.sceneCode;
|
self.selectedTemplateId = saved.id;
|
self.editor = saved;
|
if (publishAfterSave === true && saved.id) {
|
self.publishTemplate(saved, true);
|
return;
|
}
|
self.saving = false;
|
self.$message.success('保存成功');
|
self.loadTemplates();
|
})
|
.catch(function() {
|
self.saving = false;
|
self.$message.error('保存失败');
|
});
|
},
|
publishTemplate: function(row, silentAfterSave) {
|
var self = this;
|
var target = row || self.editor;
|
if (target && target.published === 1) {
|
self.$message.warning('当前版本已经是已发布状态');
|
return;
|
}
|
if (!target || !target.id) {
|
self.saveEditor(true);
|
return;
|
}
|
self.requestJson(baseUrl + '/ai/prompt/template/publish/auth?id=' + encodeURIComponent(target.id), {
|
method: 'POST'
|
})
|
.then(function(res) {
|
self.saving = false;
|
if (!res || res.code !== 200) {
|
self.$message.error((res && res.msg) ? res.msg : '发布失败');
|
return;
|
}
|
var published = self.normalizeTemplate(res.data || {});
|
self.selectedSceneCode = published.sceneCode;
|
self.selectedTemplateId = published.id;
|
self.editor = published;
|
if (!silentAfterSave) {
|
self.$message.success('已发布到当前场景');
|
} else {
|
self.$message.success('保存并发布成功');
|
}
|
self.loadTemplates();
|
})
|
.catch(function() {
|
self.saving = false;
|
self.$message.error('发布失败');
|
});
|
},
|
cancelPublish: function(row) {
|
var self = this;
|
var target = row || self.editor;
|
if (!target || !target.id) {
|
self.$message.warning('当前没有可取消发布的版本');
|
return;
|
}
|
if (target.published !== 1) {
|
self.$message.warning('当前版本不是已发布状态');
|
return;
|
}
|
self.$confirm('取消发布后,该场景会暂时没有线上 Prompt,直到你重新发布一个版本。确定继续吗?', '提示', {
|
type: 'warning',
|
closeOnClickModal: false
|
}).then(function() {
|
self.requestJson(baseUrl + '/ai/prompt/template/cancelPublish/auth?id=' + encodeURIComponent(target.id), {
|
method: 'POST'
|
})
|
.then(function(res) {
|
if (!res || res.code !== 200) {
|
self.$message.error((res && res.msg) ? res.msg : '取消发布失败');
|
return;
|
}
|
var changed = self.normalizeTemplate(res.data || {});
|
if (self.editor && self.editor.id === changed.id) {
|
self.selectedTemplateId = changed.id;
|
self.editor = changed;
|
}
|
self.$message.success('已取消发布,现在可以保存当前版本了');
|
self.loadTemplates();
|
})
|
.catch(function() {
|
self.$message.error('取消发布失败');
|
});
|
}).catch(function() {});
|
},
|
deleteTemplate: function(row) {
|
var self = this;
|
if (!row) return;
|
if (!row.id) {
|
if (row.__localKey && self.editor && self.editor.__localKey === row.__localKey) {
|
self.resetEditor();
|
self.$message.success('未保存草稿已移除');
|
}
|
return;
|
}
|
if (row.published === 1) {
|
self.$message.warning('已发布版本不能删除');
|
return;
|
}
|
self.$confirm('确定删除这个草稿版本吗?', '提示', { type: 'warning' }).then(function() {
|
self.requestJson(baseUrl + '/ai/prompt/template/delete/auth?id=' + encodeURIComponent(row.id), {
|
method: 'POST'
|
})
|
.then(function(res) {
|
if (res && res.code === 200) {
|
if (self.selectedTemplateId === row.id) {
|
self.selectedTemplateId = null;
|
self.resetEditor();
|
}
|
self.$message.success('删除成功');
|
self.loadTemplates();
|
} else {
|
self.$message.error((res && res.msg) ? res.msg : '删除失败');
|
}
|
})
|
.catch(function() {
|
self.$message.error('删除失败');
|
});
|
}).catch(function() {});
|
},
|
restoreDefaults: function() {
|
var self = this;
|
self.requestJson(baseUrl + '/ai/prompt/template/initDefaults/auth', {
|
method: 'POST'
|
})
|
.then(function(res) {
|
if (!res || res.code !== 200) {
|
self.$message.error((res && res.msg) ? res.msg : '补齐失败');
|
return;
|
}
|
self.$message.success('默认Prompt已检查并补齐');
|
self.reloadAll();
|
})
|
.catch(function() {
|
self.$message.error('补齐失败');
|
});
|
}
|
},
|
mounted: function() {
|
var self = this;
|
if (window.WCS_I18N && typeof window.WCS_I18N.onReady === 'function') {
|
window.WCS_I18N.onReady(function() {
|
self.$forceUpdate();
|
});
|
}
|
self.reloadAll();
|
}
|
});
|
</script>
|
</body>
|
</html>
|