Kaynağa Gözat

feat: sync streams

Jacky 2 ay önce
ebeveyn
işleme
34fa4eb204

+ 3 - 3
api/streams/advance.go

@@ -1,12 +1,13 @@
 package streams
 
 import (
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func AdvancedEdit(c *gin.Context) {
@@ -21,7 +22,7 @@ func AdvancedEdit(c *gin.Context) {
 	name := c.Param("name")
 	path := nginx.GetConfPath("streams-available", name)
 
-	s := query.Site
+	s := query.Stream
 
 	_, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
 	if err != nil {
@@ -39,5 +40,4 @@ func AdvancedEdit(c *gin.Context) {
 	c.JSON(http.StatusOK, gin.H{
 		"message": "ok",
 	})
-
 }

+ 1 - 0
api/streams/router.go

@@ -6,6 +6,7 @@ func InitRouter(r *gin.RouterGroup) {
 	r.GET("streams", GetStreams)
 	r.GET("streams/:name", GetStream)
 	r.POST("streams/:name", SaveStream)
+	r.POST("streams/:name/rename", RenameStream)
 	r.POST("streams/:name/enable", EnableStream)
 	r.POST("streams/:name/disable", DisableStream)
 	r.POST("streams/:name/advance", AdvancedEdit)

+ 32 - 160
api/streams/streams.go

@@ -1,18 +1,19 @@
 package streams
 
 import (
+	"net/http"
+	"os"
+	"strings"
+	"time"
+
 	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/config"
-	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/internal/stream"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
 	"github.com/sashabaranov/go-openai"
 	"github.com/uozi-tech/cosy"
-	"net/http"
-	"os"
-	"strings"
-	"time"
 )
 
 type Stream struct {
@@ -24,6 +25,7 @@ type Stream struct {
 	ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"`
 	Tokenized       *nginx.NgxConfig               `json:"tokenized,omitempty"`
 	Filepath        string                         `json:"filepath"`
+	SyncNodeIDs     []uint64                       `json:"sync_node_ids" gorm:"serializer:json"`
 }
 
 func GetStreams(c *gin.Context) {
@@ -32,14 +34,12 @@ func GetStreams(c *gin.Context) {
 	sort := c.DefaultQuery("sort", "desc")
 
 	configFiles, err := os.ReadDir(nginx.GetConfPath("streams-available"))
-
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}
 
 	enabledConfig, err := os.ReadDir(nginx.GetConfPath("streams-enabled"))
-
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
@@ -77,15 +77,8 @@ func GetStreams(c *gin.Context) {
 }
 
 func GetStream(c *gin.Context) {
-	rewriteName, ok := c.Get("rewriteConfigFileName")
-
 	name := c.Param("name")
 
-	// for modify filename
-	if ok {
-		name = rewriteName.(string)
-	}
-
 	path := nginx.GetConfPath("streams-available", name)
 	file, err := os.Stat(path)
 	if os.IsNotExist(err) {
@@ -114,14 +107,13 @@ func GetStream(c *gin.Context) {
 	}
 
 	s := query.Stream
-	stream, err := s.Where(s.Path.Eq(path)).FirstOrInit()
-
+	streamModel, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}
 
-	if stream.Advanced {
+	if streamModel.Advanced {
 		origContent, err := os.ReadFile(path)
 		if err != nil {
 			api.ErrHandler(c, err)
@@ -130,12 +122,13 @@ func GetStream(c *gin.Context) {
 
 		c.JSON(http.StatusOK, Stream{
 			ModifiedAt:      file.ModTime(),
-			Advanced:        stream.Advanced,
+			Advanced:        streamModel.Advanced,
 			Enabled:         enabled,
 			Name:            name,
 			Config:          string(origContent),
 			ChatGPTMessages: chatgpt.Content,
 			Filepath:        path,
+			SyncNodeIDs:     streamModel.SyncNodeIDs,
 		})
 		return
 	}
@@ -149,207 +142,86 @@ func GetStream(c *gin.Context) {
 
 	c.JSON(http.StatusOK, Stream{
 		ModifiedAt:      file.ModTime(),
-		Advanced:        stream.Advanced,
+		Advanced:        streamModel.Advanced,
 		Enabled:         enabled,
 		Name:            name,
 		Config:          nginxConfig.FmtCode(),
 		Tokenized:       nginxConfig,
 		ChatGPTMessages: chatgpt.Content,
 		Filepath:        path,
+		SyncNodeIDs:     streamModel.SyncNodeIDs,
 	})
 }
 
 func SaveStream(c *gin.Context) {
 	name := c.Param("name")
 
-	if name == "" {
-		c.JSON(http.StatusNotAcceptable, gin.H{
-			"message": "param name is empty",
-		})
-		return
-	}
-
 	var json struct {
-		Name      string `json:"name" binding:"required"`
-		Content   string `json:"content" binding:"required"`
-		Overwrite bool   `json:"overwrite"`
+		Content     string   `json:"content" binding:"required"`
+		SyncNodeIDs []uint64 `json:"sync_node_ids"`
+		Overwrite   bool     `json:"overwrite"`
 	}
 
 	if !cosy.BindAndValid(c, &json) {
 		return
 	}
 
-	path := nginx.GetConfPath("streams-available", name)
-
-	if !json.Overwrite && helper.FileExists(path) {
-		c.JSON(http.StatusNotAcceptable, gin.H{
-			"message": "File exists",
-		})
-		return
-	}
-
-	err := os.WriteFile(path, []byte(json.Content), 0644)
+	err := stream.Save(name, json.Content, json.Overwrite, json.SyncNodeIDs)
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}
-	enabledConfigFilePath := nginx.GetConfPath("streams-enabled", name)
-	// rename the config file if needed
-	if name != json.Name {
-		newPath := nginx.GetConfPath("streams-available", json.Name)
-		s := query.Stream
-		_, err = s.Where(s.Path.Eq(path)).Update(s.Path, newPath)
-
-		// check if dst file exists, do not rename
-		if helper.FileExists(newPath) {
-			c.JSON(http.StatusNotAcceptable, gin.H{
-				"message": "File exists",
-			})
-			return
-		}
-		// recreate a soft link
-		if helper.FileExists(enabledConfigFilePath) {
-			_ = os.Remove(enabledConfigFilePath)
-			enabledConfigFilePath = nginx.GetConfPath("streams-enabled", json.Name)
-			err = os.Symlink(newPath, enabledConfigFilePath)
-
-			if err != nil {
-				api.ErrHandler(c, err)
-				return
-			}
-		}
-
-		err = os.Rename(path, newPath)
-		if err != nil {
-			api.ErrHandler(c, err)
-			return
-		}
-
-		name = json.Name
-		c.Set("rewriteConfigFileName", name)
-	}
-
-	enabledConfigFilePath = nginx.GetConfPath("streams-enabled", name)
-	if helper.FileExists(enabledConfigFilePath) {
-		// Test nginx configuration
-		output := nginx.TestConf()
-
-		if nginx.GetLogLevel(output) > nginx.Warn {
-			c.JSON(http.StatusInternalServerError, gin.H{
-				"message": output,
-			})
-			return
-		}
-
-		output = nginx.Reload()
-
-		if nginx.GetLogLevel(output) > nginx.Warn {
-			c.JSON(http.StatusInternalServerError, gin.H{
-				"message": output,
-			})
-			return
-		}
-	}
 
 	GetStream(c)
 }
 
 func EnableStream(c *gin.Context) {
-	configFilePath := nginx.GetConfPath("streams-available", c.Param("name"))
-	enabledConfigFilePath := nginx.GetConfPath("streams-enabled", c.Param("name"))
-
-	_, err := os.Stat(configFilePath)
-
+	err := stream.Enable(c.Param("name"))
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}
 
-	if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
-		err = os.Symlink(configFilePath, enabledConfigFilePath)
-
-		if err != nil {
-			api.ErrHandler(c, err)
-			return
-		}
-	}
-
-	// Test nginx config, if not pass, then disable the stream.
-	output := nginx.TestConf()
-
-	if nginx.GetLogLevel(output) > nginx.Warn {
-		_ = os.Remove(enabledConfigFilePath)
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": output,
-		})
-		return
-	}
-
-	output = nginx.Reload()
-
-	if nginx.GetLogLevel(output) > nginx.Warn {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": output,
-		})
-		return
-	}
-
 	c.JSON(http.StatusOK, gin.H{
 		"message": "ok",
 	})
 }
 
 func DisableStream(c *gin.Context) {
-	enabledConfigFilePath := nginx.GetConfPath("streams-enabled", c.Param("name"))
-
-	_, err := os.Stat(enabledConfigFilePath)
-
+	err := stream.Disable(c.Param("name"))
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}
 
-	err = os.Remove(enabledConfigFilePath)
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+}
 
+func DeleteStream(c *gin.Context) {
+	err := stream.Delete(c.Param("name"))
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}
-	output := nginx.Reload()
-
-	if nginx.GetLogLevel(output) > nginx.Warn {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": output,
-		})
-		return
-	}
 
 	c.JSON(http.StatusOK, gin.H{
 		"message": "ok",
 	})
 }
 
-func DeleteStream(c *gin.Context) {
-	var err error
-	name := c.Param("name")
-	availablePath := nginx.GetConfPath("streams-available", name)
-	enabledPath := nginx.GetConfPath("streams-enabled", name)
-
-	if _, err = os.Stat(availablePath); os.IsNotExist(err) {
-		c.JSON(http.StatusNotFound, gin.H{
-			"message": "stream not found",
-		})
-		return
+func RenameStream(c *gin.Context) {
+	oldName := c.Param("name")
+	var json struct {
+		NewName string `json:"new_name"`
 	}
-
-	if _, err = os.Stat(enabledPath); err == nil {
-		c.JSON(http.StatusNotAcceptable, gin.H{
-			"message": "stream is enabled",
-		})
+	if !cosy.BindAndValid(c, &json) {
 		return
 	}
 
-	if err = os.Remove(availablePath); err != nil {
+	err := stream.Rename(oldName, json.NewName)
+	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}

+ 5 - 0
app/src/api/stream.ts

@@ -12,6 +12,7 @@ export interface Stream {
   config: string
   chatgpt_messages: ChatComplicationMessage[]
   tokenized?: NgxConfig
+  sync_node_ids: number[]
 }
 
 class StreamCurd extends Curd<Stream> {
@@ -31,6 +32,10 @@ class StreamCurd extends Curd<Stream> {
   advance_mode(name: string, data: { advanced: boolean }) {
     return http.post(`${this.baseUrl}/${name}/advance`, data)
   }
+
+  rename(name: string, newName: string) {
+    return http.post(`${this.baseUrl}/${name}/rename`, { new_name: newName })
+  }
 }
 
 const stream = new StreamCurd('/streams')

+ 6 - 0
app/src/components/NodeSelector/NodeSelector.vue

@@ -32,6 +32,12 @@ function newSSE() {
 
   s.onmessage = (e: SSEvent) => {
     data.value = JSON.parse(e.data)
+    nextTick(() => {
+      data_map.value = data.value.reduce((acc, node) => {
+        acc[node.id] = node
+        return acc
+      }, {} as Record<number, Environment>)
+    })
   }
 
   // reconnect

+ 5 - 0
app/src/constants/errors/stream.ts

@@ -0,0 +1,5 @@
+export default {
+  40401: () => $gettext('Stream not found'),
+  50001: () => $gettext('Destination file already exists'),
+  50002: () => $gettext('Stream is enabled'),
+}

+ 1 - 0
app/src/constants/errors/user.ts

@@ -3,6 +3,7 @@ export default {
   40303: () => $gettext('User banned'),
   40304: () => $gettext('Invalid otp code'),
   40305: () => $gettext('Invalid recovery code'),
+  40306: () => $gettext('Legacy recovery code not allowed since totp is not enabled'),
   50000: () => $gettext('WebAuthn settings are not configured'),
   50001: () => $gettext('User not enabled otp as 2fa'),
   50002: () => $gettext('Otp or recovery code empty'),

+ 1 - 1
app/src/routes/index.ts

@@ -91,7 +91,7 @@ export const routes: RouteRecordRaw[] = [
         },
       },
       {
-        path: 'stream/:name',
+        path: 'streams/:name',
         name: 'Edit Stream',
         component: () => import('@/views/stream/StreamEdit.vue'),
         meta: {

+ 44 - 43
app/src/views/stream/StreamEdit.vue

@@ -23,7 +23,7 @@ watch(route, () => {
   name.value = route.params?.name?.toString() ?? ''
 })
 
-const ngx_config: NgxConfig = reactive({
+const ngxConfig: NgxConfig = reactive({
   name: '',
   upstreams: [],
   servers: [],
@@ -31,81 +31,81 @@ const ngx_config: NgxConfig = reactive({
 
 const enabled = ref(false)
 const configText = ref('')
-const advance_mode_ref = ref(false)
+const advanceModeRef = ref(false)
 const saving = ref(false)
 const filename = ref('')
 const filepath = ref('')
-const parse_error_status = ref(false)
-const parse_error_message = ref('')
-const data = ref({})
+const parseErrorStatus = ref(false)
+const parseErrorMessage = ref('')
+const data = ref<Stream>({} as Stream)
 
 init()
 
-const advance_mode = computed({
+const advanceMode = computed({
   get() {
-    return advance_mode_ref.value || parse_error_status.value
+    return advanceModeRef.value || parseErrorStatus.value
   },
   set(v: boolean) {
-    advance_mode_ref.value = v
+    advanceModeRef.value = v
   },
 })
 
-const history_chatgpt_record = ref([]) as Ref<ChatComplicationMessage[]>
+const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
 
-function handle_response(r: Stream) {
+function handleResponse(r: Stream) {
   if (r.advanced)
-    advance_mode.value = true
+    advanceMode.value = true
 
   if (r.advanced)
-    advance_mode.value = true
+    advanceMode.value = true
 
-  parse_error_status.value = false
-  parse_error_message.value = ''
+  parseErrorStatus.value = false
+  parseErrorMessage.value = ''
   filename.value = r.name
   filepath.value = r.filepath
   configText.value = r.config
   enabled.value = r.enabled
-  history_chatgpt_record.value = r.chatgpt_messages
+  historyChatgptRecord.value = r.chatgpt_messages
   data.value = r
-  Object.assign(ngx_config, r.tokenized)
+  Object.assign(ngxConfig, r.tokenized)
 }
 
 function init() {
   if (name.value) {
     stream.get(name.value).then(r => {
-      handle_response(r)
-    }).catch(handle_parse_error)
+      handleResponse(r)
+    }).catch(handleParseError)
   }
   else {
-    history_chatgpt_record.value = []
+    historyChatgptRecord.value = []
   }
 }
 
-function handle_parse_error(e: { error?: string, message: string }) {
+function handleParseError(e: { error?: string, message: string }) {
   console.error(e)
-  parse_error_status.value = true
-  parse_error_message.value = e.message
+  parseErrorStatus.value = true
+  parseErrorMessage.value = e.message
   config.get(`streams-available/${name.value}`).then(r => {
     configText.value = r.content
   })
 }
 
-function on_mode_change(advanced: CheckedType) {
+function onModeChange(advanced: CheckedType) {
   stream.advance_mode(name.value, { advanced: advanced as boolean }).then(() => {
-    advance_mode.value = advanced as boolean
+    advanceMode.value = advanced as boolean
     if (advanced) {
-      build_config()
+      buildConfig()
     }
     else {
       return ngx.tokenize_config(configText.value).then(r => {
-        Object.assign(ngx_config, r)
-      }).catch(handle_parse_error)
+        Object.assign(ngxConfig, r)
+      }).catch(handleParseError)
     }
   })
 }
 
-async function build_config() {
-  return ngx.build_config(ngx_config).then(r => {
+async function buildConfig() {
+  return ngx.build_config(ngxConfig).then(r => {
     configText.value = r.content
   })
 }
@@ -113,9 +113,9 @@ async function build_config() {
 async function save() {
   saving.value = true
 
-  if (!advance_mode.value) {
+  if (!advanceMode.value) {
     try {
-      await build_config()
+      await buildConfig()
     }
     catch {
       saving.value = false
@@ -129,22 +129,23 @@ async function save() {
     name: filename.value || name.value,
     content: configText.value,
     overwrite: true,
+    sync_node_ids: data.value?.sync_node_ids,
   }).then(r => {
-    handle_response(r)
+    handleResponse(r)
     router.push({
-      path: `/stream/${filename.value}`,
+      path: `/streams/${filename.value}`,
       query: route.query,
     })
     message.success($gettext('Saved successfully'))
-  }).catch(handle_parse_error).finally(() => {
+  }).catch(handleParseError).finally(() => {
     saving.value = false
   })
 }
 
 provide('save_config', save)
 provide('configText', configText)
-provide('ngx_config', ngx_config)
-provide('history_chatgpt_record', history_chatgpt_record)
+provide('ngx_config', ngxConfig)
+provide('history_chatgpt_record', historyChatgptRecord)
 provide('enabled', enabled)
 provide('name', name)
 provide('filename', filename)
@@ -180,12 +181,12 @@ provide('data', data)
             <div class="switch">
               <ASwitch
                 size="small"
-                :disabled="parse_error_status"
-                :checked="advance_mode"
-                @change="on_mode_change"
+                :disabled="parseErrorStatus"
+                :checked="advanceMode"
+                @change="onModeChange"
               />
             </div>
-            <template v-if="advance_mode">
+            <template v-if="advanceMode">
               <div>{{ $gettext('Advance Mode') }}</div>
             </template>
             <template v-else>
@@ -196,16 +197,16 @@ provide('data', data)
 
         <Transition name="slide-fade">
           <div
-            v-if="advance_mode"
+            v-if="advanceMode"
             key="advance"
           >
             <div
-              v-if="parse_error_status"
+              v-if="parseErrorStatus"
               class="parse-error-alert-wrapper"
             >
               <AAlert
                 :message="$gettext('Nginx Configuration Parse Error')"
-                :description="parse_error_message"
+                :description="parseErrorMessage"
                 type="error"
                 show-icon
               />

+ 4 - 4
app/src/views/stream/StreamList.vue

@@ -18,7 +18,6 @@ const columns: Column[] = [{
     type: input,
   },
   search: true,
-  width: 200,
 }, {
   title: () => $gettext('Status'),
   dataIndex: 'enabled',
@@ -38,17 +37,18 @@ const columns: Column[] = [{
   },
   sorter: true,
   pithy: true,
-  width: 100,
+  width: 200,
 }, {
   title: () => $gettext('Updated at'),
   dataIndex: 'modified_at',
   customRender: datetime,
   sorter: true,
   pithy: true,
+  width: 200,
 }, {
   title: () => $gettext('Action'),
   dataIndex: 'action',
-  width: 120,
+  width: 250,
   fixed: 'right',
 }]
 
@@ -132,7 +132,7 @@ function handleAddStream() {
       disable-view
       :scroll-x="800"
       @click-edit="r => $router.push({
-        path: `/stream/${r}`,
+        path: `/streams/${r}`,
       })"
     >
       <template #actions="{ record }">

+ 61 - 0
app/src/views/stream/components/ConfigName.vue

@@ -0,0 +1,61 @@
+<script setup lang="ts">
+import stream from '@/api/stream'
+import { message } from 'ant-design-vue'
+
+const props = defineProps<{
+  name: string
+}>()
+
+const router = useRouter()
+
+const modify = ref(false)
+const buffer = ref('')
+const loading = ref(false)
+
+onMounted(() => {
+  buffer.value = props.name
+})
+
+function clickModify() {
+  modify.value = true
+}
+
+function save() {
+  loading.value = true
+  stream.rename(props.name, buffer.value).then(() => {
+    modify.value = false
+    message.success($gettext('Renamed successfully'))
+    router.push({
+      path: `/streams/${buffer.value}`,
+    })
+  }).finally(() => {
+    loading.value = false
+  })
+}
+</script>
+
+<template>
+  <div v-if="!modify" class="flex items-center">
+    <div class="mr-2">
+      {{ buffer }}
+    </div>
+    <div>
+      <AButton type="link" size="small" @click="clickModify">
+        {{ $gettext('Rename') }}
+      </AButton>
+    </div>
+  </div>
+  <div v-else>
+    <AInput v-model:value="buffer">
+      <template #suffix>
+        <AButton :disabled="buffer === name" type="link" size="small" :loading @click="save">
+          {{ $gettext('Save') }}
+        </AButton>
+      </template>
+    </AInput>
+  </div>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 0 - 120
app/src/views/stream/components/Deploy.vue

@@ -1,120 +0,0 @@
-<script setup lang="ts">
-import type { Ref } from 'vue'
-import stream from '@/api/stream'
-import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
-import { InfoCircleOutlined } from '@ant-design/icons-vue'
-import { Modal, notification } from 'ant-design-vue'
-
-const node_map = reactive({})
-const target = ref([])
-const overwrite = ref(false)
-const enabled = ref(false)
-const name = inject('name') as Ref<string>
-const [modal, ContextHolder] = Modal.useModal()
-function deploy() {
-  modal.confirm({
-    title: () => $ngettext('Do you want to deploy this file to remote server?', 'Do you want to deploy this file to remote servers?', target.value.length),
-    mask: false,
-    centered: true,
-    okText: $gettext('OK'),
-    cancelText: $gettext('Cancel'),
-    onOk() {
-      target.value.forEach(id => {
-        const node_name = node_map[id]
-
-        // get source content
-        stream.get(name.value).then(r => {
-          stream.save(name.value, {
-            name: name.value,
-            content: r.config,
-            overwrite: overwrite.value,
-
-          }, { headers: { 'X-Node-ID': id } }).then(async () => {
-            notification.success({
-              message: $gettext('Deploy successfully'),
-              description:
-                $gettext('Deploy %{conf_name} to %{node_name} successfully', { conf_name: name.value, node_name }),
-            })
-            if (enabled.value) {
-              stream.enable(name.value).then(() => {
-                notification.success({
-                  message: $gettext('Enable successfully'),
-                  description:
-                    $gettext('Enable %{conf_name} in %{node_name} successfully', { conf_name: name.value, node_name }),
-                })
-              }).catch(e => {
-                notification.error({
-                  message: $gettext('Enable %{conf_name} in %{node_name} failed', {
-                    conf_name: name.value,
-                    node_name,
-                  }),
-                  description: $gettext(e?.message ?? 'Server error'),
-                })
-              })
-            }
-          }).catch(e => {
-            notification.error({
-              message: $gettext('Deploy %{conf_name} to %{node_name} failed', {
-                conf_name: name.value,
-                node_name,
-              }),
-              description: $gettext(e?.message ?? 'Server error'),
-            })
-          })
-        })
-      })
-    },
-  })
-}
-</script>
-
-<template>
-  <ContextHolder />
-  <NodeSelector
-    v-model:target="target"
-    hidden-local
-    :map="node_map"
-  />
-  <div class="node-deploy-control">
-    <ACheckbox v-model:checked="enabled">
-      {{ $gettext('Enable') }}
-    </ACheckbox>
-    <div class="overwrite">
-      <ACheckbox v-model:checked="overwrite">
-        {{ $gettext('Overwrite') }}
-      </ACheckbox>
-      <ATooltip placement="bottom">
-        <template #title>
-          {{ $gettext('Overwrite exist file') }}
-        </template>
-        <InfoCircleOutlined />
-      </ATooltip>
-    </div>
-
-    <AButton
-      :disabled="target.length === 0"
-      type="primary"
-      ghost
-      @click="deploy"
-    >
-      {{ $gettext('Deploy') }}
-    </AButton>
-  </div>
-</template>
-
-<style scoped lang="less">
-.overwrite {
-  margin-right: 15px;
-
-  span {
-    color: #9b9b9b;
-  }
-}
-
-.node-deploy-control {
-  display: flex;
-  justify-content: flex-end;
-  margin-top: 10px;
-  align-items: center;
-}
-</style>

+ 13 - 9
app/src/views/stream/components/RightSettings.vue

@@ -5,18 +5,18 @@ import type { CheckedType } from '@/types'
 import type { Ref } from 'vue'
 import stream from '@/api/stream'
 import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
+import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
 import { formatDateTime } from '@/lib/helper'
 import { useSettingsStore } from '@/pinia'
-import Deploy from '@/views/stream/components/Deploy.vue'
 import { message, Modal } from 'ant-design-vue'
+import ConfigName from './ConfigName.vue'
 
 const settings = useSettingsStore()
 
 const configText = inject('configText') as Ref<string>
 const enabled = inject('enabled') as Ref<boolean>
 const name = inject('name') as Ref<string>
-const history_chatgpt_record = inject('history_chatgpt_record') as Ref<ChatComplicationMessage[]>
-const filename = inject('filename') as Ref<string>
+const historyChatgptRecord = inject('history_chatgpt_record') as Ref<ChatComplicationMessage[]>
 const filepath = inject('filepath') as Ref<string>
 const data = inject('data') as Ref<Stream>
 
@@ -42,7 +42,7 @@ function disable() {
   })
 }
 
-function on_change_enabled(checked: CheckedType) {
+function onChangeEnabled(checked: CheckedType) {
   modal.confirm({
     title: checked ? $gettext('Do you want to enable this stream?') : $gettext('Do you want to disable this stream?'),
     mask: false,
@@ -76,11 +76,11 @@ function on_change_enabled(checked: CheckedType) {
         <AFormItem :label="$gettext('Enabled')">
           <ASwitch
             :checked="enabled"
-            @change="on_change_enabled"
+            @change="onChangeEnabled"
           />
         </AFormItem>
         <AFormItem :label="$gettext('Name')">
-          <AInput v-model:value="filename" />
+          <ConfigName :name="name" />
         </AFormItem>
         <AFormItem :label="$gettext('Updated at')">
           {{ formatDateTime(data.modified_at) }}
@@ -89,16 +89,20 @@ function on_change_enabled(checked: CheckedType) {
       <ACollapsePanel
         v-if="!settings.is_remote"
         key="2"
-        :header="$gettext('Deploy')"
+        :header="$gettext('Sync')"
       >
-        <Deploy />
+        <NodeSelector
+          v-model:target="data.sync_node_ids"
+          class="mb-4"
+          hidden-local
+        />
       </ACollapsePanel>
       <ACollapsePanel
         key="3"
         header="ChatGPT"
       >
         <ChatGPT
-          v-model:history-messages="history_chatgpt_record"
+          v-model:history-messages="historyChatgptRecord"
           :content="configText"
           :path="filepath"
         />

+ 12 - 79
app/src/views/stream/components/StreamDuplicate.vue

@@ -1,35 +1,22 @@
 <script setup lang="ts">
 import stream from '@/api/stream'
 
-import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
 import gettext from '@/gettext'
-import { useSettingsStore } from '@/pinia'
-import { Form, message, notification } from 'ant-design-vue'
+import { Form, message } from 'ant-design-vue'
 
 const props = defineProps<{
-  visible: boolean
   name: string
 }>()
 
-const emit = defineEmits(['update:visible', 'duplicated'])
+const emit = defineEmits(['duplicated'])
 
-const settings = useSettingsStore()
-
-const show = computed({
-  get() {
-    return props.visible
-  },
-  set(v) {
-    emit('update:visible', v)
-  },
-})
+const visible = defineModel<boolean>('visible')
 
 interface Model {
   name: string // site name
-  target: number[] // ids of deploy targets
 }
 
-const modelRef: Model = reactive({ name: '', target: [] })
+const modelRef: Model = reactive({ name: '' })
 
 const rulesRef = reactive({
   name: [
@@ -39,73 +26,29 @@ const rulesRef = reactive({
         + 'this will be used as the filename of the new configuration!'),
     },
   ],
-  target: [
-    {
-      required: true,
-      message: () => $gettext('Please select at least one node!'),
-    },
-  ],
 })
 
 const { validate, validateInfos, clearValidate } = Form.useForm(modelRef, rulesRef)
 
 const loading = ref(false)
 
-const node_map: Record<number, string> = reactive({})
-
 function onSubmit() {
   validate().then(async () => {
     loading.value = true
 
-    modelRef.target.forEach(id => {
-      if (id === 0) {
-        stream.duplicate(props.name, { name: modelRef.name }).then(() => {
-          message.success($gettext('Duplicate to local successfully'))
-          show.value = false
-          emit('duplicated')
-        }).catch(e => {
-          message.error($gettext(e?.message ?? 'Server error'))
-        })
-      }
-      else {
-        // get source content
-
-        stream.get(props.name).then(r => {
-          stream.save(modelRef.name, {
-            name: modelRef.name,
-            content: r.config,
-
-          }, { headers: { 'X-Node-ID': id } }).then(() => {
-            notification.success({
-              message: $gettext('Duplicate successfully'),
-              description:
-                $gettext('Duplicate %{conf_name} to %{node_name} successfully', { conf_name: props.name, node_name: node_map[id] }),
-            })
-          }).catch(e => {
-            notification.error({
-              message: $gettext('Duplicate failed'),
-              description: $gettext(e?.message ?? 'Server error'),
-            })
-          })
-          if (r.enabled) {
-            stream.enable(modelRef.name, { headers: { 'X-Node-ID': id } }).then(() => {
-              notification.success({
-                message: $gettext('Enabled successfully'),
-              })
-            })
-          }
-        })
-      }
+    stream.duplicate(props.name, { name: modelRef.name }).then(() => {
+      message.success($gettext('Duplicate to local successfully'))
+      visible.value = false
+      emit('duplicated')
+    }).finally(() => {
+      loading.value = false
     })
-
-    loading.value = false
   })
 }
 
-watch(() => props.visible, v => {
+watch(visible, v => {
   if (v) {
     modelRef.name = props.name // default with source name
-    modelRef.target = [0]
     nextTick(() => clearValidate())
   }
 })
@@ -117,7 +60,7 @@ watch(() => gettext.current, () => {
 
 <template>
   <AModal
-    v-model:open="show"
+    v-model:open="visible"
     :title="$gettext('Duplicate')"
     :confirm-loading="loading"
     :mask="false"
@@ -130,16 +73,6 @@ watch(() => gettext.current, () => {
       >
         <AInput v-model:value="modelRef.name" />
       </AFormItem>
-      <AFormItem
-        v-if="!settings.is_remote"
-        :label="$gettext('Target')"
-        v-bind="validateInfos.target"
-      >
-        <NodeSelector
-          v-model:target="modelRef.target"
-          v-model:map="node_map"
-        />
-      </AFormItem>
     </AForm>
   </AModal>
 </template>

+ 1 - 1
cmd/gen/generate.go

@@ -18,7 +18,7 @@ func main() {
 	// specify the output directory (default: "./query")
 	// ### if you want to query without context constrain, set mode gen.WithoutContext ###
 	g := gen.NewGenerator(gen.Config{
-		OutPath: "../../query",
+		OutPath: "query",
 		Mode:    gen.WithoutContext | gen.WithDefaultQuery,
 		//if you want the nullable field generation property to be pointer type, set FieldNullable true
 		FieldNullable: true,

+ 0 - 1
go.mod

@@ -100,7 +100,6 @@ require (
 	github.com/civo/civogo v0.3.94 // indirect
 	github.com/cloudflare/cloudflare-go v0.115.0 // indirect
 	github.com/cloudwego/base64x v0.1.5 // indirect
-	github.com/cpu/goacmedns v0.1.1 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/dimchansky/utfbom v1.1.1 // indirect

+ 3 - 114
go.sum

@@ -39,8 +39,6 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY
 cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
 cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
 cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
-cloud.google.com/go v0.118.2 h1:bKXO7RXMFDkniAAvvuMrAPtQ/VHrs9e7J5UT3yrGdTY=
-cloud.google.com/go v0.118.2/go.mod h1:CFO4UPEPi8oV21xoezZCrd3d81K4fFkDTEJu4R8K+9M=
 cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
 cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
 cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
@@ -180,8 +178,6 @@ cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvj
 cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA=
 cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
 cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
-cloud.google.com/go/compute v1.33.0 h1:abGcwWokP7/bBpvRjUKlgchrZXYgRpwcKZIlNUHWf6Y=
-cloud.google.com/go/compute v1.33.0/go.mod h1:Z8NErRhrWA3RmVWczlAPJjZcRTlqZB1pcpD0MaIc1ug=
 cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
 cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
@@ -618,13 +614,10 @@ github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0
 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
@@ -667,8 +660,6 @@ github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsf
 github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc=
 github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
 github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.0 h1:MUkXAnvvDHgvPItl0nBj0hgk0f7hnnQbGm0h0+YxbN4=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -705,8 +696,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/aliyun/alibaba-cloud-sdk-go v1.63.84 h1:8IpC2i1mtsuUt13cbZtVCtQRSjzuMvLiDrbOJcaS+Z4=
-github.com/aliyun/alibaba-cloud-sdk-go v1.63.84/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
 github.com/aliyun/alibaba-cloud-sdk-go v1.63.88 h1:87jNTxliGqU2yB3H09xCd4U3cZCmR4AkOMqWgaluo5Q=
 github.com/aliyun/alibaba-cloud-sdk-go v1.63.88/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
@@ -723,66 +712,36 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
 github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
 github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
 github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
-github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk=
-github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
 github.com/aws/aws-sdk-go-v2 v1.36.2 h1:Ub6I4lq/71+tPb/atswvToaLGVMxKZvjYDVOWEExOcU=
 github.com/aws/aws-sdk-go-v2 v1.36.2/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
-github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
-github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
 github.com/aws/aws-sdk-go-v2/config v1.29.7 h1:71nqi6gUbAUiEQkypHQcNVSFJVUFANpSeUNShiwWX2M=
 github.com/aws/aws-sdk-go-v2/config v1.29.7/go.mod h1:yqJQ3nh2HWw/uxd56bicyvmDW4KSc+4wN6lL8pYjynU=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.60 h1:1dq+ELaT5ogfmqtV1eocq8SpOK1NRsuUfmhQtD/XAh4=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.60/go.mod h1:HDes+fn/xo9VeszXqjBVkxOo/aUy8Mc6QqKvZk32GlE=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 h1:JO8pydejFKmGcUNiiwt75dzLHRWthkwApIvPoyUtXEg=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29/go.mod h1:adxZ9i9DRmB8zAT0pO0yGnsmu0geomp5a3uq5XpgOJ8=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 h1:knLyPMw3r3JsU8MFHWctE4/e2qWbPaxDYLlohPvnY8c=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33/go.mod h1:EBp2HQ3f+XCB+5J+IoEbGhoV7CpJbnrsd4asNXmTL0A=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 h1:K0+Ne08zqti8J9jwENxZ5NoUyBnaFDTu3apwQJWrwwA=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33/go.mod h1:K97stwwzaWzmqxO8yLGHhClbVW1tC6VT1pDLk1pGrq4=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
 github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 h1:2scbY6//jy/s8+5vGrk7l1+UtHl0h9A4MjOO2k/TM2E=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14/go.mod h1:bRpZPHZpSe5YRHmPfK3h1M7UBFCn2szHzyx0rw04zro=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.14 h1:GH3vnPsdH2sTkZRBPnAeMqwkJXdwPNrEh9nI+DEdD0o=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.14/go.mod h1:fHFrxpH3kA2iK2NBg/jj3jxgVVfNS7WaKYC5axxr/PY=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.16 h1:Wg+SyAmJFupMcHW9CHn2QK0M5nksu8JeXWVJIRVL8Nk=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.16/go.mod h1:t2tzigPR3e5R46iVnpIQrfVbA9AIuy5VLYqyk3gffjg=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.48.6 h1:O7L9iEodiF07vJoXShMrw2XyeAqZhLUIXqWEitCq6EE=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.48.6/go.mod h1:E93uWfli9RToQzVA7+bYnynKOFcYOhNWqhY1hWSMZRc=
 github.com/aws/aws-sdk-go-v2/service/route53 v1.48.8 h1:abeu0IVRqYXSts7Tl1Yoi/BxC59xdXYX0uVSN0fbPOk=
 github.com/aws/aws-sdk-go-v2/service/route53 v1.48.8/go.mod h1:bOsuAIYHQbL+AqCldJ286MeljQL1sjUVGlpz9JMxCRM=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
 github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 h1:YV6xIKDJp6U7YB2bxfud9IENO1LRpGhe2Tv/OKtPrOQ=
 github.com/aws/aws-sdk-go-v2/service/sso v1.24.16/go.mod h1:DvbmMKgtpA6OihFJK13gHMZOZrCHttz8wPHGKXqU+3o=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 h1:kMyK3aKotq1aTBsj1eS8ERJLjqYRRRcsmP33ozlCvlk=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15/go.mod h1:5uPZU7vSNzb8Y0dm75xTikinegPYK3uJmIHQZFq5Aqo=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w=
 github.com/aws/aws-sdk-go-v2/service/sts v1.33.15 h1:ht1jVmeeo2anR7zDiYJLSnRYnO/9NILXXu42FP3rJg0=
 github.com/aws/aws-sdk-go-v2/service/sts v1.33.15/go.mod h1:xWZ5cOiFe3czngChE4LhCBqUxNwgfwndEF7XlYP/yD8=
 github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
-github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
-github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
 github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -804,8 +763,6 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
 github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
 github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
 github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
-github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
-github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
 github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ=
 github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@@ -816,8 +773,6 @@ github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdf
 github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
 github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
 github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
-github.com/casdoor/casdoor-go-sdk v1.3.0 h1:iUZKsrNUkhtAoyitFIFw3e6TchctAdoxmVgLDtNAgpc=
-github.com/casdoor/casdoor-go-sdk v1.3.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
 github.com/casdoor/casdoor-go-sdk v1.4.0 h1:EhnIcMeCPiDE66tedy6EISkVjndR78slnwXqTfUnyhU=
 github.com/casdoor/casdoor-go-sdk v1.4.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
 github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@@ -827,7 +782,6 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
-github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -839,8 +793,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
 github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
-github.com/civo/civogo v0.3.93 h1:wxzMamDKYu2lszObvx92tTFDpi0sCJbDO+CL3cR/P28=
-github.com/civo/civogo v0.3.93/go.mod h1:7UCYX+qeeJbrG55E1huv+0ySxcHTqq/26FcHLVelQJM=
 github.com/civo/civogo v0.3.94 h1:VhdqaJ2m4z8Jz8arzyzVjokRnO8JQ3lGjLKLshJ1eJI=
 github.com/civo/civogo v0.3.94/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
 github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
@@ -871,8 +823,6 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpu/goacmedns v0.1.1 h1:DM3H2NiN2oam7QljgGY5ygy4yDXhK5Z4JUnqaugs2C4=
-github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -965,8 +915,6 @@ github.com/gin-contrib/static v1.1.3 h1:WLOpkBtMDJ3gATFZgNJyVibFMio/UHonnueqJsQ0
 github.com/gin-contrib/static v1.1.3/go.mod h1:zejpJ/YWp8cZj/6EpiL5f/+skv5daQTNwRx1E8Pci30=
 github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
-github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o=
-github.com/go-acme/lego/v4 v4.21.0/go.mod h1:HrSWzm3Ckj45Ie3i+p1zKVobbQoMOaGu9m4up0dUeDI=
 github.com/go-acme/lego/v4 v4.22.2 h1:ck+HllWrV/rZGeYohsKQ5iKNnU/WAZxwOdiu6cxky+0=
 github.com/go-acme/lego/v4 v4.22.2/go.mod h1:E2FndyI3Ekv0usNJt46mFb9LVpV/XBYT+4E3tz02Tzo=
 github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
@@ -1016,15 +964,11 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
-github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
 github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
 github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
 github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
 github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
-github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
-github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
 github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -1034,12 +978,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
 github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
-github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
-github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
 github.com/go-webauthn/webauthn v0.12.1 h1:fQNKWc+gd7i1TW8FmlB0jQTHyc2GYYlV/QdLUxo+MSA=
 github.com/go-webauthn/webauthn v0.12.1/go.mod h1:Q13xKHZi459wU8IoFjm8jQ6CMRyad+kegblwMFFhQGU=
-github.com/go-webauthn/x v0.1.16 h1:EaVXZntpyHviN9ykjdRBQIw9B0Ed3LO5FW7mDiMQEa8=
-github.com/go-webauthn/x v0.1.16/go.mod h1:jhYjfwe/AVYaUs2mUXArj7vvZj+SpooQPyyQGNab+Us=
 github.com/go-webauthn/x v0.1.18 h1:9xxiKRKCHx/1R2RF+4xb1qY5QDIO0RlTmH5L02lmRH4=
 github.com/go-webauthn/x v0.1.18/go.mod h1:Q/uHdGGFrZ7psEcoEStYunurZuG3Z9UDZJetM8qDTtA=
 github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
@@ -1275,8 +1215,6 @@ github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKEN
 github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.134 h1:s0IUKcV6UCu84UZMKejNiUnJ4l2Jw9HM0IxHvdJCg9A=
-github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.134/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI=
 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.136 h1:T785NUg5245nWpPVHLVR8lBd+zGQYR14Vi/TCX1iu3A=
 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.136/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
 github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
@@ -1595,12 +1533,8 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
 github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
-github.com/oracle/oci-go-sdk/v65 v65.83.0 h1:KFI0oyyCTPmgevHF+QlN02Zdf23Jx1p1X+4KPyH14H8=
-github.com/oracle/oci-go-sdk/v65 v65.83.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
 github.com/oracle/oci-go-sdk/v65 v65.83.2 h1:4DtSCVe/AaHcqb08wXgjplOM8+tc4pqNwcUYZmplbv8=
 github.com/oracle/oci-go-sdk/v65 v65.83.2/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
-github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
-github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
 github.com/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw=
 github.com/ovh/go-ovh v1.7.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -1714,8 +1648,6 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
 github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
 github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
 github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
-github.com/sashabaranov/go-openai v1.36.1 h1:EVfRXwIlW2rUzpx6vR+aeIKCK/xylSrVYAx1TMTSX3g=
-github.com/sashabaranov/go-openai v1.36.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
 github.com/sashabaranov/go-openai v1.37.0 h1:hQQowgYm4OXJ1Z/wTrE+XZaO20BYsL0R3uRPSpfNZkY=
 github.com/sashabaranov/go-openai v1.37.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
@@ -1817,12 +1749,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
 github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1091 h1:RxogX8ZCPBmZ6PY7DjnWnwGRkAkYEEinT5WNNxbLVeo=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1091/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1101 h1:pz6QIjHR7TXQfEogg4pwvvTDgsB1L+RQGgnr2tBDzc4=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1101/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1091 h1:36WwNgrtoGKszQovUj3+0CjNsM1gMQ3a5lvZx2bkjng=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1091/go.mod h1:cWRGvOUnMQMky4oliMX1dXT6Z4CbsSGOIxaUcrD5Zvw=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1101 h1:9c05Ky7Ppww06YFE579TjI89pfNnC2zdJufx7SXUTi8=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1101/go.mod h1:fBdcH58lmwIwePei24b9QFdE1w8+brIX9yTrf82n7yM=
 github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
@@ -1847,8 +1775,6 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI=
 github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
-github.com/uozi-tech/cosy v1.14.3 h1:YDleGHghw5Dtd8H7Fy0Iq0caXfxmhk7Zt6tJBONjq5Q=
-github.com/uozi-tech/cosy v1.14.3/go.mod h1:DSKLtoVaGLUlJ8KiQ1vWEsnv85epRrAAMXSijuq+asM=
 github.com/uozi-tech/cosy v1.14.4 h1:9X9CzxYjTg9DRQKgBjYvDNOAYYFclOXYYq518nO4vr0=
 github.com/uozi-tech/cosy v1.14.4/go.mod h1:DSKLtoVaGLUlJ8KiQ1vWEsnv85epRrAAMXSijuq+asM=
 github.com/uozi-tech/cosy-driver-mysql v0.2.2 h1:22S/XNIvuaKGqxQPsYPXN8TZ8hHjCQdcJKVQ83Vzxoo=
@@ -1862,8 +1788,6 @@ github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjc
 github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
 github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
 github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
-github.com/volcengine/volc-sdk-golang v1.0.194 h1:3o0INQzdtYJWvdGrtX02booCqPL5TsWSq2W1Ur7Bzlo=
-github.com/volcengine/volc-sdk-golang v1.0.194/go.mod h1:u0VtPvlXWpXDTmc9IHkaW1q+5Jjwus4oAqRhNMDRInE=
 github.com/volcengine/volc-sdk-golang v1.0.195 h1:hKX4pBhmKcB3652BTdcAmtgizEPBnoQUpTM+j5blMA4=
 github.com/volcengine/volc-sdk-golang v1.0.195/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
 github.com/vultr/govultr/v3 v3.14.1 h1:9BpyZgsWasuNoR39YVMcq44MSaF576Z4D+U3ro58eJQ=
@@ -1879,12 +1803,8 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/yandex-cloud/go-genproto v0.0.0-20250203115010-0bcba64c41f6 h1:CHYGew+KO1JaK5sx/N2ApgVCTGCKvfSl0sSPplTyCog=
-github.com/yandex-cloud/go-genproto v0.0.0-20250203115010-0bcba64c41f6/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
 github.com/yandex-cloud/go-genproto v0.0.0-20250217111757-ccaea642a16c h1:WTK2XiEf68Uv0rT6mjrB5hKkwZvMnWWHPF3OjK/fYL8=
 github.com/yandex-cloud/go-genproto v0.0.0-20250217111757-ccaea642a16c/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
-github.com/yandex-cloud/go-sdk v0.0.0-20250203123950-24786ecffd92 h1:UTcY1921ZXBABB5JpSWxccyZaCjMuSbniyifW6nmLZ4=
-github.com/yandex-cloud/go-sdk v0.0.0-20250203123950-24786ecffd92/go.mod h1:MQm5WxsYpQRdUklz2C8q3uDhuIaDzzV7seLErAILLBE=
 github.com/yandex-cloud/go-sdk v0.0.0-20250210144447-399a857b9c4e h1:RiNKkceZPeMWLSIl31RSgPeSmpT9K7eTXOcA9YxTBfg=
 github.com/yandex-cloud/go-sdk v0.0.0-20250210144447-399a857b9c4e/go.mod h1:OCW2kKPZ900GNQ9aKDaX7/FUQmxGdm+CKeXVocbM4d0=
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
@@ -1911,7 +1831,6 @@ go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsX
 go.etcd.io/etcd/client/v2 v2.305.7/go.mod h1:GQGT5Z3TBuAQGvgPfhR7VPySu/SudxmEkRq9BgzFU6s=
 go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
 go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA=
-go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0=
 go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
 go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
 go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
@@ -2009,11 +1928,6 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
-golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
-golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
-golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
-golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
 golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA=
 golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -2031,10 +1945,6 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
-golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
-golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
-golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34=
-golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
 golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
 golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
@@ -2160,8 +2070,6 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
 golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -2334,7 +2242,6 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
 golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -2352,9 +2259,8 @@ golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
-golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
-golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
+golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
+golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2374,7 +2280,6 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
 golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -2461,8 +2366,6 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
-golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
-golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
 golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
 golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -2541,8 +2444,6 @@ google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60c
 google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
 google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
 google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
-google.golang.org/api v0.219.0 h1:nnKIvxKs/06jWawp2liznTBnMRQBEPpGo7I+oEypTX0=
-google.golang.org/api v0.219.0/go.mod h1:K6OmjGm+NtLrIkHxv1U3a0qIf/0JOvAHd5O/6AoyKYE=
 google.golang.org/api v0.221.0 h1:qzaJfLhDsbMeFee8zBRdt/Nc+xmOuafD/dbdgGfutOU=
 google.golang.org/api v0.221.0/go.mod h1:7sOU2+TL4TxUTdbi0gWgAIg7tH5qBXxoyhtL+9x3biQ=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -2685,16 +2586,10 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl
 google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
-google.golang.org/genproto v0.0.0-20250204164813-702378808489 h1:nQcbCCOg2h2CQ0yA8SY3AHqriNKDvsetuq9mE/HFjtc=
-google.golang.org/genproto v0.0.0-20250204164813-702378808489/go.mod h1:wkQ2Aj/xvshAUDtO/JHvu9y+AaN9cqs28QuSVSHtZSY=
 google.golang.org/genproto v0.0.0-20250218202821-56aae31c358a h1:Xx6e5r1AOINOgm2ZuzvwDueGlOOml4PKBUry8jqyS6U=
 google.golang.org/genproto v0.0.0-20250218202821-56aae31c358a/go.mod h1:Cmg1ztsSOnOsWxOiPTOUX8gegyHg5xADRncIHdtec8U=
-google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o=
-google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4=
 google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
 google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 h1:5bKytslY8ViY0Cj/ewmRtrWHW64bNF03cAatUUFCdFI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -2759,8 +2654,6 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
-google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
 google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@@ -2840,12 +2733,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
-k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc=
-k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
 k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
 k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
-k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
-k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
 k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
 k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=

+ 79 - 0
internal/stream/delete.go

@@ -0,0 +1,79 @@
+package stream
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+	"runtime"
+
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/internal/notification"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/go-resty/resty/v2"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// Delete deletes a site by removing the file in sites-available
+func Delete(name string) (err error) {
+	availablePath := nginx.GetConfPath("streams-available", name)
+
+	syncDelete(name)
+
+	s := query.Site
+	_, err = s.Where(s.Path.Eq(availablePath)).Unscoped().Delete(&model.Site{})
+	if err != nil {
+		return
+	}
+
+	enabledPath := nginx.GetConfPath("streams-enabled", name)
+
+	if !helper.FileExists(availablePath) {
+		return ErrStreamNotFound
+	}
+
+	if helper.FileExists(enabledPath) {
+		return ErrStreamIsEnabled
+	}
+
+	certModel := model.Cert{Filename: name}
+	_ = certModel.Remove()
+
+	err = os.Remove(availablePath)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+func syncDelete(name string) {
+	nodes := getSyncNodes(name)
+
+	for _, node := range nodes {
+		go func() {
+			defer func() {
+				if err := recover(); err != nil {
+					buf := make([]byte, 1024)
+					runtime.Stack(buf, false)
+					logger.Error(err)
+				}
+			}()
+			client := resty.New()
+			client.SetBaseURL(node.URL)
+			resp, err := client.R().
+				SetHeader("X-Node-Secret", node.Token).
+				Delete(fmt.Sprintf("/api/streams/%s", name))
+			if err != nil {
+				notification.Error("Delete Remote Stream Error", err.Error())
+				return
+			}
+			if resp.StatusCode() != http.StatusOK {
+				notification.Error("Delete Remote Stream Error", NewSyncResult(node.Name, name, resp).String())
+				return
+			}
+			notification.Success("Delete Remote Stream Success", NewSyncResult(node.Name, name, resp).String())
+		}()
+	}
+}

+ 81 - 0
internal/stream/disable.go

@@ -0,0 +1,81 @@
+package stream
+
+import (
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/internal/notification"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/go-resty/resty/v2"
+	"github.com/uozi-tech/cosy/logger"
+	"net/http"
+	"os"
+	"runtime"
+	"sync"
+)
+
+// Disable disables a site by removing the symlink in sites-enabled
+func Disable(name string) (err error) {
+	enabledConfigFilePath := nginx.GetConfPath("streams-enabled", name)
+	_, err = os.Stat(enabledConfigFilePath)
+	if err != nil {
+		return
+	}
+
+	err = os.Remove(enabledConfigFilePath)
+	if err != nil {
+		return
+	}
+
+	// delete auto cert record
+	certModel := model.Cert{Filename: name}
+	err = certModel.Remove()
+	if err != nil {
+		return
+	}
+
+	output := nginx.Reload()
+	if nginx.GetLogLevel(output) > nginx.Warn {
+		return fmt.Errorf("%s", output)
+	}
+
+	go syncDisable(name)
+
+	return
+}
+
+func syncDisable(name string) {
+	nodes := getSyncNodes(name)
+
+	wg := &sync.WaitGroup{}
+	wg.Add(len(nodes))
+
+	for _, node := range nodes {
+		go func() {
+			defer func() {
+				if err := recover(); err != nil {
+					buf := make([]byte, 1024)
+					runtime.Stack(buf, false)
+					logger.Error(err)
+				}
+			}()
+			defer wg.Done()
+
+			client := resty.New()
+			client.SetBaseURL(node.URL)
+			resp, err := client.R().
+				SetHeader("X-Node-Secret", node.Token).
+				Post(fmt.Sprintf("/api/streams/%s/disable", name))
+			if err != nil {
+				notification.Error("Disable Remote Stream Error", err.Error())
+				return
+			}
+			if resp.StatusCode() != http.StatusOK {
+				notification.Error("Disable Remote Stream Error", NewSyncResult(node.Name, name, resp).String())
+				return
+			}
+			notification.Success("Disable Remote Stream Success", NewSyncResult(node.Name, name, resp).String())
+		}()
+	}
+
+	wg.Wait()
+}

+ 23 - 0
internal/stream/duplicate.go

@@ -0,0 +1,23 @@
+package stream
+
+import (
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+)
+
+// Duplicate duplicates a site by copying the file
+func Duplicate(src, dst string) (err error) {
+	src = nginx.GetConfPath("streams-available", src)
+	dst = nginx.GetConfPath("streams-available", dst)
+
+	if helper.FileExists(dst) {
+		return ErrDstFileExists
+	}
+
+	_, err = helper.CopyFile(src, dst)
+	if err != nil {
+		return
+	}
+
+	return
+}

+ 87 - 0
internal/stream/enable.go

@@ -0,0 +1,87 @@
+package stream
+
+import (
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/internal/notification"
+	"github.com/go-resty/resty/v2"
+	"github.com/uozi-tech/cosy/logger"
+	"net/http"
+	"os"
+	"runtime"
+	"sync"
+)
+
+// Enable enables a site by creating a symlink in sites-enabled
+func Enable(name string) (err error) {
+	configFilePath := nginx.GetConfPath("streams-available", name)
+	enabledConfigFilePath := nginx.GetConfPath("streams-enabled", name)
+
+	_, err = os.Stat(configFilePath)
+	if err != nil {
+		return
+	}
+
+	if helper.FileExists(enabledConfigFilePath) {
+		return
+	}
+
+	err = os.Symlink(configFilePath, enabledConfigFilePath)
+	if err != nil {
+		return
+	}
+
+	// Test nginx config, if not pass, then disable the site.
+	output := nginx.TestConf()
+	if nginx.GetLogLevel(output) > nginx.Warn {
+		_ = os.Remove(enabledConfigFilePath)
+		return fmt.Errorf("%s", output)
+	}
+
+	output = nginx.Reload()
+	if nginx.GetLogLevel(output) > nginx.Warn {
+		return fmt.Errorf("%s", output)
+	}
+
+	go syncEnable(name)
+
+	return
+}
+
+func syncEnable(name string) {
+	nodes := getSyncNodes(name)
+
+	wg := &sync.WaitGroup{}
+	wg.Add(len(nodes))
+
+	for _, node := range nodes {
+		go func() {
+			defer func() {
+				if err := recover(); err != nil {
+					buf := make([]byte, 1024)
+					runtime.Stack(buf, false)
+					logger.Error(err)
+				}
+			}()
+			defer wg.Done()
+
+			client := resty.New()
+			client.SetBaseURL(node.URL)
+			resp, err := client.R().
+				SetHeader("X-Node-Secret", node.Token).
+				Post(fmt.Sprintf("/api/streams/%s/enable", name))
+			if err != nil {
+				notification.Error("Enable Remote Stream Error", err.Error())
+				return
+			}
+			if resp.StatusCode() != http.StatusOK {
+				notification.Error("Enable Remote Stream Error", NewSyncResult(node.Name, name, resp).String())
+				return
+			}
+			notification.Success("Enable Remote Stream Success", NewSyncResult(node.Name, name, resp).String())
+		}()
+	}
+
+	wg.Wait()
+}

+ 10 - 0
internal/stream/errors.go

@@ -0,0 +1,10 @@
+package stream
+
+import "github.com/uozi-tech/cosy"
+
+var (
+	e                = cosy.NewErrorScope("stream")
+	ErrStreamNotFound  = e.New(40401, "stream not found")
+	ErrDstFileExists = e.New(50001, "destination file already exists")
+	ErrStreamIsEnabled = e.New(50002, "stream is enabled")
+)

+ 108 - 0
internal/stream/rename.go

@@ -0,0 +1,108 @@
+package stream
+
+import (
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/internal/notification"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/go-resty/resty/v2"
+	"github.com/uozi-tech/cosy/logger"
+	"net/http"
+	"os"
+	"runtime"
+	"sync"
+)
+
+func Rename(oldName string, newName string) (err error) {
+	oldPath := nginx.GetConfPath("streams-available", oldName)
+	newPath := nginx.GetConfPath("streams-available", newName)
+
+	if oldPath == newPath {
+		return
+	}
+
+	// check if dst file exists, do not rename
+	if helper.FileExists(newPath) {
+		return ErrDstFileExists
+	}
+
+	s := query.Site
+	_, _ = s.Where(s.Path.Eq(oldPath)).Update(s.Path, newPath)
+
+	err = os.Rename(oldPath, newPath)
+	if err != nil {
+		return
+	}
+
+	// recreate a soft link
+	oldEnabledConfigFilePath := nginx.GetConfPath("streams-enabled", oldName)
+	if helper.SymbolLinkExists(oldEnabledConfigFilePath) {
+		_ = os.Remove(oldEnabledConfigFilePath)
+		newEnabledConfigFilePath := nginx.GetConfPath("streams-enabled", newName)
+		err = os.Symlink(newPath, newEnabledConfigFilePath)
+		if err != nil {
+			return
+		}
+	}
+
+	// test nginx configuration
+	output := nginx.TestConf()
+	if nginx.GetLogLevel(output) > nginx.Warn {
+		return fmt.Errorf("%s", output)
+	}
+
+	// reload nginx
+	output = nginx.Reload()
+	if nginx.GetLogLevel(output) > nginx.Warn {
+		return fmt.Errorf("%s", output)
+	}
+
+	go syncRename(oldName, newName)
+
+	return
+}
+
+func syncRename(oldName, newName string) {
+	nodes := getSyncNodes(newName)
+
+	wg := &sync.WaitGroup{}
+	wg.Add(len(nodes))
+
+	for _, node := range nodes {
+		go func() {
+			defer func() {
+				if err := recover(); err != nil {
+					buf := make([]byte, 1024)
+					runtime.Stack(buf, false)
+					logger.Error(err)
+				}
+			}()
+			defer wg.Done()
+
+			client := resty.New()
+			client.SetBaseURL(node.URL)
+			resp, err := client.R().
+				SetHeader("X-Node-Secret", node.Token).
+				SetBody(map[string]string{
+					"new_name": newName,
+				}).
+				Post(fmt.Sprintf("/api/streams/%s/rename", oldName))
+			if err != nil {
+				notification.Error("Rename Remote Stream Error", err.Error())
+				return
+			}
+			if resp.StatusCode() != http.StatusOK {
+				notification.Error("Rename Remote Stream Error",
+					NewSyncResult(node.Name, oldName, resp).
+						SetNewName(newName).String())
+				return
+			}
+			notification.Success("Rename Remote Stream Success",
+				NewSyncResult(node.Name, oldName, resp).
+					SetNewName(newName).String())
+		}()
+	}
+
+	wg.Wait()
+}

+ 107 - 0
internal/stream/save.go

@@ -0,0 +1,107 @@
+package stream
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+	"runtime"
+	"sync"
+
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/internal/notification"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/go-resty/resty/v2"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// Save saves a site configuration file
+func Save(name string, content string, overwrite bool, syncNodeIds []uint64) (err error) {
+	path := nginx.GetConfPath("streams-available", name)
+	if !overwrite && helper.FileExists(path) {
+		return ErrDstFileExists
+	}
+
+	err = os.WriteFile(path, []byte(content), 0644)
+	if err != nil {
+		return
+	}
+
+	enabledConfigFilePath := nginx.GetConfPath("streams-enabled", name)
+	if helper.FileExists(enabledConfigFilePath) {
+		// Test nginx configuration
+		output := nginx.TestConf()
+
+		if nginx.GetLogLevel(output) > nginx.Warn {
+			return fmt.Errorf("%s", output)
+		}
+
+		output = nginx.Reload()
+
+		if nginx.GetLogLevel(output) > nginx.Warn {
+			return fmt.Errorf("%s", output)
+		}
+	}
+
+	s := query.Stream
+	_, err = s.Where(s.Path.Eq(path)).
+		Select(s.SyncNodeIDs).
+		Updates(&model.Site{
+			SyncNodeIDs: syncNodeIds,
+		})
+	if err != nil {
+		return
+	}
+
+	go syncSave(name, content)
+
+	return
+}
+
+func syncSave(name string, content string) {
+	nodes := getSyncNodes(name)
+
+	wg := &sync.WaitGroup{}
+	wg.Add(len(nodes))
+
+	for _, node := range nodes {
+		go func() {
+			defer func() {
+				if err := recover(); err != nil {
+					buf := make([]byte, 1024)
+					runtime.Stack(buf, false)
+					logger.Error(err)
+				}
+			}()
+			defer wg.Done()
+
+			client := resty.New()
+			client.SetBaseURL(node.URL)
+			resp, err := client.R().
+				SetHeader("X-Node-Secret", node.Token).
+				SetBody(map[string]interface{}{
+					"content":   content,
+					"overwrite": true,
+				}).
+				Post(fmt.Sprintf("/api/streams/%s", name))
+			if err != nil {
+				notification.Error("Save Remote Stream Error", err.Error())
+				return
+			}
+			if resp.StatusCode() != http.StatusOK {
+				notification.Error("Save Remote Stream Error", NewSyncResult(node.Name, name, resp).String())
+				return
+			}
+			notification.Success("Save Remote Stream Success", NewSyncResult(node.Name, name, resp).String())
+
+			// Check if the site is enabled, if so then enable it on the remote node
+			enabledConfigFilePath := nginx.GetConfPath("streams-enabled", name)
+			if helper.FileExists(enabledConfigFilePath) {
+				syncEnable(name)
+			}
+		}()
+	}
+
+	wg.Wait()
+}

+ 74 - 0
internal/stream/sync.go

@@ -0,0 +1,74 @@
+package stream
+
+import (
+	"encoding/json"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"github.com/go-resty/resty/v2"
+	"github.com/samber/lo"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// getSyncNodes returns the nodes that need to be synchronized by site name
+func getSyncNodes(name string) (nodes []*model.Environment) {
+	configFilePath := nginx.GetConfPath("streams-available", name)
+	s := query.Site
+	site, err := s.Where(s.Path.Eq(configFilePath)).
+		Preload(s.SiteCategory).First()
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	syncNodeIds := site.SyncNodeIDs
+	// inherit sync node ids from site category
+	if site.SiteCategory != nil {
+		syncNodeIds = append(syncNodeIds, site.SiteCategory.SyncNodeIds...)
+	}
+	syncNodeIds = lo.Uniq(syncNodeIds)
+
+	e := query.Environment
+	nodes, err = e.Where(e.ID.In(syncNodeIds...)).Find()
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+	return
+}
+
+type SyncResult struct {
+	StatusCode int    `json:"status_code"`
+	Node       string `json:"node"`
+	Name       string `json:"name"`
+	NewName    string `json:"new_name,omitempty"`
+	Response   gin.H  `json:"response"`
+	Error      string `json:"error"`
+}
+
+func NewSyncResult(node string, siteName string, resp *resty.Response) (s *SyncResult) {
+	s = &SyncResult{
+		StatusCode: resp.StatusCode(),
+		Node:       node,
+		Name:       siteName,
+	}
+	err := json.Unmarshal(resp.Body(), &s.Response)
+	if err != nil {
+		logger.Error(err)
+	}
+	return
+}
+
+func (s *SyncResult) SetNewName(name string) *SyncResult {
+	s.NewName = name
+	return s
+}
+
+func (s *SyncResult) String() string {
+	b, err := json.Marshal(s)
+	if err != nil {
+		logger.Error(err)
+	}
+	return string(b)
+}

+ 3 - 2
model/stream.go

@@ -2,6 +2,7 @@ package model
 
 type Stream struct {
 	Model
-	Path     string `json:"path"`
-	Advanced bool   `json:"advanced"`
+	Path        string   `json:"path"`
+	Advanced    bool     `json:"advanced"`
+	SyncNodeIDs []uint64 `json:"sync_node_ids" gorm:"serializer:json"`
 }

+ 12 - 8
query/streams.gen.go

@@ -34,6 +34,7 @@ func newStream(db *gorm.DB, opts ...gen.DOOption) stream {
 	_stream.DeletedAt = field.NewField(tableName, "deleted_at")
 	_stream.Path = field.NewString(tableName, "path")
 	_stream.Advanced = field.NewBool(tableName, "advanced")
+	_stream.SyncNodeIDs = field.NewField(tableName, "sync_node_ids")
 
 	_stream.fillFieldMap()
 
@@ -43,13 +44,14 @@ func newStream(db *gorm.DB, opts ...gen.DOOption) stream {
 type stream struct {
 	streamDo
 
-	ALL       field.Asterisk
-	ID        field.Uint64
-	CreatedAt field.Time
-	UpdatedAt field.Time
-	DeletedAt field.Field
-	Path      field.String
-	Advanced  field.Bool
+	ALL         field.Asterisk
+	ID          field.Uint64
+	CreatedAt   field.Time
+	UpdatedAt   field.Time
+	DeletedAt   field.Field
+	Path        field.String
+	Advanced    field.Bool
+	SyncNodeIDs field.Field
 
 	fieldMap map[string]field.Expr
 }
@@ -72,6 +74,7 @@ func (s *stream) updateTableName(table string) *stream {
 	s.DeletedAt = field.NewField(table, "deleted_at")
 	s.Path = field.NewString(table, "path")
 	s.Advanced = field.NewBool(table, "advanced")
+	s.SyncNodeIDs = field.NewField(table, "sync_node_ids")
 
 	s.fillFieldMap()
 
@@ -88,13 +91,14 @@ func (s *stream) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
 }
 
 func (s *stream) fillFieldMap() {
-	s.fieldMap = make(map[string]field.Expr, 6)
+	s.fieldMap = make(map[string]field.Expr, 7)
 	s.fieldMap["id"] = s.ID
 	s.fieldMap["created_at"] = s.CreatedAt
 	s.fieldMap["updated_at"] = s.UpdatedAt
 	s.fieldMap["deleted_at"] = s.DeletedAt
 	s.fieldMap["path"] = s.Path
 	s.fieldMap["advanced"] = s.Advanced
+	s.fieldMap["sync_node_ids"] = s.SyncNodeIDs
 }
 
 func (s stream) clone(db *gorm.DB) stream {