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 可从机器人日志中获取。
+
+ 点击"刷新群列表"从飞书拉取机器人所在的群 +
+