feat: implement AutoProbe components and enhance LLM provider functionality

- Introduced AutoProbeForm and AutoProbeTaskStatus components for managing AutoProbe configurations and task statuses.
- Updated LLMProvider model to include a default model field for improved provider management.
- Enhanced AutoProbeList and AutoProbeDetail components for better user interaction and data display.
- Refactored related Vue components and services to streamline AutoProbe functionality and improve state management.
- Added internationalization support for new AutoProbe features and updated existing translations.
This commit is contained in:
Marvin Zhang
2025-05-12 17:37:30 +08:00
parent ec5bcf9287
commit 512f246094
58 changed files with 1118 additions and 87 deletions

View File

@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin"
)
func GetSystemInfo(c *gin.Context) (response *Response[entity.SystemInfo], err error) {
func GetSystemInfo(_ *gin.Context) (response *Response[entity.SystemInfo], err error) {
info := entity.SystemInfo{
Edition: utils.GetEdition(),
Version: utils.GetVersion(),

View File

@@ -6,10 +6,10 @@ type LLMProvider struct {
BaseModel `bson:",inline"`
Type string `json:"type" bson:"type" description:"Provider type (e.g., 'openai', 'azure-openai', 'anthropic', 'gemini')"`
Name string `json:"name" bson:"name" description:"Display name for UI"`
Enabled bool `json:"enabled" bson:"enabled" description:"Whether this provider is enabled"`
ApiKey string `json:"api_key" bson:"api_key" description:"API key for the provider"`
ApiBaseUrl string `json:"api_base_url" bson:"api_base_url" description:"API base URL for the provider"`
DeploymentName string `json:"deployment_name" bson:"deployment_name" description:"Deployment name for the provider"`
ApiVersion string `json:"api_version" bson:"api_version" description:"API version for the provider"`
Models []string `json:"models" bson:"models" description:"Models supported by this provider"`
DefaultModel string `json:"default_model" bson:"default_model" description:"Default model for this provider"`
}

View File

@@ -108,9 +108,19 @@ const toggleModel = (model: string) => {
if (index === -1) {
// Enable model
modelValue.models.push(model);
// If this is the first model being added, set it as default
if (modelValue.models.length === 1 && !modelValue.default_model) {
modelValue.default_model = model;
}
} else {
// Disable model
modelValue.models.splice(index, 1);
// If default model is being disabled, update default model
if (modelValue.default_model === model) {
modelValue.default_model = modelValue.models.length > 0 ? modelValue.models[0] : '';
}
}
emit('update:modelValue', modelValue);
@@ -121,6 +131,10 @@ const updateDefaultModels = () => {
if (!modelValue) return;
if (defaultModels.value.length > 0) {
modelValue.models = [...defaultModels.value];
// Set default model to first enabled model
if (modelValue.models.length > 0 && !modelValue.default_model) {
modelValue.default_model = modelValue.models[0];
}
}
};
@@ -220,9 +234,6 @@ defineOptions({ name: 'ClLlmProviderForm' });
:placeholder="t('views.system.ai.name')"
/>
</cl-form-item>
<cl-form-item :label="t('views.system.ai.enabled')" :span="4">
<cl-switch v-model="modelValue.enabled" />
</cl-form-item>
<cl-form-item
:label="t('views.system.ai.apiKey')"
:span="4"
@@ -272,9 +283,6 @@ defineOptions({ name: 'ClLlmProviderForm' });
<div class="models-section">
<!-- Default models from provider -->
<div v-if="defaultModels.length > 0" class="default-models">
<div class="section-title">
{{ t('views.system.ai.defaultModels') }}
</div>
<div class="model-list">
<el-checkbox
v-for="model in defaultModels"
@@ -283,17 +291,16 @@ defineOptions({ name: 'ClLlmProviderForm' });
@change="() => toggleModel(model)"
class="model-checkbox"
>
{{ model }}
<span>{{ model }}</span>
<span class="default-model-badge" v-if="model === modelValue.default_model">
({{ t('common.mode.default') }})
</span>
</el-checkbox>
</div>
</div>
<!-- Custom models -->
<div class="custom-models">
<div class="section-title">
{{ t('views.system.ai.customModels') }}
</div>
<!-- Add custom model input -->
<div class="add-model">
<el-input
@@ -334,6 +341,21 @@ defineOptions({ name: 'ClLlmProviderForm' });
</div>
</div>
</cl-form-item>
<cl-form-item :label="t('views.system.ai.defaultModel')" :span="4" prop="default_model">
<el-select
v-model="modelValue.default_model"
:placeholder="t('views.system.ai.defaultModel')"
:disabled="!modelValue.models || modelValue.models.length === 0"
>
<el-option
v-for="model in modelValue.models || []"
:key="model"
:label="model"
:value="model"
/>
</el-select>
</cl-form-item>
</cl-form>
</template>
@@ -386,5 +408,11 @@ defineOptions({ name: 'ClLlmProviderForm' });
font-style: italic;
}
}
.default-model-badge {
margin-left: 5px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
import { useStore } from 'vuex';
import { useAutoProbe } from '@/components';
import { translate } from '@/utils';
// i18n
const t = translate;
// store
const store = useStore();
const { form, formRef, formRules, isSelectiveForm, isFormItemDisabled } =
useAutoProbe(store);
defineOptions({ name: 'ClAutoProbeForm' });
</script>
<template>
<cl-form
v-if="form"
ref="formRef"
:model="form"
:rules="formRules"
:selective="isSelectiveForm"
>
<cl-form-item
:span="2"
:offset="2"
:label="t('components.project.form.name')"
not-editable
prop="name"
required
>
<el-input
v-model="form.name"
:disabled="isFormItemDisabled('name')"
:placeholder="t('components.autoprobe.form.name')"
/>
</cl-form-item>
<cl-form-item
:span="4"
:label="t('components.autoprobe.form.url')"
prop="url"
required
>
<el-input
v-model="form.url"
:disabled="isFormItemDisabled('url')"
:placeholder="t('components.autoprobe.form.url')"
type="textarea"
/>
</cl-form-item>
<cl-form-item
:span="4"
:label="t('components.autoprobe.form.query')"
prop="query"
>
<el-input
v-model="form.query"
:disabled="isFormItemDisabled('query')"
:placeholder="t('components.autoprobe.form.query')"
type="textarea"
/>
</cl-form-item>
</cl-form>
</template>

View File

@@ -0,0 +1,92 @@
<script setup lang="ts">
import { computed } from 'vue';
import { translate } from '@/utils';
import { TagProps } from '@/components/ui/tag/types';
const props = defineProps<{
status: AutoProbeTaskStatus;
size?: BasicSize;
error?: string;
clickable?: boolean;
}>();
const emit = defineEmits<{
(e: 'click'): void;
}>();
// i18n
const t = translate;
const data = computed<TagProps>(() => {
const { status, error } = props;
switch (status) {
case 'pending':
return {
label: t('components.autoprobe.task.status.label.pending'),
tooltip: t('components.autoprobe.task.status.tooltip.pending'),
type: 'primary',
icon: ['fa', 'hourglass-start'],
spinning: true,
};
case 'running':
return {
label: t('components.autoprobe.task.status.label.running'),
tooltip: t('components.autoprobe.task.status.tooltip.running'),
type: 'warning',
icon: ['fa', 'spinner'],
spinning: true,
};
case 'completed':
return {
label: t('components.autoprobe.task.status.label.completed'),
tooltip: t('components.autoprobe.task.status.tooltip.completed'),
type: 'success',
icon: ['fa', 'check'],
};
case 'failed':
return {
label: t('components.autoprobe.task.status.label.failed'),
tooltip: `${t('components.autoprobe.task.status.tooltip.failed')}<br><span style="color: 'var(--cl-red)">${error}</span>`,
type: 'danger',
icon: ['fa', 'times'],
};
case 'cancelled':
return {
label: t('components.autoprobe.task.status.label.cancelled'),
tooltip: t('components.autoprobe.task.status.tooltip.cancelled'),
type: 'info',
icon: ['fa', 'ban'],
};
default:
return {
label: t('components.autoprobe.task.status.label.unknown'),
tooltip: t('components.autoprobe.task.status.tooltip.unknown'),
type: 'info',
icon: ['fa', 'question'],
};
}
});
defineOptions({ name: 'ClAutoProbeTaskStatus' });
</script>
<template>
<cl-tag
class-name="autoprobe-task-status"
:key="data"
:icon="data.icon"
:label="data.label"
:spinning="data.spinning"
:type="data.type"
:size="size"
:clickable="clickable"
@click="emit('click')"
>
<template #tooltip>
<div v-html="data.tooltip" />
</template>
</cl-tag>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import { useStore } from 'vuex';
import { useAutoProbe } from '@/components';
// store
const store = useStore();
const {
activeDialogKey,
createEditDialogVisible,
actionFunctions,
formList,
confirmDisabled,
confirmLoading,
} = useAutoProbe(store);
defineOptions({ name: 'ClCreateEditAutoProbeDialog' });
</script>
<template>
<cl-create-edit-dialog
:type="activeDialogKey"
:visible="createEditDialogVisible"
:action-functions="actionFunctions"
:batch-form-data="formList"
:confirm-disabled="confirmDisabled"
:confirm-loading="confirmLoading"
>
<template #default>
<cl-auto-probe-form />
</template>
</cl-create-edit-dialog>
</template>

View File

@@ -0,0 +1,40 @@
import { computed } from 'vue';
import { Store } from 'vuex';
import { useForm } from '@/components';
import useAutoProbeService from '@/services/autoprobe/autoprobeService';
import { getDefaultFormComponentData } from '@/utils/form';
// form component data
const formComponentData = getDefaultFormComponentData<AutoProbe>();
const useAutoProbe = (store: Store<RootStoreState>) => {
// store
const ns = 'autoprobe';
const state = store.state[ns];
// form rules
const formRules: FormRules = {};
// all autoprobe select options
const allAutoProbeSelectOptions = computed<SelectOption[]>(() =>
state.allList.map(d => {
return {
label: d.name,
value: d._id,
};
})
);
return {
...useForm<AutoProbe>(
'autoprobe',
store,
useAutoProbeService(store),
formComponentData
),
formRules,
allAutoProbeSelectOptions,
};
};
export default useAutoProbe;

View File

@@ -10,6 +10,7 @@ import {
TASK_STATUS_RUNNING,
} from '@/constants/task';
import { translate } from '@/utils';
import { TagProps } from '@/components/ui/tag/types';
const props = defineProps<{
status: TaskStatus;

View File

@@ -19,6 +19,8 @@ import * as theme from './ui/lexical/utils/theme';
import * as VariableNode from './ui/lexical/nodes/VariableNode';
import AssistantConsole from './core/ai/AssistantConsole.vue';
import AtomMaterialIcon from './ui/icon/AtomMaterialIcon.vue';
import AutoProbeForm from './core/autoprobe/AutoProbeForm.vue';
import AutoProbeTaskStatus from './core/autoprobe/AutoProbeTaskStatus.vue';
import BlockOptionsDropdownList from './ui/lexical/components/BlockOptionsDropdownList.vue';
import Box from './ui/box/Box.vue';
import Button from './ui/button/Button.vue';
@@ -38,6 +40,7 @@ import CheckTagGroup from './ui/tag/CheckTagGroup.vue';
import ConfirmDialog from './ui/dialog/ConfirmDialog.vue';
import ContextMenu from './ui/context-menu/ContextMenu.vue';
import ContextMenuList from './ui/context-menu/ContextMenuList.vue';
import CreateEditAutoProbeDialog from './core/autoprobe/CreateEditAutoProbeDialog.vue';
import CreateEditDatabaseDialog from './core/database/CreateEditDatabaseDialog.vue';
import CreateEditDatabaseTableDialog from './core/database/CreateEditDatabaseTableDialog.vue';
import CreateEditDialog from './ui/dialog/CreateEditDialog.vue';
@@ -218,6 +221,7 @@ import UploadFilesDialog from './ui/file/UploadFilesDialog.vue';
import UploadGitFilesDialog from './core/git/UploadGitFilesDialog.vue';
import UploadSpiderFilesDialog from './core/spider/UploadSpiderFilesDialog.vue';
import useAssistantConsole from './core/ai/useAssistantConsole';
import useAutoProbe from './core/autoprobe/useAutoProbe';
import useCanShowPlaceholder from './ui/lexical/composables/useCanShowPlaceholder';
import useDatabase from './core/database/useDatabase';
import useDecorators from './ui/lexical/composables/useDecorators';
@@ -265,6 +269,8 @@ export {
VariableNode as VariableNode,
AssistantConsole as ClAssistantConsole,
AtomMaterialIcon as ClAtomMaterialIcon,
AutoProbeForm as ClAutoProbeForm,
AutoProbeTaskStatus as ClAutoProbeTaskStatus,
BlockOptionsDropdownList as ClBlockOptionsDropdownList,
Box as ClBox,
Button as ClButton,
@@ -284,6 +290,7 @@ export {
ConfirmDialog as ClConfirmDialog,
ContextMenu as ClContextMenu,
ContextMenuList as ClContextMenuList,
CreateEditAutoProbeDialog as ClCreateEditAutoProbeDialog,
CreateEditDatabaseDialog as ClCreateEditDatabaseDialog,
CreateEditDatabaseTableDialog as ClCreateEditDatabaseTableDialog,
CreateEditDialog as ClCreateEditDialog,
@@ -464,6 +471,7 @@ export {
UploadGitFilesDialog as ClUploadGitFilesDialog,
UploadSpiderFilesDialog as ClUploadSpiderFilesDialog,
useAssistantConsole as useAssistantConsole,
useAutoProbe as useAutoProbe,
useCanShowPlaceholder as useCanShowPlaceholder,
useDatabase as useDatabase,
useDecorators as useDecorators,

View File

@@ -80,6 +80,7 @@ const common: LCommon = {
proceed: 'Are you sure to proceed?',
create: 'Are you sure to create?',
continue: 'Are you sure to continue?',
setDefault: 'Are you sure to set as default?',
},
},
message: {
@@ -159,6 +160,7 @@ const common: LCommon = {
other: 'Other',
all: 'All',
unlimited: 'Unlimited',
preview: 'Preview',
},
placeholder: {
empty: 'Empty',

View File

@@ -1,4 +1,4 @@
const ai: LangAi = {
const ai: LComponentsAI = {
chatbot: {
title: 'Crawlab AI Assistant',
tooltip: 'Chat with AI Assistant',

View File

@@ -0,0 +1,29 @@
const autoprobe: LComponentsAutoProbe = {
form: {
name: 'Name',
url: 'URL',
query: 'Query',
},
task: {
status: {
label: {
pending: 'Pending',
running: 'Running',
completed: 'Completed',
failed: 'Failed',
cancelled: 'Cancelled',
unknown: 'Unknown',
},
tooltip: {
pending: 'The task is waiting to be processed',
running: 'The task is currently running',
completed: 'The task has been completed successfully',
failed: 'The task has failed',
cancelled: 'The task has been cancelled',
unknown: 'The task status is unknown',
},
},
},
};
export default autoprobe;

View File

@@ -27,6 +27,7 @@ import role from './role';
import tag from './tag';
import environment from './environment';
import ai from './ai';
import autoprobe from './autoprobe';
const components: LComponents = {
chart,
@@ -58,6 +59,7 @@ const components: LComponents = {
tag,
environment,
ai,
autoprobe,
};
export default components;

View File

@@ -227,6 +227,18 @@ const layouts: LLayouts = {
disclaimer: 'Disclaimer',
},
},
autoprobe: {
list: {
title: 'AutoProbe List',
},
detail: {
title: 'AutoProbe Detail',
tabs: {
overview: 'Overview',
tasks: 'Task',
},
},
}
},
};

View File

@@ -43,6 +43,7 @@ const router: LRouter = {
disclaimer: 'Disclaimer',
},
},
autoprobe: 'AutoProbe',
},
};

View File

@@ -0,0 +1,27 @@
const autoprobe: LViewsAutoProbe = {
table: {
columns: {
name: 'Name',
url: 'URL',
query: 'Query',
status: 'Status',
},
},
navActions: {
new: {
label: 'New AutoProbe',
tooltip: 'Create a new AutoProbe',
},
filter: {
search: {
placeholder: 'Search by name',
},
},
run: {
label: 'Run AutoProbe',
tooltip: 'Run the selected AutoProbe',
},
},
};
export default autoprobe;

View File

@@ -17,6 +17,7 @@ import environment from './environment';
import llm from './llm';
import system from './system';
import misc from './misc';
import autoprobe from './autoprobe';
const views: LViews = {
login,
@@ -38,5 +39,6 @@ const views: LViews = {
llm,
system,
misc,
autoprobe,
};
export default views;

View File

@@ -15,8 +15,7 @@ const system: LViewsSystem = {
apiBaseUrl: 'API Base URL',
apiVersion: 'API Version',
models: 'Models',
defaultModels: 'Default Models',
customModels: 'Custom Models',
defaultModel: 'Default Model',
addCustomModel: 'Add custom model',
noCustomModels: 'No custom models added',
modelAlreadyExists: 'Model already exists',

View File

@@ -80,6 +80,7 @@ const common: LCommon = {
proceed: '您是否确定继续?',
create: '您是否确定创建?',
continue: '您是否确定继续?',
setDefault: '您是否确定设为默认?',
},
},
message: {
@@ -159,6 +160,7 @@ const common: LCommon = {
other: '其他',
all: '全部',
unlimited: '无限制',
preview: '预览',
},
placeholder: {
empty: '空',

View File

@@ -1,4 +1,4 @@
const ai: LangAi = {
const ai: LComponentsAI = {
chatbot: {
title: 'Crawlab AI 助手',
tooltip: ' AI 助手聊天',

View File

@@ -0,0 +1,29 @@
const autoprobe: LComponentsAutoProbe = {
form: {
name: '名称',
url: 'URL',
query: '查询',
},
task: {
status: {
label: {
pending: '等待中',
running: '运行中',
completed: '已完成',
failed: '失败',
cancelled: '已取消',
unknown: '未知',
},
tooltip: {
pending: '任务正在等待处理',
running: '任务正在运行',
completed: '任务已成功完成',
failed: '任务执行失败',
cancelled: '任务已被取消',
unknown: '任务状态未知',
},
},
},
};
export default autoprobe;

View File

@@ -27,6 +27,7 @@ import role from './role';
import tag from './tag';
import environment from './environment';
import ai from './ai';
import autoprobe from './autoprobe';
const components: LComponents = {
chart,
@@ -58,6 +59,7 @@ const components: LComponents = {
tag,
environment,
ai,
autoprobe,
};
export default components;

View File

@@ -227,6 +227,18 @@ const layouts: LLayouts = {
disclaimer: '免责声明',
},
},
autoprobe: {
list: {
title: 'AutoProbe 列表',
},
detail: {
title: 'AutoProbe 详情',
tabs: {
overview: '概览',
tasks: '任务',
},
},
},
},
};

View File

@@ -43,6 +43,7 @@ const router: LRouter = {
disclaimer: '免责声明',
},
},
autoprobe: 'AutoProbe',
},
};

View File

@@ -0,0 +1,27 @@
const autoprobe: LViewsAutoProbe = {
table: {
columns: {
name: '名称',
url: 'URL',
query: '查询',
status: '状态',
},
},
navActions: {
new: {
label: '新建 AutoProbe',
tooltip: '创建新的 AutoProbe',
},
filter: {
search: {
placeholder: '按名称搜索',
},
},
run: {
label: '运行 AutoProbe',
tooltip: '运行选定的 AutoProbe',
},
},
};
export default autoprobe;

View File

@@ -17,6 +17,7 @@ import environment from './environment';
import llm from './llm';
import system from './system';
import misc from './misc';
import autoprobe from './autoprobe';
const views: LViews = {
login,
@@ -38,5 +39,6 @@ const views: LViews = {
llm,
system,
misc,
autoprobe,
};
export default views;

View File

@@ -15,8 +15,7 @@ const system: LViewsSystem = {
apiBaseUrl: 'API 基础 URL',
apiVersion: 'API 版本',
models: '模型',
defaultModels: '默认模型',
customModels: '自定义模型',
defaultModel: '默认模型',
addCustomModel: '添加自定义模型',
noCustomModels: '暂无自定义模型',
modelAlreadyExists: '模型已存在',

View File

@@ -93,6 +93,7 @@ export declare global {
proceed: string;
create: string;
continue: string;
setDefault: string;
};
};
message: {
@@ -172,6 +173,7 @@ export declare global {
other: string;
all: string;
unlimited: string;
preview: string;
};
placeholder: {
empty: string;

View File

@@ -1,5 +1,5 @@
export declare global {
interface LangAi {
interface LComponentsAI {
chatbot: {
title: string;
tooltip: string;

View File

@@ -0,0 +1,27 @@
interface LComponentsAutoProbe {
form: {
name: string;
url: string;
query: string;
};
task: {
status: {
label: {
pending: string;
running: string;
completed: string;
failed: string;
cancelled: string;
unknown: string;
};
tooltip: {
pending: string;
running: string;
completed: string;
failed: string;
cancelled: string;
unknown: string;
};
};
};
}

View File

@@ -20,7 +20,8 @@ export declare global {
environment: LComponentsEnvironment;
notification: LComponentsNotification;
editor: LComponentsEditor;
ai: LAi;
ai: LComponentsAI;
autoprobe: LComponentsAutoProbe;
// model-related components
node: LComponentsNode;

View File

@@ -122,6 +122,10 @@ export declare global {
pat: string;
disclaimer: string;
}>;
autoprobe: LListLayoutPage<{
overview: string;
tasks: string;
}>;
};
}
}

View File

@@ -44,6 +44,7 @@ export declare global {
disclaimer: string;
};
};
autoprobe: string;
};
}
}

View File

@@ -0,0 +1,16 @@
interface LViewsAutoProbe {
table: {
columns: {
name: string;
url: string;
query: string;
status: string;
};
};
navActions: LNavActions & {
run: {
label: string;
tooltip: string;
};
};
}

View File

@@ -19,5 +19,6 @@ export declare global {
environment: LViewsEnvironments;
llm: LViewsLLM;
system: LViewsSystem;
autoprobe: LViewsAutoProbe;
}
}

View File

@@ -15,8 +15,7 @@ interface LViewsSystem {
deploymentName: string;
apiVersion: string;
models: string;
defaultModels: string;
customModels: string;
defaultModel: string;
addCustomModel: string;
noCustomModels: string;
modelAlreadyExists: string;

View File

@@ -5,5 +5,7 @@ export declare global {
icon?: Icon;
hidden?: boolean;
routeConcept?: RouteConcept;
badge?: string;
badgeType?: BasicType;
}
}

View File

@@ -5,7 +5,7 @@ export declare global {
query?: string;
}
type AutoProbeTaskStatus = 'pending' | 'running' | 'completed' | 'failed';
type AutoProbeTaskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
type SelectorType = 'css' | 'xpath' | 'regex';
type ExtractType = 'text' | 'attribute' | 'html';
@@ -59,6 +59,7 @@ export declare global {
url?: string;
query?: string;
status: AutoProbeTaskStatus;
error?: string;
page_pattern?: PagePattern;
page_data?: PageData;
provider_id?: string;

View File

@@ -19,11 +19,11 @@ export declare global {
interface LLMProvider extends BaseModel {
type?: LLMProviderType;
name?: string;
enabled?: boolean;
api_key?: string;
api_base_url?: string;
api_version?: string;
models?: string[];
default_model?: string;
}
interface LLMProviderItem {

View File

@@ -16,11 +16,7 @@ export declare global {
auto_install?: boolean;
}
interface SettingAi {
enable_ai?: boolean;
api_key?: string;
model?: string;
max_tokens?: number;
temperature?: number;
interface SettingAI {
default_provider_id?: string;
}
}

View File

@@ -11,4 +11,7 @@ type AutoProbeStoreGetters = BaseStoreGetters<AutoProbe>;
interface AutoProbeStoreMutations extends BaseStoreMutations<AutoProbe> {}
interface AutoProbeStoreActions extends BaseStoreActions<AutoProbe> {}
interface AutoProbeStoreActions extends BaseStoreActions<AutoProbe> {
runTask: StoreAction<AutoProbeStoreState, { id: string }>;
cancelTask: StoreAction<AutoProbeStoreState, { id: string }>;
}

View File

@@ -26,7 +26,11 @@ defineOptions({ name: 'ClSidebarItem' });
>
<cl-menu-item-icon :item="item" size="normal" />
<template #title>
<span class="menu-item-title">{{ t(item.title) }}</span>
<span class="menu-item-title">
<el-badge :value="item.badge" :type="item.badgeType" :offset="[12, 0]">
{{ t(item.title) }}
</el-badge>
</span>
</template>
</el-menu-item>
@@ -58,6 +62,8 @@ defineOptions({ name: 'ClSidebarItem' });
}
.menu-item-title {
display: inline;
line-height: 24px;
margin-left: 6px;
}
}

View File

@@ -0,0 +1,47 @@
import { TAB_NAME_OVERVIEW, TAB_NAME_TASKS } from '@/constants/tab';
import {
ClAutoProbeList,
ClAutoProbeDetail,
ClAutoProbeDetailTabOverview,
ClAutoProbeDetailTabTasks,
} from '@/views';
import { getIconByTabName, translate } from '@/utils';
import { RouteLocation } from 'vue-router';
const t = translate;
const endpoint = '/autoprobes';
export default [
{
routeConcept: 'autoprobe',
name: 'AutoProbeList',
path: endpoint,
title: t('layouts.routes.autoprobe.list.title'),
component: async () => ClAutoProbeList,
},
{
routeConcept: 'autoprobe',
name: 'AutoProbeDetail',
path: `${endpoint}/:id`,
title: t('layouts.routes.autoprobe.detail.title'),
redirect: (to: RouteLocation) => {
return { path: to.path + '/overview' };
},
component: async () => ClAutoProbeDetail,
children: [
{
path: TAB_NAME_OVERVIEW,
title: t('layouts.routes.autoprobe.detail.tabs.overview'),
icon: getIconByTabName(TAB_NAME_OVERVIEW),
component: async () => ClAutoProbeDetailTabOverview,
},
{
path: TAB_NAME_TASKS,
title: t('layouts.routes.autoprobe.detail.tabs.tasks'),
icon: getIconByTabName(TAB_NAME_TASKS),
component: async () => ClAutoProbeDetailTabTasks,
},
],
},
] as Array<ExtendedRouterRecord>;

View File

@@ -13,18 +13,19 @@ import task from '@/router/task';
import schedule from '@/router/schedule';
import user from '@/router/user';
import role from '@/router/role';
import token from '@/router/token';
import notification from '@/router/notification';
import git from '@/router/git';
import database from '@/router/database';
import dependency from '@/router/dependency';
import environment from '@/router/environment';
import system from '@/router/system';
import misc from '@/router/misc';
import autoprobe from '@/router/autoprobe';
import { initRouterAuth } from '@/router/hooks/auth';
import { ROUTER_ROOT_NAME_ROOT } from '@/constants/router';
import { ClNormalLayout } from '@/layouts';
import { getIconByRouteConcept } from '@/utils';
import { getIconByRouteConcept, translate } from '@/utils';
const t = translate;
export function getDefaultRoutes(): Array<ExtendedRouterRecord> {
return [
@@ -48,9 +49,8 @@ export function getDefaultRoutes(): Array<ExtendedRouterRecord> {
...database,
...dependency,
...system,
// ...environment,
// ...token,
...misc,
...autoprobe,
],
},
];
@@ -130,6 +130,13 @@ export function getDefaultSidebarMenuItems(): MenuItem[] {
},
],
},
{
path: '/autoprobes',
title: 'router.menuItems.autoprobe',
icon: getIconByRouteConcept('autoprobe'),
badge: t('common.mode.preview'),
badgeType: 'primary',
},
{
path: '/users',
title: 'router.menuItems.users',

View File

@@ -0,0 +1,14 @@
import { Store } from 'vuex';
import { getDefaultService } from '@/utils';
const useAutoProbeService = (
store: Store<RootStoreState>
): Services<AutoProbe> => {
const ns: ListStoreNamespace = 'autoprobe';
return {
...getDefaultService<AutoProbe>(ns, store),
};
};
export default useAutoProbeService;

View File

@@ -20,6 +20,7 @@ import database from '@/store/modules/database';
import dependency from '@/store/modules/dependency';
import environment from '@/store/modules/environment';
import system from '@/store/modules/system';
import autoprobe from '@/store/modules/autoprobe';
let _store: Store<RootStoreState>;
@@ -47,6 +48,7 @@ export const createStore = (): Store<RootStoreState> => {
dependency,
environment,
system,
autoprobe,
},
});
};

View File

@@ -4,16 +4,20 @@ import {
getDefaultStoreMutations,
getDefaultStoreState,
} from '@/utils/store';
import { TAB_NAME_OVERVIEW } from '@/constants/tab';
import { TAB_NAME_OVERVIEW, TAB_NAME_TASKS } from '@/constants/tab';
import { translate } from '@/utils/i18n';
import useRequest from '@/services/request';
// i18n
const t = translate;
const { post } = useRequest();
const state = {
...getDefaultStoreState<AutoProbe>('autoprobe'),
tabs: [
{ id: TAB_NAME_OVERVIEW, title: t('common.tabs.overview') },
{ id: TAB_NAME_TASKS, title: t('common.tabs.tasks') },
],
} as AutoProbeStoreState;
@@ -26,7 +30,19 @@ const mutations = {
} as AutoProbeStoreMutations;
const actions = {
...getDefaultStoreActions<AutoProbe>('/projects'),
...getDefaultStoreActions<AutoProbe>('/ai/autoprobes'),
runTask: async (
_: StoreActionContext<AutoProbeStoreState>,
{ id }: { id: string }
) => {
await post(`/ai/autoprobes/${id}/tasks`);
},
cancelTask: async (
_: StoreActionContext<AutoProbeStoreState>,
{ id }: { id: string }
) => {
await post(`/ai/autoprobes/tasks/${id}/cancel`);
},
} as AutoProbeStoreActions;
export default {

View File

@@ -300,6 +300,8 @@ export const getIconByRouteConcept = (concept: RouteConcept): Icon => {
return ['fa', 'key'];
case 'disclaimer':
return ['fa', 'info-circle'];
case 'autoprobe':
return ['fa', 'satellite-dish'];
default:
return ['fa', 'circle'];
}

View File

@@ -0,0 +1,9 @@
<script setup lang="ts">
defineOptions({ name: 'ClAutoProbeDetail' });
</script>
<template>
<cl-detail-layout store-namespace="autoprobe" />
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
defineOptions({ name: 'ClAutoProbeDetailTabOverview' });
</script>
<template>
<div class="autoprobe-detail-tab-overview">
<cl-auto-probe-form />
</div>
</template>
<style scoped>
.autoprobe-detail-tab-overview {
margin: 20px;
}
</style>

View File

@@ -0,0 +1,236 @@
<script setup lang="tsx">
import { computed, onBeforeMount, ref } from 'vue';
import { getDefaultPagination, translate } from '@/utils';
import useRequest from '@/services/request';
import { ClNavLink, ClAutoProbeTaskStatus } from '@/components';
import { useDetail } from '@/layouts';
import { useStore } from 'vuex';
import { ElMessageBox, ElMessage } from 'element-plus';
import {
ACTION_CANCEL,
ACTION_DELETE,
ACTION_RESTART,
ACTION_VIEW,
TABLE_COLUMN_NAME_ACTIONS,
} from '@/constants';
const t = translate;
const ns: ListStoreNamespace = 'autoprobe';
const store = useStore();
const { getList, del } = useRequest();
const { activeId } = useDetail<AutoProbe>('autoprobe');
const tasks = ref<AutoProbeTask[]>([]);
const taskTotal = ref(0);
const getTasks = async () => {
const res = await getList(`/ai/autoprobes/${activeId.value}/tasks`, {
page: tablePagination.value.page,
size: tablePagination.value.size,
});
tasks.value = res.data || [];
taskTotal.value = res.total || 0;
};
// Cancel task function
const cancelTask = async (row: AutoProbeTask, force: boolean = false) => {
if (force) {
ElMessage.info(t('common.message.info.forceCancel'));
} else {
ElMessage.info(t('common.message.info.cancel'));
}
try {
await store.dispatch(`${ns}/cancelTask`, { id: row._id });
await getTasks(); // Refresh the list
ElMessage.success(t('common.message.success.cancel'));
} catch (error) {
console.error('Failed to cancel task:', error);
ElMessage.error(t('common.message.error.action'));
}
};
// Delete task function
const deleteTask = async (row: AutoProbeTask) => {
try {
await del(`/ai/autoprobes/tasks/${row._id}`);
ElMessage.success(t('common.message.success.delete'));
await getTasks(); // Refresh the list
} catch (error) {
console.error('Failed to delete task:', error);
ElMessage.error(t('common.message.error.action'));
}
};
const tableColumns = computed<TableColumns<AutoProbeTask>>(() => {
return [
{
key: 'url',
label: t('views.autoprobe.table.columns.url'),
icon: ['fa', 'at'],
width: 'auto',
minWidth: '400px',
value: (row: AutoProbeTask) => (
<ClNavLink path={row.url} label={row.url} external />
),
allowFilterSearch: true,
},
{
key: 'query',
label: t('views.autoprobe.table.columns.query'),
icon: ['fa', 'search'],
width: '200px',
value: (row: AutoProbeTask) => row.query,
},
{
key: 'status',
label: t('views.autoprobe.table.columns.status'),
icon: ['fa', 'info-circle'],
width: '120px',
value: (row: AutoProbeTask) => (
<ClAutoProbeTaskStatus status={row.status} error={row.error} />
),
},
{
key: TABLE_COLUMN_NAME_ACTIONS,
label: t('components.table.columns.actions'),
icon: ['fa', 'tools'],
width: '150px',
fixed: 'right',
buttons: (row: AutoProbeTask) =>
(
[
{
tooltip: t('common.actions.view'),
onClick: async (row: AutoProbeTask) => {
// View task details implementation
ElMessage.info('View task details - to be implemented');
},
action: ACTION_VIEW,
},
{
tooltip: t('common.actions.restart'),
contextMenu: true,
onClick: async (_: AutoProbeTask) => {
await ElMessageBox.confirm(
t('common.messageBox.confirm.restart'),
t('common.actions.restart'),
{
type: 'warning',
confirmButtonClass: 'restart-confirm-btn',
}
);
await store.dispatch(`${ns}/runTask`, {
id: activeId.value,
});
ElMessage.success(t('common.message.success.restart'));
await getTasks();
},
action: ACTION_RESTART,
},
{
tooltip: t('common.actions.cancel'),
contextMenu: true,
onClick: async (row: AutoProbeTask) => {
await ElMessageBox.confirm(
t('common.messageBox.confirm.cancel'),
t('common.actions.cancel'),
{
type: 'warning',
confirmButtonClass: 'cancel-confirm-btn',
}
);
await cancelTask(row, false);
},
action: ACTION_CANCEL,
},
{
tooltip: t('common.actions.delete'),
contextMenu: true,
onClick: async (row: AutoProbeTask) => {
await ElMessageBox.confirm(
t('common.messageBox.confirm.delete'),
t('common.actions.delete'),
{
type: 'warning',
confirmButtonClass: 'delete-confirm-btn',
}
);
await deleteTask(row);
},
action: ACTION_DELETE,
},
] as TableColumnButton<AutoProbeTask>[]
).filter(btn => {
switch (btn.action) {
case ACTION_CANCEL:
return row.status === 'pending' || row.status === 'running';
case ACTION_DELETE:
return row.status !== 'pending' && row.status !== 'running';
default:
return true;
}
}),
disableTransfer: true,
},
];
});
const tableData = computed(() => tasks.value);
const tablePagination = ref<TablePagination>(getDefaultPagination());
const tableTotal = computed(() => taskTotal.value);
const onTablePaginationChange = async (pagination: TablePagination) => {
tablePagination.value = pagination;
await getTasks();
};
const onClickRun = async () => {
try {
await store.dispatch(`${ns}/runTask`, { id: activeId.value });
await getTasks(); // Refresh the list after running a task
} catch (error) {
console.error('Failed to run task:', error);
ElMessage.error(t('common.message.error.action'));
}
};
onBeforeMount(async () => {
await Promise.all([getTasks()]);
});
defineOptions({ name: 'ClAutoProbeDetailTabTasks' });
</script>
<template>
<div class="tasks-container">
<cl-table
:columns="tableColumns"
:data="tableData"
:page="tablePagination.page"
:page-size="tablePagination.size"
:total="tableTotal"
selectable
embedded
@pagination-change="onTablePaginationChange"
>
<template #empty>
<cl-label-button
:icon="['fa', 'play']"
:label="t('views.autoprobe.navActions.run.label')"
@click="onClickRun"
/>
</template>
</cl-table>
</div>
</template>
<style scoped>
.tasks-container {
display: flex;
flex-direction: column;
height: 100%;
}
</style>

View File

@@ -1,9 +1,32 @@
<script setup lang="ts">
defineOptions({ name: 'ClExtractList' });
import { useAutoProbeList } from '@/views';
const {
navActions,
tableColumns,
tableData,
tableTotal,
tablePagination,
actionFunctions,
} = useAutoProbeList();
defineOptions({ name: 'ClAutoProbeList' });
</script>
<template>
<cl-list-layout
store-namespace="extract"
/>
class="autoprobe-list"
:action-functions="actionFunctions"
:nav-actions="navActions"
:table-pagination="tablePagination"
:table-columns="tableColumns"
:table-data="tableData"
:table-total="tableTotal"
>
<template #extra>
<!-- Dialogs (handled by store) -->
<cl-create-edit-auto-probe-dialog />
<!-- ./Dialogs -->
</template>
</cl-list-layout>
</template>

View File

@@ -1,13 +0,0 @@
import { useStore } from 'vuex';
import { useList } from '@/layouts';
const useAutoProbeList = () => {
const ns: ListStoreNamespace = 'autoprobe';
const store = useStore();
return {
...useList<AutoProbe>(ns, store),
};
};
export default useAutoProbeList;

View File

@@ -0,0 +1,154 @@
import { useStore } from 'vuex';
import { useList } from '@/layouts';
import { computed } from 'vue';
import {
ACTION_ADD,
ACTION_DELETE,
ACTION_FILTER,
ACTION_FILTER_SEARCH,
ACTION_RUN,
ACTION_VIEW,
ACTION_VIEW_SPIDERS,
FILTER_OP_CONTAINS,
TABLE_COLUMN_NAME_ACTIONS,
} from '@/constants';
import { getIconByAction, onListFilterChangeByKey, translate } from '@/utils';
import { ClNavLink } from '@/components';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
const t = translate;
const useAutoProbeList = () => {
const router = useRouter();
const ns: ListStoreNamespace = 'autoprobe';
const store = useStore();
const { commit } = store;
const { actionFunctions } = useList<AutoProbe>(ns, store);
const { deleteByIdConfirm } = actionFunctions;
// nav actions
const navActions = computed<ListActionGroup[]>(() => [
{
name: 'common',
children: [
{
action: ACTION_ADD,
id: 'add-btn',
className: 'add-btn',
buttonType: 'label',
label: t('views.autoprobe.navActions.new.label'),
tooltip: t('views.autoprobe.navActions.new.tooltip'),
icon: getIconByAction(ACTION_ADD),
onClick: () => {
commit(`${ns}/showDialog`, 'create');
},
},
],
},
{
action: ACTION_FILTER,
name: 'filter',
children: [
{
action: ACTION_FILTER_SEARCH,
id: 'filter-search',
className: 'search',
placeholder: t(
'views.autoprobe.navActions.filter.search.placeholder'
),
onChange: onListFilterChangeByKey(
store,
ns,
'name',
FILTER_OP_CONTAINS
),
},
],
},
]);
// table columns
const tableColumns = computed<TableColumns<AutoProbe>>(
() =>
[
{
className: 'name',
key: 'name',
label: t('views.autoprobe.table.columns.name'),
icon: ['fa', 'font'],
width: '150',
value: (row: AutoProbe) => (
<ClNavLink path={`/autoprobes/${row._id}`} label={row.name} />
),
hasSort: true,
hasFilter: true,
allowFilterSearch: true,
},
{
key: 'url',
label: t('views.autoprobe.table.columns.url'),
icon: ['fa', 'at'],
width: 'auto',
value: (row: AutoProbe) => (
<ClNavLink path={row.url} label={row.url} external />
),
hasFilter: true,
allowFilterSearch: true,
},
{
key: TABLE_COLUMN_NAME_ACTIONS,
label: t('components.table.columns.actions'),
fixed: 'right',
width: '150',
buttons: [
{
tooltip: t('common.actions.view'),
onClick: async row => {
await router.push(`/autoprobes/${row._id}`);
},
action: ACTION_VIEW,
},
{
tooltip: t('common.actions.run'),
onClick: async row => {
await ElMessageBox.confirm(
t('common.messageBox.confirm.run'),
t('common.actions.restart'),
{
type: 'warning',
confirmButtonClass: 'confirm-btn',
}
);
try {
await store.dispatch(`${ns}/runTask`, { id: row._id });
ElMessage.success(t('common.message.success.run'));
} catch (e) {
ElMessage.error((e as Error).message);
}
},
action: ACTION_RUN,
contextMenu: true,
},
{
tooltip: t('common.actions.delete'),
onClick: deleteByIdConfirm,
action: ACTION_DELETE,
contextMenu: true,
},
],
disableTransfer: true,
},
] as TableColumns<AutoProbe>
);
return {
...useList<AutoProbe>(ns, store),
navActions,
tableColumns,
};
};
export default useAutoProbeList;

View File

@@ -35,7 +35,6 @@ const useEnvironmentList = () => {
label: t('views.environment.navActions.new.label'),
tooltip: t('views.environment.navActions.new.tooltip'),
icon: ['fa', 'plus'],
type: 'success',
onClick: async () => {
commit(`${ns}/showDialog`, 'create');
},

View File

@@ -1,3 +1,7 @@
import AutoProbeDetail from './autoprobe/detail/AutoProbeDetail.vue';
import AutoProbeDetailTabOverview from './autoprobe/detail/tabs/AutoProbeDetailTabOverview.vue';
import AutoProbeDetailTabTasks from './autoprobe/detail/tabs/AutoProbeDetailTabTasks.vue';
import AutoProbeList from './autoprobe/list/AutoProbeList.vue';
import DatabaseDetail from './database/detail/DatabaseDetail.vue';
import DatabaseDetailActionsCommon from './database/detail/actions/DatabaseDetailActionsCommon.vue';
import DatabaseDetailActionsConsole from './database/detail/actions/DatabaseDetailActionsConsole.vue';
@@ -87,6 +91,7 @@ import TaskDetailTabLogs from './task/detail/tabs/TaskDetailTabLogs.vue';
import TaskDetailTabOverview from './task/detail/tabs/TaskDetailTabOverview.vue';
import TaskList from './task/list/TaskList.vue';
import TokenList from './token/list/TokenList.vue';
import useAutoProbeList from './autoprobe/list/useAutoProbeList';
import useDatabaseDetail from './database/detail/useDatabaseDetail';
import useDatabaseList from './database/list/useDatabaseList';
import useDependencyList from './dependency/list/useDependencyList';
@@ -118,6 +123,10 @@ import useUserDetail from './user/detail/useUserDetail';
import useUserList from './user/list/useUserList';
export {
AutoProbeDetail as ClAutoProbeDetail,
AutoProbeDetailTabOverview as ClAutoProbeDetailTabOverview,
AutoProbeDetailTabTasks as ClAutoProbeDetailTabTasks,
AutoProbeList as ClAutoProbeList,
DatabaseDetail as ClDatabaseDetail,
DatabaseDetailActionsCommon as ClDatabaseDetailActionsCommon,
DatabaseDetailActionsConsole as ClDatabaseDetailActionsConsole,
@@ -207,6 +216,7 @@ export {
TaskDetailTabOverview as ClTaskDetailTabOverview,
TaskList as ClTaskList,
TokenList as ClTokenList,
useAutoProbeList as useAutoProbeList,
useDatabaseDetail as useDatabaseDetail,
useDatabaseList as useDatabaseList,
useDependencyList as useDependencyList,

View File

@@ -1,4 +1,4 @@
import { computed, h } from 'vue';
import { computed } from 'vue';
import { TABLE_COLUMN_NAME_ACTIONS } from '@/constants/table';
import { useStore } from 'vuex';
import useList from '@/layouts/content/list/useList';

View File

@@ -1,26 +1,53 @@
<script setup lang="tsx">
import { ref, computed, onBeforeMount } from 'vue';
import { ElSpace, ElMessage, ElMessageBox } from 'element-plus';
import { ClSwitch, ClTag, ClNavLink, ClIcon } from '@/components';
import { ElSpace, ElMessage, ElMessageBox, ElCheckbox } from 'element-plus';
import { ClTag, ClNavLink, ClIcon } from '@/components';
import useRequest from '@/services/request';
import { getDefaultPagination, plainClone, translate } from '@/utils';
import {
ACTION_DELETE,
ACTION_EDIT,
ACTION_VIEW,
TABLE_COLUMN_NAME_ACTIONS,
} from '@/constants';
import { getLLMProviderItems } from '@/utils/ai';
const t = translate;
const { getList, put, post, del } = useRequest();
const { get, getList, put, post, del } = useRequest();
const llmProviders = ref<LLMProvider[]>([]);
const llmProvidersTotal = ref(0);
const form = ref<LLMProvider>();
const formRef = ref();
const settingAI = ref<Setting<SettingAI>>();
const defaultProviderId = computed(
() => settingAI.value?.value?.default_provider_id
);
const getSettingAI = async () => {
const res = await get('/settings/ai');
settingAI.value = res.data;
};
const updateDefaultProviderId = async (id: string) => {
try {
const data = {
...settingAI.value,
value: {
...settingAI.value?.value,
default_provider_id: id,
},
};
if (!settingAI.value) {
await post('/settings/ai', { data });
} else {
await put('/settings/ai', { data });
}
await getSettingAI();
} catch (e) {
ElMessage.error((e as Error).message);
}
};
const getLlmProviderItem = (type: LLMProviderType) => {
return getLLMProviderItems().find(item => item.type === type);
};
@@ -54,9 +81,15 @@ const onConfirm = async () => {
data: form.value,
});
} else {
await post('/ai/llm/providers', {
data: form.value,
});
const res = await post<any, ResponseWithData<LLMProvider>>(
'/ai/llm/providers',
{
data: form.value,
}
);
if (!settingAI.value?.value?.default_provider_id) {
await updateDefaultProviderId(res.data?._id!);
}
}
dialogVisible.value = false;
form.value = undefined;
@@ -108,29 +141,24 @@ const tableColumns = computed<TableColumns<LLMProvider>>(() => {
},
},
{
key: 'enabled',
label: t('views.system.ai.enabled'),
key: 'default',
label: t('common.mode.default'),
width: '90px',
value: (row: LLMProvider) => {
const isDefault = row._id === defaultProviderId.value;
return (
<ClSwitch
modelValue={row.enabled}
onChange={async (enabled: boolean) => {
const originalEnabled = row.enabled;
row.enabled = enabled;
try {
await put(`/ai/llm/providers/${row._id}`, {
data: { ...row, enabled },
});
ElMessage.success(
t(
`common.message.success.${enabled ? 'enabled' : 'disabled'}`
)
);
} catch (e) {
ElMessage.error((e as Error).message);
row.enabled = originalEnabled;
}
<ElCheckbox
modelValue={isDefault}
disabled={row._id === defaultProviderId.value}
onChange={async () => {
await ElMessageBox.confirm(
t('common.messageBox.confirm.setDefault'),
{
type: 'warning',
}
);
await updateDefaultProviderId(row._id!);
ElMessage.success(t('common.message.success.action'));
}}
/>
);
@@ -145,7 +173,15 @@ const tableColumns = computed<TableColumns<LLMProvider>>(() => {
return (
<ElSpace direction="horizontal" gap={8} wrap>
{row.models?.map(model => {
return <ClTag label={model} />;
if (row.default_model === model) {
return (
<ClTag
label={`${model} (${t('common.mode.default')})`}
type="warning"
/>
);
}
return <ClTag label={model} type="primary" />;
})}
</ElSpace>
);
@@ -194,7 +230,9 @@ const dialogTitle = computed(() => {
});
const dialogConfirmLoading = ref(false);
onBeforeMount(getLLMProviderList);
onBeforeMount(async () => {
await Promise.all([getSettingAI(), getLLMProviderList()]);
});
defineOptions({ name: 'ClSystemDetailTabModels' });
</script>