{"_id":"@bitclub.ai/opendesk-server-sdk","_rev":"4-4fda8cf498e91ea0c06cd50dadd84b29","name":"@bitclub.ai/opendesk-server-sdk","dist-tags":{"latest":"0.0.4"},"versions":{"0.0.1":{"name":"@bitclub.ai/opendesk-server-sdk","version":"0.0.1","license":"MulanPSL-2.0","_id":"@bitclub.ai/opendesk-server-sdk@0.0.1","maintainers":[{"name":"uv0xab","email":"liyi.fm@foxmail.com"}],"dist":{"shasum":"66f5a50e9eabd52debb2707382f8180fa7168445","tarball":"https://registry.npmjs.org/@bitclub.ai/opendesk-server-sdk/-/opendesk-server-sdk-0.0.1.tgz","fileCount":2,"integrity":"sha512-+p3Zx02T6iXZOXnR8dU2CYFhGXgSVln1KbBYfuGs0SLp6ED0eUqWNsikAn/i9rNXtIvr/CIoJKgMsNbTApzKjA==","signatures":[{"sig":"MEQCIC+Vci2HDpolaD6YMXKlIri6yZ06b4/JEznkBelxeMWOAiBdS1L279Bo4k+ecOJJM0cwTQ6nqCRwPcjNGpZJ6oIxCg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":186},"main":"index.js","_npmUser":{"name":"uv0xab","email":"liyi.fm@foxmail.com"},"_npmVersion":"11.16.0","description":"OpenDesk Server SDK","directories":{},"_nodeVersion":"22.15.1","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/opendesk-server-sdk_0.0.1_1786411068998_0.9454828158912982","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@bitclub.ai/opendesk-server-sdk","version":"0.0.2","license":"MulanPSL-2.0","_id":"@bitclub.ai/opendesk-server-sdk@0.0.2","maintainers":[{"name":"uv0xab","email":"liyi.fm@foxmail.com"}],"dist":{"shasum":"f0b203cc9483ddcdef46c2429fd27504915242ef","tarball":"https://registry.npmjs.org/@bitclub.ai/opendesk-server-sdk/-/opendesk-server-sdk-0.0.2.tgz","fileCount":22,"integrity":"sha512-QpYn36KNLAgu9XC6sD4YO/UKySrAPT2zepkBbnMag8xC17hjD2YNrciLBIfiqmbBIFn0a1ONW9WGKCBcwvVTBA==","signatures":[{"sig":"MEUCIDUF+sh5qMF2nAF0L80naGPqmibLMIp1aQplTT3csm99AiEAoBd7XKdr+MjaVkSk0y03ANuxznG+55NkhyDkKCVdc2M=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":264301},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"07691ca1a34b689f81c95b90b19df6adc33fda95","scripts":{"dev":"tsup src/index.ts --format esm,cjs --dts --watch","lint":"eslint src test --max-warnings 0","test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"uv0xab","email":"liyi.fm@foxmail.com"},"repository":{"url":"git+https://gitcode.com/openharmony-robot/OpenDesk-server","type":"git"},"_npmVersion":"9.2.0","description":"OpenDesk Server Node.js SDK - JavaScript client for OpenDesk API","directories":{},"sideEffects":false,"_nodeVersion":"22.22.1","_hasShrinkwrap":false,"packageManager":"pnpm@11.21.0","devDependencies":{"tsup":"^8.3.0","eslint":"^10.8.0","vitest":"^4.1.10","typescript":"^5.6.0","typescript-eslint":"^8.66.0"},"_npmOperationalInternal":{"tmp":"tmp/opendesk-server-sdk_0.0.2_1786423361175_0.3465429856291684","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@bitclub.ai/opendesk-server-sdk","version":"0.0.3","license":"MulanPSL-2.0","_id":"@bitclub.ai/opendesk-server-sdk@0.0.3","maintainers":[{"name":"uv0xab","email":"liyi.fm@foxmail.com"}],"dist":{"shasum":"9ac1ab2716245bcc2e130a758c1c1c2c190063e4","tarball":"https://registry.npmjs.org/@bitclub.ai/opendesk-server-sdk/-/opendesk-server-sdk-0.0.3.tgz","fileCount":22,"integrity":"sha512-Ss25jYSBAhCfiXD/Bc9YH96OzgSQdJGPZcFe9PTJgTRH344vG0DTnJ9+i30IVUQCQYCwZIo8s/KgcbQf1U3ABw==","signatures":[{"sig":"MEYCIQD5cH9bPLfIGQCZwgy8CmspJdhTQY6F3+mtKdwP+9YvaQIhAJ4ppwMYcmG+DyqQHsRQXJ5uUIseHac47EHIgnc6fMlj","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIAUTuyKB8ZyapyyXWTzXUNXBnSiOHpiLJS9NIp42nFMbAiEAwHgdF+MinC0zIfQBgLt+HOGYphVnIpmXl44x8hJKgaY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":250469},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"tsup src/index.ts --format esm,cjs --dts --watch","lint":"eslint src test --max-warnings 0","test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"uv0xab","email":"liyi.fm@foxmail.com"},"repository":{"url":"git+https://gitcode.com/openharmony-robot/OpenDesk-server","type":"git"},"description":"OpenDesk Server Node.js SDK - JavaScript client for OpenDesk API","directories":{},"sideEffects":false,"_nodeVersion":"22.22.1","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","eslint":"^10.8.0","vitest":"^4.1.10","typescript":"^5.6.0","typescript-eslint":"^8.66.0"},"_npmOperationalInternal":{"tmp":"tmp/opendesk-server-sdk_0.0.3_1788862537009_0.010649893594082638","host":"s3://npm-registry-packages-npm-production"}},"0.0.4":{"_id":"@bitclub.ai/opendesk-server-sdk@0.0.4","dist":{"shasum":"e639567c9babdee14fb77323938c8f336c9244a6","tarball":"https://registry.npmjs.org/@bitclub.ai/opendesk-server-sdk/-/opendesk-server-sdk-0.0.4.tgz","fileCount":22,"integrity":"sha512-NEHu2m/UIxv8CuyHUoQaOISqY32pnodbf9QxfZL+obHFgZDby2GkGv4fzwXLPkgRZjwwZjzHkk3334RTS69eEw==","signatures":[{"sig":"MEQCIC5QINkdX0EfW4HMWAuf3IACK8IoaQLE1Wjgujzam0qlAiALAADlg9HxzjBAw0wdvmyzcOn8cJeHCs0Yeg7ksY67Vw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFZZUZv01P5DKHYFQMUixioYJUy972tQHizPbldYG5ckAiEAsy7AY+L8pegtZONrUgs23C+6pgpSpW8vZh8F8xtM7Pw="}],"unpackedSize":304658},"main":"./dist/index.cjs","name":"@bitclub.ai/opendesk-server-sdk","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"license":"MulanPSL-2.0","scripts":{"dev":"tsup src/index.ts --format esm,cjs --dts --watch","lint":"eslint src test --max-warnings 0","test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit","test:watch":"vitest"},"version":"0.0.4","_npmUser":{"name":"uv0xab","email":"liyi.fm@foxmail.com"},"repository":{"url":"git+https://gitcode.com/openharmony-robot/OpenDesk-server","type":"git"},"description":"OpenDesk Server Node.js SDK - JavaScript client for OpenDesk API","directories":{},"maintainers":[{"name":"uv0xab","email":"liyi.fm@foxmail.com"}],"sideEffects":false,"_nodeVersion":"22.22.1","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","eslint":"^10.8.0","vitest":"^4.1.10","typescript":"^5.6.0","typescript-eslint":"^8.66.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/opendesk-server-sdk_0.0.4_1789953898583_0.9951043466294727"}}},"time":{"created":"2026-08-11T01:17:48.814Z","modified":"2026-09-21T01:24:58.919Z","0.0.1":"2026-08-11T01:17:49.146Z","0.0.2":"2026-08-11T04:42:41.317Z","0.0.3":"2026-09-08T10:15:37.111Z","0.0.4":"2026-09-21T01:24:58.692Z"},"license":"MulanPSL-2.0","repository":{"url":"git+https://gitcode.com/openharmony-robot/OpenDesk-server","type":"git"},"description":"OpenDesk Server Node.js SDK - JavaScript client for OpenDesk API","maintainers":[{"name":"uv0xab","email":"liyi.fm@foxmail.com"}],"readme":"# @bitclub.ai/opendesk-server-sdk\n\nOpenDesk Server Node.js SDK - JavaScript/TypeScript client for OpenDesk API.\n\n## Features\n\n- **Full API Coverage** - 90+ endpoints across 9 modules (auth / namespaces / skills / review / labels / teams / admin / users / relay)\n- **Type Safe** - Written in TypeScript with full type definitions\n- **JWT Auth** - Login sets the JWT session token (auto-applied to every request)\n- **Timeout & Retry** - Configurable request timeout, idempotent-request retry with exponential backoff\n- **Cancellation** - `AbortSignal` passthrough for every request\n- **Interceptors** - `onRequest` / `onResponse` hooks, custom `fetch` injection\n- **Error Handling** - Typed error classes; parses both FastAPI `{detail}` and OpenAI-style `{\"error\":{message,code}}` bodies\n- **Pagination** - Built-in async generators for automatic page iteration\n- **Zero Dependencies** - Uses native fetch API (Node.js >= 18) and works in browsers\n- **Download** - Files as `Blob` (browser and Node.js >= 18) or raw `ArrayBuffer`\n\n## Installation\n\n```bash\nnpm install @bitclub.ai/opendesk-server-sdk\n```\n\n## Quick Start\n\n```typescript\nimport { OpenDeskClient } from '@bitclub.ai/opendesk-server-sdk';\n\nconst client = new OpenDeskClient({\n  baseUrl: 'http://127.0.0.1:8000/api/v1',\n});\n\n// Discover the login methods the server allows\n// (production defaults to third-party login only)\nconst methods = await client.auth.getAuthMethods();\n\n// Third-party login (OpenDesk-direct): redirect the browser, then exchange the\n// callback `code` + `state`. The returned token is applied automatically.\nconst { login_url } = await client.auth.getSocialLoginUrl(methods.providers[0].name);\nwindow.location.href = login_url;\n// ...on the callback page:\n// await client.auth.socialCallback(code, state);\n\n// Password login is only available when methods.password_login is true:\n// await client.auth.login({ username: 'admin', password: 'admin' });\n\n// Get current user\nconst me = await client.auth.getMe();\nconsole.log('Hello,', me.username);\n\n// List namespaces\nconst namespaces = await client.namespaces.list();\nconsole.log('Namespaces:', namespaces.items);\n```\n\n## Authentication\n\n### Third-party Login (OpenDesk-direct)\n\n```typescript\n// Ask the server which login methods are available\nconst methods = await client.auth.getAuthMethods();\n// -> { password_login: boolean, registration: boolean, providers: [{ name, display_name }] }\n\n// Render one button per provider and redirect straight to the IdP\nconst providers = await client.auth.getSocialProviders();\nconst { login_url } = await client.auth.getSocialLoginUrl(providers.items[0].name);\nwindow.location.href = login_url;\n\n// On the callback page, exchange code + state; the token is applied automatically\nawait client.auth.socialCallback(code, state);\n```\n\nCasdoor is used only as the internal user/RBAC store. The legacy browser flow\n(`getCasdoorLoginUrl()` / `casdoorCallback()`) is deprecated in favor of the\nOpenDesk-direct social flow above.\n\n### Desktop / Native App Login\n\nApps that can open a browser but cannot receive a redirect (Windows GUI, CLI)\nuse the desktop polling flow. `startDesktopLogin()` wraps it end-to-end: it\nfetches the URL, hands it to your `openUrl`, polls until the browser completes\nand sets the token automatically.\n\n```typescript\nimport { shell } from 'electron'; // any platform opener\n\nconst token = await client.auth.startDesktopLogin({\n  provider: 'gitcode',\n  openUrl: (url) => shell.openExternal(url),\n  onPending: ({ elapsedSeconds, expiresAt }) => {\n    console.log(`Waiting for authorization… ${elapsedSeconds}s`, expiresAt);\n  },\n  // intervalSeconds: 5,  // fallback when the server omits retry_after\n  // timeoutSeconds: 600, // signed-state TTL\n  // signal: controller.signal, // cancel when the user gives up\n});\n// token: { access_token, token_type, mode: 'desktop' }, already set on the client\n```\n\nOn failure it rejects with `ServerError` (provider denied), `OpenDeskError(408)`\n(timeout) or `AbortError` (cancelled). The lower-level primitives\n`getSocialLoginUrl(provider, 'desktop')` and `getSocialSession(state)` remain\navailable if you prefer to drive polling yourself.\n\n### Session Management\n\n```typescript\nconst client = new OpenDeskClient({ baseUrl: 'http://127.0.0.1:8000/api/v1' });\n\n// Refresh returns a new token WITHOUT auto-applying it:\nconst token = await client.auth.refresh();\nclient.setToken(token.access_token); // rotate manually\n\n// Logout - revokes the current JWT\nawait client.auth.logout();\n```\n\n### Password Login (may be disabled)\n\n```typescript\n// Password login is disabled by default in production.\n// When methods.password_login is false, POST /auth/login, /auth/register and\n// /auth/local/change-password all return 403 - hide those entry points.\nawait client.auth.login({ username: 'your-user', password: 'your-pass' });\nawait client.auth.getMe(); // Authorization header sent automatically\n```\n\n### Account Management\n\n```typescript\nawait client.auth.register({ username, email, password }); // only when methods.registration\nawait client.auth.updateMe({ display_name: 'New Name' });  // update profile\nawait client.auth.updateAvatar(formData);                  // upload avatar (<= 2MB)\nconst accounts = await client.auth.listOAuthAccounts();    // bound OAuth accounts\nawait client.auth.unlinkOAuthAccount('gitcode');           // unbind\nawait client.auth.changePassword({ old_password, new_password });\n```\n\n## API Modules\n\n### Namespaces (Teams)\n\n```typescript\n// Create a namespace\nconst ns = await client.namespaces.create({\n  slug: 'my-team',\n  name: 'My Team',\n  is_public: false,\n});\n\n// Members\nawait client.namespaces.addMember('my-team', { user_id: 42, role: 'ADMIN' });\nconst members = await client.namespaces.listMembers('my-team');\n\n// Invitations\nconst inv = await client.namespaces.createInvitation('my-team', { role: 'MEMBER', expires_in_days: 7 });\nconst invitations = await client.namespaces.listInvitations('my-team');\nawait client.namespaces.revokeInvitation('my-team', inv.id);\nawait client.namespaces.acceptInvitation(inv.token); // join as member\n```\n\n### Skills\n\n```typescript\n// Search skills\nconst results = await client.skills.search({ q: 'ai', page_size: 20 });\n\n// Publish a skill (two-step: upload then publish)\nconst upload = await client.skills.upload(formData); // FormData with .zip file\nconst published = await client.skills.publish({\n  namespace: 'my-team',\n  slug: 'my-skill',\n  name: 'My Skill',\n  version: '1.0.0',\n  upload_token: upload.upload_token,\n});\n\n// Ownership transfer / copy\nawait client.skills.transfer('my-team', 'my-skill', { target_namespace: 'other', target_user_id: 42 });\nawait client.skills.copy('my-team', 'my-skill', { target_namespace: 'other' });\n\n// Download a skill\nconst blob = await client.skills.download('my-team', 'my-skill', '1.0.0');\n```\n\n### Review (Governance)\n\n```typescript\n// List pending reviews / my todos\nconst pending = await client.review.pending({ namespace: 'my-team' });\nconst todos = await client.review.myTodos();\n\n// Approve / reject / promote\nawait client.review.approve('publish-id', { reason: 'Looks good!' });\nawait client.review.reject('publish-id', { reason: 'Needs fixes' });\nawait client.review.promoteGlobal('publish-id', { reason: 'Excellent' });\n\n// Skill change requests\nconst requests = await client.review.changeRequests({ status: 'PENDING' });\nawait client.review.approveChangeRequest('request-id');\n\n// Super admin: audit log & promotion candidates\nawait client.review.auditLog({ action: 'APPROVE' });\nawait client.review.promotionCandidates();\n```\n\n### Labels\n\n```typescript\nconst labels = await client.labels.list();          // all labels\nawait client.labels.create({ name: 'AI', category: 'tech' });\n```\n\n### Teams (Join Requests + Notifications)\n\n```typescript\nawait client.teams.applyJoin('my-namespace');                        // apply to join\nconst requests = await client.teams.listJoinRequests('my-namespace');\nawait client.teams.approveJoin('my-namespace', 'request-id');\nawait client.teams.rejectJoin('my-namespace', 'request-id');\n\nconst notifs = await client.teams.listNotifications();\nawait client.teams.markRead('notification-id');\nawait client.teams.markAllRead();\n```\n\n### Admin (Super Admin Only)\n\n```typescript\n// Operation logs\nconst logs = await client.admin.listLogs({ source: 'opendesk' });\nconst csvBlob = await client.admin.exportLogs({ action: 'approve' });\n\n// Namespace quota\nawait client.admin.updateNamespaceQuota('my-team', { max_members: 50, max_skills: 100 });\n\n// RBAC\nconst permissions = await client.admin.listPermissions();\nconst roles = await client.admin.listRoles();\nawait client.admin.createRole('admin', 'my-team');               // query params\nawait client.admin.deleteRole('admin');\nawait client.admin.listUserRoles(42);                            // by user id\nawait client.admin.assignRole({ username: 'alice', role: 'owner', namespace_slug: 'my-team' });\nawait client.admin.removeRole('alice', 'owner', 'my-team');\n\n// Skill import (skillhub.cn)\nconst preview = await client.admin.previewImportSkills({ limit: 10 });\nawait client.admin.importSkills(preview.items);\n```\n\n### Users\n\n```typescript\nconst profile = await client.users.getProfile('username');\n\nconst followers = await client.users.getFollowers('username');   // { items: [...] }, non-paginated\nconst following = await client.users.getFollowing('username');\n\nawait client.users.follow('username');\nawait client.users.unfollow('username');\n\nconst dataBlob = await client.users.exportMyData(); // GDPR export (ZIP)\nawait client.users.deleteAccount(true);\n```\n\n### Relay (模型接入)\n\nThe relay is an OpenAI-compatible gateway backed by **LiteLLM**. Use it with the **official OpenAI SDK** - just swap the `baseURL`. First-party apps use the login JWT; third-party tools should use the per-user virtual key (`sk-...`) from `getCredential()` / `createCredential()` / `rotateCredential()`. The module covers channels (LiteLLM model deployments), credits, usage and credentials.\n\n```typescript\n// Admin side (super_admin)\nconst { channels } = await client.relay.listChannels();\nawait client.relay.createChannel({\n  name: 'deepseek-chat',       // exposed model name (no provider prefix)\n  model: 'deepseek-chat',      // upstream model (defaults to name)\n  base_url: 'https://api.deepseek.com/v1',\n  api_key: 'sk-...',\n  ratio: 1,                    // billing multiplier (actual price = price x ratio)\n  input_cost_per_token: 1,     // CNY per 1M tokens (see note below)\n  output_cost_per_token: 2,\n});\nawait client.relay.testChannel(channelId);\nawait client.relay.fetchUpstreamModels({ base_url: 'https://api.deepseek.com/v1', api_key: 'sk-...' });\n\n// Credits management (credit_enabled)\nconst credits = await client.relay.listCredits();\nawait client.relay.adjustCredit(userId, { amount: 10, reason: 'welcome', valid_months: 12 }); // delta\nawait client.relay.adjustCredit(userId, { amount: 0, mode: 'set' });                          // absolute\n\n// Credit policy (monthly gift / top-up validity / reset day)\nawait client.relay.updateCreditPolicy({ monthly_quota_points: 100, reset_day: 1 });\n\nconst usage = await client.relay.getUsage({ status: 'ok' });\nconst stats = await client.relay.getStats({ group_by: 'model' });\nconst trend = await client.relay.getTrend({ start: '2026-01-01' });\n\n// User side\nconst myCredits = await client.relay.getCredits();\nconst lots = await client.relay.getMyCreditLots(); // per-lot expiry + balance\nconst myUsage = await client.relay.getMyUsage();\nconst { credential } = await client.relay.getCredential(); // sk-xxx, copyable\n```\n\n> Cost fields keep the `*_per_token` names for SDK compatibility, but their value\n> is **CNY per 1M tokens** (the backend converts to LiteLLM per-token).\n\n#### Calling the relay with the official OpenAI SDK\n\n```typescript\nimport OpenAI from 'openai';\n\n// 1. Get the relay base URL (or hardcode the server's relay URL)\nconst info = await client.relay.getInfo(); // { base_url, models }\n\n// 2a. First-party: use the login JWT\nconst relay = new OpenAI({\n  baseURL: info.base_url,          // e.g. https://relay.example.com/v1\n  apiKey: client.token,            // the OpenDesk JWT\n});\n\n// 2b. Third-party: use a per-user virtual key (scoped to the relay only)\nconst { credential } = await client.relay.createCredential();\nconst relay3p = new OpenAI({ baseURL: info.base_url, apiKey: credential });\n\nconst chat = await relay.chat.completions.create({\n  model: info.models[0],\n  messages: [{ role: 'user', content: 'Hello' }],\n});\n\n// Streaming works as usual:\nconst stream = await relay.chat.completions.create({\n  model: info.models[0],\n  messages: [{ role: 'user', content: 'Hi' }],\n  stream: true,\n});\nfor await (const chunk of stream) {\n  process.stdout.write(chunk.choices[0]?.delta?.content ?? '');\n}\n```\n\n## Client Options\n\n```typescript\nconst client = new OpenDeskClient({\n  baseUrl: 'https://your-server.com/api/v1', // API base URL\n  token: '<jwt>',                            // optional: pre-set JWT session token\n  headers: { 'X-Custom-Header': 'value' },   // optional: sent with every request\n\n  timeoutMs: 30_000,     // default 30000 - request timeout\n  retries: 0,            // default 0 - retries for idempotent requests (GET/DELETE), exponential backoff\n  retryDelayMs: 500,     // default 500 - base backoff delay (multiplied by 2^n)\n\n  fetch: customFetch,    // optional: inject a fetch implementation\n  onRequest(method, path, init) {},   // optional: per-attempt request hook\n  onResponse(method, path, response) {}, // optional: response hook\n});\n```\n\n### Timeout, Retry & Cancellation\n\n```typescript\n// A timeout raises OpenDeskError with status 408.\n// Retries apply only to idempotent methods (GET/DELETE) and never to\n// user-cancelled requests; backoff is retryDelayMs * 2^n.\n\n// Cancel a long-running request with AbortSignal:\nconst controller = new AbortController();\nconst data = await client.request('GET', '/skills/search', undefined, {\n  params: { q: 'ai' },\n  signal: controller.signal,\n});\ncontroller.abort(); // aborts in-flight requests\n```\n\n### Downloading Binary Data\n\n```typescript\n// Default: Blob (browser) / Buffer (Node)\nconst blob = await client.download('/users/me/export');\n\n// Raw bytes:\nconst buf: ArrayBuffer = await client.download('/files/data.bin', undefined, 'arraybuffer');\n```\n\n### Low-level requests\n\n```typescript\n// Raw request with per-call overrides\nconst result = await client.request('POST', '/custom/endpoint', { payload: 1 }, {\n  params: { page: 1 },\n  headers: { 'X-Extra': 'yes' },\n  responseType: 'json', // 'json' | 'blob' | 'arraybuffer'\n  signal,\n});\n\n// Convenience methods: get / post / put / patch / delete / upload\nawait client.upload('/skills/upload', formData);\n```\n\n## Pagination\n\n```typescript\n// Method 1: Manual\nconst page1 = await client.skills.catalog({ page: 1, page_size: 20 });\n\n// Method 2: Async generator (memory-efficient)\nfor await (const items of client.skills.catalogPaginator({}, 10)) {\n  console.log('Batch:', items.length);\n}\n\n// Method 3: Fetch all\nconst all = await client.skills.catalogAll({}, 100);\n```\n\n## Error Handling\n\nErrors are typed by status code, and carry `status`, `detail` and an optional OpenAI-style `code`.\n\n```typescript\nimport {\n  OpenDeskClient,\n  AuthenticationError,     // 401\n  PermissionDeniedError,   // 403\n  NotFoundError,           // 404\n  ValidationError,         // 422\n  QuotaExceededError,      // 422 (quota message)\n  ServerError,             // >= 500\n} from '@bitclub.ai/opendesk-server-sdk';\n\ntry {\n  await client.auth.login({ username: 'wrong', password: 'wrong' });\n} catch (error) {\n  if (error instanceof AuthenticationError) {\n    console.error('Login failed:', error.detail);\n  } else if (error instanceof NotFoundError) {\n    console.error('Not found:', error.detail);\n  } else if (error instanceof OpenDeskError) {\n    console.error(`API error [${error.status}] code=${error.code}:`, error.detail);\n  }\n}\n```\n\nBoth FastAPI `{detail: \"...\"}` and OpenAI-style `{\"error\": {\"message\": \"...\", \"code\": \"...\"}}`\nerror bodies are parsed automatically.\n\n## Environment Variables (SDK, Node.js only)\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `OPENDESK_BASE_URL` | `http://127.0.0.1:8000/api/v1` | API base URL when `baseUrl` is not passed |\n| `OPENDESK_TOKEN` | - | JWT token used when `token` is not passed |\n\n## Requirements\n\n- Node.js >= 18 (for native fetch API)\n- Or modern browsers with fetch support\n\n## License\n\n项目采用 [Mulan PSL v2](http://license.coscl.org.cn/MulanPSL2)（Mulan Permissive Software License, Version 2，SPDX: `MulanPSL-2.0`）开源协议。\n","readmeFilename":""}