meeglesdk
v0.2.3
Published
飞书项目 Open API TypeScript SDK
Readme
meeglesdk
TypeScript SDK for Feishu Project (Meego) Open API.
Runtime: Node.js 18+ (server-side only; browser usage is not supported).
Release notes: CHANGELOG.md Release protocol: RELEASE.md
Install
npm install meeglesdkQuick Start
import { MeegoClient, withUserKey } from 'meeglesdk';
const client = new MeegoClient({
pluginId: 'YOUR_PLUGIN_ID',
pluginSecret: 'YOUR_PLUGIN_SECRET',
baseURL: 'https://project.feishu.cn',
});
const items = await client.workItem.query(
'project_key',
'story',
{ work_item_ids: [123456] },
withUserKey('user_key')
);Client Options
const client = new MeegoClient({
pluginId: 'YOUR_PLUGIN_ID',
pluginSecret: 'YOUR_PLUGIN_SECRET',
baseURL: 'https://project.feishu.cn', // optional
timeout: 30000, // optional, ms
retry: {
maxRetries: 2,
retryDelay: 1000, // base delay, exponential backoff
maxRetryDelay: 30000, // optional cap for backoff delay
retryableErrorCodes: [10429, 50006] // optional override
},
rateLimit: 'meego-openapi', // preset: doc-based limits (15 QPS + endpoint overrides)
// or customize:
// rateLimit: {
// enabled: true, // optional, default true when provided
// qps: 15, // per token QPS
// burst: 15, // optional burst size
// scope: 'token+method+path', // 'token' | 'token+path' | 'token+method+path'
// entryTtlMs: 600000, // optional, idle bucket cleanup (default 10 min)
// rules: [ // optional overrides
// { method: 'POST', path: '/open_api/work_item/actual_time/update', qps: 10 },
// ],
// },
tokenType: 0, // 0: plugin_access_token, 1: virtual_plugin_token
onTokenRefresh: (token, type, userKey) => {
// persist token if needed
},
logger: console, // optional
});Auth Flows
Plugin Token (Server)
const token = await client.auth.getPluginToken();User Token (Auth Code Exchange)
const { token, expire_time, refresh_token } = await client.auth.getUserToken(code);Manually Set User Token
client.setUserToken({
userKey,
token,
expireTime: expireTimeSeconds,
refreshToken,
refreshTokenExpireTime,
saasTenantKey: tenantKey,
});Request-Level Auth (Recommended for Multi-User)
Do not store user tokens on a shared client. Instead, pass auth per request:
await client.workItem.query(
'project_key',
'story',
{ work_item_ids: [123456] },
{ auth: { type: 'user', token: userToken } }
);
// Helper shortcuts (return request options)
await client.workItem.query(
'project_key',
'story',
{ work_item_ids: [123456] },
withUserKey('user_key')
);Rules:
type: 'user': use the provided user token;userKeyis not needed.type: 'plugin': use plugin token;userKeyis required for user-scoped APIs.type: 'auto'(default): ifuserKeyis provided and a cached token exists for that user, it is used; otherwise plugin token is used.authMode(optional): only applies when a plugin token is used; controlsx-auth-modefor strict read permission checks.
x-auth-mode (Force Read Auth for Plugin Token)
When calling read APIs with plugin_access_token (or virtual_plugin_token), you can set x-auth-mode to
force the server to respect the X-User-Key permissions.
This header has no effect when a user token is used.
authMode: 1-> strict mode (recommended when you must enforce the user's read permissions)authMode: 0or omitted -> compatibility mode
await client.workItem.query(
'project_key',
'story',
{ work_item_ids: [123456] },
{ auth: { type: 'plugin', userKey: 'user_key', authMode: 1 } }
);Token Management Notes
TokenManagercachesplugin_access_tokenand refreshes it automatically.user_access_tokenis cached peruserKeywhen you callauth.getUserTokenorclient.setUserToken.user_access_tokenwill be auto-refreshed per user when possible (requiresrefresh_token).- Use request-level
authfor multi-user apps; avoid sharing a client-scoped user token across users.
Timeout & Retry
- Default timeout: 30s.
- Retries use exponential backoff with a base delay and are enabled by default.
- Network errors, timeouts, and retryable API error codes are retried (default:
10429,50006). - Per-request overrides are supported via
options.retry,options.skipRetry, andoptions.timeout.
AI Field
await client.aiField.update({
ai_task_id: '7629005114472926736',
field_value: 'Generated field value',
});AI field webhooks use event types 8101 and 8102.
Work Item Field Options
const options = await client.workItem.getFieldOptions({
project_key: 'project_key',
work_item_type_key: 'story',
work_item_id: 123456,
field_key: 'priority',
});Work Item Workflow Query
const workflow = await client.workItem.workflow.query(
'project_key',
'story',
123456,
{
fields: ['priority', 'description'],
flow_type: 0,
expand: {
need_user_detail: true,
need_sub_task_parent: true,
need_rich_text_mark_down: true,
need_sub_workitem_detail: true,
},
},
{ auth: { type: 'plugin', userKey: 'user_key' } }
);
console.log(workflow.workflow_nodes);Compound Field Values
await client.workItem.updateCompoundFieldValue({
project_key: 'project_key',
work_item_id: 123456,
field_key: 'field_compound',
action: 'add',
fields: [[{ field_key: 'sub_field', field_value: 'value' }]],
});Create Instance from Resource
const created = await client.config.resource.createInstanceFromResource(
'project_key',
872360000,
{
work_item_type_key: 'story',
name: '校验必填创建实例',
required_mode: 1,
field_value_pairs: [
{ field_key: 'description', field_value: '测试描述' },
],
},
{ auth: { type: 'plugin', userKey: 'user_key' } }
);
console.log(created.work_item_id);WBS View (Query + Body)
const wbs = await client.workItem.workflow.getWbsView(
'project_key',
'story',
123456,
{
query: { need_union_deliverable: true, need_schedule_table_agg: true },
body: {
expand: {
need_union_deliverable: true,
need_wbs_relation_chain_entity: true,
need_wbs_relation_chain_path: true,
},
},
},
{ auth: { type: 'plugin', userKey: 'user_key' } }
);WBS Draft
WBS Draft is a standalone sub-service under workItem.wbsDraft.
Do not call draft APIs from workItem.workflow.
const draft = await client.workItem.wbsDraft.createDraft({
project_key: 'project_key',
work_item_id: 123456,
work_item_type_key: 'story',
});
await client.workItem.wbsDraft.patchDraft({
project_key: 'project_key',
work_item_id: 123456,
draft_id: draft.draft_meta.draft_id,
operation: {
operation_type: 'UpdateName',
operation_value: {
update_name: {
uuid: 'node_xxx',
name: '更新后名称',
},
},
},
});
const confs = await client.workItem.wbsDraft.getSubWorkItemConf({
project_key: 'project_key',
work_item_id: 123456,
draft_id: draft.draft_meta.draft_id,
node_uuid: 'node_xxx',
});
const createProgress = await client.workItem.wbsDraft.getCreateDraftProgress({
project_key: 'project_key',
work_item_id: 123456,
draft_id: draft.draft_meta.draft_id,
});
const patchProgress = await client.workItem.wbsDraft.getPatchDraftProgress({
project_key: 'project_key',
transaction_id: 'tx_xxx',
});
const publishResult = await client.workItem.wbsDraft.publishDraft({
project_key: 'project_key',
work_item_id: 123456,
draft_id: draft.draft_meta.draft_id,
});
const publishProgress = await client.workItem.wbsDraft.getPublishDraftProgress({
project_key: 'project_key',
work_item_id: 123456,
draft_id: draft.draft_meta.draft_id,
publish_order_id: publishResult.publish_order_id,
});
const resetResult = await client.workItem.wbsDraft.resetDraft({
project_key: 'project_key',
work_item_id: 123456,
draft_id: draft.draft_meta.draft_id,
});
const resetProgress = await client.workItem.wbsDraft.getResetDraftProgress({
project_key: 'project_key',
work_item_id: 123456,
draft_id: draft.draft_meta.draft_id,
});
const structure = await client.workItem.wbsDraft.queryStructure({
project_key: 'project_key',
work_item_id: 123456,
draft_id: draft.draft_meta.draft_id,
});
const rowDetails = await client.workItem.wbsDraft.queryRowDetails({
project_key: 'project_key',
work_item_id: 123456,
draft_id: draft.draft_meta.draft_id,
uuids: ['row_xxx'],
row_field_marks: ['meta.*', 'base.*'],
});
const rowConfig = await client.workItem.wbsDraft.queryRowConfig({
project_key: 'project_key',
work_item_id: 123456,
draft_id: draft.draft_meta.draft_id,
uuid: 'row_xxx',
});WBS View
WBS View is a standalone sub-service under workItem.wbsView.
const structure = await client.workItem.wbsView.queryStructure({
project_key: 'project_key',
work_item_id: 123456,
});
const rowDetails = await client.workItem.wbsView.queryRowDetails({
project_key: 'project_key',
work_item_id: 123456,
uuids: ['row_xxx'],
row_field_marks: ['meta.*', 'base.schedule'],
});
const expandInfo = await client.workItem.wbsView.queryExpandInfo({
project_key: 'project_key',
work_item_id: 123456,
work_item_type_key: 'story',
expand: {
need_parent_work_item: true,
need_wbs_relation_chain_entity: true,
need_wbs_relation_chain_path: true,
},
});Universal Search (Field Select)
const request = {
project_key: 'project_key',
work_item_type_key: 'story',
search_group: {
conjunction: 'AND',
search_params: [{ param_key: 'created_at', value: Date.now() - 86400000, operator: '>' }],
},
field_selected: ['work_item_id', 'name'],
pagination: { page_size: 20 },
};
const result = await client.workItem.search.universalSearch(request, {
auth: { type: 'plugin', userKey: 'user_key' },
});
// Next page
const next = await client.workItem.search.universalSearch(
{
...request,
pagination: { page_size: 20, search_after: result.pagination.search_after },
},
{ auth: { type: 'plugin', userKey: 'user_key' } }
);Generic File Upload
const urls = await client.workItem.attachment.uploadFile(
'project_key',
{ file: new Blob(['hello'], { type: 'text/plain' }), fileName: 'hello.txt' },
{ auth: { type: 'plugin', userKey: 'user_key' } }
);
const firstUrl = urls[0];Release and OpenAPI Checks
# Build dist, verify npm pack contents, and compile package-name import examples.
bun run release:artifacts
# Compare selected OpenAPI request body fields with the TS SDK request types.
bun run openapi:field-diff
# Emit the same scan as JSON for CI/report consumers.
bun run openapi:field-report
# Refresh the local OpenAPI docs snapshot after intentionally updating Meego-API-docs.
bun run openapi:snapshot:update
# Verify the local OpenAPI docs mirror has not changed without a reviewed snapshot update.
bun run openapi:snapshot:check
# Validate the scan manifest before running the diff.
bun run openapi:field-targets:check
# Same diff scan, but exits non-zero when differences are found.
bun run openapi:field-check
# Verify response models keep response-only fields out of shared request structures.
bun run response-model-checkThe OpenAPI docs snapshot pins the local Meego-API-docs markdown mirror by path, byte size, line count, and SHA-256 digest. Non-markdown files such as .DS_Store are ignored. When the mirror is intentionally refreshed, review SDK impact first, then run bun run openapi:snapshot:update and commit the snapshot diff.
The OpenAPI field scan reads the local Meego-API-docs mirror and reports the exact document path used for each target. It checks two source classes: endpoint request bodies from their API pages, and reusable SDK structures from Meego-API-docs/数据结构汇总.md. It currently covers 38 selected request bodies across work item, workflow, search, resource, review, attachment deletion, comment, subtask, work hour, batch update, compound-field update, field option, operation record, deliverable, and custom field APIs, plus shared structures such as expand params, field configs, field value pairs, role owners, schedules, search filters, relation details, transition-required info, work hour records, node schedule constraint rules, team options, number config, and user detail.
Add or remove scanned targets in scripts/openapi-field-targets.json; the checker reads that manifest instead of keeping endpoint config in code. bun run openapi:field-targets:check validates the manifest shape, referenced docs, structure headings, and SDK type declarations. bun run release:check runs openapi:snapshot:check and openapi:field-check, so npm publish and CI fail when the local OpenAPI mirror changes without a reviewed snapshot update or SDK fields drift from that mirror.
Shared request structures stay aligned to 数据结构汇总.md. Response-only additions use explicit response types such as FieldValuePairResponse, RoleOwnerResponse, and ScheduleResponse; endpoint-specific expand options use endpoint-specific types such as QueryWorkItemExpand and WorkflowQueryExpand. response-model-check keeps the main work item, workflow, resource, and WBS draft response models on those response-specific types.
Services Overview
Top-level services:
authworkItemspaceuserconfigviewmeasuretenanteventaiNodeaiField
workItem sub-services:
search,batch,comment,subtask,attachment,chat,workflow,wbsDraft,wbsView,workHour,review
Tests
Integration tests use real credentials in tests/config.ts.
bun testNetwork access is required.
The resource detail success-path check is fixture-based because the public OpenAPI mirror does not expose a resource-instance delete endpoint. Set TEST_RESOURCE_WORK_ITEM_TYPE_KEY and TEST_RESOURCE_WORK_ITEM_ID to validate a real resource detail response without creating new resource instances.
Unit Tests (Edge Cases)
Unit tests mock network calls and focus on request parsing, retries, and token caching.
bun test tests/unitPerformance Benchmarks (Local)
Quick micro-benchmarks for request parsing and big-int handling (no network).
bun run tests/perf/benchmark.ts