From 88a79d19362f7c1b8d058977b83e881f26b29377 Mon Sep 17 00:00:00 2001
From: Jeason <1710884619@qq.com>
Date: Thu, 2 Apr 2026 15:30:02 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A3=9E=E4=B9=A6=E7=BE=A4=E8=87=AA?=
=?UTF-8?q?=E5=8A=A8=E5=8F=91=E7=8E=B0=E4=B8=8E=E5=8F=AF=E8=A7=86=E5=8C=96?=
=?UTF-8?q?=E7=BB=91=E5=AE=9A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- FeishuService 新增 list_bot_chats() 拉取机器人所在的所有群(含群名、chat_id)
- 新增 GET /api/tenants/feishu-groups 端点,返回群列表并标注每个群当前绑定的租户
- 租户编辑弹窗改为可视化群列表(checkbox 勾选绑定),替代手动填 chat_id
- 已绑定其他租户的群显示为禁用状态,防止重复绑定
- 编辑租户时自动加载群列表
- 新建租户时可手动刷新群列表
---
src/integrations/feishu_service.py | 46 +++++++++++
src/web/blueprints/tenants.py | 32 +++++++
src/web/static/js/modules/tenants.js | 119 +++++++++++++++++----------
src/web/templates/dashboard.html | 9 +-
4 files changed, 159 insertions(+), 47 deletions(-)
diff --git a/src/integrations/feishu_service.py b/src/integrations/feishu_service.py
index 58db62a..7f4bda5 100644
--- a/src/integrations/feishu_service.py
+++ b/src/integrations/feishu_service.py
@@ -128,3 +128,49 @@ class FeishuService:
except requests.RequestException as e:
logger.error(f"回复消息时发生网络错误: {e}")
return False
+
+ def list_bot_chats(self) -> list:
+ """
+ 获取机器人所在的所有群列表。
+ 返回 [{"chat_id": "oc_xxx", "name": "群名", "chat_type": "group"}]
+ """
+ token = self._get_tenant_access_token()
+ if not token:
+ return []
+
+ all_chats = []
+ page_token = ""
+ while True:
+ url = f"{self.BASE_URL}/im/v1/chats?page_size=100"
+ if page_token:
+ url += f"&page_token={page_token}"
+ headers = {"Authorization": f"Bearer {token}"}
+
+ try:
+ response = requests.get(url, headers=headers)
+ response.raise_for_status()
+ data = response.json()
+
+ if data.get("code") != 0:
+ logger.error(f"获取群列表失败: {data.get('msg')}")
+ break
+
+ items = data.get("data", {}).get("items", [])
+ for item in items:
+ all_chats.append({
+ "chat_id": item.get("chat_id", ""),
+ "name": item.get("name", "未命名群"),
+ "description": item.get("description", ""),
+ "chat_type": item.get("chat_type", ""),
+ "owner_id": item.get("owner_id", ""),
+ })
+
+ if not data.get("data", {}).get("has_more", False):
+ break
+ page_token = data.get("data", {}).get("page_token", "")
+ except requests.RequestException as e:
+ logger.error(f"获取群列表网络错误: {e}")
+ break
+
+ logger.info(f"获取到 {len(all_chats)} 个机器人所在的群")
+ return all_chats
diff --git a/src/web/blueprints/tenants.py b/src/web/blueprints/tenants.py
index c71c8ae..c3bc826 100644
--- a/src/web/blueprints/tenants.py
+++ b/src/web/blueprints/tenants.py
@@ -43,6 +43,38 @@ def list_tenants():
return jsonify({"error": str(e)}), 500
+@tenants_bp.route('/feishu-groups', methods=['GET'])
+def list_feishu_groups():
+ """获取机器人所在的所有飞书群,并标注每个群当前绑定的租户"""
+ try:
+ from src.integrations.feishu_service import FeishuService
+ feishu = FeishuService()
+ groups = feishu.list_bot_chats()
+
+ # 构建 chat_id → tenant_id 映射
+ bound_map = {}
+ with db_manager.get_session() as session:
+ tenants = session.query(Tenant).filter(Tenant.is_active == True).all()
+ for t in tenants:
+ if not t.config:
+ continue
+ try:
+ cfg = json.loads(t.config)
+ except (json.JSONDecodeError, TypeError):
+ continue
+ for cid in cfg.get('feishu', {}).get('chat_groups', []):
+ bound_map[cid] = t.tenant_id
+
+ # 给每个群标注绑定状态
+ for g in groups:
+ g['bound_tenant_id'] = bound_map.get(g['chat_id'], None)
+
+ return jsonify({"success": True, "groups": groups})
+ except Exception as e:
+ logger.error(f"获取飞书群列表失败: {e}")
+ return jsonify({"error": str(e)}), 500
+
+
@tenants_bp.route('', methods=['POST'])
def create_tenant():
"""创建新租户"""
diff --git a/src/web/static/js/modules/tenants.js b/src/web/static/js/modules/tenants.js
index 095e5b4..9126ced 100644
--- a/src/web/static/js/modules/tenants.js
+++ b/src/web/static/js/modules/tenants.js
@@ -4,16 +4,13 @@ Object.assign(TSPDashboard.prototype, {
const container = document.getElementById('tenant-list');
if (!container) return;
container.innerHTML = '
';
-
try {
const response = await fetch('/api/tenants');
const tenants = await response.json();
-
if (!Array.isArray(tenants) || tenants.length === 0) {
container.innerHTML = '暂无租户,请点击"新建租户"创建
';
return;
}
-
container.innerHTML = tenants.map(t => {
const chatGroups = t.config?.feishu?.chat_groups || [];
return `
@@ -23,21 +20,15 @@ Object.assign(TSPDashboard.prototype, {
${t.name}
(${t.tenant_id})
${t.description ? `
${t.description}` : ''}
- ${!t.is_active ? '已禁用' : ''}
${chatGroups.length > 0 ? `${chatGroups.length} 个飞书群` : '未绑定飞书群'}
-
- ${t.tenant_id !== 'default' ? `
- ` : ''}
+
+ ${t.tenant_id !== 'default' ? `` : ''}
-
- `}).join('');
+ `;
+ }).join('');
} catch (error) {
console.error('加载租户列表失败:', error);
container.innerHTML = '加载失败
';
@@ -53,6 +44,7 @@ Object.assign(TSPDashboard.prototype, {
document.getElementById('tenant-name-input').value = '';
document.getElementById('tenant-desc-input').value = '';
document.getElementById('tenant-feishu-chatgroups').value = '';
+ document.getElementById('feishu-groups-list').innerHTML = '点击"刷新群列表"从飞书拉取机器人所在的群';
new bootstrap.Modal(document.getElementById('tenantModal')).show();
},
@@ -62,8 +54,6 @@ Object.assign(TSPDashboard.prototype, {
document.getElementById('tenant-id-input').value = tenantId;
document.getElementById('tenant-id-input').disabled = true;
document.getElementById('tenant-id-group').style.display = 'none';
-
- // 加载租户完整数据
try {
const resp = await fetch('/api/tenants');
const tenants = await resp.json();
@@ -71,14 +61,64 @@ Object.assign(TSPDashboard.prototype, {
if (tenant) {
document.getElementById('tenant-name-input').value = tenant.name || '';
document.getElementById('tenant-desc-input').value = tenant.description || '';
- const chatGroups = tenant.config?.feishu?.chat_groups || [];
- document.getElementById('tenant-feishu-chatgroups').value = chatGroups.join('\n');
+ document.getElementById('tenant-feishu-chatgroups').value = (tenant.config?.feishu?.chat_groups || []).join('\n');
}
- } catch (e) {
- console.warn('加载租户数据失败:', e);
- }
-
+ } catch (e) { console.warn('加载租户数据失败:', e); }
+ // 自动加载飞书群列表
+ document.getElementById('feishu-groups-list').innerHTML = '加载群列表中...';
new bootstrap.Modal(document.getElementById('tenantModal')).show();
+ this.loadFeishuGroups();
+ },
+
+ async loadFeishuGroups() {
+ const container = document.getElementById('feishu-groups-list');
+ const currentTenantId = document.getElementById('tenant-edit-id').value || document.getElementById('tenant-id-input').value.trim();
+ const boundRaw = document.getElementById('tenant-feishu-chatgroups').value.trim();
+ const boundSet = new Set(boundRaw ? boundRaw.split('\n').map(s => s.trim()).filter(Boolean) : []);
+
+ container.innerHTML = '正在从飞书拉取群列表...';
+ try {
+ const resp = await fetch('/api/tenants/feishu-groups');
+ const data = await resp.json();
+ if (!data.success || !data.groups || data.groups.length === 0) {
+ container.innerHTML = '未找到机器人所在的群,请确认飞书配置正确';
+ return;
+ }
+ const groups = data.groups;
+ container.innerHTML = `
+
+ ${groups.map(g => {
+ const isBoundHere = boundSet.has(g.chat_id);
+ const isBoundElsewhere = g.bound_tenant_id && g.bound_tenant_id !== currentTenantId;
+ const checked = isBoundHere ? 'checked' : '';
+ const disabled = isBoundElsewhere ? 'disabled' : '';
+ const badge = isBoundElsewhere
+ ? `
已绑定: ${g.bound_tenant_id}`
+ : isBoundHere
+ ? '
已绑定'
+ : '';
+ return `
+
`;
+ }).join('')}
+
+ 勾选要绑定到此租户的群,已绑定其他租户的群不可重复绑定
+ `;
+ } catch (error) {
+ console.error('加载飞书群列表失败:', error);
+ container.innerHTML = '加载飞书群列表失败: ' + error.message + '';
+ }
+ },
+
+ _getSelectedChatGroups() {
+ const checkboxes = document.querySelectorAll('.feishu-group-cb:checked:not(:disabled)');
+ return Array.from(checkboxes).map(cb => cb.value);
},
async saveTenant() {
@@ -87,11 +127,14 @@ Object.assign(TSPDashboard.prototype, {
const name = document.getElementById('tenant-name-input').value.trim();
const description = document.getElementById('tenant-desc-input').value.trim();
- // 飞书群绑定
- const chatGroupsText = document.getElementById('tenant-feishu-chatgroups').value.trim();
- const chatGroups = chatGroupsText ? chatGroupsText.split('\n').map(s => s.trim()).filter(Boolean) : [];
+ // 从勾选的 checkbox 获取群列表,如果群列表 UI 没加载则回退到 hidden input
+ let chatGroups = this._getSelectedChatGroups();
+ if (chatGroups.length === 0) {
+ const raw = document.getElementById('tenant-feishu-chatgroups').value.trim();
+ chatGroups = raw ? raw.split('\n').map(s => s.trim()).filter(Boolean) : [];
+ }
- // 构建 config,保留已有的非 feishu 配置
+ // 保留已有配置
let existingConfig = {};
if (editId) {
try {
@@ -104,12 +147,9 @@ Object.assign(TSPDashboard.prototype, {
const config = { ...existingConfig };
if (chatGroups.length > 0) {
config.feishu = { ...(config.feishu || {}), chat_groups: chatGroups };
- } else {
- // 清空飞书群绑定
- if (config.feishu) {
- delete config.feishu.chat_groups;
- if (Object.keys(config.feishu).length === 0) delete config.feishu;
- }
+ } else if (config.feishu) {
+ delete config.feishu.chat_groups;
+ if (Object.keys(config.feishu).length === 0) delete config.feishu;
}
if (!name) { this.showNotification('租户名称不能为空', 'error'); return; }
@@ -118,28 +158,23 @@ Object.assign(TSPDashboard.prototype, {
let response;
if (editId) {
response = await fetch(`/api/tenants/${encodeURIComponent(editId)}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
+ method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description, config })
});
} else {
if (!tenantId) { this.showNotification('租户标识不能为空', 'error'); return; }
response = await fetch('/api/tenants', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenant_id: tenantId, name, description, config })
});
}
-
const data = await response.json();
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('tenantModal')).hide();
this.showNotification(editId ? '租户已更新' : '租户已创建', 'success');
this.loadTenantList();
this.populateTenantSelectors();
- } else {
- this.showNotification(data.error || '操作失败', 'error');
- }
+ } else { this.showNotification(data.error || '操作失败', 'error'); }
} catch (error) {
console.error('保存租户失败:', error);
this.showNotification('保存租户失败: ' + error.message, 'error');
@@ -147,7 +182,7 @@ Object.assign(TSPDashboard.prototype, {
},
async deleteTenant(tenantId) {
- if (!confirm(`确定要删除租户 "${tenantId}" 吗?该操作不会删除关联数据。`)) return;
+ if (!confirm(`确定要删除租户 "${tenantId}" 吗?`)) return;
try {
const response = await fetch(`/api/tenants/${encodeURIComponent(tenantId)}`, { method: 'DELETE' });
const data = await response.json();
@@ -155,9 +190,7 @@ Object.assign(TSPDashboard.prototype, {
this.showNotification('租户已删除', 'success');
this.loadTenantList();
this.populateTenantSelectors();
- } else {
- this.showNotification(data.error || '删除失败', 'error');
- }
+ } else { this.showNotification(data.error || '删除失败', 'error'); }
} catch (error) {
console.error('删除租户失败:', error);
this.showNotification('删除租户失败: ' + error.message, 'error');
diff --git a/src/web/templates/dashboard.html b/src/web/templates/dashboard.html
index 814c7a0..02514f6 100644
--- a/src/web/templates/dashboard.html
+++ b/src/web/templates/dashboard.html
@@ -2266,11 +2266,12 @@
- 飞书群绑定
+ 飞书群绑定
-
-
-
将飞书群绑定到此租户,机器人在该群收到消息时自动使用此租户的知识库。Chat ID 可从机器人日志中获取。
+
+ 点击"刷新群列表"从飞书拉取机器人所在的群
+
+