飞书同步组织架构到本地

py代码案例:

bash 复制代码
在这里插入代码片import requests, json, os

APP_ID     = "xxxxe"
APP_SECRET = "Rxxxxxxxqjm"
BASE       = "https://open.feishu.cn/open-apis"

# ── 鉴权 ──────────────────────────────────────────────────────
def get_token():
    r = requests.post(f"{BASE}/auth/v3/tenant_access_token/internal",
                      json={"app_id": APP_ID, "app_secret": APP_SECRET}).json()
    if r.get("code") != 0:
        raise Exception(f"Token 获取失败: {r.get('msg')}")
    return r["tenant_access_token"]

H = {"Authorization": f"Bearer {get_token()}", "Content-Type": "application/json"}

# ── 分页通用封装 ───────────────────────────────────────────────
def paginate(url, params):
    items, pt = [], ""
    while True:
        if pt: params["page_token"] = pt
        d = requests.get(url, headers=H, params=params).json().get("data", {})
        items.extend(d.get("items", []))
        pt = d.get("page_token")
        if not d.get("has_more") or not pt:
            break
    return items

# ── 拉取全量部门 ───────────────────────────────────────────────
def fetch_depts():
    items = paginate(f"{BASE}/contact/v3/departments", {
        "department_id": "0", "department_id_type": "open_department_id",
        "fetch_child": True, "page_size": 50
    })
    print(f"✅ 部门:{len(items)} 个")
    return items

# ── 逐部门拉取成员 ─────────────────────────────────────────────
def fetch_members(all_depts):
    dept_map = {}
    dept_ids = [d["open_department_id"] for d in all_depts]  # 根部门(0)通常无直属成员,跳过
    print(f"正在拉取 {len(dept_ids)} 个部门的成员...")

    for dept_id in dept_ids:
        res = requests.get(f"{BASE}/contact/v3/users/find_by_department", headers=H, params={
            "department_id": dept_id, "department_id_type": "open_department_id", "page_size": 50
        }).json()
        if res.get("code") != 0:
            continue  # 无权限则跳过,不打印噪音
        users = [{"name": u["name"], "title": u.get("job_title", ""),
                  "avatar": u.get("avatar", {}).get("avatar_72", "")}
                 for u in res.get("data", {}).get("items", [])]
        if users:
            dept_map[dept_id] = users

    print(f"✅ 成员:{sum(len(v) for v in dept_map.values())} 名")
    return dept_map

# ── 递归构建树 ────────────────────────────────────────────────
def build_tree(all_depts, member_map, parent_id):
    children = sorted([d for d in all_depts if d["parent_department_id"] == parent_id],
                      key=lambda x: int(x.get("order", 0)))
    return [{"name": d["name"], "id": d["open_department_id"],
             "members": member_map.get(d["open_department_id"], []),
             "children": build_tree(all_depts, member_map, d["open_department_id"])}
            for d in children]

# ── 主流程 ────────────────────────────────────────────────────
root_name = requests.get(f"{BASE}/contact/v3/departments/0", headers=H,
    params={"department_id_type": "open_department_id"}).json(
    ).get("data", {}).get("department", {}).get("name", "企业")

all_depts  = fetch_depts()
member_map = fetch_members(all_depts)
tree_data  = {"name": root_name, "id": "0", "members": [],
              "children": build_tree(all_depts, member_map, "0")}

# ── 生成 HTML ─────────────────────────────────────────────────
tree_json = json.dumps(tree_data, ensure_ascii=False)
html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"><title>组织架构图</title>
<style>
*{{box-sizing:border-box;margin:0;padding:0}}
body{{font-family:"PingFang SC","Microsoft YaHei",sans-serif;background:#f5f6f8;color:#1a1a1a}}
header{{background:#fff;border-bottom:1px solid #e8e8e8;padding:16px 24px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}}
header h1{{font-size:16px;font-weight:600}}
.layout{{display:flex;height:calc(100vh - 57px)}}
.sidebar{{width:280px;background:#fff;border-right:1px solid #e8e8e8;overflow-y:auto;padding:8px 0;flex-shrink:0}}
.main{{flex:1;overflow-y:auto;padding:24px}}
.label{{display:flex;align-items:center;gap:8px;padding:8px 16px;cursor:pointer;border-radius:6px;margin:1px 8px;transition:background .15s;font-size:13px}}
.label:hover{{background:#f0f2f5}}
.label.active{{background:#e6f0ff;color:#1677ff}}
.toggle{{width:14px;font-size:10px;color:#999;transition:transform .2s}}
.toggle.open{{transform:rotate(90deg)}}
.toggle.leaf{{opacity:0}}
.children{{padding-left:18px}}
.hidden{{display:none}}
.card{{background:#fff;border-radius:10px;border:1px solid #e8e8e8;padding:24px;margin-bottom:16px}}
.dept-title{{font-size:20px;font-weight:600;margin-bottom:12px}}
.members{{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}}
.member{{background:#f8f9fc;border:1px solid #e8e8e8;border-radius:8px;padding:12px;display:flex;align-items:center;gap:12px}}
.avatar{{width:38px;height:38px;border-radius:50%;background:#1677ff;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;overflow:hidden;flex-shrink:0}}
.avatar img{{width:100%;height:100%;object-fit:cover}}
.mname{{font-size:14px;font-weight:500}}
.mtitle{{font-size:12px;color:#999}}
input{{width:calc(100% - 24px);padding:8px 12px;border:1px solid #e8e8e8;border-radius:6px;margin:8px 12px;outline:none;font-size:13px}}
</style>
</head>
<body>
<header>
  <div style="width:32px;height:32px;background:#1677ff;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold">Org</div>
  <h1>组织架构预览</h1>
</header>
<div class="layout">
  <aside class="sidebar">
    <input placeholder="搜索部门..." oninput="search(this.value)">
    <div id="tree"></div>
  </aside>
  <main class="main" id="main"></main>
</div>
<script>
const DATA = {tree_json};
const idx = {{}};
function buildIdx(n) {{ idx[n.id]=n; n.children?.forEach(buildIdx); }}
buildIdx(DATA);

function renderNode(node, depth=0) {{
  const hasChild = node.children?.length > 0;
  const el = document.createElement('div');
  el.className = 'node'; el.dataset.name = node.name;
  const label = document.createElement('div');
  label.className = 'label';
  label.innerHTML = `<span class="toggle ${{hasChild?'':'leaf'}}">▶</span>
    <span>${{depth===0?'🏢':'📁'}}</span>
    <span style="flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${{node.name}}</span>`;
  label.onclick = e => {{
    e.stopPropagation();
    document.querySelectorAll('.label').forEach(l=>l.classList.remove('active'));
    label.classList.add('active');
    showDetail(node.id);
    if(hasChild) {{
      el.querySelector(':scope>.children').classList.toggle('hidden');
      label.querySelector('.toggle').classList.toggle('open');
    }}
  }};
  el.appendChild(label);
  if(hasChild) {{
    const ch = document.createElement('div');
    ch.className = 'children hidden';
    node.children.forEach(c => ch.appendChild(renderNode(c, depth+1)));
    el.appendChild(ch);
  }}
  return el;
}}

function showDetail(id) {{
  const node = idx[id];
  let html = `<div class="card"><div class="dept-title">📁 ${{node.name}}</div>`;
  if(node.members?.length) {{
    html += '<div class="members">' + node.members.map(m => `
      <div class="member">
        <div class="avatar">${{m.avatar?`<img src="${{m.avatar}}">`:m.name[0]}}</div>
        <div><div class="mname">${{m.name}}</div><div class="mtitle">${{m.title||'员工'}}</div></div>
      </div>`).join('') + '</div>';
  }} else {{
    html += '<div style="color:#999">暂无直属成员</div>';
  }}
  document.getElementById('main').innerHTML = html + '</div>';
}}

function search(q) {{
  document.querySelectorAll('.node').forEach(n => {{
    n.style.display = q && !n.dataset.name.toLowerCase().includes(q.toLowerCase()) ? 'none' : 'block';
  }});
}}

document.getElementById('tree').appendChild(renderNode(DATA));
showDetail(DATA.id);
</script>
</body>
</html>"""

out = "org_chart.html"
with open(out, "w", encoding="utf-8") as f:
    f.write(html)
print(f"🎉 完成!打开: {os.path.abspath(out)}")
相关推荐
w3296362715 小时前
飞书接单到月度结算:我用一个 DSH 插件跑通鞋类采购 WMS,附 22 项验收
pdf·node.js·飞书·后端开发·ai agent
宝哥聊编程12 天前
主流 Agent 平台对比:Coze、Dify、飞书、扣子、OpenClaw 落地实操手册
前端·人工智能·飞书
guoran_shini14 天前
Agent系统服务化封装和IM接入
飞书
mudtools16 天前
在飞书 SDK 上用 Microsoft Agent Framework 搭一个能干活的工作搭子
人工智能·ai·.net·飞书·agent
极致了17 天前
飞书插件、RPA、API三条路的真实分界线
飞书·rpa
玩泥巴的18 天前
在飞书 SDK 上用 Microsoft Agent Framework 搭一个能干活的工作搭子
ai·.net·飞书·agent
玩泥巴的19 天前
一份声明,多份产物:飞书 SDK 的 AI 工具面是怎么生成的
ai·.net·飞书·agent
OpenPortal认证计费系统21 天前
基于 飞书认证 通过 华为、H3C 结合 OpenPortal认证计费系统 实现 网络准入 飞书授权 实名认证
华为·飞书·h3c·portal认证·openportal
前进的李工22 天前
飞书企业知识库设计+后端高频八股
数据库·redis·后端·mysql·性能优化·飞书·rag