Skip to main content
Glama
yuque-client.ts7.43 kB
/** * Yuque API Client with enhanced error handling and configuration support * 增强的语雀API客户端,支持更好的错误处理和配置 */ import axios, { AxiosInstance, AxiosResponse } from 'axios'; import type { Config } from './config/index.js'; // Enhanced type definitions with better documentation export interface YuqueUser { id: number; type: string; login: string; name: string; description?: string; avatar_url: string; public: number; followers_count: number; following_count: number; created_at: string; updated_at: string; } export interface YuqueRepo { id: number; type: string; slug: string; name: string; description?: string; namespace: string; public: boolean; items_count: number; likes_count: number; watches_count: number; created_at: string; updated_at: string; user?: YuqueUser; } export interface YuqueDoc { id: number; type: string; slug: string; title: string; description?: string; format: 'markdown' | 'lake' | 'html'; body: string; body_draft: string; created_at: string; updated_at: string; word_count: number; published_at?: string; first_published_at?: string; } // Custom error class for Yuque API errors export class YuqueApiError extends Error { constructor( message: string, public status?: number, public code?: string, public response?: any ) { super(message); this.name = 'YuqueApiError'; } } export class YuqueClient { private client: AxiosInstance; private config: Config; constructor(configOrToken: Config | string) { // Support both legacy string token and new config object if (typeof configOrToken === 'string') { this.config = { token: configOrToken, apiBaseURL: 'https://www.yuque.com/api/v2', timeout: 30000, retries: 3, }; } else { this.config = configOrToken; } this.client = axios.create({ baseURL: this.config.apiBaseURL, timeout: this.config.timeout, headers: { 'X-Auth-Token': this.config.token, 'Content-Type': 'application/json', 'User-Agent': 'Yuque-MCP-Server/1.1.0', }, }); // Add response interceptor for better error handling this.client.interceptors.response.use( (response) => response, (error) => { const status = error.response?.status; const data = error.response?.data; const message = data?.message || error.message || 'Unknown error'; throw new YuqueApiError( `Yuque API Error (${status}): ${message}`, status, data?.code, data ); } ); } /** * Make a request to the Yuque API with retry logic */ private async request<T>(endpoint: string, options: any = {}): Promise<T> { let lastError: Error; for (let attempt = 1; attempt <= this.config.retries; attempt++) { try { const response: AxiosResponse = await this.client.request({ url: endpoint, ...options, }); // Handle different response formats return response.data.data || response.data; } catch (error) { lastError = error as Error; // Don't retry on client errors (4xx) if (error instanceof YuqueApiError && error.status && error.status >= 400 && error.status < 500) { throw error; } // Wait before retry (exponential backoff) if (attempt < this.config.retries) { await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000) ); } } } throw lastError!; } /** * Get current user information */ async getUser(): Promise<YuqueUser> { return this.request<YuqueUser>('/user'); } /** * Get repositories (knowledge bases) */ async getRepos(userId?: string): Promise<YuqueRepo[]> { // Use /users/{userId}/repos as confirmed working endpoint if (!userId) { const user = await this.getUser(); userId = user.id.toString(); } return this.request<YuqueRepo[]>(`/users/${userId}/repos`); } /** * Get documents in a repository */ async getDocs(repoId: number, options?: { limit?: number; offset?: number }): Promise<YuqueDoc[]> { const params = new URLSearchParams(); if (options?.limit) params.append('limit', options.limit.toString()); if (options?.offset) params.append('offset', options.offset.toString()); const endpoint = `/repos/${repoId}/docs${params.toString() ? '?' + params.toString() : ''}`; return this.request<YuqueDoc[]>(endpoint); } /** * Get document details */ async getDoc(docId: number, repoId: number): Promise<YuqueDoc> { return this.request<YuqueDoc>(`/repos/${repoId}/docs/${docId}`); } /** * Create a new document */ async createDoc( repoId: number, title: string, content: string, format: 'markdown' | 'lake' | 'html' = 'markdown' ): Promise<YuqueDoc> { return this.request<YuqueDoc>(`/repos/${repoId}/docs`, { method: 'POST', data: { title, slug: this.generateSlug(title), body: content, format, }, }); } /** * Update an existing document */ async updateDoc( docId: number, repoId: number, title?: string, content?: string, format?: 'markdown' | 'lake' | 'html' ): Promise<YuqueDoc> { const data: any = {}; if (title) data.title = title; if (content) data.body = content; if (format) data.format = format; return this.request<YuqueDoc>(`/repos/${repoId}/docs/${docId}`, { method: 'PUT', data, }); } /** * Delete a document */ async deleteDoc(docId: number, repoId: number): Promise<void> { await this.request(`/repos/${repoId}/docs/${docId}`, { method: 'DELETE', }); } /** * Search documents */ async searchDocs(query: string, repoId?: number): Promise<YuqueDoc[]> { const params = new URLSearchParams({ q: query, type: 'doc', // Required parameter }); if (repoId) params.append('repo_id', repoId.toString()); return this.request<YuqueDoc[]>(`/search?${params.toString()}`); } /** * Create a new repository (knowledge base) */ async createRepo( name: string, description?: string, isPublic: boolean = false ): Promise<YuqueRepo> { const slug = this.generateSlug(name); // Get current user ID for repository creation const user = await this.getUser(); const userId = user.id; return this.request<YuqueRepo>(`/users/${userId}/repos`, { method: 'POST', data: { name, slug, description: description || '', public: isPublic ? 1 : 0, type: 'Book', }, }); } /** * Generate a valid slug from repository/document name */ private generateSlug(name: string): string { // Remove Chinese characters and convert to lowercase let slug = name.toLowerCase() .replace(/[\u4e00-\u9fa5]/g, '') // Remove Chinese characters .replace(/[^a-z0-9]/g, '-') // Replace non-alphanumeric with hyphens .replace(/-+/g, '-') // Merge multiple hyphens .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens // If slug is empty after processing, use timestamp if (!slug) { slug = 'item-' + Date.now(); } return slug; } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tanis2010/yuque-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server