meego-client
v1.4.4
Published
TypeScript/JavaScript SDK for Meego API
Maintainers
Readme
Meego Client - TypeScript SDK
飞书项目(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.cnconst 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 文件
