✨ 新增正则和扩展模块
This commit is contained in:
21
web-app-vue/package-lock.json
generated
21
web-app-vue/package-lock.json
generated
@@ -8,10 +8,12 @@
|
||||
"name": "web-app-vue",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"axios": "^1.13.5",
|
||||
"element-plus": "^2.13.2",
|
||||
"pinia": "^3.0.4",
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
@@ -1317,6 +1319,12 @@
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
@@ -2846,6 +2854,19 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"axios": "^1.13.5",
|
||||
"element-plus": "^2.13.2",
|
||||
"pinia": "^3.0.4",
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
|
||||
103
web-app-vue/src/api/extension.ts
Normal file
103
web-app-vue/src/api/extension.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import request from '@/utils/request'
|
||||
import type {
|
||||
Extension,
|
||||
ExtensionManifest,
|
||||
CreateExtensionRequest,
|
||||
UpdateExtensionRequest,
|
||||
ExtensionListParams,
|
||||
ExtensionListResponse,
|
||||
ToggleExtensionRequest,
|
||||
UpdateExtensionSettingsRequest,
|
||||
ExtensionStatsRequest,
|
||||
} from '@/types/extension'
|
||||
|
||||
// 创建/安装扩展
|
||||
export function createExtension(data: CreateExtensionRequest) {
|
||||
return request.post<Extension>('/app/extension', data)
|
||||
}
|
||||
|
||||
// 更新扩展
|
||||
export function updateExtension(id: number, data: UpdateExtensionRequest) {
|
||||
return request.put(`/app/extension/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除/卸载扩展
|
||||
export function deleteExtension(id: number, deleteFiles = false) {
|
||||
return request.delete(`/app/extension/${id}`, { params: { deleteFiles } })
|
||||
}
|
||||
|
||||
// 获取扩展详情
|
||||
export function getExtension(id: number) {
|
||||
return request.get<Extension>(`/app/extension/${id}`)
|
||||
}
|
||||
|
||||
// 获取扩展列表
|
||||
export function getExtensionList(params?: ExtensionListParams) {
|
||||
return request.get<ExtensionListResponse>('/app/extension/list', { params })
|
||||
}
|
||||
|
||||
// 获取启用的扩展列表
|
||||
export function getEnabledExtensions() {
|
||||
return request.get<Extension[]>('/app/extension/enabled')
|
||||
}
|
||||
|
||||
// 启用/禁用扩展
|
||||
export function toggleExtension(id: number, data: ToggleExtensionRequest) {
|
||||
return request.post(`/app/extension/${id}/toggle`, data)
|
||||
}
|
||||
|
||||
// 获取扩展配置
|
||||
export function getExtensionSettings(id: number) {
|
||||
return request.get<Record<string, any>>(`/app/extension/${id}/settings`)
|
||||
}
|
||||
|
||||
// 更新扩展配置
|
||||
export function updateExtensionSettings(id: number, data: UpdateExtensionSettingsRequest) {
|
||||
return request.put(`/app/extension/${id}/settings`, data)
|
||||
}
|
||||
|
||||
// 获取扩展 manifest
|
||||
export function getExtensionManifest(id: number) {
|
||||
return request.get<ExtensionManifest>(`/app/extension/${id}/manifest`)
|
||||
}
|
||||
|
||||
// 导入扩展
|
||||
export function importExtension(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request.post<Extension>('/app/extension/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 导出扩展
|
||||
export function exportExtension(id: number) {
|
||||
return request.get(`/app/extension/${id}/export`, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
}
|
||||
|
||||
// 更新扩展统计
|
||||
export function updateExtensionStats(data: ExtensionStatsRequest) {
|
||||
return request.post('/app/extension/stats', data)
|
||||
}
|
||||
|
||||
// 智能安装扩展(自动识别 Git URL 或 Manifest URL,兼容 SillyTavern)
|
||||
export function installExtensionFromUrl(url: string, branch = 'main') {
|
||||
return request.post<Extension>('/app/extension/install/url', { url, branch })
|
||||
}
|
||||
|
||||
// 从 Git URL 安装扩展
|
||||
export function installExtensionFromGit(gitUrl: string, branch = 'main') {
|
||||
return request.post<Extension>('/app/extension/install/git', {
|
||||
gitUrl,
|
||||
branch,
|
||||
})
|
||||
}
|
||||
|
||||
// 升级扩展版本(从源重新安装)
|
||||
export function upgradeExtension(id: number, force = false) {
|
||||
return request.post<Extension>(`/app/extension/${id}/update`, { force })
|
||||
}
|
||||
141
web-app-vue/src/api/regexScript.ts
Normal file
141
web-app-vue/src/api/regexScript.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import request from '@/utils/request'
|
||||
import type {
|
||||
RegexScript,
|
||||
RegexScriptListParams,
|
||||
RegexScriptListResponse,
|
||||
CreateRegexScriptRequest,
|
||||
UpdateRegexScriptRequest,
|
||||
TestRegexScriptRequest,
|
||||
TestRegexScriptResponse,
|
||||
ApplyRegexScriptsRequest,
|
||||
ApplyRegexScriptsResponse,
|
||||
RegexScriptExportData,
|
||||
} from '@/types/regexScript'
|
||||
|
||||
// 创建正则脚本
|
||||
export function createRegexScript(data: CreateRegexScriptRequest) {
|
||||
return request<RegexScript>({
|
||||
url: '/app/regex',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 更新正则脚本
|
||||
export function updateRegexScript(id: number, data: UpdateRegexScriptRequest) {
|
||||
return request({
|
||||
url: `/app/regex/${id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 删除正则脚本
|
||||
export function deleteRegexScript(id: number) {
|
||||
return request({
|
||||
url: `/app/regex/${id}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
// 获取正则脚本详情
|
||||
export function getRegexScript(id: number) {
|
||||
return request<RegexScript>({
|
||||
url: `/app/regex/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 获取正则脚本列表
|
||||
export function getRegexScriptList(params?: RegexScriptListParams) {
|
||||
return request<RegexScriptListResponse>({
|
||||
url: '/app/regex',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
// 复制正则脚本
|
||||
export function duplicateRegexScript(id: number) {
|
||||
return request<RegexScript>({
|
||||
url: `/app/regex/${id}/duplicate`,
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
// 关联角色到正则脚本
|
||||
export function linkCharactersToRegex(id: number, characterIds: number[]) {
|
||||
return request({
|
||||
url: `/app/regex/${id}/link`,
|
||||
method: 'post',
|
||||
data: { characterIds },
|
||||
})
|
||||
}
|
||||
|
||||
// 获取角色的正则脚本
|
||||
export function getCharacterRegexScripts(characterId: number) {
|
||||
return request<RegexScript[]>({
|
||||
url: `/app/regex/character/${characterId}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 测试正则脚本
|
||||
export function testRegexScript(data: TestRegexScriptRequest) {
|
||||
return request<TestRegexScriptResponse>({
|
||||
url: '/app/regex/test',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 应用正则脚本
|
||||
export function applyRegexScripts(data: ApplyRegexScriptsRequest) {
|
||||
return request<ApplyRegexScriptsResponse>({
|
||||
url: '/app/regex/apply',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 导入正则脚本
|
||||
export function importRegexScripts(file: File, overwriteMode: string = 'skip') {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('overwriteMode', overwriteMode)
|
||||
|
||||
return request({
|
||||
url: '/app/regex/import',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 导出正则脚本
|
||||
export function exportRegexScripts(scriptIds?: number[]) {
|
||||
const params = scriptIds && scriptIds.length > 0 ? { scriptIds: scriptIds.join(',') } : {}
|
||||
|
||||
return request<RegexScriptExportData>({
|
||||
url: '/app/regex/export',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
// 下载正则脚本(JSON 文件)
|
||||
export function downloadRegexScriptsJSON(scriptIds?: number[]) {
|
||||
return exportRegexScripts(scriptIds).then((data) => {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `regex_scripts_${Date.now()}.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
})
|
||||
}
|
||||
18
web-app-vue/src/components.d.ts
vendored
18
web-app-vue/src/components.d.ts
vendored
@@ -11,13 +11,19 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
@@ -27,19 +33,25 @@ declare module 'vue' {
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElStatistic: typeof import('element-plus/es')['ElStatistic']
|
||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
ExtensionDrawer: typeof import('./components/ExtensionDrawer.vue')['default']
|
||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
740
web-app-vue/src/components/ExtensionDrawer.vue
Normal file
740
web-app-vue/src/components/ExtensionDrawer.vue
Normal file
@@ -0,0 +1,740 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
title="扩展管理"
|
||||
:size="600"
|
||||
direction="rtl"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="extension-drawer-content">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索扩展..."
|
||||
clearable
|
||||
size="small"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-bar">
|
||||
<el-button size="small" @click="showInstallDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
安装扩展
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleUpdateAll"
|
||||
:disabled="!hasUpdatableExtensions"
|
||||
>
|
||||
<el-icon><Refresh /></el-icon>
|
||||
全部更新
|
||||
</el-button>
|
||||
<el-button size="small" @click="handleRefresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 扩展折叠面板 -->
|
||||
<div class="extension-list" v-loading="extensionStore.loading">
|
||||
<el-empty v-if="filteredExtensions.length === 0" description="暂无扩展" />
|
||||
|
||||
<el-collapse v-else v-model="activeNames">
|
||||
<el-collapse-item
|
||||
v-for="ext in filteredExtensions"
|
||||
:key="ext.id"
|
||||
:name="ext.id"
|
||||
class="extension-item"
|
||||
>
|
||||
<!-- 扩展头部 -->
|
||||
<template #title>
|
||||
<div class="extension-header" @click.stop>
|
||||
<el-switch
|
||||
v-model="ext.isEnabled"
|
||||
size="default"
|
||||
@change="handleToggle(ext)"
|
||||
style="margin-right: 12px"
|
||||
/>
|
||||
<div class="extension-title">
|
||||
<span class="name">{{ ext.displayName || ext.name }}</span>
|
||||
<el-tag v-if="ext.isSystemExt" size="small" type="info" style="margin-left: 8px">
|
||||
系统
|
||||
</el-tag>
|
||||
<el-tag :type="getRunningStatus(ext).type" size="small" style="margin-left: 8px">
|
||||
{{ getRunningStatus(ext).text }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 扩展内容 -->
|
||||
<div class="extension-content">
|
||||
<div class="extension-description" v-if="ext.description">
|
||||
<p>{{ ext.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 扩展配置 -->
|
||||
<el-divider content-position="left">配置</el-divider>
|
||||
|
||||
<!-- 自定义配置容器(扩展可以在运行时注入 UI) -->
|
||||
<div
|
||||
:id="`extension-settings-${ext.name}`"
|
||||
class="extension-custom-settings"
|
||||
:data-extension-id="ext.id"
|
||||
:data-extension-name="ext.name"
|
||||
>
|
||||
<!-- 扩展的自定义配置 UI 将动态注入到这里 -->
|
||||
</div>
|
||||
|
||||
<!-- 基于 manifest schema 的配置表单(兜底方案) -->
|
||||
<el-form label-position="top" size="small">
|
||||
<template v-for="(setting, key) in getSettingsSchema(ext)" :key="key">
|
||||
<!-- Text 输入框 -->
|
||||
<el-form-item
|
||||
v-if="setting.type === 'text' || !setting.type"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-input
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
:placeholder="setting.placeholder"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
/>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- Number 数字输入 -->
|
||||
<el-form-item
|
||||
v-else-if="setting.type === 'number'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
:min="setting.min"
|
||||
:max="setting.max"
|
||||
:step="setting.step || 1"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- Boolean 开关 -->
|
||||
<el-form-item
|
||||
v-else-if="setting.type === 'boolean'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-switch
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
/>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- Select 下拉选择 -->
|
||||
<el-form-item
|
||||
v-else-if="setting.type === 'select'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-select
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in setting.options"
|
||||
:key="option.value || option"
|
||||
:label="option.label || option"
|
||||
:value="option.value || option"
|
||||
/>
|
||||
</el-select>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- Textarea 文本域 -->
|
||||
<el-form-item
|
||||
v-else-if="setting.type === 'textarea'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-input
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
type="textarea"
|
||||
:rows="setting.rows || 3"
|
||||
:placeholder="setting.placeholder"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
/>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- Slider 滑块 -->
|
||||
<el-form-item
|
||||
v-else-if="setting.type === 'slider'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-slider
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
:min="setting.min || 0"
|
||||
:max="setting.max || 100"
|
||||
:step="setting.step || 1"
|
||||
:show-input="true"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
/>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 没有配置项 -->
|
||||
<el-empty
|
||||
v-if="Object.keys(getSettingsSchema(ext)).length === 0"
|
||||
description="此扩展没有可配置项"
|
||||
:image-size="60"
|
||||
/>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="extension-actions">
|
||||
<el-button
|
||||
v-if="ext.isEnabled && getRunningStatus(ext).isRunning"
|
||||
size="small"
|
||||
type="warning"
|
||||
@click="handleReload(ext)"
|
||||
>
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
重新加载
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="ext.sourceUrl && (ext.installSource === 'git' || ext.installSource === 'url')"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleUpdate(ext)"
|
||||
>
|
||||
<el-icon><Refresh /></el-icon>
|
||||
更新
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!ext.isSystemExt"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(ext)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
卸载
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 安装扩展对话框 -->
|
||||
<el-dialog
|
||||
v-model="showInstallDialog"
|
||||
title="安装扩展"
|
||||
width="500px"
|
||||
append-to-body
|
||||
>
|
||||
<el-tabs v-model="installTab">
|
||||
<el-tab-pane label="上传文件" name="file">
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:show-file-list="true"
|
||||
:limit="1"
|
||||
accept=".json"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将 manifest.json 拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="URL 安装" name="url">
|
||||
<el-form :model="installForm" label-width="120px">
|
||||
<el-form-item label="URL">
|
||||
<el-input
|
||||
v-model="installForm.url"
|
||||
placeholder="Git 仓库 URL 或 manifest.json URL"
|
||||
>
|
||||
<template #append>
|
||||
<el-tooltip content="支持 Git 仓库 URL(如 GitHub、GitLab)或直接的 manifest.json URL" placement="top">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="分支(可选)">
|
||||
<el-input
|
||||
v-model="installForm.branch"
|
||||
placeholder="main"
|
||||
>
|
||||
<template #append>
|
||||
<el-tooltip content="Git 仓库的分支名称,默认为 main" placement="top">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
<template #title>
|
||||
<div style="font-size: 12px;">
|
||||
示例:<br />
|
||||
• https://github.com/user/extension.git<br />
|
||||
• https://gitlab.com/user/extension<br />
|
||||
• https://example.com/manifest.json
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showInstallDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleInstall" :loading="installing">
|
||||
安装
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useExtensionStore } from '@/stores/extension'
|
||||
import { extensionRuntime } from '@/utils/extensionRuntime'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
Refresh,
|
||||
RefreshRight,
|
||||
Delete,
|
||||
UploadFilled,
|
||||
QuestionFilled,
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { Extension } from '@/types/extension'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}>()
|
||||
|
||||
// Store
|
||||
const extensionStore = useExtensionStore()
|
||||
|
||||
// State
|
||||
const drawerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val),
|
||||
})
|
||||
|
||||
const searchKeyword = ref('')
|
||||
const activeNames = ref<number[]>([])
|
||||
const extensionSettings = reactive<Record<number, Record<string, any>>>({})
|
||||
const showInstallDialog = ref(false)
|
||||
const installTab = ref('file')
|
||||
const installing = ref(false)
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const installForm = reactive({
|
||||
url: '',
|
||||
branch: 'main',
|
||||
})
|
||||
|
||||
// Computed
|
||||
const filteredExtensions = computed(() => {
|
||||
let extensions = extensionStore.extensions
|
||||
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
extensions = extensions.filter(ext =>
|
||||
(ext.name?.toLowerCase().includes(keyword)) ||
|
||||
(ext.displayName?.toLowerCase().includes(keyword)) ||
|
||||
(ext.description?.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
return extensions
|
||||
})
|
||||
|
||||
// 是否有可更新的扩展
|
||||
const hasUpdatableExtensions = computed(() => {
|
||||
return extensionStore.extensions.some(ext =>
|
||||
ext.sourceUrl && (ext.installSource === 'git' || ext.installSource === 'url')
|
||||
)
|
||||
})
|
||||
|
||||
// Methods
|
||||
const getSettingsSchema = (ext: Extension) => {
|
||||
try {
|
||||
const manifest = typeof ext.manifestData === 'string'
|
||||
? JSON.parse(ext.manifestData)
|
||||
: ext.manifestData
|
||||
return manifest?.settings || {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const getRunningStatus = (ext: Extension) => {
|
||||
const instance = extensionRuntime.getInstance(ext.id)
|
||||
if (!ext.isEnabled) {
|
||||
return { text: '已禁用', type: 'info', isRunning: false }
|
||||
}
|
||||
if (instance?.isRunning) {
|
||||
return { text: '运行中', type: 'success', isRunning: true }
|
||||
}
|
||||
if (instance?.isLoaded) {
|
||||
return { text: '已加载', type: 'warning', isRunning: false }
|
||||
}
|
||||
return { text: '未加载', type: 'danger', isRunning: false }
|
||||
}
|
||||
|
||||
const handleToggle = async (ext: Extension) => {
|
||||
try {
|
||||
// 传递当前的启用状态
|
||||
await extensionStore.toggleExtension(ext.id, ext.isEnabled)
|
||||
|
||||
// 根据状态加载/卸载扩展
|
||||
if (ext.isEnabled) {
|
||||
const success = await extensionRuntime.enableExtension(ext)
|
||||
if (!success) {
|
||||
ext.isEnabled = false
|
||||
ElMessage.error('启用扩展失败')
|
||||
} else {
|
||||
ElMessage.success('扩展已启用并运行')
|
||||
}
|
||||
} else {
|
||||
await extensionRuntime.disableExtension(ext.id)
|
||||
ElMessage.success('扩展已禁用')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换扩展状态失败:', error)
|
||||
// 恢复状态
|
||||
ext.isEnabled = !ext.isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
const handleSettingChange = async (extId: number, key: string) => {
|
||||
try {
|
||||
await extensionStore.updateExtensionSettings(extId, {
|
||||
[key]: extensionSettings[extId][key],
|
||||
})
|
||||
|
||||
// 通知扩展配置已更改
|
||||
extensionRuntime.emitEvent('settingsChanged', {
|
||||
extensionId: extId,
|
||||
key,
|
||||
value: extensionSettings[extId][key],
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('保存设置失败:', error)
|
||||
ElMessage.error('保存设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleReload = async (ext: Extension) => {
|
||||
try {
|
||||
await extensionRuntime.reloadExtension(ext)
|
||||
ElMessage.success('扩展已重新加载')
|
||||
} catch (error) {
|
||||
console.error('重新加载失败:', error)
|
||||
ElMessage.error('重新加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = async (ext: Extension) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要更新扩展 "${ext.displayName || ext.name}" 吗?这将从原始安装源重新下载扩展。`,
|
||||
'确认更新',
|
||||
{
|
||||
type: 'info',
|
||||
confirmButtonText: '更新',
|
||||
cancelButtonText: '取消',
|
||||
}
|
||||
)
|
||||
|
||||
// 更新扩展
|
||||
await extensionStore.upgradeExtension(ext.id)
|
||||
|
||||
// 如果扩展正在运行,重新加载
|
||||
if (ext.isEnabled) {
|
||||
await extensionRuntime.reloadExtension(ext)
|
||||
}
|
||||
|
||||
ElMessage.success('扩展已更新')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('更新失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (ext: Extension) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要卸载扩展 "${ext.displayName || ext.name}" 吗?`,
|
||||
'确认卸载',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
// 先卸载运行时
|
||||
await extensionRuntime.unloadExtension(ext.id)
|
||||
|
||||
// 再删除数据
|
||||
await extensionStore.deleteExtension(ext.id)
|
||||
|
||||
ElMessage.success('扩展已卸载')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('卸载失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
extensionStore.fetchExtensionList()
|
||||
}
|
||||
|
||||
// 批量更新所有扩展
|
||||
const handleUpdateAll = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要更新所有可更新的扩展吗?',
|
||||
'批量更新',
|
||||
{
|
||||
type: 'info',
|
||||
confirmButtonText: '更新',
|
||||
cancelButtonText: '取消',
|
||||
}
|
||||
)
|
||||
|
||||
const updatableExtensions = extensionStore.extensions.filter(ext =>
|
||||
ext.sourceUrl && (ext.installSource === 'git' || ext.installSource === 'url')
|
||||
)
|
||||
|
||||
if (updatableExtensions.length === 0) {
|
||||
ElMessage.info('没有可更新的扩展')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.info(`开始更新 ${updatableExtensions.length} 个扩展...`)
|
||||
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
for (const ext of updatableExtensions) {
|
||||
try {
|
||||
await extensionStore.upgradeExtension(ext.id)
|
||||
successCount++
|
||||
} catch (error) {
|
||||
console.error(`更新扩展 ${ext.name} 失败:`, error)
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (failCount === 0) {
|
||||
ElMessage.success(`成功更新 ${successCount} 个扩展`)
|
||||
} else {
|
||||
ElMessage.warning(`更新完成:成功 ${successCount} 个,失败 ${failCount} 个`)
|
||||
}
|
||||
|
||||
// 重新加载所有启用的扩展
|
||||
await extensionStore.fetchExtensionList()
|
||||
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('批量更新失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileChange = (file: any) => {
|
||||
selectedFile.value = file.raw
|
||||
}
|
||||
|
||||
const handleInstall = async () => {
|
||||
try {
|
||||
installing.value = true
|
||||
|
||||
if (installTab.value === 'file') {
|
||||
if (!selectedFile.value) {
|
||||
ElMessage.warning('请选择 manifest.json 文件')
|
||||
return
|
||||
}
|
||||
await extensionStore.importExtension(selectedFile.value)
|
||||
} else {
|
||||
if (!installForm.url) {
|
||||
ElMessage.warning('请输入 Manifest URL')
|
||||
return
|
||||
}
|
||||
|
||||
// 从 URL 安装扩展(支持分支参数)
|
||||
await installFromURL(installForm.url, installForm.branch || 'main')
|
||||
}
|
||||
|
||||
showInstallDialog.value = false
|
||||
selectedFile.value = null
|
||||
installForm.url = ''
|
||||
installForm.branch = 'main'
|
||||
} catch (error) {
|
||||
console.error('安装失败:', error)
|
||||
} finally {
|
||||
installing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 从 URL 安装扩展(智能识别 Git URL 或 Manifest URL)
|
||||
const installFromURL = async (url: string, branch = 'main') => {
|
||||
try {
|
||||
// 验证 URL 格式
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
ElMessage.error('请输入有效的 HTTP(S) URL')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.info('正在安装扩展...')
|
||||
|
||||
// 调用后端智能安装 API(自动识别 Git URL 或 Manifest URL)
|
||||
await extensionStore.installExtensionFromUrl(url, branch)
|
||||
|
||||
ElMessage.success('扩展安装成功')
|
||||
} catch (error: any) {
|
||||
console.error('安装失败:', error)
|
||||
ElMessage.error(error.message || '安装失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 监听扩展列表变化,初始化设置
|
||||
watch(
|
||||
() => extensionStore.extensions,
|
||||
(extensions) => {
|
||||
extensions.forEach(ext => {
|
||||
if (!extensionSettings[ext.id]) {
|
||||
extensionSettings[ext.id] = {}
|
||||
}
|
||||
|
||||
// 加载已保存的设置
|
||||
if (ext.settings) {
|
||||
const settings = typeof ext.settings === 'string'
|
||||
? JSON.parse(ext.settings)
|
||||
: ext.settings
|
||||
Object.assign(extensionSettings[ext.id], settings)
|
||||
}
|
||||
|
||||
// 如果扩展已启用,自动加载
|
||||
if (ext.isEnabled && !extensionRuntime.getInstance(ext.id)) {
|
||||
extensionRuntime.enableExtension(ext)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
// 当抽屉打开时刷新列表
|
||||
watch(drawerVisible, (visible) => {
|
||||
if (visible) {
|
||||
handleRefresh()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.extension-drawer-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.extension-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.extension-item {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.extension-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.extension-title {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extension-content {
|
||||
.extension-description {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-hint {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.extension-actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,26 @@
|
||||
<el-icon><Files /></el-icon>
|
||||
<span>我的角色卡</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="authStore.isLoggedIn" index="/worldbook">
|
||||
<el-icon><Reading /></el-icon>
|
||||
<span>世界书</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="authStore.isLoggedIn" index="/regex">
|
||||
<el-icon><MagicStick /></el-icon>
|
||||
<span>正则脚本</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
<!-- 扩展快捷按钮 -->
|
||||
<el-button
|
||||
v-if="authStore.isLoggedIn"
|
||||
text
|
||||
@click="extensionDrawerVisible = true"
|
||||
class="extension-menu-button"
|
||||
>
|
||||
<el-icon><Connection /></el-icon>
|
||||
<span>扩展</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
@@ -58,25 +77,34 @@
|
||||
<el-main class="layout-main">
|
||||
<router-view />
|
||||
</el-main>
|
||||
|
||||
<!-- 全局扩展抽屉 -->
|
||||
<ExtensionDrawer v-model:visible="extensionDrawerVisible" />
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { Grid, Files } from '@element-plus/icons-vue'
|
||||
import { Grid, Files, Reading, Connection, MagicStick } from '@element-plus/icons-vue'
|
||||
import ExtensionDrawer from '@/components/ExtensionDrawer.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 扩展抽屉状态
|
||||
const extensionDrawerVisible = ref(false)
|
||||
|
||||
// 初始化用户信息
|
||||
authStore.initUserInfo()
|
||||
|
||||
// 当前激活的菜单
|
||||
const activeMenu = computed(() => {
|
||||
if (route.path.startsWith('/my-characters')) return '/my-characters'
|
||||
if (route.path.startsWith('/worldbook')) return '/worldbook'
|
||||
if (route.path.startsWith('/regex')) return '/regex'
|
||||
return '/'
|
||||
})
|
||||
|
||||
@@ -138,6 +166,15 @@ function handleCommand(command: string) {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.extension-menu-button {
|
||||
margin-left: 12px;
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
|
||||
@@ -58,6 +58,54 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/character/Edit.vue'),
|
||||
meta: { title: '编辑角色卡', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'worldbook',
|
||||
name: 'WorldBookList',
|
||||
component: () => import('@/views/worldbook/WorldBookList.vue'),
|
||||
meta: { title: '世界书管理', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'worldbook/create',
|
||||
name: 'CreateWorldBook',
|
||||
component: () => import('@/views/worldbook/WorldBookEdit.vue'),
|
||||
meta: { title: '创建世界书', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'worldbook/edit/:id',
|
||||
name: 'EditWorldBook',
|
||||
component: () => import('@/views/worldbook/WorldBookEdit.vue'),
|
||||
meta: { title: '编辑世界书', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'extension',
|
||||
name: 'ExtensionList',
|
||||
component: () => import('@/views/extension/ExtensionListNew.vue'),
|
||||
meta: { title: '扩展管理', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'extension/settings/:id',
|
||||
name: 'ExtensionSettings',
|
||||
component: () => import('@/views/extension/ExtensionSettings.vue'),
|
||||
meta: { title: '扩展配置', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'regex',
|
||||
name: 'RegexScriptList',
|
||||
component: () => import('@/views/regex/RegexScriptList.vue'),
|
||||
meta: { title: '正则脚本管理', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'regex/create',
|
||||
name: 'CreateRegexScript',
|
||||
component: () => import('@/views/regex/RegexScriptEdit.vue'),
|
||||
meta: { title: '创建正则脚本', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'regex/edit/:id',
|
||||
name: 'EditRegexScript',
|
||||
component: () => import('@/views/regex/RegexScriptEdit.vue'),
|
||||
meta: { title: '编辑正则脚本', requiresAuth: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
368
web-app-vue/src/stores/extension.ts
Normal file
368
web-app-vue/src/stores/extension.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as extensionApi from '@/api/extension'
|
||||
import type {
|
||||
Extension,
|
||||
ExtensionListParams,
|
||||
CreateExtensionRequest,
|
||||
UpdateExtensionRequest,
|
||||
} from '@/types/extension'
|
||||
|
||||
export const useExtensionStore = defineStore('extension', () => {
|
||||
// 状态
|
||||
const extensions = ref<Extension[]>([])
|
||||
const enabledExtensions = ref<Extension[]>([])
|
||||
const currentExtension = ref<Extension | null>(null)
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const loading = ref(false)
|
||||
|
||||
// 过滤条件
|
||||
const filters = ref<ExtensionListParams>({
|
||||
extensionType: '',
|
||||
category: '',
|
||||
isEnabled: undefined,
|
||||
tag: '',
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取扩展列表
|
||||
*/
|
||||
const fetchExtensionList = async (params?: ExtensionListParams) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const requestParams: ExtensionListParams = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
...filters.value,
|
||||
...params,
|
||||
}
|
||||
|
||||
const response = await extensionApi.getExtensionList(requestParams)
|
||||
extensions.value = response.data.list
|
||||
total.value = response.data.total
|
||||
currentPage.value = response.data.page
|
||||
pageSize.value = response.data.pageSize
|
||||
} catch (error: any) {
|
||||
console.error('获取扩展列表失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取扩展列表失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的扩展列表
|
||||
*/
|
||||
const fetchEnabledExtensions = async () => {
|
||||
try {
|
||||
const response = await extensionApi.getEnabledExtensions()
|
||||
enabledExtensions.value = response.data
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('获取启用扩展列表失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展详情
|
||||
*/
|
||||
const fetchExtension = async (id: number) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await extensionApi.getExtension(id)
|
||||
currentExtension.value = response.data
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('获取扩展详情失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取扩展详情失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建/安装扩展
|
||||
*/
|
||||
const createExtension = async (data: CreateExtensionRequest) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await extensionApi.createExtension(data)
|
||||
ElMessage.success('安装成功')
|
||||
await fetchExtensionList()
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('创建扩展失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '创建扩展失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新扩展
|
||||
*/
|
||||
const updateExtension = async (id: number, data: UpdateExtensionRequest) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await extensionApi.updateExtension(id, data)
|
||||
ElMessage.success('更新成功')
|
||||
await fetchExtensionList()
|
||||
} catch (error: any) {
|
||||
console.error('更新扩展失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '更新扩展失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除/卸载扩展
|
||||
*/
|
||||
const deleteExtension = async (id: number, deleteFiles = false) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await extensionApi.deleteExtension(id, deleteFiles)
|
||||
ElMessage.success('卸载成功')
|
||||
await fetchExtensionList()
|
||||
} catch (error: any) {
|
||||
console.error('删除扩展失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '删除扩展失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用扩展
|
||||
*/
|
||||
const toggleExtension = async (id: number, isEnabled: boolean) => {
|
||||
try {
|
||||
await extensionApi.toggleExtension(id, { isEnabled })
|
||||
ElMessage.success(isEnabled ? '启用成功' : '禁用成功')
|
||||
await fetchExtensionList()
|
||||
await fetchEnabledExtensions()
|
||||
} catch (error: any) {
|
||||
console.error('切换扩展状态失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '操作失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展配置
|
||||
*/
|
||||
const getExtensionSettings = async (id: number) => {
|
||||
try {
|
||||
const response = await extensionApi.getExtensionSettings(id)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('获取扩展配置失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取扩展配置失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新扩展配置
|
||||
*/
|
||||
const updateExtensionSettings = async (id: number, settings: Record<string, any>) => {
|
||||
try {
|
||||
await extensionApi.updateExtensionSettings(id, { settings })
|
||||
ElMessage.success('配置更新成功')
|
||||
} catch (error: any) {
|
||||
console.error('更新扩展配置失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '更新配置失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展 manifest
|
||||
*/
|
||||
const getExtensionManifest = async (id: number) => {
|
||||
try {
|
||||
const response = await extensionApi.getExtensionManifest(id)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('获取扩展 manifest 失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入扩展
|
||||
*/
|
||||
const importExtension = async (file: File) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await extensionApi.importExtension(file)
|
||||
ElMessage.success('导入成功')
|
||||
await fetchExtensionList()
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('导入扩展失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '导入扩展失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出扩展
|
||||
*/
|
||||
const exportExtension = async (id: number, name: string) => {
|
||||
try {
|
||||
const response = await extensionApi.exportExtension(id)
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([response.data], { type: 'application/json' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${name}_manifest.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('导出成功')
|
||||
} catch (error: any) {
|
||||
console.error('导出扩展失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '导出扩展失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能安装扩展(自动识别 Git URL 或 Manifest URL)
|
||||
*/
|
||||
const installExtensionFromUrl = async (url: string, branch = 'main') => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await extensionApi.installExtensionFromUrl(url, branch)
|
||||
ElMessage.success('安装成功')
|
||||
await fetchExtensionList()
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('安装扩展失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || error.message || '安装扩展失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Git URL 安装扩展
|
||||
*/
|
||||
const installExtensionFromGit = async (gitUrl: string, branch = 'main') => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await extensionApi.installExtensionFromGit(gitUrl, branch)
|
||||
ElMessage.success('安装成功')
|
||||
await fetchExtensionList()
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('从 Git 安装扩展失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '安装扩展失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级扩展版本(从源重新安装)
|
||||
*/
|
||||
const upgradeExtension = async (id: number, force = false) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await extensionApi.upgradeExtension(id, force)
|
||||
ElMessage.success('升级成功')
|
||||
await fetchExtensionList()
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('升级扩展失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '升级扩展失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新扩展统计
|
||||
*/
|
||||
const updateStats = async (extensionId: number, action: 'usage' | 'error' | 'load', value = 1) => {
|
||||
try {
|
||||
await extensionApi.updateExtensionStats({ extensionId, action, value })
|
||||
} catch (error: any) {
|
||||
console.error('更新扩展统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过滤条件
|
||||
*/
|
||||
const setFilters = (newFilters: Partial<ExtensionListParams>) => {
|
||||
filters.value = { ...filters.value, ...newFilters }
|
||||
currentPage.value = 1
|
||||
fetchExtensionList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置过滤条件
|
||||
*/
|
||||
const resetFilters = () => {
|
||||
filters.value = {
|
||||
extensionType: '',
|
||||
category: '',
|
||||
isEnabled: undefined,
|
||||
tag: '',
|
||||
}
|
||||
currentPage.value = 1
|
||||
fetchExtensionList()
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
extensions,
|
||||
enabledExtensions,
|
||||
currentExtension,
|
||||
total,
|
||||
currentPage,
|
||||
pageSize,
|
||||
loading,
|
||||
filters,
|
||||
|
||||
// 操作
|
||||
fetchExtensionList,
|
||||
fetchEnabledExtensions,
|
||||
fetchExtension,
|
||||
createExtension,
|
||||
updateExtension,
|
||||
deleteExtension,
|
||||
toggleExtension,
|
||||
getExtensionSettings,
|
||||
updateExtensionSettings,
|
||||
getExtensionManifest,
|
||||
importExtension,
|
||||
exportExtension,
|
||||
installExtensionFromUrl,
|
||||
installExtensionFromGit,
|
||||
upgradeExtension,
|
||||
updateStats,
|
||||
setFilters,
|
||||
resetFilters,
|
||||
}
|
||||
})
|
||||
229
web-app-vue/src/stores/regexScript.ts
Normal file
229
web-app-vue/src/stores/regexScript.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as regexScriptApi from '@/api/regexScript'
|
||||
import type {
|
||||
RegexScript,
|
||||
RegexScriptListParams,
|
||||
CreateRegexScriptRequest,
|
||||
UpdateRegexScriptRequest,
|
||||
TestRegexScriptRequest,
|
||||
} from '@/types/regexScript'
|
||||
|
||||
export const useRegexScriptStore = defineStore('regexScript', () => {
|
||||
// 状态
|
||||
const regexScripts = ref<RegexScript[]>([])
|
||||
const currentRegexScript = ref<RegexScript | null>(null)
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
// 获取正则脚本列表
|
||||
const fetchRegexScriptList = async (params?: RegexScriptListParams) => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
const requestParams: RegexScriptListParams = {
|
||||
page: params?.page || currentPage.value,
|
||||
pageSize: params?.pageSize || pageSize.value,
|
||||
...params,
|
||||
}
|
||||
|
||||
const response = await regexScriptApi.getRegexScriptList(requestParams)
|
||||
regexScripts.value = response.data.list
|
||||
total.value = response.data.total
|
||||
currentPage.value = response.data.page
|
||||
pageSize.value = response.data.pageSize
|
||||
} catch (error: any) {
|
||||
console.error('获取正则脚本列表失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取正则脚本列表失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取正则脚本详情
|
||||
const fetchRegexScriptDetail = async (id: number): Promise<RegexScript> => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await regexScriptApi.getRegexScript(id)
|
||||
currentRegexScript.value = response.data
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('获取正则脚本详情失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取正则脚本详情失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建正则脚本
|
||||
const createRegexScript = async (data: CreateRegexScriptRequest): Promise<RegexScript> => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await regexScriptApi.createRegexScript(data)
|
||||
ElMessage.success('创建成功')
|
||||
await fetchRegexScriptList()
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('创建正则脚本失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '创建正则脚本失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新正则脚本
|
||||
const updateRegexScript = async (id: number, data: UpdateRegexScriptRequest) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await regexScriptApi.updateRegexScript(id, data)
|
||||
ElMessage.success('更新成功')
|
||||
await fetchRegexScriptList()
|
||||
} catch (error: any) {
|
||||
console.error('更新正则脚本失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '更新正则脚本失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除正则脚本
|
||||
const deleteRegexScript = async (id: number) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await regexScriptApi.deleteRegexScript(id)
|
||||
ElMessage.success('删除成功')
|
||||
await fetchRegexScriptList()
|
||||
} catch (error: any) {
|
||||
console.error('删除正则脚本失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '删除正则脚本失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制正则脚本
|
||||
const duplicateRegexScript = async (id: number): Promise<RegexScript> => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await regexScriptApi.duplicateRegexScript(id)
|
||||
ElMessage.success('复制成功')
|
||||
await fetchRegexScriptList()
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('复制正则脚本失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '复制正则脚本失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关联角色到脚本
|
||||
const linkCharacters = async (id: number, characterIds: number[]) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await regexScriptApi.linkCharactersToRegex(id, characterIds)
|
||||
ElMessage.success('关联成功')
|
||||
await fetchRegexScriptDetail(id)
|
||||
} catch (error: any) {
|
||||
console.error('关联角色失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '关联角色失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色的脚本
|
||||
const fetchCharacterRegexScripts = async (characterId: number): Promise<RegexScript[]> => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await regexScriptApi.getCharacterRegexScripts(characterId)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('获取角色脚本失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取角色脚本失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试正则脚本
|
||||
const testRegexScript = async (data: TestRegexScriptRequest) => {
|
||||
try {
|
||||
const response = await regexScriptApi.testRegexScript(data)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('测试正则脚本失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '测试正则脚本失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 导入正则脚本
|
||||
const importRegexScripts = async (file: File, overwriteMode: string = 'skip') => {
|
||||
try {
|
||||
loading.value = true
|
||||
await regexScriptApi.importRegexScripts(file, overwriteMode)
|
||||
ElMessage.success('导入成功')
|
||||
await fetchRegexScriptList()
|
||||
} catch (error: any) {
|
||||
console.error('导入正则脚本失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '导入正则脚本失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导出正则脚本
|
||||
const exportRegexScripts = async (scriptIds?: number[]) => {
|
||||
try {
|
||||
await regexScriptApi.downloadRegexScriptsJSON(scriptIds)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (error: any) {
|
||||
console.error('导出正则脚本失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '导出正则脚本失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 重置分页
|
||||
const resetPagination = () => {
|
||||
currentPage.value = 1
|
||||
pageSize.value = 20
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
regexScripts,
|
||||
currentRegexScript,
|
||||
loading,
|
||||
total,
|
||||
currentPage,
|
||||
pageSize,
|
||||
|
||||
// 方法
|
||||
fetchRegexScriptList,
|
||||
fetchRegexScriptDetail,
|
||||
createRegexScript,
|
||||
updateRegexScript,
|
||||
deleteRegexScript,
|
||||
duplicateRegexScript,
|
||||
linkCharacters,
|
||||
fetchCharacterRegexScripts,
|
||||
testRegexScript,
|
||||
importRegexScripts,
|
||||
exportRegexScripts,
|
||||
resetPagination,
|
||||
}
|
||||
})
|
||||
@@ -35,10 +35,10 @@ export const useWorldInfoStore = defineStore('worldInfo', () => {
|
||||
}
|
||||
|
||||
const response = await worldInfoApi.getWorldBookList(requestParams)
|
||||
worldBooks.value = response.data.data.list
|
||||
total.value = response.data.data.total
|
||||
currentPage.value = response.data.data.page
|
||||
pageSize.value = response.data.data.pageSize
|
||||
worldBooks.value = response.data.list
|
||||
total.value = response.data.total
|
||||
currentPage.value = response.data.page
|
||||
pageSize.value = response.data.pageSize
|
||||
} catch (error: any) {
|
||||
console.error('获取世界书列表失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取世界书列表失败')
|
||||
@@ -53,8 +53,8 @@ export const useWorldInfoStore = defineStore('worldInfo', () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await worldInfoApi.getWorldBook(id)
|
||||
currentWorldBook.value = response.data.data
|
||||
return response.data.data
|
||||
currentWorldBook.value = response.data
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('获取世界书详情失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取世界书详情失败')
|
||||
@@ -71,7 +71,7 @@ export const useWorldInfoStore = defineStore('worldInfo', () => {
|
||||
const response = await worldInfoApi.createWorldBook(data)
|
||||
ElMessage.success('创建成功')
|
||||
await fetchWorldBookList()
|
||||
return response.data.data
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('创建世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '创建世界书失败')
|
||||
@@ -120,7 +120,7 @@ export const useWorldInfoStore = defineStore('worldInfo', () => {
|
||||
const response = await worldInfoApi.duplicateWorldBook(id)
|
||||
ElMessage.success('复制成功')
|
||||
await fetchWorldBookList()
|
||||
return response.data.data
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('复制世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '复制世界书失败')
|
||||
@@ -207,8 +207,8 @@ export const useWorldInfoStore = defineStore('worldInfo', () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await worldInfoApi.getCharacterWorldBooks(characterId)
|
||||
characterWorldBooks.value = response.data.data
|
||||
return response.data.data
|
||||
characterWorldBooks.value = response.data
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('获取角色世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取角色世界书失败')
|
||||
@@ -225,7 +225,7 @@ export const useWorldInfoStore = defineStore('worldInfo', () => {
|
||||
const response = await worldInfoApi.importWorldBook(file, bookName)
|
||||
ElMessage.success('导入成功')
|
||||
await fetchWorldBookList()
|
||||
return response.data.data
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('导入世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '导入世界书失败')
|
||||
@@ -254,7 +254,7 @@ export const useWorldInfoStore = defineStore('worldInfo', () => {
|
||||
const matchWorldInfo = async (params: MatchWorldInfoRequest): Promise<MatchedWorldInfoEntry[]> => {
|
||||
try {
|
||||
const response = await worldInfoApi.matchWorldInfo(params)
|
||||
return response.data.data.entries
|
||||
return response.data.entries
|
||||
} catch (error: any) {
|
||||
console.error('匹配世界书失败:', error)
|
||||
return []
|
||||
|
||||
129
web-app-vue/src/types/extension.d.ts
vendored
Normal file
129
web-app-vue/src/types/extension.d.ts
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
// 扩展类型定义(兼容 SillyTavern Extension 规范)
|
||||
|
||||
export interface Extension {
|
||||
id: number
|
||||
userId: number
|
||||
name: string
|
||||
displayName: string
|
||||
version: string
|
||||
author: string
|
||||
description: string
|
||||
homepage: string
|
||||
repository: string
|
||||
license: string
|
||||
tags: string[]
|
||||
extensionType: 'ui' | 'server' | 'hybrid'
|
||||
category: 'utilities' | 'themes' | 'integrations' | 'tools' | string
|
||||
dependencies: Record<string, string>
|
||||
conflicts: string[]
|
||||
manifestData: Record<string, any>
|
||||
scriptPath: string
|
||||
stylePath: string
|
||||
assetsPaths: string[]
|
||||
settings: Record<string, any>
|
||||
options: Record<string, any>
|
||||
isEnabled: boolean
|
||||
isInstalled: boolean
|
||||
isSystemExt: boolean
|
||||
installSource: 'url' | 'git' | 'file' | 'marketplace'
|
||||
sourceUrl: string // 原始安装 URL(用于更新)
|
||||
branch: string // Git 分支
|
||||
autoUpdate: boolean // 是否自动更新
|
||||
lastUpdateCheck?: string // 最后检查更新时间
|
||||
availableVersion?: string // 可用的新版本
|
||||
installDate: string
|
||||
lastEnabled: string
|
||||
usageCount: number
|
||||
errorCount: number
|
||||
loadTime: number
|
||||
metadata: Record<string, any>
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface ExtensionManifest {
|
||||
name: string
|
||||
display_name?: string
|
||||
version: string
|
||||
description: string
|
||||
author: string
|
||||
homepage?: string
|
||||
repository?: string
|
||||
license?: string
|
||||
tags?: string[]
|
||||
type?: 'ui' | 'server' | 'hybrid'
|
||||
category?: string
|
||||
dependencies?: Record<string, string>
|
||||
conflicts?: string[]
|
||||
entry?: string
|
||||
style?: string
|
||||
assets?: string[]
|
||||
settings?: Record<string, any>
|
||||
options?: Record<string, any>
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface CreateExtensionRequest {
|
||||
name: string
|
||||
displayName?: string
|
||||
version?: string
|
||||
author?: string
|
||||
description?: string
|
||||
homepage?: string
|
||||
repository?: string
|
||||
license?: string
|
||||
tags?: string[]
|
||||
extensionType: 'ui' | 'server' | 'hybrid'
|
||||
category?: string
|
||||
dependencies?: Record<string, string>
|
||||
conflicts?: string[]
|
||||
manifestData: Record<string, any>
|
||||
scriptPath?: string
|
||||
stylePath?: string
|
||||
assetsPaths?: string[]
|
||||
settings?: Record<string, any>
|
||||
options?: Record<string, any>
|
||||
installSource?: string
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface UpdateExtensionRequest {
|
||||
displayName?: string
|
||||
description?: string
|
||||
settings?: Record<string, any>
|
||||
options?: Record<string, any>
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface ExtensionListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
name?: string
|
||||
extensionType?: string
|
||||
category?: string
|
||||
isEnabled?: boolean
|
||||
isInstalled?: boolean
|
||||
tag?: string
|
||||
}
|
||||
|
||||
export interface ExtensionListResponse {
|
||||
list: Extension[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ToggleExtensionRequest {
|
||||
isEnabled: boolean
|
||||
}
|
||||
|
||||
export interface UpdateExtensionSettingsRequest {
|
||||
settings: Record<string, any>
|
||||
}
|
||||
|
||||
export interface ExtensionStatsRequest {
|
||||
extensionId: number
|
||||
action: 'usage' | 'error' | 'load'
|
||||
value?: number
|
||||
}
|
||||
123
web-app-vue/src/types/regexScript.d.ts
vendored
Normal file
123
web-app-vue/src/types/regexScript.d.ts
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
// 正则脚本类型定义
|
||||
|
||||
export interface RegexScript {
|
||||
id: number
|
||||
userId: number
|
||||
scriptName: string
|
||||
description: string
|
||||
findRegex: string
|
||||
replaceString: string
|
||||
enabled: boolean
|
||||
isGlobal: boolean
|
||||
trimStrings: boolean
|
||||
onlyFormat: boolean
|
||||
runOnEdit: boolean
|
||||
substituteRegex: boolean
|
||||
minDepth?: number
|
||||
maxDepth?: number
|
||||
placement: string
|
||||
affectMinDepth?: number
|
||||
affectMaxDepth?: number
|
||||
linkedChars: string[]
|
||||
scriptData?: Record<string, any>
|
||||
usageCount: number
|
||||
lastUsedAt?: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface RegexScriptListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
scriptName?: string
|
||||
isGlobal?: boolean
|
||||
enabled?: boolean
|
||||
characterId?: number
|
||||
}
|
||||
|
||||
export interface RegexScriptListResponse {
|
||||
list: RegexScript[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface CreateRegexScriptRequest {
|
||||
scriptName: string
|
||||
description?: string
|
||||
findRegex: string
|
||||
replaceString?: string
|
||||
enabled?: boolean
|
||||
isGlobal?: boolean
|
||||
trimStrings?: boolean
|
||||
onlyFormat?: boolean
|
||||
runOnEdit?: boolean
|
||||
substituteRegex?: boolean
|
||||
minDepth?: number
|
||||
maxDepth?: number
|
||||
placement?: string
|
||||
affectMinDepth?: number
|
||||
affectMaxDepth?: number
|
||||
linkedChars?: string[]
|
||||
scriptData?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface UpdateRegexScriptRequest {
|
||||
scriptName?: string
|
||||
description?: string
|
||||
findRegex?: string
|
||||
replaceString?: string
|
||||
enabled?: boolean
|
||||
isGlobal?: boolean
|
||||
trimStrings?: boolean
|
||||
onlyFormat?: boolean
|
||||
runOnEdit?: boolean
|
||||
substituteRegex?: boolean
|
||||
minDepth?: number
|
||||
maxDepth?: number
|
||||
placement?: string
|
||||
affectMinDepth?: number
|
||||
affectMaxDepth?: number
|
||||
linkedChars?: string[]
|
||||
scriptData?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface TestRegexScriptRequest {
|
||||
findRegex: string
|
||||
replaceString?: string
|
||||
testInput: string
|
||||
trimStrings?: boolean
|
||||
substituteRegex?: boolean
|
||||
}
|
||||
|
||||
export interface TestRegexScriptResponse {
|
||||
success: boolean
|
||||
input: string
|
||||
output: string
|
||||
matchedCount: number
|
||||
matches: string[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ApplyRegexScriptsRequest {
|
||||
text: string
|
||||
regexIds?: number[]
|
||||
characterId?: number
|
||||
placement?: string
|
||||
minDepth?: number
|
||||
maxDepth?: number
|
||||
useGlobal?: boolean
|
||||
}
|
||||
|
||||
export interface ApplyRegexScriptsResponse {
|
||||
originalText: string
|
||||
processedText: string
|
||||
appliedCount: number
|
||||
appliedScripts: number[]
|
||||
}
|
||||
|
||||
export interface RegexScriptExportData {
|
||||
version: string
|
||||
scripts: RegexScript[]
|
||||
exportedAt: number
|
||||
}
|
||||
488
web-app-vue/src/utils/extensionRuntime.ts
Normal file
488
web-app-vue/src/utils/extensionRuntime.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
/**
|
||||
* SillyTavern 扩展运行时环境
|
||||
* 负责加载、运行和管理扩展的生命周期
|
||||
*/
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { Extension } from '@/types/extension'
|
||||
|
||||
interface ExtensionInstance {
|
||||
extension: Extension
|
||||
manifest: any
|
||||
scriptElement?: HTMLScriptElement
|
||||
styleElement?: HTMLLinkElement
|
||||
isLoaded: boolean
|
||||
isRunning: boolean
|
||||
api?: any
|
||||
}
|
||||
|
||||
class ExtensionRuntime {
|
||||
private instances: Map<number, ExtensionInstance> = new Map()
|
||||
private stAPI: any = null
|
||||
// 扩展设置存储(兼容原版 SillyTavern 的 extension_settings)
|
||||
private extensionSettings: Record<string, any> = {}
|
||||
|
||||
constructor() {
|
||||
this.initSillyTavernAPI()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展的配置容器 DOM 元素
|
||||
*/
|
||||
private getExtensionSettingsContainer(extensionName: string): HTMLElement | null {
|
||||
return document.getElementById(`extension-settings-${extensionName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载扩展的已保存设置
|
||||
*/
|
||||
private loadExtensionSettings(extensionName: string): any {
|
||||
if (!this.extensionSettings[extensionName]) {
|
||||
const key = `ext_settings_${extensionName}`
|
||||
const saved = localStorage.getItem(key)
|
||||
if (saved) {
|
||||
try {
|
||||
this.extensionSettings[extensionName] = JSON.parse(saved)
|
||||
} catch (e) {
|
||||
console.error(`[ExtensionRuntime] 加载设置失败: ${extensionName}`, e)
|
||||
this.extensionSettings[extensionName] = {}
|
||||
}
|
||||
} else {
|
||||
this.extensionSettings[extensionName] = {}
|
||||
}
|
||||
}
|
||||
return this.extensionSettings[extensionName]
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存扩展设置到 localStorage
|
||||
*/
|
||||
private saveExtensionSettingsToStorage(extensionName: string) {
|
||||
const key = `ext_settings_${extensionName}`
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(this.extensionSettings[extensionName] || {}))
|
||||
} catch (e) {
|
||||
console.error(`[ExtensionRuntime] 保存设置失败: ${extensionName}`, e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 SillyTavern API(兼容层)
|
||||
*/
|
||||
private initSillyTavernAPI() {
|
||||
// 创建全局 SillyTavern API 对象
|
||||
this.stAPI = {
|
||||
// 扩展管理
|
||||
extensions: {
|
||||
register: (name: string, init: Function) => {
|
||||
console.log(`[Extension] 注册扩展: ${name}`)
|
||||
// 执行扩展的初始化函数
|
||||
try {
|
||||
init()
|
||||
} catch (error) {
|
||||
console.error(`[Extension] 初始化失败: ${name}`, error)
|
||||
}
|
||||
},
|
||||
|
||||
// 获取扩展设置
|
||||
getSettings: (extName: string) => {
|
||||
// 从 localStorage 或 store 获取设置
|
||||
const settings = localStorage.getItem(`ext_settings_${extName}`)
|
||||
return settings ? JSON.parse(settings) : {}
|
||||
},
|
||||
|
||||
// 保存扩展设置
|
||||
saveSettings: (extName: string, settings: any) => {
|
||||
localStorage.setItem(`ext_settings_${extName}`, JSON.stringify(settings))
|
||||
// 触发设置变更事件
|
||||
this.emitEvent('extensionSettingsLoaded', { name: extName })
|
||||
},
|
||||
|
||||
// 获取扩展列表
|
||||
list: () => {
|
||||
return Array.from(this.instances.values()).map(inst => ({
|
||||
name: inst.extension.name,
|
||||
displayName: inst.extension.displayName,
|
||||
version: inst.extension.version,
|
||||
enabled: inst.extension.isEnabled,
|
||||
loaded: inst.isLoaded,
|
||||
running: inst.isRunning,
|
||||
}))
|
||||
},
|
||||
},
|
||||
|
||||
// UI 工具
|
||||
ui: {
|
||||
// 创建设置面板
|
||||
createSettings: (title: string, content: HTMLElement) => {
|
||||
console.log(`[Extension] 创建设置面板: ${title}`)
|
||||
// 可以在这里创建一个设置面板并添加到页面
|
||||
},
|
||||
|
||||
// 显示通知
|
||||
notify: (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
|
||||
ElMessage[type](message)
|
||||
},
|
||||
|
||||
// 添加 UI 元素到指定位置
|
||||
addElement: (element: HTMLElement, targetSelector: string) => {
|
||||
const target = document.querySelector(targetSelector)
|
||||
if (target) {
|
||||
target.appendChild(element)
|
||||
}
|
||||
},
|
||||
|
||||
// 创建按钮
|
||||
createButton: (text: string, onClick: Function) => {
|
||||
const button = document.createElement('button')
|
||||
button.textContent = text
|
||||
button.onclick = onClick as any
|
||||
return button
|
||||
},
|
||||
},
|
||||
|
||||
// 事件系统
|
||||
eventSource: {
|
||||
on: (event: string, callback: Function) => {
|
||||
window.addEventListener(`st:${event}`, (e: any) => callback(e.detail))
|
||||
},
|
||||
|
||||
once: (event: string, callback: Function) => {
|
||||
const handler = (e: any) => {
|
||||
callback(e.detail)
|
||||
window.removeEventListener(`st:${event}`, handler)
|
||||
}
|
||||
window.addEventListener(`st:${event}`, handler)
|
||||
},
|
||||
|
||||
off: (event: string, callback?: Function) => {
|
||||
if (callback) {
|
||||
window.removeEventListener(`st:${event}`, callback as any)
|
||||
}
|
||||
},
|
||||
|
||||
emit: (event: string, data?: any) => {
|
||||
window.dispatchEvent(new CustomEvent(`st:${event}`, { detail: data }))
|
||||
},
|
||||
},
|
||||
|
||||
// 获取当前上下文
|
||||
getContext: () => {
|
||||
// 尝试从路由或 store 获取当前上下文
|
||||
const context = {
|
||||
characterId: null as number | null,
|
||||
chatId: null as number | null,
|
||||
groupId: null as number | null,
|
||||
userName: '',
|
||||
characterName: '',
|
||||
|
||||
// extension_settings 对象(兼容原版 SillyTavern)
|
||||
extension_settings: new Proxy(this.extensionSettings, {
|
||||
get: (target, prop: string) => {
|
||||
if (typeof prop === 'string') {
|
||||
// 自动加载设置(如果还没加载)
|
||||
this.loadExtensionSettings(prop)
|
||||
return target[prop] || {}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
set: (target, prop: string, value) => {
|
||||
if (typeof prop === 'string') {
|
||||
target[prop] = value
|
||||
// 自动保存到 localStorage
|
||||
this.saveExtensionSettingsToStorage(prop)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}),
|
||||
|
||||
// 获取扩展配置容器的方法
|
||||
getExtensionSettingsContainer: (extensionName: string) => {
|
||||
return this.getExtensionSettingsContainer(extensionName)
|
||||
},
|
||||
}
|
||||
|
||||
// 可以从实际应用状态获取
|
||||
try {
|
||||
const route = (window as any).$route
|
||||
if (route) {
|
||||
context.characterId = route.params.characterId ? parseInt(route.params.characterId) : null
|
||||
context.chatId = route.params.chatId ? parseInt(route.params.chatId) : null
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return context
|
||||
},
|
||||
|
||||
// 工具函数
|
||||
utils: {
|
||||
// 延迟执行
|
||||
delay: (ms: number) => new Promise(resolve => setTimeout(resolve, ms)),
|
||||
|
||||
// 生成唯一 ID
|
||||
generateId: () => `ext_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
|
||||
// 安全解析 JSON
|
||||
parseJSON: (str: string, fallback: any = null) => {
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// 存储工具
|
||||
storage: {
|
||||
get: (key: string, defaultValue: any = null) => {
|
||||
const value = localStorage.getItem(`st_${key}`)
|
||||
return value ? JSON.parse(value) : defaultValue
|
||||
},
|
||||
|
||||
set: (key: string, value: any) => {
|
||||
localStorage.setItem(`st_${key}`, JSON.stringify(value))
|
||||
},
|
||||
|
||||
remove: (key: string) => {
|
||||
localStorage.removeItem(`st_${key}`)
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
const keys = Object.keys(localStorage).filter(k => k.startsWith('st_'))
|
||||
keys.forEach(k => localStorage.removeItem(k))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 挂载到 window 对象
|
||||
;(window as any).SillyTavern = this.stAPI
|
||||
;(window as any).st = this.stAPI // 简写别名
|
||||
|
||||
// 触发 API 初始化完成事件
|
||||
this.emitEvent('apiReady')
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载扩展
|
||||
*/
|
||||
async loadExtension(extension: Extension): Promise<boolean> {
|
||||
try {
|
||||
console.log(`[Extension] 加载扩展: ${extension.name}`)
|
||||
|
||||
// 检查是否已加载
|
||||
if (this.instances.has(extension.id) && this.instances.get(extension.id)?.isLoaded) {
|
||||
console.log(`[Extension] 扩展已加载: ${extension.name}`)
|
||||
return true
|
||||
}
|
||||
|
||||
// 解析 manifest
|
||||
const manifest = typeof extension.manifestData === 'string'
|
||||
? JSON.parse(extension.manifestData)
|
||||
: extension.manifestData
|
||||
|
||||
const instance: ExtensionInstance = {
|
||||
extension,
|
||||
manifest,
|
||||
isLoaded: false,
|
||||
isRunning: false,
|
||||
}
|
||||
|
||||
// 加载样式文件
|
||||
if (extension.stylePath) {
|
||||
await this.loadStyle(extension, instance)
|
||||
}
|
||||
|
||||
// 加载脚本文件
|
||||
if (extension.scriptPath) {
|
||||
await this.loadScript(extension, instance)
|
||||
} else if (manifest.inline_script) {
|
||||
// 支持内联脚本
|
||||
await this.loadInlineScript(extension, instance, manifest.inline_script)
|
||||
}
|
||||
|
||||
instance.isLoaded = true
|
||||
this.instances.set(extension.id, instance)
|
||||
|
||||
console.log(`[Extension] 加载成功: ${extension.name}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`[Extension] 加载失败: ${extension.name}`, error)
|
||||
ElMessage.error(`加载扩展失败: ${extension.name}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载扩展样式
|
||||
*/
|
||||
private async loadStyle(extension: Extension, instance: ExtensionInstance): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.href = this.getAssetURL(extension.stylePath!)
|
||||
link.dataset.extension = extension.name
|
||||
|
||||
link.onload = () => {
|
||||
instance.styleElement = link
|
||||
resolve()
|
||||
}
|
||||
|
||||
link.onerror = () => reject(new Error(`样式加载失败: ${extension.stylePath}`))
|
||||
|
||||
document.head.appendChild(link)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载扩展脚本
|
||||
*/
|
||||
private async loadScript(extension: Extension, instance: ExtensionInstance): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = this.getAssetURL(extension.scriptPath!)
|
||||
script.dataset.extension = extension.name
|
||||
script.async = true
|
||||
|
||||
script.onload = () => {
|
||||
instance.scriptElement = script
|
||||
instance.isRunning = true
|
||||
resolve()
|
||||
}
|
||||
|
||||
script.onerror = () => reject(new Error(`脚本加载失败: ${extension.scriptPath}`))
|
||||
|
||||
document.body.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载内联脚本
|
||||
*/
|
||||
private async loadInlineScript(extension: Extension, instance: ExtensionInstance, scriptCode: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const script = document.createElement('script')
|
||||
script.textContent = scriptCode
|
||||
script.dataset.extension = extension.name
|
||||
script.dataset.inline = 'true'
|
||||
|
||||
document.body.appendChild(script)
|
||||
|
||||
instance.scriptElement = script
|
||||
instance.isRunning = true
|
||||
resolve()
|
||||
} catch (error) {
|
||||
reject(new Error(`内联脚本执行失败: ${error}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载扩展
|
||||
*/
|
||||
async unloadExtension(extensionId: number): Promise<boolean> {
|
||||
const instance = this.instances.get(extensionId)
|
||||
if (!instance) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[Extension] 卸载扩展: ${instance.extension.name}`)
|
||||
|
||||
// 移除样式
|
||||
if (instance.styleElement) {
|
||||
instance.styleElement.remove()
|
||||
}
|
||||
|
||||
// 移除脚本
|
||||
if (instance.scriptElement) {
|
||||
instance.scriptElement.remove()
|
||||
}
|
||||
|
||||
instance.isLoaded = false
|
||||
instance.isRunning = false
|
||||
|
||||
this.instances.delete(extensionId)
|
||||
|
||||
console.log(`[Extension] 卸载成功: ${instance.extension.name}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`[Extension] 卸载失败: ${instance.extension.name}`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用扩展
|
||||
*/
|
||||
async enableExtension(extension: Extension): Promise<boolean> {
|
||||
if (!extension.isEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
return await this.loadExtension(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用扩展
|
||||
*/
|
||||
async disableExtension(extensionId: number): Promise<boolean> {
|
||||
return await this.unloadExtension(extensionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载扩展
|
||||
*/
|
||||
async reloadExtension(extension: Extension): Promise<boolean> {
|
||||
await this.unloadExtension(extension.id)
|
||||
return await this.loadExtension(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源 URL
|
||||
* 这里需要根据实际情况处理:
|
||||
* 1. 如果扩展文件存储在服务器上,需要通过 API 获取
|
||||
* 2. 如果是 CDN,直接使用 URL
|
||||
* 3. 如果是 base64 编码,需要转换
|
||||
*/
|
||||
private getAssetURL(path: string): string {
|
||||
// 如果是完整 URL,直接返回
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path
|
||||
}
|
||||
|
||||
// 如果是相对路径,需要根据实际部署情况构建完整 URL
|
||||
// 这里假设扩展文件存储在 /api/extension/assets/ 路径下
|
||||
return `/api/app/extension/assets/${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展实例
|
||||
*/
|
||||
getInstance(extensionId: number): ExtensionInstance | undefined {
|
||||
return this.instances.get(extensionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已加载的扩展
|
||||
*/
|
||||
getLoadedExtensions(): ExtensionInstance[] {
|
||||
return Array.from(this.instances.values()).filter(inst => inst.isLoaded)
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发扩展事件
|
||||
*/
|
||||
emitEvent(event: string, data?: any) {
|
||||
this.stAPI.eventSource.emit(event, data)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例
|
||||
export const extensionRuntime = new ExtensionRuntime()
|
||||
|
||||
// 导出类型
|
||||
export type { ExtensionInstance }
|
||||
522
web-app-vue/src/views/extension/ExtensionList.vue
Normal file
522
web-app-vue/src/views/extension/ExtensionList.vue
Normal file
@@ -0,0 +1,522 @@
|
||||
<template>
|
||||
<div class="extension-list-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>扩展管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="showInstallDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
安装扩展
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 过滤器 -->
|
||||
<div class="filters">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索扩展名称..."
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
style="width: 300px; margin-right: 10px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-select
|
||||
v-model="filterType"
|
||||
placeholder="扩展类型"
|
||||
clearable
|
||||
@change="handleFilterChange"
|
||||
style="width: 150px; margin-right: 10px"
|
||||
>
|
||||
<el-option label="UI 扩展" value="ui" />
|
||||
<el-option label="服务端扩展" value="server" />
|
||||
<el-option label="混合扩展" value="hybrid" />
|
||||
</el-select>
|
||||
|
||||
<el-select
|
||||
v-model="filterCategory"
|
||||
placeholder="分类"
|
||||
clearable
|
||||
@change="handleFilterChange"
|
||||
style="width: 150px; margin-right: 10px"
|
||||
>
|
||||
<el-option label="工具" value="utilities" />
|
||||
<el-option label="主题" value="themes" />
|
||||
<el-option label="集成" value="integrations" />
|
||||
<el-option label="其他" value="tools" />
|
||||
</el-select>
|
||||
|
||||
<el-select
|
||||
v-model="filterEnabled"
|
||||
placeholder="状态"
|
||||
clearable
|
||||
@change="handleFilterChange"
|
||||
style="width: 120px; margin-right: 10px"
|
||||
>
|
||||
<el-option label="已启用" :value="true" />
|
||||
<el-option label="已禁用" :value="false" />
|
||||
</el-select>
|
||||
|
||||
<el-button @click="handleResetFilters">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 扩展列表 -->
|
||||
<el-table
|
||||
v-loading="extensionStore.loading"
|
||||
:data="extensionStore.extensions"
|
||||
style="width: 100%; margin-top: 20px"
|
||||
>
|
||||
<el-table-column prop="displayName" label="扩展名称" width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="extension-name">
|
||||
<strong>{{ row.displayName || row.name }}</strong>
|
||||
<el-tag
|
||||
v-if="row.isSystemExt"
|
||||
size="small"
|
||||
type="info"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
系统
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="description" label="描述" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="version" label="版本" width="100" />
|
||||
|
||||
<el-table-column prop="author" label="作者" width="120" />
|
||||
|
||||
<el-table-column prop="extensionType" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeTagType(row.extensionType)" size="small">
|
||||
{{ getTypeLabel(row.extensionType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="category" label="分类" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getCategoryLabel(row.category) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="isEnabled" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.isEnabled"
|
||||
:disabled="row.isSystemExt"
|
||||
@change="handleToggle(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleViewDetail(row)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleSettings(row)"
|
||||
>
|
||||
配置
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleExport(row)"
|
||||
>
|
||||
导出
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!row.isSystemExt"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
卸载
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="extensionStore.currentPage"
|
||||
v-model:page-size="extensionStore.pageSize"
|
||||
:total="extensionStore.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
style="margin-top: 20px; justify-content: flex-end"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 安装扩展对话框 -->
|
||||
<el-dialog
|
||||
v-model="showInstallDialog"
|
||||
title="安装扩展"
|
||||
width="600px"
|
||||
>
|
||||
<el-tabs v-model="installTab">
|
||||
<el-tab-pane label="从文件导入" name="file">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".json"
|
||||
:on-change="handleFileSelect"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将 manifest.json 拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
只能上传 manifest.json 文件
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="从 Git 安装" name="git">
|
||||
<el-form :model="installForm" label-width="120px">
|
||||
<el-form-item label="Git 仓库 URL">
|
||||
<el-input
|
||||
v-model="installForm.gitUrl"
|
||||
placeholder="https://github.com/username/extension-name.git"
|
||||
/>
|
||||
<template #extra>
|
||||
<div style="font-size: 12px; color: #909399; margin-top: 5px">
|
||||
支持 GitHub、GitLab、Gitee 等 Git 仓库<br />
|
||||
示例:https://github.com/SillyTavern/Extension-Example.git
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<el-form-item label="分支(可选)">
|
||||
<el-input
|
||||
v-model="installForm.branch"
|
||||
placeholder="main"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="从 URL 安装" name="url">
|
||||
<el-form :model="installForm" label-width="120px">
|
||||
<el-form-item label="Manifest URL">
|
||||
<el-input
|
||||
v-model="installForm.url"
|
||||
placeholder="https://example.com/manifest.json"
|
||||
/>
|
||||
<template #extra>
|
||||
<div style="font-size: 12px; color: #909399; margin-top: 5px">
|
||||
直接指向 manifest.json 文件的 URL
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showInstallDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleInstall" :loading="extensionStore.loading">
|
||||
安装
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 扩展详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="showDetailDialog"
|
||||
:title="currentExtension?.displayName || currentExtension?.name"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions v-if="currentExtension" :column="2" border>
|
||||
<el-descriptions-item label="扩展名称">
|
||||
{{ currentExtension.displayName || currentExtension.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="版本">
|
||||
{{ currentExtension.version }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="作者">
|
||||
{{ currentExtension.author }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="许可证">
|
||||
{{ currentExtension.license || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
{{ getTypeLabel(currentExtension.extensionType) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分类">
|
||||
{{ getCategoryLabel(currentExtension.category) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="主页" :span="2">
|
||||
<a v-if="currentExtension.homepage" :href="currentExtension.homepage" target="_blank">
|
||||
{{ currentExtension.homepage }}
|
||||
</a>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="仓库" :span="2">
|
||||
<a v-if="currentExtension.repository" :href="currentExtension.repository" target="_blank">
|
||||
{{ currentExtension.repository }}
|
||||
</a>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">
|
||||
{{ currentExtension.description }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="标签" :span="2">
|
||||
<el-tag
|
||||
v-for="tag in currentExtension.tags"
|
||||
:key="tag"
|
||||
size="small"
|
||||
style="margin-right: 5px"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="安装日期">
|
||||
{{ formatDate(currentExtension.installDate) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="使用次数">
|
||||
{{ currentExtension.usageCount }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { Plus, Search, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { useExtensionStore } from '@/stores/extension'
|
||||
import type { Extension } from '@/types/extension'
|
||||
|
||||
const router = useRouter()
|
||||
const extensionStore = useExtensionStore()
|
||||
|
||||
// 搜索和过滤
|
||||
const searchKeyword = ref('')
|
||||
const filterType = ref('')
|
||||
const filterCategory = ref('')
|
||||
const filterEnabled = ref<boolean | undefined>(undefined)
|
||||
|
||||
// 对话框
|
||||
const showInstallDialog = ref(false)
|
||||
const showDetailDialog = ref(false)
|
||||
const installTab = ref('git')
|
||||
const installForm = ref({
|
||||
gitUrl: '',
|
||||
branch: 'main',
|
||||
url: '',
|
||||
})
|
||||
const uploadRef = ref()
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const currentExtension = ref<Extension | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
extensionStore.fetchExtensionList()
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
extensionStore.fetchExtensionList({ keyword: searchKeyword.value })
|
||||
}
|
||||
|
||||
const handleFilterChange = () => {
|
||||
extensionStore.setFilters({
|
||||
extensionType: filterType.value,
|
||||
category: filterCategory.value,
|
||||
isEnabled: filterEnabled.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleResetFilters = () => {
|
||||
searchKeyword.value = ''
|
||||
filterType.value = ''
|
||||
filterCategory.value = ''
|
||||
filterEnabled.value = undefined
|
||||
extensionStore.resetFilters()
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
extensionStore.currentPage = page
|
||||
extensionStore.fetchExtensionList()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
extensionStore.pageSize = size
|
||||
extensionStore.fetchExtensionList()
|
||||
}
|
||||
|
||||
const handleToggle = async (row: Extension) => {
|
||||
try {
|
||||
await extensionStore.toggleExtension(row.id, row.isEnabled)
|
||||
} catch (error) {
|
||||
// 切换失败,还原状态
|
||||
row.isEnabled = !row.isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewDetail = (row: Extension) => {
|
||||
currentExtension.value = row
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
const handleSettings = (row: Extension) => {
|
||||
router.push(`/extension/settings/${row.id}`)
|
||||
}
|
||||
|
||||
const handleExport = async (row: Extension) => {
|
||||
try {
|
||||
await extensionStore.exportExtension(row.id, row.name)
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row: Extension) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要卸载扩展 "${row.displayName || row.name}" 吗?`,
|
||||
'确认卸载',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
await extensionStore.deleteExtension(row.id, false)
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('卸载失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileSelect = (file: any) => {
|
||||
selectedFile.value = file.raw
|
||||
}
|
||||
|
||||
const handleInstall = async () => {
|
||||
try {
|
||||
if (installTab.value === 'file') {
|
||||
if (!selectedFile.value) {
|
||||
ElMessage.warning('请选择要上传的文件')
|
||||
return
|
||||
}
|
||||
await extensionStore.importExtension(selectedFile.value)
|
||||
} else if (installTab.value === 'git') {
|
||||
if (!installForm.value.gitUrl) {
|
||||
ElMessage.warning('请输入 Git 仓库 URL')
|
||||
return
|
||||
}
|
||||
await extensionStore.installExtensionFromGit(
|
||||
installForm.value.gitUrl,
|
||||
installForm.value.branch || 'main'
|
||||
)
|
||||
} else {
|
||||
if (!installForm.value.url) {
|
||||
ElMessage.warning('请输入 Manifest URL')
|
||||
return
|
||||
}
|
||||
await extensionStore.installExtensionFromUrl(installForm.value.url)
|
||||
}
|
||||
|
||||
showInstallDialog.value = false
|
||||
selectedFile.value = null
|
||||
installForm.value = { gitUrl: '', branch: 'main', url: '' }
|
||||
} catch (error) {
|
||||
console.error('安装失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
ui: 'UI',
|
||||
server: '服务端',
|
||||
hybrid: '混合',
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const getTypeTagType = (type: string) => {
|
||||
const types: Record<string, any> = {
|
||||
ui: 'primary',
|
||||
server: 'success',
|
||||
hybrid: 'warning',
|
||||
}
|
||||
return types[type] || ''
|
||||
}
|
||||
|
||||
const getCategoryLabel = (category: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
utilities: '工具',
|
||||
themes: '主题',
|
||||
integrations: '集成',
|
||||
tools: '其他',
|
||||
}
|
||||
return labels[category] || category || '-'
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleString('zh-CN')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.extension-list-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.extension-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-icon--upload {
|
||||
font-size: 67px;
|
||||
color: #c0c4cc;
|
||||
margin: 40px 0 16px;
|
||||
line-height: 50px;
|
||||
}
|
||||
</style>
|
||||
798
web-app-vue/src/views/extension/ExtensionListNew.vue
Normal file
798
web-app-vue/src/views/extension/ExtensionListNew.vue
Normal file
@@ -0,0 +1,798 @@
|
||||
<template>
|
||||
<div class="extension-panel-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>扩展管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="showInstallDialog = true" size="small">
|
||||
<el-icon><Plus /></el-icon>
|
||||
安装扩展
|
||||
</el-button>
|
||||
<el-button @click="handleRefresh" size="small">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索和过滤 -->
|
||||
<div class="filters">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索扩展..."
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
style="max-width: 400px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 折叠面板式扩展列表 -->
|
||||
<div class="extension-panels" v-loading="extensionStore.loading">
|
||||
<el-empty v-if="filteredExtensions.length === 0" description="暂无扩展" />
|
||||
|
||||
<el-collapse v-else v-model="activeNames" accordion>
|
||||
<el-collapse-item
|
||||
v-for="ext in filteredExtensions"
|
||||
:key="ext.id"
|
||||
:name="ext.id"
|
||||
class="extension-item"
|
||||
>
|
||||
<!-- 扩展头部 -->
|
||||
<template #title>
|
||||
<div class="extension-header">
|
||||
<div class="extension-info">
|
||||
<div class="extension-title">
|
||||
<el-switch
|
||||
v-model="ext.isEnabled"
|
||||
size="default"
|
||||
@click.stop="handleToggle(ext)"
|
||||
@change="handleToggle(ext)"
|
||||
style="margin-right: 12px"
|
||||
/>
|
||||
<span class="name">{{ ext.displayName || ext.name }}</span>
|
||||
<el-tag
|
||||
v-if="ext.isSystemExt"
|
||||
size="small"
|
||||
type="info"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
系统
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="getTypeTagType(ext.extensionType)"
|
||||
size="small"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
{{ getTypeLabel(ext.extensionType) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="extension-meta">
|
||||
<span class="version">v{{ ext.version }}</span>
|
||||
<span class="author" v-if="ext.author">by {{ ext.author }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extension-actions" @click.stop>
|
||||
<el-button
|
||||
v-if="!ext.isSystemExt"
|
||||
size="small"
|
||||
type="danger"
|
||||
text
|
||||
@click="handleDelete(ext)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 扩展详情和配置 -->
|
||||
<div class="extension-content">
|
||||
<div class="extension-description" v-if="ext.description">
|
||||
<p>{{ ext.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 扩展配置区域 -->
|
||||
<div class="extension-settings" v-loading="loadingSettings[ext.id]">
|
||||
<el-form
|
||||
v-if="extensionSettings[ext.id]"
|
||||
:model="extensionSettings[ext.id]"
|
||||
label-position="top"
|
||||
size="default"
|
||||
>
|
||||
<!-- 动态生成配置项 -->
|
||||
<template v-for="(setting, key) in getSettingsSchema(ext)" :key="key">
|
||||
<!-- 文本输入 -->
|
||||
<el-form-item
|
||||
v-if="setting.type === 'text' || setting.type === 'string'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-input
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
:placeholder="setting.placeholder || setting.description"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
/>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 数字输入 -->
|
||||
<el-form-item
|
||||
v-else-if="setting.type === 'number'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
:min="setting.min"
|
||||
:max="setting.max"
|
||||
:step="setting.step || 1"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
/>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 布尔开关 -->
|
||||
<el-form-item
|
||||
v-else-if="setting.type === 'boolean' || setting.type === 'checkbox'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-switch
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
/>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 下拉选择 -->
|
||||
<el-form-item
|
||||
v-else-if="setting.type === 'select'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-select
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
:placeholder="setting.placeholder"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in setting.options"
|
||||
:key="option.value || option"
|
||||
:label="option.label || option"
|
||||
:value="option.value || option"
|
||||
/>
|
||||
</el-select>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 文本域 -->
|
||||
<el-form-item
|
||||
v-else-if="setting.type === 'textarea'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-input
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
type="textarea"
|
||||
:rows="setting.rows || 3"
|
||||
:placeholder="setting.placeholder || setting.description"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
/>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 滑块 -->
|
||||
<el-form-item
|
||||
v-else-if="setting.type === 'slider'"
|
||||
:label="setting.label || key"
|
||||
>
|
||||
<el-slider
|
||||
v-model="extensionSettings[ext.id][key]"
|
||||
:min="setting.min || 0"
|
||||
:max="setting.max || 100"
|
||||
:step="setting.step || 1"
|
||||
:show-input="true"
|
||||
@change="handleSettingChange(ext.id, key)"
|
||||
/>
|
||||
<div v-if="setting.description" class="setting-hint">
|
||||
{{ setting.description }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 如果没有配置项 -->
|
||||
<el-empty
|
||||
v-if="Object.keys(getSettingsSchema(ext)).length === 0"
|
||||
description="此扩展没有可配置项"
|
||||
:image-size="60"
|
||||
/>
|
||||
</el-form>
|
||||
|
||||
<el-alert
|
||||
v-else-if="settingsError[ext.id]"
|
||||
type="error"
|
||||
:title="settingsError[ext.id]"
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 扩展详细信息 -->
|
||||
<el-divider />
|
||||
<div class="extension-info-detail">
|
||||
<el-descriptions :column="2" size="small" border>
|
||||
<el-descriptions-item label="分类">
|
||||
{{ getCategoryLabel(ext.category) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="许可证">
|
||||
{{ ext.license || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="主页" :span="2" v-if="ext.homepage">
|
||||
<el-link :href="ext.homepage" target="_blank" type="primary">
|
||||
{{ ext.homepage }}
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="仓库" :span="2" v-if="ext.repository">
|
||||
<el-link :href="ext.repository" target="_blank" type="primary">
|
||||
{{ ext.repository }}
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="安装来源">
|
||||
{{ getInstallSourceLabel(ext.installSource) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="安装日期">
|
||||
{{ formatDate(ext.installDate) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="extension-actions-bar">
|
||||
<el-button size="small" @click="handleExport(ext)">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出
|
||||
</el-button>
|
||||
<el-button size="small" @click="handleRefreshExtension(ext)">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 安装扩展对话框 -->
|
||||
<el-dialog
|
||||
v-model="showInstallDialog"
|
||||
title="安装扩展"
|
||||
width="600px"
|
||||
>
|
||||
<el-tabs v-model="installTab">
|
||||
<el-tab-pane label="从文件导入" name="file">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".json"
|
||||
:on-change="handleFileSelect"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将 manifest.json 拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
只能上传 manifest.json 文件
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="从 Git 安装" name="git">
|
||||
<el-form :model="installForm" label-width="120px">
|
||||
<el-form-item label="Git 仓库 URL">
|
||||
<el-input
|
||||
v-model="installForm.gitUrl"
|
||||
placeholder="https://github.com/username/extension-name.git"
|
||||
/>
|
||||
<template #extra>
|
||||
<div style="font-size: 12px; color: #909399; margin-top: 5px">
|
||||
支持 GitHub、GitLab、Gitee 等 Git 仓库<br />
|
||||
示例:https://github.com/SillyTavern/Extension-Example.git
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<el-form-item label="分支(可选)">
|
||||
<el-input
|
||||
v-model="installForm.branch"
|
||||
placeholder="main"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="从 URL 安装" name="url">
|
||||
<el-form :model="installForm" label-width="120px">
|
||||
<el-form-item label="Manifest URL">
|
||||
<el-input
|
||||
v-model="installForm.url"
|
||||
placeholder="https://example.com/manifest.json"
|
||||
/>
|
||||
<template #extra>
|
||||
<div style="font-size: 12px; color: #909399; margin-top: 5px">
|
||||
直接指向 manifest.json 文件的 URL
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showInstallDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleInstall" :loading="extensionStore.loading">
|
||||
安装
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useExtensionStore } from '@/stores/extension'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Refresh,
|
||||
Delete,
|
||||
Download,
|
||||
UploadFilled,
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { Extension } from '@/types/extension'
|
||||
|
||||
const extensionStore = useExtensionStore()
|
||||
|
||||
// 搜索和过滤
|
||||
const searchKeyword = ref('')
|
||||
const activeNames = ref<number | null>(null)
|
||||
|
||||
// 安装对话框
|
||||
const showInstallDialog = ref(false)
|
||||
const installTab = ref('git')
|
||||
const installForm = reactive({
|
||||
gitUrl: '',
|
||||
branch: 'main',
|
||||
url: '',
|
||||
})
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const uploadRef = ref()
|
||||
|
||||
// 扩展配置
|
||||
const extensionSettings = ref<Record<number, any>>({})
|
||||
const loadingSettings = ref<Record<number, boolean>>({})
|
||||
const settingsError = ref<Record<number, string>>({})
|
||||
|
||||
// 计算属性:过滤后的扩展列表
|
||||
const filteredExtensions = computed(() => {
|
||||
let extensions = extensionStore.extensions
|
||||
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
extensions = extensions.filter(ext =>
|
||||
(ext.name?.toLowerCase().includes(keyword)) ||
|
||||
(ext.displayName?.toLowerCase().includes(keyword)) ||
|
||||
(ext.description?.toLowerCase().includes(keyword)) ||
|
||||
(ext.author?.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
return extensions
|
||||
})
|
||||
|
||||
// 获取扩展的配置结构
|
||||
const getSettingsSchema = (ext: Extension) => {
|
||||
try {
|
||||
// 从 manifest 中提取 settings 定义
|
||||
if (ext.manifestData && typeof ext.manifestData === 'object') {
|
||||
const manifest = ext.manifestData as any
|
||||
if (manifest.settings && typeof manifest.settings === 'object') {
|
||||
return manifest.settings
|
||||
}
|
||||
}
|
||||
|
||||
// 从扩展的 Settings 字段提取
|
||||
if (ext.settings && typeof ext.settings === 'object') {
|
||||
return ext.settings
|
||||
}
|
||||
|
||||
return {}
|
||||
} catch (error) {
|
||||
console.error('解析配置结构失败:', error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载扩展配置
|
||||
const loadExtensionSettings = async (extId: number) => {
|
||||
if (loadingSettings.value[extId]) return
|
||||
|
||||
try {
|
||||
loadingSettings.value[extId] = true
|
||||
settingsError.value[extId] = ''
|
||||
|
||||
const settings = await extensionStore.getExtensionSettings(extId)
|
||||
extensionSettings.value[extId] = settings || {}
|
||||
} catch (error: any) {
|
||||
console.error('加载扩展配置失败:', error)
|
||||
settingsError.value[extId] = error.response?.data?.msg || '加载配置失败'
|
||||
extensionSettings.value[extId] = {}
|
||||
} finally {
|
||||
loadingSettings.value[extId] = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听折叠面板展开,自动加载配置
|
||||
watch(activeNames, (newVal) => {
|
||||
if (newVal && !extensionSettings.value[newVal]) {
|
||||
loadExtensionSettings(newVal)
|
||||
}
|
||||
})
|
||||
|
||||
// 配置项变更
|
||||
const handleSettingChange = async (extId: number, key: string) => {
|
||||
try {
|
||||
await extensionStore.updateExtensionSettings(extId, extensionSettings.value[extId])
|
||||
ElMessage.success('配置已保存')
|
||||
} catch (error: any) {
|
||||
console.error('保存配置失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '保存配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑已在 computed 中处理
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
extensionStore.fetchExtensionList()
|
||||
}
|
||||
|
||||
// 启用/禁用扩展
|
||||
const handleToggle = async (ext: Extension) => {
|
||||
try {
|
||||
await extensionStore.toggleExtension(ext.id, ext.isEnabled)
|
||||
ElMessage.success(ext.isEnabled ? '已启用' : '已禁用')
|
||||
} catch (error: any) {
|
||||
console.error('切换扩展状态失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '操作失败')
|
||||
ext.isEnabled = !ext.isEnabled // 恢复状态
|
||||
}
|
||||
}
|
||||
|
||||
// 删除扩展
|
||||
const handleDelete = async (ext: Extension) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要卸载扩展"${ext.displayName || ext.name}"吗?`,
|
||||
'确认卸载',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
|
||||
await extensionStore.deleteExtension(ext.id)
|
||||
ElMessage.success('卸载成功')
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('卸载扩展失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '卸载失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出扩展
|
||||
const handleExport = async (ext: Extension) => {
|
||||
try {
|
||||
await extensionStore.exportExtension(ext.id)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (error: any) {
|
||||
console.error('导出扩展失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新扩展配置
|
||||
const handleRefreshExtension = (ext: Extension) => {
|
||||
delete extensionSettings.value[ext.id]
|
||||
loadExtensionSettings(ext.id)
|
||||
}
|
||||
|
||||
// 文件选择
|
||||
const handleFileSelect = (file: any) => {
|
||||
selectedFile.value = file.raw
|
||||
}
|
||||
|
||||
// 安装扩展
|
||||
const handleInstall = async () => {
|
||||
try {
|
||||
if (installTab.value === 'file') {
|
||||
if (!selectedFile.value) {
|
||||
ElMessage.warning('请选择 manifest.json 文件')
|
||||
return
|
||||
}
|
||||
await extensionStore.importExtension(selectedFile.value)
|
||||
} else if (installTab.value === 'git') {
|
||||
if (!installForm.gitUrl) {
|
||||
ElMessage.warning('请输入 Git 仓库 URL')
|
||||
return
|
||||
}
|
||||
await extensionStore.installExtensionFromGit(
|
||||
installForm.gitUrl,
|
||||
installForm.branch || 'main'
|
||||
)
|
||||
} else {
|
||||
if (!installForm.url) {
|
||||
ElMessage.warning('请输入 Manifest URL')
|
||||
return
|
||||
}
|
||||
await extensionStore.installExtensionFromUrl(installForm.url)
|
||||
}
|
||||
|
||||
showInstallDialog.value = false
|
||||
selectedFile.value = null
|
||||
installForm.gitUrl = ''
|
||||
installForm.branch = 'main'
|
||||
installForm.url = ''
|
||||
|
||||
// 重置上传组件
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.clearFiles()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('安装失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: string | Date) => {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
return d.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 类型标签颜色
|
||||
const getTypeTagType = (type: string) => {
|
||||
const types: Record<string, string> = {
|
||||
ui: '',
|
||||
server: 'success',
|
||||
hybrid: 'warning',
|
||||
}
|
||||
return types[type] || ''
|
||||
}
|
||||
|
||||
// 类型标签文本
|
||||
const getTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
ui: 'UI',
|
||||
server: 'Server',
|
||||
hybrid: 'Hybrid',
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
// 分类标签文本
|
||||
const getCategoryLabel = (category: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
utilities: '工具',
|
||||
themes: '主题',
|
||||
integrations: '集成',
|
||||
tools: '其他',
|
||||
}
|
||||
return labels[category] || category || '-'
|
||||
}
|
||||
|
||||
// 安装来源标签
|
||||
const getInstallSourceLabel = (source: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
file: '文件导入',
|
||||
url: 'URL 安装',
|
||||
git: 'Git 安装',
|
||||
marketplace: '扩展市场',
|
||||
}
|
||||
return labels[source] || source || '-'
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
extensionStore.fetchExtensionList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.extension-panel-page {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.extension-panels {
|
||||
margin-top: 20px;
|
||||
|
||||
:deep(.el-collapse) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item) {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__header) {
|
||||
height: auto;
|
||||
min-height: 56px;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__wrap) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__content) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.extension-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
|
||||
.extension-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.extension-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.extension-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.version {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extension-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.extension-content {
|
||||
.extension-description {
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.6;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.extension-settings {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.extension-info-detail {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.extension-actions-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions) {
|
||||
.el-descriptions__label {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
329
web-app-vue/src/views/extension/ExtensionSettings.vue
Normal file
329
web-app-vue/src/views/extension/ExtensionSettings.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div class="extension-settings-page">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h2>{{ extension?.displayName || extension?.name }} - 配置</h2>
|
||||
<p class="subtitle">{{ extension?.description }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button @click="handleBack">返回</el-button>
|
||||
<el-button type="primary" @click="handleSave" :loading="saving">
|
||||
保存配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 扩展信息 -->
|
||||
<el-descriptions :column="3" border style="margin-bottom: 20px">
|
||||
<el-descriptions-item label="版本">
|
||||
{{ extension?.version }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="作者">
|
||||
{{ extension?.author }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
{{ getTypeLabel(extension?.extensionType) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 配置表单 -->
|
||||
<el-form
|
||||
v-if="settings && extension"
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
label-width="200px"
|
||||
style="max-width: 800px"
|
||||
>
|
||||
<el-divider content-position="left">扩展配置</el-divider>
|
||||
|
||||
<!-- 动态生成配置项 -->
|
||||
<template v-if="hasSettings">
|
||||
<el-form-item
|
||||
v-for="(value, key) in settings"
|
||||
:key="key"
|
||||
:label="getSettingLabel(key)"
|
||||
>
|
||||
<!-- 布尔值 -->
|
||||
<el-switch
|
||||
v-if="typeof value === 'boolean'"
|
||||
v-model="formData[key]"
|
||||
/>
|
||||
|
||||
<!-- 数字 -->
|
||||
<el-input-number
|
||||
v-else-if="typeof value === 'number'"
|
||||
v-model="formData[key]"
|
||||
:min="0"
|
||||
/>
|
||||
|
||||
<!-- 选择器(如果有 options) -->
|
||||
<el-select
|
||||
v-else-if="isSelectField(key)"
|
||||
v-model="formData[key]"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in getSelectOptions(key)"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<!-- 多行文本 -->
|
||||
<el-input
|
||||
v-else-if="isTextareaField(key)"
|
||||
v-model="formData[key]"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
/>
|
||||
|
||||
<!-- 默认文本输入 -->
|
||||
<el-input
|
||||
v-else
|
||||
v-model="formData[key]"
|
||||
:placeholder="getSettingPlaceholder(key)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 没有配置项 -->
|
||||
<el-empty v-else description="该扩展暂无可配置项" />
|
||||
|
||||
<!-- 高级选项 -->
|
||||
<template v-if="extension.options && Object.keys(extension.options).length > 0">
|
||||
<el-divider content-position="left">高级选项</el-divider>
|
||||
|
||||
<el-form-item
|
||||
v-for="(value, key) in extension.options"
|
||||
:key="'option_' + key"
|
||||
:label="getSettingLabel(key)"
|
||||
>
|
||||
<el-input
|
||||
v-model="optionsData[key]"
|
||||
:placeholder="getSettingPlaceholder(key)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<!-- Manifest 信息 -->
|
||||
<el-divider content-position="left">Manifest 信息</el-divider>
|
||||
<el-descriptions v-if="extension" :column="2" border>
|
||||
<el-descriptions-item label="主脚本">
|
||||
{{ extension.scriptPath || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="样式文件">
|
||||
{{ extension.stylePath || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="依赖扩展" :span="2">
|
||||
<template v-if="extension.dependencies && Object.keys(extension.dependencies).length > 0">
|
||||
<el-tag
|
||||
v-for="(version, name) in extension.dependencies"
|
||||
:key="name"
|
||||
size="small"
|
||||
style="margin-right: 5px"
|
||||
>
|
||||
{{ name }} {{ version }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else>无</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="冲突扩展" :span="2">
|
||||
<template v-if="extension.conflicts && extension.conflicts.length > 0">
|
||||
<el-tag
|
||||
v-for="conflict in extension.conflicts"
|
||||
:key="conflict"
|
||||
size="small"
|
||||
type="danger"
|
||||
style="margin-right: 5px"
|
||||
>
|
||||
{{ conflict }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else>无</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<el-divider content-position="left">使用统计</el-divider>
|
||||
<el-descriptions v-if="extension" :column="3" border>
|
||||
<el-descriptions-item label="使用次数">
|
||||
{{ extension.usageCount }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="错误次数">
|
||||
{{ extension.errorCount }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="平均加载时间">
|
||||
{{ extension.loadTime }}ms
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="安装日期">
|
||||
{{ formatDate(extension.installDate) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后启用时间">
|
||||
{{ formatDate(extension.lastEnabled) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="安装来源">
|
||||
{{ getInstallSourceLabel(extension.installSource) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useExtensionStore } from '@/stores/extension'
|
||||
import type { Extension } from '@/types/extension'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const extensionStore = useExtensionStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const extension = ref<Extension | null>(null)
|
||||
const settings = ref<Record<string, any> | null>(null)
|
||||
const formData = ref<Record<string, any>>({})
|
||||
const optionsData = ref<Record<string, any>>({})
|
||||
const formRef = ref()
|
||||
|
||||
const extensionId = computed(() => Number(route.params.id))
|
||||
|
||||
const hasSettings = computed(() => {
|
||||
return settings.value && Object.keys(settings.value).length > 0
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadExtension()
|
||||
await loadSettings()
|
||||
})
|
||||
|
||||
const loadExtension = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
extension.value = await extensionStore.fetchExtension(extensionId.value)
|
||||
|
||||
// 初始化 options 数据
|
||||
if (extension.value.options) {
|
||||
optionsData.value = { ...extension.value.options }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载扩展失败:', error)
|
||||
ElMessage.error('加载扩展失败')
|
||||
router.back()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
settings.value = await extensionStore.getExtensionSettings(extensionId.value)
|
||||
formData.value = { ...settings.value }
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
saving.value = true
|
||||
await extensionStore.updateExtensionSettings(extensionId.value, formData.value)
|
||||
ElMessage.success('配置保存成功')
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const getTypeLabel = (type?: string) => {
|
||||
if (!type) return '-'
|
||||
const labels: Record<string, string> = {
|
||||
ui: 'UI 扩展',
|
||||
server: '服务端扩展',
|
||||
hybrid: '混合扩展',
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const getSettingLabel = (key: string) => {
|
||||
// 将驼峰命名转换为可读标签
|
||||
return key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, (str) => str.toUpperCase())
|
||||
.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
const getSettingPlaceholder = (key: string) => {
|
||||
return `请输入 ${getSettingLabel(key)}`
|
||||
}
|
||||
|
||||
const isSelectField = (key: string) => {
|
||||
// 判断是否是选择字段(可根据 extension.options 或 manifest 定义)
|
||||
return false
|
||||
}
|
||||
|
||||
const getSelectOptions = (key: string) => {
|
||||
// 返回选择器选项
|
||||
return []
|
||||
}
|
||||
|
||||
const isTextareaField = (key: string) => {
|
||||
// 判断是否是多行文本字段
|
||||
const textareaFields = ['description', 'content', 'notes', 'script', 'code']
|
||||
return textareaFields.some(field => key.toLowerCase().includes(field))
|
||||
}
|
||||
|
||||
const getInstallSourceLabel = (source: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
url: 'URL',
|
||||
file: '文件',
|
||||
marketplace: '应用市场',
|
||||
}
|
||||
return labels[source] || source
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleString('zh-CN')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.extension-settings-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
421
web-app-vue/src/views/regex/RegexScriptEdit.vue
Normal file
421
web-app-vue/src/views/regex/RegexScriptEdit.vue
Normal file
@@ -0,0 +1,421 @@
|
||||
<template>
|
||||
<div class="regex-script-edit">
|
||||
<el-page-header @back="handleBack" :title="isEdit ? '编辑正则脚本' : '创建正则脚本'">
|
||||
<template #content>
|
||||
<span class="page-title">{{ isEdit ? '编辑正则脚本' : '创建正则脚本' }}</span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<el-card shadow="never" style="margin-top: 20px">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="140px"
|
||||
label-position="left"
|
||||
>
|
||||
<!-- 基础信息 -->
|
||||
<el-divider content-position="left">基础信息</el-divider>
|
||||
|
||||
<el-form-item label="脚本名称" prop="scriptName">
|
||||
<el-input
|
||||
v-model="formData.scriptName"
|
||||
placeholder="请输入脚本名称"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="脚本描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入脚本描述"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否启用" prop="enabled">
|
||||
<el-switch v-model="formData.enabled" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="全局脚本" prop="isGlobal">
|
||||
<el-switch v-model="formData.isGlobal" />
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px">
|
||||
全局脚本会应用到所有对话,角色脚本仅应用到关联的角色
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 正则表达式 -->
|
||||
<el-divider content-position="left">正则表达式</el-divider>
|
||||
|
||||
<el-form-item label="查找正则" prop="findRegex">
|
||||
<el-input
|
||||
v-model="formData.findRegex"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入正则表达式,例如:\b(Hello|Hi)\b"
|
||||
@input="handleRegexChange"
|
||||
/>
|
||||
<div style="margin-top: 5px; font-size: 12px; color: #909399">
|
||||
<el-link
|
||||
href="https://regexr.com/"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
style="font-size: 12px"
|
||||
>
|
||||
正则表达式测试工具 →
|
||||
</el-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="替换为" prop="replaceString">
|
||||
<el-input
|
||||
v-model="formData.replaceString"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入替换字符串,支持 $1、$2 等捕获组引用"
|
||||
@input="handleRegexChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 测试区域 -->
|
||||
<el-form-item label="测试输入">
|
||||
<el-input
|
||||
v-model="testInput"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="输入测试文本"
|
||||
@input="handleTest"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
style="margin-top: 10px"
|
||||
:loading="testing"
|
||||
@click="handleTest"
|
||||
>
|
||||
测试
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="testResult" label="测试结果">
|
||||
<div class="test-result">
|
||||
<div v-if="testResult.success" class="result-success">
|
||||
<div class="result-row">
|
||||
<span class="label">匹配次数:</span>
|
||||
<el-tag type="success">{{ testResult.matchedCount }}</el-tag>
|
||||
</div>
|
||||
<div v-if="testResult.matches.length > 0" class="result-row">
|
||||
<span class="label">匹配内容:</span>
|
||||
<el-tag
|
||||
v-for="(match, idx) in testResult.matches"
|
||||
:key="idx"
|
||||
size="small"
|
||||
style="margin-right: 5px"
|
||||
>
|
||||
{{ match }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="result-row">
|
||||
<span class="label">处理后:</span>
|
||||
<div class="output-text">{{ testResult.output }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="result-error">
|
||||
<el-alert type="error" :title="testResult.error" :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 高级选项 -->
|
||||
<el-divider content-position="left">高级选项</el-divider>
|
||||
|
||||
<el-form-item label="去除空格">
|
||||
<el-switch v-model="formData.trimStrings" />
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px">
|
||||
处理前去除文本首尾空格
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="正则替换">
|
||||
<el-switch v-model="formData.substituteRegex" />
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px">
|
||||
启用后支持 $1、$2 等捕获组引用
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="仅格式化">
|
||||
<el-switch v-model="formData.onlyFormat" />
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px">
|
||||
仅用于格式化消息,不影响 AI 处理
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="编辑时运行">
|
||||
<el-switch v-model="formData.runOnEdit" />
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px">
|
||||
在编辑消息时也运行脚本
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="应用位置">
|
||||
<el-select
|
||||
v-model="formData.placement"
|
||||
placeholder="选择应用位置"
|
||||
clearable
|
||||
>
|
||||
<el-option label="全部消息" value="" />
|
||||
<el-option label="用户消息" value="user" />
|
||||
<el-option label="AI消息" value="ai" />
|
||||
<el-option label="系统消息" value="sys" />
|
||||
<el-option label="斜杠命令" value="slash" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最小深度">
|
||||
<el-input-number
|
||||
v-model="formData.minDepth"
|
||||
:min="0"
|
||||
:max="100"
|
||||
placeholder="不限制"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px">
|
||||
仅在消息深度 ≥ 此值时应用
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最大深度">
|
||||
<el-input-number
|
||||
v-model="formData.maxDepth"
|
||||
:min="0"
|
||||
:max="100"
|
||||
placeholder="不限制"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span style="margin-left: 10px; color: #909399; font-size: 12px">
|
||||
仅在消息深度 ≤ 此值时应用
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="regexScriptStore.loading" @click="handleSubmit">
|
||||
{{ isEdit ? '保存' : '创建' }}
|
||||
</el-button>
|
||||
<el-button @click="handleBack">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
CopyDocument,
|
||||
Upload,
|
||||
Download,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useRegexScriptStore } from '@/stores/regexScript'
|
||||
import type { CreateRegexScriptRequest, TestRegexScriptResponse } from '@/types/regexScript'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const regexScriptStore = useRegexScriptStore()
|
||||
|
||||
const isEdit = ref(false)
|
||||
const scriptId = ref<number | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<CreateRegexScriptRequest>({
|
||||
scriptName: '',
|
||||
description: '',
|
||||
findRegex: '',
|
||||
replaceString: '',
|
||||
enabled: true,
|
||||
isGlobal: false,
|
||||
trimStrings: false,
|
||||
onlyFormat: false,
|
||||
runOnEdit: false,
|
||||
substituteRegex: false,
|
||||
placement: '',
|
||||
linkedChars: [],
|
||||
})
|
||||
|
||||
// 测试相关
|
||||
const testInput = ref('')
|
||||
const testResult = ref<TestRegexScriptResponse | null>(null)
|
||||
const testing = ref(false)
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
scriptName: [{ required: true, message: '请输入脚本名称', trigger: 'blur' }],
|
||||
findRegex: [
|
||||
{ required: true, message: '请输入正则表达式', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// 正则表达式变化时重新测试
|
||||
const handleRegexChange = () => {
|
||||
if (testInput.value) {
|
||||
testResult.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 测试正则脚本
|
||||
const handleTest = async () => {
|
||||
if (!formData.findRegex || !testInput.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
testing.value = true
|
||||
testResult.value = await regexScriptStore.testRegexScript({
|
||||
findRegex: formData.findRegex,
|
||||
replaceString: formData.replaceString || '',
|
||||
testInput: testInput.value,
|
||||
trimStrings: formData.trimStrings,
|
||||
substituteRegex: formData.substituteRegex,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('测试失败:', error)
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
if (isEdit.value && scriptId.value) {
|
||||
await regexScriptStore.updateRegexScript(scriptId.value, formData)
|
||||
} else {
|
||||
await regexScriptStore.createRegexScript(formData)
|
||||
}
|
||||
router.push('/regex')
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回列表
|
||||
const handleBack = () => {
|
||||
router.push('/regex')
|
||||
}
|
||||
|
||||
// 加载脚本详情
|
||||
const loadScriptDetail = async (id: number) => {
|
||||
try {
|
||||
const script = await regexScriptStore.fetchRegexScriptDetail(id)
|
||||
|
||||
// 填充表单
|
||||
Object.assign(formData, {
|
||||
scriptName: script.scriptName,
|
||||
description: script.description,
|
||||
findRegex: script.findRegex,
|
||||
replaceString: script.replaceString,
|
||||
enabled: script.enabled,
|
||||
isGlobal: script.isGlobal,
|
||||
trimStrings: script.trimStrings,
|
||||
onlyFormat: script.onlyFormat,
|
||||
runOnEdit: script.runOnEdit,
|
||||
substituteRegex: script.substituteRegex,
|
||||
minDepth: script.minDepth,
|
||||
maxDepth: script.maxDepth,
|
||||
placement: script.placement,
|
||||
affectMinDepth: script.affectMinDepth,
|
||||
affectMaxDepth: script.affectMaxDepth,
|
||||
linkedChars: script.linkedChars,
|
||||
scriptData: script.scriptData,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('加载脚本详情失败:', error)
|
||||
router.push('/regex')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
const id = route.params.id
|
||||
if (id && typeof id === 'string') {
|
||||
isEdit.value = true
|
||||
scriptId.value = parseInt(id)
|
||||
loadScriptDetail(scriptId.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.regex-script-edit {
|
||||
padding: 20px;
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.test-result {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
|
||||
.result-success {
|
||||
.result-row {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.output-text {
|
||||
margin-top: 8px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-error {
|
||||
:deep(.el-alert) {
|
||||
background: #fef0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-divider__text) {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
</style>
|
||||
314
web-app-vue/src/views/regex/RegexScriptList.vue
Normal file
314
web-app-vue/src/views/regex/RegexScriptList.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="regex-script-list">
|
||||
<el-card shadow="never">
|
||||
<!-- 搜索和操作栏 -->
|
||||
<div class="header-actions">
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchParams.scriptName"
|
||||
placeholder="搜索脚本名称"
|
||||
style="width: 300px"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-select
|
||||
v-model="searchParams.enabled"
|
||||
placeholder="状态"
|
||||
style="width: 120px; margin-left: 10px"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="已启用" :value="true" />
|
||||
<el-option label="已禁用" :value="false" />
|
||||
</el-select>
|
||||
|
||||
<el-select
|
||||
v-model="searchParams.isGlobal"
|
||||
placeholder="范围"
|
||||
style="width: 120px; margin-left: 10px"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="全局脚本" :value="true" />
|
||||
<el-option label="角色脚本" :value="false" />
|
||||
</el-select>
|
||||
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch" style="margin-left: 10px">
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:before-upload="handleImport"
|
||||
accept=".json"
|
||||
>
|
||||
<el-button :icon="Upload">导入</el-button>
|
||||
</el-upload>
|
||||
|
||||
<el-button :icon="Download" @click="handleExport">
|
||||
导出
|
||||
</el-button>
|
||||
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">
|
||||
新建脚本
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 脚本列表 -->
|
||||
<el-table
|
||||
v-loading="regexScriptStore.loading"
|
||||
:data="regexScriptStore.regexScripts"
|
||||
style="width: 100%; margin-top: 20px"
|
||||
>
|
||||
<el-table-column prop="scriptName" label="脚本名称" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-tag v-if="row.isGlobal" type="warning" size="small" style="margin-right: 8px">
|
||||
全局
|
||||
</el-tag>
|
||||
{{ row.scriptName }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="findRegex" label="正则表达式" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<code style="font-size: 12px">{{ row.findRegex }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="replaceString" label="替换为" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<code v-if="row.replaceString" style="font-size: 12px">{{ row.replaceString }}</code>
|
||||
<span v-else style="color: #ccc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="placement" label="应用位置" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.placement" size="small">{{ row.placement }}</el-tag>
|
||||
<span v-else style="color: #ccc">全部</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="usageCount" label="使用次数" width="100" align="center" />
|
||||
|
||||
<el-table-column prop="enabled" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.enabled"
|
||||
@change="handleToggleEnabled(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
text
|
||||
size="small"
|
||||
:icon="Edit"
|
||||
@click="handleEdit(row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
type="info"
|
||||
text
|
||||
size="small"
|
||||
:icon="CopyDocument"
|
||||
@click="handleDuplicate(row.id)"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
|
||||
<el-popconfirm
|
||||
title="确定要删除这个脚本吗?"
|
||||
@confirm="handleDelete(row.id)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
type="danger"
|
||||
text
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="margin-top: 20px; display: flex; justify-content: flex-end">
|
||||
<el-pagination
|
||||
v-model:current-page="regexScriptStore.currentPage"
|
||||
v-model:page-size="regexScriptStore.pageSize"
|
||||
:total="regexScriptStore.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSearch"
|
||||
@current-change="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
CopyDocument,
|
||||
Upload,
|
||||
Download,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useRegexScriptStore } from '@/stores/regexScript'
|
||||
import type { RegexScriptListParams } from '@/types/regexScript'
|
||||
|
||||
const router = useRouter()
|
||||
const regexScriptStore = useRegexScriptStore()
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = ref<RegexScriptListParams>({
|
||||
scriptName: '',
|
||||
enabled: undefined,
|
||||
isGlobal: undefined,
|
||||
})
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
regexScriptStore.resetPagination()
|
||||
regexScriptStore.fetchRegexScriptList(searchParams.value)
|
||||
}
|
||||
|
||||
// 新建
|
||||
const handleCreate = () => {
|
||||
router.push('/regex/create')
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (id: number) => {
|
||||
router.push(`/regex/edit/${id}`)
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await regexScriptStore.deleteRegexScript(id)
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制
|
||||
const handleDuplicate = async (id: number) => {
|
||||
try {
|
||||
await regexScriptStore.duplicateRegexScript(id)
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换启用状态
|
||||
const handleToggleEnabled = async (row: any) => {
|
||||
try {
|
||||
await regexScriptStore.updateRegexScript(row.id, {
|
||||
enabled: row.enabled,
|
||||
})
|
||||
} catch (error) {
|
||||
// 恢复状态
|
||||
row.enabled = !row.enabled
|
||||
}
|
||||
}
|
||||
|
||||
// 导入
|
||||
const handleImport = async (file: File) => {
|
||||
try {
|
||||
const mode = await ElMessageBox.prompt(
|
||||
'选择导入模式:\nskip - 跳过已存在的脚本\noverwrite - 覆盖已存在的脚本\nmerge - 合并(重命名)',
|
||||
'导入正则脚本',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: 'skip',
|
||||
inputPattern: /^(skip|overwrite|merge)$/,
|
||||
inputErrorMessage: '请输入 skip、overwrite 或 merge',
|
||||
}
|
||||
)
|
||||
|
||||
await regexScriptStore.importRegexScripts(file, mode.value)
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('导入失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await regexScriptStore.exportRegexScripts()
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
regexScriptStore.fetchRegexScriptList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.regex-script-list {
|
||||
padding: 20px;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
}
|
||||
</style>
|
||||
302
web-app-vue/src/views/worldbook/WorldBookEdit.vue
Normal file
302
web-app-vue/src/views/worldbook/WorldBookEdit.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<div class="world-book-edit">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2>{{ isEdit ? '编辑世界书' : '创建世界书' }}</h2>
|
||||
<div class="actions">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" :loading="worldInfoStore.loading" @click="handleSave">
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 世界书基本信息 -->
|
||||
<el-card class="info-card">
|
||||
<template #header>
|
||||
<span>基本信息</span>
|
||||
</template>
|
||||
<el-form :model="formData" :rules="rules" ref="formRef" label-width="120px">
|
||||
<el-form-item label="世界书名称" prop="bookName">
|
||||
<el-input
|
||||
v-model="formData.bookName"
|
||||
placeholder="请输入世界书名称"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-switch
|
||||
v-model="formData.isGlobal"
|
||||
active-text="全局世界书"
|
||||
inactive-text="角色专属"
|
||||
/>
|
||||
<el-tooltip
|
||||
content="全局世界书将应用于所有角色,角色专属世界书需要手动关联角色"
|
||||
placement="right"
|
||||
>
|
||||
<el-icon style="margin-left: 8px"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 条目列表 -->
|
||||
<el-card class="entries-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>条目列表({{ formData.entries.length }})</span>
|
||||
<el-button type="primary" size="small" :icon="Plus" @click="handleAddEntry">
|
||||
添加条目
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty
|
||||
v-if="formData.entries.length === 0"
|
||||
description="暂无条目,点击上方按钮添加"
|
||||
/>
|
||||
|
||||
<!-- 条目列表 -->
|
||||
<div v-else class="entry-list">
|
||||
<el-collapse v-model="activeEntries" accordion>
|
||||
<el-collapse-item
|
||||
v-for="(entry, index) in formData.entries"
|
||||
:key="entry.uid"
|
||||
:name="entry.uid"
|
||||
>
|
||||
<template #title>
|
||||
<div class="entry-title">
|
||||
<el-tag :type="entry.enabled ? 'success' : 'info'" size="small">
|
||||
{{ entry.enabled ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
<span class="entry-index">#{{ index + 1 }}</span>
|
||||
<span class="entry-keys">{{ formatKeys(entry.keys) }}</span>
|
||||
<el-tag v-if="entry.constant" type="warning" size="small">常驻</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 条目编辑表单 -->
|
||||
<WorldInfoEntryForm
|
||||
:entry="entry"
|
||||
@update="(updated) => handleUpdateEntry(index, updated)"
|
||||
@delete="() => handleDeleteEntry(index)"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, QuestionFilled } from '@element-plus/icons-vue'
|
||||
import { useWorldInfoStore } from '@/stores/worldInfo'
|
||||
import type { CreateWorldBookRequest, WorldInfoEntry } from '@/types/worldInfo'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import WorldInfoEntryForm from './WorldInfoEntryForm.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const worldInfoStore = useWorldInfoStore()
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref()
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
|
||||
// 展开的条目
|
||||
const activeEntries = ref<string[]>([])
|
||||
|
||||
// 表单数据
|
||||
const formData = ref<CreateWorldBookRequest>({
|
||||
bookName: '',
|
||||
isGlobal: false,
|
||||
entries: [],
|
||||
linkedChars: []
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
bookName: [
|
||||
{ required: true, message: '请输入世界书名称', trigger: 'blur' },
|
||||
{ min: 1, max: 500, message: '长度在 1 到 500 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 格式化关键词显示
|
||||
const formatKeys = (keys: string[]) => {
|
||||
if (!keys || keys.length === 0) return '无关键词'
|
||||
return keys.slice(0, 3).join(', ') + (keys.length > 3 ? '...' : '')
|
||||
}
|
||||
|
||||
// 创建默认条目
|
||||
const createDefaultEntry = (): WorldInfoEntry => ({
|
||||
uid: uuidv4(),
|
||||
keys: [],
|
||||
secondary_keys: [],
|
||||
content: '',
|
||||
comment: '',
|
||||
enabled: true,
|
||||
constant: false,
|
||||
selective: false,
|
||||
order: formData.value.entries.length,
|
||||
position: 'before_char',
|
||||
depth: 4,
|
||||
probability: 100,
|
||||
use_probability: false,
|
||||
group: '',
|
||||
group_override: false,
|
||||
group_weight: 0,
|
||||
prevent_recursion: false,
|
||||
delay_until_recursion: false,
|
||||
scan_depth: null,
|
||||
case_sensitive: null,
|
||||
match_whole_words: null,
|
||||
use_regex: null,
|
||||
automation_id: '',
|
||||
role: '',
|
||||
vectorized: '',
|
||||
extensions: {}
|
||||
})
|
||||
|
||||
// 添加条目
|
||||
const handleAddEntry = () => {
|
||||
const newEntry = createDefaultEntry()
|
||||
formData.value.entries.push(newEntry)
|
||||
activeEntries.value = [newEntry.uid]
|
||||
}
|
||||
|
||||
// 更新条目
|
||||
const handleUpdateEntry = (index: number, updatedEntry: WorldInfoEntry) => {
|
||||
formData.value.entries[index] = updatedEntry
|
||||
}
|
||||
|
||||
// 删除条目
|
||||
const handleDeleteEntry = async (index: number) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除此条目吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
formData.value.entries.splice(index, 1)
|
||||
} catch (error) {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 验证表单
|
||||
await formRef.value.validate()
|
||||
|
||||
// 验证至少有一个条目
|
||||
if (formData.value.entries.length === 0) {
|
||||
ElMessage.warning('请至少添加一个条目')
|
||||
return
|
||||
}
|
||||
|
||||
// 保存
|
||||
if (isEdit.value) {
|
||||
await worldInfoStore.updateWorldBook(Number(route.params.id), {
|
||||
bookName: formData.value.bookName,
|
||||
isGlobal: formData.value.isGlobal,
|
||||
entries: formData.value.entries,
|
||||
linkedChars: formData.value.linkedChars
|
||||
})
|
||||
} else {
|
||||
await worldInfoStore.createWorldBook(formData.value)
|
||||
}
|
||||
|
||||
router.push('/worldbook')
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
if (isEdit.value) {
|
||||
try {
|
||||
const book = await worldInfoStore.fetchWorldBookDetail(Number(route.params.id))
|
||||
formData.value = {
|
||||
bookName: book.bookName,
|
||||
isGlobal: book.isGlobal,
|
||||
entries: book.entries || [],
|
||||
linkedChars: book.linkedChars || []
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载世界书失败')
|
||||
router.push('/worldbook')
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.world-book-edit {
|
||||
padding: 20px;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.entries-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entry-list {
|
||||
.entry-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
|
||||
.entry-index {
|
||||
font-weight: bold;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.entry-keys {
|
||||
color: #606266;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
236
web-app-vue/src/views/worldbook/WorldInfoEntryForm.vue
Normal file
236
web-app-vue/src/views/worldbook/WorldInfoEntryForm.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<div class="entry-form">
|
||||
<el-form :model="localEntry" label-width="140px" size="default">
|
||||
<!-- 基础信息 -->
|
||||
<el-divider content-position="left">基础设置</el-divider>
|
||||
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="localEntry.enabled" active-text="启用" inactive-text="禁用" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="主要关键词" required>
|
||||
<el-select
|
||||
v-model="localEntry.keys"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="输入关键词后按回车添加"
|
||||
style="width: 100%"
|
||||
>
|
||||
</el-select>
|
||||
<div class="form-tip">当消息中包含这些关键词时,条目将被激活</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="次要关键词">
|
||||
<el-select
|
||||
v-model="localEntry.secondary_keys"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="输入关键词后按回车添加(可选)"
|
||||
style="width: 100%"
|
||||
>
|
||||
</el-select>
|
||||
<div class="form-tip">启用"选择性激活"时,需同时匹配主要和次要关键词</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="条目内容" required>
|
||||
<el-input
|
||||
v-model="localEntry.content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="输入条目内容,这将被插入到提示词中"
|
||||
maxlength="5000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="localEntry.comment"
|
||||
placeholder="添加备注(不会被插入到提示词中)"
|
||||
maxlength="500"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 激活设置 -->
|
||||
<el-divider content-position="left">激活设置</el-divider>
|
||||
|
||||
<el-form-item label="常驻激活">
|
||||
<el-switch v-model="localEntry.constant" active-text="是" inactive-text="否" />
|
||||
<div class="form-tip">启用后,此条目将始终激活(忽略关键词匹配)</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择性激活">
|
||||
<el-switch v-model="localEntry.selective" active-text="是" inactive-text="否" />
|
||||
<div class="form-tip">启用后,需要同时匹配主要和次要关键词才能激活</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="使用概率">
|
||||
<el-switch v-model="localEntry.use_probability" active-text="是" inactive-text="否" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="localEntry.use_probability" label="激活概率">
|
||||
<el-slider v-model="localEntry.probability" :min="0" :max="100" show-input />
|
||||
<div class="form-tip">即使匹配成功,也有 {{ localEntry.probability }}% 的概率激活</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 匹配设置 -->
|
||||
<el-divider content-position="left">匹配设置</el-divider>
|
||||
|
||||
<el-form-item label="大小写敏感">
|
||||
<el-select v-model="localEntry.case_sensitive" placeholder="使用全局设置">
|
||||
<el-option label="使用全局设置" :value="null" />
|
||||
<el-option label="是" :value="true" />
|
||||
<el-option label="否" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="匹配整词">
|
||||
<el-select v-model="localEntry.match_whole_words" placeholder="使用全局设置">
|
||||
<el-option label="使用全局设置" :value="null" />
|
||||
<el-option label="是" :value="true" />
|
||||
<el-option label="否" :value="false" />
|
||||
</el-select>
|
||||
<div class="form-tip">启用后,"world" 不会匹配 "worldbuilding"</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="使用正则表达式">
|
||||
<el-select v-model="localEntry.use_regex" placeholder="使用全局设置">
|
||||
<el-option label="使用全局设置" :value="null" />
|
||||
<el-option label="是" :value="true" />
|
||||
<el-option label="否" :value="false" />
|
||||
</el-select>
|
||||
<div class="form-tip">启用后,关键词将被视为正则表达式</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="扫描深度">
|
||||
<el-input-number
|
||||
v-model="localEntry.scan_depth"
|
||||
:min="0"
|
||||
:max="100"
|
||||
placeholder="使用全局设置"
|
||||
/>
|
||||
<div class="form-tip">从最近消息往前扫描几条(null=使用全局设置)</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 插入设置 -->
|
||||
<el-divider content-position="left">插入设置</el-divider>
|
||||
|
||||
<el-form-item label="插入顺序">
|
||||
<el-input-number v-model="localEntry.order" :min="0" :max="999" />
|
||||
<div class="form-tip">数字越小越靠前</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="插入位置">
|
||||
<el-select v-model="localEntry.position">
|
||||
<el-option label="在角色定义之前" value="before_char" />
|
||||
<el-option label="在角色定义之后" value="after_char" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="localEntry.role" placeholder="默认" clearable>
|
||||
<el-option label="系统" value="system" />
|
||||
<el-option label="用户" value="user" />
|
||||
<el-option label="助手" value="assistant" />
|
||||
</el-select>
|
||||
<div class="form-tip">指定此条目的角色类型</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 递归设置 -->
|
||||
<el-divider content-position="left">递归设置</el-divider>
|
||||
|
||||
<el-form-item label="防止递归">
|
||||
<el-switch v-model="localEntry.prevent_recursion" active-text="是" inactive-text="否" />
|
||||
<div class="form-tip">防止此条目在递归激活中被触发</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="延迟递归">
|
||||
<el-switch v-model="localEntry.delay_until_recursion" active-text="是" inactive-text="否" />
|
||||
<div class="form-tip">仅在递归激活时触发此条目</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 分组设置 -->
|
||||
<el-divider content-position="left">分组设置</el-divider>
|
||||
|
||||
<el-form-item label="分组">
|
||||
<el-input v-model="localEntry.group" placeholder="留空表示不分组" />
|
||||
<div class="form-tip">同一分组中只会激活一个条目(按权重选择)</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="localEntry.group" label="分组权重">
|
||||
<el-input-number v-model="localEntry.group_weight" :min="0" :max="999" />
|
||||
<div class="form-tip">数字越大优先级越高</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="localEntry.group" label="分组覆盖">
|
||||
<el-switch v-model="localEntry.group_override" active-text="是" inactive-text="否" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-divider />
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleUpdate">保存更改</el-button>
|
||||
<el-button type="danger" @click="handleDelete">删除条目</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import type { WorldInfoEntry } from '@/types/worldInfo'
|
||||
|
||||
const props = defineProps<{
|
||||
entry: WorldInfoEntry
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [entry: WorldInfoEntry]
|
||||
delete: []
|
||||
}>()
|
||||
|
||||
// 本地编辑副本
|
||||
const localEntry = ref<WorldInfoEntry>({ ...props.entry })
|
||||
|
||||
// 监听 props 变化
|
||||
watch(
|
||||
() => props.entry,
|
||||
(newEntry) => {
|
||||
localEntry.value = { ...newEntry }
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 保存更改
|
||||
const handleUpdate = () => {
|
||||
emit('update', { ...localEntry.value })
|
||||
}
|
||||
|
||||
// 删除条目
|
||||
const handleDelete = () => {
|
||||
emit('delete')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.entry-form {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-divider__text) {
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user