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