🎨 优化预设正则解析
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"/src/view/ai/binding/index.vue": "Index",
|
||||
"/src/view/ai/model/index.vue": "Index",
|
||||
"/src/view/ai/preset/components/PromptEditor.vue": "PromptEditor",
|
||||
"/src/view/ai/preset/components/RegexEditor.vue": "RegexEditor",
|
||||
"/src/view/ai/preset/index.vue": "Index",
|
||||
"/src/view/ai/provider/index.vue": "Index",
|
||||
"/src/view/dashboard/components/banner.vue": "Banner",
|
||||
|
||||
526
web/src/view/ai/preset/components/RegexEditor.vue
Normal file
526
web/src/view/ai/preset/components/RegexEditor.vue
Normal file
@@ -0,0 +1,526 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="正则脚本编辑器"
|
||||
width="1200px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="regex-editor">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" size="small" @click="addScript">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加脚本
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="importScripts">
|
||||
<el-icon><Upload /></el-icon>
|
||||
导入脚本
|
||||
</el-button>
|
||||
<el-button type="warning" size="small" @click="testRegex" :disabled="!currentScript">
|
||||
<el-icon><View /></el-icon>
|
||||
测试当前脚本
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="editor-content">
|
||||
<div class="script-list">
|
||||
<div class="list-header">
|
||||
<span>脚本列表 ({{ scripts.length }})</span>
|
||||
</div>
|
||||
<el-scrollbar height="500px">
|
||||
<div
|
||||
v-for="(script, index) in scripts"
|
||||
:key="script.id || index"
|
||||
:class="['script-item', { active: currentIndex === index, disabled: script.disabled }]"
|
||||
@click="selectScript(index)"
|
||||
>
|
||||
<div class="script-info">
|
||||
<div class="script-name">
|
||||
<el-icon v-if="script.disabled"><CircleClose /></el-icon>
|
||||
<el-icon v-else><CircleCheck /></el-icon>
|
||||
{{ script.scriptName || '未命名脚本' }}
|
||||
</div>
|
||||
<div class="script-placement">
|
||||
<el-tag v-if="script.placement?.includes(1)" size="small" type="info">输入前</el-tag>
|
||||
<el-tag v-if="script.placement?.includes(2)" size="small" type="success">输出后</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="script-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="duplicateScript(index)"
|
||||
>
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="deleteScript(index)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<div class="script-editor" v-if="currentScript">
|
||||
<el-form :model="currentScript" label-width="120px" label-position="left">
|
||||
<el-form-item label="脚本名称">
|
||||
<el-input v-model="currentScript.scriptName" placeholder="请输入脚本名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="正则表达式">
|
||||
<el-input
|
||||
v-model="currentScript.findRegex"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入正则表达式,如: /<pattern>/flags"
|
||||
/>
|
||||
<div class="hint">支持 JavaScript 正则表达式格式,如: /pattern/gi</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="替换字符串">
|
||||
<el-input
|
||||
v-model="currentScript.replaceString"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="替换内容,支持 $1, $2 等捕获组引用"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="执行位置">
|
||||
<el-checkbox-group v-model="currentScript.placement">
|
||||
<el-checkbox :value="1">用户输入前处理</el-checkbox>
|
||||
<el-checkbox :value="2">AI输出后处理</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="应用深度">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-input-number
|
||||
v-model="currentScript.minDepth"
|
||||
placeholder="最小深度"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="hint">最小深度 (null=不限制)</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-input-number
|
||||
v-model="currentScript.maxDepth"
|
||||
placeholder="最大深度"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="hint">最大深度 (null=不限制)</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选项">
|
||||
<el-checkbox v-model="currentScript.disabled">禁用此脚本</el-checkbox>
|
||||
<el-checkbox v-model="currentScript.markdownOnly">仅处理 Markdown</el-checkbox>
|
||||
<el-checkbox v-model="currentScript.promptOnly">仅处理提示词</el-checkbox>
|
||||
<el-checkbox v-model="currentScript.runOnEdit">编辑时运行</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="修剪字符串">
|
||||
<el-select
|
||||
v-model="currentScript.trimStrings"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
placeholder="输入要修剪的字符串"
|
||||
style="width: 100%"
|
||||
>
|
||||
</el-select>
|
||||
<div class="hint">在应用正则前要删除的字符串列表</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<el-empty description="请选择或创建一个脚本" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 测试对话框 -->
|
||||
<el-dialog v-model="testDialogVisible" title="测试正则脚本" width="800px" append-to-body>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="测试文本">
|
||||
<el-input
|
||||
v-model="testInput"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="输入要测试的文本"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="匹配结果">
|
||||
<el-input
|
||||
v-model="testOutput"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
readonly
|
||||
placeholder="替换后的结果将显示在这里"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="testDialogVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="runTest">执行测试</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Upload,
|
||||
View,
|
||||
Delete,
|
||||
CopyDocument,
|
||||
CircleCheck,
|
||||
CircleClose
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
regexScripts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const scripts = ref([])
|
||||
const currentIndex = ref(-1)
|
||||
const testDialogVisible = ref(false)
|
||||
const testInput = ref('')
|
||||
const testOutput = ref('')
|
||||
|
||||
const currentScript = computed(() => {
|
||||
if (currentIndex.value >= 0 && currentIndex.value < scripts.value.length) {
|
||||
return scripts.value[currentIndex.value]
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
scripts.value = JSON.parse(JSON.stringify(props.regexScripts || []))
|
||||
if (scripts.value.length > 0) {
|
||||
currentIndex.value = 0
|
||||
} else {
|
||||
currentIndex.value = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const generateId = () => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
const addScript = () => {
|
||||
const newScript = {
|
||||
id: generateId(),
|
||||
scriptName: '新建脚本',
|
||||
findRegex: '',
|
||||
replaceString: '',
|
||||
trimStrings: [],
|
||||
placement: [2],
|
||||
disabled: false,
|
||||
markdownOnly: false,
|
||||
promptOnly: false,
|
||||
runOnEdit: false,
|
||||
substituteRegex: 0,
|
||||
minDepth: null,
|
||||
maxDepth: null
|
||||
}
|
||||
scripts.value.push(newScript)
|
||||
currentIndex.value = scripts.value.length - 1
|
||||
ElMessage.success('已添加新脚本')
|
||||
}
|
||||
|
||||
const selectScript = (index) => {
|
||||
currentIndex.value = index
|
||||
}
|
||||
|
||||
const duplicateScript = (index) => {
|
||||
const original = scripts.value[index]
|
||||
const duplicate = {
|
||||
...JSON.parse(JSON.stringify(original)),
|
||||
id: generateId(),
|
||||
scriptName: original.scriptName + ' (副本)'
|
||||
}
|
||||
scripts.value.splice(index + 1, 0, duplicate)
|
||||
currentIndex.value = index + 1
|
||||
ElMessage.success('已复制脚本')
|
||||
}
|
||||
|
||||
const deleteScript = (index) => {
|
||||
ElMessageBox.confirm('确定要删除这个脚本吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
scripts.value.splice(index, 1)
|
||||
if (currentIndex.value >= scripts.value.length) {
|
||||
currentIndex.value = scripts.value.length - 1
|
||||
}
|
||||
ElMessage.success('已删除脚本')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const importScripts = () => {
|
||||
ElMessageBox.prompt('请粘贴正则脚本 JSON 数组', '导入脚本', {
|
||||
confirmButtonText: '导入',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'textarea'
|
||||
}).then(({ value }) => {
|
||||
try {
|
||||
const imported = JSON.parse(value)
|
||||
if (Array.isArray(imported)) {
|
||||
imported.forEach(script => {
|
||||
if (!script.id) {
|
||||
script.id = generateId()
|
||||
}
|
||||
})
|
||||
scripts.value.push(...imported)
|
||||
ElMessage.success(`成功导入 ${imported.length} 个脚本`)
|
||||
} else {
|
||||
ElMessage.error('导入的数据必须是数组格式')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('JSON 解析失败: ' + error.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const testRegex = () => {
|
||||
if (!currentScript.value) {
|
||||
ElMessage.warning('请先选择一个脚本')
|
||||
return
|
||||
}
|
||||
testInput.value = ''
|
||||
testOutput.value = ''
|
||||
testDialogVisible.value = true
|
||||
}
|
||||
|
||||
const runTest = () => {
|
||||
if (!testInput.value) {
|
||||
ElMessage.warning('请输入测试文本')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
let text = testInput.value
|
||||
|
||||
// 应用 trimStrings
|
||||
if (currentScript.value.trimStrings && currentScript.value.trimStrings.length > 0) {
|
||||
currentScript.value.trimStrings.forEach(trim => {
|
||||
text = text.split(trim).join('')
|
||||
})
|
||||
}
|
||||
|
||||
// 解析正则表达式
|
||||
let regex
|
||||
const regexStr = currentScript.value.findRegex
|
||||
|
||||
// 尝试解析 /pattern/flags 格式
|
||||
const match = regexStr.match(/^\/(.+)\/([gimsuvy]*)$/)
|
||||
if (match) {
|
||||
regex = new RegExp(match[1], match[2])
|
||||
} else {
|
||||
// 直接作为正则表达式
|
||||
regex = new RegExp(regexStr, 'g')
|
||||
}
|
||||
|
||||
// 执行替换
|
||||
testOutput.value = text.replace(regex, currentScript.value.replaceString || '')
|
||||
|
||||
ElMessage.success('测试完成')
|
||||
} catch (error) {
|
||||
ElMessage.error('正则表达式错误: ' + error.message)
|
||||
testOutput.value = '错误: ' + error.message
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
// 验证脚本
|
||||
for (let i = 0; i < scripts.value.length; i++) {
|
||||
const script = scripts.value[i]
|
||||
if (!script.scriptName) {
|
||||
ElMessage.warning(`第 ${i + 1} 个脚本缺少名称`)
|
||||
currentIndex.value = i
|
||||
return
|
||||
}
|
||||
if (!script.findRegex) {
|
||||
ElMessage.warning(`脚本 "${script.scriptName}" 缺少正则表达式`)
|
||||
currentIndex.value = i
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
emit('save', scripts.value)
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.regex-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
height: 550px;
|
||||
}
|
||||
|
||||
.script-list {
|
||||
width: 280px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
padding: 10px 15px;
|
||||
background: #f5f7fa;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.script-item {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.script-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.script-item.active {
|
||||
background: #ecf5ff;
|
||||
border-left: 3px solid #409eff;
|
||||
}
|
||||
|
||||
.script-item.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.script-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.script-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.script-placement {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.script-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.script-item:hover .script-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.script-editor {
|
||||
flex: 1;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -185,6 +185,13 @@
|
||||
@save="handlePromptSave"
|
||||
/>
|
||||
|
||||
<!-- 正则脚本编辑器 -->
|
||||
<RegexEditor
|
||||
v-model="regexEditorVisible"
|
||||
:regex-scripts="formData.regex_scripts"
|
||||
@save="handleRegexSave"
|
||||
/>
|
||||
|
||||
<!-- 查看预设对话框 -->
|
||||
<el-dialog v-model="viewDialogVisible" title="预设详情" width="900px">
|
||||
<el-descriptions :column="2" border>
|
||||
@@ -253,6 +260,7 @@ import {
|
||||
importAiPresetFile
|
||||
} from '@/api/aiPreset'
|
||||
import PromptEditor from './components/PromptEditor.vue'
|
||||
import RegexEditor from './components/RegexEditor.vue'
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
@@ -271,6 +279,7 @@ const importTabActive = ref('file')
|
||||
const uploadRef = ref(null)
|
||||
const uploadFile = ref(null)
|
||||
const promptEditorVisible = ref(false)
|
||||
const regexEditorVisible = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
@@ -468,7 +477,12 @@ const handlePromptSave = (prompts) => {
|
||||
}
|
||||
|
||||
const openRegexEditor = () => {
|
||||
ElMessage.info('正则脚本编辑器功能开发中...')
|
||||
regexEditorVisible.value = true
|
||||
}
|
||||
|
||||
const handleRegexSave = (regexScripts) => {
|
||||
formData.value.regex_scripts = regexScripts
|
||||
ElMessage.success('正则脚本已更新')
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
@@ -76,9 +76,8 @@ export default ({ mode }) => {
|
||||
[process.env.VITE_BASE_API]: {
|
||||
// 需要代理的路径 例如 '/api'
|
||||
target: `${process.env.VITE_BASE_PATH}:${process.env.VITE_SERVER_PORT}/`, // 代理到 目标路径
|
||||
changeOrigin: true,
|
||||
rewrite: (path) =>
|
||||
path.replace(new RegExp('^' + process.env.VITE_BASE_API), '')
|
||||
changeOrigin: true
|
||||
// 不需要 rewrite,保留 /api 前缀
|
||||
},
|
||||
"/plugin": {
|
||||
// 需要代理的路径 例如 '/api'
|
||||
|
||||
Reference in New Issue
Block a user