@bitclub.ai/opendesk-server-sdk
v0.0.2
Published
OpenDesk Server Node.js SDK - JavaScript client for OpenDesk API
Readme
@opendesk/sdk
OpenDesk Server Node.js SDK - JavaScript/TypeScript client for OpenDesk API.
Features
- Full API Coverage - 90+ endpoints across 9 modules (auth / namespaces / skills / review / labels / teams / admin / users / relay)
- Type Safe - Written in TypeScript with full type definitions
- Dual Auth - JWT login (auto-applied) and API Tokens / Relay Keys (
sk_xxx,od_xxx) - Timeout & Retry - Configurable request timeout, idempotent-request retry with exponential backoff
- Cancellation -
AbortSignalpassthrough for every request - Interceptors -
onRequest/onResponsehooks, customfetchinjection - Error Handling - Typed error classes; parses both FastAPI
{detail}and OpenAI-style{"error":{message,code}}bodies - Pagination - Built-in async generators for automatic page iteration
- Zero Dependencies - Uses native fetch API (Node.js >= 18) and works in browsers
- Download - Files as
Blob(browser and Node.js >= 18) or rawArrayBuffer
Installation
npm install @opendesk/sdkQuick Start
import { OpenDeskClient } from '@opendesk/sdk';
const client = new OpenDeskClient({
baseUrl: 'http://127.0.0.1:8000/api/v1',
});
// Login - automatically sets the token for subsequent requests
await client.auth.login({ username: 'admin', password: 'admin' });
// Get current user
const me = await client.auth.getMe();
console.log('Hello,', me.username);
// List namespaces
const namespaces = await client.namespaces.list();
console.log('Namespaces:', namespaces.items);Authentication
JWT Login (Session-based)
const client = new OpenDeskClient({ baseUrl: 'http://127.0.0.1:8000/api/v1' });
// Login - token is stored on the client automatically
await client.auth.login({ username: 'your-user', password: 'your-pass' });
await client.auth.getMe(); // Authorization header sent automatically
// Refresh returns a new token WITHOUT auto-applying it:
const token = await client.auth.refresh();
client.setToken(token.access_token); // rotate manually
// Logout - revokes the current JWT
await client.auth.logout();API Token (Server-to-Server)
// Initialize with API Token or Relay Key
const client = new OpenDeskClient({
baseUrl: 'http://127.0.0.1:8000/api/v1',
token: 'sk_your_api_token_here', // or od_ relay key
});
// All requests use the token automatically
const skills = await client.skills.catalog();Managing API Tokens
// List existing tokens
const tokens = await client.auth.listTokens();
// Create a new token
const newToken = await client.auth.createToken({
name: 'CI Pipeline',
expires_in_days: 90,
scopes: ['skill:read', 'skill:publish'],
});
console.log('Token:', newToken.access_token); // Save this - shown only once!
// Delete a token
await client.auth.deleteToken(newToken.id);Casdoor SSO & OAuth
// Is Casdoor enabled?
const status = await client.auth.getCasdoorStatus();
// Browser redirect flow:
const { login_url } = await client.auth.getCasdoorLoginUrl();
window.location.href = login_url;
// Handle the callback (code + state from the query string)
// On success the token is applied to the client automatically:
await client.auth.casdoorCallback(code, state);
// Account management
await client.auth.register({ username, email, password }); // Casdoor signup
await client.auth.updateMe({ display_name: 'New Name' }); // update profile
const accounts = await client.auth.listOAuthAccounts(); // bound OAuth accounts
await client.auth.unlinkOAuthAccount('casdoor'); // unbind (400 when it is the only provider)API Modules
Namespaces (Teams)
// Create a namespace
const ns = await client.namespaces.create({
slug: 'my-team',
name: 'My Team',
is_public: false,
});
// Members
await client.namespaces.addMember('my-team', { user_id: 42, role: 'ADMIN' });
const members = await client.namespaces.listMembers('my-team');
// Invitations
const inv = await client.namespaces.createInvitation('my-team', { role: 'MEMBER', expires_in_days: 7 });
const invitations = await client.namespaces.listInvitations('my-team');
await client.namespaces.revokeInvitation('my-team', inv.id);
await client.namespaces.acceptInvitation(inv.token); // join as memberSkills
// Search skills
const results = await client.skills.search({ q: 'ai', page_size: 20 });
// Publish a skill (two-step: upload then publish)
const upload = await client.skills.upload(formData); // FormData with .zip file
const published = await client.skills.publish({
namespace: 'my-team',
slug: 'my-skill',
name: 'My Skill',
version: '1.0.0',
upload_token: upload.upload_token,
});
// Ownership transfer / copy
await client.skills.transfer('my-team', 'my-skill', { target_namespace: 'other', target_user_id: 42 });
await client.skills.copy('my-team', 'my-skill', { target_namespace: 'other' });
// Download a skill
const blob = await client.skills.download('my-team', 'my-skill', '1.0.0');Review (Governance)
// List pending reviews / my todos
const pending = await client.review.pending({ namespace: 'my-team' });
const todos = await client.review.myTodos();
// Approve / reject / promote
await client.review.approve('publish-id', { reason: 'Looks good!' });
await client.review.reject('publish-id', { reason: 'Needs fixes' });
await client.review.promoteGlobal('publish-id', { reason: 'Excellent' });
// Skill change requests
const requests = await client.review.changeRequests({ status: 'PENDING' });
await client.review.approveChangeRequest('request-id');
// Super admin: audit log & promotion candidates
await client.review.auditLog({ action: 'APPROVE' });
await client.review.promotionCandidates();Labels
const labels = await client.labels.list(); // all labels
await client.labels.create({ name: 'AI', category: 'tech' });Teams (Join Requests + Notifications)
await client.teams.applyJoin('my-namespace'); // apply to join
const requests = await client.teams.listJoinRequests('my-namespace');
await client.teams.approveJoin('my-namespace', 'request-id');
await client.teams.rejectJoin('my-namespace', 'request-id');
const notifs = await client.teams.listNotifications();
await client.teams.markRead('notification-id');
await client.teams.markAllRead();Admin (Super Admin Only)
// Operation logs
const logs = await client.admin.listLogs({ source: 'opendesk' });
const csvBlob = await client.admin.exportLogs({ action: 'approve' });
// Namespace quota
await client.admin.updateNamespaceQuota('my-team', { max_members: 50, max_skills: 100 });
// RBAC (Casdoor)
const permissions = await client.admin.listPermissions();
const roles = await client.admin.listRoles();
await client.admin.createRole('admin', 'my-team'); // query params
await client.admin.deleteRole('admin');
await client.admin.listUserRoles(42); // by user id
await client.admin.assignRole({ username: 'alice', role: 'owner', namespace_slug: 'my-team' });
await client.admin.removeRole('alice', 'owner', 'my-team');
// Skill import (skillhub.cn)
const preview = await client.admin.previewImportSkills({ limit: 10 });
await client.admin.importSkills(preview.items);Users
const profile = await client.users.getProfile('username');
const followers = await client.users.getFollowers('username'); // { items: [...] }, non-paginated
const following = await client.users.getFollowing('username');
await client.users.follow('username');
await client.users.unfollow('username');
const dataBlob = await client.users.exportMyData(); // GDPR export (ZIP)
await client.users.deleteAccount(true);Relay (中转站)
The relay is an OpenAI-compatible proxy gateway. Use it with the official OpenAI SDK - just swap the baseURL and apiKey (see below). The module covers key/channel/usage management.
// User side
const info = await client.relay.getInfo(); // relay base URL + available models
const key = await client.relay.createKey({ name: 'my-app' });
// key.key contains the od_ secret - save it now, it is shown only once!
// Admin side (super_admin)
const channels = await client.relay.listChannels();
await client.relay.createChannel({ name: 'OpenAI', base_url: 'https://api.openai.com/v1', api_key: 'sk-...' });
await client.relay.testChannel(channelId);
const usage = await client.relay.getUsage({ key_id: 1 });
const stats = await client.relay.getStats({ group_by: 'model' });
const trend = await client.relay.getTrend({ start: '2026-01-01' });
const routes = await client.relay.getModelRoutes();Calling the relay with the official OpenAI SDK
import OpenAI from 'openai';
// 1. Get the relay base URL (or hardcode the server's relay URL)
const info = await client.relay.getInfo(); // { base_url, models }
// 2. Point the OpenAI SDK at the relay with an od_ key
const relay = new OpenAI({
baseURL: info.base_url, // e.g. https://relay.example.com/v1
apiKey: 'od_xxx', // relay key issued via client.relay.createKey()
});
const chat = await relay.chat.completions.create({
model: info.models[0],
messages: [{ role: 'user', content: 'Hello' }],
});
// Streaming works as usual:
const stream = await relay.chat.completions.create({
model: info.models[0],
messages: [{ role: 'user', content: 'Hi' }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
}Client Options
const client = new OpenDeskClient({
baseUrl: 'https://your-server.com/api/v1', // API base URL
token: 'sk_xxx', // optional: pre-set token / relay key
headers: { 'X-Custom-Header': 'value' }, // optional: sent with every request
timeoutMs: 30_000, // default 30000 - request timeout
retries: 0, // default 0 - retries for idempotent requests (GET/DELETE), exponential backoff
retryDelayMs: 500, // default 500 - base backoff delay (multiplied by 2^n)
fetch: customFetch, // optional: inject a fetch implementation
onRequest(method, path, init) {}, // optional: per-attempt request hook
onResponse(method, path, response) {}, // optional: response hook
});Timeout, Retry & Cancellation
// A timeout raises OpenDeskError with status 408.
// Retries apply only to idempotent methods (GET/DELETE) and never to
// user-cancelled requests; backoff is retryDelayMs * 2^n.
// Cancel a long-running request with AbortSignal:
const controller = new AbortController();
const data = await client.request('GET', '/skills/search', undefined, {
params: { q: 'ai' },
signal: controller.signal,
});
controller.abort(); // aborts in-flight requestsDownloading Binary Data
// Default: Blob (browser) / Buffer (Node)
const blob = await client.download('/users/me/export');
// Raw bytes:
const buf: ArrayBuffer = await client.download('/files/data.bin', undefined, 'arraybuffer');Low-level requests
// Raw request with per-call overrides
const result = await client.request('POST', '/custom/endpoint', { payload: 1 }, {
params: { page: 1 },
headers: { 'X-Extra': 'yes' },
responseType: 'json', // 'json' | 'blob' | 'arraybuffer'
signal,
});
// Convenience methods: get / post / put / patch / delete / upload
await client.upload('/skills/upload', formData);Pagination
// Method 1: Manual
const page1 = await client.skills.catalog({ page: 1, page_size: 20 });
// Method 2: Async generator (memory-efficient)
for await (const items of client.skills.catalogPaginator({}, 10)) {
console.log('Batch:', items.length);
}
// Method 3: Fetch all
const all = await client.skills.catalogAll({}, 100);Error Handling
Errors are typed by status code, and carry status, detail and an optional OpenAI-style code.
import {
OpenDeskClient,
AuthenticationError, // 401
PermissionDeniedError, // 403
NotFoundError, // 404
ValidationError, // 422
QuotaExceededError, // 422 (quota message)
ServerError, // >= 500
} from '@opendesk/sdk';
try {
await client.auth.login({ username: 'wrong', password: 'wrong' });
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Login failed:', error.detail);
} else if (error instanceof NotFoundError) {
console.error('Not found:', error.detail);
} else if (error instanceof OpenDeskError) {
console.error(`API error [${error.status}] code=${error.code}:`, error.detail);
}
}Both FastAPI {detail: "..."} and OpenAI-style {"error": {"message": "...", "code": "..."}}
error bodies are parsed automatically.
Environment Variables (SDK, Node.js only)
| Variable | Default | Description |
|----------|---------|-------------|
| OPENDESK_BASE_URL | http://127.0.0.1:8000/api/v1 | API base URL when baseUrl is not passed |
| OPENDESK_TOKEN | - | Token used when token is not passed |
Requirements
- Node.js >= 18 (for native fetch API)
- Or modern browsers with fetch support
License
项目采用 Mulan PSL v2(Mulan Permissive Software License, Version 2,SPDX: MulanPSL-2.0)开源协议。
