Compare commits
4 Commits
8eaac4df96
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 154823b2c4 | |||
| eeedce5b5c | |||
| 57f29f855b | |||
| b085ba3c60 |
14
README.md
14
README.md
@@ -1,7 +1,13 @@
|
|||||||
# Tauri + Vue + TypeScript
|
# Spary
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
Proxy software launcher
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## Build
|
||||||
|
|
||||||
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
### Generate icons
|
||||||
|
|
||||||
|
```shell
|
||||||
|
yarn tauri icon src/assets/logo.svg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start For Dev
|
||||||
|
|||||||
2
api/api.http
Normal file
2
api/api.http
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# 获取开关状态
|
||||||
|
GET 127.0.0.1:3000/mcp/tools/list
|
||||||
@@ -27,3 +27,6 @@ tauri-plugin-fs = "2"
|
|||||||
tauri-plugin-os = "2"
|
tauri-plugin-os = "2"
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
|
rocket = { version = "0.5", features = ["json"] }
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
use tauri::Manager;
|
|
||||||
use crate::spary::spary_switch;
|
use crate::spary::spary_switch;
|
||||||
|
use std::sync::Arc;
|
||||||
use tauri::menu::{Menu, MenuItem};
|
use tauri::menu::{Menu, MenuItem};
|
||||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||||
|
use tauri::Manager;
|
||||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
mod mcp_server;
|
||||||
mod spary;
|
mod spary;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -71,8 +74,60 @@ pub fn run() {
|
|||||||
})
|
})
|
||||||
.build(app)?;
|
.build(app)?;
|
||||||
tray.set_menu(Some(menu))?;
|
tray.set_menu(Some(menu))?;
|
||||||
|
|
||||||
|
// Start the MCP HTTP server in a background task
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
start_mcp_server(app_handle);
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_mcp_server(app_handle: tauri::AppHandle) {
|
||||||
|
// Disable MCP server for now
|
||||||
|
if false {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
use mcp_server::AppState;
|
||||||
|
|
||||||
|
// Create the shared application state
|
||||||
|
let state = AppState {
|
||||||
|
app_handle,
|
||||||
|
spray_status: Arc::new(Mutex::new(false)), // Initial spray status is off
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configure Rocket with custom settings
|
||||||
|
let rocket = rocket::build()
|
||||||
|
.configure(rocket::Config {
|
||||||
|
port: 3000,
|
||||||
|
address: "127.0.0.1".parse().expect("Invalid address"),
|
||||||
|
..rocket::Config::default()
|
||||||
|
})
|
||||||
|
.manage(state)
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
rocket::routes![
|
||||||
|
mcp_server::initialize,
|
||||||
|
mcp_server::get_capabilities,
|
||||||
|
mcp_server::list_tools,
|
||||||
|
mcp_server::call_tool,
|
||||||
|
mcp_server::set_spray_status,
|
||||||
|
mcp_server::get_spray_status
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("MCP server running on http://127.0.0.1:3000");
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
rocket.launch().await.unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
181
src-tauri/src/mcp_server.rs
Normal file
181
src-tauri/src/mcp_server.rs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
use rocket::{State, serde::{Deserialize, Serialize, json::Json}};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tauri::AppHandle;
|
||||||
|
|
||||||
|
use crate::spary::spary_switch;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub app_handle: AppHandle,
|
||||||
|
pub spray_status: Arc<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct InitializeResponse {
|
||||||
|
protocol_version: String,
|
||||||
|
server_info: ServerInfo,
|
||||||
|
capabilities: Capabilities,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct ServerInfo {
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct Capabilities {
|
||||||
|
experimental: Option<Value>,
|
||||||
|
tools: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::post("/mcp/initialize", data = "<_data>")]
|
||||||
|
pub async fn initialize(_state: &State<AppState>, _data: Json<Value>) -> Json<InitializeResponse> {
|
||||||
|
Json(InitializeResponse {
|
||||||
|
protocol_version: "1.0".to_string(),
|
||||||
|
server_info: ServerInfo {
|
||||||
|
name: "spary-mcp-server".to_string(),
|
||||||
|
version: "0.1.0".to_string(),
|
||||||
|
},
|
||||||
|
capabilities: Capabilities {
|
||||||
|
experimental: None,
|
||||||
|
tools: Some(json!({
|
||||||
|
"list": true,
|
||||||
|
"call": true
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/mcp/capabilities")]
|
||||||
|
pub async fn get_capabilities(_state: &State<AppState>) -> Json<Value> {
|
||||||
|
Json(json!({
|
||||||
|
"tools": {
|
||||||
|
"list": true,
|
||||||
|
"call": true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
struct Tool {
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
input_schema: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/mcp/tools/list")]
|
||||||
|
pub async fn list_tools(_state: &State<AppState>) -> Json<Value> {
|
||||||
|
let tools = vec![
|
||||||
|
Tool {
|
||||||
|
name: "spray_toggle".to_string(),
|
||||||
|
description: "Toggle the spray functionality on or off".to_string(),
|
||||||
|
input_schema: json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to turn spray on (true) or off (false)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status"]
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
Json(json!({ "tools": tools }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct ToolCallRequest {
|
||||||
|
name: String,
|
||||||
|
arguments: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::post("/mcp/tools/call", data = "<payload>")]
|
||||||
|
pub async fn call_tool(
|
||||||
|
state: &State<AppState>,
|
||||||
|
payload: Json<ToolCallRequest>,
|
||||||
|
) -> Result<Json<Value>, rocket::http::Status> {
|
||||||
|
match payload.name.as_str() {
|
||||||
|
"spray_toggle" => {
|
||||||
|
if let Some(status) = payload.arguments.get("status").and_then(|v| v.as_bool()) {
|
||||||
|
// Call the existing spray_switch function
|
||||||
|
spary_switch(status);
|
||||||
|
|
||||||
|
// Update the spray status in the shared state
|
||||||
|
{
|
||||||
|
let mut spray_status = state.spray_status.lock().await;
|
||||||
|
*spray_status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"result": {
|
||||||
|
"success": true,
|
||||||
|
"message": format!("Spray toggled to {}", status)
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
Err(rocket::http::Status::BadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(rocket::http::Status::NotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct SprayRequest {
|
||||||
|
status: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct SprayResponse {
|
||||||
|
success: bool,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::post("/v1/spray", data = "<payload>")]
|
||||||
|
pub async fn set_spray_status(
|
||||||
|
state: &State<AppState>,
|
||||||
|
payload: Json<SprayRequest>,
|
||||||
|
) -> Json<SprayResponse> {
|
||||||
|
// Call the existing spray_switch function
|
||||||
|
spary_switch(payload.status);
|
||||||
|
|
||||||
|
// Update the spray status in the shared state
|
||||||
|
{
|
||||||
|
let mut spray_status = state.spray_status.lock().await;
|
||||||
|
*spray_status = payload.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(SprayResponse {
|
||||||
|
success: true,
|
||||||
|
message: format!("Spray toggled to {}", payload.status),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct SprayStatusResponse {
|
||||||
|
status: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/v1/spray")]
|
||||||
|
pub async fn get_spray_status(
|
||||||
|
state: &State<AppState>,
|
||||||
|
) -> Json<SprayStatusResponse> {
|
||||||
|
let spray_status = state.spray_status.lock().await;
|
||||||
|
Json(SprayStatusResponse {
|
||||||
|
status: *spray_status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
10
src/App.vue
10
src/App.vue
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<notification-provider/>
|
<notification-provider/>
|
||||||
<v-main>
|
<v-main class="d-flex" style="height: 100vh;">
|
||||||
<v-card class="fill-height">
|
<v-card class="d-flex flex-column" style="height: 100%; width: 100%;">
|
||||||
<v-layout class="fill-height">
|
<v-layout class="d-flex flex-column" style="height: 100%;">
|
||||||
<main-drawer/>
|
<main-drawer/>
|
||||||
<v-main style="height: 100vh">
|
<v-card class="flex-grow-1 overflow-y-auto ma-4">
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</v-main>
|
</v-card>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-main>
|
</v-main>
|
||||||
|
|||||||
2
src/components.d.ts
vendored
2
src/components.d.ts
vendored
@@ -13,11 +13,13 @@ declare module 'vue' {
|
|||||||
LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default']
|
LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default']
|
||||||
MainConsole: typeof import('./components/index/mainConsole.vue')['default']
|
MainConsole: typeof import('./components/index/mainConsole.vue')['default']
|
||||||
MainDrawer: typeof import('./components/index/mainDrawer.vue')['default']
|
MainDrawer: typeof import('./components/index/mainDrawer.vue')['default']
|
||||||
|
NodeConfigurator: typeof import('./components/nodeEdit/nodeConfigurator.vue')['default']
|
||||||
NodeList: typeof import('./components/nodeEdit/nodeList.vue')['default']
|
NodeList: typeof import('./components/nodeEdit/nodeList.vue')['default']
|
||||||
NodesFloatButton: typeof import('./components/nodeEdit/nodesFloatButton.vue')['default']
|
NodesFloatButton: typeof import('./components/nodeEdit/nodesFloatButton.vue')['default']
|
||||||
NotificationProvider: typeof import('./components/notify/notificationProvider.vue')['default']
|
NotificationProvider: typeof import('./components/notify/notificationProvider.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
SchemaField: typeof import('./components/nodeEdit/SchemaField.vue')['default']
|
||||||
Spary: typeof import('./components/index/spary.vue')['default']
|
Spary: typeof import('./components/index/spary.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
132
src/components/nodeEdit/SchemaField.vue
Normal file
132
src/components/nodeEdit/SchemaField.vue
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, toRaw} from 'vue';
|
||||||
|
import { z, ZodObject, ZodString, ZodNumber, ZodBoolean, ZodEnum, ZodOptional } from 'zod';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SchemaField'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
schema: z.ZodTypeAny;
|
||||||
|
modelValue: any;
|
||||||
|
label: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 使用toRaw获取原始schema对象,避免Proxy问题
|
||||||
|
const rawSchema = toRaw(props.schema);
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const isOptional = computed(() => rawSchema.optional());
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeName = computed(() => {
|
||||||
|
if (rawSchema instanceof ZodObject) return 'ZodObject';
|
||||||
|
if (rawSchema instanceof ZodString) return 'ZodString';
|
||||||
|
if (rawSchema instanceof ZodNumber) return 'ZodNumber';
|
||||||
|
if (rawSchema instanceof ZodBoolean) return 'ZodBoolean';
|
||||||
|
if (rawSchema instanceof ZodEnum) return 'ZodEnum';
|
||||||
|
if (rawSchema instanceof ZodOptional) return 'ZodOptional';
|
||||||
|
// Fallback for unknown types or types not handled explicitly
|
||||||
|
return 'Unknown';
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectValue = computed(() => value.value as Record<string, any> | null);
|
||||||
|
|
||||||
|
const objectSchema = computed(() => {
|
||||||
|
const schema = rawSchema;
|
||||||
|
if (schema instanceof ZodOptional) {
|
||||||
|
const unwrapped = schema.unwrap();
|
||||||
|
if (unwrapped instanceof ZodObject) {
|
||||||
|
return unwrapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (schema instanceof ZodObject) {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="schema-field">
|
||||||
|
<template v-if="typeName === 'ZodString'">
|
||||||
|
<v-text-field
|
||||||
|
v-model="value"
|
||||||
|
:label="label"
|
||||||
|
:hint="isOptional ? 'Optional' : ''"
|
||||||
|
persistent-hint
|
||||||
|
></v-text-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="typeName === 'ZodNumber'">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="value"
|
||||||
|
type="number"
|
||||||
|
:label="label"
|
||||||
|
:hint="isOptional ? 'Optional' : ''"
|
||||||
|
persistent-hint
|
||||||
|
></v-text-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="typeName === 'ZodBoolean'">
|
||||||
|
<v-switch
|
||||||
|
v-model="value"
|
||||||
|
:label="label"
|
||||||
|
color="primary"
|
||||||
|
:hint="isOptional ? 'Optional' : ''"
|
||||||
|
persistent-hint
|
||||||
|
></v-switch>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="typeName === 'ZodEnum'">
|
||||||
|
<v-select
|
||||||
|
v-model="value"
|
||||||
|
:items="(rawSchema as ZodEnum<any>).options"
|
||||||
|
:label="label"
|
||||||
|
:hint="isOptional ? 'Optional' : ''"
|
||||||
|
persistent-hint
|
||||||
|
></v-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="objectSchema">
|
||||||
|
<v-card variant="outlined" class="pa-4 mb-4">
|
||||||
|
<div class="text-subtitle-1 font-weight-medium">{{ label }}</div>
|
||||||
|
<div v-for="(field, key) in objectSchema.shape" :key="key">
|
||||||
|
<SchemaField
|
||||||
|
:schema="field"
|
||||||
|
:label="key.toString()"
|
||||||
|
:model-value="objectValue ? objectValue[key] : undefined"
|
||||||
|
@update:modelValue="newValue => {
|
||||||
|
const newObjectValue = { ...value.value };
|
||||||
|
newObjectValue[key] = newValue;
|
||||||
|
value.value = newObjectValue;
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="typeName === 'ZodOptional'">
|
||||||
|
<SchemaField
|
||||||
|
:schema="(rawSchema as ZodOptional<any>).unwrap()"
|
||||||
|
v-model="value"
|
||||||
|
:label="label"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<p class="text-caption">Unsupported type: {{ typeName }} for {{label}}</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.schema-field {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,6 +4,9 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import {Group, groupRepository} from "@/entities/group.ts";
|
import {Group, groupRepository} from "@/entities/group.ts";
|
||||||
import {nodeRepository} from "@/entities/node.ts";
|
import {nodeRepository} from "@/entities/node.ts";
|
||||||
import {notify} from "@/components/notify/notifyStore.ts";
|
import {notify} from "@/components/notify/notifyStore.ts";
|
||||||
|
import {ConfigurationSchema} from "@/utils/core/configurator/schema/schema.ts";
|
||||||
|
import {CoreTypes} from "@/utils/core/CoreDef.ts";
|
||||||
|
import NodeConfigurator from "@/components/nodeEdit/nodeConfigurator.vue";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -11,6 +14,8 @@ const props = defineProps<{ groupId: string }>()
|
|||||||
|
|
||||||
const allGroups = ref<Group[]>([])
|
const allGroups = ref<Group[]>([])
|
||||||
const selectedGroupId = ref<number | null>(null)
|
const selectedGroupId = ref<number | null>(null)
|
||||||
|
const configurationSchema= ref<ConfigurationSchema | null>(null)
|
||||||
|
const nodeConfig = ref<object | null>(null)
|
||||||
|
|
||||||
const loadGroups = async () => {
|
const loadGroups = async () => {
|
||||||
allGroups.value = await groupRepository.findAll()
|
allGroups.value = await groupRepository.findAll()
|
||||||
@@ -21,33 +26,47 @@ const loadGroups = async () => {
|
|||||||
onMounted(loadGroups)
|
onMounted(loadGroups)
|
||||||
|
|
||||||
const nodeAlias = ref('')
|
const nodeAlias = ref('')
|
||||||
const nodeArguments = ref<string | null>(null)
|
|
||||||
const isAdding = ref(false)
|
const isAdding = ref(false)
|
||||||
|
|
||||||
const isAddDisabled = computed(() => {
|
const isAddDisabled = computed(() => {
|
||||||
return !nodeAlias.value || nodeAlias.value.length < 3 || nodeAlias.value.length > 15 || isAdding.value
|
return !nodeAlias.value || nodeAlias.value.length < 3 || nodeAlias.value.length > 15 || isAdding.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function onConfigurationSchemaChange(value: any) {
|
||||||
|
console.log('Configuration schema changed:', value);
|
||||||
|
// 确保设置的值是正确的对象结构
|
||||||
|
configurationSchema.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
async function addNode() {
|
async function addNode() {
|
||||||
if (!isAddDisabled.value && selectedGroupId.value !== null) {
|
if (!isAddDisabled.value && selectedGroupId.value !== null) {
|
||||||
isAdding.value = true
|
isAdding.value = true
|
||||||
await nodeRepository.insert({
|
try {
|
||||||
created_at: null,
|
await nodeRepository.insert({
|
||||||
id: null,
|
created_at: null,
|
||||||
updated_at: null,
|
id: null,
|
||||||
alias: nodeAlias.value,
|
updated_at: null,
|
||||||
arguments: nodeArguments.value,
|
alias: nodeAlias.value,
|
||||||
group_id: selectedGroupId.value
|
arguments: nodeConfig.value || {},
|
||||||
})
|
group_id: selectedGroupId.value
|
||||||
notify(t('addNode.nodeAddedSuccess'))
|
})
|
||||||
isAdding.value = false
|
notify(t('addNode.nodeAddedSuccess'))
|
||||||
|
// Reset form after successful submission
|
||||||
|
nodeAlias.value = ''
|
||||||
|
nodeConfig.value = null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding node:', error)
|
||||||
|
notify(t('addNode.nodeAddedError'))
|
||||||
|
} finally {
|
||||||
|
isAdding.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<v-sheet class="mx-auto" width="80vw">
|
<v-sheet class="mx-auto" width="80vw">
|
||||||
<v-form fast-fail @submit.prevent>
|
<v-form fast-fail @submit.prevent="addNode">
|
||||||
<v-select
|
<v-select
|
||||||
v-model="selectedGroupId"
|
v-model="selectedGroupId"
|
||||||
:label="$t('addNode.group')"
|
:label="$t('addNode.group')"
|
||||||
@@ -62,17 +81,24 @@ async function addNode() {
|
|||||||
:label="$t('addNode.nodeAlias')"
|
:label="$t('addNode.nodeAlias')"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
<v-textarea
|
<v-select
|
||||||
v-model="nodeArguments"
|
v-model="configurationSchema"
|
||||||
:label="$t('addNode.arguments')"
|
:label="$t('addNode.nodeType')"
|
||||||
></v-textarea>
|
:items="CoreTypes"
|
||||||
|
item-title="name"
|
||||||
|
return-object
|
||||||
|
variant="solo"
|
||||||
|
@update:modelValue="onConfigurationSchemaChange">
|
||||||
|
</v-select>
|
||||||
|
|
||||||
|
<node-configurator v-if="configurationSchema" :configuration-schema="configurationSchema" v-model="nodeConfig"/>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
type="submit"
|
type="submit"
|
||||||
block
|
block
|
||||||
:disabled="isAddDisabled"
|
:disabled="isAddDisabled"
|
||||||
:loading="isAdding"
|
:loading="isAdding">
|
||||||
@click="addNode">
|
|
||||||
{{ $t('addNode.addNodeButton') }}
|
{{ $t('addNode.addNodeButton') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-form>
|
</v-form>
|
||||||
|
|||||||
58
src/components/nodeEdit/nodeConfigurator.vue
Normal file
58
src/components/nodeEdit/nodeConfigurator.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { watch, ref, computed, toRaw} from 'vue';
|
||||||
|
import { ConfigurationSchema } from "@/utils/core/configurator/schema/schema.ts";
|
||||||
|
import SchemaField from './SchemaField.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
configurationSchema: ConfigurationSchema,
|
||||||
|
modelValue: object | null, // Allow null for initial state
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
// Use a ref instead of a computed property with setter to avoid proxy issues
|
||||||
|
const localConfig = ref<Record<string, any>>(props.modelValue || {});
|
||||||
|
|
||||||
|
// Watch for changes in the configurationSchema prop to reset internalConfig
|
||||||
|
watch(() => props.configurationSchema, () => {
|
||||||
|
// Reset localConfig to an empty object when schema changes
|
||||||
|
localConfig.value = {};
|
||||||
|
emit('update:modelValue', {});
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Watch for changes in modelValue prop to update localConfig
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
localConfig.value = newVal || {};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes in localConfig to emit updates
|
||||||
|
watch(localConfig, (newVal) => {
|
||||||
|
emit('update:modelValue', newVal);
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// 使用toRaw来避免响应式代理问题
|
||||||
|
const mainSchema = computed(() => {
|
||||||
|
const rawSchema = toRaw(props.configurationSchema);
|
||||||
|
return rawSchema.schema;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="pa-4" variant="tonal">
|
||||||
|
<div v-for="(field, key) in mainSchema.shape" :key="key.toString()">
|
||||||
|
<SchemaField
|
||||||
|
:schema="field"
|
||||||
|
:label="key.toString()"
|
||||||
|
:model-value="localConfig[key.toString()]"
|
||||||
|
@update:modelValue="newValue => {
|
||||||
|
const newConfig = { ...localConfig.value };
|
||||||
|
newConfig[key.toString()] = newValue;
|
||||||
|
localConfig.value = newConfig;
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
"arguments": "Arguments",
|
"arguments": "Arguments",
|
||||||
"addGroupButton": "Add",
|
"addGroupButton": "Add",
|
||||||
"groupExists": "Group already exists.",
|
"groupExists": "Group already exists.",
|
||||||
"groupAdded": "Group added."
|
"groupAdded": "Group added.",
|
||||||
|
"nodeType": "Node Type"
|
||||||
},
|
},
|
||||||
"addNode": {
|
"addNode": {
|
||||||
"title": "Add Node",
|
"title": "Add Node",
|
||||||
@@ -40,7 +41,8 @@
|
|||||||
"nodeAlias": "Node alias",
|
"nodeAlias": "Node alias",
|
||||||
"arguments": "Arguments",
|
"arguments": "Arguments",
|
||||||
"addNodeButton": "Add",
|
"addNodeButton": "Add",
|
||||||
"nodeAddedSuccess": "Node added successfully"
|
"nodeAddedSuccess": "Node added successfully",
|
||||||
|
"nodeAddedError": "Error adding node"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
|||||||
@@ -41,7 +41,9 @@
|
|||||||
"nodeAlias": "节点别名",
|
"nodeAlias": "节点别名",
|
||||||
"arguments": "参数",
|
"arguments": "参数",
|
||||||
"addNodeButton": "添加",
|
"addNodeButton": "添加",
|
||||||
"nodeAddedSuccess": "节点添加成功"
|
"nodeAddedSuccess": "节点添加成功",
|
||||||
|
"nodeAddedError": "添加节点时出错",
|
||||||
|
"nodeType": "节点类型"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
|
|||||||
8
src/utils/core/CoreDef.ts
Normal file
8
src/utils/core/CoreDef.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import {XraySchema} from "@/utils/core/configurator/schema/xray.schema.ts";
|
||||||
|
import {createConfigurationSchema} from "@/utils/core/configurator/schema/schema.ts";
|
||||||
|
import {V2flySchema} from "@/utils/core/configurator/schema/v2fly.schema.ts";
|
||||||
|
|
||||||
|
export const CoreTypes = [
|
||||||
|
createConfigurationSchema("xray", XraySchema),
|
||||||
|
createConfigurationSchema("v2fly", V2flySchema),
|
||||||
|
]
|
||||||
13
src/utils/core/configurator/schema/schema.ts
Normal file
13
src/utils/core/configurator/schema/schema.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import {ZodObject} from "zod";
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
|
// Use a simple plain object type to avoid Proxy issues
|
||||||
|
export type ConfigurationSchema = {
|
||||||
|
name: string;
|
||||||
|
schema: ZodObject<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createConfigurationSchema(name: string, schema: ZodObject<any>): ConfigurationSchema {
|
||||||
|
// Use markRaw to mark the schema object to prevent Vue from converting it to reactive, thus avoiding Proxy issues
|
||||||
|
return { name, schema: markRaw(schema) };
|
||||||
|
}
|
||||||
278
src/utils/core/configurator/schema/v2fly.schema.ts
Normal file
278
src/utils/core/configurator/schema/v2fly.schema.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
const LogObject = z.object({
|
||||||
|
access: z.string().optional(),
|
||||||
|
error: z.string().optional(),
|
||||||
|
loglevel: z.enum(["debug", "info", "warning", "error", "none"]).optional(),
|
||||||
|
dnsLog: z.boolean().optional(),
|
||||||
|
maskAddress: z.enum(["quarter", "half", "full"]).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const ApiObject = z.object({
|
||||||
|
tag: z.string().optional(),
|
||||||
|
listen: z.string().optional(),
|
||||||
|
services: z.array(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const DnsObject = z.object({
|
||||||
|
hosts: z.record(z.string(), z.union([
|
||||||
|
z.string(),
|
||||||
|
z.array(z.string())
|
||||||
|
])).optional(),
|
||||||
|
servers: z.array(z.union([
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
address: z.string(),
|
||||||
|
port: z.number().optional(),
|
||||||
|
domains: z.array(z.string()).optional(),
|
||||||
|
expectedIPs: z.array(z.string()).optional(),
|
||||||
|
unexpectedIPs: z.array(z.string()).optional(),
|
||||||
|
skipFallback: z.boolean().optional(),
|
||||||
|
clientIP: z.string().optional(),
|
||||||
|
queryStrategy: z.enum(["UseIP", "UseIPv4", "UseIPv6", "UseSystem"]).optional(),
|
||||||
|
tag: z.string().optional(),
|
||||||
|
timeoutMs: z.number().optional(),
|
||||||
|
disableCache: z.boolean().optional(),
|
||||||
|
finalQuery: z.boolean().optional()
|
||||||
|
})
|
||||||
|
])).optional(),
|
||||||
|
clientIp: z.string().optional(),
|
||||||
|
queryStrategy: z.enum(["UseIP", "UseIPv4", "UseIPv6", "UseSystem"]).optional(),
|
||||||
|
disableCache: z.boolean().optional(),
|
||||||
|
disableFallback: z.boolean().optional(),
|
||||||
|
disableFallbackIfMatch: z.boolean().optional(),
|
||||||
|
useSystemHosts: z.boolean().optional(),
|
||||||
|
tag: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const RoutingObject = z.object({
|
||||||
|
domainStrategy: z.enum(["AsIs", "IPIfNonMatch", "IPOnDemand"]).optional(),
|
||||||
|
domainMatcher: z.enum(["hybrid", "linear"]).optional(),
|
||||||
|
rules: z.array(z.object({
|
||||||
|
domainMatcher: z.enum(["hybrid", "linear"]).optional(),
|
||||||
|
type: z.literal("field").optional(),
|
||||||
|
domain: z.array(z.string()).optional(),
|
||||||
|
ip: z.array(z.string()).optional(),
|
||||||
|
port: z.union([z.number(), z.string()]).optional(),
|
||||||
|
sourcePort: z.union([z.number(), z.string()]).optional(),
|
||||||
|
network: z.enum(["tcp", "udp", "tcp,udp"]).optional(),
|
||||||
|
source: z.array(z.string()).optional(),
|
||||||
|
user: z.array(z.string()).optional(),
|
||||||
|
inboundTag: z.array(z.string()).optional(),
|
||||||
|
protocol: z.array(z.enum(["http", "tls", "bittorrent"])).optional(),
|
||||||
|
attrs: z.record(z.string(), z.string()).optional(),
|
||||||
|
outboundTag: z.string().optional(),
|
||||||
|
balancerTag: z.string().optional()
|
||||||
|
})).optional(),
|
||||||
|
balancers: z.array(z.object({
|
||||||
|
tag: z.string().optional(),
|
||||||
|
selector: z.array(z.string()).optional()
|
||||||
|
})).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const PolicyObject = z.object({
|
||||||
|
levels: z.record(z.string(), z.object({
|
||||||
|
handshake: z.number().optional(),
|
||||||
|
connIdle: z.number().optional(),
|
||||||
|
uplinkOnly: z.number().optional(),
|
||||||
|
downlinkOnly: z.number().optional(),
|
||||||
|
statsUserUplink: z.boolean().optional(),
|
||||||
|
statsUserDownlink: z.boolean().optional(),
|
||||||
|
statsUserOnline: z.boolean().optional(),
|
||||||
|
bufferSize: z.number().optional()
|
||||||
|
})).optional(),
|
||||||
|
system: z.object({
|
||||||
|
statsInboundUplink: z.boolean().optional(),
|
||||||
|
statsInboundDownlink: z.boolean().optional(),
|
||||||
|
statsOutboundUplink: z.boolean().optional(),
|
||||||
|
statsOutboundDownlink: z.boolean().optional()
|
||||||
|
}).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const InboundObject = z.object({
|
||||||
|
listen: z.string().optional(),
|
||||||
|
port: z.union([z.number(), z.string()]).optional(),
|
||||||
|
protocol: z.enum(["dokodemo-door", "http", "shadowsocks", "socks", "vless", "vmess", "trojan", "wireguard"]).optional(),
|
||||||
|
settings: z.record(z.string(), z.any()).optional(),
|
||||||
|
streamSettings: z.record(z.string(), z.any()).optional(),
|
||||||
|
tag: z.string().optional(),
|
||||||
|
sniffing: z.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
destOverride: z.array(z.enum(["http", "tls", "quic", "fakedns"])).optional(),
|
||||||
|
metadataOnly: z.boolean().optional(),
|
||||||
|
domainsExcluded: z.array(z.string()).optional(),
|
||||||
|
routeOnly: z.boolean().optional()
|
||||||
|
}).optional(),
|
||||||
|
allocate: z.object({
|
||||||
|
strategy: z.enum(["always", "random"]).optional(),
|
||||||
|
refresh: z.number().optional(),
|
||||||
|
concurrency: z.number().optional()
|
||||||
|
}).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const OutboundObject = z.object({
|
||||||
|
sendThrough: z.string().optional(),
|
||||||
|
protocol: z.string().optional(),
|
||||||
|
settings: z.record(z.string(), z.any()).optional(),
|
||||||
|
tag: z.string().optional(),
|
||||||
|
streamSettings: z.record(z.string(), z.any()).optional(),
|
||||||
|
proxySettings: z.object({
|
||||||
|
tag: z.string().optional()
|
||||||
|
}).optional(),
|
||||||
|
mux: z.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
concurrency: z.number().optional(),
|
||||||
|
xudpConcurrency: z.number().optional(),
|
||||||
|
xudpProxyUDP443: z.enum(["reject", "allow", "skip"]).optional()
|
||||||
|
}).optional(),
|
||||||
|
targetStrategy: z.enum([
|
||||||
|
"AsIs", "UseIP", "UseIPv6v4", "UseIPv6", "UseIPv4v6", "UseIPv4",
|
||||||
|
"ForceIP", "ForceIPv6v4", "ForceIPv6", "ForceIPv4v6", "ForceIPv4"
|
||||||
|
]).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const TlsSettingsObject = z.object({
|
||||||
|
serverName: z.string().optional(),
|
||||||
|
rejectUnknownSni: z.boolean().optional(),
|
||||||
|
verifyPeerCertInNames: z.array(z.string()).optional(),
|
||||||
|
allowInsecure: z.boolean().optional(),
|
||||||
|
alpn: z.array(z.string()).optional(),
|
||||||
|
minVersion: z.string().optional(),
|
||||||
|
maxVersion: z.string().optional(),
|
||||||
|
cipherSuites: z.string().optional(),
|
||||||
|
certificates: z.array(z.object({
|
||||||
|
ocspStapling: z.number().optional(),
|
||||||
|
oneTimeLoading: z.boolean().optional(),
|
||||||
|
usage: z.enum(["encipherment", "verify", "issue"]).optional(),
|
||||||
|
buildChain: z.boolean().optional(),
|
||||||
|
certificateFile: z.string().optional(),
|
||||||
|
keyFile: z.string().optional(),
|
||||||
|
certificate: z.array(z.string()).optional()
|
||||||
|
})).optional(),
|
||||||
|
disableSystemRoot: z.boolean().optional(),
|
||||||
|
enableSessionResumption: z.boolean().optional(),
|
||||||
|
fingerprint: z.string().optional(),
|
||||||
|
pinnedPeerCertificateChainSha256: z.array(z.string()).optional(),
|
||||||
|
masterKeyLog: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const RealitySettingsObject = z.object({
|
||||||
|
show: z.boolean().optional(),
|
||||||
|
dest: z.string().optional(),
|
||||||
|
xver: z.number().optional(),
|
||||||
|
serverNames: z.array(z.string()).optional(),
|
||||||
|
privateKey: z.string().optional(),
|
||||||
|
minClientVer: z.string().optional(),
|
||||||
|
maxClientVer: z.string().optional(),
|
||||||
|
maxTimeDiff: z.number().optional(),
|
||||||
|
shortIds: z.array(z.string()).optional(),
|
||||||
|
fingerprint: z.string().optional(),
|
||||||
|
serverName: z.string().optional(),
|
||||||
|
publicKey: z.string().optional(),
|
||||||
|
shortId: z.string().optional(),
|
||||||
|
spiderX: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const SockoptObject = z.object({
|
||||||
|
mark: z.number().optional(),
|
||||||
|
tcpMaxSeg: z.number().optional(),
|
||||||
|
tcpFastOpen: z.boolean().optional(),
|
||||||
|
tproxy: z.enum(["off", "redirect", "tproxy"]).optional(),
|
||||||
|
domainStrategy: z.enum(["AsIs", "UseIP", "UseIPv4", "UseIPv6"]).optional(),
|
||||||
|
happyEyeballs: z.object({
|
||||||
|
tryDelayMs: z.number().optional()
|
||||||
|
}).optional(),
|
||||||
|
dialerProxy: z.string().optional(),
|
||||||
|
acceptProxyProtocol: z.boolean().optional(),
|
||||||
|
tcpKeepAliveInterval: z.number().optional(),
|
||||||
|
tcpKeepAliveIdle: z.number().optional(),
|
||||||
|
tcpUserTimeout: z.number().optional(),
|
||||||
|
tcpCongestion: z.string().optional(),
|
||||||
|
interface: z.string().optional(),
|
||||||
|
v6only: z.boolean().optional(),
|
||||||
|
tcpWindowClamp: z.number().optional(),
|
||||||
|
tcpMptcp: z.boolean().optional(),
|
||||||
|
tcpNoDelay: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const TransportObject = z.object({
|
||||||
|
network: z.enum(["raw", "xhttp", "kcp", "grpc", "ws", "httpupgrade"]).optional(),
|
||||||
|
security: z.enum(["none", "tls", "reality"]).optional(),
|
||||||
|
tlsSettings: TlsSettingsObject.optional(),
|
||||||
|
realitySettings: RealitySettingsObject.optional(),
|
||||||
|
rawSettings: z.object({}).optional(),
|
||||||
|
xhttpSettings: z.object({}).optional(),
|
||||||
|
kcpSettings: z.object({}).optional(),
|
||||||
|
grpcSettings: z.object({}).optional(),
|
||||||
|
wsSettings: z.object({}).optional(),
|
||||||
|
httpupgradeSettings: z.object({}).optional(),
|
||||||
|
sockopt: SockoptObject.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const StatsObject = z.object({
|
||||||
|
// StatsObject currently doesn't require any parameters
|
||||||
|
// Internal statistics will be enabled as long as this object exists
|
||||||
|
});
|
||||||
|
|
||||||
|
const ReverseObject = z.object({
|
||||||
|
bridges: z.array(z.object({
|
||||||
|
tag: z.string().optional(),
|
||||||
|
domain: z.string().optional()
|
||||||
|
})).optional(),
|
||||||
|
portals: z.array(z.object({
|
||||||
|
tag: z.string().optional(),
|
||||||
|
domain: z.string().optional()
|
||||||
|
})).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const FakeDnsObject = z.union([
|
||||||
|
z.object({
|
||||||
|
ipPool: z.string().optional(),
|
||||||
|
poolSize: z.number().optional()
|
||||||
|
}).optional(),
|
||||||
|
z.array(z.object({
|
||||||
|
ipPool: z.string(),
|
||||||
|
poolSize: z.number().optional()
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
|
||||||
|
const MetricsObject = z.object({
|
||||||
|
tag: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const ObservatoryObject = z.object({
|
||||||
|
subjectSelector: z.array(z.string()).optional(),
|
||||||
|
probeUrl: z.string().optional(),
|
||||||
|
probeInterval: z.string().optional(),
|
||||||
|
enableConcurrency: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const BurstObservatoryObject = z.object({
|
||||||
|
subjectSelector: z.array(z.string()).optional(),
|
||||||
|
pingConfig: z.object({
|
||||||
|
destination: z.string().optional(),
|
||||||
|
connectivity: z.string().optional(),
|
||||||
|
interval: z.string().optional(),
|
||||||
|
sampling: z.number().optional(),
|
||||||
|
timeout: z.string().optional()
|
||||||
|
}).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const V2flySchema = z.object({
|
||||||
|
log: LogObject.optional(),
|
||||||
|
api: ApiObject.optional(),
|
||||||
|
dns: DnsObject.optional(),
|
||||||
|
routing: RoutingObject.optional(),
|
||||||
|
policy: PolicyObject.optional(),
|
||||||
|
inbounds: z.array(InboundObject).optional(),
|
||||||
|
outbounds: z.array(OutboundObject).optional(),
|
||||||
|
transport: TransportObject.optional(),
|
||||||
|
stats: StatsObject.optional(),
|
||||||
|
reverse: ReverseObject.optional(),
|
||||||
|
fakedns: FakeDnsObject.optional(),
|
||||||
|
metrics: MetricsObject.optional(),
|
||||||
|
observatory: ObservatoryObject.optional(),
|
||||||
|
burstObservatory: BurstObservatoryObject.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type v2flyConfig = z.infer<typeof V2flySchema>;
|
||||||
272
src/utils/core/configurator/schema/xray.schema.ts
Normal file
272
src/utils/core/configurator/schema/xray.schema.ts
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
const LogSchema = z.object({
|
||||||
|
access: z.string().optional(),
|
||||||
|
error: z.string().optional(),
|
||||||
|
loglevel: z.enum(["debug", "info", "warning", "error", "none"]).optional(),
|
||||||
|
dnsLog: z.boolean().optional(),
|
||||||
|
maskAddress: z.enum(["quarter", "half", "full"]).optional()
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
const ApiSchema = z.object({
|
||||||
|
tag: z.string().optional(),
|
||||||
|
listen: z.string().optional(),
|
||||||
|
services: z.array(z.string()).optional()
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
const DnsSchema = z.object({
|
||||||
|
hosts: z.record(z.string(), z.union([
|
||||||
|
z.string(),
|
||||||
|
z.array(z.string())
|
||||||
|
])).optional(),
|
||||||
|
servers: z.array(z.union([
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
address: z.string(),
|
||||||
|
port: z.number().optional(),
|
||||||
|
domains: z.array(z.string()).optional(),
|
||||||
|
expectedIPs: z.array(z.string()).optional(),
|
||||||
|
unexpectedIPs: z.array(z.string()).optional(),
|
||||||
|
skipFallback: z.boolean().optional(),
|
||||||
|
clientIP: z.string().optional(),
|
||||||
|
queryStrategy: z.enum(["UseIP", "UseIPv4", "UseIPv6", "UseSystem"]).optional(),
|
||||||
|
tag: z.string().optional(),
|
||||||
|
timeoutMs: z.number().optional(),
|
||||||
|
disableCache: z.boolean().optional(),
|
||||||
|
finalQuery: z.boolean().optional()
|
||||||
|
})
|
||||||
|
])).optional(),
|
||||||
|
clientIp: z.string().optional(),
|
||||||
|
queryStrategy: z.enum(["UseIP", "UseIPv4", "UseIPv6", "UseSystem"]).optional(),
|
||||||
|
disableCache: z.boolean().optional(),
|
||||||
|
disableFallback: z.boolean().optional(),
|
||||||
|
disableFallbackIfMatch: z.boolean().optional(),
|
||||||
|
useSystemHosts: z.boolean().optional(),
|
||||||
|
tag: z.string().optional()
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
const RoutingSchema = z.object({
|
||||||
|
domainStrategy: z.enum(["AsIs", "IPIfNonMatch", "IPOnDemand"]).optional(),
|
||||||
|
domainMatcher: z.enum(["hybrid", "linear"]).optional(),
|
||||||
|
rules: z.array(z.object({
|
||||||
|
domainMatcher: z.enum(["hybrid", "linear"]).optional(),
|
||||||
|
type: z.literal("field").optional(),
|
||||||
|
domain: z.array(z.string()).optional(),
|
||||||
|
ip: z.array(z.string()).optional(),
|
||||||
|
port: z.union([z.number(), z.string()]).optional(),
|
||||||
|
sourcePort: z.union([z.number(), z.string()]).optional(),
|
||||||
|
network: z.enum(["tcp", "udp", "tcp,udp"]).optional(),
|
||||||
|
source: z.array(z.string()).optional(),
|
||||||
|
user: z.array(z.string()).optional(),
|
||||||
|
inboundTag: z.array(z.string()).optional(),
|
||||||
|
protocol: z.array(z.enum(["http", "tls", "bittorrent"])).optional(),
|
||||||
|
attrs: z.record(z.string(), z.string()).optional(),
|
||||||
|
outboundTag: z.string().optional(),
|
||||||
|
balancerTag: z.string().optional()
|
||||||
|
})).optional(),
|
||||||
|
balancers: z.array(z.object({
|
||||||
|
tag: z.string().optional(),
|
||||||
|
selector: z.array(z.string()).optional()
|
||||||
|
})).optional()
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
const PolicySchema = z.object({
|
||||||
|
levels: z.record(z.string(), z.object({
|
||||||
|
handshake: z.number().optional(),
|
||||||
|
connIdle: z.number().optional(),
|
||||||
|
uplinkOnly: z.number().optional(),
|
||||||
|
downlinkOnly: z.number().optional(),
|
||||||
|
statsUserUplink: z.boolean().optional(),
|
||||||
|
statsUserDownlink: z.boolean().optional(),
|
||||||
|
statsUserOnline: z.boolean().optional(),
|
||||||
|
bufferSize: z.number().optional()
|
||||||
|
})).optional(),
|
||||||
|
system: z.object({
|
||||||
|
statsInboundUplink: z.boolean().optional(),
|
||||||
|
statsInboundDownlink: z.boolean().optional(),
|
||||||
|
statsOutboundUplink: z.boolean().optional(),
|
||||||
|
statsOutboundDownlink: z.boolean().optional()
|
||||||
|
}).optional()
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
const InboundSchema = z.object({
|
||||||
|
listen: z.string().optional(),
|
||||||
|
port: z.union([z.number(), z.string()]).optional(),
|
||||||
|
protocol: z.enum(["dokodemo-door", "http", "shadowsocks", "socks", "vless", "vmess", "trojan", "wireguard"]).optional(),
|
||||||
|
settings: z.record(z.string(), z.any()).optional(),
|
||||||
|
streamSettings: z.record(z.string(), z.any()).optional(),
|
||||||
|
tag: z.string().optional(),
|
||||||
|
sniffing: z.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
destOverride: z.array(z.enum(["http", "tls", "quic", "fakedns"])).optional(),
|
||||||
|
metadataOnly: z.boolean().optional(),
|
||||||
|
domainsExcluded: z.array(z.string()).optional(),
|
||||||
|
routeOnly: z.boolean().optional()
|
||||||
|
}).optional(),
|
||||||
|
allocate: z.object({
|
||||||
|
strategy: z.enum(["always", "random"]).optional(),
|
||||||
|
refresh: z.number().optional(),
|
||||||
|
concurrency: z.number().optional()
|
||||||
|
}).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const OutboundSchema = z.object({
|
||||||
|
sendThrough: z.string().optional(),
|
||||||
|
protocol: z.string().optional(),
|
||||||
|
settings: z.record(z.string(), z.any()).optional(),
|
||||||
|
tag: z.string().optional(),
|
||||||
|
streamSettings: z.record(z.string(), z.any()).optional(),
|
||||||
|
proxySettings: z.object({
|
||||||
|
tag: z.string().optional()
|
||||||
|
}).optional(),
|
||||||
|
mux: z.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
concurrency: z.number().optional(),
|
||||||
|
xudpConcurrency: z.number().optional(),
|
||||||
|
xudpProxyUDP443: z.enum(["reject", "allow", "skip"]).optional()
|
||||||
|
}).optional(),
|
||||||
|
targetStrategy: z.enum([
|
||||||
|
"AsIs", "UseIP", "UseIPv6v4", "UseIPv6", "UseIPv4v6", "UseIPv4",
|
||||||
|
"ForceIP", "ForceIPv6v4", "ForceIPv6", "ForceIPv4v6", "ForceIPv4"
|
||||||
|
]).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const TransportSchema = z.object({
|
||||||
|
network: z.enum(["raw", "xhttp", "kcp", "grpc", "ws", "httpupgrade"]).optional(),
|
||||||
|
security: z.enum(["none", "tls", "reality"]).optional(),
|
||||||
|
tlsSettings: z.object({
|
||||||
|
serverName: z.string().optional(),
|
||||||
|
rejectUnknownSni: z.boolean().optional(),
|
||||||
|
verifyPeerCertInNames: z.array(z.string()).optional(),
|
||||||
|
allowInsecure: z.boolean().optional(),
|
||||||
|
alpn: z.array(z.string()).optional(),
|
||||||
|
minVersion: z.string().optional(),
|
||||||
|
maxVersion: z.string().optional(),
|
||||||
|
cipherSuites: z.string().optional(),
|
||||||
|
certificates: z.array(z.object({
|
||||||
|
ocspStapling: z.number().optional(),
|
||||||
|
oneTimeLoading: z.boolean().optional(),
|
||||||
|
usage: z.enum(["encipherment", "verify", "issue"]).optional(),
|
||||||
|
buildChain: z.boolean().optional(),
|
||||||
|
certificateFile: z.string().optional(),
|
||||||
|
keyFile: z.string().optional(),
|
||||||
|
certificate: z.array(z.string()).optional()
|
||||||
|
})).optional(),
|
||||||
|
disableSystemRoot: z.boolean().optional(),
|
||||||
|
enableSessionResumption: z.boolean().optional(),
|
||||||
|
fingerprint: z.string().optional(),
|
||||||
|
pinnedPeerCertificateChainSha256: z.array(z.string()).optional(),
|
||||||
|
masterKeyLog: z.string().optional()
|
||||||
|
}).optional(),
|
||||||
|
realitySettings: z.object({
|
||||||
|
show: z.boolean().optional(),
|
||||||
|
dest: z.string().optional(),
|
||||||
|
xver: z.number().optional(),
|
||||||
|
serverNames: z.array(z.string()).optional(),
|
||||||
|
privateKey: z.string().optional(),
|
||||||
|
minClientVer: z.string().optional(),
|
||||||
|
maxClientVer: z.string().optional(),
|
||||||
|
maxTimeDiff: z.number().optional(),
|
||||||
|
shortIds: z.array(z.string()).optional(),
|
||||||
|
fingerprint: z.string().optional(),
|
||||||
|
serverName: z.string().optional(),
|
||||||
|
publicKey: z.string().optional(),
|
||||||
|
shortId: z.string().optional(),
|
||||||
|
spiderX: z.string().optional()
|
||||||
|
}).optional(),
|
||||||
|
rawSettings: z.object({}).optional(),
|
||||||
|
xhttpSettings: z.object({}).optional(),
|
||||||
|
kcpSettings: z.object({}).optional(),
|
||||||
|
grpcSettings: z.object({}).optional(),
|
||||||
|
wsSettings: z.object({}).optional(),
|
||||||
|
httpupgradeSettings: z.object({}).optional(),
|
||||||
|
sockopt: z.object({
|
||||||
|
mark: z.number().optional(),
|
||||||
|
tcpMaxSeg: z.number().optional(),
|
||||||
|
tcpFastOpen: z.boolean().optional(),
|
||||||
|
tproxy: z.enum(["off", "redirect", "tproxy"]).optional(),
|
||||||
|
domainStrategy: z.enum(["AsIs", "UseIP", "UseIPv4", "UseIPv6"]).optional(),
|
||||||
|
happyEyeballs: z.object({
|
||||||
|
tryDelayMs: z.number().optional()
|
||||||
|
}).optional(),
|
||||||
|
dialerProxy: z.string().optional(),
|
||||||
|
acceptProxyProtocol: z.boolean().optional(),
|
||||||
|
tcpKeepAliveInterval: z.number().optional(),
|
||||||
|
tcpKeepAliveIdle: z.number().optional(),
|
||||||
|
tcpUserTimeout: z.number().optional(),
|
||||||
|
tcpCongestion: z.string().optional(),
|
||||||
|
interface: z.string().optional(),
|
||||||
|
v6only: z.boolean().optional(),
|
||||||
|
tcpWindowClamp: z.number().optional(),
|
||||||
|
tcpMptcp: z.boolean().optional(),
|
||||||
|
tcpNoDelay: z.boolean().optional()
|
||||||
|
}).optional()
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
const StatsSchema = z.object({
|
||||||
|
// StatsObject currently doesn't require any parameters
|
||||||
|
// Internal statistics will be enabled as long as this object exists
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
const ReverseSchema = z.object({
|
||||||
|
bridges: z.array(z.object({
|
||||||
|
tag: z.string().optional(),
|
||||||
|
domain: z.string().optional()
|
||||||
|
})).optional(),
|
||||||
|
portals: z.array(z.object({
|
||||||
|
tag: z.string().optional(),
|
||||||
|
domain: z.string().optional()
|
||||||
|
})).optional()
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
const FakeDnsSchema = z.union([
|
||||||
|
z.object({
|
||||||
|
ipPool: z.string().optional(),
|
||||||
|
poolSize: z.number().optional()
|
||||||
|
}).optional(),
|
||||||
|
z.array(z.object({
|
||||||
|
ipPool: z.string(),
|
||||||
|
poolSize: z.number().optional()
|
||||||
|
}))
|
||||||
|
]).optional();
|
||||||
|
|
||||||
|
const MetricsSchema = z.object({
|
||||||
|
tag: z.string().optional()
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
const ObservatorySchema = z.object({
|
||||||
|
subjectSelector: z.array(z.string()).optional(),
|
||||||
|
probeUrl: z.string().optional(),
|
||||||
|
probeInterval: z.string().optional(),
|
||||||
|
enableConcurrency: z.boolean().optional()
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
const BurstObservatorySchema = z.object({
|
||||||
|
subjectSelector: z.array(z.string()).optional(),
|
||||||
|
pingConfig: z.object({
|
||||||
|
destination: z.string().optional(),
|
||||||
|
connectivity: z.string().optional(),
|
||||||
|
interval: z.string().optional(),
|
||||||
|
sampling: z.number().optional(),
|
||||||
|
timeout: z.string().optional()
|
||||||
|
}).optional()
|
||||||
|
}).optional();
|
||||||
|
|
||||||
|
export const XraySchema = z.object({
|
||||||
|
log: LogSchema,
|
||||||
|
api: ApiSchema,
|
||||||
|
dns: DnsSchema,
|
||||||
|
routing: RoutingSchema,
|
||||||
|
policy: PolicySchema,
|
||||||
|
inbounds: z.array(InboundSchema).optional(),
|
||||||
|
outbounds: z.array(OutboundSchema).optional(),
|
||||||
|
transport: TransportSchema,
|
||||||
|
stats: StatsSchema,
|
||||||
|
reverse: ReverseSchema,
|
||||||
|
fakedns: FakeDnsSchema,
|
||||||
|
metrics: MetricsSchema,
|
||||||
|
observatory: ObservatorySchema,
|
||||||
|
burstObservatory: BurstObservatorySchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export type xrayConfig = z.infer<typeof XraySchema>;
|
||||||
Reference in New Issue
Block a user