Przeglądaj źródła

Merge branch 'main' of http://gitlab.pubdata.cn/hlm/multi-platform-media-manage

Ethanfly 14 godzin temu
rodzic
commit
8ddac482bf

+ 11 - 0
client/src/stores/server.ts

@@ -11,6 +11,7 @@ interface ServerConfig {
 
 const STORAGE_KEY = 'server_configs';
 const CURRENT_SERVER_KEY = 'current_server';
+const SINGLE_SERVER_ID = 'server_default';
 
 export const useServerStore = defineStore('server', () => {
   const servers = ref<ServerConfig[]>([]);
@@ -87,6 +88,15 @@ export const useServerStore = defineStore('server', () => {
     }
   }
 
+  // 单服务器模式:覆盖保存一个服务器配置并设为当前
+  function setSingleServer(config: { url: string; name?: string }) {
+    const url = config.url.replace(/\/$/, '');
+    const name = (config.name || '').trim() || '服务器';
+    servers.value = [{ id: SINGLE_SERVER_ID, name, url, isDefault: true }];
+    currentServerId.value = SINGLE_SERVER_ID;
+    saveConfig();
+  }
+
   // 检查服务器连接
   async function checkConnection(url?: string): Promise<boolean> {
     const targetUrl = url || currentServer.value?.url;
@@ -114,6 +124,7 @@ export const useServerStore = defineStore('server', () => {
     updateServer,
     removeServer,
     setCurrentServer,
+    setSingleServer,
     checkConnection,
   };
 });

+ 250 - 133
client/src/views/ServerConfig/index.vue

@@ -38,60 +38,19 @@
         <h1>服务器配置</h1>
         <p>配置后端服务器地址以连接系统</p>
       </div>
-      
-      <!-- 服务器列表 -->
-      <div class="server-list" v-if="serverStore.servers.length > 0">
-        <div
-          v-for="server in serverStore.servers"
-          :key="server.id"
-          class="server-item"
-          :class="{ active: server.id === serverStore.currentServerId }"
-          @click="selectServer(server.id)"
-        >
-          <div class="server-info">
-            <div class="server-name">{{ server.name }}</div>
-            <div class="server-url">{{ server.url }}</div>
-          </div>
-          <div class="server-actions">
-            <el-tag v-if="server.id === serverStore.currentServerId" type="success" size="small">
-              当前使用
-            </el-tag>
-            <el-button
-              type="danger"
-              link
-              size="small"
-              @click.stop="deleteServer(server.id)"
-            >
-              删除
-            </el-button>
-          </div>
-        </div>
-      </div>
-      
-      <el-divider v-if="serverStore.servers.length > 0" />
-      
-      <!-- 添加服务器表单 -->
+
       <el-form
-        ref="formRef"
-        :model="form"
-        :rules="rules"
+        ref="serverFormRef"
+        :model="serverForm"
+        :rules="serverRules"
         label-position="top"
         class="config-form"
       >
-        <el-form-item label="服务器名称" prop="name">
-          <el-input 
-            v-model="form.name" 
-            placeholder="例如:本地服务器" 
-            size="large"
-            :prefix-icon="Connection"
-          />
-        </el-form-item>
-        
         <el-form-item label="服务器地址" prop="url">
           <div class="url-input-row">
-            <el-input 
-              v-model="form.url" 
-              placeholder="例如:http://localhost:3000" 
+            <el-input
+              v-model="serverForm.url"
+              placeholder="例如:https://media-manage.example.com"
               size="large"
               :prefix-icon="Link"
             />
@@ -100,19 +59,6 @@
             </el-button>
           </div>
         </el-form-item>
-        
-        <el-form-item class="checkbox-row">
-          <el-checkbox v-model="form.isDefault">设为默认服务器</el-checkbox>
-        </el-form-item>
-        
-        <el-form-item class="action-row">
-          <el-button type="primary" @click="addServer" :loading="loading" class="primary-btn">
-            添加服务器
-          </el-button>
-          <el-button v-if="serverStore.isConfigured" @click="goBack" class="back-btn">
-            返回
-          </el-button>
-        </el-form-item>
       </el-form>
       
       <div class="connection-status" v-if="connectionResult !== null">
@@ -123,24 +69,67 @@
           show-icon
         />
       </div>
+
+      <el-divider />
+
+      <div class="python-config">
+        <div class="section-title-row">
+          <div class="section-title">Python 服务配置</div>
+          <el-tag v-if="!canManagePythonService" type="info" size="small">需管理员登录</el-tag>
+        </div>
+        <el-form :model="pythonService" label-position="top" class="config-form">
+          <el-form-item label="服务地址">
+            <div class="url-input-row">
+              <el-input
+                v-model="pythonService.url"
+                placeholder="例如:http://localhost:5005"
+                size="large"
+                :prefix-icon="Link"
+                :disabled="!pythonFormEnabled"
+              />
+              <el-button @click="checkPythonService" :loading="checkingPython" class="test-btn" :disabled="!pythonFormEnabled">
+                测试连接
+              </el-button>
+            </div>
+          </el-form-item>
+        </el-form>
+
+        <div class="connection-status" v-if="pythonCheckResult">
+          <el-alert
+            :type="pythonCheckResult.ok ? 'success' : 'error'"
+            :title="pythonCheckResult.ok ? '连接成功' : '连接失败'"
+            :description="pythonCheckResult.ok ? 'Python 服务响应正常' : (pythonCheckResult.error || '无法连接到 Python 服务')"
+            show-icon
+          />
+        </div>
+      </div>
+
+      <div class="bottom-actions">
+        <el-button type="primary" @click="saveAll" :loading="savingAll" class="primary-btn">
+          保存配置
+        </el-button>
+        <el-button @click="goBack" class="back-btn">
+          返回
+        </el-button>
+      </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from 'vue';
+import { ref, reactive, onMounted, computed, watch } from 'vue';
 import { useRouter } from 'vue-router';
-import { Setting, Connection, Link } from '@element-plus/icons-vue';
-import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
+import { Setting, Link } from '@element-plus/icons-vue';
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
 import { useServerStore } from '@/stores/server';
 import { useAuthStore } from '@/stores/auth';
+import request from '@/api/request';
 
 const router = useRouter();
 const serverStore = useServerStore();
 const authStore = useAuthStore();
 
-const formRef = ref<FormInstance>();
-const loading = ref(false);
+const serverFormRef = ref<FormInstance>();
 const testing = ref(false);
 const connectionResult = ref<boolean | null>(null);
 const isMaximized = ref(false);
@@ -168,22 +157,141 @@ onMounted(async () => {
   });
 });
 
-const form = reactive({
-  name: '',
+const serverForm = reactive({
   url: '',
-  isDefault: true,
 });
 
-const rules: FormRules = {
-  name: [{ required: true, message: '请输入服务器名称', trigger: 'blur' }],
+const serverRules: FormRules = {
   url: [
     { required: true, message: '请输入服务器地址', trigger: 'blur' },
     { pattern: /^https?:\/\/.+/, message: '请输入有效的 URL', trigger: 'blur' },
   ],
 };
 
+const pythonService = reactive({
+  url: '',
+});
+const savingAll = ref(false);
+const checkingPython = ref(false);
+const pythonCheckResult = ref<any | null>(null);
+
+function normalizeBaseUrl(url?: string): string {
+  const raw = String(url || '').trim();
+  if (!raw) return '';
+  try {
+    const u = new URL(raw);
+    return `${u.protocol}//${u.host}`.replace(/\/$/, '');
+  } catch {
+    return raw.replace(/\/$/, '');
+  }
+}
+
+function isLocalServerUrl(url?: string): boolean {
+  if (!url) return false;
+  try {
+    const u = new URL(url);
+    const host = u.hostname;
+    return host === 'localhost' || host === '127.0.0.1' || host === '::1';
+  } catch {
+    return false;
+  }
+}
+
+const apiBaseUrl = computed(() => {
+  return normalizeBaseUrl(serverStore.currentServer?.url) || normalizeBaseUrl(serverForm.url);
+});
+
+const canManagePythonService = computed(() => authStore.isAdmin || isLocalServerUrl(apiBaseUrl.value));
+const pythonFormEnabled = computed(() => !!apiBaseUrl.value && canManagePythonService.value);
+
+async function loadPythonService() {
+  try {
+    if (!pythonFormEnabled.value) return;
+    const config = await request.get('/api/system/python-service', { baseURL: apiBaseUrl.value });
+    pythonService.url = String(config.url || '');
+  } catch {
+    // 错误已处理
+  }
+}
+
+async function saveAll() {
+  if (!serverFormRef.value) return;
+
+  const valid = await serverFormRef.value.validate().catch(() => false);
+  if (!valid) return;
+
+  const normalizedServerUrl = normalizeBaseUrl(serverForm.url);
+  if (!normalizedServerUrl) {
+    ElMessage.warning('请输入服务器地址');
+    return;
+  }
+
+  savingAll.value = true;
+  try {
+    serverStore.setSingleServer({
+      url: normalizedServerUrl,
+    });
+
+    if (pythonService.url && canManagePythonService.value) {
+      await request.put('/api/system/python-service', { url: normalizeBaseUrl(pythonService.url) }, { baseURL: normalizedServerUrl });
+    }
+
+    ElMessage.success('配置已保存');
+  } catch {
+    // 错误已处理
+  } finally {
+    savingAll.value = false;
+  }
+}
+
+async function checkPythonService() {
+  if (!canManagePythonService.value) {
+    ElMessage.warning('需要管理员登录后才能配置');
+    return;
+  }
+  if (!apiBaseUrl.value) {
+    ElMessage.warning('请先填写服务器地址');
+    return;
+  }
+  checkingPython.value = true;
+  pythonCheckResult.value = null;
+  try {
+    const result = await request.post(
+      '/api/system/python-service/check',
+      { url: pythonService.url ? normalizeBaseUrl(pythonService.url) : undefined },
+      { baseURL: apiBaseUrl.value }
+    );
+    pythonCheckResult.value = result;
+    if (result.ok) {
+      ElMessage.success('连接成功');
+    } else {
+      ElMessage.error('连接失败');
+    }
+  } catch {
+    pythonCheckResult.value = { ok: false, error: '请求失败' };
+    ElMessage.error('连接失败');
+  } finally {
+    checkingPython.value = false;
+  }
+}
+
+onMounted(() => {
+  serverForm.url = serverStore.currentServer?.url || '';
+  if (pythonFormEnabled.value) loadPythonService();
+});
+
+watch(
+  () => serverStore.currentServerId,
+  () => {
+    pythonCheckResult.value = null;
+    pythonService.url = '';
+    serverForm.url = serverStore.currentServer?.url || serverForm.url;
+    if (pythonFormEnabled.value) loadPythonService();
+  }
+);
+
 async function testConnection() {
-  if (!form.url) {
+  if (!serverForm.url) {
     ElMessage.warning('请先输入服务器地址');
     return;
   }
@@ -192,7 +300,7 @@ async function testConnection() {
   connectionResult.value = null;
   
   try {
-    const result = await serverStore.checkConnection(form.url);
+    const result = await serverStore.checkConnection(normalizeBaseUrl(serverForm.url));
     connectionResult.value = result;
     if (result) {
       ElMessage.success('连接成功');
@@ -207,66 +315,6 @@ async function testConnection() {
   }
 }
 
-async function addServer() {
-  if (!formRef.value) return;
-  
-  const valid = await formRef.value.validate().catch(() => false);
-  if (!valid) return;
-  
-  loading.value = true;
-  try {
-    // 先测试连接
-    const connected = await serverStore.checkConnection(form.url);
-    if (!connected) {
-      ElMessage.warning('服务器连接失败,仍要添加吗?');
-    }
-    
-    serverStore.addServer({
-      name: form.name,
-      url: form.url.replace(/\/$/, ''), // 移除末尾斜杠
-      isDefault: form.isDefault,
-    });
-    
-    ElMessage.success('服务器添加成功');
-    
-    // 清空表单
-    form.name = '';
-    form.url = '';
-    form.isDefault = false;
-    connectionResult.value = null;
-    
-    // 如果已登录且切换了服务器,清除登录状态
-    if (authStore.isAuthenticated) {
-      authStore.clearTokens();
-    }
-  } finally {
-    loading.value = false;
-  }
-}
-
-function selectServer(id: string) {
-  if (id === serverStore.currentServerId) return;
-  
-  serverStore.setCurrentServer(id);
-  // 切换服务器后清除登录状态
-  authStore.clearTokens();
-  ElMessage.success('已切换服务器');
-}
-
-async function deleteServer(id: string) {
-  try {
-    await ElMessageBox.confirm('确定要删除这个服务器配置吗?', '提示', {
-      confirmButtonText: '确定',
-      cancelButtonText: '取消',
-      type: 'warning',
-    });
-    serverStore.removeServer(id);
-    ElMessage.success('已删除');
-  } catch {
-    // 取消
-  }
-}
-
 function goBack() {
   if (authStore.isAuthenticated) {
     router.push('/');
@@ -590,4 +638,73 @@ function goBack() {
     border-radius: $radius-base;
   }
 }
+
+.python-config {
+  margin-top: 4px;
+}
+
+.section-title {
+  font-size: 15px;
+  font-weight: 700;
+  color: $text-primary;
+  margin-bottom: 12px;
+}
+
+.section-title-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 12px;
+
+  .section-title {
+    margin-bottom: 0;
+  }
+}
+
+.bottom-actions {
+  display: flex;
+  gap: 12px;
+  margin-top: 28px;
+
+  .primary-btn {
+    flex: 1;
+    height: 44px;
+    padding: 0 28px;
+    font-size: 15px;
+    font-weight: 600;
+    border-radius: $radius-base;
+    background: linear-gradient(135deg, $primary-color 0%, #3a7bd5 100%);
+    border: none;
+    box-shadow: 0 4px 16px rgba($primary-color, 0.3);
+    transition: all 0.3s;
+    
+    &:hover {
+      transform: translateY(-1px);
+      box-shadow: 0 6px 20px rgba($primary-color, 0.4);
+    }
+    
+    &:active {
+      transform: translateY(0);
+    }
+  }
+
+  .back-btn {
+    height: 44px;
+    padding: 0 28px;
+    font-size: 15px;
+    font-weight: 500;
+    border-radius: $radius-base;
+    border: 1px solid $border-base;
+    background: #fff;
+    color: $text-regular;
+    transition: all 0.2s;
+    white-space: nowrap;
+    
+    &:hover {
+      border-color: $primary-color;
+      color: $primary-color;
+    }
+  }
+}
 </style>

+ 76 - 0
client/src/views/Settings/index.vue

@@ -45,6 +45,32 @@
           </el-form>
         </div>
       </el-tab-pane>
+
+      <el-tab-pane label="Python 服务" name="python-service">
+        <div class="page-card">
+          <el-form :model="pythonService" label-width="150px">
+            <el-form-item label="服务地址">
+              <el-input v-model="pythonService.url" placeholder="例如:http://localhost:5005" style="width: 100%" />
+              <span class="form-tip">留空则使用服务端环境变量/默认值(http://localhost:5005)</span>
+            </el-form-item>
+
+            <el-form-item>
+              <el-button type="primary" @click="savePythonService">保存配置</el-button>
+              <el-button @click="loadPythonService">刷新</el-button>
+              <el-button @click="checkPythonService" :loading="checkingPython">测试连接</el-button>
+            </el-form-item>
+          </el-form>
+
+          <div v-if="pythonCheckResult" style="margin-top: 12px;">
+            <el-alert
+              :type="pythonCheckResult.ok ? 'success' : 'error'"
+              :title="pythonCheckResult.ok ? '连接成功' : '连接失败'"
+              :description="pythonCheckResult.ok ? `HTTP ${pythonCheckResult.status || ''}` : (pythonCheckResult.error || '请求失败')"
+              show-icon
+            />
+          </div>
+        </div>
+      </el-tab-pane>
       
       <el-tab-pane label="用户管理" name="users">
         <div class="page-card">
@@ -135,6 +161,13 @@ const publishProxy = reactive({
   signature: '',
 });
 
+const pythonService = reactive({
+  url: '',
+});
+
+const checkingPython = ref(false);
+const pythonCheckResult = ref<any | null>(null);
+
 const users = ref<User[]>([]);
 const showAddUserDialog = ref(false);
 
@@ -197,6 +230,48 @@ async function savePublishProxy() {
   }
 }
 
+async function loadPythonService() {
+  try {
+    const config = await request.get('/api/system/python-service');
+    pythonService.url = String(config.url || '');
+  } catch {
+    // 错误已处理
+  }
+}
+
+async function savePythonService() {
+  try {
+    await request.put('/api/system/python-service', {
+      url: pythonService.url,
+    });
+    ElMessage.success('Python 服务配置已保存');
+    loadPythonService();
+  } catch {
+    // 错误已处理
+  }
+}
+
+async function checkPythonService() {
+  checkingPython.value = true;
+  pythonCheckResult.value = null;
+  try {
+    const result = await request.post('/api/system/python-service/check', {
+      url: pythonService.url || undefined,
+    });
+    pythonCheckResult.value = result;
+    if (result.ok) {
+      ElMessage.success('连接成功');
+    } else {
+      ElMessage.error('连接失败');
+    }
+  } catch {
+    pythonCheckResult.value = { ok: false, error: '请求失败' };
+    ElMessage.error('连接失败');
+  } finally {
+    checkingPython.value = false;
+  }
+}
+
 async function saveSettings() {
   try {
     await request.put('/api/system/config', settings);
@@ -245,6 +320,7 @@ async function deleteUser(id: number) {
 onMounted(() => {
   loadSettings();
   loadPublishProxy();
+  loadPythonService();
   loadUsers();
   loadSystemStatus();
 });

+ 18 - 2
server/env.example

@@ -78,8 +78,11 @@ ALLOW_REGISTRATION=true
 # ----------------------------------------
 # CORS 跨域配置
 # ----------------------------------------
-# 允许的来源 (多个用逗号分隔)
-CORS_ORIGIN=http://localhost:5173
+# 允许的来源 (多个用逗号分隔;设置为 * 表示允许任意来源)
+# 示例:
+#   CORS_ORIGIN=*
+#   CORS_ORIGIN=http://localhost:5173,https://media-manage.example.com
+CORS_ORIGIN=*
 
 # ----------------------------------------
 # 文件上传配置
@@ -109,6 +112,19 @@ INTERNAL_API_KEY=internal-api-key-default
 NODEJS_API_URL=http://localhost:3000
 
 # ----------------------------------------
+# Python 发布服务配置 (Node.js 调用 Python)
+# ----------------------------------------
+# Python 发布服务地址(默认: http://localhost:5005)
+# 也可在系统设置里配置(优先级更高)
+PYTHON_PUBLISH_SERVICE_URL=http://localhost:5005
+
+# 兼容旧变量名(可不填)
+XHS_SERVICE_URL=
+
+# Python 可执行文件(用于部分导出脚本)
+PYTHON_BIN=python
+
+# ----------------------------------------
 # AI 配置 - 阿里云百炼千问大模型 (可选,用于智能功能)
 # ----------------------------------------
 # 阿里云百炼 API Key (以 sk- 开头)

+ 5 - 1
server/src/app.ts

@@ -30,7 +30,11 @@ app.use(helmet({
   crossOriginResourcePolicy: { policy: 'cross-origin' },
 }));
 app.use(cors({
-  origin: config.cors.origin,
+  origin: (origin, callback) => {
+    if (!origin || origin === 'null') return callback(null, true);
+    if (config.cors.origin.includes('*')) return callback(null, true);
+    return callback(null, config.cors.origin.includes(origin));
+  },
   credentials: true,
 }));
 app.use(compression());

+ 5 - 5
server/src/automation/platforms/baijiahao.ts

@@ -12,9 +12,7 @@ import type {
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
 import { aiService } from '../../ai/index.js';
-
-// Python 多平台发布服务配置
-const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
+import { getPythonServiceBaseUrl } from '../../services/PythonServiceConfigService.js';
 
 // 服务器根目录(用于构造绝对路径)
 const SERVER_ROOT = path.resolve(process.cwd());
@@ -36,7 +34,8 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
    */
   private async checkPythonServiceAvailable(): Promise<boolean> {
     try {
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/health`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/health`, {
         method: 'GET',
         signal: AbortSignal.timeout(3000),
       });
@@ -288,7 +287,8 @@ export class BaijiahaoAdapter extends BasePlatformAdapter {
 
       // 使用 AI 辅助发布接口
       const extra = (params.extra || {}) as Record<string, unknown>;
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',

+ 5 - 5
server/src/automation/platforms/bilibili.ts

@@ -12,9 +12,7 @@ import type {
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
 import { aiService } from '../../ai/index.js';
-
-// Python 多平台发布服务配置
-const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
+import { getPythonServiceBaseUrl } from '../../services/PythonServiceConfigService.js';
 
 // 服务器根目录(用于构造绝对路径)
 const SERVER_ROOT = path.resolve(process.cwd());
@@ -134,7 +132,8 @@ export class BilibiliAdapter extends BasePlatformAdapter {
    */
   private async checkPythonServiceAvailable(): Promise<boolean> {
     try {
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/health`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/health`, {
         method: 'GET',
         signal: AbortSignal.timeout(3000),
       });
@@ -182,7 +181,8 @@ export class BilibiliAdapter extends BasePlatformAdapter {
 
       // 使用 AI 辅助发布接口
       const extra = (params.extra || {}) as Record<string, unknown>;
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',

+ 7 - 6
server/src/automation/platforms/douyin.ts

@@ -12,9 +12,7 @@ import type {
 } from './base.js';
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
-
-// Python 多平台发布服务配置
-const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
+import { getPythonServiceBaseUrl } from '../../services/PythonServiceConfigService.js';
 
 // 服务器根目录(用于构造绝对路径)
 const SERVER_ROOT = path.resolve(process.cwd());
@@ -874,7 +872,8 @@ export class DouyinAdapter extends BasePlatformAdapter {
    */
   private async checkPythonServiceAvailable(): Promise<boolean> {
     try {
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/health`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/health`, {
         method: 'GET',
         signal: AbortSignal.timeout(3000),
       });
@@ -913,7 +912,8 @@ export class DouyinAdapter extends BasePlatformAdapter {
 
       // 使用 AI 辅助发布接口
       const extra = (params.extra || {}) as Record<string, unknown>;
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -1711,7 +1711,8 @@ export class DouyinAdapter extends BasePlatformAdapter {
   private async getCommentsViaPython(cookies: string, videoId: string): Promise<CommentData[]> {
     logger.info('[Douyin] Getting comments via Python API...');
 
-    const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/comments`, {
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+    const response = await fetch(`${pythonUrl}/comments`, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',

+ 7 - 8
server/src/automation/platforms/kuaishou.ts

@@ -11,9 +11,7 @@ import type {
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
 import { aiService } from '../../ai/index.js';
-
-// Python 多平台发布服务配置
-const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
+import { getPythonServiceBaseUrl } from '../../services/PythonServiceConfigService.js';
 
 // 服务器根目录(用于构造绝对路径)
 const SERVER_ROOT = path.resolve(process.cwd());
@@ -133,7 +131,8 @@ export class KuaishouAdapter extends BasePlatformAdapter {
    */
   private async checkPythonServiceAvailable(): Promise<boolean> {
     try {
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/health`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/health`, {
         method: 'GET',
         signal: AbortSignal.timeout(3000),
       });
@@ -170,7 +169,8 @@ export class KuaishouAdapter extends BasePlatformAdapter {
 
       // 使用 AI 辅助发布接口
       const extra = (params.extra || {}) as Record<string, unknown>;
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -524,14 +524,13 @@ export class KuaishouAdapter extends BasePlatformAdapter {
   private async getCommentsViaPython(cookies: string, videoId: string): Promise<CommentData[]> {
     logger.info('[Kuaishou] Getting comments via Python API...');
     
-    const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/comments`, {
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+    const response = await fetch(`${pythonUrl}/comments`, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
       },
       body: JSON.stringify({
-        platform: 'kuaishou',
-        cookie: cookies,
         work_id: videoId,
       }),
     });

+ 7 - 6
server/src/automation/platforms/weixin.ts

@@ -12,9 +12,7 @@ import type {
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
 import { aiService } from '../../ai/index.js';
-
-// Python 多平台发布服务配置
-const PYTHON_PUBLISH_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
+import { getPythonServiceBaseUrl } from '../../services/PythonServiceConfigService.js';
 
 // 服务器根目录(用于构造绝对路径)
 const SERVER_ROOT = path.resolve(process.cwd());
@@ -373,7 +371,8 @@ export class WeixinAdapter extends BasePlatformAdapter {
    */
   private async checkPythonServiceAvailable(): Promise<boolean> {
     try {
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/health`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/health`, {
         method: 'GET',
         signal: AbortSignal.timeout(3000),
       });
@@ -410,7 +409,8 @@ export class WeixinAdapter extends BasePlatformAdapter {
 
       // 使用 AI 辅助发布接口
       const extra = (params.extra || {}) as Record<string, unknown>;
-      const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/publish/ai-assisted`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -1017,7 +1017,8 @@ export class WeixinAdapter extends BasePlatformAdapter {
   private async getCommentsViaPython(cookies: string, videoId: string): Promise<CommentData[]> {
     logger.info('[Weixin] Getting comments via Python API...');
 
-    const response = await fetch(`${PYTHON_PUBLISH_SERVICE_URL}/comments`, {
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+    const response = await fetch(`${pythonUrl}/comments`, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',

+ 7 - 6
server/src/automation/platforms/xiaohongshu.ts

@@ -13,9 +13,7 @@ import type {
 import type { PlatformType, QRCodeInfo, LoginStatusResult } from '@media-manager/shared';
 import { logger } from '../../utils/logger.js';
 import { aiService } from '../../ai/index.js';
-
-// 小红书 Python API 服务配置
-const XHS_PYTHON_SERVICE_URL = process.env.XHS_SERVICE_URL || 'http://localhost:5005';
+import { getPythonServiceBaseUrl } from '../../services/PythonServiceConfigService.js';
 
 // 服务器根目录(用于构造绝对路径)
 const SERVER_ROOT = path.resolve(process.cwd());
@@ -430,7 +428,8 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
    */
   private async checkPythonServiceAvailable(): Promise<boolean> {
     try {
-      const response = await fetch(`${XHS_PYTHON_SERVICE_URL}/health`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/health`, {
         method: 'GET',
         signal: AbortSignal.timeout(3000),
       });
@@ -501,7 +500,8 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
       });
 
       // 使用 AI 辅助发布接口
-      const response = await fetch(`${XHS_PYTHON_SERVICE_URL}/publish/ai-assisted`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/publish/ai-assisted`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -1281,7 +1281,8 @@ export class XiaohongshuAdapter extends BasePlatformAdapter {
   private async getCommentsViaPython(cookies: string, videoId: string): Promise<CommentData[]> {
     logger.info('[Xiaohongshu] Getting comments via Python API...');
 
-    const response = await fetch(`${XHS_PYTHON_SERVICE_URL}/comments`, {
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+    const response = await fetch(`${pythonUrl}/comments`, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',

+ 3 - 1
server/src/config/index.ts

@@ -39,7 +39,9 @@ export const config = {
 
   // CORS 配置
   cors: {
-    origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:5173'],
+    origin: process.env.CORS_ORIGIN
+      ? process.env.CORS_ORIGIN.split(',').map(s => s.trim()).filter(Boolean)
+      : ['*'],
   },
 
   // 上传配置

+ 2 - 1
server/src/routes/analytics.ts

@@ -2,6 +2,7 @@ import { Router } from 'express';
 import { query } from 'express-validator';
 import { AnalyticsService } from '../services/AnalyticsService.js';
 import { WorkDayStatisticsService } from '../services/WorkDayStatisticsService.js';
+import { getPythonServiceBaseUrl } from '../services/PythonServiceConfigService.js';
 import { authenticate } from '../middleware/auth.js';
 import { asyncHandler } from '../middleware/error.js';
 import { validateRequest } from '../middleware/validate.js';
@@ -21,7 +22,7 @@ declare const fetch: any;
  * 默认地址: http://localhost:5005
  */
 async function callPythonAnalyticsApi(pathname: string, params: Record<string, string | number | undefined>) {
-  const base = process.env.PYTHON_API_URL || 'http://localhost:5005';
+  const base = await getPythonServiceBaseUrl();
   const url = new URL(base);
   url.pathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
 

+ 56 - 0
server/src/routes/system.ts

@@ -4,10 +4,31 @@ import { SystemService } from '../services/SystemService.js';
 import { authenticate, authorize } from '../middleware/auth.js';
 import { asyncHandler } from '../middleware/error.js';
 import { validateRequest } from '../middleware/validate.js';
+import type { Request, Response, NextFunction } from 'express';
 
 const router = Router();
 const systemService = new SystemService();
 
+function isLoopbackRequest(req: Request): boolean {
+  const ip = req.ip || '';
+  return ip === '127.0.0.1'
+    || ip === '::1'
+    || ip.startsWith('::ffff:127.');
+}
+
+function requireAdminOrLoopback(req: Request, res: Response, next: NextFunction): void {
+  if (isLoopbackRequest(req)) {
+    next();
+    return;
+  }
+
+  try {
+    authenticate(req, res, () => authorize('admin')(req, res, next));
+  } catch (e) {
+    next(e);
+  }
+}
+
 // 获取系统配置(公开)
 router.get(
   '/config',
@@ -76,6 +97,41 @@ router.put(
   })
 );
 
+router.get(
+  '/python-service',
+  requireAdminOrLoopback,
+  asyncHandler(async (_req, res) => {
+    const config = await systemService.getPythonServiceAdminConfig();
+    res.json({ success: true, data: config });
+  })
+);
+
+router.put(
+  '/python-service',
+  requireAdminOrLoopback,
+  [
+    body('url').optional().isString(),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    await systemService.updatePythonServiceAdminConfig(req.body);
+    res.json({ success: true, message: 'Python 服务配置已更新' });
+  })
+);
+
+router.post(
+  '/python-service/check',
+  requireAdminOrLoopback,
+  [
+    body('url').optional().isString(),
+    validateRequest,
+  ],
+  asyncHandler(async (req, res) => {
+    const result = await systemService.checkPythonService(req.body.url);
+    res.json({ success: true, data: result });
+  })
+);
+
 // 获取系统状态(需要管理员权限)
 router.get(
   '/status',

+ 3 - 3
server/src/routes/workDayStatistics.ts

@@ -7,6 +7,7 @@ import { authenticate } from '../middleware/auth.js';
 import { asyncHandler } from '../middleware/error.js';
 import { validateRequest } from '../middleware/validate.js';
 import { WorkDayStatisticsService } from '../services/WorkDayStatisticsService.js';
+import { getPythonServiceBaseUrl } from '../services/PythonServiceConfigService.js';
 import { AppDataSource, Work, PlatformAccount } from '../models/index.js';
 import { logger } from '../utils/logger.js';
 import { CookieManager } from '../automation/cookie.js';
@@ -537,8 +538,6 @@ router.get(
   })
 );
 
-const PYTHON_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
-
 /**
  * POST /api/work-day-statistics/sync-weixin-video/:workId
  * 同步视频号作品的每日数据(浏览器自动化 + CSV 导入)
@@ -591,7 +590,8 @@ router.post(
       );
     }
 
-    const pyRes = await fetch(`${PYTHON_SERVICE_URL}/sync_weixin_work_daily_stats`, {
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+    const pyRes = await fetch(`${pythonUrl}/sync_weixin_work_daily_stats`, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({

+ 3 - 1
server/src/scheduler/index.ts

@@ -14,6 +14,7 @@ import { XiaohongshuWorkNoteStatisticsImportService } from '../services/Xiaohong
 import { DouyinWorkStatisticsImportService } from '../services/DouyinWorkStatisticsImportService.js';
 import { WeixinVideoWorkStatisticsImportService } from '../services/WeixinVideoWorkStatisticsImportService.js';
 import { BaijiahaoWorkDailyStatisticsImportService } from '../services/BaijiahaoWorkDailyStatisticsImportService.js';
+import { getPythonServiceBaseUrl } from '../services/PythonServiceConfigService.js';
 
 /**
  * 定时任务调度器
@@ -467,7 +468,8 @@ export class TaskScheduler {
           const pythonPlatform = account.platform === 'weixin_video' ? 'weixin' : account.platform;
           
           // 调用 Python 服务执行自动回复
-          const response = await fetch('http://localhost:5005/auto-reply', {
+          const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+          const response = await fetch(`${pythonUrl}/auto-reply`, {
             method: 'POST',
             headers: {
               'Content-Type': 'application/json',

+ 16 - 12
server/src/services/HeadlessBrowserService.ts

@@ -3,9 +3,7 @@ import { chromium, type BrowserContext, type Page } from 'playwright';
 import { logger } from '../utils/logger.js';
 import { extractDeclaredNotesCountFromPostedResponse } from '../utils/xiaohongshu.js';
 import type { PlatformType } from '@media-manager/shared';
-
-// Python 服务配置
-const PYTHON_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
+import { getPythonServiceBaseUrl } from './PythonServiceConfigService.js';
 
 // 抖音 API 接口配置
 const DOUYIN_API = {
@@ -234,7 +232,7 @@ class HeadlessBrowserService {
    * 通过 Python 服务检查登录状态(浏览器访问后台页面,检测是否需要登录)
    */
   private async checkLoginStatusViaPython(platform: PlatformType, cookies: CookieData[]): Promise<CookieCheckResult> {
-    const pythonUrl = PYTHON_SERVICE_URL;
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
 
     // 构建 Cookie 字符串
     const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
@@ -683,7 +681,8 @@ class HeadlessBrowserService {
    */
   private async checkPythonServiceAvailable(): Promise<boolean> {
     try {
-      const response = await fetch(`${PYTHON_SERVICE_URL}/health`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/health`, {
         method: 'GET',
         signal: AbortSignal.timeout(3000),
       });
@@ -718,6 +717,7 @@ class HeadlessBrowserService {
 
     const cookieString = JSON.stringify(cookies);
     const pythonPlatform = platform === 'weixin_video' ? 'weixin' : platform;
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
 
     // 抖音 work_list 接口 count 最大 20,需与创作者中心一致
     const pageSize = platform === 'xiaohongshu' ? 20 : platform === 'douyin' ? 20 : 50;
@@ -735,8 +735,8 @@ class HeadlessBrowserService {
       const pageParam: number | string = useCursorPagination ? cursor : pageIndex;
       logger.info(`[Python API] Fetching works page=${String(pageParam)}, page_size=${pageSize} for ${platform}`);
 
-      logger.info(`[Python API] 调用 Python /works: platform=${platform} -> pythonPlatform=${pythonPlatform}, page=${String(pageParam)}, url=${PYTHON_SERVICE_URL}`);
-      const response: Response = await fetch(`${PYTHON_SERVICE_URL}/works`, {
+      logger.info(`[Python API] 调用 Python /works: platform=${platform} -> pythonPlatform=${pythonPlatform}, page=${String(pageParam)}, url=${pythonUrl}`);
+      const response: Response = await fetch(`${pythonUrl}/works`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -2756,7 +2756,8 @@ class HeadlessBrowserService {
       const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
 
       // 调用 Python API
-      const response = await fetch(`${PYTHON_SERVICE_URL}/account_info`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const response = await fetch(`${pythonUrl}/account_info`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -3999,10 +4000,11 @@ class HeadlessBrowserService {
   private async fetchCommentsViaPythonApi(platform: 'douyin' | 'xiaohongshu' | 'weixin', cookies: CookieData[]): Promise<WorkComments[]> {
     const allWorkComments: WorkComments[] = [];
     const cookieString = JSON.stringify(cookies);
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
 
     // 1. 先获取作品列表
     logger.info(`[${platform} Comments Python] Fetching works list...`);
-    const worksResponse = await fetch(`${PYTHON_SERVICE_URL}/works`, {
+    const worksResponse = await fetch(`${pythonUrl}/works`, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
@@ -4032,7 +4034,7 @@ class HeadlessBrowserService {
 
       try {
         logger.info(`[${platform} Comments Python] Fetching comments for work ${workId}...`);
-        const commentsResponse = await fetch(`${PYTHON_SERVICE_URL}/comments`, {
+        const commentsResponse = await fetch(`${pythonUrl}/comments`, {
           method: 'POST',
           headers: { 'Content-Type': 'application/json' },
           body: JSON.stringify({
@@ -4098,9 +4100,10 @@ class HeadlessBrowserService {
    */
   private async fetchDouyinCommentsViaPythonApi(cookies: CookieData[]): Promise<WorkComments[]> {
     const cookieString = JSON.stringify(cookies);
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
 
     logger.info('[Douyin Comments Python] Fetching all comments...');
-    const response = await fetch(`${PYTHON_SERVICE_URL}/all_comments`, {
+    const response = await fetch(`${pythonUrl}/all_comments`, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
@@ -4676,9 +4679,10 @@ class HeadlessBrowserService {
    */
   private async fetchXiaohongshuCommentsViaPythonApi(cookies: CookieData[]): Promise<WorkComments[]> {
     const cookieString = JSON.stringify(cookies);
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
 
     logger.info('[Xiaohongshu Comments Python] Fetching all comments...');
-    const response = await fetch(`${PYTHON_SERVICE_URL}/all_comments`, {
+    const response = await fetch(`${pythonUrl}/all_comments`, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({

+ 3 - 2
server/src/services/PublishService.ts

@@ -23,6 +23,7 @@ import { config } from '../config/index.js';
 import { CookieManager } from '../automation/cookie.js';
 import { taskQueueService } from './TaskQueueService.js';
 import { In } from 'typeorm';
+import { getPythonServiceBaseUrl } from './PythonServiceConfigService.js';
 
 interface GetTasksParams {
   page: number;
@@ -564,7 +565,7 @@ export class PublishService {
     const publishProxyExtra = await this.buildPublishProxyExtra(task.publishProxy);
 
     // 6. 调用 Python API(有头浏览器模式)
-    const PYTHON_SERVICE_URL = process.env.PYTHON_PUBLISH_SERVICE_URL || 'http://localhost:5005';
+    const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
 
     logger.info(`[Headful Publish] Starting headful browser publish for account ${account.accountName} (${account.platform})`);
 
@@ -589,7 +590,7 @@ export class PublishService {
         ? videoPath
         : path.resolve(process.cwd(), videoPath);
 
-      const response = await fetch(`${PYTHON_SERVICE_URL}/publish/ai-assisted`, {
+      const response = await fetch(`${pythonUrl}/publish/ai-assisted`, {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({

+ 67 - 0
server/src/services/PythonServiceConfigService.ts

@@ -0,0 +1,67 @@
+import { AppDataSource, SystemConfig } from '../models/index.js';
+
+const FALLBACK_PYTHON_SERVICE_URL =
+  process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || process.env.PYTHON_API_URL || 'http://localhost:5005';
+
+const CONFIG_KEY = 'python_publish_service_url';
+const CACHE_TTL_MS = 30_000;
+
+let cached: { url: string; expiresAt: number } | null = null;
+
+export function invalidatePythonServiceBaseUrlCache(): void {
+  cached = null;
+}
+
+export async function getPythonServiceBaseUrl(): Promise<string> {
+  const now = Date.now();
+  if (cached && cached.expiresAt > now) return cached.url;
+
+  let fromDb = '';
+  try {
+    if (AppDataSource.isInitialized) {
+      const repo = AppDataSource.getRepository(SystemConfig);
+      const row = await repo.findOne({ where: { configKey: CONFIG_KEY } });
+      fromDb = String(row?.configValue || '').trim();
+    }
+  } catch {
+    fromDb = '';
+  }
+
+  const url = fromDb || FALLBACK_PYTHON_SERVICE_URL;
+  cached = { url, expiresAt: now + CACHE_TTL_MS };
+  return url;
+}
+
+export async function checkPythonServiceHealth(url?: string): Promise<{
+  ok: boolean;
+  status?: number;
+  error?: string;
+  data?: any;
+}> {
+  const baseUrl = (url || (await getPythonServiceBaseUrl())).replace(/\/$/, '');
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 5000);
+
+  try {
+    const res = await fetch(`${baseUrl}/health`, {
+      method: 'GET',
+      signal: controller.signal,
+      headers: { 'Content-Type': 'application/json' },
+    });
+    const status = res.status;
+    const text = await res.text();
+    let data: any = null;
+    try {
+      data = text ? JSON.parse(text) : null;
+    } catch {
+      data = text || null;
+    }
+    return { ok: res.ok, status, data };
+  } catch (e) {
+    const error = e instanceof Error ? e.message : '请求失败';
+    return { ok: false, error };
+  } finally {
+    clearTimeout(timeoutId);
+  }
+}
+

+ 33 - 0
server/src/services/SystemService.ts

@@ -3,6 +3,11 @@ import type { SystemConfig as SystemConfigType } from '@media-manager/shared';
 import { AVAILABLE_PLATFORM_TYPES } from '@media-manager/shared';
 import { wsManager } from '../websocket/index.js';
 import { RegionService, type ChinaRegionOption } from './RegionService.js';
+import {
+  checkPythonServiceHealth,
+  getPythonServiceBaseUrl,
+  invalidatePythonServiceBaseUrlCache,
+} from './PythonServiceConfigService.js';
 
 interface UpdateConfigParams {
   allowRegistration?: boolean;
@@ -20,6 +25,15 @@ export interface UpdatePublishProxyAdminConfig {
   signature?: string;
 }
 
+export interface PythonServiceAdminConfig {
+  url: string;
+  effectiveUrl: string;
+}
+
+export interface UpdatePythonServiceAdminConfig {
+  url?: string;
+}
+
 interface SystemStatus {
   database: 'connected' | 'disconnected';
   redis: 'connected' | 'disconnected';
@@ -88,6 +102,25 @@ export class SystemService {
     }
   }
 
+  async getPythonServiceAdminConfig(): Promise<PythonServiceAdminConfig> {
+    const configs = await this.configRepository.find();
+    const configMap = new Map(configs.map(c => [c.configKey, c.configValue]));
+    const url = (configMap.get('python_publish_service_url') || '').trim();
+    const effectiveUrl = (await getPythonServiceBaseUrl()).trim();
+    return { url, effectiveUrl };
+  }
+
+  async updatePythonServiceAdminConfig(params: UpdatePythonServiceAdminConfig): Promise<void> {
+    if (params.url !== undefined) {
+      await this.setConfig('python_publish_service_url', String(params.url || '').trim());
+      invalidatePythonServiceBaseUrlCache();
+    }
+  }
+
+  async checkPythonService(url?: string) {
+    return checkPythonServiceHealth(url);
+  }
+
   async getPublishProxyCitiesFromApi(): Promise<string[]> {
     const regions = await this.regionService.getChinaRegionsFromCsv();
     const citySet = new Set<string>();

+ 3 - 4
server/src/services/WeixinVideoWorkStatisticsImportService.ts

@@ -11,9 +11,7 @@
 import { AppDataSource, PlatformAccount, Work } from '../models/index.js';
 import { logger } from '../utils/logger.js';
 import { CookieManager } from '../automation/cookie.js';
-
-const PYTHON_SERVICE_URL =
-  process.env.PYTHON_PUBLISH_SERVICE_URL || process.env.XHS_SERVICE_URL || 'http://localhost:5005';
+import { getPythonServiceBaseUrl } from './PythonServiceConfigService.js';
 
 function tryDecryptCookieData(cookieData: string | null): string | null {
   if (!cookieData) return null;
@@ -119,7 +117,8 @@ export class WeixinVideoWorkStatisticsImportService {
     );
 
     try {
-      const pyRes = await fetch(`${PYTHON_SERVICE_URL}/sync_weixin_account_works_daily_stats`, {
+      const pythonUrl = (await getPythonServiceBaseUrl()).replace(/\/$/, '');
+      const pyRes = await fetch(`${pythonUrl}/sync_weixin_account_works_daily_stats`, {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({