diff --git a/lzwcai_lark_mcp/lzwcai_lark_mcp/tools.py b/lzwcai_lark_mcp/lzwcai_lark_mcp/tools.py index 27f260c..6b40a9e 100644 --- a/lzwcai_lark_mcp/lzwcai_lark_mcp/tools.py +++ b/lzwcai_lark_mcp/lzwcai_lark_mcp/tools.py @@ -189,6 +189,62 @@ tools = [ "required": ["user_id", "order_number", "asset_list", "face_cap", "user_ids"] } ), + Tool( + name="get_user_okr_list", + description="根据用户user_id获取该用户的OKR列表,返回OKR ID及目标(Objective)和关键结果(Key Result)详情", + inputSchema={ + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "用户的user_id" + }, + "offset": { + "type": "string", + "description": "分页偏移,默认0" + }, + "limit": { + "type": "string", + "description": "每页数量,范围0-10,默认10" + }, + "lang": { + "type": "string", + "description": "语言版本: zh_cn 或 en_us,默认 zh_cn", + "enum": ["zh_cn", "en_us"] + }, + "period_ids": { + "type": "array", + "description": "OKR周期ID列表,最多10个,不传则返回所有周期", + "items": { + "type": "string" + } + } + }, + "required": ["user_id"] + } + ), + Tool( + name="batch_get_okr", + description="根据OKR ID批量获取OKR详情,返回目标(Objective)及其关键结果(Key Result)详情", + inputSchema={ + "type": "object", + "properties": { + "okr_ids": { + "type": "array", + "description": "OKR ID列表,最多10个", + "items": { + "type": "string" + } + }, + "lang": { + "type": "string", + "description": "语言版本: zh_cn 或 en_us,默认 zh_cn", + "enum": ["zh_cn", "en_us"] + } + }, + "required": ["okr_ids"] + } + ), Tool( name="send_feedback_card", description="发送资产变动反馈单,入参为receiver_ids、user_id、order_number、change_time、asset_list和remark", @@ -417,6 +473,107 @@ def _build_asset_options(inputs: List[str], outputs: List[str]) -> tuple[list[Di return options, "、".join(labels) +def get_user_okr_list( + token: str, + user_id: str, + offset: str = "0", + limit: str = "10", + lang: str = "zh_cn", + period_ids: List[str] | None = None, +) -> str: + url = f"https://open.feishu.cn/open-apis/okr/v1/users/{user_id}/okrs" + headers = {"Authorization": f"Bearer {token}"} + params: Dict[str, object] = { + "user_id_type": "user_id", + "offset": offset, + "limit": limit, + "lang": lang, + } + if period_ids: + params["period_ids"] = period_ids + response = requests.get(url, headers=headers, params=params, timeout=15) + response.raise_for_status() + payload = response.json() + if payload.get("code") not in (0, None): + raise RuntimeError(f"获取用户OKR列表失败: {payload}") + data = payload.get("data", {}) + total = data.get("total", 0) + okr_list = data.get("okr_list", []) + return json.dumps({"total": total, "okr_list": okr_list}, ensure_ascii=False) + + +def batch_get_okr( + token: str, + okr_ids: List[str], + lang: str = "zh_cn", +) -> str: + url = "https://open.feishu.cn/open-apis/okr/v1/okrs/batch_get" + headers = {"Authorization": f"Bearer {token}"} + params: Dict[str, object] = { + "okr_ids": okr_ids, + "user_id_type": "user_id", + "lang": lang, + } + response = requests.get(url, headers=headers, params=params, timeout=15) + response.raise_for_status() + payload = response.json() + if payload.get("code") not in (0, None): + raise RuntimeError(f"批量获取OKR失败: {payload}") + data = payload.get("data", {}) + okr_list = data.get("okr_list", []) + lines: List[str] = [f"共返回 {len(okr_list)} 条OKR记录:"] + status_map = {"-1": "暂无", "0": "正常", "1": "风险", "2": "延期"} + confirm_map = {0: "初始状态", 1: "待提交", 2: "待确认", 3: "已拒绝", 4: "已确认"} + for idx, okr in enumerate(okr_list, 1): + okr_name = okr.get("name") or okr.get("id", "") + confirm = okr.get("confirm_status") + confirm_text = confirm_map.get(confirm, str(confirm)) if confirm is not None else "" + lines.append(f"\n{'='*40}") + lines.append(f"OKR #{idx}: {okr_name} (周期ID: {okr.get('period_id', '')})") + if confirm_text: + lines.append(f" 确认状态: {confirm_text}") + objective_list = okr.get("objective_list", []) + for oi, obj in enumerate(objective_list, 1): + content = obj.get("content", "") + score = obj.get("score", "") + weight = obj.get("weight", "") + progress_report = obj.get("progress_report", "") + progress = obj.get("progress_rate") or {} + percent = progress.get("percent", "") + status = status_map.get(str(progress.get("status", "")), "") + deadline = obj.get("deadline", "") + lines.append(f" O{oi}: {content}") + if weight: + lines.append(f" 权重: {weight}%") + if score: + lines.append(f" 评分: {score}") + if percent != "": + lines.append(f" 进度: {percent}%{(' (' + status + ')') if status else ''}") + if deadline and deadline != "0": + lines.append(f" 截止: {datetime.fromtimestamp(int(deadline) / 1000, tz=timezone(timedelta(hours=8))).strftime('%Y-%m-%d')}") + if progress_report: + lines.append(f" 备注: {progress_report}") + kr_list = obj.get("kr_list", []) + for ki, kr in enumerate(kr_list, 1): + kr_content = kr.get("content", "") + kr_score = kr.get("score", "") + kr_weight = kr.get("kr_weight", "") + kr_deadline = kr.get("deadline", "") + kr_progress = kr.get("progress_rate") or {} + kr_percent = kr_progress.get("percent", "") + kr_status = status_map.get(str(kr_progress.get("status", "")), "") + lines.append(f" KR{ki}: {kr_content}") + if kr_weight: + lines.append(f" 权重: {kr_weight}%") + if kr_score: + lines.append(f" 评分: {kr_score}") + if kr_percent != "": + lines.append(f" 进度: {kr_percent}%{(' (' + kr_status + ')') if kr_status else ''}") + if kr_deadline and kr_deadline != "0": + lines.append(f" 截止: {datetime.fromtimestamp(int(kr_deadline) / 1000, tz=timezone(timedelta(hours=8))).strftime('%Y-%m-%d')}") + return "\n".join(lines) + + def _build_select_options(items: List[str]) -> list[Dict[str, object]]: options: list[Dict[str, object]] = [] for item in items: @@ -1883,6 +2040,39 @@ async def handle_call_tool(name: str, arguments: Dict[str, object], token: str) user_ids, remark ) + elif name == "get_user_okr_list": + user_id = str(arguments.get("user_id", "")).strip() + if not user_id: + raise ValueError("missing user_id") + offset = str(arguments.get("offset") or "0").strip() + limit = str(arguments.get("limit") or "10").strip() + lang = str(arguments.get("lang") or "zh_cn").strip() + period_ids = arguments.get("period_ids") + if period_ids is not None and not isinstance(period_ids, list): + period_ids = None + result = get_user_okr_list( + token, + user_id, + offset=offset, + limit=limit, + lang=lang, + period_ids=period_ids, + ) + elif name == "batch_get_okr": + okr_ids = arguments.get("okr_ids") + if not okr_ids or not isinstance(okr_ids, list): + raise ValueError("missing or invalid okr_ids, must be a list of OKR ID strings") + if len(okr_ids) > 10: + raise ValueError("okr_ids supports at most 10 items") + okr_ids = [str(oid).strip() for oid in okr_ids if str(oid).strip()] + if not okr_ids: + raise ValueError("okr_ids is empty after filtering") + lang = str(arguments.get("lang") or "zh_cn").strip() + result = batch_get_okr( + token, + okr_ids, + lang=lang, + ) elif name == "send_feedback_card": user_id = str(arguments.get("user_id", "")).strip() order_number = str(arguments.get("order_number", "")).strip() diff --git a/lzwcai_lark_mcp/pyproject.toml b/lzwcai_lark_mcp/pyproject.toml index 79ee0d0..cc5b42a 100644 --- a/lzwcai_lark_mcp/pyproject.toml +++ b/lzwcai_lark_mcp/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "lzwcai-lark-mcp" -version = "0.1.15" +version = "0.1.16" description = "Lark MCP server" requires-python = ">=3.10" dependencies = [