import axios from 'axios';
export interface YuqueUser {
id: number;
name: string;
login: string;
description?: string;
avatar_url: string;
}
export interface YuqueRepo {
id: number;
name: string;
slug: string;
description?: string;
namespace: string;
public: boolean;
created_at: string;
updated_at: string;
}
export interface YuqueDoc {
id: number;
slug: string;
title: string;
description?: string;
format: 'markdown' | 'lake' | 'html';
body: string;
body_draft: string;
created_at: string;
updated_at: string;
word_count: number;
}
export class YuqueClient {
private baseURL = 'https://www.yuque.com/api/v2';
private token: string;
constructor(token: string) {
this.token = token;
}
private async request<T>(endpoint: string, options: any = {}): Promise<T> {
const url = `${this.baseURL}${endpoint}`;
try {
const response = await axios({
url,
headers: {
'X-Auth-Token': this.token,
'Content-Type': 'application/json',
'User-Agent': 'Yuque-MCP-Server/1.0.0',
...options.headers,
},
...options,
});
return response.data.data || response.data;
} catch (error: any) {
throw new Error(`Yuque API Error: ${error.response?.data?.message || error.message}`);
}
}
// 获取用户信息
async getUser(): Promise<YuqueUser> {
return this.request<YuqueUser>('/user');
}
// 获取知识库列表
async getRepos(userId?: string): Promise<YuqueRepo[]> {
// 根据调试结果,必须使用 /users/{userId}/repos
if (!userId) {
// 如果没有提供userId,需要先获取当前用户信息
const user = await this.getUser();
userId = user.id.toString();
}
return this.request<YuqueRepo[]>(`/users/${userId}/repos`);
}
// 获取文档列表
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);
}
// 获取文档详情 - 需要知识库ID和文档ID
async getDoc(docId: number, repoId?: number): Promise<YuqueDoc> {
if (!repoId) {
// 如果没有提供repoId,需要从文档列表中查找
throw new Error('getDoc requires repoId parameter');
}
return this.request<YuqueDoc>(`/repos/${repoId}/docs/${docId}`);
}
// 创建文档
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: title.toLowerCase().replace(/[^a-z0-9]/g, '-'),
body: content,
format,
},
});
}
// 更新文档 - 需要知识库ID
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,
});
}
// 删除文档 - 需要知识库ID
async deleteDoc(docId: number, repoId: number): Promise<void> {
await this.request(`/repos/${repoId}/docs/${docId}`, {
method: 'DELETE',
});
}
// 搜索文档
async searchDocs(query: string, repoId?: number): Promise<YuqueDoc[]> {
const params = new URLSearchParams({
q: query,
type: 'doc' // 必须提供type参数
});
if (repoId) params.append('repo_id', repoId.toString());
return this.request<YuqueDoc[]>(`/search?${params.toString()}`);
}
// 创建知识库
async createRepo(name: string, description?: string, isPublic: boolean = false): Promise<YuqueRepo> {
// 生成符合语雀要求的 slug:只允许字母、数字、连字符,且不能以连字符开头或结尾
let slug = name.toLowerCase()
.replace(/[\u4e00-\u9fa5]/g, '') // 移除中文字符
.replace(/[^a-z0-9]/g, '-') // 其他非字母数字字符替换为连字符
.replace(/-+/g, '-') // 多个连字符合并为一个
.replace(/^-|-$/g, ''); // 移除开头和结尾的连字符
// 如果处理后为空,使用时间戳
if (!slug) {
slug = 'repo-' + Date.now();
}
// 先获取当前用户ID
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',
},
});
}
}