# 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>` — key=deptId, value=1-based depth - [ ] **Step 1: Add `getDeptDepthMap` after `getAllDeptIds` (after line 263)** ```typescript /** * 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> { const depthMap = new Map(); 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** ```bash 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` 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`: ```typescript 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(); 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: ```typescript // 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: ```typescript // 组装成树(父节点可能已被过滤,缺失的父节点 → 节点提升为根) const map = new Map(); 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** ```bash git add apps/server/src/integration/dingtalk.service.ts git commit -m "feat: filter org-tree-with-users to deepest 2 levels" ```