浏览代码

wip: ChatGPT assistant

0xJacky 2 年之前
父节点
当前提交
4cd77f28eb

+ 6 - 0
app.example.ini

@@ -14,3 +14,9 @@ NginxConfigDir =
 [nginx_log]
 AccessLogPath = /var/log/nginx/access.log
 ErrorLogPath = /var/log/nginx/error.log
+
+[openai]
+Model =
+BaseUrl =
+Proxy =
+Token =

+ 3 - 0
frontend/components.d.ts

@@ -16,6 +16,7 @@ declare module '@vue/runtime-core' {
     ACol: typeof import('ant-design-vue/es')['Col']
     ACollapse: typeof import('ant-design-vue/es')['Collapse']
     ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
+    AComment: typeof import('ant-design-vue/es')['Comment']
     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
     ADivider: typeof import('ant-design-vue/es')['Divider']
     ADrawer: typeof import('ant-design-vue/es')['Drawer']
@@ -55,9 +56,11 @@ declare module '@vue/runtime-core' {
     ATabs: typeof import('ant-design-vue/es')['Tabs']
     ATag: typeof import('ant-design-vue/es')['Tag']
     ATextarea: typeof import('ant-design-vue/es')['Textarea']
+    ATooltip: typeof import('ant-design-vue/es')['Tooltip']
     BreadcrumbBreadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
     ChartAreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
     ChartRadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default']
+    ChatGPTChatGPT: typeof import('./src/components/ChatGPT/ChatGPT.vue')['default']
     CodeEditorCodeEditor: typeof import('./src/components/CodeEditor/CodeEditor.vue')['default']
     FooterToolbarFooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
     LogoLogo: typeof import('./src/components/Logo/Logo.vue')['default']

+ 1 - 0
frontend/package.json

@@ -20,6 +20,7 @@
         "apexcharts": "^3.36.3",
         "axios": "^1.2.2",
         "dayjs": "^1.11.7",
+        "highlight.js": "^11.7.0",
         "marked": "^4.2.5",
         "nprogress": "^0.2.0",
         "pinia": "^2.0.28",

+ 9 - 0
frontend/src/api/openai.ts

@@ -0,0 +1,9 @@
+import http from '@/lib/http'
+
+const openai = {
+    store_record(data: any) {
+        return http.post('/chat_gpt_record', data)
+    }
+}
+
+export default openai

+ 219 - 0
frontend/src/components/ChatGPT/ChatGPT.vue

@@ -0,0 +1,219 @@
+<script setup lang="ts">
+import {computed, ref, watch} from 'vue'
+import {useGettext} from 'vue3-gettext'
+import {useUserStore} from '@/pinia'
+import {storeToRefs} from 'pinia'
+import {urlJoin} from '@/lib/helper'
+import {marked} from 'marked'
+import hljs from 'highlight.js'
+import 'highlight.js/styles/vs2015.css'
+import {SendOutlined} from '@ant-design/icons-vue'
+import Template from '@/views/template/Template.vue'
+import openai from '@/api/openai'
+
+const {$gettext} = useGettext()
+
+const props = defineProps(['content', 'path', 'history_messages'])
+
+watch(computed(() => props.history_messages), () => {
+    messages.value = props.history_messages
+})
+
+const {current} = useGettext()
+
+const messages: any = ref([])
+
+const loading = ref(false)
+const ask_buffer = ref('')
+
+async function send() {
+    if (messages.value.length === 0) {
+        messages.value.push({
+            role: 'user',
+            content: props.content + '\n\nCurrent Language Code: ' + current
+        })
+    } else {
+        messages.value.push({
+            role: 'user',
+            content: ask_buffer.value
+        })
+        ask_buffer.value = ''
+    }
+    loading.value = true
+    const t = ref({
+        role: 'assistant',
+        content: ''
+    })
+    const user = useUserStore()
+
+    const {token} = storeToRefs(user)
+
+    console.log('fetching...')
+
+    let res = await fetch(urlJoin(window.location.pathname, '/api/chat_gpt'), {
+        method: 'POST',
+        headers: {'Accept': 'text/event-stream', Authorization: token.value},
+        body: JSON.stringify({messages: messages.value})
+    })
+
+    messages.value.push(t.value)
+    // read body as stream
+    console.log('reading...')
+    let reader = res.body!.getReader()
+
+    // read stream
+    console.log('reading stream...')
+
+    let buffer = ''
+
+    while (true) {
+        let {done, value} = await reader.read()
+        if (done) {
+            console.log('done')
+            loading.value = false
+            store_record()
+            break
+        }
+
+        apply(value)
+    }
+
+    function apply(input: any) {
+        const decoder = new TextDecoder('utf-8')
+        const raw = decoder.decode(input)
+
+        const regex = /{"content":"(.+?)"}/g
+        const matches = raw.match(regex)
+
+        matches?.forEach(v => {
+            const content = JSON.parse(v).content
+            for (let c of content) {
+                buffer += c
+                if (isCodeBlockComplete(buffer)) {
+                    t.value.content = buffer
+                } else {
+                    t.value.content = buffer + '\n```'
+                }
+            }
+        })
+    }
+
+    function isCodeBlockComplete(text: string) {
+        const codeBlockRegex = /```/g
+        const matches = text.match(codeBlockRegex)
+        if (matches) {
+            return matches.length % 2 === 0
+        } else {
+            return true
+        }
+    }
+
+}
+
+const renderer = new marked.Renderer()
+renderer.code = (code, lang: string) => {
+    const language = hljs.getLanguage(lang) ? lang : 'nginx'
+    const highlightedCode = hljs.highlight(code, {language}).value
+    return `<pre><code class="hljs ${language}">${highlightedCode}</code></pre>`
+}
+
+marked.setOptions({
+    renderer: renderer,
+    langPrefix: 'hljs language-', // highlight.js css expects a top-level 'hljs' class.
+    pedantic: false,
+    gfm: true,
+    breaks: false,
+    sanitize: false,
+    smartypants: true,
+    xhtml: false
+})
+
+function store_record() {
+    openai.store_record({
+        file_name: props.path,
+        messages: messages.value
+    })
+}
+</script>
+
+<template>
+    <a-card title="ChatGPT">
+        <div class="chatgpt-container">
+            <template v-if="messages?.length>0">
+                <a-list
+                    class="chatgpt-log"
+                    item-layout="horizontal"
+                    :data-source="messages"
+                >
+                    <template #renderItem="{ item }">
+                        <a-list-item>
+                            <a-comment :author="item.role" :avatar="item.avatar">
+                                <template #content>
+                                    <div class="content" v-html="marked.parse(item.content)"></div>
+                                </template>
+                            </a-comment>
+                        </a-list-item>
+                    </template>
+                </a-list>
+                <div class="input-msg">
+                    <a-textarea auto-size v-model:value="ask_buffer"/>
+                    <div class="sned-btn">
+                        <a-button size="small" type="text" :loading="loading" @click="send">
+                            <send-outlined/>
+                        </a-button>
+                    </div>
+                </div>
+            </template>
+            <template v-else>
+                <a-button @click="send">{{ $gettext('Chat with ChatGPT') }}</a-button>
+            </template>
+        </div>
+    </a-card>
+</template>
+
+<style lang="less" scoped>
+.chatgpt-container {
+    margin: 0 auto;
+    max-width: 800px;
+
+    .chatgpt-log {
+        .content {
+            width: 100%;
+
+            :deep(.hljs) {
+                border-radius: 5px;
+            }
+        }
+
+        :deep(.ant-comment-content) {
+            width: 100%;
+        }
+
+        :deep(.ant-comment) {
+            width: 100%;
+        }
+
+        :deep(.ant-comment-content-detail) {
+            width: 100%;
+
+            p {
+                margin-bottom: 10px;
+            }
+        }
+
+        :deep(.ant-list-item:first-child) {
+            display: none;
+        }
+    }
+
+    .input-msg {
+        position: relative;
+
+        .sned-btn {
+            position: absolute;
+            right: 0;
+            bottom: 3px;
+        }
+    }
+}
+</style>

+ 77 - 57
frontend/src/views/domain/DomainEdit.vue

@@ -10,6 +10,7 @@ import domain from '@/api/domain'
 import ngx from '@/api/ngx'
 import {message} from 'ant-design-vue'
 import config from '@/api/config'
+import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
 
 
 const {$gettext, interpolate} = useGettext()
@@ -52,6 +53,7 @@ const advance_mode = computed({
         advance_mode_ref.value = v
     }
 })
+const history_chatgpt_record = ref([])
 
 function handle_response(r: any) {
 
@@ -64,6 +66,7 @@ function handle_response(r: any) {
     configText.value = r.config
     enabled.value = r.enabled
     auto_cert.value = r.auto_cert
+    history_chatgpt_record.value = r.chatgpt_messages
     Object.assign(ngx_config, r.tokenized)
     Object.assign(cert_info_map, r.cert_info)
 }
@@ -73,6 +76,8 @@ function init() {
         domain.get(name.value).then((r: any) => {
             handle_response(r)
         }).catch(handle_parse_error)
+    } else {
+        history_chatgpt_record.value = []
     }
 }
 
@@ -159,65 +164,71 @@ function on_change_enabled(checked: boolean) {
 }
 </script>
 <template>
-    <div>
-        <a-card :bordered="false">
-            <template #title>
-                <span style="margin-right: 10px">{{ interpolate($gettext('Edit %{n}'), {n: name}) }}</span>
-                <a-tag color="blue" v-if="enabled">
-                    {{ $gettext('Enabled') }}
-                </a-tag>
-                <a-tag color="orange" v-else>
-                    {{ $gettext('Disabled') }}
-                </a-tag>
-            </template>
-            <template #extra>
-                <div class="mode-switch">
-                    <div class="switch">
-                        <a-switch size="small" :disabled="parse_error_status"
-                                  v-model:checked="advance_mode" @change="on_mode_change"/>
+    <a-row :gutter="16">
+        <a-col :xs="24" :sm="18" :md="16">
+            <a-card :bordered="false">
+                <template #title>
+                    <span style="margin-right: 10px">{{ interpolate($gettext('Edit %{n}'), {n: name}) }}</span>
+                    <a-tag color="blue" v-if="enabled">
+                        {{ $gettext('Enabled') }}
+                    </a-tag>
+                    <a-tag color="orange" v-else>
+                        {{ $gettext('Disabled') }}
+                    </a-tag>
+                </template>
+                <template #extra>
+                    <div class="mode-switch">
+                        <div class="switch">
+                            <a-switch size="small" :disabled="parse_error_status"
+                                      v-model:checked="advance_mode" @change="on_mode_change"/>
+                        </div>
+                        <template v-if="advance_mode">
+                            <div>{{ $gettext('Advance Mode') }}</div>
+                        </template>
+                        <template v-else>
+                            <div>{{ $gettext('Basic Mode') }}</div>
+                        </template>
                     </div>
-                    <template v-if="advance_mode">
-                        <div>{{ $gettext('Advance Mode') }}</div>
-                    </template>
-                    <template v-else>
-                        <div>{{ $gettext('Basic Mode') }}</div>
-                    </template>
-                </div>
-            </template>
-
-            <transition name="slide-fade">
-                <div v-if="advance_mode" key="advance">
-                    <div class="parse-error-alert-wrapper" v-if="parse_error_status">
-                        <a-alert :message="$gettext('Nginx Configuration Parse Error')"
-                                 :description="parse_error_message"
-                                 type="error"
-                                 show-icon
-                        />
+                </template>
+
+                <transition name="slide-fade">
+                    <div v-if="advance_mode" key="advance">
+                        <div class="parse-error-alert-wrapper" v-if="parse_error_status">
+                            <a-alert :message="$gettext('Nginx Configuration Parse Error')"
+                                     :description="parse_error_message"
+                                     type="error"
+                                     show-icon
+                            />
+                        </div>
+                        <div>
+                            <code-editor v-model:content="configText"/>
+                        </div>
                     </div>
-                    <div>
-                        <code-editor v-model:content="configText"/>
+
+                    <div class="domain-edit-container" key="basic" v-else>
+                        <a-form-item :label="$gettext('Enabled')">
+                            <a-switch v-model:checked="enabled" @change="on_change_enabled"/>
+                        </a-form-item>
+                        <a-form-item :label="$gettext('Name')">
+                            <a-input v-model:value="filename"/>
+                        </a-form-item>
+                        <ngx-config-editor
+                            ref="ngx_config_editor"
+                            :ngx_config="ngx_config"
+                            :cert_info="cert_info_map"
+                            v-model:auto_cert="auto_cert"
+                            :enabled="enabled"
+                            @callback="save()"
+                        />
                     </div>
-                </div>
-
-                <div class="domain-edit-container" key="basic" v-else>
-                    <a-form-item :label="$gettext('Enabled')">
-                        <a-switch v-model:checked="enabled" @change="on_change_enabled"/>
-                    </a-form-item>
-                    <a-form-item :label="$gettext('Name')">
-                        <a-input v-model:value="filename"/>
-                    </a-form-item>
-                    <ngx-config-editor
-                        ref="ngx_config_editor"
-                        :ngx_config="ngx_config"
-                        :cert_info="cert_info_map"
-                        v-model:auto_cert="auto_cert"
-                        :enabled="enabled"
-                        @callback="save()"
-                    />
-                </div>
-            </transition>
-
-        </a-card>
+                </transition>
+            </a-card>
+        </a-col>
+
+        <a-col class="col-right" :xs="24" :sm="6" :md="8">
+            <chat-g-p-t class="chatgpt" :content="configText" :path="ngx_config.file_name"
+                        :history_messages="history_chatgpt_record"/>
+        </a-col>
 
         <footer-tool-bar>
             <a-space>
@@ -229,7 +240,7 @@ function on_change_enabled(checked: boolean) {
                 </a-button>
             </a-space>
         </footer-tool-bar>
-    </div>
+    </a-row>
 </template>
 
 <style lang="less">
@@ -237,6 +248,15 @@ function on_change_enabled(checked: boolean) {
 </style>
 
 <style lang="less" scoped>
+.col-right {
+    position: relative;
+
+    .chatgpt {
+        position: sticky;
+        top: 78px;
+    }
+}
+
 .ant-card {
     margin: 10px 0;
     box-shadow: unset;

+ 5 - 0
frontend/yarn.lock

@@ -1912,6 +1912,11 @@ he@1.2.0, he@^1.2.0:
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
+highlight.js@^11.7.0:
+  version "11.7.0"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
+  integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==
+
 html-minifier-terser@^6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab"

+ 22 - 15
go.mod

@@ -11,7 +11,7 @@ require (
 	github.com/go-co-op/gocron v1.18.0
 	github.com/go-playground/locales v0.14.1
 	github.com/go-playground/universal-translator v0.18.1
-	github.com/go-playground/validator/v10 v10.11.2
+	github.com/go-playground/validator/v10 v10.12.0
 	github.com/golang-jwt/jwt v3.2.2+incompatible
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/websocket v1.5.0
@@ -19,54 +19,61 @@ require (
 	github.com/jpillora/overseer v1.1.6
 	github.com/lib/pq v1.10.7
 	github.com/pkg/errors v0.9.1
+	github.com/sashabaranov/go-openai v1.5.3
 	github.com/shirou/gopsutil/v3 v3.23.1
 	github.com/spf13/cast v1.5.0
 	github.com/tufanbarisyildirim/gonginx v0.0.0-20230104065106-9ae864d29eed
 	github.com/unknwon/com v1.0.1
-	golang.org/x/crypto v0.6.0
+	golang.org/x/crypto v0.7.0
 	gopkg.in/ini.v1 v1.67.0
 	gorm.io/driver/sqlite v1.4.4
-	gorm.io/gorm v1.24.5
+	gorm.io/gen v0.3.21
+	gorm.io/gorm v1.24.6
+	gorm.io/plugin/dbresolver v1.4.1
 )
 
 require (
 	github.com/StackExchange/wmi v1.2.1 // indirect
-	github.com/bytedance/sonic v1.8.2 // indirect
+	github.com/bytedance/sonic v1.8.5 // indirect
 	github.com/cenkalti/backoff/v4 v4.2.0 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
-	github.com/goccy/go-json v0.10.0 // indirect
+	github.com/go-sql-driver/mysql v1.7.0 // indirect
+	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/jpillora/s3 v1.1.4 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
-	github.com/leodido/go-urn v1.2.1 // indirect
+	github.com/leodido/go-urn v1.2.2 // indirect
 	github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect
 	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/mattn/go-sqlite3 v1.14.16 // indirect
 	github.com/miekg/dns v1.1.50 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.7 // indirect
 	github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
 	github.com/robfig/cron/v3 v3.0.1 // indirect
 	github.com/tklauser/go-sysconf v0.3.11 // indirect
 	github.com/tklauser/numcpus v0.6.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
-	github.com/ugorji/go/codec v1.2.10 // indirect
+	github.com/ugorji/go/codec v1.2.11 // indirect
 	github.com/yusufpapurcu/wmi v1.2.2 // indirect
-	golang.org/x/arch v0.2.0 // indirect
-	golang.org/x/mod v0.8.0 // indirect
-	golang.org/x/net v0.7.0 // indirect
+	golang.org/x/arch v0.3.0 // indirect
+	golang.org/x/mod v0.9.0 // indirect
+	golang.org/x/net v0.8.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
-	golang.org/x/sys v0.5.0 // indirect
-	golang.org/x/text v0.7.0 // indirect
-	golang.org/x/tools v0.6.0 // indirect
-	google.golang.org/protobuf v1.28.1 // indirect
+	golang.org/x/sys v0.6.0 // indirect
+	golang.org/x/text v0.8.0 // indirect
+	golang.org/x/tools v0.7.0 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/fsnotify.v1 v1.4.7 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	gorm.io/datatypes v1.1.1 // indirect
+	gorm.io/driver/mysql v1.4.7 // indirect
+	gorm.io/hints v1.1.1 // indirect
 )

+ 67 - 33
go.sum

@@ -2,8 +2,8 @@ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrU
 github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
 github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
-github.com/bytedance/sonic v1.8.2 h1:Eq1oE3xWIBE3tj2ZtJFK1rDAx7+uA4bRytozVhXMHKY=
-github.com/bytedance/sonic v1.8.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/bytedance/sonic v1.8.5 h1:kjX0/vo5acEQ/sinD/18SkA/lDDUk23F0RcaHvI7omc=
+github.com/bytedance/sonic v1.8.5/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
 github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
@@ -44,12 +44,17 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
 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.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
-github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
-github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
-github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
-github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
+github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
+github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
 github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -67,6 +72,14 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
+github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
+github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
+github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
+github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
+github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@@ -87,8 +100,8 @@ github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8t
 github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
-github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
+github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
 github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
 github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
@@ -100,6 +113,7 @@ github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
 github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
 github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
 github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -108,8 +122,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
-github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
+github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -119,7 +133,10 @@ github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3g
 github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
-github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
+github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
+github.com/sashabaranov/go-openai v1.5.3 h1:o6n6dj0h9u+5mE1m+D8eT0zYhh7229o8ymDd2zDwAXU=
+github.com/sashabaranov/go-openai v1.5.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
 github.com/shirou/gopsutil/v3 v3.23.1 h1:a9KKO+kGLKEvcPIs4W62v0nu3sciVDOOOPUD0Hz7z/4=
 github.com/shirou/gopsutil/v3 v3.23.1/go.mod h1:NN6mnm5/0k8jw4cBfCnJtr5L7ErOTg18tMNpgFkn0hA=
 github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -141,8 +158,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
 github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
 github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
@@ -153,8 +171,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ=
-github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
 github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -162,25 +180,25 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
 github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
 github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/arch v0.2.0 h1:W1sUEHXiJTfjaFJ5SLo0N6lZn+0eO5gWD1MFeTGqQEY=
-golang.org/x/arch v0.2.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -202,30 +220,30 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
+golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
@@ -237,11 +255,27 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/datatypes v1.1.1 h1:XAjO7NNfUKVUvnS3+BkqMrPXxCAcxDlpOYbjnizxNCw=
+gorm.io/datatypes v1.1.1/go.mod h1:u8GEgFjJ+GpsGfgHmBUcQqHm/937t3sj/SO9dvbndTg=
+gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
+gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
+gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
+gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
+gorm.io/driver/sqlite v1.4.2/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
 gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
 gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
+gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
+gorm.io/gen v0.3.21 h1:t8329wT4tW1ZZWOm7vn4LV6OIrz8a5zCg+p78ezt+rA=
+gorm.io/gen v0.3.21/go.mod h1:aWgvoKdG9f8Des4TegSa0N5a+gwhGsFo0JJMaLwokvk=
+gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
-gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE=
-gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
+gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
+gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
+gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/hints v1.1.1 h1:NPampLxQujY+277452rt4yqtg6JmzNZ1jA2olk0eFXw=
+gorm.io/hints v1.1.1/go.mod h1:zdwzfFqvBWGbpuKiAhLFOSGSpeD3/VsRgkXR9Y7Z3cs=
+gorm.io/plugin/dbresolver v1.4.1 h1:Ug4LcoPhrvqq71UhxtF346f+skTYoCa/nEsdjvHwEzk=
+gorm.io/plugin/dbresolver v1.4.1/go.mod h1:CTbCtMWhsjXSiJqiW2R8POvJ2cq18RVOl4WGyT5nhNc=
 gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
 gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

+ 3 - 1
main.go

@@ -7,6 +7,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/server/model"
 	"github.com/0xJacky/Nginx-UI/server/pkg/cert"
 	"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+	"github.com/0xJacky/Nginx-UI/server/query"
 	"github.com/0xJacky/Nginx-UI/server/router"
 	"github.com/0xJacky/Nginx-UI/server/service"
 	"github.com/0xJacky/Nginx-UI/server/settings"
@@ -50,7 +51,8 @@ func prog(state overseer.State) {
 
 	log.Printf("Nginx config dir path: %s", nginx.GetConfPath())
 	if "" != settings.ServerSettings.JwtSecret {
-		model.Init()
+		db := model.Init()
+		query.Init(db)
 	}
 
 	s := gocron.NewScheduler(time.UTC)

+ 11 - 1
server/api/config.go

@@ -3,6 +3,7 @@ package api
 import (
 	"github.com/0xJacky/Nginx-UI/server/pkg/config_list"
 	"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+	"github.com/0xJacky/Nginx-UI/server/query"
 	"github.com/gin-gonic/gin"
 	"log"
 	"net/http"
@@ -84,8 +85,17 @@ func GetConfig(c *gin.Context) {
 		return
 	}
 
+	g := query.ChatGPTLog
+	chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
 	c.JSON(http.StatusOK, gin.H{
-		"config": string(content),
+		"config":           string(content),
+		"chatgpt_messages": chatgpt.Content,
 	})
 
 }

+ 385 - 375
server/api/domain.go

@@ -1,440 +1,450 @@
 package api
 
 import (
-    "github.com/0xJacky/Nginx-UI/server/model"
-    "github.com/0xJacky/Nginx-UI/server/pkg/cert"
-    "github.com/0xJacky/Nginx-UI/server/pkg/config_list"
-    "github.com/0xJacky/Nginx-UI/server/pkg/helper"
-    "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
-    "github.com/gin-gonic/gin"
-    "log"
-    "net/http"
-    "os"
-    "strings"
-    "time"
+	"github.com/0xJacky/Nginx-UI/server/model"
+	"github.com/0xJacky/Nginx-UI/server/pkg/cert"
+	"github.com/0xJacky/Nginx-UI/server/pkg/config_list"
+	"github.com/0xJacky/Nginx-UI/server/pkg/helper"
+	"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+	"github.com/0xJacky/Nginx-UI/server/query"
+	"github.com/gin-gonic/gin"
+	"log"
+	"net/http"
+	"os"
+	"strings"
+	"time"
 )
 
 func GetDomains(c *gin.Context) {
-    name := c.Query("name")
-    orderBy := c.Query("order_by")
-    sort := c.DefaultQuery("sort", "desc")
-
-    mySort := map[string]string{
-        "enabled": "bool",
-        "name":    "string",
-        "modify":  "time",
-    }
-
-    configFiles, err := os.ReadDir(nginx.GetConfPath("sites-available"))
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    enabledConfig, err := os.ReadDir(nginx.GetConfPath("sites-enabled"))
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    enabledConfigMap := make(map[string]bool)
-    for i := range enabledConfig {
-        enabledConfigMap[enabledConfig[i].Name()] = true
-    }
-
-    var configs []gin.H
-
-    for i := range configFiles {
-        file := configFiles[i]
-        fileInfo, _ := file.Info()
-        if !file.IsDir() {
-            if name != "" && !strings.Contains(file.Name(), name) {
-                continue
-            }
-            configs = append(configs, gin.H{
-                "name":    file.Name(),
-                "size":    fileInfo.Size(),
-                "modify":  fileInfo.ModTime(),
-                "enabled": enabledConfigMap[file.Name()],
-            })
-        }
-    }
-
-    configs = config_list.Sort(orderBy, sort, mySort[orderBy], configs)
-
-    c.JSON(http.StatusOK, gin.H{
-        "data": configs,
-    })
+	name := c.Query("name")
+	orderBy := c.Query("order_by")
+	sort := c.DefaultQuery("sort", "desc")
+
+	mySort := map[string]string{
+		"enabled": "bool",
+		"name":    "string",
+		"modify":  "time",
+	}
+
+	configFiles, err := os.ReadDir(nginx.GetConfPath("sites-available"))
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	enabledConfig, err := os.ReadDir(nginx.GetConfPath("sites-enabled"))
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	enabledConfigMap := make(map[string]bool)
+	for i := range enabledConfig {
+		enabledConfigMap[enabledConfig[i].Name()] = true
+	}
+
+	var configs []gin.H
+
+	for i := range configFiles {
+		file := configFiles[i]
+		fileInfo, _ := file.Info()
+		if !file.IsDir() {
+			if name != "" && !strings.Contains(file.Name(), name) {
+				continue
+			}
+			configs = append(configs, gin.H{
+				"name":    file.Name(),
+				"size":    fileInfo.Size(),
+				"modify":  fileInfo.ModTime(),
+				"enabled": enabledConfigMap[file.Name()],
+			})
+		}
+	}
+
+	configs = config_list.Sort(orderBy, sort, mySort[orderBy], configs)
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": configs,
+	})
 }
 
 type CertificateInfo struct {
-    SubjectName string    `json:"subject_name"`
-    IssuerName  string    `json:"issuer_name"`
-    NotAfter    time.Time `json:"not_after"`
-    NotBefore   time.Time `json:"not_before"`
+	SubjectName string    `json:"subject_name"`
+	IssuerName  string    `json:"issuer_name"`
+	NotAfter    time.Time `json:"not_after"`
+	NotBefore   time.Time `json:"not_before"`
 }
 
 func GetDomain(c *gin.Context) {
-    rewriteName, ok := c.Get("rewriteConfigFileName")
+	rewriteName, ok := c.Get("rewriteConfigFileName")
 
-    name := c.Param("name")
+	name := c.Param("name")
 
-    // for modify filename
-    if ok {
-        name = rewriteName.(string)
-    }
+	// for modify filename
+	if ok {
+		name = rewriteName.(string)
+	}
 
-    path := nginx.GetConfPath("sites-available", name)
+	path := nginx.GetConfPath("sites-available", name)
 
-    enabled := true
-    if _, err := os.Stat(nginx.GetConfPath("sites-enabled", name)); os.IsNotExist(err) {
-        enabled = false
-    }
+	enabled := true
+	if _, err := os.Stat(nginx.GetConfPath("sites-enabled", name)); os.IsNotExist(err) {
+		enabled = false
+	}
 
-    c.Set("maybe_error", "nginx_config_syntax_error")
-    config, err := nginx.ParseNgxConfig(path)
+	c.Set("maybe_error", "nginx_config_syntax_error")
+	config, err := nginx.ParseNgxConfig(path)
 
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
 
-    c.Set("maybe_error", "")
+	c.Set("maybe_error", "")
 
-    certInfoMap := make(map[int]CertificateInfo)
-    for serverIdx, server := range config.Servers {
-        for _, directive := range server.Directives {
-            if directive.Directive == "ssl_certificate" {
+	certInfoMap := make(map[int]CertificateInfo)
+	for serverIdx, server := range config.Servers {
+		for _, directive := range server.Directives {
+			if directive.Directive == "ssl_certificate" {
 
-                pubKey, err := cert.GetCertInfo(directive.Params)
+				pubKey, err := cert.GetCertInfo(directive.Params)
 
-                if err != nil {
-                    log.Println("Failed to get certificate information", err)
-                    break
-                }
+				if err != nil {
+					log.Println("Failed to get certificate information", err)
+					break
+				}
 
-                certInfoMap[serverIdx] = CertificateInfo{
-                    SubjectName: pubKey.Subject.CommonName,
-                    IssuerName:  pubKey.Issuer.CommonName,
-                    NotAfter:    pubKey.NotAfter,
-                    NotBefore:   pubKey.NotBefore,
-                }
+				certInfoMap[serverIdx] = CertificateInfo{
+					SubjectName: pubKey.Subject.CommonName,
+					IssuerName:  pubKey.Issuer.CommonName,
+					NotAfter:    pubKey.NotAfter,
+					NotBefore:   pubKey.NotBefore,
+				}
 
-                break
-            }
-        }
-    }
+				break
+			}
+		}
+	}
 
-    certModel, _ := model.FirstCert(name)
+	certModel, _ := model.FirstCert(name)
 
-    c.Set("maybe_error", "nginx_config_syntax_error")
+	g := query.ChatGPTLog
+	chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
 
-    c.JSON(http.StatusOK, gin.H{
-        "enabled":   enabled,
-        "name":      name,
-        "config":    config.FmtCode(),
-        "tokenized": config,
-        "auto_cert": certModel.AutoCert == model.AutoCertEnabled,
-        "cert_info": certInfoMap,
-    })
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	c.Set("maybe_error", "nginx_config_syntax_error")
+
+	c.JSON(http.StatusOK, gin.H{
+		"enabled":          enabled,
+		"name":             name,
+		"config":           config.FmtCode(),
+		"tokenized":        config,
+		"auto_cert":        certModel.AutoCert == model.AutoCertEnabled,
+		"cert_info":        certInfoMap,
+		"chatgpt_messages": chatgpt.Content,
+	})
 
 }
 
 func SaveDomain(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"`
-    }
-
-    if !BindAndValid(c, &json) {
-        return
-    }
-
-    path := nginx.GetConfPath("sites-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)
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-    enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
-    // rename the config file if needed
-    if name != json.Name {
-        newPath := nginx.GetConfPath("sites-available", json.Name)
-        // check if dst file exists, do not rename
-        if helper.FileExists(newPath) {
-            c.JSON(http.StatusNotAcceptable, gin.H{
-                "message": "File exists",
-            })
-            return
-        }
-        // recreate soft link
-        if helper.FileExists(enabledConfigFilePath) {
-            _ = os.Remove(enabledConfigFilePath)
-            enabledConfigFilePath = nginx.GetConfPath("sites-enabled", json.Name)
-            err = os.Symlink(newPath, enabledConfigFilePath)
-
-            if err != nil {
-                ErrHandler(c, err)
-                return
-            }
-        }
-
-        err = os.Rename(path, newPath)
-        if err != nil {
-            ErrHandler(c, err)
-            return
-        }
-
-        name = json.Name
-        c.Set("rewriteConfigFileName", name)
-    }
-
-    enabledConfigFilePath = nginx.GetConfPath("sites-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,
-                "error":   "nginx_config_syntax_error",
-            })
-            return
-        }
-
-        output = nginx.Reload()
-
-        if nginx.GetLogLevel(output) >= nginx.Warn {
-            c.JSON(http.StatusInternalServerError, gin.H{
-                "message": output,
-            })
-            return
-        }
-    }
-
-    GetDomain(c)
+	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"`
+	}
+
+	if !BindAndValid(c, &json) {
+		return
+	}
+
+	path := nginx.GetConfPath("sites-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)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
+	// rename the config file if needed
+	if name != json.Name {
+		newPath := nginx.GetConfPath("sites-available", json.Name)
+		// check if dst file exists, do not rename
+		if helper.FileExists(newPath) {
+			c.JSON(http.StatusNotAcceptable, gin.H{
+				"message": "File exists",
+			})
+			return
+		}
+		// recreate soft link
+		if helper.FileExists(enabledConfigFilePath) {
+			_ = os.Remove(enabledConfigFilePath)
+			enabledConfigFilePath = nginx.GetConfPath("sites-enabled", json.Name)
+			err = os.Symlink(newPath, enabledConfigFilePath)
+
+			if err != nil {
+				ErrHandler(c, err)
+				return
+			}
+		}
+
+		err = os.Rename(path, newPath)
+		if err != nil {
+			ErrHandler(c, err)
+			return
+		}
+
+		name = json.Name
+		c.Set("rewriteConfigFileName", name)
+	}
+
+	enabledConfigFilePath = nginx.GetConfPath("sites-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,
+				"error":   "nginx_config_syntax_error",
+			})
+			return
+		}
+
+		output = nginx.Reload()
+
+		if nginx.GetLogLevel(output) >= nginx.Warn {
+			c.JSON(http.StatusInternalServerError, gin.H{
+				"message": output,
+			})
+			return
+		}
+	}
+
+	GetDomain(c)
 }
 
 func EnableDomain(c *gin.Context) {
-    configFilePath := nginx.GetConfPath("sites-available", c.Param("name"))
-    enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
-
-    _, err := os.Stat(configFilePath)
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
-        err = os.Symlink(configFilePath, enabledConfigFilePath)
-
-        if err != nil {
-            ErrHandler(c, err)
-            return
-        }
-    }
-
-    // Test nginx config, if not pass then disable the site.
-    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",
-    })
+	configFilePath := nginx.GetConfPath("sites-available", c.Param("name"))
+	enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
+
+	_, err := os.Stat(configFilePath)
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
+		err = os.Symlink(configFilePath, enabledConfigFilePath)
+
+		if err != nil {
+			ErrHandler(c, err)
+			return
+		}
+	}
+
+	// Test nginx config, if not pass then disable the site.
+	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 DisableDomain(c *gin.Context) {
-    enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
-
-    _, err := os.Stat(enabledConfigFilePath)
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    err = os.Remove(enabledConfigFilePath)
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    // delete auto cert record
-    certModel := model.Cert{Filename: c.Param("name")}
-    err = certModel.Remove()
-    if err != nil {
-        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",
-    })
+	enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
+
+	_, err := os.Stat(enabledConfigFilePath)
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	err = os.Remove(enabledConfigFilePath)
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	// delete auto cert record
+	certModel := model.Cert{Filename: c.Param("name")}
+	err = certModel.Remove()
+	if err != nil {
+		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 DeleteDomain(c *gin.Context) {
-    var err error
-    name := c.Param("name")
-    availablePath := nginx.GetConfPath("sites-available", name)
-    enabledPath := nginx.GetConfPath("sites-enabled", name)
-
-    if _, err = os.Stat(availablePath); os.IsNotExist(err) {
-        c.JSON(http.StatusNotFound, gin.H{
-            "message": "site not found",
-        })
-        return
-    }
-
-    if _, err = os.Stat(enabledPath); err == nil {
-        c.JSON(http.StatusNotAcceptable, gin.H{
-            "message": "site is enabled",
-        })
-        return
-    }
-
-    certModel := model.Cert{Filename: name}
-    _ = certModel.Remove()
-
-    err = os.Remove(availablePath)
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    c.JSON(http.StatusOK, gin.H{
-        "message": "ok",
-    })
+	var err error
+	name := c.Param("name")
+	availablePath := nginx.GetConfPath("sites-available", name)
+	enabledPath := nginx.GetConfPath("sites-enabled", name)
+
+	if _, err = os.Stat(availablePath); os.IsNotExist(err) {
+		c.JSON(http.StatusNotFound, gin.H{
+			"message": "site not found",
+		})
+		return
+	}
+
+	if _, err = os.Stat(enabledPath); err == nil {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "site is enabled",
+		})
+		return
+	}
+
+	certModel := model.Cert{Filename: name}
+	_ = certModel.Remove()
+
+	err = os.Remove(availablePath)
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
 
 }
 
 func AddDomainToAutoCert(c *gin.Context) {
-    name := c.Param("name")
+	name := c.Param("name")
 
-    var json struct {
-        Domains []string `json:"domains"`
-    }
+	var json struct {
+		Domains []string `json:"domains"`
+	}
 
-    if !BindAndValid(c, &json) {
-        return
-    }
+	if !BindAndValid(c, &json) {
+		return
+	}
 
-    certModel, err := model.FirstOrCreateCert(name)
+	certModel, err := model.FirstOrCreateCert(name)
 
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
 
-    err = certModel.Updates(&model.Cert{
-        Name:     name,
-        Domains:  json.Domains,
-        AutoCert: model.AutoCertEnabled,
-    })
+	err = certModel.Updates(&model.Cert{
+		Name:     name,
+		Domains:  json.Domains,
+		AutoCert: model.AutoCertEnabled,
+	})
 
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
 
-    c.JSON(http.StatusOK, certModel)
+	c.JSON(http.StatusOK, certModel)
 }
 
 func RemoveDomainFromAutoCert(c *gin.Context) {
-    name := c.Param("name")
-    certModel, err := model.FirstCert(name)
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    err = certModel.Updates(&model.Cert{
-        AutoCert: model.AutoCertDisabled,
-    })
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-    c.JSON(http.StatusOK, nil)
+	name := c.Param("name")
+	certModel, err := model.FirstCert(name)
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	err = certModel.Updates(&model.Cert{
+		AutoCert: model.AutoCertDisabled,
+	})
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, nil)
 }
 
 func DuplicateSite(c *gin.Context) {
-    name := c.Param("name")
+	name := c.Param("name")
 
-    var json struct {
-        Name string `json:"name" binding:"required"`
-    }
+	var json struct {
+		Name string `json:"name" binding:"required"`
+	}
 
-    if !BindAndValid(c, &json) {
-        return
-    }
+	if !BindAndValid(c, &json) {
+		return
+	}
 
-    src := nginx.GetConfPath("sites-available", name)
-    dst := nginx.GetConfPath("sites-available", json.Name)
+	src := nginx.GetConfPath("sites-available", name)
+	dst := nginx.GetConfPath("sites-available", json.Name)
 
-    if helper.FileExists(dst) {
-        c.JSON(http.StatusNotAcceptable, gin.H{
-            "message": "File exists",
-        })
-        return
-    }
+	if helper.FileExists(dst) {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "File exists",
+		})
+		return
+	}
 
-    _, err := helper.CopyFile(src, dst)
+	_, err := helper.CopyFile(src, dst)
 
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
 
-    c.JSON(http.StatusOK, gin.H{
-        "dst": dst,
-    })
+	c.JSON(http.StatusOK, gin.H{
+		"dst": dst,
+	})
 }

+ 3 - 2
server/api/install.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"github.com/0xJacky/Nginx-UI/server/model"
+	"github.com/0xJacky/Nginx-UI/server/query"
 	"github.com/0xJacky/Nginx-UI/server/settings"
 	"github.com/gin-gonic/gin"
 	"github.com/google/uuid"
@@ -54,7 +55,8 @@ func InstallNginxUI(c *gin.Context) {
 	}
 
 	// Init model
-	model.Init()
+	db := model.Init()
+	query.Init(db)
 
 	curd := model.NewCurd(&model.Auth{})
 	pwd, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
@@ -70,5 +72,4 @@ func InstallNginxUI(c *gin.Context) {
 	c.JSON(http.StatusOK, gin.H{
 		"message": "ok",
 	})
-
 }

+ 160 - 0
server/api/openai.go

@@ -0,0 +1,160 @@
+package api
+
+import (
+    "context"
+    "fmt"
+    "github.com/0xJacky/Nginx-UI/server/model"
+    "github.com/0xJacky/Nginx-UI/server/query"
+    "github.com/0xJacky/Nginx-UI/server/settings"
+    "github.com/gin-gonic/gin"
+    "github.com/pkg/errors"
+    "github.com/sashabaranov/go-openai"
+    "io"
+    "log"
+    "net/http"
+    "net/url"
+    "os"
+)
+
+const ChatGPTInitPrompt = "You are a assistant who can help users write and optimise the configurations of Nginx, the first user message contains the content of the configuration file which is currently opened by the user and the current language code(CLC). You suppose to use the language corresponding to the CLC to give the first reply. Later the language environment depends on the user message. The first reply should involve the key information of the file and ask user what can you help them."
+
+func MakeChatCompletionRequest(c *gin.Context) {
+    var json struct {
+        Messages []openai.ChatCompletionMessage `json:"messages"`
+    }
+
+    if !BindAndValid(c, &json) {
+        return
+    }
+
+    messages := []openai.ChatCompletionMessage{
+        {
+            Role:    openai.ChatMessageRoleSystem,
+            Content: ChatGPTInitPrompt,
+        },
+    }
+    messages = append(messages, json.Messages...)
+    // sse server
+    c.Writer.Header().Set("Content-Type", "text/event-stream")
+    c.Writer.Header().Set("Cache-Control", "no-cache")
+    c.Writer.Header().Set("Connection", "keep-alive")
+    c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+    log.Println(settings.OpenAISettings.Token)
+
+    config := openai.DefaultConfig(settings.OpenAISettings.Token)
+
+    if settings.OpenAISettings.Proxy != "" {
+        proxyUrl, err := url.Parse(settings.OpenAISettings.Proxy)
+        if err != nil {
+            c.Stream(func(w io.Writer) bool {
+                c.SSEvent("message", gin.H{
+                    "type":    "error",
+                    "content": err.Error(),
+                })
+                return false
+            })
+            return
+        }
+        transport := &http.Transport{
+            Proxy: http.ProxyURL(proxyUrl),
+        }
+        config.HTTPClient = &http.Client{
+            Transport: transport,
+        }
+    }
+
+    if settings.OpenAISettings.BaseUrl != "" {
+        config.BaseURL = settings.OpenAISettings.BaseUrl
+    }
+
+    openaiClient := openai.NewClientWithConfig(config)
+    ctx := context.Background()
+
+    req := openai.ChatCompletionRequest{
+        Model:    openai.GPT3Dot5Turbo0301,
+        Messages: messages,
+        Stream:   true,
+    }
+    stream, err := openaiClient.CreateChatCompletionStream(ctx, req)
+    if err != nil {
+        fmt.Printf("CompletionStream error: %v\n", err)
+        c.Stream(func(w io.Writer) bool {
+            c.SSEvent("message", gin.H{
+                "type":    "error",
+                "content": err.Error(),
+            })
+            return false
+        })
+        return
+    }
+    defer stream.Close()
+    msgChan := make(chan string)
+    go func() {
+        for {
+            response, err := stream.Recv()
+            if errors.Is(err, io.EOF) {
+                close(msgChan)
+                fmt.Println()
+                return
+            }
+
+            if err != nil {
+                fmt.Printf("Stream error: %v\n", err)
+                close(msgChan)
+                return
+            }
+
+            // Send SSE to client
+            message := fmt.Sprintf("%s", response.Choices[0].Delta.Content)
+            fmt.Printf("%s", response.Choices[0].Delta.Content)
+            _ = os.Stdout.Sync()
+
+            msgChan <- message
+        }
+    }()
+
+    c.Stream(func(w io.Writer) bool {
+        if m, ok := <-msgChan; ok {
+            c.SSEvent("message", gin.H{
+                "type":    "message",
+                "content": m,
+            })
+            return true
+        }
+        return false
+    })
+}
+
+func StoreChatGPTRecord(c *gin.Context) {
+    var json struct {
+        FileName string                         `json:"file_name"`
+        Messages []openai.ChatCompletionMessage `json:"messages"`
+    }
+
+    if !BindAndValid(c, &json) {
+        return
+    }
+
+    name := json.FileName
+    g := query.ChatGPTLog
+    _, err := g.Where(g.Name.Eq(name)).FirstOrCreate()
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    _, err = g.Where(g.Name.Eq(name)).Updates(&model.ChatGPTLog{
+        Name:    name,
+        Content: json.Messages,
+    })
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    c.JSON(http.StatusOK, gin.H{
+        "message": "ok",
+    })
+}

+ 67 - 0
server/cmd/generate/generate.go

@@ -0,0 +1,67 @@
+package main
+
+import (
+    "flag"
+    "fmt"
+    "github.com/0xJacky/Nginx-UI/server/model"
+    "github.com/0xJacky/Nginx-UI/server/settings"
+    "gorm.io/driver/sqlite"
+    "gorm.io/gen"
+    "gorm.io/gorm"
+    "gorm.io/gorm/logger"
+    "log"
+    "path"
+)
+
+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",
+        Mode:    gen.WithoutContext | gen.WithDefaultQuery,
+        //if you want the nullable field generation property to be pointer type, set FieldNullable true
+        FieldNullable: true,
+        //if you want to assign field which has default value in `Create` API, set FieldCoverable true, reference: https://gorm.io/docs/create.html#Default-Values
+        FieldCoverable: true,
+        // if you want to generate field with unsigned integer type, set FieldSignable true
+        /* FieldSignable: true,*/
+        //if you want to generate index tags from database, set FieldWithIndexTag true
+        /* FieldWithIndexTag: true,*/
+        //if you want to generate type tags from database, set FieldWithTypeTag true
+        /* FieldWithTypeTag: true,*/
+        //if you need unit tests for query code, set WithUnitTest true
+        /* WithUnitTest: true, */
+    })
+
+    // reuse the database connection in Project or create a connection here
+    // if you want to use GenerateModel/GenerateModelAs, UseDB is necessary or it will panic
+    var confPath string
+    flag.StringVar(&confPath, "config", "app.ini", "Specify the configuration file")
+    flag.Parse()
+
+    settings.Init(confPath)
+    dbPath := path.Join(path.Dir(settings.ConfPath), fmt.Sprintf("%s.db", settings.ServerSettings.Database))
+
+    var err error
+    db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
+        Logger:                                   logger.Default.LogMode(logger.Info),
+        PrepareStmt:                              true,
+        DisableForeignKeyConstraintWhenMigrating: true,
+    })
+
+    if err != nil {
+        log.Fatalln(err)
+    }
+
+    g.UseDB(db)
+
+    // apply basic crud api on structs or table models which is specified by table name with function
+    // GenerateModel/GenerateModelAs. And generator will generate table models' code when calling Excute.
+    g.ApplyBasic(model.GenerateAllModel()...)
+
+    // apply diy interfaces on structs or table models
+    g.ApplyInterface(func(method model.Method) {}, model.GenerateAllModel()...)
+
+    // execute the action of code generation
+    g.Execute()
+}

+ 34 - 0
server/model/chatgpt_log.go

@@ -0,0 +1,34 @@
+package model
+
+import (
+	"database/sql/driver"
+	"encoding/json"
+	"fmt"
+	"github.com/pkg/errors"
+	"github.com/sashabaranov/go-openai"
+)
+
+type JSON []openai.ChatCompletionMessage
+
+// Scan scan value into Jsonb, implements sql.Scanner interface
+func (j *JSON) Scan(value interface{}) error {
+	bytes, ok := value.([]byte)
+	if !ok {
+		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
+	}
+
+	var result []openai.ChatCompletionMessage
+	err := json.Unmarshal(bytes, &result)
+	*j = result
+	return err
+}
+
+// Value return json value, implement driver.Valuer interface
+func (j *JSON) Value() (driver.Value, error) {
+	return json.Marshal(*j)
+}
+
+type ChatGPTLog struct {
+	Name    string `json:"name"`
+	Content JSON   `json:"content" gorm:"serializer:json"`
+}

+ 28 - 11
server/model/model.go

@@ -6,6 +6,7 @@ import (
 	"github.com/gin-gonic/gin"
 	"github.com/spf13/cast"
 	"gorm.io/driver/sqlite"
+	"gorm.io/gen"
 	"gorm.io/gorm"
 	"gorm.io/gorm/logger"
 	"log"
@@ -22,28 +23,37 @@ type Model struct {
 	DeletedAt *time.Time `gorm:"index" json:"deleted_at"`
 }
 
-func Init() {
+func GenerateAllModel() []any {
+	return []any{
+		ConfigBackup{},
+		Auth{},
+		AuthToken{},
+		Cert{},
+		ChatGPTLog{},
+	}
+}
+
+func Init() *gorm.DB {
 	dbPath := path.Join(path.Dir(settings.ConfPath), fmt.Sprintf("%s.db", settings.ServerSettings.Database))
+
 	var err error
 	db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
-		Logger:      logger.Default.LogMode(logger.Info),
-		PrepareStmt: true,
+		Logger:                                   logger.Default.LogMode(logger.Info),
+		PrepareStmt:                              true,
+		DisableForeignKeyConstraintWhenMigrating: true,
 	})
+
 	if err != nil {
 		log.Println(err)
 	}
-	// Migrate the schema
-	AutoMigrate(&ConfigBackup{})
-	AutoMigrate(&Auth{})
-	AutoMigrate(&AuthToken{})
-	AutoMigrate(&Cert{})
-}
 
-func AutoMigrate(model interface{}) {
-	err := db.AutoMigrate(model)
+	// Migrate the schema
+	err = db.AutoMigrate(GenerateAllModel()...)
 	if err != nil {
 		log.Fatal(err)
 	}
+
+	return db
 }
 
 func orderAndPaginate(c *gin.Context) func(db *gorm.DB) *gorm.DB {
@@ -114,3 +124,10 @@ func GetListWithPagination(models interface{},
 
 	return
 }
+
+type Method interface {
+	// FirstByID Where("id=@id")
+	FirstByID(id int) (*gen.T, error)
+	// DeleteByID update @@table set deleted_at=NOW() where id=@id
+	DeleteByID(id int) error
+}

+ 354 - 0
server/query/auth_tokens.gen.go

@@ -0,0 +1,354 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"strings"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/0xJacky/Nginx-UI/server/model"
+)
+
+func newAuthToken(db *gorm.DB, opts ...gen.DOOption) authToken {
+	_authToken := authToken{}
+
+	_authToken.authTokenDo.UseDB(db, opts...)
+	_authToken.authTokenDo.UseModel(&model.AuthToken{})
+
+	tableName := _authToken.authTokenDo.TableName()
+	_authToken.ALL = field.NewAsterisk(tableName)
+	_authToken.Token = field.NewString(tableName, "token")
+
+	_authToken.fillFieldMap()
+
+	return _authToken
+}
+
+type authToken struct {
+	authTokenDo
+
+	ALL   field.Asterisk
+	Token field.String
+
+	fieldMap map[string]field.Expr
+}
+
+func (a authToken) Table(newTableName string) *authToken {
+	a.authTokenDo.UseTable(newTableName)
+	return a.updateTableName(newTableName)
+}
+
+func (a authToken) As(alias string) *authToken {
+	a.authTokenDo.DO = *(a.authTokenDo.As(alias).(*gen.DO))
+	return a.updateTableName(alias)
+}
+
+func (a *authToken) updateTableName(table string) *authToken {
+	a.ALL = field.NewAsterisk(table)
+	a.Token = field.NewString(table, "token")
+
+	a.fillFieldMap()
+
+	return a
+}
+
+func (a *authToken) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := a.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (a *authToken) fillFieldMap() {
+	a.fieldMap = make(map[string]field.Expr, 1)
+	a.fieldMap["token"] = a.Token
+}
+
+func (a authToken) clone(db *gorm.DB) authToken {
+	a.authTokenDo.ReplaceConnPool(db.Statement.ConnPool)
+	return a
+}
+
+func (a authToken) replaceDB(db *gorm.DB) authToken {
+	a.authTokenDo.ReplaceDB(db)
+	return a
+}
+
+type authTokenDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (a authTokenDo) FirstByID(id int) (result *model.AuthToken, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = a.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+// DeleteByID update @@table set deleted_at=NOW() where id=@id
+func (a authTokenDo) DeleteByID(id int) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update auth_tokens set deleted_at=NOW() where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = a.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (a authTokenDo) Debug() *authTokenDo {
+	return a.withDO(a.DO.Debug())
+}
+
+func (a authTokenDo) WithContext(ctx context.Context) *authTokenDo {
+	return a.withDO(a.DO.WithContext(ctx))
+}
+
+func (a authTokenDo) ReadDB() *authTokenDo {
+	return a.Clauses(dbresolver.Read)
+}
+
+func (a authTokenDo) WriteDB() *authTokenDo {
+	return a.Clauses(dbresolver.Write)
+}
+
+func (a authTokenDo) Session(config *gorm.Session) *authTokenDo {
+	return a.withDO(a.DO.Session(config))
+}
+
+func (a authTokenDo) Clauses(conds ...clause.Expression) *authTokenDo {
+	return a.withDO(a.DO.Clauses(conds...))
+}
+
+func (a authTokenDo) Returning(value interface{}, columns ...string) *authTokenDo {
+	return a.withDO(a.DO.Returning(value, columns...))
+}
+
+func (a authTokenDo) Not(conds ...gen.Condition) *authTokenDo {
+	return a.withDO(a.DO.Not(conds...))
+}
+
+func (a authTokenDo) Or(conds ...gen.Condition) *authTokenDo {
+	return a.withDO(a.DO.Or(conds...))
+}
+
+func (a authTokenDo) Select(conds ...field.Expr) *authTokenDo {
+	return a.withDO(a.DO.Select(conds...))
+}
+
+func (a authTokenDo) Where(conds ...gen.Condition) *authTokenDo {
+	return a.withDO(a.DO.Where(conds...))
+}
+
+func (a authTokenDo) Exists(subquery interface{ UnderlyingDB() *gorm.DB }) *authTokenDo {
+	return a.Where(field.CompareSubQuery(field.ExistsOp, nil, subquery.UnderlyingDB()))
+}
+
+func (a authTokenDo) Order(conds ...field.Expr) *authTokenDo {
+	return a.withDO(a.DO.Order(conds...))
+}
+
+func (a authTokenDo) Distinct(cols ...field.Expr) *authTokenDo {
+	return a.withDO(a.DO.Distinct(cols...))
+}
+
+func (a authTokenDo) Omit(cols ...field.Expr) *authTokenDo {
+	return a.withDO(a.DO.Omit(cols...))
+}
+
+func (a authTokenDo) Join(table schema.Tabler, on ...field.Expr) *authTokenDo {
+	return a.withDO(a.DO.Join(table, on...))
+}
+
+func (a authTokenDo) LeftJoin(table schema.Tabler, on ...field.Expr) *authTokenDo {
+	return a.withDO(a.DO.LeftJoin(table, on...))
+}
+
+func (a authTokenDo) RightJoin(table schema.Tabler, on ...field.Expr) *authTokenDo {
+	return a.withDO(a.DO.RightJoin(table, on...))
+}
+
+func (a authTokenDo) Group(cols ...field.Expr) *authTokenDo {
+	return a.withDO(a.DO.Group(cols...))
+}
+
+func (a authTokenDo) Having(conds ...gen.Condition) *authTokenDo {
+	return a.withDO(a.DO.Having(conds...))
+}
+
+func (a authTokenDo) Limit(limit int) *authTokenDo {
+	return a.withDO(a.DO.Limit(limit))
+}
+
+func (a authTokenDo) Offset(offset int) *authTokenDo {
+	return a.withDO(a.DO.Offset(offset))
+}
+
+func (a authTokenDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *authTokenDo {
+	return a.withDO(a.DO.Scopes(funcs...))
+}
+
+func (a authTokenDo) Unscoped() *authTokenDo {
+	return a.withDO(a.DO.Unscoped())
+}
+
+func (a authTokenDo) Create(values ...*model.AuthToken) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return a.DO.Create(values)
+}
+
+func (a authTokenDo) CreateInBatches(values []*model.AuthToken, batchSize int) error {
+	return a.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (a authTokenDo) Save(values ...*model.AuthToken) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return a.DO.Save(values)
+}
+
+func (a authTokenDo) First() (*model.AuthToken, error) {
+	if result, err := a.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.AuthToken), nil
+	}
+}
+
+func (a authTokenDo) Take() (*model.AuthToken, error) {
+	if result, err := a.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.AuthToken), nil
+	}
+}
+
+func (a authTokenDo) Last() (*model.AuthToken, error) {
+	if result, err := a.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.AuthToken), nil
+	}
+}
+
+func (a authTokenDo) Find() ([]*model.AuthToken, error) {
+	result, err := a.DO.Find()
+	return result.([]*model.AuthToken), err
+}
+
+func (a authTokenDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.AuthToken, err error) {
+	buf := make([]*model.AuthToken, 0, batchSize)
+	err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (a authTokenDo) FindInBatches(result *[]*model.AuthToken, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return a.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (a authTokenDo) Attrs(attrs ...field.AssignExpr) *authTokenDo {
+	return a.withDO(a.DO.Attrs(attrs...))
+}
+
+func (a authTokenDo) Assign(attrs ...field.AssignExpr) *authTokenDo {
+	return a.withDO(a.DO.Assign(attrs...))
+}
+
+func (a authTokenDo) Joins(fields ...field.RelationField) *authTokenDo {
+	for _, _f := range fields {
+		a = *a.withDO(a.DO.Joins(_f))
+	}
+	return &a
+}
+
+func (a authTokenDo) Preload(fields ...field.RelationField) *authTokenDo {
+	for _, _f := range fields {
+		a = *a.withDO(a.DO.Preload(_f))
+	}
+	return &a
+}
+
+func (a authTokenDo) FirstOrInit() (*model.AuthToken, error) {
+	if result, err := a.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.AuthToken), nil
+	}
+}
+
+func (a authTokenDo) FirstOrCreate() (*model.AuthToken, error) {
+	if result, err := a.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.AuthToken), nil
+	}
+}
+
+func (a authTokenDo) FindByPage(offset int, limit int) (result []*model.AuthToken, count int64, err error) {
+	result, err = a.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = a.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (a authTokenDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = a.Count()
+	if err != nil {
+		return
+	}
+
+	err = a.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (a authTokenDo) Scan(result interface{}) (err error) {
+	return a.DO.Scan(result)
+}
+
+func (a authTokenDo) Delete(models ...*model.AuthToken) (result gen.ResultInfo, err error) {
+	return a.DO.Delete(models)
+}
+
+func (a *authTokenDo) withDO(do gen.Dao) *authTokenDo {
+	a.DO = *do.(*gen.DO)
+	return a
+}

+ 374 - 0
server/query/auths.gen.go

@@ -0,0 +1,374 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"strings"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/0xJacky/Nginx-UI/server/model"
+)
+
+func newAuth(db *gorm.DB, opts ...gen.DOOption) auth {
+	_auth := auth{}
+
+	_auth.authDo.UseDB(db, opts...)
+	_auth.authDo.UseModel(&model.Auth{})
+
+	tableName := _auth.authDo.TableName()
+	_auth.ALL = field.NewAsterisk(tableName)
+	_auth.ID = field.NewUint(tableName, "id")
+	_auth.CreatedAt = field.NewTime(tableName, "created_at")
+	_auth.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_auth.DeletedAt = field.NewTime(tableName, "deleted_at")
+	_auth.Name = field.NewString(tableName, "name")
+	_auth.Password = field.NewString(tableName, "password")
+
+	_auth.fillFieldMap()
+
+	return _auth
+}
+
+type auth struct {
+	authDo
+
+	ALL       field.Asterisk
+	ID        field.Uint
+	CreatedAt field.Time
+	UpdatedAt field.Time
+	DeletedAt field.Time
+	Name      field.String
+	Password  field.String
+
+	fieldMap map[string]field.Expr
+}
+
+func (a auth) Table(newTableName string) *auth {
+	a.authDo.UseTable(newTableName)
+	return a.updateTableName(newTableName)
+}
+
+func (a auth) As(alias string) *auth {
+	a.authDo.DO = *(a.authDo.As(alias).(*gen.DO))
+	return a.updateTableName(alias)
+}
+
+func (a *auth) updateTableName(table string) *auth {
+	a.ALL = field.NewAsterisk(table)
+	a.ID = field.NewUint(table, "id")
+	a.CreatedAt = field.NewTime(table, "created_at")
+	a.UpdatedAt = field.NewTime(table, "updated_at")
+	a.DeletedAt = field.NewTime(table, "deleted_at")
+	a.Name = field.NewString(table, "name")
+	a.Password = field.NewString(table, "password")
+
+	a.fillFieldMap()
+
+	return a
+}
+
+func (a *auth) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := a.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (a *auth) fillFieldMap() {
+	a.fieldMap = make(map[string]field.Expr, 6)
+	a.fieldMap["id"] = a.ID
+	a.fieldMap["created_at"] = a.CreatedAt
+	a.fieldMap["updated_at"] = a.UpdatedAt
+	a.fieldMap["deleted_at"] = a.DeletedAt
+	a.fieldMap["name"] = a.Name
+	a.fieldMap["password"] = a.Password
+}
+
+func (a auth) clone(db *gorm.DB) auth {
+	a.authDo.ReplaceConnPool(db.Statement.ConnPool)
+	return a
+}
+
+func (a auth) replaceDB(db *gorm.DB) auth {
+	a.authDo.ReplaceDB(db)
+	return a
+}
+
+type authDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (a authDo) FirstByID(id int) (result *model.Auth, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = a.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+// DeleteByID update @@table set deleted_at=NOW() where id=@id
+func (a authDo) DeleteByID(id int) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update auths set deleted_at=NOW() where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = a.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (a authDo) Debug() *authDo {
+	return a.withDO(a.DO.Debug())
+}
+
+func (a authDo) WithContext(ctx context.Context) *authDo {
+	return a.withDO(a.DO.WithContext(ctx))
+}
+
+func (a authDo) ReadDB() *authDo {
+	return a.Clauses(dbresolver.Read)
+}
+
+func (a authDo) WriteDB() *authDo {
+	return a.Clauses(dbresolver.Write)
+}
+
+func (a authDo) Session(config *gorm.Session) *authDo {
+	return a.withDO(a.DO.Session(config))
+}
+
+func (a authDo) Clauses(conds ...clause.Expression) *authDo {
+	return a.withDO(a.DO.Clauses(conds...))
+}
+
+func (a authDo) Returning(value interface{}, columns ...string) *authDo {
+	return a.withDO(a.DO.Returning(value, columns...))
+}
+
+func (a authDo) Not(conds ...gen.Condition) *authDo {
+	return a.withDO(a.DO.Not(conds...))
+}
+
+func (a authDo) Or(conds ...gen.Condition) *authDo {
+	return a.withDO(a.DO.Or(conds...))
+}
+
+func (a authDo) Select(conds ...field.Expr) *authDo {
+	return a.withDO(a.DO.Select(conds...))
+}
+
+func (a authDo) Where(conds ...gen.Condition) *authDo {
+	return a.withDO(a.DO.Where(conds...))
+}
+
+func (a authDo) Exists(subquery interface{ UnderlyingDB() *gorm.DB }) *authDo {
+	return a.Where(field.CompareSubQuery(field.ExistsOp, nil, subquery.UnderlyingDB()))
+}
+
+func (a authDo) Order(conds ...field.Expr) *authDo {
+	return a.withDO(a.DO.Order(conds...))
+}
+
+func (a authDo) Distinct(cols ...field.Expr) *authDo {
+	return a.withDO(a.DO.Distinct(cols...))
+}
+
+func (a authDo) Omit(cols ...field.Expr) *authDo {
+	return a.withDO(a.DO.Omit(cols...))
+}
+
+func (a authDo) Join(table schema.Tabler, on ...field.Expr) *authDo {
+	return a.withDO(a.DO.Join(table, on...))
+}
+
+func (a authDo) LeftJoin(table schema.Tabler, on ...field.Expr) *authDo {
+	return a.withDO(a.DO.LeftJoin(table, on...))
+}
+
+func (a authDo) RightJoin(table schema.Tabler, on ...field.Expr) *authDo {
+	return a.withDO(a.DO.RightJoin(table, on...))
+}
+
+func (a authDo) Group(cols ...field.Expr) *authDo {
+	return a.withDO(a.DO.Group(cols...))
+}
+
+func (a authDo) Having(conds ...gen.Condition) *authDo {
+	return a.withDO(a.DO.Having(conds...))
+}
+
+func (a authDo) Limit(limit int) *authDo {
+	return a.withDO(a.DO.Limit(limit))
+}
+
+func (a authDo) Offset(offset int) *authDo {
+	return a.withDO(a.DO.Offset(offset))
+}
+
+func (a authDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *authDo {
+	return a.withDO(a.DO.Scopes(funcs...))
+}
+
+func (a authDo) Unscoped() *authDo {
+	return a.withDO(a.DO.Unscoped())
+}
+
+func (a authDo) Create(values ...*model.Auth) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return a.DO.Create(values)
+}
+
+func (a authDo) CreateInBatches(values []*model.Auth, batchSize int) error {
+	return a.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (a authDo) Save(values ...*model.Auth) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return a.DO.Save(values)
+}
+
+func (a authDo) First() (*model.Auth, error) {
+	if result, err := a.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Auth), nil
+	}
+}
+
+func (a authDo) Take() (*model.Auth, error) {
+	if result, err := a.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Auth), nil
+	}
+}
+
+func (a authDo) Last() (*model.Auth, error) {
+	if result, err := a.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Auth), nil
+	}
+}
+
+func (a authDo) Find() ([]*model.Auth, error) {
+	result, err := a.DO.Find()
+	return result.([]*model.Auth), err
+}
+
+func (a authDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Auth, err error) {
+	buf := make([]*model.Auth, 0, batchSize)
+	err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (a authDo) FindInBatches(result *[]*model.Auth, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return a.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (a authDo) Attrs(attrs ...field.AssignExpr) *authDo {
+	return a.withDO(a.DO.Attrs(attrs...))
+}
+
+func (a authDo) Assign(attrs ...field.AssignExpr) *authDo {
+	return a.withDO(a.DO.Assign(attrs...))
+}
+
+func (a authDo) Joins(fields ...field.RelationField) *authDo {
+	for _, _f := range fields {
+		a = *a.withDO(a.DO.Joins(_f))
+	}
+	return &a
+}
+
+func (a authDo) Preload(fields ...field.RelationField) *authDo {
+	for _, _f := range fields {
+		a = *a.withDO(a.DO.Preload(_f))
+	}
+	return &a
+}
+
+func (a authDo) FirstOrInit() (*model.Auth, error) {
+	if result, err := a.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Auth), nil
+	}
+}
+
+func (a authDo) FirstOrCreate() (*model.Auth, error) {
+	if result, err := a.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Auth), nil
+	}
+}
+
+func (a authDo) FindByPage(offset int, limit int) (result []*model.Auth, count int64, err error) {
+	result, err = a.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = a.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (a authDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = a.Count()
+	if err != nil {
+		return
+	}
+
+	err = a.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (a authDo) Scan(result interface{}) (err error) {
+	return a.DO.Scan(result)
+}
+
+func (a authDo) Delete(models ...*model.Auth) (result gen.ResultInfo, err error) {
+	return a.DO.Delete(models)
+}
+
+func (a *authDo) withDO(do gen.Dao) *authDo {
+	a.DO = *do.(*gen.DO)
+	return a
+}

+ 394 - 0
server/query/certs.gen.go

@@ -0,0 +1,394 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"strings"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/0xJacky/Nginx-UI/server/model"
+)
+
+func newCert(db *gorm.DB, opts ...gen.DOOption) cert {
+	_cert := cert{}
+
+	_cert.certDo.UseDB(db, opts...)
+	_cert.certDo.UseModel(&model.Cert{})
+
+	tableName := _cert.certDo.TableName()
+	_cert.ALL = field.NewAsterisk(tableName)
+	_cert.ID = field.NewUint(tableName, "id")
+	_cert.CreatedAt = field.NewTime(tableName, "created_at")
+	_cert.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_cert.DeletedAt = field.NewTime(tableName, "deleted_at")
+	_cert.Name = field.NewString(tableName, "name")
+	_cert.Domains = field.NewField(tableName, "domains")
+	_cert.Filename = field.NewString(tableName, "filename")
+	_cert.SSLCertificatePath = field.NewString(tableName, "ssl_certificate_path")
+	_cert.SSLCertificateKeyPath = field.NewString(tableName, "ssl_certificate_key_path")
+	_cert.AutoCert = field.NewInt(tableName, "auto_cert")
+	_cert.Log = field.NewString(tableName, "log")
+
+	_cert.fillFieldMap()
+
+	return _cert
+}
+
+type cert struct {
+	certDo
+
+	ALL                   field.Asterisk
+	ID                    field.Uint
+	CreatedAt             field.Time
+	UpdatedAt             field.Time
+	DeletedAt             field.Time
+	Name                  field.String
+	Domains               field.Field
+	Filename              field.String
+	SSLCertificatePath    field.String
+	SSLCertificateKeyPath field.String
+	AutoCert              field.Int
+	Log                   field.String
+
+	fieldMap map[string]field.Expr
+}
+
+func (c cert) Table(newTableName string) *cert {
+	c.certDo.UseTable(newTableName)
+	return c.updateTableName(newTableName)
+}
+
+func (c cert) As(alias string) *cert {
+	c.certDo.DO = *(c.certDo.As(alias).(*gen.DO))
+	return c.updateTableName(alias)
+}
+
+func (c *cert) updateTableName(table string) *cert {
+	c.ALL = field.NewAsterisk(table)
+	c.ID = field.NewUint(table, "id")
+	c.CreatedAt = field.NewTime(table, "created_at")
+	c.UpdatedAt = field.NewTime(table, "updated_at")
+	c.DeletedAt = field.NewTime(table, "deleted_at")
+	c.Name = field.NewString(table, "name")
+	c.Domains = field.NewField(table, "domains")
+	c.Filename = field.NewString(table, "filename")
+	c.SSLCertificatePath = field.NewString(table, "ssl_certificate_path")
+	c.SSLCertificateKeyPath = field.NewString(table, "ssl_certificate_key_path")
+	c.AutoCert = field.NewInt(table, "auto_cert")
+	c.Log = field.NewString(table, "log")
+
+	c.fillFieldMap()
+
+	return c
+}
+
+func (c *cert) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := c.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (c *cert) fillFieldMap() {
+	c.fieldMap = make(map[string]field.Expr, 11)
+	c.fieldMap["id"] = c.ID
+	c.fieldMap["created_at"] = c.CreatedAt
+	c.fieldMap["updated_at"] = c.UpdatedAt
+	c.fieldMap["deleted_at"] = c.DeletedAt
+	c.fieldMap["name"] = c.Name
+	c.fieldMap["domains"] = c.Domains
+	c.fieldMap["filename"] = c.Filename
+	c.fieldMap["ssl_certificate_path"] = c.SSLCertificatePath
+	c.fieldMap["ssl_certificate_key_path"] = c.SSLCertificateKeyPath
+	c.fieldMap["auto_cert"] = c.AutoCert
+	c.fieldMap["log"] = c.Log
+}
+
+func (c cert) clone(db *gorm.DB) cert {
+	c.certDo.ReplaceConnPool(db.Statement.ConnPool)
+	return c
+}
+
+func (c cert) replaceDB(db *gorm.DB) cert {
+	c.certDo.ReplaceDB(db)
+	return c
+}
+
+type certDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (c certDo) FirstByID(id int) (result *model.Cert, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = c.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+// DeleteByID update @@table set deleted_at=NOW() where id=@id
+func (c certDo) DeleteByID(id int) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update certs set deleted_at=NOW() where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = c.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (c certDo) Debug() *certDo {
+	return c.withDO(c.DO.Debug())
+}
+
+func (c certDo) WithContext(ctx context.Context) *certDo {
+	return c.withDO(c.DO.WithContext(ctx))
+}
+
+func (c certDo) ReadDB() *certDo {
+	return c.Clauses(dbresolver.Read)
+}
+
+func (c certDo) WriteDB() *certDo {
+	return c.Clauses(dbresolver.Write)
+}
+
+func (c certDo) Session(config *gorm.Session) *certDo {
+	return c.withDO(c.DO.Session(config))
+}
+
+func (c certDo) Clauses(conds ...clause.Expression) *certDo {
+	return c.withDO(c.DO.Clauses(conds...))
+}
+
+func (c certDo) Returning(value interface{}, columns ...string) *certDo {
+	return c.withDO(c.DO.Returning(value, columns...))
+}
+
+func (c certDo) Not(conds ...gen.Condition) *certDo {
+	return c.withDO(c.DO.Not(conds...))
+}
+
+func (c certDo) Or(conds ...gen.Condition) *certDo {
+	return c.withDO(c.DO.Or(conds...))
+}
+
+func (c certDo) Select(conds ...field.Expr) *certDo {
+	return c.withDO(c.DO.Select(conds...))
+}
+
+func (c certDo) Where(conds ...gen.Condition) *certDo {
+	return c.withDO(c.DO.Where(conds...))
+}
+
+func (c certDo) Exists(subquery interface{ UnderlyingDB() *gorm.DB }) *certDo {
+	return c.Where(field.CompareSubQuery(field.ExistsOp, nil, subquery.UnderlyingDB()))
+}
+
+func (c certDo) Order(conds ...field.Expr) *certDo {
+	return c.withDO(c.DO.Order(conds...))
+}
+
+func (c certDo) Distinct(cols ...field.Expr) *certDo {
+	return c.withDO(c.DO.Distinct(cols...))
+}
+
+func (c certDo) Omit(cols ...field.Expr) *certDo {
+	return c.withDO(c.DO.Omit(cols...))
+}
+
+func (c certDo) Join(table schema.Tabler, on ...field.Expr) *certDo {
+	return c.withDO(c.DO.Join(table, on...))
+}
+
+func (c certDo) LeftJoin(table schema.Tabler, on ...field.Expr) *certDo {
+	return c.withDO(c.DO.LeftJoin(table, on...))
+}
+
+func (c certDo) RightJoin(table schema.Tabler, on ...field.Expr) *certDo {
+	return c.withDO(c.DO.RightJoin(table, on...))
+}
+
+func (c certDo) Group(cols ...field.Expr) *certDo {
+	return c.withDO(c.DO.Group(cols...))
+}
+
+func (c certDo) Having(conds ...gen.Condition) *certDo {
+	return c.withDO(c.DO.Having(conds...))
+}
+
+func (c certDo) Limit(limit int) *certDo {
+	return c.withDO(c.DO.Limit(limit))
+}
+
+func (c certDo) Offset(offset int) *certDo {
+	return c.withDO(c.DO.Offset(offset))
+}
+
+func (c certDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *certDo {
+	return c.withDO(c.DO.Scopes(funcs...))
+}
+
+func (c certDo) Unscoped() *certDo {
+	return c.withDO(c.DO.Unscoped())
+}
+
+func (c certDo) Create(values ...*model.Cert) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return c.DO.Create(values)
+}
+
+func (c certDo) CreateInBatches(values []*model.Cert, batchSize int) error {
+	return c.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (c certDo) Save(values ...*model.Cert) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return c.DO.Save(values)
+}
+
+func (c certDo) First() (*model.Cert, error) {
+	if result, err := c.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Cert), nil
+	}
+}
+
+func (c certDo) Take() (*model.Cert, error) {
+	if result, err := c.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Cert), nil
+	}
+}
+
+func (c certDo) Last() (*model.Cert, error) {
+	if result, err := c.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Cert), nil
+	}
+}
+
+func (c certDo) Find() ([]*model.Cert, error) {
+	result, err := c.DO.Find()
+	return result.([]*model.Cert), err
+}
+
+func (c certDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Cert, err error) {
+	buf := make([]*model.Cert, 0, batchSize)
+	err = c.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (c certDo) FindInBatches(result *[]*model.Cert, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return c.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (c certDo) Attrs(attrs ...field.AssignExpr) *certDo {
+	return c.withDO(c.DO.Attrs(attrs...))
+}
+
+func (c certDo) Assign(attrs ...field.AssignExpr) *certDo {
+	return c.withDO(c.DO.Assign(attrs...))
+}
+
+func (c certDo) Joins(fields ...field.RelationField) *certDo {
+	for _, _f := range fields {
+		c = *c.withDO(c.DO.Joins(_f))
+	}
+	return &c
+}
+
+func (c certDo) Preload(fields ...field.RelationField) *certDo {
+	for _, _f := range fields {
+		c = *c.withDO(c.DO.Preload(_f))
+	}
+	return &c
+}
+
+func (c certDo) FirstOrInit() (*model.Cert, error) {
+	if result, err := c.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Cert), nil
+	}
+}
+
+func (c certDo) FirstOrCreate() (*model.Cert, error) {
+	if result, err := c.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Cert), nil
+	}
+}
+
+func (c certDo) FindByPage(offset int, limit int) (result []*model.Cert, count int64, err error) {
+	result, err = c.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = c.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (c certDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = c.Count()
+	if err != nil {
+		return
+	}
+
+	err = c.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (c certDo) Scan(result interface{}) (err error) {
+	return c.DO.Scan(result)
+}
+
+func (c certDo) Delete(models ...*model.Cert) (result gen.ResultInfo, err error) {
+	return c.DO.Delete(models)
+}
+
+func (c *certDo) withDO(do gen.Dao) *certDo {
+	c.DO = *do.(*gen.DO)
+	return c
+}

+ 358 - 0
server/query/chat_gpt_logs.gen.go

@@ -0,0 +1,358 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"strings"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/0xJacky/Nginx-UI/server/model"
+)
+
+func newChatGPTLog(db *gorm.DB, opts ...gen.DOOption) chatGPTLog {
+	_chatGPTLog := chatGPTLog{}
+
+	_chatGPTLog.chatGPTLogDo.UseDB(db, opts...)
+	_chatGPTLog.chatGPTLogDo.UseModel(&model.ChatGPTLog{})
+
+	tableName := _chatGPTLog.chatGPTLogDo.TableName()
+	_chatGPTLog.ALL = field.NewAsterisk(tableName)
+	_chatGPTLog.Name = field.NewString(tableName, "name")
+	_chatGPTLog.Content = field.NewField(tableName, "content")
+
+	_chatGPTLog.fillFieldMap()
+
+	return _chatGPTLog
+}
+
+type chatGPTLog struct {
+	chatGPTLogDo
+
+	ALL     field.Asterisk
+	Name    field.String
+	Content field.Field
+
+	fieldMap map[string]field.Expr
+}
+
+func (c chatGPTLog) Table(newTableName string) *chatGPTLog {
+	c.chatGPTLogDo.UseTable(newTableName)
+	return c.updateTableName(newTableName)
+}
+
+func (c chatGPTLog) As(alias string) *chatGPTLog {
+	c.chatGPTLogDo.DO = *(c.chatGPTLogDo.As(alias).(*gen.DO))
+	return c.updateTableName(alias)
+}
+
+func (c *chatGPTLog) updateTableName(table string) *chatGPTLog {
+	c.ALL = field.NewAsterisk(table)
+	c.Name = field.NewString(table, "name")
+	c.Content = field.NewField(table, "content")
+
+	c.fillFieldMap()
+
+	return c
+}
+
+func (c *chatGPTLog) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := c.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (c *chatGPTLog) fillFieldMap() {
+	c.fieldMap = make(map[string]field.Expr, 2)
+	c.fieldMap["name"] = c.Name
+	c.fieldMap["content"] = c.Content
+}
+
+func (c chatGPTLog) clone(db *gorm.DB) chatGPTLog {
+	c.chatGPTLogDo.ReplaceConnPool(db.Statement.ConnPool)
+	return c
+}
+
+func (c chatGPTLog) replaceDB(db *gorm.DB) chatGPTLog {
+	c.chatGPTLogDo.ReplaceDB(db)
+	return c
+}
+
+type chatGPTLogDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (c chatGPTLogDo) FirstByID(id int) (result *model.ChatGPTLog, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = c.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+// DeleteByID update @@table set deleted_at=NOW() where id=@id
+func (c chatGPTLogDo) DeleteByID(id int) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update chat_gpt_logs set deleted_at=NOW() where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = c.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (c chatGPTLogDo) Debug() *chatGPTLogDo {
+	return c.withDO(c.DO.Debug())
+}
+
+func (c chatGPTLogDo) WithContext(ctx context.Context) *chatGPTLogDo {
+	return c.withDO(c.DO.WithContext(ctx))
+}
+
+func (c chatGPTLogDo) ReadDB() *chatGPTLogDo {
+	return c.Clauses(dbresolver.Read)
+}
+
+func (c chatGPTLogDo) WriteDB() *chatGPTLogDo {
+	return c.Clauses(dbresolver.Write)
+}
+
+func (c chatGPTLogDo) Session(config *gorm.Session) *chatGPTLogDo {
+	return c.withDO(c.DO.Session(config))
+}
+
+func (c chatGPTLogDo) Clauses(conds ...clause.Expression) *chatGPTLogDo {
+	return c.withDO(c.DO.Clauses(conds...))
+}
+
+func (c chatGPTLogDo) Returning(value interface{}, columns ...string) *chatGPTLogDo {
+	return c.withDO(c.DO.Returning(value, columns...))
+}
+
+func (c chatGPTLogDo) Not(conds ...gen.Condition) *chatGPTLogDo {
+	return c.withDO(c.DO.Not(conds...))
+}
+
+func (c chatGPTLogDo) Or(conds ...gen.Condition) *chatGPTLogDo {
+	return c.withDO(c.DO.Or(conds...))
+}
+
+func (c chatGPTLogDo) Select(conds ...field.Expr) *chatGPTLogDo {
+	return c.withDO(c.DO.Select(conds...))
+}
+
+func (c chatGPTLogDo) Where(conds ...gen.Condition) *chatGPTLogDo {
+	return c.withDO(c.DO.Where(conds...))
+}
+
+func (c chatGPTLogDo) Exists(subquery interface{ UnderlyingDB() *gorm.DB }) *chatGPTLogDo {
+	return c.Where(field.CompareSubQuery(field.ExistsOp, nil, subquery.UnderlyingDB()))
+}
+
+func (c chatGPTLogDo) Order(conds ...field.Expr) *chatGPTLogDo {
+	return c.withDO(c.DO.Order(conds...))
+}
+
+func (c chatGPTLogDo) Distinct(cols ...field.Expr) *chatGPTLogDo {
+	return c.withDO(c.DO.Distinct(cols...))
+}
+
+func (c chatGPTLogDo) Omit(cols ...field.Expr) *chatGPTLogDo {
+	return c.withDO(c.DO.Omit(cols...))
+}
+
+func (c chatGPTLogDo) Join(table schema.Tabler, on ...field.Expr) *chatGPTLogDo {
+	return c.withDO(c.DO.Join(table, on...))
+}
+
+func (c chatGPTLogDo) LeftJoin(table schema.Tabler, on ...field.Expr) *chatGPTLogDo {
+	return c.withDO(c.DO.LeftJoin(table, on...))
+}
+
+func (c chatGPTLogDo) RightJoin(table schema.Tabler, on ...field.Expr) *chatGPTLogDo {
+	return c.withDO(c.DO.RightJoin(table, on...))
+}
+
+func (c chatGPTLogDo) Group(cols ...field.Expr) *chatGPTLogDo {
+	return c.withDO(c.DO.Group(cols...))
+}
+
+func (c chatGPTLogDo) Having(conds ...gen.Condition) *chatGPTLogDo {
+	return c.withDO(c.DO.Having(conds...))
+}
+
+func (c chatGPTLogDo) Limit(limit int) *chatGPTLogDo {
+	return c.withDO(c.DO.Limit(limit))
+}
+
+func (c chatGPTLogDo) Offset(offset int) *chatGPTLogDo {
+	return c.withDO(c.DO.Offset(offset))
+}
+
+func (c chatGPTLogDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *chatGPTLogDo {
+	return c.withDO(c.DO.Scopes(funcs...))
+}
+
+func (c chatGPTLogDo) Unscoped() *chatGPTLogDo {
+	return c.withDO(c.DO.Unscoped())
+}
+
+func (c chatGPTLogDo) Create(values ...*model.ChatGPTLog) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return c.DO.Create(values)
+}
+
+func (c chatGPTLogDo) CreateInBatches(values []*model.ChatGPTLog, batchSize int) error {
+	return c.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (c chatGPTLogDo) Save(values ...*model.ChatGPTLog) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return c.DO.Save(values)
+}
+
+func (c chatGPTLogDo) First() (*model.ChatGPTLog, error) {
+	if result, err := c.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.ChatGPTLog), nil
+	}
+}
+
+func (c chatGPTLogDo) Take() (*model.ChatGPTLog, error) {
+	if result, err := c.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.ChatGPTLog), nil
+	}
+}
+
+func (c chatGPTLogDo) Last() (*model.ChatGPTLog, error) {
+	if result, err := c.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.ChatGPTLog), nil
+	}
+}
+
+func (c chatGPTLogDo) Find() ([]*model.ChatGPTLog, error) {
+	result, err := c.DO.Find()
+	return result.([]*model.ChatGPTLog), err
+}
+
+func (c chatGPTLogDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.ChatGPTLog, err error) {
+	buf := make([]*model.ChatGPTLog, 0, batchSize)
+	err = c.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (c chatGPTLogDo) FindInBatches(result *[]*model.ChatGPTLog, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return c.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (c chatGPTLogDo) Attrs(attrs ...field.AssignExpr) *chatGPTLogDo {
+	return c.withDO(c.DO.Attrs(attrs...))
+}
+
+func (c chatGPTLogDo) Assign(attrs ...field.AssignExpr) *chatGPTLogDo {
+	return c.withDO(c.DO.Assign(attrs...))
+}
+
+func (c chatGPTLogDo) Joins(fields ...field.RelationField) *chatGPTLogDo {
+	for _, _f := range fields {
+		c = *c.withDO(c.DO.Joins(_f))
+	}
+	return &c
+}
+
+func (c chatGPTLogDo) Preload(fields ...field.RelationField) *chatGPTLogDo {
+	for _, _f := range fields {
+		c = *c.withDO(c.DO.Preload(_f))
+	}
+	return &c
+}
+
+func (c chatGPTLogDo) FirstOrInit() (*model.ChatGPTLog, error) {
+	if result, err := c.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.ChatGPTLog), nil
+	}
+}
+
+func (c chatGPTLogDo) FirstOrCreate() (*model.ChatGPTLog, error) {
+	if result, err := c.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.ChatGPTLog), nil
+	}
+}
+
+func (c chatGPTLogDo) FindByPage(offset int, limit int) (result []*model.ChatGPTLog, count int64, err error) {
+	result, err = c.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = c.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (c chatGPTLogDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = c.Count()
+	if err != nil {
+		return
+	}
+
+	err = c.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (c chatGPTLogDo) Scan(result interface{}) (err error) {
+	return c.DO.Scan(result)
+}
+
+func (c chatGPTLogDo) Delete(models ...*model.ChatGPTLog) (result gen.ResultInfo, err error) {
+	return c.DO.Delete(models)
+}
+
+func (c *chatGPTLogDo) withDO(do gen.Dao) *chatGPTLogDo {
+	c.DO = *do.(*gen.DO)
+	return c
+}

+ 378 - 0
server/query/config_backups.gen.go

@@ -0,0 +1,378 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"strings"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/0xJacky/Nginx-UI/server/model"
+)
+
+func newConfigBackup(db *gorm.DB, opts ...gen.DOOption) configBackup {
+	_configBackup := configBackup{}
+
+	_configBackup.configBackupDo.UseDB(db, opts...)
+	_configBackup.configBackupDo.UseModel(&model.ConfigBackup{})
+
+	tableName := _configBackup.configBackupDo.TableName()
+	_configBackup.ALL = field.NewAsterisk(tableName)
+	_configBackup.ID = field.NewUint(tableName, "id")
+	_configBackup.CreatedAt = field.NewTime(tableName, "created_at")
+	_configBackup.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_configBackup.DeletedAt = field.NewTime(tableName, "deleted_at")
+	_configBackup.Name = field.NewString(tableName, "name")
+	_configBackup.FilePath = field.NewString(tableName, "file_path")
+	_configBackup.Content = field.NewString(tableName, "content")
+
+	_configBackup.fillFieldMap()
+
+	return _configBackup
+}
+
+type configBackup struct {
+	configBackupDo
+
+	ALL       field.Asterisk
+	ID        field.Uint
+	CreatedAt field.Time
+	UpdatedAt field.Time
+	DeletedAt field.Time
+	Name      field.String
+	FilePath  field.String
+	Content   field.String
+
+	fieldMap map[string]field.Expr
+}
+
+func (c configBackup) Table(newTableName string) *configBackup {
+	c.configBackupDo.UseTable(newTableName)
+	return c.updateTableName(newTableName)
+}
+
+func (c configBackup) As(alias string) *configBackup {
+	c.configBackupDo.DO = *(c.configBackupDo.As(alias).(*gen.DO))
+	return c.updateTableName(alias)
+}
+
+func (c *configBackup) updateTableName(table string) *configBackup {
+	c.ALL = field.NewAsterisk(table)
+	c.ID = field.NewUint(table, "id")
+	c.CreatedAt = field.NewTime(table, "created_at")
+	c.UpdatedAt = field.NewTime(table, "updated_at")
+	c.DeletedAt = field.NewTime(table, "deleted_at")
+	c.Name = field.NewString(table, "name")
+	c.FilePath = field.NewString(table, "file_path")
+	c.Content = field.NewString(table, "content")
+
+	c.fillFieldMap()
+
+	return c
+}
+
+func (c *configBackup) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := c.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (c *configBackup) fillFieldMap() {
+	c.fieldMap = make(map[string]field.Expr, 7)
+	c.fieldMap["id"] = c.ID
+	c.fieldMap["created_at"] = c.CreatedAt
+	c.fieldMap["updated_at"] = c.UpdatedAt
+	c.fieldMap["deleted_at"] = c.DeletedAt
+	c.fieldMap["name"] = c.Name
+	c.fieldMap["file_path"] = c.FilePath
+	c.fieldMap["content"] = c.Content
+}
+
+func (c configBackup) clone(db *gorm.DB) configBackup {
+	c.configBackupDo.ReplaceConnPool(db.Statement.ConnPool)
+	return c
+}
+
+func (c configBackup) replaceDB(db *gorm.DB) configBackup {
+	c.configBackupDo.ReplaceDB(db)
+	return c
+}
+
+type configBackupDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (c configBackupDo) FirstByID(id int) (result *model.ConfigBackup, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = c.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+// DeleteByID update @@table set deleted_at=NOW() where id=@id
+func (c configBackupDo) DeleteByID(id int) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update config_backups set deleted_at=NOW() where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = c.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (c configBackupDo) Debug() *configBackupDo {
+	return c.withDO(c.DO.Debug())
+}
+
+func (c configBackupDo) WithContext(ctx context.Context) *configBackupDo {
+	return c.withDO(c.DO.WithContext(ctx))
+}
+
+func (c configBackupDo) ReadDB() *configBackupDo {
+	return c.Clauses(dbresolver.Read)
+}
+
+func (c configBackupDo) WriteDB() *configBackupDo {
+	return c.Clauses(dbresolver.Write)
+}
+
+func (c configBackupDo) Session(config *gorm.Session) *configBackupDo {
+	return c.withDO(c.DO.Session(config))
+}
+
+func (c configBackupDo) Clauses(conds ...clause.Expression) *configBackupDo {
+	return c.withDO(c.DO.Clauses(conds...))
+}
+
+func (c configBackupDo) Returning(value interface{}, columns ...string) *configBackupDo {
+	return c.withDO(c.DO.Returning(value, columns...))
+}
+
+func (c configBackupDo) Not(conds ...gen.Condition) *configBackupDo {
+	return c.withDO(c.DO.Not(conds...))
+}
+
+func (c configBackupDo) Or(conds ...gen.Condition) *configBackupDo {
+	return c.withDO(c.DO.Or(conds...))
+}
+
+func (c configBackupDo) Select(conds ...field.Expr) *configBackupDo {
+	return c.withDO(c.DO.Select(conds...))
+}
+
+func (c configBackupDo) Where(conds ...gen.Condition) *configBackupDo {
+	return c.withDO(c.DO.Where(conds...))
+}
+
+func (c configBackupDo) Exists(subquery interface{ UnderlyingDB() *gorm.DB }) *configBackupDo {
+	return c.Where(field.CompareSubQuery(field.ExistsOp, nil, subquery.UnderlyingDB()))
+}
+
+func (c configBackupDo) Order(conds ...field.Expr) *configBackupDo {
+	return c.withDO(c.DO.Order(conds...))
+}
+
+func (c configBackupDo) Distinct(cols ...field.Expr) *configBackupDo {
+	return c.withDO(c.DO.Distinct(cols...))
+}
+
+func (c configBackupDo) Omit(cols ...field.Expr) *configBackupDo {
+	return c.withDO(c.DO.Omit(cols...))
+}
+
+func (c configBackupDo) Join(table schema.Tabler, on ...field.Expr) *configBackupDo {
+	return c.withDO(c.DO.Join(table, on...))
+}
+
+func (c configBackupDo) LeftJoin(table schema.Tabler, on ...field.Expr) *configBackupDo {
+	return c.withDO(c.DO.LeftJoin(table, on...))
+}
+
+func (c configBackupDo) RightJoin(table schema.Tabler, on ...field.Expr) *configBackupDo {
+	return c.withDO(c.DO.RightJoin(table, on...))
+}
+
+func (c configBackupDo) Group(cols ...field.Expr) *configBackupDo {
+	return c.withDO(c.DO.Group(cols...))
+}
+
+func (c configBackupDo) Having(conds ...gen.Condition) *configBackupDo {
+	return c.withDO(c.DO.Having(conds...))
+}
+
+func (c configBackupDo) Limit(limit int) *configBackupDo {
+	return c.withDO(c.DO.Limit(limit))
+}
+
+func (c configBackupDo) Offset(offset int) *configBackupDo {
+	return c.withDO(c.DO.Offset(offset))
+}
+
+func (c configBackupDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *configBackupDo {
+	return c.withDO(c.DO.Scopes(funcs...))
+}
+
+func (c configBackupDo) Unscoped() *configBackupDo {
+	return c.withDO(c.DO.Unscoped())
+}
+
+func (c configBackupDo) Create(values ...*model.ConfigBackup) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return c.DO.Create(values)
+}
+
+func (c configBackupDo) CreateInBatches(values []*model.ConfigBackup, batchSize int) error {
+	return c.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (c configBackupDo) Save(values ...*model.ConfigBackup) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return c.DO.Save(values)
+}
+
+func (c configBackupDo) First() (*model.ConfigBackup, error) {
+	if result, err := c.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.ConfigBackup), nil
+	}
+}
+
+func (c configBackupDo) Take() (*model.ConfigBackup, error) {
+	if result, err := c.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.ConfigBackup), nil
+	}
+}
+
+func (c configBackupDo) Last() (*model.ConfigBackup, error) {
+	if result, err := c.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.ConfigBackup), nil
+	}
+}
+
+func (c configBackupDo) Find() ([]*model.ConfigBackup, error) {
+	result, err := c.DO.Find()
+	return result.([]*model.ConfigBackup), err
+}
+
+func (c configBackupDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.ConfigBackup, err error) {
+	buf := make([]*model.ConfigBackup, 0, batchSize)
+	err = c.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (c configBackupDo) FindInBatches(result *[]*model.ConfigBackup, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return c.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (c configBackupDo) Attrs(attrs ...field.AssignExpr) *configBackupDo {
+	return c.withDO(c.DO.Attrs(attrs...))
+}
+
+func (c configBackupDo) Assign(attrs ...field.AssignExpr) *configBackupDo {
+	return c.withDO(c.DO.Assign(attrs...))
+}
+
+func (c configBackupDo) Joins(fields ...field.RelationField) *configBackupDo {
+	for _, _f := range fields {
+		c = *c.withDO(c.DO.Joins(_f))
+	}
+	return &c
+}
+
+func (c configBackupDo) Preload(fields ...field.RelationField) *configBackupDo {
+	for _, _f := range fields {
+		c = *c.withDO(c.DO.Preload(_f))
+	}
+	return &c
+}
+
+func (c configBackupDo) FirstOrInit() (*model.ConfigBackup, error) {
+	if result, err := c.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.ConfigBackup), nil
+	}
+}
+
+func (c configBackupDo) FirstOrCreate() (*model.ConfigBackup, error) {
+	if result, err := c.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.ConfigBackup), nil
+	}
+}
+
+func (c configBackupDo) FindByPage(offset int, limit int) (result []*model.ConfigBackup, count int64, err error) {
+	result, err = c.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = c.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (c configBackupDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = c.Count()
+	if err != nil {
+		return
+	}
+
+	err = c.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (c configBackupDo) Scan(result interface{}) (err error) {
+	return c.DO.Scan(result)
+}
+
+func (c configBackupDo) Delete(models ...*model.ConfigBackup) (result gen.ResultInfo, err error) {
+	return c.DO.Delete(models)
+}
+
+func (c *configBackupDo) withDO(do gen.Dao) *configBackupDo {
+	c.DO = *do.(*gen.DO)
+	return c
+}

+ 131 - 0
server/query/gen.go

@@ -0,0 +1,131 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"database/sql"
+
+	"gorm.io/gorm"
+
+	"gorm.io/gen"
+
+	"gorm.io/plugin/dbresolver"
+)
+
+var (
+	Q            = new(Query)
+	Auth         *auth
+	AuthToken    *authToken
+	Cert         *cert
+	ChatGPTLog   *chatGPTLog
+	ConfigBackup *configBackup
+)
+
+func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
+	*Q = *Use(db, opts...)
+	Auth = &Q.Auth
+	AuthToken = &Q.AuthToken
+	Cert = &Q.Cert
+	ChatGPTLog = &Q.ChatGPTLog
+	ConfigBackup = &Q.ConfigBackup
+}
+
+func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
+	return &Query{
+		db:           db,
+		Auth:         newAuth(db, opts...),
+		AuthToken:    newAuthToken(db, opts...),
+		Cert:         newCert(db, opts...),
+		ChatGPTLog:   newChatGPTLog(db, opts...),
+		ConfigBackup: newConfigBackup(db, opts...),
+	}
+}
+
+type Query struct {
+	db *gorm.DB
+
+	Auth         auth
+	AuthToken    authToken
+	Cert         cert
+	ChatGPTLog   chatGPTLog
+	ConfigBackup configBackup
+}
+
+func (q *Query) Available() bool { return q.db != nil }
+
+func (q *Query) clone(db *gorm.DB) *Query {
+	return &Query{
+		db:           db,
+		Auth:         q.Auth.clone(db),
+		AuthToken:    q.AuthToken.clone(db),
+		Cert:         q.Cert.clone(db),
+		ChatGPTLog:   q.ChatGPTLog.clone(db),
+		ConfigBackup: q.ConfigBackup.clone(db),
+	}
+}
+
+func (q *Query) ReadDB() *Query {
+	return q.ReplaceDB(q.db.Clauses(dbresolver.Read))
+}
+
+func (q *Query) WriteDB() *Query {
+	return q.ReplaceDB(q.db.Clauses(dbresolver.Write))
+}
+
+func (q *Query) ReplaceDB(db *gorm.DB) *Query {
+	return &Query{
+		db:           db,
+		Auth:         q.Auth.replaceDB(db),
+		AuthToken:    q.AuthToken.replaceDB(db),
+		Cert:         q.Cert.replaceDB(db),
+		ChatGPTLog:   q.ChatGPTLog.replaceDB(db),
+		ConfigBackup: q.ConfigBackup.replaceDB(db),
+	}
+}
+
+type queryCtx struct {
+	Auth         *authDo
+	AuthToken    *authTokenDo
+	Cert         *certDo
+	ChatGPTLog   *chatGPTLogDo
+	ConfigBackup *configBackupDo
+}
+
+func (q *Query) WithContext(ctx context.Context) *queryCtx {
+	return &queryCtx{
+		Auth:         q.Auth.WithContext(ctx),
+		AuthToken:    q.AuthToken.WithContext(ctx),
+		Cert:         q.Cert.WithContext(ctx),
+		ChatGPTLog:   q.ChatGPTLog.WithContext(ctx),
+		ConfigBackup: q.ConfigBackup.WithContext(ctx),
+	}
+}
+
+func (q *Query) Transaction(fc func(tx *Query) error, opts ...*sql.TxOptions) error {
+	return q.db.Transaction(func(tx *gorm.DB) error { return fc(q.clone(tx)) }, opts...)
+}
+
+func (q *Query) Begin(opts ...*sql.TxOptions) *QueryTx {
+	return &QueryTx{q.clone(q.db.Begin(opts...))}
+}
+
+type QueryTx struct{ *Query }
+
+func (q *QueryTx) Commit() error {
+	return q.db.Commit().Error
+}
+
+func (q *QueryTx) Rollback() error {
+	return q.db.Rollback().Error
+}
+
+func (q *QueryTx) SavePoint(name string) error {
+	return q.db.SavePoint(name).Error
+}
+
+func (q *QueryTx) RollbackTo(name string) error {
+	return q.db.RollbackTo(name).Error
+}

+ 9 - 0
server/query/query.go

@@ -0,0 +1,9 @@
+package query
+
+import (
+	"gorm.io/gorm"
+)
+
+func Init(db *gorm.DB) {
+	SetDefault(db)
+}

+ 110 - 106
server/router/routers.go

@@ -1,113 +1,117 @@
 package router
 
 import (
-    "github.com/0xJacky/Nginx-UI/server/api"
-    "github.com/gin-contrib/static"
-    "github.com/gin-gonic/gin"
-    "net/http"
+	"github.com/0xJacky/Nginx-UI/server/api"
+	"github.com/gin-contrib/static"
+	"github.com/gin-gonic/gin"
+	"net/http"
 )
 
 func InitRouter() *gin.Engine {
-    r := gin.New()
-    r.Use(gin.Logger())
-
-    r.Use(recovery())
-
-    r.Use(cacheJs())
-
-    r.Use(static.Serve("/", mustFS("")))
-
-    r.NoRoute(func(c *gin.Context) {
-        c.Redirect(http.StatusMovedPermanently, "/")
-    })
-
-    root := r.Group("/api")
-    {
-        root.GET("install", api.InstallLockCheck)
-        root.POST("install", api.InstallNginxUI)
-
-        root.POST("/login", api.Login)
-        root.DELETE("/logout", api.Logout)
-
-        g := root.Group("/", authRequired())
-        {
-            g.GET("analytic", api.Analytic)
-            g.GET("analytic/init", api.GetAnalyticInit)
-
-            g.GET("users", api.GetUsers)
-            g.GET("user/:id", api.GetUser)
-            g.POST("user", api.AddUser)
-            g.POST("user/:id", api.EditUser)
-            g.DELETE("user/:id", api.DeleteUser)
-
-            g.GET("domains", api.GetDomains)
-            g.GET("domain/:name", api.GetDomain)
-
-            // Modify site configuration directly
-            g.POST("domain/:name", api.SaveDomain)
-
-            // Transform NgxConf to nginx configuration
-            g.POST("ngx/build_config", api.BuildNginxConfig)
-            // Tokenized nginx configuration to NgxConf
-            g.POST("ngx/tokenize_config", api.TokenizeNginxConfig)
-            // Format nginx configuration code
-            g.POST("ngx/format_code", api.FormatNginxConfig)
-            // nginx reload
-            g.POST("nginx/reload", api.ReloadNginx)
-            // nginx restart
-            g.POST("nginx/restart", api.RestartNginx)
-            // nginx test
-            g.POST("nginx/test", api.TestNginx)
-            // nginx status
-            g.GET("nginx/status", api.NginxStatus)
-
-            g.POST("domain/:name/enable", api.EnableDomain)
-            g.POST("domain/:name/disable", api.DisableDomain)
-            g.DELETE("domain/:name", api.DeleteDomain)
-            // duplicate site
-            g.POST("domain/:name/duplicate", api.DuplicateSite)
-            g.GET("domain/:name/cert", api.IssueCert)
-
-            g.GET("configs", api.GetConfigs)
-            g.GET("config/*name", api.GetConfig)
-            g.POST("config", api.AddConfig)
-            g.POST("config/*name", api.EditConfig)
-
-            //g.GET("backups", api.GetFileBackupList)
-            //g.GET("backup/:id", api.GetFileBackup)
-
-            g.GET("template", api.GetTemplate)
-            g.GET("template/configs", api.GetTemplateConfList)
-            g.GET("template/blocks", api.GetTemplateBlockList)
-            g.GET("template/block/:name", api.GetTemplateBlock)
-
-            g.GET("certs", api.GetCertList)
-            g.GET("cert/:id", api.GetCert)
-            g.POST("cert", api.AddCert)
-            g.POST("cert/:id", api.ModifyCert)
-            g.DELETE("cert/:id", api.RemoveCert)
-            // Add domain to auto-renew cert list
-            g.POST("auto_cert/:name", api.AddDomainToAutoCert)
-            // Delete domain from auto-renew cert list
-            g.DELETE("auto_cert/:name", api.RemoveDomainFromAutoCert)
-
-            // pty
-            g.GET("pty", api.Pty)
-
-            // Nginx log
-            g.GET("nginx_log", api.NginxLog)
-            g.POST("nginx_log", api.GetNginxLogPage)
-
-            // Settings
-            g.GET("settings", api.GetSettings)
-            g.POST("settings", api.SaveSettings)
-
-            // Upgrade
-            g.GET("upgrade/release", api.GetRelease)
-            g.GET("upgrade/current", api.GetCurrentVersion)
-            g.GET("upgrade/perform", api.PerformCoreUpgrade)
-        }
-    }
-
-    return r
+	r := gin.New()
+	r.Use(gin.Logger())
+
+	r.Use(recovery())
+
+	r.Use(cacheJs())
+
+	r.Use(static.Serve("/", mustFS("")))
+
+	r.NoRoute(func(c *gin.Context) {
+		c.Redirect(http.StatusMovedPermanently, "/")
+	})
+
+	root := r.Group("/api")
+	{
+		root.GET("install", api.InstallLockCheck)
+		root.POST("install", api.InstallNginxUI)
+
+		root.POST("/login", api.Login)
+		root.DELETE("/logout", api.Logout)
+
+		g := root.Group("/", authRequired())
+		{
+			g.GET("analytic", api.Analytic)
+			g.GET("analytic/init", api.GetAnalyticInit)
+
+			g.GET("users", api.GetUsers)
+			g.GET("user/:id", api.GetUser)
+			g.POST("user", api.AddUser)
+			g.POST("user/:id", api.EditUser)
+			g.DELETE("user/:id", api.DeleteUser)
+
+			g.GET("domains", api.GetDomains)
+			g.GET("domain/:name", api.GetDomain)
+
+			// Modify site configuration directly
+			g.POST("domain/:name", api.SaveDomain)
+
+			// Transform NgxConf to nginx configuration
+			g.POST("ngx/build_config", api.BuildNginxConfig)
+			// Tokenized nginx configuration to NgxConf
+			g.POST("ngx/tokenize_config", api.TokenizeNginxConfig)
+			// Format nginx configuration code
+			g.POST("ngx/format_code", api.FormatNginxConfig)
+			// nginx reload
+			g.POST("nginx/reload", api.ReloadNginx)
+			// nginx restart
+			g.POST("nginx/restart", api.RestartNginx)
+			// nginx test
+			g.POST("nginx/test", api.TestNginx)
+			// nginx status
+			g.GET("nginx/status", api.NginxStatus)
+
+			g.POST("domain/:name/enable", api.EnableDomain)
+			g.POST("domain/:name/disable", api.DisableDomain)
+			g.DELETE("domain/:name", api.DeleteDomain)
+			// duplicate site
+			g.POST("domain/:name/duplicate", api.DuplicateSite)
+			g.GET("domain/:name/cert", api.IssueCert)
+
+			g.GET("configs", api.GetConfigs)
+			g.GET("config/*name", api.GetConfig)
+			g.POST("config", api.AddConfig)
+			g.POST("config/*name", api.EditConfig)
+
+			//g.GET("backups", api.GetFileBackupList)
+			//g.GET("backup/:id", api.GetFileBackup)
+
+			g.GET("template", api.GetTemplate)
+			g.GET("template/configs", api.GetTemplateConfList)
+			g.GET("template/blocks", api.GetTemplateBlockList)
+			g.GET("template/block/:name", api.GetTemplateBlock)
+
+			g.GET("certs", api.GetCertList)
+			g.GET("cert/:id", api.GetCert)
+			g.POST("cert", api.AddCert)
+			g.POST("cert/:id", api.ModifyCert)
+			g.DELETE("cert/:id", api.RemoveCert)
+			// Add domain to auto-renew cert list
+			g.POST("auto_cert/:name", api.AddDomainToAutoCert)
+			// Delete domain from auto-renew cert list
+			g.DELETE("auto_cert/:name", api.RemoveDomainFromAutoCert)
+
+			// pty
+			g.GET("pty", api.Pty)
+
+			// Nginx log
+			g.GET("nginx_log", api.NginxLog)
+			g.POST("nginx_log", api.GetNginxLogPage)
+
+			// Settings
+			g.GET("settings", api.GetSettings)
+			g.POST("settings", api.SaveSettings)
+
+			// Upgrade
+			g.GET("upgrade/release", api.GetRelease)
+			g.GET("upgrade/current", api.GetCurrentVersion)
+			g.GET("upgrade/perform", api.PerformCoreUpgrade)
+
+			// ChatGPT
+			g.POST("/chat_gpt", api.MakeChatCompletionRequest)
+			g.POST("/chat_gpt_record", api.StoreChatGPTRecord)
+		}
+	}
+
+	return r
 }

+ 10 - 0
server/settings/settings.go

@@ -36,6 +36,13 @@ type NginxLog struct {
 	ErrorLogPath  string `json:"error_log_path"`
 }
 
+type OpenAI struct {
+	BaseUrl string `json:"base_url"`
+	Token   string `json:"token"`
+	Proxy   string `json:"proxy"`
+	Model   string `json:"model"`
+}
+
 var ServerSettings = &Server{
 	HttpPort:          "9000",
 	RunMode:           "debug",
@@ -53,11 +60,14 @@ var NginxLogSettings = &NginxLog{
 	ErrorLogPath:  "",
 }
 
+var OpenAISettings = &OpenAI{}
+
 var ConfPath string
 
 var sections = map[string]interface{}{
 	"server":    ServerSettings,
 	"nginx_log": NginxLogSettings,
+	"openai":    OpenAISettings,
 }
 
 func init() {

+ 51 - 0
server/test/chatgpt_test.go

@@ -0,0 +1,51 @@
+package test
+
+import (
+	"context"
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/pkg/errors"
+	"github.com/sashabaranov/go-openai"
+	"io"
+	"os"
+	"testing"
+)
+
+func TestChatGPT(t *testing.T) {
+	settings.Init("../../app.ini")
+	c := openai.NewClient(settings.OpenAISettings.Token)
+
+	ctx := context.Background()
+
+	req := openai.ChatCompletionRequest{
+		Model: openai.GPT3Dot5Turbo0301,
+		Messages: []openai.ChatCompletionMessage{
+			{
+				Role:    openai.ChatMessageRoleUser,
+				Content: "帮我写一个 nginx 配置文件的示例",
+			},
+		},
+		Stream: true,
+	}
+	stream, err := c.CreateChatCompletionStream(ctx, req)
+	if err != nil {
+		fmt.Printf("CompletionStream error: %v\n", err)
+		return
+	}
+	defer stream.Close()
+
+	for {
+		response, err := stream.Recv()
+		if errors.Is(err, io.EOF) {
+			return
+		}
+
+		if err != nil {
+			fmt.Printf("Stream error: %v\n", err)
+			return
+		}
+
+		fmt.Printf("%v", response.Choices[0].Delta.Content)
+		_ = os.Stdout.Sync()
+	}
+}