Outline API完全指南:从入门到精通
核心概念:构建API交互基础
作为开发者,我们在与Outline知识库集成时,首先需要理解其API设计的核心原则和基础组件。这些概念将贯穿我们使用API的整个过程,帮助我们构建更健壮、高效的集成方案。
API架构概览
Outline采用了RESTful设计风格,但在实现上有其独特之处。所有API端点都以/api为基础路径,采用JSON格式进行数据交换。这种设计确保了接口的一致性和可预测性,让我们能够轻松地理解和使用各个功能模块。
认证机制详解
Outline API使用JWT(JSON Web Token,一种基于JSON的轻量级身份认证令牌)进行身份验证。这种机制允许我们在不频繁传递用户名和密码的情况下,安全地进行API调用。
认证流程如下:
- 使用用户名和密码获取JWT令牌
- 在后续请求的Authorization头中携带令牌
- 服务器验证令牌有效性并授权访问
以下是获取令牌的示例代码:
async function getAuthToken(email, password) {
const response = await fetch('/api/auth.login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error(`认证失败: ${response.statusText}`);
}
const data = await response.json();
return data.data.token;
}
通用响应结构
Outline API的所有响应都遵循一致的JSON结构,这大大简化了我们处理API返回数据的过程。理解这个结构是高效解析API响应的关键。
标准响应格式:
{
"pagination": { // 分页信息,仅在返回列表数据时包含
"offset": 0,
"limit": 20,
"total": 100
},
"data": [], // 接口返回的具体数据
"policies": {} // 权限信息,描述当前用户对返回资源的操作权限
}
错误处理机制
API调用过程中难免会遇到错误,Outline提供了结构化的错误响应,帮助我们快速定位和解决问题。错误响应包含错误名称、消息、状态码和详细信息,使调试过程更加高效。
常见错误状态码:
- 400: 请求参数错误
- 401: 未授权,需要认证
- 403: 权限不足
- 404: 资源不存在
- 422: 验证错误
- 500: 服务器内部错误
错误响应格式:
{
"error": {
"name": "ValidationError",
"message": "Invalid input provided",
"status": 422,
"details": [
{
"path": ["title"],
"message": "Title is required"
}
]
}
}
自测问题
思考一下:在你的项目中,如何设计一个健壮的API错误处理机制?考虑网络错误、认证失败和业务逻辑错误等不同场景。
操作指南:API核心功能实战
掌握了核心概念后,让我们深入了解如何实际操作Outline API。这一部分将通过具体的业务场景,详细介绍常用接口的使用方法和最佳实践。
文档管理基础
文档是Outline的核心资源,掌握文档的CRUD(创建、读取、更新、删除)操作是使用API的基础。让我们逐一了解这些操作的实现方法。
创建文档
场景描述:在项目管理系统中,当创建一个新项目时,自动在Outline中创建一个对应的项目文档,包含项目描述、目标和时间线。
请求参数:
| 参数名 | 类型 | 描述 | 必要性 |
|---|---|---|---|
| title | string | 文档标题 | 必需 |
| text | string | 文档内容,使用ProseMirror JSON格式 | 必需 |
| collectionId | string | 所属集合ID | 必需 |
| parentDocumentId | string | 父文档ID | 可选 |
| publish | boolean | 是否发布 | 可选,默认false |
| icon | string | 文档图标 | 可选 |
| color | string | 图标颜色,十六进制格式 | 可选 |
示例代码:
async function createProjectDocument(token, projectData) {
const response = await fetch('/api/documents.create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
title: `${projectData.name} - 项目文档`,
text: {
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: `${projectData.name} 项目文档` }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: projectData.description }]
},
// 更多文档内容...
]
},
collectionId: projectData.outlineCollectionId,
publish: true,
icon: 'file-text',
color: '#3B82F6'
})
});
const result = await response.json();
if (result.error) {
throw new Error(`创建文档失败: ${result.error.message}`);
}
return result.data;
}
错误案例:
{
"error": {
"name": "ValidationError",
"message": "Invalid input provided",
"status": 422,
"details": [
{
"path": ["title"],
"message": "Title is required"
},
{
"path": ["collectionId"],
"message": "Collection not found"
}
]
}
}
获取文档详情
场景描述:在内部知识库门户中,展示Outline文档内容,并提供编辑功能。
请求参数:
| 参数名 | 类型 | 描述 | 必要性 |
|---|---|---|---|
| id | string | 文档ID | 必需 |
| shareId | string | 共享链接ID,用于通过共享链接访问 | 可选 |
| apiVersion | number | API版本,可选值:1, 2 | 可选,默认1 |
示例代码:
async function getDocumentDetails(token, documentId) {
const response = await fetch('/api/documents.info', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
id: documentId,
apiVersion: 2
})
});
const result = await response.json();
if (result.error) {
throw new Error(`获取文档失败: ${result.error.message}`);
}
return result.data.document;
}
💡 技巧:使用apiVersion=2可以获取更丰富的文档元数据,包括修订历史和评论信息,适合构建更完整的文档管理界面。
更新文档
场景描述:实现一个自动化工作流,当项目状态更新时,自动更新Outline中的项目文档状态部分。
请求参数:
| 参数名 | 类型 | 描述 | 必要性 |
|---|---|---|---|
| id | string | 文档ID | 必需 |
| title | string | 新文档标题 | 可选 |
| text | string | 新文档内容 | 可选 |
| append | boolean | 是否追加内容 | 可选,默认false |
| publish | boolean | 是否发布 | 可选 |
示例代码:
async function updateProjectStatus(token, documentId, newStatus) {
// 先获取当前文档内容
const document = await getDocumentDetails(token, documentId);
// 在文档末尾添加状态更新
const statusUpdate = {
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: `状态更新: ${new Date().toISOString().split('T')[0]}` }]
};
const statusParagraph = {
type: 'paragraph',
content: [{ type: 'text', text: newStatus }]
};
// 如果设置了append=true,则可以直接添加内容而不是替换整个文档
const response = await fetch('/api/documents.update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
id: documentId,
text: {
...document.text,
content: [...document.text.content, statusUpdate, statusParagraph]
},
publish: true
})
});
const result = await response.json();
if (result.error) {
throw new Error(`更新文档失败: ${result.error.message}`);
}
return result.data;
}
⚠️ 警告:直接替换文档内容(append=false)会覆盖现有内容,请确保在更新前备份重要信息或使用版本控制功能。
删除文档
场景描述:实现一个文档清理工具,自动将超过90天未更新的临时文档移到回收站。
请求参数:
| 参数名 | 类型 | 描述 | 必要性 |
|---|---|---|---|
| id | string | 文档ID | 必需 |
| permanent | boolean | 是否永久删除,默认为false(放入回收站) | 可选 |
示例代码:
async function archiveOldDocuments(token, oldDocumentIds) {
const results = [];
for (const documentId of oldDocumentIds) {
try {
const response = await fetch('/api/documents.delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
id: documentId,
permanent: false // 先移到回收站,而不是直接永久删除
})
});
const result = await response.json();
if (result.error) {
console.error(`删除文档 ${documentId} 失败: ${result.error.message}`);
results.push({ id: documentId, success: false, error: result.error.message });
} else {
results.push({ id: documentId, success: true });
}
} catch (error) {
console.error(`删除文档 ${documentId} 时发生错误: ${error.message}`);
results.push({ id: documentId, success: false, error: error.message });
}
}
return results;
}
文档列表与搜索
在实际应用中,我们经常需要获取文档列表或搜索特定内容。Outline提供了强大的列表和搜索接口,让我们能够高效地查找和筛选文档。
获取文档列表
场景描述:构建一个自定义文档管理界面,按更新时间展示团队最近修改的文档。
请求参数:
| 参数名 | 类型 | 描述 | 必要性 |
|---|---|---|---|
| sort | string | 排序字段,可选值:createdAt, updatedAt, publishedAt, index, title | 可选,默认updatedAt |
| direction | string | 排序方向,可选值:ASC, DESC | 可选,默认DESC |
| collectionId | string | 集合ID,用于筛选特定集合下的文档 | 可选 |
| userId | string | 创建者ID,用于筛选特定用户创建的文档 | 可选 |
| statusFilter | array | 状态筛选,可选值:published, draft, archived | 可选 |
| limit | number | 每页数量 | 可选,默认20 |
| offset | number | 偏移量,用于分页 | 可选,默认0 |
示例代码:
async function getRecentDocuments(token, options = {}) {
const {
collectionId,
limit = 20,
offset = 0,
statusFilter = ["published"]
} = options;
const response = await fetch('/api/documents.list', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
sort: 'updatedAt',
direction: 'DESC',
collectionId,
statusFilter,
limit,
offset
})
});
const result = await response.json();
if (result.error) {
throw new Error(`获取文档列表失败: ${result.error.message}`);
}
return {
documents: result.data,
pagination: result.pagination
};
}
🔍 重点:使用分页参数(limit和offset)可以避免一次性加载过多数据,提高应用性能。特别是在处理大型知识库时,分页是必不可少的优化手段。
搜索文档内容
场景描述:实现一个智能搜索功能,允许用户在多个集合中搜索特定关键词,并高亮显示匹配内容。
请求参数:
| 参数名 | 类型 | 描述 | 必要性 |
|---|---|---|---|
| query | string | 搜索关键词 | 必需 |
| collectionId | string | 集合ID,限制搜索范围 | 可选 |
| dateFilter | string | 日期筛选,可选值:day, week, month, year | 可选 |
| statusFilter | array | 状态筛选 | 可选 |
| snippetMinWords | number | 摘要最小单词数 | 可选 |
| snippetMaxWords | number | 摘要最大单词数 | 可选 |
示例代码:
async function searchDocuments(token, query, options = {}) {
const {
collectionId,
dateFilter,
statusFilter = ["published"],
snippetMinWords = 20,
snippetMaxWords = 30,
limit = 20,
offset = 0
} = options;
const response = await fetch('/api/documents.search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
query,
collectionId,
dateFilter,
statusFilter,
snippetMinWords,
snippetMaxWords,
limit,
offset
})
});
const result = await response.json();
if (result.error) {
throw new Error(`搜索文档失败: ${result.error.message}`);
}
return {
results: result.data,
pagination: result.pagination
};
}
文档状态管理
Outline提供了丰富的文档状态管理功能,包括归档、恢复、发布和取消发布等操作,让我们能够灵活地管理文档生命周期。
归档与恢复文档
场景描述:实现一个季度内容审核工作流,将过时的文档归档,需要时可以快速恢复。
请求参数(归档):
| 参数名 | 类型 | 描述 | 必要性 |
|---|---|---|---|
| id | string | 文档ID | 必需 |
请求参数(恢复):
| 参数名 | 类型 | 描述 | 必要性 |
|---|---|---|---|
| id | string | 文档ID | 必需 |
| collectionId | string | 恢复到的集合ID | 可选 |
| revisionId | string | 恢复到的版本ID | 可选 |
示例代码:
// 归档文档
async function archiveDocument(token, documentId) {
const response = await fetch('/api/documents.archive', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ id: documentId })
});
const result = await response.json();
if (result.error) {
throw new Error(`归档文档失败: ${result.error.message}`);
}
return result.data;
}
// 恢复文档
async function restoreDocument(token, documentId, options = {}) {
const { collectionId, revisionId } = options;
const response = await fetch('/api/documents.restore', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
id: documentId,
collectionId,
revisionId
})
});
const result = await response.json();
if (result.error) {
throw new Error(`恢复文档失败: ${result.error.message}`);
}
return result.data;
}
💡 技巧:结合文档列表接口和状态筛选,可以实现一个归档管理界面,方便查看和管理所有已归档文档。
发布与取消发布
场景描述:实现一个内容审核工作流,作者创建文档后提交审核,审核通过后自动发布。
请求参数(取消发布):
| 参数名 | 类型 | 描述 | 必要性 |
|---|---|---|---|
| id | string | 文档ID | 必需 |
| detach | boolean | 是否从集合中分离 | 可选,默认false |
示例代码:
// 发布文档(创建或更新时设置publish: true)
async function publishDocument(token, documentId) {
const response = await fetch('/api/documents.update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
id: documentId,
publish: true
})
});
const result = await response.json();
if (result.error) {
throw new Error(`发布文档失败: ${result.error.message}`);
}
return result.data;
}
// 取消发布文档
async function unpublishDocument(token, documentId, detach = false) {
const response = await fetch('/api/documents.unpublish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
id: documentId,
detach
})
});
const result = await response.json();
if (result.error) {
throw new Error(`取消发布文档失败: ${result.error.message}`);
}
return result.data;
}
自测问题
尝试设计一个API调用序列,实现以下业务流程:创建一个新文档,设置其权限,发布它,然后创建一个更新版本,最后将旧版本归档。
高级应用:接口组合与性能优化
掌握了基础操作后,让我们探索如何组合使用多个API接口来实现复杂业务流程,并学习如何优化API调用性能。
接口组合使用
实际业务场景往往需要多个API接口的协同工作。下面我们将介绍几个常见的跨接口业务流程。
文档批量迁移
场景描述:将一个集合中的所有文档迁移到另一个集合,并保持文档之间的父子关系。
实现步骤:
- 获取源集合中的所有文档
- 按层级顺序创建文档(先父后子)
- 更新新文档的权限设置
- 迁移完成后可选删除源文档
流程图:
graph TD
A[获取源集合文档列表] --> B{是否有未处理文档};
B -- 是 --> C[创建新文档];
C --> D[设置文档权限];
D --> E[记录新旧文档ID映射];
E --> F[处理子文档];
F --> B;
B -- 否 --> G[迁移完成];
示例代码:
async function migrateDocuments(token, sourceCollectionId, targetCollectionId) {
// 1. 获取源集合中的所有文档
let offset = 0;
const batchSize = 50;
const documentMap = new Map(); // 存储旧文档ID到新文档ID的映射
console.log('开始文档迁移...');
while (true) {
const { documents, pagination } = await getRecentDocuments(token, {
collectionId: sourceCollectionId,
limit: batchSize,
offset,
statusFilter: ["published", "draft"]
});
if (documents.length === 0) break;
// 2. 按层级顺序处理文档(先处理顶级文档)
const topLevelDocuments = documents.filter(doc => !doc.parentDocumentId);
for (const doc of topLevelDocuments) {
await migrateDocumentWithChildren(
token, doc, sourceCollectionId, targetCollectionId, documentMap
);
}
offset += batchSize;
if (offset >= pagination.total) break;
}
console.log(`文档迁移完成,共迁移 ${documentMap.size} 个文档`);
return Array.from(documentMap.entries());
}
async function migrateDocumentWithChildren(
token, doc, sourceCollectionId, targetCollectionId, documentMap
) {
// 如果已迁移过,直接返回
if (documentMap.has(doc.id)) {
return documentMap.get(doc.id);
}
console.log(`迁移文档: ${doc.title}`);
// 创建新文档
const newDoc = await createProjectDocument(token, {
name: doc.title,
description: doc.text,
outlineCollectionId: targetCollectionId,
// 从原文档复制其他属性
icon: doc.icon,
color: doc.color
});
// 记录映射关系
documentMap.set(doc.id, newDoc.id);
// 迁移权限设置
await copyDocumentPermissions(token, doc.id, newDoc.id);
// 迁移子文档
const { documents } = await getRecentDocuments(token, {
collectionId: sourceCollectionId,
parentDocumentId: doc.id
});
for (const childDoc of documents) {
// 递归迁移子文档,此时父文档ID已经映射为新ID
await migrateDocumentWithChildren(
token, childDoc, sourceCollectionId, targetCollectionId, documentMap
);
// 更新子文档的父文档ID为新ID
await updateDocumentParent(token, documentMap.get(childDoc.id), newDoc.id);
}
return newDoc.id;
}
// 辅助函数:复制文档权限
async function copyDocumentPermissions(token, sourceDocId, targetDocId) {
// 实际实现需要获取源文档权限并应用到目标文档
// 这里简化处理
}
// 辅助函数:更新文档父ID
async function updateDocumentParent(token, documentId, parentDocumentId) {
const response = await fetch('/api/documents.move', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
id: documentId,
parentDocumentId,
collectionId: null // 保持当前集合
})
});
const result = await response.json();
if (result.error) {
throw new Error(`更新文档父ID失败: ${result.error.message}`);
}
return result.data;
}
文档版本控制与回滚
场景描述:实现一个文档版本管理系统,允许用户查看历史版本并回滚到指定版本。
实现步骤:
- 获取文档的修订历史
- 展示版本列表,包含版本号、修改时间和修改人
- 允许用户选择特定版本查看内容
- 提供回滚到选中版本的功能
示例代码:
// 获取文档修订历史
async function getDocumentRevisions(token, documentId) {
const document = await getDocumentDetails(token, documentId);
return document.revisions || [];
}
// 恢复到指定版本
async function revertToRevision(token, documentId, revisionId) {
// 1. 获取指定版本的内容
const response = await fetch('/api/revisions.info', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
id: revisionId,
documentId
})
});
const result = await response.json();
if (result.error) {
throw new Error(`获取修订版本失败: ${result.error.message}`);
}
const revisionContent = result.data.text;
// 2. 创建新版本,保留原版本的内容
const updateResponse = await fetch('/api/documents.update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
id: documentId,
text: revisionContent,
// 添加版本说明
title: `${document.title} (已回滚到版本 ${revisionId.substring(0, 8)})`
})
});
const updateResult = await response.json();
if (updateResult.error) {
throw new Error(`回滚版本失败: ${updateResult.error.message}`);
}
return updateResult.data;
}
性能优化建议
高效使用API不仅能提升应用性能,还能减少服务器负载。以下是一些API调用的性能优化建议。
分页参数最佳实践
场景描述:在构建文档列表页面时,如何高效加载大量文档数据。
优化策略:
- 使用合理的分页大小(建议20-50条/页)
- 实现无限滚动或分页加载,避免一次性加载过多数据
- 缓存已加载的页面,避免重复请求
- 在列表页只请求必要的字段,详情页再请求完整数据
示例代码:
class DocumentPaginator {
constructor(token, collectionId) {
this.token = token;
this.collectionId = collectionId;
this.pageSize = 30; // 合理的分页大小
this.cache = new Map(); // 缓存已加载的页面
this.totalDocuments = null;
}
async getPage(pageNumber) {
if (this.cache.has(pageNumber)) {
return this.cache.get(pageNumber);
}
const offset = (pageNumber - 1) * this.pageSize;
const { documents, pagination } = await getRecentDocuments(this.token, {
collectionId: this.collectionId,
limit: this.pageSize,
offset,
// 只请求列表页需要的字段
fields: ['id', 'title', 'updatedAt', 'createdBy', 'icon', 'color']
});
this.totalDocuments = pagination.total;
const result = {
documents,
pageNumber,
totalPages: Math.ceil(this.totalDocuments / this.pageSize),
totalDocuments: this.totalDocuments
};
this.cache.set(pageNumber, result);
return result;
}
// 预加载下一页,提升用户体验
async preloadNextPage(pageNumber) {
const nextPage = pageNumber + 1;
if (!this.cache.has(nextPage) &&
(!this.totalDocuments || nextPage * this.pageSize < this.totalDocuments)) {
// 后台预加载
this.getPage(nextPage).catch(err =>
console.error(`预加载页面 ${nextPage} 失败: ${err.message}`)
);
}
}
}
请求合并策略
场景描述:当需要同时获取多个文档的详情时,如何减少API调用次数。
优化策略:
- 实现请求批处理,合并多个请求
- 使用Promise.all并发请求,但控制并发数量
- 实现请求缓存,避免重复请求相同资源
示例代码:
class DocumentCache {
constructor(token) {
this.token = token;
this.cache = new Map();
this.pendingRequests = new Map(); // 用于处理并发请求
}
async getDocument(documentId) {
// 如果缓存中已有,直接返回
if (this.cache.has(documentId)) {
return this.cache.get(documentId);
}
// 如果有 pending 请求,等待其完成
if (this.pendingRequests.has(documentId)) {
return await this.pendingRequests.get(documentId);
}
// 发起新请求
const promise = getDocumentDetails(this.token, documentId)
.then(doc => {
this.cache.set(documentId, doc);
return doc;
})
.finally(() => {
this.pendingRequests.delete(documentId);
});
this.pendingRequests.set(documentId, promise);
return promise;
}
// 批量获取文档
async getDocuments(documentIds) {
// 分离已缓存和未缓存的文档ID
const cached = [];
const toFetch = [];
for (const id of documentIds) {
if (this.cache.has(id)) {
cached.push(this.cache.get(id));
} else {
toFetch.push(id);
}
}
// 并发获取未缓存的文档,控制并发数量
const concurrency = 5; // 限制并发请求数量
const fetched = [];
for (let i = 0; i < toFetch.length; i += concurrency) {
const batch = toFetch.slice(i, i + concurrency);
const batchResults = await Promise.all(
batch.map(id => this.getDocument(id))
);
fetched.push(...batchResults);
}
// 按原始ID顺序返回结果
return documentIds.map(id =>
cached.find(doc => doc.id === id) || fetched.find(doc => doc.id === id)
);
}
// 清除缓存
clearCache(documentId) {
if (documentId) {
this.cache.delete(documentId);
} else {
this.cache.clear();
}
}
}
接口版本迁移指南
随着API的迭代,不同版本之间可能存在不兼容的变更。了解如何处理版本迁移可以帮助我们保持应用的兼容性。
v1到v2的兼容性处理
主要变更:
- v2版本返回更丰富的文档元数据
- 权限模型有所调整
- 部分字段名称变更
迁移策略:
- 检测API版本支持情况
- 实现版本兼容的请求处理
- 平滑过渡到新版本特性
示例代码:
class APIVersionAdapter {
constructor(token) {
this.token = token;
this.supportedVersions = null;
}
// 检测支持的API版本
async detectSupportedVersions() {
const response = await fetch('/api/version', {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.token}`
}
});
const result = await response.json();
this.supportedVersions = result.data.supportedVersions;
return this.supportedVersions;
}
// 获取文档详情,自动适配API版本
async getDocumentDetails(documentId) {
if (!this.supportedVersions) {
await this.detectSupportedVersions();
}
const apiVersion = this.supportedVersions.includes(2) ? 2 : 1;
const response = await fetch('/api/documents.info', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({
id: documentId,
apiVersion
})
});
const result = await response.json();
if (result.error) {
throw new Error(`获取文档失败: ${result.error.message}`);
}
// 标准化响应格式,无论API版本如何
return this.normalizeDocumentResponse(result.data.document, apiVersion);
}
// 标准化不同版本的响应格式
normalizeDocumentResponse(document, apiVersion) {
if (apiVersion === 1) {
// v1版本响应转换为v2格式
return {
...document,
// v1中没有的字段添加默认值
revisions: [],
comments: [],
attachments: []
};
}
// v2版本直接返回
return document;
}
}
自测问题
如何设计一个API请求合并和缓存系统,以优化同时加载多个文档详情的性能?考虑缓存失效策略和并发控制。
问题解决:常见错误与调试技巧
在使用API的过程中,我们不可避免会遇到各种问题。本节将介绍常见错误的排查方法和解决方案,以及实用的调试技巧。
常见错误及解决方案
认证失败 (401错误)
常见原因:
- JWT令牌过期
- 令牌格式错误
- 用户权限已被撤销
排查步骤:
- 检查令牌是否过期
- 验证令牌格式是否正确
- 确认用户是否具有访问资源的权限
解决方案:
// 实现自动令牌刷新机制
class AuthManager {
constructor() {
this.token = localStorage.getItem('outline_token');
this.refreshToken = localStorage.getItem('outline_refresh_token');
this.tokenExpiry = parseInt(localStorage.getItem('outline_token_expiry') || '0');
}
// 检查令牌是否有效
isTokenValid() {
if (!this.token) return false;
// 提前30秒刷新令牌
return Date.now() < this.tokenExpiry - 30 * 1000;
}
// 获取有效的令牌
async getValidToken() {
if (this.isTokenValid()) {
return this.token;
}
// 尝试刷新令牌
if (this.refreshToken) {
try {
const response = await fetch('/api/auth.refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken: this.refreshToken })
});
const result = await response.json();
if (result.error) {
throw new Error('刷新令牌失败');
}
// 更新令牌信息
this.token = result.data.token;
this.refreshToken = result.data.refreshToken;
this.tokenExpiry = Date.now() + result.data.expiresIn * 1000;
// 保存到本地存储
localStorage.setItem('outline_token', this.token);
localStorage.setItem('outline_refresh_token', this.refreshToken);
localStorage.setItem('outline_token_expiry', this.tokenExpiry.toString());
return this.token;
} catch (error) {
console.error('令牌刷新失败:', error);
// 刷新失败,需要重新登录
this.logout();
throw new Error('需要重新登录');
}
}
// 没有刷新令牌,需要登录
this.logout();
throw new Error('需要登录');
}
// 登出
logout() {
this.token = null;
this.refreshToken = null;
this.tokenExpiry = 0;
localStorage.removeItem('outline_token');
localStorage.removeItem('outline_refresh_token');
localStorage.removeItem('outline_token_expiry');
// 重定向到登录页
window.location.href = '/login';
}
// 包装API请求,自动处理认证
async fetchWithAuth(url, options = {}) {
const token = await this.getValidToken();
const headers = {
'Content-Type': 'application/json',
...options.headers,
'Authorization': `Bearer ${token}`
};
const response = await fetch(url, { ...options, headers });
// 如果仍然认证失败,强制重新登录
if (response.status === 401) {
this.logout();
throw new Error('会话已过期,请重新登录');
}
return response;
}
}
权限不足 (403错误)
常见原因:
- 用户没有操作资源的权限
- 共享链接权限已被撤销
- 文档已被移动或删除
排查步骤:
- 检查用户是否具有所需权限
- 验证资源是否存在且未被移动
- 确认API调用参数是否正确
解决方案:
// 权限检查辅助函数
async function checkDocumentPermission(authManager, documentId, requiredPermission) {
try {
const response = await authManager.fetchWithAuth('/api/documents.permissions', {
method: 'POST',
body: JSON.stringify({ id: documentId })
});
const result = await response.json();
if (result.error) {
throw new Error(result.error.message);
}
const permissions = result.data.permissions;
// 权限级别:admin > read_write > read
const permissionLevels = {
'read': 1,
'read_write': 2,
'admin': 3
};
return permissionLevels[permissions] >= permissionLevels[requiredPermission];
} catch (error) {
console.error('权限检查失败:', error);
return false;
}
}
// 使用示例
async function safeUpdateDocument(authManager, documentId, updates) {
// 先检查权限
const hasPermission = await checkDocumentPermission(
authManager, documentId, 'read_write'
);
if (!hasPermission) {
throw new Error('没有更新文档的权限');
}
// 执行更新
const response = await authManager.fetchWithAuth('/api/documents.update', {
method: 'POST',
body: JSON.stringify({ id: documentId, ...updates })
});
const result = await response.json();
if (result.error) {
throw new Error(`更新文档失败: ${result.error.message}`);
}
return result.data;
}
请求参数错误 (422错误)
常见原因:
- 参数格式不正确
- 缺少必填参数
- 参数值超出范围
排查步骤:
- 检查请求参数是否完整
- 验证参数类型和格式
- 查看错误详情中的具体字段
解决方案:
// 请求参数验证工具
class RequestValidator {
constructor(schema) {
this.schema = schema;
}
validate(data) {
const errors = [];
// 检查必填字段
for (const [field, config] of Object.entries(this.schema)) {
if (config.required && (data[field] === undefined || data[field] === null)) {
errors.push({
path: [field],
message: `${field} is required`
});
} else if (data[field] !== undefined && config.type) {
// 检查类型
if (typeof data[field] !== config.type) {
errors.push({
path: [field],
message: `${field} must be of type ${config.type}`
});
}
// 检查枚举值
if (config.enum && !config.enum.includes(data[field])) {
errors.push({
path: [field],
message: `${field} must be one of: ${config.enum.join(', ')}`
});
}
// 检查自定义验证规则
if (config.validate && !config.validate(data[field])) {
errors.push({
path: [field],
message: config.errorMessage || `${field} is invalid`
});
}
}
}
return {
valid: errors.length === 0,
errors
};
}
}
// 创建文档的参数验证器
const documentCreateValidator = new RequestValidator({
title: { type: 'string', required: true },
text: { type: 'object', required: true },
collectionId: { type: 'string', required: true },
publish: { type: 'boolean', required: false },
icon: { type: 'string', required: false },
color: {
type: 'string',
required: false,
validate: (value) => /^#([0-9A-F]{3}){1,2}$/i.test(value),
errorMessage: 'color must be a valid hex color code'
},
parentDocumentId: { type: 'string', required: false }
});
// 使用验证器
function createDocumentWithValidation(authManager, documentData) {
const { valid, errors } = documentCreateValidator.validate(documentData);
if (!valid) {
return Promise.reject({
name: 'ValidationError',
message: 'Invalid input provided',
details: errors
});
}
// 验证通过,发送请求
return authManager.fetchWithAuth('/api/documents.create', {
method: 'POST',
body: JSON.stringify(documentData)
}).then(response => response.json());
}
API调试工具推荐
有效的调试工具可以大大提高解决问题的效率。以下是一些推荐的API调试工具和配置示例。
Postman配置
Postman是一个功能强大的API调试工具,可以帮助我们轻松测试API端点。
配置步骤:
- 创建一个新的Collection
- 设置认证方式为Bearer Token
- 添加常用API端点
- 创建环境变量管理不同环境的API地址
环境变量配置:
- base_url: http://localhost:3000/api
- token: {{your_jwt_token}}
示例请求:
- 方法: POST
- URL: {{base_url}}/documents.list
- 头部: Authorization: Bearer {{token}}
- 请求体:
{
"collectionId": "{{collection_id}}",
"sort": "updatedAt",
"direction": "DESC",
"limit": 20
}
curl命令示例
对于喜欢命令行的开发者,curl是一个强大的工具:
# 获取认证令牌
curl -X POST http://localhost:3000/api/auth.login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"your_password"}'
# 获取文档列表
curl -X POST http://localhost:3000/api/documents.list \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{"collectionId":"YOUR_COLLECTION_ID","limit":20}'
浏览器开发工具
现代浏览器的开发工具也提供了强大的API调试功能:
- 打开Chrome开发者工具 (F12)
- 切换到Network标签
- 筛选XHR/fetch请求
- 查看请求详情、响应和时间线
社区贡献接口扩展
Outline作为开源项目,欢迎社区贡献新的API功能。如果你有好的想法,可以通过以下方式参与:
- 查看项目源码中的API路由定义:server/routes/
- 参考现有API实现,遵循相同的设计模式
- 添加新的API端点和相应的测试
- 提交Pull Request到官方仓库
在实现新API时,请遵循以下原则:
- 保持与现有API风格一致
- 提供完整的参数验证
- 返回标准化的响应格式
- 添加详细的API文档
- 编写单元测试
自测问题
如何设计一个API错误监控系统,能够自动捕获、分类和报告API调用错误?考虑如何区分临时错误和永久错误,并实现相应的重试策略。
GLM-5智谱 AI 正式发布 GLM-5,旨在应对复杂系统工程和长时域智能体任务。Jinja00
GLM-5-w4a8GLM-5-w4a8基于混合专家架构,专为复杂系统工程与长周期智能体任务设计。支持单/多节点部署,适配Atlas 800T A3,采用w4a8量化技术,结合vLLM推理优化,高效平衡性能与精度,助力智能应用开发Jinja00
jiuwenclawJiuwenClaw 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。Python0213- QQwen3.5-397B-A17BQwen3.5 实现了重大飞跃,整合了多模态学习、架构效率、强化学习规模以及全球可访问性等方面的突破性进展,旨在为开发者和企业赋予前所未有的能力与效率。Jinja00
AtomGit城市坐标计划AtomGit 城市坐标计划开启!让开源有坐标,让城市有星火。致力于与城市合伙人共同构建并长期运营一个健康、活跃的本地开发者生态。01
OpenDeepWikiOpenDeepWiki 是 DeepWiki 项目的开源版本,旨在提供一个强大的知识管理和协作平台。该项目主要使用 C# 和 TypeScript 开发,支持模块化设计,易于扩展和定制。C#00