Browse Source

优化会议记录

main
liwei1dao 4 months ago
parent
commit
ae39fdeef1
  1. 2
      apps/admin/.nuxt/components.d.ts
  2. 3350
      apps/admin/.nuxt/dev/index.mjs
  3. 1
      apps/admin/.nuxt/dev/index.mjs.map
  4. 2
      apps/admin/.nuxt/imports.d.ts
  5. 1
      apps/admin/.nuxt/manifest/latest.json
  6. 1
      apps/admin/.nuxt/manifest/meta/dev.json
  7. 17
      apps/admin/.nuxt/nitro.json
  8. 4
      apps/admin/.nuxt/nuxt.d.ts
  9. 9
      apps/admin/.nuxt/nuxt.json
  10. 9
      apps/admin/.nuxt/nuxt.lock
  11. 4
      apps/admin/.nuxt/nuxt.node.d.ts
  12. 2
      apps/admin/.nuxt/tailwind/postcss.mjs
  13. 4
      apps/admin/.nuxt/tsconfig.app.json
  14. 2
      apps/admin/.nuxt/tsconfig.json
  15. 2
      apps/admin/.nuxt/tsconfig.shared.json
  16. 2
      apps/admin/.nuxt/types/build.d.ts
  17. 2
      apps/admin/.nuxt/types/components.d.ts
  18. 4
      apps/admin/.nuxt/types/imports.d.ts
  19. 9
      apps/admin/.nuxt/types/plugins.d.ts
  20. 2
      apps/admin/.nuxt/types/runtime-config.d.ts
  21. 15
      apps/admin/node_modules/.cache/vite/client/deps/_metadata.json
  22. 51
      apps/admin/node_modules/.cache/vite/client/deps/errx.js
  23. 7
      apps/admin/node_modules/.cache/vite/client/deps/errx.js.map
  24. 3
      apps/admin/node_modules/.cache/vite/client/deps/package.json
  25. BIN
      apps/services/dump.rdb
  26. 13
      apps/services/log/console.log
  27. 11
      apps/services/modules/echomeet/core.go
  28. 55
      apps/services/modules/echomeet/model.go
  29. 30
      apps/services/modules/echomeet/model_cache.go
  30. 74
      deploy/console/README.md
  31. 8
      deploy/console/confs/console.yaml.example
  32. 184
      deploy/console/deploy.sh
  33. 76
      deploy/console/dev-deploy.sh
  34. 6
      deploy/console/docker-compose.yml
  35. 26
      deploy/console/env/dev.env.example
  36. 27
      deploy/console/env/env.example
  37. 23
      deploy/console/env/local.env.example
  38. 26
      deploy/console/env/prod.env.example
  39. 44
      deploy/console/prod-build.sh
  40. 87
      deploy/console/prod-deploy.sh
  41. 50
      deploy/voitrans/README.md
  42. 39
      deploy/voitrans/dev-deploy.sh
  43. 0
      deploy/voitrans/env/env.example
  44. 37
      deploy/voitrans/env/japan.env.example
  45. 38
      deploy/voitrans/env/shanghai.env.example
  46. 37
      deploy/voitrans/env/singapore.env.example
  47. 62
      deploy/voitrans/prod-deploy.sh

2
apps/admin/.nuxt/components.d.ts

@ -14,6 +14,7 @@ type HydrationStrategies = {
type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
export const AppLoading: typeof import("../app/components/AppLoading.vue")['default']
export const AppToast: typeof import("../app/components/AppToast.vue")['default']
export const NuxtWelcome: typeof import("../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/components/welcome.vue")['default']
export const NuxtLayout: typeof import("../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/components/nuxt-layout")['default']
@ -39,6 +40,7 @@ export const Head: typeof import("../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin
export const Html: typeof import("../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/head/runtime/components")['Html']
export const Body: typeof import("../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/head/runtime/components")['Body']
export const NuxtIsland: typeof import("../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/components/nuxt-island")['default']
export const LazyAppLoading: LazyComponent<typeof import("../app/components/AppLoading.vue")['default']>
export const LazyAppToast: LazyComponent<typeof import("../app/components/AppToast.vue")['default']>
export const LazyNuxtWelcome: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/components/welcome.vue")['default']>
export const LazyNuxtLayout: LazyComponent<typeof import("../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/components/nuxt-layout")['default']>

3350
apps/admin/.nuxt/dev/index.mjs

File diff suppressed because it is too large

1
apps/admin/.nuxt/dev/index.mjs.map

File diff suppressed because one or more lines are too long

2
apps/admin/.nuxt/imports.d.ts

@ -32,6 +32,8 @@ export { setInterval } from '#app/compat/interval';
export { definePageMeta, PageMeta } from '#app/composables/pages';
export { defineLazyHydrationComponent } from '#app/composables/lazy-hydration';
export { useApi } from '../app/composables/useApi';
export { useLoading } from '../app/composables/useLoading';
export { useToast, ToastType, Toast } from '../app/composables/useToast';
export { IDENTITY_LABELS, useAuthStore } from '../app/stores/auth';
export { defineStore, acceptHMRUpdate, usePinia, storeToRefs } from '../node_modules/.pnpm/@pinia+nuxt@0.11.3_magicast@0.5.3_pinia@3.0.4_typescript@5.9.3_vue@3.5.38_typescript@5.9.3__/node_modules/@pinia/nuxt/dist/runtime/composables';
export { useNuxtDevTools } from '../node_modules/.pnpm/@nuxt+devtools@3.2.4_vite@7.3.5_jiti@2.7.0_terser@5.48.0_yaml@2.9.0__vue@3.5.38_typescript@5.9.3_/node_modules/@nuxt/devtools/dist/runtime/use-nuxt-devtools';

1
apps/admin/.nuxt/manifest/latest.json

@ -0,0 +1 @@
{"id":"dev","timestamp":1782613791473}

1
apps/admin/.nuxt/manifest/meta/dev.json

@ -0,0 +1 @@
{"id":"dev","timestamp":1782613791473,"prerendered":[]}

17
apps/admin/.nuxt/nitro.json

@ -0,0 +1,17 @@
{
"date": "2026-06-28T02:29:52.480Z",
"preset": "nitro-dev",
"framework": {
"name": "nuxt",
"version": "4.4.8"
},
"versions": {
"nitro": "2.13.4"
},
"dev": {
"pid": 21165,
"workerAddress": {
"socketPath": "/var/folders/1w/40ft_rjj7qx8xn21kb1lj51r0000gn/T/nitro-worker-21165-1-1-132.sock"
}
}
}

4
apps/admin/.nuxt/nuxt.d.ts

@ -1,7 +1,7 @@
/// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="@nuxt/telemetry" />
/// <reference types="@nuxt/devtools" />
/// <reference types="@pinia/nuxt" />
/// <reference types="@nuxt/devtools" />
/// <reference types="@nuxt/telemetry" />
/// <reference path="types/nitro-layouts.d.ts" />
/// <reference path="types/builder-env.d.ts" />
/// <reference path="types/plugins.d.ts" />

9
apps/admin/.nuxt/nuxt.json

@ -0,0 +1,9 @@
{
"_hash": "EtElel9LL0Jf8LAcFEfkc2DWVGnfU9BxHYuI4FliSL8",
"project": {
"rootDir": "/Users/liwei/work/go/yunyan/yunyan-sas/apps/admin"
},
"versions": {
"nuxt": "4.4.8"
}
}

9
apps/admin/.nuxt/nuxt.lock

@ -0,0 +1,9 @@
{
"pid": 21165,
"startedAt": 1782613791026,
"command": "dev",
"cwd": "/Users/liwei/work/go/yunyan/yunyan-sas/apps/admin",
"port": 3000,
"hostname": "127.0.0.1",
"url": "http://127.0.0.1:3000"
}

4
apps/admin/.nuxt/nuxt.node.d.ts

@ -1,7 +1,7 @@
/// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="@nuxt/telemetry" />
/// <reference types="@nuxt/devtools" />
/// <reference types="@pinia/nuxt" />
/// <reference types="@nuxt/devtools" />
/// <reference types="@nuxt/telemetry" />
/// <reference path="types/nitro-layouts.d.ts" />
/// <reference path="types/modules.d.ts" />
/// <reference path="types/runtime-config.d.ts" />

2
apps/admin/.nuxt/tailwind/postcss.mjs

@ -1,4 +1,4 @@
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 6/24/2026, 4:45:28 PM
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 6/28/2026, 10:29:51 AM
import "@nuxtjs/tailwindcss/config-ctx"
import configMerger from "@nuxtjs/tailwindcss/merger";

4
apps/admin/.nuxt/tsconfig.app.json

@ -107,7 +107,7 @@
"./imports"
],
"#app-manifest": [
"./manifest/meta/69eeaf8b-1851-4f23-9104-41b4cfccebd5.json"
"./manifest/meta/dev.json"
],
"#components": [
"./components"
@ -197,7 +197,7 @@
"../node_modules/.pnpm/@nuxt+telemetry@2.8.0_@nuxt+kit@4.4.8_magicast@0.5.3_/node_modules/dist/runtime/server",
"../node_modules/.pnpm/@nuxt+telemetry@2.8.0_@nuxt+kit@4.4.8_magicast@0.5.3_/node_modules/*.*",
"../node_modules/.pnpm/@nuxt+telemetry@2.8.0_@nuxt+kit@4.4.8_magicast@0.5.3_/node_modules/dist/*.*",
"../.output",
"dev",
"../server"
],
"vueCompilerOptions": {

2
apps/admin/.nuxt/tsconfig.json

@ -107,7 +107,7 @@
"./imports"
],
"#app-manifest": [
"./manifest/meta/69eeaf8b-1851-4f23-9104-41b4cfccebd5.json"
"./manifest/meta/dev.json"
],
"#components": [
"./components"

2
apps/admin/.nuxt/tsconfig.shared.json

@ -107,7 +107,7 @@
"./imports"
],
"#app-manifest": [
"./manifest/meta/69eeaf8b-1851-4f23-9104-41b4cfccebd5.json"
"./manifest/meta/dev.json"
],
"#build": [
"."

2
apps/admin/.nuxt/types/build.d.ts

@ -12,6 +12,8 @@ declare module "#build/paths.mjs";
declare module "#build/root-component.mjs";
declare module "#build/plugins.server.mjs";
declare module "#build/test-component-wrapper.mjs";
declare module "#build/devtools/settings.mjs";
declare module "#build/runtime.vue-devtools-client.8eTWAfLgVpINiZM922q1ozAHX8JlyUM2pBnpX1z932g.js";
declare module "#build/routes.mjs";
declare module "#build/pages.mjs";
declare module "#build/router.options.mjs";

2
apps/admin/.nuxt/types/components.d.ts

@ -14,6 +14,7 @@ type HydrationStrategies = {
type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
interface _GlobalComponents {
AppLoading: typeof import("../../app/components/AppLoading.vue")['default']
AppToast: typeof import("../../app/components/AppToast.vue")['default']
NuxtWelcome: typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/components/welcome.vue")['default']
NuxtLayout: typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/components/nuxt-layout")['default']
@ -39,6 +40,7 @@ interface _GlobalComponents {
Html: typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/head/runtime/components")['Html']
Body: typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/head/runtime/components")['Body']
NuxtIsland: typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/components/nuxt-island")['default']
LazyAppLoading: LazyComponent<typeof import("../../app/components/AppLoading.vue")['default']>
LazyAppToast: LazyComponent<typeof import("../../app/components/AppToast.vue")['default']>
LazyNuxtWelcome: LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/components/welcome.vue")['default']>
LazyNuxtLayout: LazyComponent<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/components/nuxt-layout")['default']>

4
apps/admin/.nuxt/types/imports.d.ts

@ -118,10 +118,12 @@ declare global {
const useLazyAsyncData: typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/asyncData').useLazyAsyncData
const useLazyFetch: typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/fetch').useLazyFetch
const useLink: typeof import('../../node_modules/.pnpm/vue-router@5.1.0_@vue+compiler-sfc@3.5.38_esbuild@0.28.1_pinia@3.0.4_typescript@5.9.3_v_e9d2979651266ddc95cb9659efaeda1e/node_modules/vue-router').useLink
const useLoading: typeof import('../../app/composables/useLoading').useLoading
const useLoadingIndicator: typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/loading-indicator').useLoadingIndicator
const useModel: typeof import('vue').useModel
const useNuxtApp: typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/nuxt').useNuxtApp
const useNuxtData: typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/asyncData').useNuxtData
const useNuxtDevTools: typeof import('../../node_modules/.pnpm/@nuxt+devtools@3.2.4_vite@7.3.5_jiti@2.7.0_terser@5.48.0_yaml@2.9.0__vue@3.5.38_typescript@5.9.3_/node_modules/@nuxt/devtools/dist/runtime/use-nuxt-devtools').useNuxtDevTools
const usePinia: typeof import('../../node_modules/.pnpm/@pinia+nuxt@0.11.3_magicast@0.5.3_pinia@3.0.4_typescript@5.9.3_vue@3.5.38_typescript@5.9.3__/node_modules/@pinia/nuxt/dist/runtime/composables').usePinia
const usePreviewMode: typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/preview').usePreviewMode
const useRequestEvent: typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/ssr').useRequestEvent
@ -332,10 +334,12 @@ declare module 'vue' {
readonly useLazyAsyncData: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/asyncData')['useLazyAsyncData']>
readonly useLazyFetch: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/fetch')['useLazyFetch']>
readonly useLink: UnwrapRef<typeof import('../../node_modules/.pnpm/vue-router@5.1.0_@vue+compiler-sfc@3.5.38_esbuild@0.28.1_pinia@3.0.4_typescript@5.9.3_v_e9d2979651266ddc95cb9659efaeda1e/node_modules/vue-router')['useLink']>
readonly useLoading: UnwrapRef<typeof import('../../app/composables/useLoading')['useLoading']>
readonly useLoadingIndicator: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/loading-indicator')['useLoadingIndicator']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useNuxtApp: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/nuxt')['useNuxtApp']>
readonly useNuxtData: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/asyncData')['useNuxtData']>
readonly useNuxtDevTools: UnwrapRef<typeof import('../../node_modules/.pnpm/@nuxt+devtools@3.2.4_vite@7.3.5_jiti@2.7.0_terser@5.48.0_yaml@2.9.0__vue@3.5.38_typescript@5.9.3_/node_modules/@nuxt/devtools/dist/runtime/use-nuxt-devtools')['useNuxtDevTools']>
readonly usePinia: UnwrapRef<typeof import('../../node_modules/.pnpm/@pinia+nuxt@0.11.3_magicast@0.5.3_pinia@3.0.4_typescript@5.9.3_vue@3.5.38_typescript@5.9.3__/node_modules/@pinia/nuxt/dist/runtime/composables')['usePinia']>
readonly usePreviewMode: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/preview')['usePreviewMode']>
readonly useRequestEvent: UnwrapRef<typeof import('../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/composables/ssr')['useRequestEvent']>

9
apps/admin/.nuxt/types/plugins.d.ts

@ -10,20 +10,27 @@ type NuxtAppInjections =
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/plugins/revive-payload.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/head/runtime/plugins/unhead.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/pages/runtime/plugins/router.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/plugins/browser-devtools-timing.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/plugins/payload.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/plugins/dev-server-logs.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/plugins/navigation-repaint.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/plugins/check-outdated-build.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/plugins/revive-payload.server.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/plugins/chunk-reload.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@pinia+nuxt@0.11.3_magicast@0.5.3_pinia@3.0.4_typescript@5.9.3_vue@3.5.38_typescript@5.9.3__/node_modules/@pinia/nuxt/dist/runtime/plugin.vue3.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/pages/runtime/plugins/prefetch.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/pages/runtime/plugins/check-if-page-unused.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxt+devtools@3.2.4_vite@7.3.5_jiti@2.7.0_terser@5.48.0_yaml@2.9.0__vue@3.5.38_typescript@5.9.3_/node_modules/@nuxt/devtools/dist/runtime/plugins/devtools.server.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/@nuxt+devtools@3.2.4_vite@7.3.5_jiti@2.7.0_terser@5.48.0_yaml@2.9.0__vue@3.5.38_typescript@5.9.3_/node_modules/@nuxt/devtools/dist/runtime/plugins/devtools.client.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/plugins/warn.dev.server.js")> &
InjectionType<typeof import("../../node_modules/.pnpm/nuxt@4.4.8_@babel+plugin-syntax-jsx@7.29.7_@babel+core@7.29.7__@babel+plugin-syntax-typ_25b4a4016600149c0331b87503aafc6b/node_modules/nuxt/dist/app/plugins/check-if-layout-used.js")> &
InjectionType<typeof import("../../app/plugins/auth.client")>
declare module '#app' {
interface NuxtApp extends NuxtAppInjections { }
interface NuxtAppLiterals {
pluginName: 'nuxt:revive-payload:client' | 'nuxt:head' | 'nuxt:router' | 'nuxt:payload' | 'nuxt:revive-payload:server' | 'nuxt:chunk-reload' | 'pinia' | 'nuxt:global-components' | 'nuxt:prefetch'
pluginName: 'vue-devtools-client' | 'nuxt:revive-payload:client' | 'nuxt:head' | 'nuxt:router' | 'nuxt:browser-devtools-timing' | 'nuxt:payload' | 'nuxt:revive-payload:server' | 'nuxt:chunk-reload' | 'pinia' | 'nuxt:global-components' | 'nuxt:prefetch' | 'nuxt:checkIfPageUnused' | 'nuxt:checkIfLayoutUsed'
}
}

2
apps/admin/.nuxt/types/runtime-config.d.ts

@ -15,8 +15,6 @@ import { RuntimeConfig as UserRuntimeConfig, PublicRuntimeConfig as UserPublicRu
},
}
interface SharedPublicRuntimeConfig {
apiBase: string,
appTitle: string,
}
declare module '@nuxt/schema' {

15
apps/admin/node_modules/.cache/vite/client/deps/_metadata.json

@ -0,0 +1,15 @@
{
"hash": "35ac884a",
"configHash": "5e84f24f",
"lockfileHash": "ec044b5a",
"browserHash": "0dbf3739",
"optimized": {
"errx": {
"src": "../../../../.pnpm/errx@0.1.0/node_modules/errx/dist/index.js",
"file": "errx.js",
"fileHash": "54268792",
"needsInterop": false
}
},
"chunks": {}
}

51
apps/admin/node_modules/.cache/vite/client/deps/errx.js

@ -0,0 +1,51 @@
// node_modules/.pnpm/errx@0.1.0/node_modules/errx/dist/index.js
var IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[a-z]:[/\\]/i;
var LINE_RE = /^\s+at (?:(?<function>[^)]+) \()?(?<source>[^)]+)\)?$/u;
var SOURCE_RE = /^(?<source>.+):(?<line>\d+):(?<column>\d+)$/u;
function captureRawStackTrace() {
if (!Error.captureStackTrace) {
return;
}
const stack = new Error();
Error.captureStackTrace(stack);
return stack.stack;
}
function captureStackTrace() {
const stack = captureRawStackTrace();
return stack ? parseRawStackTrace(stack) : [];
}
function parseRawStackTrace(stacktrace) {
const trace = [];
for (const line of stacktrace.split("\n")) {
const parsed = LINE_RE.exec(line)?.groups;
if (!parsed) {
continue;
}
if (!parsed.source) {
continue;
}
const parsedSource = SOURCE_RE.exec(parsed.source)?.groups;
if (parsedSource) {
Object.assign(parsed, parsedSource);
}
if (IS_ABSOLUTE_RE.test(parsed.source)) {
parsed.source = `file://${parsed.source}`;
}
if (parsed.source === import.meta.url) {
continue;
}
for (const key of ["line", "column"]) {
if (parsed[key]) {
parsed[key] = Number(parsed[key]);
}
}
trace.push(parsed);
}
return trace;
}
export {
captureRawStackTrace,
captureStackTrace,
parseRawStackTrace
};
//# sourceMappingURL=errx.js.map

7
apps/admin/node_modules/.cache/vite/client/deps/errx.js.map

@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../../../.pnpm/errx@0.1.0/node_modules/errx/dist/index.js"],
"sourcesContent": ["const IS_ABSOLUTE_RE = /^[/\\\\](?![/\\\\])|^[/\\\\]{2}(?!\\.)|^[a-z]:[/\\\\]/i;\nconst LINE_RE = /^\\s+at (?:(?<function>[^)]+) \\()?(?<source>[^)]+)\\)?$/u;\nconst SOURCE_RE = /^(?<source>.+):(?<line>\\d+):(?<column>\\d+)$/u;\nfunction captureRawStackTrace() {\n if (!Error.captureStackTrace) {\n return;\n }\n const stack = new Error();\n Error.captureStackTrace(stack);\n return stack.stack;\n}\nfunction captureStackTrace() {\n const stack = captureRawStackTrace();\n return stack ? parseRawStackTrace(stack) : [];\n}\nfunction parseRawStackTrace(stacktrace) {\n const trace = [];\n for (const line of stacktrace.split(\"\\n\")) {\n const parsed = LINE_RE.exec(line)?.groups;\n if (!parsed) {\n continue;\n }\n if (!parsed.source) {\n continue;\n }\n const parsedSource = SOURCE_RE.exec(parsed.source)?.groups;\n if (parsedSource) {\n Object.assign(parsed, parsedSource);\n }\n if (IS_ABSOLUTE_RE.test(parsed.source)) {\n parsed.source = `file://${parsed.source}`;\n }\n if (parsed.source === import.meta.url) {\n continue;\n }\n for (const key of [\"line\", \"column\"]) {\n if (parsed[key]) {\n parsed[key] = Number(parsed[key]);\n }\n }\n trace.push(parsed);\n }\n return trace;\n}\n\nexport { captureRawStackTrace, captureStackTrace, parseRawStackTrace };\n"],
"mappings": ";AAAA,IAAM,iBAAiB;AACvB,IAAM,UAAU;AAChB,IAAM,YAAY;AAClB,SAAS,uBAAuB;AAC9B,MAAI,CAAC,MAAM,mBAAmB;AAC5B;AAAA,EACF;AACA,QAAM,QAAQ,IAAI,MAAM;AACxB,QAAM,kBAAkB,KAAK;AAC7B,SAAO,MAAM;AACf;AACA,SAAS,oBAAoB;AAC3B,QAAM,QAAQ,qBAAqB;AACnC,SAAO,QAAQ,mBAAmB,KAAK,IAAI,CAAC;AAC9C;AACA,SAAS,mBAAmB,YAAY;AACtC,QAAM,QAAQ,CAAC;AACf,aAAW,QAAQ,WAAW,MAAM,IAAI,GAAG;AACzC,UAAM,SAAS,QAAQ,KAAK,IAAI,GAAG;AACnC,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AACA,QAAI,CAAC,OAAO,QAAQ;AAClB;AAAA,IACF;AACA,UAAM,eAAe,UAAU,KAAK,OAAO,MAAM,GAAG;AACpD,QAAI,cAAc;AAChB,aAAO,OAAO,QAAQ,YAAY;AAAA,IACpC;AACA,QAAI,eAAe,KAAK,OAAO,MAAM,GAAG;AACtC,aAAO,SAAS,UAAU,OAAO,MAAM;AAAA,IACzC;AACA,QAAI,OAAO,WAAW,YAAY,KAAK;AACrC;AAAA,IACF;AACA,eAAW,OAAO,CAAC,QAAQ,QAAQ,GAAG;AACpC,UAAI,OAAO,GAAG,GAAG;AACf,eAAO,GAAG,IAAI,OAAO,OAAO,GAAG,CAAC;AAAA,MAClC;AAAA,IACF;AACA,UAAM,KAAK,MAAM;AAAA,EACnB;AACA,SAAO;AACT;",
"names": []
}

3
apps/admin/node_modules/.cache/vite/client/deps/package.json

@ -0,0 +1,3 @@
{
"type": "module"
}

BIN
apps/services/dump.rdb

Binary file not shown.

13
apps/services/log/console.log

@ -0,0 +1,13 @@
2026/06/28 10:29:27.276 INFO sys log Init success !
2026/06/28 10:29:27.276 INFO sys event Init success !
2026/06/28 10:29:28.801 INFO console: sys.postgres(主库)初始化成功
2026/06/28 10:29:28.802 INFO console: sys.redis 初始化成功
2026/06/28 10:29:28.803 INFO console: sys.nats 初始化成功
2026/06/28 10:29:28.803 INFO console: sys.cos 初始化成功
2026/06/28 10:29:28.803 INFO 服务[console-d908e1s4kvnl2senkasg] 初始化完成!
2026/06/28 10:29:28.803 INFO 服务[console-d908e1s4kvnl2senkasg:1.0.0.0] 启动完成!
2026/06/28 10:29:33.457 INFO console.stat: 已创建 JetStream 流 STATS(subject=stats.event)
2026/06/28 10:29:33.457 INFO console.stat: 订阅前 JetStream 流 STATS 当前消息数=0 subjects=[stats.event]
2026/06/28 10:29:33.458 INFO console.stat: 统计快照消费已启动 subject=stats.event stream=STATS durable=console-stats
2026/06/28 10:29:33.458 INFO console: HTTP 服务监听 :8080(静态目录 ./consoleweb)
2026/06/28 10:29:33.458 DEBUG 启动模块【console】

11
apps/services/modules/echomeet/core.go

@ -155,3 +155,14 @@ func isByteDanceSupported(bcp47 string) bool {
_, ok := bcp47ToByteDance[bcp47]
return ok
}
// baseLang 取 BCP-47 的基础语言子标签(首个 '-' 之前),如 es-MX→es、zh-TW→zh。
// 无区域后缀(如 "es")或空串则原样返回。仅用于模板查找的区域回退,不参与各平台语言码映射。
func baseLang(bcp47 string) string {
for i := 0; i < len(bcp47); i++ {
if bcp47[i] == '-' {
return bcp47[:i]
}
}
return bcp47
}

55
apps/services/modules/echomeet/model.go

@ -45,11 +45,44 @@ func (this *modelComp) getcommtemplates(language string) (models []*pb.DBEchoMee
models = make([]*pb.DBEchoMeetTemplate, 0)
if language == "" {
err = postgres.Find(comm.TableEchomeetTemplate, &models, "source = ?", "public")
} else {
err = postgres.Find(comm.TableEchomeetTemplate, &models, "source = ? AND language = ?", "public", language)
return
}
if err = postgres.Find(comm.TableEchomeetTemplate, &models, "source = ? AND language = ?", "public", language); err != nil {
return
}
if len(models) > 0 {
return // 精确命中
}
// 回退:该区域 locale 无专属模板时,取同基础语言变体(裸 base 或区域变体,如 es-MX→es/es-ES),
// 按 tid 去重保留 id 最小一条,避免同一模板出现多条。
if base := baseLang(language); base != "" && base != language {
var variants []*pb.DBEchoMeetTemplate
if err = postgres.Find(comm.TableEchomeetTemplate, &variants, "source = ? AND (language = ? OR language LIKE ?)", "public", base, base+"-%"); err != nil {
return
}
models = pickBaseLangVariants(variants, base)
}
return
}
// pickBaseLangVariants 从候选模板里挑出与 base 同基础语言的,按 tid 去重保留 id 最小一条。
// 用于区域 locale(如 es-MX)无专属模板时回退到同基础语言那套(es / es-ES)做列表,避免同 tid 重复。
func pickBaseLangVariants(candidates []*pb.DBEchoMeetTemplate, base string) []*pb.DBEchoMeetTemplate {
byTid := make(map[string]*pb.DBEchoMeetTemplate)
for _, m := range candidates {
if baseLang(m.Language) != base {
continue
}
if cur, ok := byTid[m.Tid]; !ok || m.Id < cur.Id {
byTid[m.Tid] = m
}
}
out := make([]*pb.DBEchoMeetTemplate, 0, len(byTid))
for _, m := range byTid {
out = append(out, m)
}
return out
}
// 读取模版
func (this *modelComp) gettemplates(uid string, language string) (models []*pb.DBEchoMeetTemplate, err error) {
@ -86,14 +119,24 @@ func (this *modelComp) resolvetemplate(tid string, templateid uint64, language s
return this.gettemplateforid(templateid)
}
// gettemplatefortid 按 tid + language 查找模板(用于公共模板)
// gettemplatefortid 按 tid + language 查找模板(用于公共模板)。
// 精确匹配不到时,回退到同一基础语言的任一变体(如 es-MX→es-ES、zh-TW→zh-CN),
// 避免区域 locale 没有专属模板时解析失败;FindOne→gorm First 默认按主键 id 升序,取最小一条保证结果稳定。
func (this *modelComp) gettemplatefortid(tid string, language string) (template *pb.DBEchoMeetTemplate, err error) {
template = &pb.DBEchoMeetTemplate{}
err = postgres.FindOne(comm.TableEchomeetTemplate, template, "tid=? AND language=?", tid, language)
if err == nil {
if err = postgres.FindOne(comm.TableEchomeetTemplate, template, "tid=? AND language=?", tid, language); err == nil {
return
}
err = mysql.FindOne(comm.TableEchomeetTemplate, template, "tid=? AND language=?", tid, language)
if err = mysql.FindOne(comm.TableEchomeetTemplate, template, "tid=? AND language=?", tid, language); err == nil {
return
}
// 回退:同基础语言(裸 base 或其任一区域变体,如 es / es-ES 都能兜底 es-MX)
if base := baseLang(language); base != "" && base != language {
if err = postgres.FindOne(comm.TableEchomeetTemplate, template, "tid=? AND (language=? OR language LIKE ?)", tid, base, base+"-%"); err == nil {
return
}
err = mysql.FindOne(comm.TableEchomeetTemplate, template, "tid=? AND (language=? OR language LIKE ?)", tid, base, base+"-%")
}
return
}

30
apps/services/modules/echomeet/model_cache.go

@ -116,6 +116,13 @@ func (this *modelCacheComp) GetCommTemplates(language string) (models []*pb.DBEc
models = append(models, m)
}
}
if len(models) > 0 || language == "" {
return // 精确命中(或不限语言)
}
// 回退:该区域 locale 无专属模板时,取同基础语言变体(如 es-MX→es/es-ES),按 tid 去重保留 id 最小一条
if base := baseLang(language); base != "" && base != language {
models = pickBaseLangVariants(list, base)
}
return
}
@ -127,13 +134,28 @@ func (this *modelCacheComp) GetTemplateForId(id uint64) (template *pb.DBEchoMeet
return this.module.model.gettemplateforid(id)
}
// GetTemplateForTid 按 tid+language 读取模板:先扫 public 缓存命中即返回,否则回退 DB
// GetTemplateForTid 按 tid+language 读取模板:先扫 public 缓存命中即返回,否则回退 DB。
// 精确匹配不到时,回退到同一基础语言的任一变体(如 es-MX→es-ES、zh-TW→zh-CN),
// 避免区域 locale 没有专属模板时解析失败;候选取 id 最小的一条保证结果稳定。
func (this *modelCacheComp) GetTemplateForTid(tid string, language string) (template *pb.DBEchoMeetTemplate, err error) {
if list, cerr := cache.GetAll[pb.DBEchoMeetTemplate](context.Background(), comm.Cache_EchomeetTemplate); cerr == nil {
if list, cerr := cache.GetAll[pb.DBEchoMeetTemplate](context.Background(), comm.Cache_EchomeetTemplate); cerr == nil && len(list) > 0 {
base := baseLang(language)
var fallback *pb.DBEchoMeetTemplate
for _, m := range list {
if m.Tid == tid && m.Language == language {
return m, nil
if m.Tid != tid {
continue
}
if m.Language == language {
return m, nil // 精确命中
}
if base != "" && base != language && baseLang(m.Language) == base {
if fallback == nil || m.Id < fallback.Id {
fallback = m
}
}
}
if fallback != nil {
return fallback, nil
}
}
return this.module.model.gettemplatefortid(tid, language)

74
deploy/console/README.md

@ -0,0 +1,74 @@
# console 管理后台部署
把单服务 **console**(lego single 服务,维护 app_registry、跨应用统计)打成镜像 `console-a11`,
推送到私有仓库 `docker-registry.ideapsound.com`,再到目标机拉取重启。依赖 PostgreSQL / Redis / NATS
由目标环境共享提供(在 `.env` 里填地址/容器名),不在此 compose 内搭建。
> 约定(与 voitrans 一致):仓库**只提交模板**(`*.example`)。部署脚本会把**模板 + `docker-compose.yml`** 同步到服务器,
> 但**绝不上传/覆盖**你在服务器上填好的真实 `.env` / `confs/console.yaml`(首次需登录服务器照模板复制并填值)。
## 文件
| 文件 | 说明 |
|---|---|
| `../../apps/services/Dockerfile.console` | 镜像构建(编译 console 二进制 + entrypoint + 内置 consoleweb 静态),构建上下文 = `apps/services/` |
| `../../apps/services/entrypoint.console.sh` | 容器入口 |
| `docker-compose.yml` | 运行镜像 `console-a11`,挂 `./confs`、`./log`,`.env` 注入容器(此文件由你放到服务器) |
| `confs/console.yaml.example` | console 配置**模板**;真实 `console.yaml` 被 gitignore,密钥/端点用 `${VAR}` 由 `.env` 注入 |
| `env/env.example` | 运行时环境**模板**(只一份,不分环境);真实 `.env` 被 gitignore |
| `dev-deploy.sh` | **开发**:构建 dev 镜像 → 推送 → 远端拉取重启(固定 tag `dev-latest`) |
| `prod-build.sh` | **生产①**:构建带版本号镜像并推送(`:版本` + `:latest`),不部署 |
| `prod-deploy.sh` | **生产②**:部署指定版本到生产机(单机,无区域),支持回滚 |
## 用法
### 本地(local)
本机直接起容器(用本目录的 `.env` 与 `confs/`,自行从 `*.example` 复制填写):
```bash
cd deploy/console
cp env/env.example .env # 填真实值
cp confs/console.yaml.example confs/console.yaml
docker compose --env-file .env up -d # 本机起 console-a11
```
### 服务器准备(dev / prod,一次性)
脚本每次会同步 `docker-compose.yml` + `*.example` 模板到目标机。**首次**需登录服务器把模板复制成真实文件并填值
(`cp env.example .env`、`cp confs/console.yaml.example confs/console.yaml`,dev-deploy 缺真实文件时也会打印这两条命令):
```
<部署目录>/ # dev 默认 /home/work/console-a11,prod 默认 /opt/yunyan/console
├── docker-compose.yml # 从本目录拷贝
├── .env # 照 env/env.example 填(POSTGRES_DSN/REDIS_*/NATS_URL/
│ # CONSOLE_TOKEN_KEY/CONSOLE_ADMIN_*/FIELD_ENCRYPT_KEY 等)
├── confs/console.yaml # 照 confs/console.yaml.example 填
└── log/ # 日志目录
```
### 部署
```bash
cd deploy/console
./dev-deploy.sh # 开发:构建 dev-latest → 推送 → 远端拉取重启
./prod-build.sh 0.1.1 # 生产①:构建并推送 console-a11:0.1.1 (+latest)
./prod-deploy.sh 0.1.1 # 生产②:部署 0.1.1 到生产机
./prod-deploy.sh rollback # 生产回滚到上一版本(与当前版本来回切换)
```
> 改了服务器上的 `.env` 后,必须经 `up`(脚本里的 `docker compose up -d`)**重建容器**才生效,`restart` 不会重读环境变量。
> 生产版本/回滚记录存服务器上的 `.deploy_tag` / `.deploy_tag.prev`,不动你维护的 `.env`。
## 需要填写 / 提供的东西
- **部署目标与凭据**:写死在各脚本顶部。`dev-deploy.sh` 的 `DEV_HOST`、`prod-deploy.sh` 的 `DEPLOY_HOST`;
仓库账号 `admin` / 密码已写死;`DEPLOY_KEY` 默认 `~/.ssh/id_rsa`。
- ⚠️ 本目录脚本纳入 git,**公开仓库前务必清空** `*_HOST` 与 `REGISTRY_PASS`。
- **依赖端点**:填服务器 `.env` 的 `POSTGRES_DSN / REDIS_* / NATS_URL`,以及 `CONSOLE_TOKEN_KEY`、
超管账号 `CONSOLE_ADMIN_ACCOUNT/PASSWORD`、字段加密 `FIELD_ENCRYPT_KEY`、站点名 `CONSOLE_SITE_NAME`。
## 端口
console HTTP `8080`(容器内,对外经反代/NPM 暴露)。依赖的 `postgres / redis / nats` 须可达
(同 `DOCKER_NETWORK`,默认 `1panel-network`,用容器名,或在 `.env` 填外部地址)。

8
deploy/console/confs/console.yaml → deploy/console/confs/console.yaml.example

@ -1,4 +1,4 @@
# console 服务配置(部署环境,敏感值通过 .env 注入)
# console 服务配置模板(真实 console.yaml 被 gitignore;敏感值通过 .env 的 ${VAR} 注入)
type: "console"
version: 1.0
@ -38,11 +38,11 @@ modules:
HTTP:
Addr: ":8080"
StaticDir: "./consoleweb"
TokenKey: "${CONSOLE_TOKEN_KEY:-console-secret-change-me}"
TokenKey: "${CONSOLE_TOKEN_KEY:-}"
AdminAccount: "${CONSOLE_ADMIN_ACCOUNT:-admin}"
AdminPassword: "${CONSOLE_ADMIN_PASSWORD:-123456}"
AdminPassword: "${CONSOLE_ADMIN_PASSWORD:-}"
SiteName: "${CONSOLE_SITE_NAME:-云言 SaaS 管理平台}"
EncryptKey: "${FIELD_ENCRYPT_KEY:-console-default-encrypt-key-xxxxx!}"
EncryptKey: "${FIELD_ENCRYPT_KEY:-}"
Stat:
Subject: "stats.event"
Stream: "STATS"

184
deploy/console/deploy.sh

@ -1,184 +0,0 @@
#!/usr/bin/env bash
# <服务> 部署脚本(每个服务目录各一份,服务名取自所在目录名)
#
# 用法: ./deploy.sh <env> [action] (在 deploy/<服务>/ 下运行)
# env : local | dev | prod
# action : dev/prod → deploy(默认) | up | build | pull | down | restart | logs | ps
# local → up(默认) | build | pull | down | restart | logs | ps
#
# deploy = 构建推送镜像 + 同步配置 + 远程 pull + up(一键发布,仅 dev/prod)
# up = 远程:仅同步配置 + 远程 pull + up(用已推送镜像);本机:起容器
#
# 例(在 deploy/console/ 下):
# ./deploy.sh prod # 一键发布生产 console
# ./deploy.sh prod logs # 看生产 console 日志
# ./deploy.sh local up # 本机起 console
# ./deploy.sh dev build # 只构建推送 dev 镜像
set -euo pipefail
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # 本服务部署目录(deploy/<服务>)
SERVICE="$(basename "$SVC_DIR")" # 服务名 = 目录名(console / admin)
REPO_ROOT="$(cd "$SVC_DIR/../.." && pwd)" # 仓库根(deploy/<服务>/../..)
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
red() { printf "\033[31m%s\033[0m\n" "$*"; }
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ 部署目标 / 私有仓库配置 —— 写死在这里,改这里即可(按环境) ║
# ║ console、admin 同机不同目录;DEPLOY_DIR 自动 = /opt/yunyan/<服务名> ║
# ║ 注意:本文件纳入 git,下面的密码/IP 会随仓库提交(私有仓库内可接受) ║
# ╚══════════════════════════════════════════════════════════════════════╝
deploy_profile() {
DEPLOY_HOST=""; REGISTRY_PASS="" # 占位,local 用不到;dev/prod 在下面填
# —— 各环境通用默认 ——
REGISTRY=docker-registry.ideapsound.com # 私有镜像仓库地址
REGISTRY_USER=admin # 仓库账号
DEPLOY_PORT=22 # 服务器 SSH 端口
DEPLOY_USER=root # 服务器 SSH 用户
DEPLOY_KEY=~/.ssh/id_rsa # 本机访问服务器的 SSH 私钥路径
case "$ENV_NAME" in
dev)
DEPLOY_HOST="" # ← 开发服务器 IP(必填)
REGISTRY_PASS="" # ← 私有仓库密码(必填)
;;
prod)
DEPLOY_HOST="" # ← 生产服务器 IP(必填)
REGISTRY_PASS="" # ← 私有仓库密码(必填)
;;
local) ;; # 本机不走 SSH,无需填
esac
DEPLOY_DIR="/opt/yunyan/$SERVICE" # 远程部署目录(自动按服务名)
}
usage() {
cat >&2 <<EOF
用法: ./deploy.sh <env> [action] (在 deploy/$SERVICE/ 下运行)
env : local | dev | prod
action : dev/prod → deploy(默认) | up | build | pull | down | restart | logs | ps
local → up(默认) | build | pull | down | restart | logs | ps
例:
./deploy.sh prod # 一键发布生产 $SERVICE
./deploy.sh prod logs # 看生产 $SERVICE 日志
./deploy.sh local up # 本机起 $SERVICE
./deploy.sh dev build # 只构建推送 dev 镜像
EOF
exit 1
}
# ── 参数解析与校验 ──
ENV_NAME="${1:-}"; ACTION="${2:-}"
[ -n "$ENV_NAME" ] || usage
[ -f "$SVC_DIR/docker-compose.yml" ] || { red "✗ $SERVICE 目录缺 docker-compose.yml(deploy.sh 须放在服务目录内)"; exit 1; }
case "$ENV_NAME" in
local|dev|prod) ;;
*) red "✗ 未知环境: ${ENV_NAME}(应为 local|dev|prod)"; usage ;;
esac
# 默认动作:远程发布默认 deploy,本机默认 up
[ -n "$ACTION" ] || { [ "$ENV_NAME" = local ] && ACTION=up || ACTION=deploy; }
# 从 ENV_FILE 安全读取单个变量值(去除可能的外层双引号)
_envget() {
{ grep -E "^$1=" "$ENV_FILE" || true; } | head -1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//'
}
# 载入 env/<环境>.env —— 现在只放容器运行时配置(DSN / Redis / NATS / 密钥 / 端口 / TAG / 网络)。
# 部署目标与仓库凭据已写死在 deploy_profile,不再放 env 文件。脚本只需从这里取 compose 仍要的 TAG/网络。
load_env() {
ENV_FILE="$SVC_DIR/env/$ENV_NAME.env"
if [ ! -f "$ENV_FILE" ]; then
red "✗ 缺少 $SERVICE/env/$ENV_NAME.env —— 请先从 $SERVICE/env/$ENV_NAME.env.example 复制并填写"
exit 1
fi
TAG="$(_envget TAG)"; TAG="${TAG:-latest}"
DOCKER_NETWORK="$(_envget DOCKER_NETWORK)"; DOCKER_NETWORK="${DOCKER_NETWORK:-1panel-network}"
}
# 生成 compose 用 .env = 服务运行时 env 文件 + 注入写死的 REGISTRY(镜像地址替换用)
# 先滤掉源文件里可能残留的 REGISTRY 行,保证唯一、不依赖 compose 的覆盖顺序。
gen_env() {
{ grep -v '^REGISTRY=' "$ENV_FILE" || true; } > "$SVC_DIR/.env"
printf 'REGISTRY=%s\n' "$REGISTRY" >> "$SVC_DIR/.env"
}
# 构建并推送本服务镜像(复用仓库根目录 build.sh,目标 = 服务名)。
# local 环境只构建不推送(PUSH=0),dev/prod 构建并推送到私有仓库。
build_push() {
local push=1; [ "$ENV_NAME" = local ] && push=0
blue ">>> 构建$([ "$push" = 1 ] && echo 推送) $SERVICE 镜像 (TAG=${TAG:-latest}, PUSH=$push)"
TAG="${TAG:-latest}" REGISTRY="$REGISTRY" PUSH="$push" "$REPO_ROOT/build.sh" "$SERVICE"
}
ensure_network_local() {
docker network inspect "$DOCKER_NETWORK" >/dev/null 2>&1 || docker network create "$DOCKER_NETWORK"
}
# 本机 docker compose(gen_env 落为同目录 .env,既做变量替换又做容器 env_file)
compose_local() {
gen_env
ensure_network_local
( cd "$SVC_DIR" && docker compose --env-file .env "$@" )
}
# ── 远程 SSH 基础 ──
_ssh() { ssh -p "${DEPLOY_PORT:-22}" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "${DEPLOY_USER:-root}@${DEPLOY_HOST}" "$@"; }
_scp() { scp -P "${DEPLOY_PORT:-22}" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; }
_dest() { echo "${DEPLOY_USER:-root}@${DEPLOY_HOST}:$1"; }
# 远程发布:同步配置 + 远程 login + pull + up
remote_deploy() {
: "${DEPLOY_HOST:?请在 deploy.sh 顶部 deploy_profile 的 $ENV_NAME 段填 DEPLOY_HOST}"
gen_env
blue ">>> 同步 $SERVICE 配置到 ${DEPLOY_USER:-root}@${DEPLOY_HOST}:${DEPLOY_DIR}"
_ssh "mkdir -p '$DEPLOY_DIR'"
_scp "$SVC_DIR/docker-compose.yml" "$(_dest "$DEPLOY_DIR/docker-compose.yml")"
_scp "$SVC_DIR/.env" "$(_dest "$DEPLOY_DIR/.env")"
# 带 confs/ 的服务(console)一并同步配置并预建 log 目录
if [ -d "$SVC_DIR/confs" ]; then
_ssh "mkdir -p '$DEPLOY_DIR/confs' '$DEPLOY_DIR/log'"
_scp -r "$SVC_DIR/confs/." "$(_dest "$DEPLOY_DIR/confs/")"
fi
blue ">>> 远程拉起 $SERVICE"
[ -n "${REGISTRY_PASS:-}" ] && \
_ssh "docker login '$REGISTRY' -u '${REGISTRY_USER:-admin}' -p '$REGISTRY_PASS'"
_ssh "docker network inspect '$DOCKER_NETWORK' >/dev/null 2>&1 || docker network create '$DOCKER_NETWORK'"
_ssh "cd '$DEPLOY_DIR' && docker compose --env-file .env pull && docker compose --env-file .env up -d"
}
# 远程运维(down/restart/logs/ps/pull)
remote_compose() {
: "${DEPLOY_HOST:?请在 deploy.sh 顶部 deploy_profile 的 $ENV_NAME 段填 DEPLOY_HOST}"
_ssh "cd '$DEPLOY_DIR' && docker compose --env-file .env $*"
}
# ── 分发 ──
deploy_profile # 写死的部署目标 / 仓库凭据
load_env # env 文件里的运行时配置(TAG / 网络)
if [ "$ENV_NAME" = local ]; then
case "$ACTION" in
up) compose_local up -d ;;
down) compose_local down ;;
restart) compose_local restart ;;
logs) compose_local logs -f --tail=120 ;;
ps) compose_local ps ;;
pull) compose_local pull ;;
build) build_push ;;
*) red "未知操作: ${ACTION}(local 支持 up|down|restart|logs|ps|pull|build)"; exit 1 ;;
esac
else
case "$ACTION" in
deploy) build_push; remote_deploy ;;
up) remote_deploy ;;
build) build_push ;;
pull) remote_compose pull ;;
down) remote_compose down ;;
restart) remote_compose restart ;;
ps) remote_compose ps ;;
logs) remote_compose logs -f --tail=120 ;;
*) red "未知操作: ${ACTION}(dev/prod 支持 deploy|up|build|pull|down|restart|logs|ps)"; exit 1 ;;
esac
fi
green "✓ $SERVICE $ENV_NAME $ACTION 完成"

76
deploy/console/dev-deploy.sh

@ -0,0 +1,76 @@
#!/usr/bin/env bash
# console 开发环境:构建开发镜像 → 推送 → 远端拉取重启。
#
# 单服务 console 打成镜像 console-a11,推到私有仓库后远端拉取重启。
# 仅做镜像部署:服务器上的 docker-compose.yml / .env / confs/ 由你自行维护
# (照着仓库里的 env/env.example、confs/console.yaml.example 在服务器上放好真实文件),本脚本不上传任何配置。
#
# 用法(在 deploy/console/ 下):
# ./dev-deploy.sh # 构建并发布 console-a11:dev-latest
#
# 开发版镜像始终使用固定 tag dev-latest,每次构建覆盖同一 tag。
set -euo pipefail
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # deploy/console
REPO_ROOT="$(cd "$SVC_DIR/../.." && pwd)" # 仓库根
BUILD_CONTEXT="$REPO_ROOT/apps/services" # Go 构建上下文
DOCKERFILE="$BUILD_CONTEXT/Dockerfile.console"
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
red() { printf "\033[31m%s\033[0m\n" "$*"; }
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ 开发部署目标 / 私有仓库凭据 —— 写死在此(本文件纳入 git,私有仓库内可接受) ║
# ║ ⚠️ 公开仓库前务必清空 DEV_HOST / REGISTRY_PASS ║
# ╚══════════════════════════════════════════════════════════════════════╝
IMAGE_NAME="console-a11" # 业务镜像名
REGISTRY="docker-registry.ideapsound.com" # 私有镜像仓库
REGISTRY_USER="admin" # 仓库账号
REGISTRY_PASS="li13451234" # 仓库密码
DEV_HOST="124.222.232.218" # ← 开发服务器 IP(必填)
DEV_PORT=22
DEV_USER=root
DEV_KEY=/Users/liwei/Documents/key/earphone.pem # 本机访问开发服务器的 SSH 私钥
DEV_DIR="/home/work/console-a11" # 远端部署目录(其中 docker-compose.yml/.env/confs 由你自己维护)
PLATFORM="${PLATFORM:-linux/amd64}"
TAG="dev-latest" # 开发版固定 tag,每次构建覆盖同一 dev-latest
FULL_IMAGE="$REGISTRY/$IMAGE_NAME:$TAG"
[ -f "$DOCKERFILE" ] || { red "✗ 缺少 $DOCKERFILE"; exit 1; }
: "${DEV_HOST:?请在 dev-deploy.sh 顶部填 DEV_HOST(开发服务器 IP)}"
# ── 远程 SSH ──
_ssh() { ssh -p "$DEV_PORT" -i "${DEV_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$DEV_USER@$DEV_HOST" "$@"; }
_scp() { scp -P "$DEV_PORT" -i "${DEV_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; }
_dest() { echo "$DEV_USER@$DEV_HOST:$1"; }
# 1) 构建并推送镜像
blue ">>> 构建镜像 $FULL_IMAGE (context=$BUILD_CONTEXT)"
echo "$REGISTRY_PASS" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
docker build --network=host --platform "$PLATFORM" -f "$DOCKERFILE" -t "$FULL_IMAGE" "$BUILD_CONTEXT"
docker push "$FULL_IMAGE"
# 2) 同步「模板 + compose」到服务器(只传模板,绝不覆盖你维护的真实 .env / confs/console.yaml)
blue ">>> 同步模板与 compose → $DEV_USER@$DEV_HOST:$DEV_DIR"
_ssh "mkdir -p '$DEV_DIR/confs' '$DEV_DIR/log'"
_scp "$SVC_DIR/docker-compose.yml" "$(_dest "$DEV_DIR/docker-compose.yml")"
_scp "$SVC_DIR/confs/console.yaml.example" "$(_dest "$DEV_DIR/confs/console.yaml.example")"
_scp "$SVC_DIR/env/env.example" "$(_dest "$DEV_DIR/env.example")"
# 3) 远端拉取并重启(用你在服务器维护的真实 .env / confs/console.yaml)
blue ">>> 远端拉取并重启"
_ssh "echo '$REGISTRY_PASS' | docker login '$REGISTRY' -u '$REGISTRY_USER' --password-stdin >/dev/null"
_ssh "docker network inspect 1panel-network >/dev/null 2>&1 || docker network create 1panel-network"
if ! _ssh "test -f '$DEV_DIR/.env' && test -f '$DEV_DIR/confs/console.yaml'"; then
red "⚠ 服务器缺少真实 .env 或 confs/console.yaml —— 模板已同步,请登录服务器按模板填好后重跑本脚本:"
echo " ssh -i $DEV_KEY $DEV_USER@$DEV_HOST"
echo " cd $DEV_DIR && cp env.example .env && cp confs/console.yaml.example confs/console.yaml"
echo " # 编辑 .env 与 confs/console.yaml 填真实值,然后重跑 ./dev-deploy.sh"
exit 0
fi
_ssh "cd '$DEV_DIR' && REGISTRY='$REGISTRY' TAG='$TAG' docker compose --env-file .env pull && REGISTRY='$REGISTRY' TAG='$TAG' docker compose --env-file .env up -d && docker image prune -f"
green "✓ console dev 已发布: $FULL_IMAGE → $DEV_HOST:$DEV_DIR"

6
deploy/console/docker-compose.yml

@ -7,9 +7,9 @@
# 它会把 env/<环境>.env 落为同目录 .env(既做变量替换又做容器 env_file)。
services:
yunyan-console:
image: ${REGISTRY:-docker-registry.ideapsound.com}/yunyan-console:${TAG:-latest}
container_name: yunyan-console
console-a11:
image: ${REGISTRY:-docker-registry.ideapsound.com}/console-a11:${TAG:-latest}
container_name: console-a11
restart: unless-stopped
platform: ${PLATFORM:-linux/amd64}

26
deploy/console/env/dev.env.example

@ -1,26 +0,0 @@
# console 服务 · dev 开发环境(复制为 dev.env 后填写真实值)
# ── 镜像(仓库地址 REGISTRY 已写死在 deploy.sh,这里不填)──
TAG=dev
PLATFORM=linux/amd64
DOCKER_NETWORK=yunyan-net
# ── 依赖服务(目标环境共享提供,填地址;密码特殊字符 URL 编码 & → %26)──
POSTGRES_DSN=postgresql://user:password@host:5432/dbname?sslmode=require
REDIS_ADDR=redis:6379
REDIS_PASSWORD=
NATS_URL=nats://nats:4222
# ── COS(可选)──
COS_SECRET_ID=
COS_SECRET_KEY=
# ── Console 服务配置 ──
CONSOLE_TOKEN_KEY=dev-change-me
CONSOLE_ADMIN_ACCOUNT=admin
CONSOLE_ADMIN_PASSWORD=change-me
CONSOLE_SITE_NAME=云言 SaaS 管理平台(开发)
FIELD_ENCRYPT_KEY=dev-change-me-to-random-key
# 部署目标(DEPLOY_HOST/USER/KEY/DIR)与仓库凭据(REGISTRY_USER/PASS)已写死在
# deploy.sh 顶部的 deploy_profile(dev 段),不在此文件填。

27
deploy/console/env/env.example

@ -0,0 +1,27 @@
# console 服务运行时环境模板(只一份,不分 local/dev/prod)。
# 用法:在目标机上复制为 .env 并填真实值;本地起则在本目录复制为 .env。
# 真实 .env 已 gitignore,勿提交。
# ── 镜像(仓库地址 REGISTRY 已写死在脚本里,这里不填)──
TAG=latest # 生产建议固定版本号(如 0.1.1)便于回滚;dev 用 dev-latest
PLATFORM=linux/amd64
DOCKER_NETWORK=1panel-network # compose 外部网络名,须与目标机一致
# ── 依赖服务(目标环境共享提供,填地址;密码特殊字符需 URL 编码 & → %26)──
POSTGRES_DSN=postgresql://user:password@host:5432/dbname?sslmode=require
REDIS_ADDR=redis:6379
REDIS_PASSWORD=
NATS_URL=nats://nats:4222
# ── COS(可选)──
COS_SECRET_ID=
COS_SECRET_KEY=
# ── Console 服务配置(密钥务必强随机)──
CONSOLE_TOKEN_KEY=
CONSOLE_ADMIN_ACCOUNT=admin
CONSOLE_ADMIN_PASSWORD=
CONSOLE_SITE_NAME=云言 SaaS 管理平台
FIELD_ENCRYPT_KEY=
# 部署目标(DEPLOY_HOST/USER/KEY/DIR)与仓库凭据(REGISTRY_USER/PASS)写死在 dev-deploy.sh / prod-deploy.sh 顶部,不在此填。

23
deploy/console/env/local.env.example

@ -1,23 +0,0 @@
# console 服务 · local 本机环境(复制为 local.env 后使用)
# ── 镜像(仓库地址 REGISTRY 已写死在 deploy.sh,这里不填)──
TAG=latest
PLATFORM=linux/amd64
DOCKER_NETWORK=1panel-network
# ── 依赖服务(本机/共享环境提供,仅填连接地址,不在本服务搭建)──
POSTGRES_DSN=postgresql://user:password@host:5432/dbname?sslmode=require
REDIS_ADDR=redis:6379
REDIS_PASSWORD=
NATS_URL=nats://nats:4222
# ── COS(可选)──
COS_SECRET_ID=
COS_SECRET_KEY=
# ── Console 服务配置 ──
CONSOLE_TOKEN_KEY=console-secret-change-me
CONSOLE_ADMIN_ACCOUNT=admin
CONSOLE_ADMIN_PASSWORD=change-me
CONSOLE_SITE_NAME=云言 SaaS 管理平台
FIELD_ENCRYPT_KEY=change-me-to-random-key

26
deploy/console/env/prod.env.example

@ -1,26 +0,0 @@
# console 服务 · prod 生产环境(复制为 prod.env 后填写真实值)
# ── 镜像(仓库地址 REGISTRY 已写死在 deploy.sh,这里不填)──
TAG=latest # 生产建议固定版本号,如 v1.2.0,便于回滚
PLATFORM=linux/amd64
DOCKER_NETWORK=yunyan-net
# ── 依赖服务(目标环境共享提供,填地址;密码特殊字符 URL 编码 & → %26)──
POSTGRES_DSN=postgresql://user:password@host:5432/dbname?sslmode=require
REDIS_ADDR=redis:6379
REDIS_PASSWORD=
NATS_URL=nats://nats:4222
# ── COS(可选)──
COS_SECRET_ID=
COS_SECRET_KEY=
# ── Console 服务配置(密钥务必强随机)──
CONSOLE_TOKEN_KEY=prod-change-me-to-strong-secret
CONSOLE_ADMIN_ACCOUNT=admin
CONSOLE_ADMIN_PASSWORD=change-me
CONSOLE_SITE_NAME=云言 SaaS 管理平台
FIELD_ENCRYPT_KEY=prod-change-me-to-strong-random-key
# 部署目标(DEPLOY_HOST/USER/KEY/DIR)与仓库凭据(REGISTRY_USER/PASS)已写死在
# deploy.sh 顶部的 deploy_profile(prod 段),不在此文件填。

44
deploy/console/prod-build.sh

@ -0,0 +1,44 @@
#!/usr/bin/env bash
# console 生产环境:仅「构建带版本号的镜像并推送」,不部署。
#
# 单服务 console 打成镜像 console-a11,同时推送 :<version> 与 :latest 两个 tag。
# 部署用 ./prod-deploy.sh <version>。
#
# 用法(在 deploy/console/ 下):
# ./prod-build.sh <version> # version 必须形如 X.Y.Z,如 0.1.1
set -euo pipefail
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SVC_DIR/../.." && pwd)"
BUILD_CONTEXT="$REPO_ROOT/apps/services"
DOCKERFILE="$BUILD_CONTEXT/Dockerfile.console"
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
red() { printf "\033[31m%s\033[0m\n" "$*"; }
# ── 私有仓库凭据(写死,公开仓库前清空 REGISTRY_PASS)──
IMAGE_NAME="console-a11"
REGISTRY="docker-registry.ideapsound.com"
REGISTRY_USER="admin"
REGISTRY_PASS="li13451234"
PLATFORM="${PLATFORM:-linux/amd64}"
VERSION="${1:-}"
[ -n "$VERSION" ] || { red "用法: ./prod-build.sh <version>(如 0.1.1)"; exit 1; }
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
red "✗ version 必须形如 0.1.1,当前: $VERSION"; exit 1
fi
[ -f "$DOCKERFILE" ] || { red "✗ 缺少 $DOCKERFILE"; exit 1; }
IMG_VER="$REGISTRY/$IMAGE_NAME:$VERSION"
IMG_LATEST="$REGISTRY/$IMAGE_NAME:latest"
blue ">>> 构建生产镜像 $IMG_VER (+latest) context=$BUILD_CONTEXT"
echo "$REGISTRY_PASS" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
docker build --network=host --platform "$PLATFORM" -f "$DOCKERFILE" -t "$IMG_VER" -t "$IMG_LATEST" "$BUILD_CONTEXT"
docker push "$IMG_VER"
docker push "$IMG_LATEST"
green "✓ 已推送 $IMG_VER 与 $IMG_LATEST"
echo " 部署:./prod-deploy.sh $VERSION"

87
deploy/console/prod-deploy.sh

@ -0,0 +1,87 @@
#!/usr/bin/env bash
# console 生产环境:部署指定镜像版本到生产机,支持回滚。console 为单机管理后台,无区域之分。
#
# 镜像由 ./prod-build.sh 预先构建推送;本脚本只做「远端拉取 + 重启」。
# 仅做镜像部署:服务器上的 docker-compose.yml / .env / confs/ 由你自行维护
# (照着仓库里的 env/env.example、confs/console.yaml.example 在服务器上放好真实文件),本脚本不上传任何配置。
#
# 用法(在 deploy/console/ 下):
# ./prod-deploy.sh <tag> # 部署指定版本,如 ./prod-deploy.sh 0.1.1
# ./prod-deploy.sh rollback # 回滚到上一次部署版本(与当前版本来回切换)
set -euo pipefail
SVC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
red() { printf "\033[31m%s\033[0m\n" "$*"; }
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ 生产部署目标 / 私有仓库凭据 —— 写死在此(本文件纳入 git)。公开仓库前清空 HOST/PASS。 ║
# ╚══════════════════════════════════════════════════════════════════════╝
IMAGE_NAME="console-a11"
REGISTRY="docker-registry.ideapsound.com"
REGISTRY_USER="admin"
REGISTRY_PASS="li13451234"
DOCKER_NETWORK="1panel-network" # compose 外部网络名(与服务器一致)
DEPLOY_HOST="" # ← 生产服务器 IP(必填)
DEPLOY_PORT=22
DEPLOY_USER=root
DEPLOY_KEY=~/.ssh/id_rsa
DEPLOY_DIR="/opt/yunyan/console" # 远端部署目录(其中 docker-compose.yml/.env/confs 由你自己维护)
# ── 参数 ──
ARG1="${1:-}"
[ -n "$ARG1" ] || { red "用法: ./prod-deploy.sh <tag|rollback>(如 0.1.1)"; exit 1; }
if [ "$ARG1" = "rollback" ]; then MODE="rollback"; NEW_TAG=""; else MODE="deploy"; NEW_TAG="$ARG1"; fi
: "${DEPLOY_HOST:?请在 prod-deploy.sh 顶部填 DEPLOY_HOST(生产服务器 IP)}"
_ssh() { ssh -p "$DEPLOY_PORT" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST" "$@"; }
_scp() { scp -P "$DEPLOY_PORT" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; }
_dest() { echo "$DEPLOY_USER@$DEPLOY_HOST:$1"; }
# 同步「模板 + compose」(只传模板,绝不覆盖你维护的真实 .env / confs/console.yaml)
blue ">>> 同步模板与 compose → $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR"
_ssh "mkdir -p '$DEPLOY_DIR/confs' '$DEPLOY_DIR/log'"
_scp "$SVC_DIR/docker-compose.yml" "$(_dest "$DEPLOY_DIR/docker-compose.yml")"
_scp "$SVC_DIR/confs/console.yaml.example" "$(_dest "$DEPLOY_DIR/confs/console.yaml.example")"
_scp "$SVC_DIR/env/env.example" "$(_dest "$DEPLOY_DIR/env.example")"
# 远端拉取并重启(不上传真实配置):tag 历史存服务器上独立的 .deploy_tag,不动你维护的 .env。
blue ">>> 远端部署($MODE${NEW_TAG:+ $NEW_TAG}) → $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR"
_ssh bash -s -- "$DEPLOY_DIR" "$MODE" "$NEW_TAG" "$REGISTRY/$IMAGE_NAME" "$REGISTRY" "$REGISTRY_USER" "$REGISTRY_PASS" "$DOCKER_NETWORK" <<'REMOTE'
set -e
DIR="$1"; MODE="$2"; NEW_TAG="$3"; IMG_PREFIX="$4"; REG="$5"; REG_USER="$6"; REG_PASS="$7"; NET="$8"
[ -f "$DIR/.env" ] || { echo "✗ 缺少 $DIR/.env(请照 env.example 在服务器填好真实 .env)"; exit 1; }
[ -f "$DIR/confs/console.yaml" ] || { echo "✗ 缺少 $DIR/confs/console.yaml(请照 confs/console.yaml.example 在服务器填好)"; exit 1; }
[ -f "$DIR/docker-compose.yml" ] || { echo "✗ 缺少 $DIR/docker-compose.yml"; exit 1; }
STATE="$DIR/.deploy_tag" # 当前部署 tag(独立文件,不动用户的 .env)
PREVF="$DIR/.deploy_tag.prev" # 上一次部署 tag
CUR_TAG="$([ -f "$STATE" ] && cat "$STATE" || true)"
CUR_PREV="$([ -f "$PREVF" ] && cat "$PREVF" || true)"
if [ "$MODE" = "rollback" ]; then
[ -n "$CUR_PREV" ] || { echo "没有上一次版本记录(.deploy_tag.prev),无法回滚"; exit 1; }
FINAL_TAG="$CUR_PREV"; FINAL_PREV="$CUR_TAG" # 与当前版本来回切换
else
FINAL_TAG="$NEW_TAG"
if [ -n "$CUR_TAG" ] && [ "$CUR_TAG" != "$NEW_TAG" ]; then FINAL_PREV="$CUR_TAG"; else FINAL_PREV="$CUR_PREV"; fi
fi
echo "$REG_PASS" | docker login "$REG" -u "$REG_USER" --password-stdin >/dev/null
docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET"
docker pull "$IMG_PREFIX:$FINAL_TAG"
# REGISTRY/TAG 通过 inline 环境变量传给 compose(优先级高于 --env-file),不改写用户的 .env
cd "$DIR" && REGISTRY="$REG" TAG="$FINAL_TAG" docker compose --env-file .env up -d
docker image prune -f
echo "$FINAL_TAG" > "$STATE"
[ -n "$FINAL_PREV" ] && echo "$FINAL_PREV" > "$PREVF" || true
echo "已部署版本 TAG=$FINAL_TAG (上一次=${FINAL_PREV:-无})"
REMOTE
green "✓ console $MODE 完成"

50
deploy/voitrans/README.md

@ -4,38 +4,50 @@
推送到私有仓库 `docker-registry.ideapsound.com`,再到目标机拉取重启。容器内用 shell(entrypoint) 管理 5 个进程,
任一进程退出即整体重启。
> 约定:仓库**只提交模板**(`*.example`)。部署脚本会把**模板 + `docker-compose.yml`** 同步到服务器,
> 但**绝不上传/覆盖**你在服务器上填好的真实 `.env` / `confs/*.yaml`(首次需登录服务器照模板复制并填值)。
## 文件
| 文件 | 说明 |
|---|---|
| `../../apps/services/Dockerfile.voitrans` | 镜像构建(编译 5 个二进制 + entrypoint),构建上下文 = `apps/services/` |
| `../../apps/services/entrypoint.voitrans.sh` | 容器入口,拉起并守护 5 个服务进程 |
| `docker-compose.yml` | 运行镜像,把本目录挂为容器 `/app`,`.env` 注入容器 |
| `confs/*.yaml` | 5 个服务的运行配置(含真实密钥,随仓库提交);基础设施端点用 `${VAR}` 由 `.env` 注入 |
| `env/<env>.env.example` | 运行时配置模板(真实 `.env` 被 gitignore,需复制后填写) |
| `dev-deploy.sh` | **开发**:构建 dev 镜像 → 推送 → 更新开发环境(一条命令) |
| `docker-compose.yml` | 运行镜像,把部署目录挂为容器 `/app`,`.env` 注入容器(此文件由你放到服务器) |
| `confs/*.yaml.example` | 5 个服务配置的**模板**;真实 `confs/*.yaml` 被 gitignore,基础设施端点/密钥用 `${VAR}` 由 `.env` 注入 |
| `env/env.example` | 运行时环境**模板**(只一份,不分区域);真实 `.env` 被 gitignore |
| `dev-deploy.sh` | **开发**:构建 dev 镜像 → 推送 → 远端拉取重启(固定 tag `dev-latest`) |
| `prod-build.sh` | **生产①**:构建带版本号镜像并推送(`:版本` + `:latest`),不部署 |
| `prod-deploy.sh` | **生产②**:选区域 + 版本 tag 部署,支持回滚 |
## 用法
首次:复制并填写 env(真实文件已 gitignore)
### 一、在服务器上准备好真实配置(一次性)
```bash
cd deploy/voitrans
cp env/dev.env.example env/dev.env
cp env/shanghai.env.example env/shanghai.env # 生产区域同理 japan / singapore
# 编辑 env/*.env,填 DSN / Redis / ETCD / NATS / 统计应用名等(CHANGE_ME 处)
脚本每次会同步 `docker-compose.yml` + `*.example` 模板到目标机。**首次**需登录服务器把模板复制成真实文件并填值
(`cp env.example .env`、`for f in confs/*.yaml.example; do cp "$f" "${f%.example}"; done`,dev-deploy 缺真实文件时也会提示):
```
<部署目录>/
├── docker-compose.yml # 从本目录拷贝
├── .env # 照 env/env.example 填真实值(DSN/Redis/ETCD/NATS/CLUSTER_TAG/
│ # GATEWAY_TOKEN_KEY/GATEWAY_ENCRYPT_KEY/ANALYZE_APP_NAME 等)
├── confs/*.yaml # 照 confs/*.yaml.example 填(${VAR} 端点保持,密钥按需填)
└── confs/ 附属文件 # 见下「需要提供的东西」
```
开发环境(一键发布)
dev 部署目录默认 `/home/work/voitrans-a11`,prod 默认 `/opt/yunyan/voitrans`(见脚本)。
### 二、部署
开发:
```bash
./dev-deploy.sh # 构建并发布 dev-latest
./dev-deploy.sh dev_meet-latest # 指定 tag
cd deploy/voitrans
./dev-deploy.sh # 构建 dev-latest → 推送 → 远端拉取重启
```
生产环境(构建与部署分离)
生产(构建与部署分离):
```bash
./prod-build.sh 0.1.1 # 构建并推送 voitrans-a11:0.1.1 (+latest)
@ -45,6 +57,9 @@ cp env/shanghai.env.example env/shanghai.env # 生产区域同理 japan / sin
./prod-deploy.sh shanghai rollback # 上海回滚到上一版本(与当前版本来回切换)
```
> 改了服务器上的 `.env` 后,必须经 `up`(脚本里的 `docker compose up -d`)**重建容器**才生效,`restart` 不会重读环境变量。
> prod 的版本/回滚记录存服务器上的 `.deploy_tag` / `.deploy_tag.prev`,不动你维护的 `.env`。
## 需要填写 / 提供的东西
- **部署目标与凭据**:写死在各脚本顶部。
@ -52,9 +67,10 @@ cp env/shanghai.env.example env/shanghai.env # 生产区域同理 japan / sin
- `prod-deploy.sh`:`region_profile()` 里各区域的 `DEPLOY_HOST`。
- 仓库账号 `admin` / 密码已写死;`DEPLOY_KEY` 默认 `~/.ssh/id_rsa`。
- ⚠️ 本目录脚本纳入 git,**公开仓库前务必清空** `*_HOST` 与 `REGISTRY_PASS`。
- **基础设施端点**:填 `env/<env>.env` 的 `ETCD_SERVERS / DB_ADMIN_DSN / DB_DEVICE_DSN / REDIS_* / NATS_URL`,
以及统计上报 `ANALYZE_APP_NAME`(须与 console 注册表应用名一致)/ `ANALYZE_REGION`、集群隔离 `CLUSTER_TAG`。
- **confs 引用的附属文件**(按需放入本目录,会随 `confs/`/工作目录挂入容器 `/app`):
- **基础设施端点**:填服务器 `.env` 的 `ETCD_SERVERS / DB_ADMIN_DSN / DB_DEVICE_DSN / REDIS_* / NATS_URL`,
以及统计上报 `ANALYZE_APP_NAME`(须与 console 注册表应用名一致)/ `ANALYZE_REGION`、集群隔离 `CLUSTER_TAG`、
网关 `GATEWAY_TOKEN_KEY` 与 v2 加密 `GATEWAY_ENCRYPT_KEY`(须与 Flutter 端 AES key 一致)。
- **confs 引用的附属文件**(按需放入服务器部署目录,会随工作目录挂入容器 `/app`):
- `home.yaml`:`./smart-bluetooth-447104-2b269474bd72.json`(Google)、firebase/google 登录 json、
`./apiclient_key.pem`(微信支付)、`confs/ip2region_v4.xdb` 与 `v6.xdb`(IP 库)、
`wordfilter/*.txt`(敏感词,不需要则删掉 `home.yaml` 的 `wordfilter` 段)。

39
deploy/voitrans/dev-deploy.sh

@ -1,7 +1,9 @@
#!/usr/bin/env bash
# voitrans 开发环境:一键「构建开发镜像 → 推送 → 更新开发环境」。
# voitrans 开发环境:构建开发镜像 → 推送 → 远端拉取重启。
#
# 业务五服务(gateway/home/api/mcp/timer)打成单镜像 voitrans-a11,推到私有仓库后远端拉取重启。
# 仅做镜像部署:服务器上的 docker-compose.yml / .env / confs/ 由你自行维护
# (照着仓库里的 env/env.example、confs/*.yaml.example 在服务器上放好真实文件),本脚本不上传任何配置。
#
# 用法(在 deploy/voitrans/ 下):
# ./dev-deploy.sh # 构建并发布 voitrans-a11:dev-latest
@ -31,27 +33,16 @@ DEV_HOST="124.222.232.218" # ← 开发服
DEV_PORT=22
DEV_USER=root
DEV_KEY=/Users/liwei/Documents/key/earphone.pem # 本机访问开发服务器的 SSH 私钥
DEV_DIR="/home/work/voitrans-a11" # 远端部署目录
DEV_DIR="/home/work/voitrans-a11" # 远端部署目录(其中 docker-compose.yml/.env/confs 由你自己维护)
PLATFORM="${PLATFORM:-linux/amd64}"
TAG="dev-latest" # 开发版固定 tag,每次构建覆盖同一 dev-latest
ENV_FILE="$SVC_DIR/env/dev.env"
FULL_IMAGE="$REGISTRY/$IMAGE_NAME:$TAG"
[ -f "$DOCKERFILE" ] || { red "✗ 缺少 $DOCKERFILE"; exit 1; }
[ -f "$ENV_FILE" ] || { red "✗ 缺少 env/dev.env(先 cp env/dev.env.example env/dev.env 并填写)"; exit 1; }
: "${DEV_HOST:?请在 dev-deploy.sh 顶部填 DEV_HOST(开发服务器 IP)}"
# 生成 compose/容器用 .env = dev.env(去掉 TAG/REGISTRY) + 注入 REGISTRY + 本次 TAG
gen_env() {
{ grep -vE '^(REGISTRY|TAG)=' "$ENV_FILE" || true; } > "$SVC_DIR/.env"
{
printf 'REGISTRY=%s\n' "$REGISTRY"
printf 'TAG=%s\n' "$TAG"
} >> "$SVC_DIR/.env"
}
# ── 远程 SSH 基础 ──
# ── 远程 SSH ──
_ssh() { ssh -p "$DEV_PORT" -i "${DEV_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$DEV_USER@$DEV_HOST" "$@"; }
_scp() { scp -P "$DEV_PORT" -i "${DEV_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; }
_dest() { echo "$DEV_USER@$DEV_HOST:$1"; }
@ -62,18 +53,24 @@ echo "$REGISTRY_PASS" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-
docker build --network=host --platform "$PLATFORM" -f "$DOCKERFILE" -t "$FULL_IMAGE" "$BUILD_CONTEXT"
docker push "$FULL_IMAGE"
# 2) 同步配置并远端拉起
: "${DEV_HOST:?请在 dev-deploy.sh 顶部填 DEV_HOST}"
gen_env
blue ">>> 同步配置到 $DEV_USER@$DEV_HOST:$DEV_DIR"
# 2) 同步「模板 + compose」到服务器(只传模板,绝不覆盖你维护的真实 .env / confs/*.yaml)
blue ">>> 同步模板与 compose → $DEV_USER@$DEV_HOST:$DEV_DIR"
_ssh "mkdir -p '$DEV_DIR/confs' '$DEV_DIR/log'"
_scp "$SVC_DIR/docker-compose.yml" "$(_dest "$DEV_DIR/docker-compose.yml")"
_scp "$SVC_DIR/.env" "$(_dest "$DEV_DIR/.env")"
_scp -r "$SVC_DIR/confs/." "$(_dest "$DEV_DIR/confs/")"
_scp "$SVC_DIR"/confs/*.yaml.example "$(_dest "$DEV_DIR/confs/")"
_scp "$SVC_DIR/env/env.example" "$(_dest "$DEV_DIR/env.example")"
# 3) 远端拉取并重启(用你在服务器维护的真实 .env / confs/*.yaml)
# REGISTRY/TAG 通过 inline 环境变量传给 compose(优先级高于 --env-file),确保拉到本次镜像。
blue ">>> 远端拉取并重启"
_ssh "echo '$REGISTRY_PASS' | docker login '$REGISTRY' -u '$REGISTRY_USER' --password-stdin >/dev/null"
_ssh "docker network inspect 1panel-network >/dev/null 2>&1 || docker network create 1panel-network"
_ssh "cd '$DEV_DIR' && docker compose --env-file .env pull && docker compose --env-file .env up -d && docker image prune -f"
if ! _ssh "test -f '$DEV_DIR/.env' && test -f '$DEV_DIR/confs/gateway.yaml'"; then
red "⚠ 服务器缺少真实 .env 或 confs/*.yaml —— 模板已同步,请登录服务器按模板填好后重跑本脚本:"
echo " ssh -i $DEV_KEY $DEV_USER@$DEV_HOST"
echo " cd $DEV_DIR && cp env.example .env # 再把 confs/*.yaml.example 各自复制为 confs/*.yaml 并填真实值"
exit 0
fi
_ssh "cd '$DEV_DIR' && REGISTRY='$REGISTRY' TAG='$TAG' docker compose --env-file .env pull && REGISTRY='$REGISTRY' TAG='$TAG' docker compose --env-file .env up -d && docker image prune -f"
green "✓ voitrans dev 已发布: $FULL_IMAGE → $DEV_HOST:$DEV_DIR"

0
deploy/voitrans/env/dev.env.example → deploy/voitrans/env/env.example

37
deploy/voitrans/env/japan.env.example

@ -1,37 +0,0 @@
# voitrans · 生产-日本 容器运行时配置(REGISTRY 由 prod-deploy.sh 注入)
TAG=latest
PLATFORM=linux/amd64
DOCKER_NETWORK=1panel-network
CLUSTER_TAG=voitrans-a11-jp
ETCD_SERVERS=etcd:2379
# ── 存储(按日本生产实际填写)──
DB_ADMIN_DSN=root:CHANGE_ME@tcp(mysql:3306)/voitrans?charset=utf8mb4&parseTime=True&loc=Local
DB_DEVICE_DSN=postgresql://postgres:CHANGE_ME@postgres:5432/postgres?sslmode=require
DB_DEVICE_READ_DSN=
REDIS_ADDR=redis:6379
REDIS_PASSWORD=CHANGE_ME
REDIS_DB=1
REDIS_KEY_PREFIX=VoitransJP
NATS_URL=nats://nats:4222
ANALYZE_APP_NAME=voitrans-a11-jp
ANALYZE_REGION=2 # 区域编码见 pb.Region(海外)
LOG_DEBUG=false
GATEWAY_TOKEN_KEY=CHANGE_ME
# v2 配置接口响应加密 AES key(16/24/32 字节),须与 Flutter ApiCrypto._key 完全一致;留空=不加密
GATEWAY_ENCRYPT_KEY=
API_ADMIN_ACCOUNT=admin
API_ADMIN_PASSWORD=CHANGE_ME
API_SITE_NAME=Voitrans
API_CURRENCY_TYPE=1 # 海外计费美元
MCP_ADDR=http://localhost:7300
AZURE_TRANSLATE_KEY=
TIMER_DOUBAO_APIKEY=
TIMER_DOUBAO_BOTID=
TIMER_DOUBAO_ROLE=

38
deploy/voitrans/env/shanghai.env.example

@ -1,38 +0,0 @@
# voitrans · 生产-上海 容器运行时配置(REGISTRY 由 prod-deploy.sh 注入)
# 依赖的 etcd/mysql/redis/nats 用容器名或区域内网地址连接,按实际填写。
TAG=latest # 实际部署版本由 prod-deploy.sh <区域> <tag> 覆盖
PLATFORM=linux/amd64
DOCKER_NETWORK=1panel-network
CLUSTER_TAG=voitrans-a11-sh # 各区域独立集群标签,避免跨区域互相发现
ETCD_SERVERS=etcd:2379
# ── 存储(按上海生产实际填写)──
DB_ADMIN_DSN=root:CHANGE_ME@tcp(mysql:3306)/voitrans?charset=utf8mb4&parseTime=True&loc=Local
DB_DEVICE_DSN=postgresql://postgres:CHANGE_ME@postgres:5432/postgres?sslmode=require
DB_DEVICE_READ_DSN=
REDIS_ADDR=redis:6379
REDIS_PASSWORD=CHANGE_ME
REDIS_DB=1
REDIS_KEY_PREFIX=VoitransSH
NATS_URL=nats://nats:4222
ANALYZE_APP_NAME=voitrans-a11-sh # 须与 console 注册表应用名一致
ANALYZE_REGION=1 # 区域编码见 pb.Region(国内)
LOG_DEBUG=false
GATEWAY_TOKEN_KEY=CHANGE_ME
# v2 配置接口响应加密 AES key(16/24/32 字节),须与 Flutter ApiCrypto._key 完全一致;留空=不加密
GATEWAY_ENCRYPT_KEY=
API_ADMIN_ACCOUNT=admin
API_ADMIN_PASSWORD=CHANGE_ME
API_SITE_NAME=Voitrans
API_CURRENCY_TYPE=0
MCP_ADDR=http://localhost:7300
AZURE_TRANSLATE_KEY=
TIMER_DOUBAO_APIKEY=
TIMER_DOUBAO_BOTID=
TIMER_DOUBAO_ROLE=

37
deploy/voitrans/env/singapore.env.example

@ -1,37 +0,0 @@
# voitrans · 生产-新加坡 容器运行时配置(REGISTRY 由 prod-deploy.sh 注入)
TAG=latest
PLATFORM=linux/amd64
DOCKER_NETWORK=1panel-network
CLUSTER_TAG=voitrans-a11-sg
ETCD_SERVERS=etcd:2379
# ── 存储(按新加坡生产实际填写)──
DB_ADMIN_DSN=root:CHANGE_ME@tcp(mysql:3306)/voitrans?charset=utf8mb4&parseTime=True&loc=Local
DB_DEVICE_DSN=postgresql://postgres:CHANGE_ME@postgres:5432/postgres?sslmode=require
DB_DEVICE_READ_DSN=
REDIS_ADDR=redis:6379
REDIS_PASSWORD=CHANGE_ME
REDIS_DB=1
REDIS_KEY_PREFIX=VoitransSG
NATS_URL=nats://nats:4222
ANALYZE_APP_NAME=voitrans-a11-sg
ANALYZE_REGION=2 # 区域编码见 pb.Region(海外)
LOG_DEBUG=false
GATEWAY_TOKEN_KEY=CHANGE_ME
# v2 配置接口响应加密 AES key(16/24/32 字节),须与 Flutter ApiCrypto._key 完全一致;留空=不加密
GATEWAY_ENCRYPT_KEY=
API_ADMIN_ACCOUNT=admin
API_ADMIN_PASSWORD=CHANGE_ME
API_SITE_NAME=Voitrans
API_CURRENCY_TYPE=1
MCP_ADDR=http://localhost:7300
AZURE_TRANSLATE_KEY=
TIMER_DOUBAO_APIKEY=
TIMER_DOUBAO_BOTID=
TIMER_DOUBAO_ROLE=

62
deploy/voitrans/prod-deploy.sh

@ -1,8 +1,10 @@
#!/usr/bin/env bash
# voitrans 生产环境:部署到指定区域(选择区域 + 镜像版本 tag),支持回滚。
#
# 镜像由 ./prod-build.sh 预先构建推送;本脚本只做「同步配置 + 远端拉取 + 重启」。
# 每个区域一套服务器配置(见下方 region_profile)。各区域共用同一镜像 voitrans-a11。
# 镜像由 ./prod-build.sh 预先构建推送;本脚本只做「远端拉取 + 重启」。
# 仅做镜像部署:服务器上的 docker-compose.yml / .env / confs/ 由你自行维护
# (照着仓库里的 env/env.example、confs/*.yaml.example 在服务器上放好真实文件),本脚本不上传任何配置。
# 各区域一套服务器(见下方 region_profile),各区域共用同一镜像 voitrans-a11。
#
# 用法(在 deploy/voitrans/ 下):
# ./prod-deploy.sh <区域> <tag> # 部署指定版本,如 ./prod-deploy.sh shanghai 0.1.1
@ -21,16 +23,17 @@ IMAGE_NAME="voitrans-a11"
REGISTRY="docker-registry.ideapsound.com"
REGISTRY_USER="admin"
REGISTRY_PASS="li13451234"
DOCKER_NETWORK="1panel-network" # compose 外部网络名(与服务器一致)
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ 各区域部署目标 —— 写死在此(本文件纳入 git)。公开仓库前清空 HOST。 ║
# ║ 新增区域:在此加一段 case + 在 env/ 下加 <区域>.env ║
# ║ 新增区域:在此加一段 case 填 DEPLOY_HOST ║
# ╚══════════════════════════════════════════════════════════════════════╝
region_profile() {
DEPLOY_PORT=22
DEPLOY_USER=root
DEPLOY_KEY=~/.ssh/id_rsa
DEPLOY_DIR="/opt/yunyan/voitrans" # 远端部署目录(各区域机器各一份)
DEPLOY_DIR="/opt/yunyan/voitrans" # 远端部署目录(其中 docker-compose.yml/.env/confs 由你自己维护)
case "$REGION" in
shanghai) DEPLOY_HOST="" ;; # ← 上海生产服务器 IP(必填)
japan) DEPLOY_HOST="" ;; # ← 日本生产服务器 IP(必填)
@ -47,59 +50,50 @@ if [ "$ARG2" = "rollback" ]; then MODE="rollback"; NEW_TAG=""; else MODE="deploy
region_profile
: "${DEPLOY_HOST:?请在 prod-deploy.sh 的 region_profile($REGION) 段填 DEPLOY_HOST}"
ENV_FILE="$SVC_DIR/env/$REGION.env"
[ -f "$ENV_FILE" ] || { red "✗ 缺少 env/$REGION.env"; exit 1; }
[ -f "$SVC_DIR/docker-compose.yml" ] || { red "✗ 缺少 docker-compose.yml"; exit 1; }
# 取 DOCKER_NETWORK(compose 外部网络名)
DOCKER_NETWORK="$({ grep -E '^DOCKER_NETWORK=' "$ENV_FILE" || true; } | head -1 | cut -d= -f2-)"
DOCKER_NETWORK="${DOCKER_NETWORK:-1panel-network}"
# 生成 .env.infra = <区域>.env(去掉 TAG/PREV_TAG/REGISTRY) + 注入 REGISTRY。TAG/PREV_TAG 由远端管理。
gen_env_infra() {
{ grep -vE '^(REGISTRY|TAG|PREV_TAG)=' "$ENV_FILE" || true; } > "$SVC_DIR/.env.infra"
printf 'REGISTRY=%s\n' "$REGISTRY" >> "$SVC_DIR/.env.infra"
}
_ssh() { ssh -p "$DEPLOY_PORT" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST" "$@"; }
_scp() { scp -P "$DEPLOY_PORT" -i "${DEPLOY_KEY/#\~/$HOME}" -o StrictHostKeyChecking=accept-new "$@"; }
_dest() { echo "$DEPLOY_USER@$DEPLOY_HOST:$1"; }
gen_env_infra
blue ">>> [$REGION] 同步配置到 $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR ($MODE${NEW_TAG:+ $NEW_TAG})"
# 同步「模板 + compose」(只传模板,绝不覆盖你维护的真实 .env / confs/*.yaml)
blue ">>> [$REGION] 同步模板与 compose → $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR"
_ssh "mkdir -p '$DEPLOY_DIR/confs' '$DEPLOY_DIR/log'"
_scp "$SVC_DIR/docker-compose.yml" "$(_dest "$DEPLOY_DIR/docker-compose.yml")"
_scp "$SVC_DIR/.env.infra" "$(_dest "$DEPLOY_DIR/.env.infra")"
_scp -r "$SVC_DIR/confs/." "$(_dest "$DEPLOY_DIR/confs/")"
_scp "$SVC_DIR"/confs/*.yaml.example "$(_dest "$DEPLOY_DIR/confs/")"
_scp "$SVC_DIR/env/env.example" "$(_dest "$DEPLOY_DIR/env.example")"
blue ">>> [$REGION] 远端计算版本、拉取并重启"
# 远端拉取并重启(不上传真实配置):tag 历史存服务器上独立的 .deploy_tag,不动你维护的 .env。
blue ">>> [$REGION] 远端部署($MODE${NEW_TAG:+ $NEW_TAG}) → $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR"
_ssh bash -s -- "$DEPLOY_DIR" "$MODE" "$NEW_TAG" "$REGISTRY/$IMAGE_NAME" "$REGISTRY" "$REGISTRY_USER" "$REGISTRY_PASS" "$DOCKER_NETWORK" <<'REMOTE'
set -e
DIR="$1"; MODE="$2"; NEW_TAG="$3"; IMG_PREFIX="$4"; REG="$5"; REG_USER="$6"; REG_PASS="$7"; NET="$8"
getline() { [ -f "$DIR/.env" ] && { grep -E "^$1=" "$DIR/.env" 2>/dev/null | tail -1 | cut -d= -f2- || true; } | tr -d '\r'; }
CUR_TAG="$(getline TAG)"
CUR_PREV="$(getline PREV_TAG)"
[ -f "$DIR/.env" ] || { echo "✗ 缺少 $DIR/.env(请照 env.example 在服务器填好真实 .env)"; exit 1; }
[ -f "$DIR/confs/gateway.yaml" ] || { echo "✗ 缺少 $DIR/confs/*.yaml(请照 confs/*.yaml.example 在服务器填好)"; exit 1; }
[ -f "$DIR/docker-compose.yml" ] || { echo "✗ 缺少 $DIR/docker-compose.yml"; exit 1; }
STATE="$DIR/.deploy_tag" # 当前部署 tag(独立文件,不动用户的 .env)
PREVF="$DIR/.deploy_tag.prev" # 上一次部署 tag
CUR_TAG="$([ -f "$STATE" ] && cat "$STATE" || true)"
CUR_PREV="$([ -f "$PREVF" ] && cat "$PREVF" || true)"
if [ "$MODE" = "rollback" ]; then
[ -n "$CUR_PREV" ] || { echo "没有 PREV_TAG,无法回滚"; exit 1; }
[ -n "$CUR_PREV" ] || { echo "没有上一次版本记录(.deploy_tag.prev),无法回滚"; exit 1; }
FINAL_TAG="$CUR_PREV"; FINAL_PREV="$CUR_TAG" # 与当前版本来回切换
else
FINAL_TAG="$NEW_TAG"
if [ -n "$CUR_TAG" ] && [ "$CUR_TAG" != "$NEW_TAG" ]; then FINAL_PREV="$CUR_TAG"; else FINAL_PREV="$CUR_PREV"; fi
fi
# 写新 .env = 同步来的基础设施配置 + PREV_TAG + TAG
cp "$DIR/.env.infra" "$DIR/.env"
[ -n "$FINAL_PREV" ] && echo "PREV_TAG=$FINAL_PREV" >> "$DIR/.env"
echo "TAG=$FINAL_TAG" >> "$DIR/.env"
echo "$REG_PASS" | docker login "$REG" -u "$REG_USER" --password-stdin >/dev/null
docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET"
docker pull "$IMG_PREFIX:$FINAL_TAG"
cd "$DIR" && docker compose --env-file .env up -d
# REGISTRY/TAG 通过 inline 环境变量传给 compose(优先级高于 --env-file),不改写用户的 .env
cd "$DIR" && REGISTRY="$REG" TAG="$FINAL_TAG" docker compose --env-file .env up -d
docker image prune -f
echo "已部署版本 TAG=$FINAL_TAG (PREV_TAG=${FINAL_PREV:-无})"
echo "$FINAL_TAG" > "$STATE"
[ -n "$FINAL_PREV" ] && echo "$FINAL_PREV" > "$PREVF" || true
echo "已部署版本 TAG=$FINAL_TAG (上一次=${FINAL_PREV:-无})"
REMOTE
green "✓ voitrans [$REGION] $MODE 完成"

Loading…
Cancel
Save