npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

meego-client

v1.4.4

Published

TypeScript/JavaScript SDK for Meego API

Readme

Meego Client - TypeScript SDK

npm version License: Apache-2.0 TypeScript Node.js

飞书项目(Meegle)API 的 TypeScript/JavaScript SDK,提供类型安全、开发友好的接口调用方式。

🚀 快速开始

安装

npm install meego-client

基本使用

import { createMeegoClient, WithBaseUrl } from "meego-client";

// 创建客户端
const client = createMeegoClient(
  "your-app-id",
  "your-app-secret",
  WithBaseUrl("https://project.feishu.cn")
);

// 创建工作项
const workItem = await client.workItem
  .create("project-key")
  .workItemTypeKey("story")
  .name("新功能开发")
  .description("实现用户登录功能")
  .execute({
    userKey: "your-user-key", // 必需:用户身份标识
  });

console.log("工作项创建成功:", workItem.data);

📋 核心功能

工作项管理

  • ✅ 创建、查询、更新、删除工作项
  • ✅ 工作项搜索
  • ✅ 批量操作、工作流状态流转
  • ✅ 评论、附件管理

用户和空间管理

  • ✅ 用户信息查询、用户搜索
  • ✅ 空间列表、空间详情
  • ✅ 用户组管理(需要管理员权限)

其他功能

  • ✅ 工作项配置管理
  • ✅ 富文本内容支持

🔑 用户身份认证

💡 智能的 userKey 处理

SDK 对不同模块采用了最优的 userKey 处理方式:

// 🚀 Space 模块
const spaces = await client.space
  .list()
  .user("user-key") // 自动处理请求体和认证头
  .execute();

// 🔧 其他模块 - 标准方式,只需认证头
const workItem = await client.workItem
  .create("project-key")
  .name("新任务")
  .execute({ userKey: "user-key" });

// ✅ 或者使用 WithUserKey 辅助函数
import { WithUserKey } from "meego-client";

const users = await client.user.searchUsers(
  "user-key",
  "project-key",
  WithUserKey("current-user-key")
);

🎯 为什么 Space 模块特殊?

Space API 需要在请求体中指定"查询哪个用户的空间",同时还需要认证头。

// Space API 的双重 userKey 需求:
// 1. 请求体: { "user_key": "查询目标用户" }
// 2. 请求头: { "X-USER-KEY": "认证身份" }

await client.space
  .list()
  .user("user-key") // SDK 自动用于请求体和认证头
  .execute();

常见认证错误处理

try {
  const result = await client.workItem.delete("project-key", "story", 123);
} catch (error) {
  if (error.code === 20039) {
    console.error("缺少用户身份认证");
    // 解决方案:传递 userKey
    await client.workItem.delete("project-key", "story", 123, {
      userKey: "your-user-key",
    });
  }
}

📖 使用示例

工作项创建

// 创建并指定工作项类型与基础信息
const createdId = await client.workItem
  .create("project-key")
  .workItemTypeKey("story") // 指定工作项类型
  .name("README 示例 - 创建工作项")
  .description("示例:通过 SDK 创建工作项并填写基础信息")
  .execute({
    userKey: "your-user-key",
  });

console.log("创建成功,工作项ID:", createdId.data); // number

工作项搜索

// 单空间搜索
const results = await client.workItem.search
  .filter("project-key")
  .workItemTypeKeys(["story"])
  .workItemName("用户登录")
  .pageSize(20)
  .execute({
    userKey: "your-user-key",
  });

// 访问搜索结果
console.log("搜索结果:", {
  workItems: results.data?.items || [],
  count: results.data?.items?.length || 0,
  hasMore: results.data?.pagination?.has_more,
});

// 跨空间搜索
const crossResults = await client.workItem.search
  .filterAcrossProjects()
  .projects(["project-1", "project-2"])
  .workItemTypeKey("story")
  .execute({
    userKey: "your-user-key",
  });

console.log("跨空间搜索结果:", crossResults.data?.items || []);

// 全局搜索
const globalResults = await client.workItem.search
  .compositiveSearch()
  .queryType("workitem")
  .query("用户登录")
  .execute({
    userKey: "your-user-key",
  });

console.log("全局搜索结果:", globalResults.data?.items || []);

工作项配置(basic)

// 读取工作项基础信息配置
const config = await client.workItemConfig.basic.getConfig(
  "project-key",
  "story",
  { userKey: "your-user-key" }
);

if (config.success) {
  console.log("工作项配置:", {
    typeKey: config.data?.type_key,
    name: config.data?.name,
    flowMode: config.data?.flow_mode, // "workflow" | "stateflow"
    isDisabled: config.data?.is_disabled,
    isPinned: config.data?.is_pinned,
    enableSchedule: config.data?.enable_schedule,
    scheduleFieldKey: config.data?.schedule_field_key,
    estimatePointFieldKey: config.data?.estimate_point_field_key,
    actualWorkTimeFieldKey: config.data?.actual_work_time_field_key,
    actualWorkTimeSwitch: config.data?.actual_work_time_switch,
    enableModelResourceLib: config.data?.enable_model_resource_lib,
  });
} else {
  console.log("获取配置失败:", config.err_code, config.err_msg);
}

// 便捷方法:判断开关/模式
const scheduleEnabled = await client.workItemConfig.basic.isScheduleEnabled(
  "project-key",
  "story",
  { userKey: "your-user-key" }
);
console.log("是否支持整体排期:", scheduleEnabled);

const flowMode = await client.workItemConfig.basic.getFlowMode(
  "project-key",
  "story",
  { userKey: "your-user-key" }
);
console.log("流程模式:", flowMode.data); // "workflow" | "stateflow"

// 提示:更新配置(如启用排期、开启工时、设置导航入口等)通常需要管理员权限
// 例如启用整体排期(需管理员权限):
// await client.workItemConfig.basic.enableSchedule("project-key", "story", undefined, undefined, undefined, { userKey: "admin-user-key" });

用户管理

// 搜索用户
const users = await client.user.searchUsers("张三", "project-key", {
  userKey: "your-user-key",
});

// 访问用户搜索结果
console.log("用户搜索结果:", {
  userList: users.data || [],
  count: users.data?.length || 0,
});

// 获取用户信息
const userResult = await client.user.getUserByKey("user-key");
if (userResult.success) {
  console.log("用户信息:", {
    用户名: userResult.data?.name_cn,
    用户键: userResult.data?.user_key,
    邮箱: userResult.data?.email,
    状态: userResult.data?.status,
  });
} else {
  console.log("获取用户失败:", userResult.err_msg);
  console.log("错误码:", userResult.err_code);

  // 可以根据错误码进行不同处理
  switch (userResult.err_code) {
    case 404:
      console.log("用户不存在");
      break;
    case 403:
      console.log("无权限访问");
      break;
  }
}

// 查询项目成员(需要管理员权限和user_access_token)
try {
  const members = await client.user.group
    .queryProjectMembers("project-key")
    .pageSize(50)
    .execute({
      userKey: "admin-user-key",
    });

  console.log("项目成员:", {
    成员列表: members.data?.list || [],
    成员数量: members.data?.list?.length || 0,
  });
} catch (error) {
  if (error.code === 1000052755) {
    console.log(
      "⚠️ 用户组管理需要user_access_token,当前使用的是plugin_access_token"
    );
    console.log("💡 请参考文档配置正确的认证方式");
  }
}

空间管理

// ✅ 简洁调用 - 只需设置一次 userKey
const spaces = await client.space
  .list()
  .user("user-key") // 自动用于请求体和认证头
  .orderByLastVisitedDesc()
  .execute();

// 访问空间列表(分页结构)
// 该接口返回 string[](空间 project_key 列表)
console.log("空间列表:", {
  projectKeys: Array.isArray(spaces.data) ? spaces.data : [],
  count: Array.isArray(spaces.data) ? spaces.data.length : 0,
});

// ✅ 灵活调用 - 支持动态覆盖认证用户
const spacesWithAuth = await client.space
  .list()
  .user("query-user-key") // 查询目标用户的空间
  .execute({
    userKey: "auth-user-key", // 认证身份(可选,覆盖上面的用户)
  });

// 获取空间详情
const spaceDetails = await client.space
  .detail()
  .user("user-key") // 只需设置一次
  .projectKeys(["project-1", "project-2"])
  .execute();

// 访问空间详情(分页结构)
// 该接口返回 Record<string, Project>
console.log("空间详情:", {
  details: spaceDetails.data || {},
  count: spaceDetails.data ? Object.keys(spaceDetails.data).length : 0,
});

// 便捷方法 - 返回字符串数组
const userSpaces = await client.space.getUserSpaces("user-key");
console.log("用户空间Keys:", {
  projectKeys: userSpaces.data || [], // 直接是string[]
  count: userSpaces.data?.length || 0,
});

// 便捷方法 - 返回单个空间对象
const spaceInfo = await client.space.getSpaceByKey("user-key", "project-key");
console.log("空间信息:", {
  projectKey: spaceInfo.data?.project_key,
  projectName: spaceInfo.data?.name,
  simpleName: spaceInfo.data?.simple_name,
});

视图管理

// 查询视图列表
const views = await client.view.query
  .viewConfList("project-key")
  .workItemTypeKey("story")
  .pageSize(10)
  .execute({ userKey: "your-user-key" });

console.log("视图列表:", {
  views: views.data?.data || [],
  count: views.data?.data?.length || 0,
});

// 创建固定视图(基于工作项ID列表)
const fixedView = await client.view.manage
  .createFixView("project-key", "story")
  .name("我的关注项")
  .workItems([12345, 67890])
  .withAllMembers() // 全部成员可访问
  .execute({ userKey: "your-user-key" });

console.log("固定视图创建成功:", fixedView.data?.view_id);

// 创建条件视图(基于搜索条件)
import { ViewSearchOperator } from "meego-client";

const conditionView = await client.view.manage
  .createConditionView()
  .projectKey("project-key")
  .workItemTypeKey("story")
  .name("进行中的需求")
  .addWorkItemStatus(["doing"], ViewSearchOperator.EQ)
  .execute({ userKey: "your-user-key" });

console.log("条件视图创建成功:", conditionView.data?.view_id);

度量管理

// 获取度量图表数据
const chartData = await client.measure.query.getChartData(
  "project-key",
  "chart-id",
  { userKey: "your-user-key" }
);

console.log("图表数据:", {
  chartName: chartData.data?.name,
  chartId: chartData.data?.chart_id,
  dataSetCount: chartData.data?.chart_data_list?.length || 0,
});

// 使用 Fluent API 查询图表数据
const chartResult = await client.measure.query
  .chartData("project-key", "chart-id")
  .execute({ userKey: "your-user-key" });

console.log("Fluent API 查询成功:", chartResult.data?.name);

评论管理

// 创建评论
import { RichTextBuilder, RichTextType } from "meego-client";

const richText = new RichTextBuilder()
  .addText("这是一条评论")
  .addMention("user-key-to-mention")
  .build();

const comment = await client.workItem.comment.create(
  "project-key",
  "story",
  12345, // workItemId
  richText,
  { userKey: "your-user-key" }
);

console.log("评论创建成功:", comment.data);

// 查询评论列表
const comments = await client.workItem.comment
  .list("project-key", "story", 12345)
  .pageSize(20)
  .execute({ userKey: "your-user-key" });

console.log("评论列表:", {
  comments: comments.data?.data || [],
  total: comments.data?.pagination?.total || 0,
});

// 删除评论
await client.workItem.comment.delete("project-key", "story", 12345, "comment-id", {
  userKey: "your-user-key",
});

附件管理

import * as fs from "fs";

// 上传工作项附件
const fileBuffer = fs.readFileSync("/path/to/file.pdf");
const uploadResult = await client.workItem.attachments
  .uploadAttachment("project-key", "story", 12345)
  .file(fileBuffer)
  .name("document.pdf")
  .fieldKey("attachment") // 附件字段的 field_key
  .execute({ userKey: "your-user-key" });

console.log("附件上传成功:", uploadResult.data);

// 下载附件
const downloadBuilder = client.workItem.attachments.downloadFile(
  "project-key",
  "story",
  12345,
  "file-uuid"
);
const downloadResult = await downloadBuilder.execute({ userKey: "your-user-key" });

console.log("附件下载成功");

// 删除附件
const deleteResult = await client.workItem.attachments
  .deleteAttachment("project-key", 12345)
  .fileKey("file-uuid")
  .fieldKey("attachment")
  .execute({ userKey: "your-user-key" });

console.log("附件删除成功:", deleteResult.success);

工作流管理

// 查询工作流详情
const workflow = await client.workItem.workflow
  .query("project-key", "story", 12345)
  .execute({ userKey: "your-user-key" });

console.log("工作流详情:", {
  currentState: workflow.data?.state_key,
  nodes: workflow.data?.nodes?.length || 0,
});

// 状态流转
const transitionResult = await client.workItem.workflow
  .stateChange("project-key", "story", 12345)
  .transitionId(1001) // 流转ID
  .execute({ userKey: "your-user-key" });

console.log("状态流转成功:", transitionResult.success);

// 节点操作(完成节点)
const nodeResult = await client.workItem.workflow
  .nodeOperate("project-key", "story", 12345, "node-id")
  .action("finish")
  .execute({ userKey: "your-user-key" });

console.log("节点操作成功:", nodeResult.success);

子任务管理

// 创建子任务
const subTask = await client.workItem.subTask
  .create("project-key", "story", 12345)
  .name("子任务名称")
  .execute({ userKey: "your-user-key" });

console.log("子任务创建成功:", subTask.data);

// 查询子任务列表
const subTasks = await client.workItem.subTask.list(
  "project-key",
  "story",
  12345,
  { userKey: "your-user-key" }
);

console.log("子任务列表:", {
  subTasks: subTasks.data || [],
  count: subTasks.data?.length || 0,
});

// 完成子任务
await client.workItem.subTask.complete("project-key", "story", 12345, "subtask-id", {
  userKey: "your-user-key",
});

// 删除子任务
await client.workItem.subTask.delete("project-key", "story", 12345, "subtask-id", {
  userKey: "your-user-key",
});

工时管理

// 创建工时记录
const workHour = await client.workItem.workHour
  .create("project-key", "story", 12345)
  .workTime(4) // 工时(小时)
  .workDate("2024-01-15") // 工作日期
  .workDescription("完成需求分析")
  .execute({ userKey: "your-user-key" });

console.log("工时记录创建成功:", workHour.data);

// 查询工时记录
const workHours = await client.workItem.workHour.list(
  "project-key",
  "story",
  12345,
  { userKey: "your-user-key" }
);

console.log("工时记录:", {
  records: workHours.data || [],
  totalHours: workHours.data?.reduce((sum, r) => sum + (r.work_time || 0), 0) || 0,
});

批量操作

// 批量更新工作项字段
const batchResult = await client.workItem.batch
  .update("project-key", "story")
  .workItemIds([12345, 67890])
  .field("priority", { value: "1" }) // 设置优先级
  .execute({ userKey: "your-user-key" });

console.log("批量更新任务ID:", batchResult.data?.job_id);

// 查询批量任务结果
const taskResult = await client.workItem.batch.getTaskResult("task-id");

console.log("批量任务状态:", taskResult.data?.status);

🔧 配置选项

基本配置

import {
  createMeegoClient,
  WithBaseUrl,
  WithTimeout,
  WithLogLevel,
  LogLevel,
} from "meego-client";

const client = createMeegoClient(
  "app-id",
  "app-secret",
  WithBaseUrl("https://project.feishu.cn"),
  WithTimeout(30000), // 30秒超时
  WithLogLevel(LogLevel.DEBUG) // 调试日志(使用枚举)
);

性能优化

import { WithPerformanceOptimization } from "meego-client";

const client = createMeegoClient(
  "app-id",
  "app-secret",
  WithBaseUrl("https://project.feishu.cn"),
  WithPerformanceOptimization("advanced") // 高级优化
);

环境变量配置

# .env
MEEGO_APP_ID=your-app-id
MEEGO_APP_SECRET=your-app-secret
MEEGO_BASE_URL=https://project.feishu.cn
const client = createMeegoClient(
  process.env.MEEGO_APP_ID!,
  process.env.MEEGO_APP_SECRET!,
  WithBaseUrl(process.env.MEEGO_BASE_URL!)
);

🐛 错误处理

try {
  const result = await client.workItem.get("project-key", "story", 123, {
    userKey: "your-user-key",
  });
  console.log("工作项信息:", result.data);
} catch (error) {
  // SDK 抛出的错误为 Error(包含 code 等扩展属性)
  const code = (error as any)?.code;
  const message = (error as any)?.message || String(error);
  const httpStatusCode =
    (error as any)?.httpStatus || (error as any)?.http_status_code;

  console.error("API错误:", { code, message, httpStatusCode });

  // 处理特定错误码
  switch (code) {
    case 20039:
      console.error("❌ 缺少用户身份认证,请传递 userKey 参数");
      break;
    case 1000052755:
      console.error(
        "❌ Token 类型错误,用户组管理需要 user_access_token(请参考平台文档)"
      );
      break;
    case 404:
      console.error("❌ 资源不存在");
      break;
    case 403:
      console.error("❌ 权限不足");
      break;
    default:
      console.error("❌ 未知错误:", message);
  }
}

// 标准响应处理
const userResult = await client.user.getUserByKey("user-key");
if (userResult.success) {
  console.log("✅ 用户信息:", userResult.data);
} else {
  console.log("❌ 获取失败:", {
    错误码: userResult.err_code,
    错误信息: userResult.err_msg,
  });

  // 根据标准响应的错误码处理
  switch (userResult.err_code) {
    case 20039:
      console.log("💡 解决方案: 添加 userKey 参数");
      break;
    case 404:
      console.log("💡 解决方案: 检查用户键是否正确");
      break;
  }
}

// 常见认证错误处理
try {
  await client.workItem.delete("project-key", "story", 123);
} catch (error) {
  if (error.code === 20039) {
    console.error("❌ 缺少用户身份认证");
    // 重试时添加 userKey
    const result = await client.workItem.delete("project-key", "story", 123, {
      userKey: "your-user-key",
    });
    console.log("✅ 重试成功");
  }
}

📊 数据结构说明

分页响应结构

大多数列表查询 API 返回如下分页结构(以文档为准):

interface PaginatedResponse<T> {
  data: {
    items: T[]; // 实际数据数组
    pagination: {
      page_num: number; // 当前页码
      page_size: number; // 页面大小
      total: number; // 总条数
    };
  };
  err_code: number;
  err_msg: string;
}

// 正确的访问方式
const results = await client.workItem.search
  .filter("project-key")
  .execute({ userKey: "your-user-key" });
const workItems = results.data?.items || [];
const pagination = results.data?.pagination;
// 计算是否还有更多(通用方式)
const hasMore = pagination
  ? pagination.page_num * pagination.page_size < (pagination.total ?? 0)
  : false;

// 注意:
// - 少数接口会单独返回 has_more(例如部分评论查询),请以各接口文档为准;
// - SDK 不会强行添加 has_more 字段,建议使用 total 计算是否还有下一页。

便捷方法返回结构

便捷方法通常直接返回数据数组:

// 便捷方法 - 直接返回数组
const userSpaces = await client.space.getUserSpaces("user-key");
const projectKeys = userSpaces.data || []; // string[]

// 便捷方法 - 返回单个对象
const spaceInfo = await client.space.getSpaceByKey("user-key", "project-key");
const projectName = spaceInfo.data?.name; // Project对象

用户信息字段

用户相关 API 使用标准字段名:

interface UserInfo {
  user_key: string; // ✅ 用户键
  name_cn: string; // ✅ 中文姓名
  name_en: string; // ✅ 英文姓名
  email: string; // 邮箱
  status: string; // 状态
}

💡 最佳实践

0. 关于“选项字段”的取值(必须传 value)

  • 所有带选项的字段(如 select/multi_select/tree_select/signal 等),SDK 强制要求传“选项的 value”,不要传 label/别名。
  • 这样可以避免 20050「字段值不合法」错误,也避免环境中 label 变化导致的不确定性。
  • 如何拿到 value:
    • 通过工作项字段配置接口查询该字段的可选项,并从中读取 value;
    • 或者在你的应用层维护一份稳定的“label → value”映射,但在调用 SDK 前完成转换。

示例:

// ✅ 正确:直接传 value
await client.workItem
  .update("project-key", "story", 6300034462)
  .field("priority", { value: "1" }) // P1 的实际值
  .execute({ userKey: "your-user-key" });

// ❌ 错误:传 label(将会 20050)
await client.workItem
  .update("project-key", "story", 6300034462)
  .field("priority", { label: "P1" }) // 不被接受
  .execute({ userKey: "your-user-key" });

说明:

  • SDK 不会做 label→value 的“自动转换”,请在调用前完成映射;
  • 这么做更透明、稳定,也更容易在出错时快速定位到“值不合法”的问题。

1. 数据访问模式

// ✅ 推荐:使用可选链和默认值
const items = response.data?.items || [];
const count = response.data?.items?.length || 0;

// ❌ 避免:直接访问可能不存在的属性
const items = response.data.items; // 可能报错

2. 错误处理模式

// ✅ 推荐:结合异常捕获和响应检查
try {
  const result = await client.user.getUserByKey("user-key");
  if (result.success) {
    // 处理成功情况
    console.log(result.data?.name_cn);
  } else {
    // 处理业务错误
    console.log(`错误: ${result.err_msg} (${result.err_code})`);
  }
} catch (error) {
  // 处理系统异常
  console.error("系统错误:", error);
}

3. 权限管理

// ✅ 明确区分不同类型的API
// 大部分业务API:使用 plugin_access_token
const workItem = await client.workItem
  .create("project-key")
  .name("任务名称")
  .execute({ userKey: "user-key" });
// 用户组管理API:需要 user_access_token(当出现 1000052755 错误时,请按提示使用用户级别凭据。
// SDK 当前默认使用插件凭据,不会自动切换 token 类型,令牌的获取与使用请参考平台文档)

📄 许可证

Apache-2.0 License - 详见 LICENSE 文件