Files
gongxue-base/docs/superpowers/plans/2026-07-09-org-tree-deepest-levels.md

6.6 KiB

Org Tree Deepest Levels — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. Required skills per agent: superpowers:vercel-react-best-practices, superpowers:ui-ux-pro-max

Goal: fetchOrgTreeWithUsers only returns departments at the deepest 2 levels of the global org tree.

Architecture: Add a getDeptDepthMap BFS method that mirrors getAllDeptIds but tracks level. Insert depth filtering in fetchOrgTreeWithUsers before the detail+user fetch loop, slashing API calls.

Tech Stack: NestJS + TypeScript, DingTalk Open API v2

Global Constraints

  • Depth is computed from rootDeptId=1 globally, regardless of the user's rootDeptId param
  • fetchOrgTree (dept picker) is NOT modified
  • syncAll is NOT modified
  • Frontend receives the same DingOrgTreeNodeWithUsers[] shape, zero frontend changes

Task 1: Add getDeptDepthMap method

Files:

  • Modify: apps/server/src/integration/dingtalk.service.ts (insert after getAllDeptIds)

Interfaces:

  • Consumes: nothing new (uses existing rateLimit(), SubDeptIdListResponse, DingTalk listsubid API)

  • Produces: private async getDeptDepthMap(token: string): Promise<Map<number, number>> — key=deptId, value=1-based depth

  • Step 1: Add getDeptDepthMap after getAllDeptIds (after line 263)

  /**
   * BFS from rootDeptId=1, returns depth of every department.
   * Depth is 1-based (root=1). Uses listsubid API only, no detail fetches.
   */
  private async getDeptDepthMap(token: string): Promise<Map<number, number>> {
    const depthMap = new Map<number, number>();
    const queue: Array<{ id: number; depth: number }> = [{ id: 1, depth: 1 }];

    while (queue.length > 0) {
      const { id, depth } = queue.shift()!;
      depthMap.set(id, depth);

      try {
        await this.rateLimit();
        const res = await fetch(
          `https://oapi.dingtalk.com/topapi/v2/department/listsubid?access_token=${token}`,
          {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ dept_id: id }),
          },
        );
        const body: SubDeptIdListResponse = await res.json();
        if (body.errcode === 0 && body.result?.dept_id_list) {
          for (const childId of body.result.dept_id_list) {
            queue.push({ id: childId, depth: depth + 1 });
          }
        }
      } catch (e) {
        this.logger.error(`getDeptDepthMap 获取部门 ${id} 子部门失败: ${(e as Error).message}`);
      }
    }

    return depthMap;
  }
  • Step 2: Verify it compiles

Run: cd apps/server && npx tsc --noEmit Expected: no new errors (may have pre-existing ones in other files)

  • Step 3: Commit
git add apps/server/src/integration/dingtalk.service.ts
git commit -m "feat: add getDeptDepthMap BFS method for org tree depth tracking"

Task 2: Filter fetchOrgTreeWithUsers to deepest 2 levels

Files:

  • Modify: apps/server/src/integration/dingtalk.service.ts:476-547

Interfaces:

  • Consumes: getDeptDepthMap(token) from Task 1

  • Produces: same Promise<DingOrgTreeNodeWithUsers[]> return type, now filtered

  • Step 1: Insert depth filtering before the detail+user fetch loop

Replace lines 480-481 (const token = ...; const deptIds = ...;) with the depth-aware version, and wrap the loop to use filteredIds:

    const token = await this.getAccessToken();

    // Compute global depth map (always from rootDeptId=1)
    const depthMap = await this.getDeptDepthMap(token);
    const maxDepth = Math.max(...depthMap.values());

    // Get subtree IDs for the user's selected root
    const subtreeIds = await this.getAllDeptIds(token, rootDeptId);

    // Filter: only keep departments at the deepest 2 levels of the global tree
    const filteredIds = subtreeIds.filter((id) => {
      const d = depthMap.get(id) ?? -1;
      return d === maxDepth || d === maxDepth - 1;
    });

    if (filteredIds.length === 0) {
      return [];
    }

    // 拉每个部门的详情(仅过滤后的)
    const nodes: DingOrgTreeNodeWithUsers[] = [];
    const userDeptMap = new Map<string, number[]>();

    for (let i = 0; i < filteredIds.length; i++) {
      if (i > 0) await this.delay(i);
      const detail = await this.getDeptDetail(token, filteredIds[i]);

And update the deptId reference inside the loop body — line 494 uses deptIds[i]:

Change line 494:

      // Before:
      this.logger.log(`[dingtalk] dept ${deptIds[i]} (${detail.name}): ${dingUsers.length} users`);
      // After:
      this.logger.log(`[dingtalk] dept ${filteredIds[i]} (${detail.name}): ${dingUsers.length} users`);

And the getDeptUsers call on line 493 must also use filteredIds[i] instead of deptIds[i] — it already does since we're iterating filteredIds.

  • Step 2: Update tree assembly to handle missing parent nodes

The tree assembly at lines 534-546 currently checks node.id !== rootDeptId to decide if a node is a root. Since intermediate levels are filtered out, a maxDepth-1 node's parent won't be in the map. Update the root detection:

    // 组装成树(父节点可能已被过滤,缺失的父节点 → 节点提升为根)
    const map = new Map<number, DingOrgTreeNodeWithUsers>();
    nodes.forEach((n) => map.set(n.id, n));
    const roots: DingOrgTreeNodeWithUsers[] = [];
    for (const node of nodes) {
      const parent = map.get(node.parentId);
      if (parent) {
        parent.children.push(node);
      } else {
        roots.push(node);
      }
    }

Note: removed the && node.id !== rootDeptId condition — it's redundant with the map.get check when intermediate levels are filtered. Nodes whose parent exists in map get attached; those whose parent was filtered out become roots. Same behavior, simpler logic.

  • Step 3: Verify it compiles

Run: cd apps/server && npx tsc --noEmit Expected: no new errors

  • Step 4: Smoke test with a real DingTalk config (if available)

Run: curl -s http://localhost:3000/api/sync/dingtalk/org-tree-with-users | jq '. | length' Expected: returns departments; check that the response depth is at most 2 levels (manual inspection of parentId chains, or verify response size is smaller than before).

  • Step 5: Commit
git add apps/server/src/integration/dingtalk.service.ts
git commit -m "feat: filter org-tree-with-users to deepest 2 levels"