首页
/ Outline API完全指南:从入门到精通

Outline API完全指南:从入门到精通

2026-03-12 04:44:49作者:卓艾滢Kingsley

核心概念:构建API交互基础

作为开发者,我们在与Outline知识库集成时,首先需要理解其API设计的核心原则和基础组件。这些概念将贯穿我们使用API的整个过程,帮助我们构建更健壮、高效的集成方案。

API架构概览

Outline采用了RESTful设计风格,但在实现上有其独特之处。所有API端点都以/api为基础路径,采用JSON格式进行数据交换。这种设计确保了接口的一致性和可预测性,让我们能够轻松地理解和使用各个功能模块。

Outline API架构示意图

认证机制详解

Outline API使用JWT(JSON Web Token,一种基于JSON的轻量级身份认证令牌)进行身份验证。这种机制允许我们在不频繁传递用户名和密码的情况下,安全地进行API调用。

认证流程如下:

  1. 使用用户名和密码获取JWT令牌
  2. 在后续请求的Authorization头中携带令牌
  3. 服务器验证令牌有效性并授权访问

以下是获取令牌的示例代码:

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接口的协同工作。下面我们将介绍几个常见的跨接口业务流程。

文档批量迁移

场景描述:将一个集合中的所有文档迁移到另一个集合,并保持文档之间的父子关系。

实现步骤

  1. 获取源集合中的所有文档
  2. 按层级顺序创建文档(先父后子)
  3. 更新新文档的权限设置
  4. 迁移完成后可选删除源文档

流程图

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;
}

文档版本控制与回滚

场景描述:实现一个文档版本管理系统,允许用户查看历史版本并回滚到指定版本。

实现步骤

  1. 获取文档的修订历史
  2. 展示版本列表,包含版本号、修改时间和修改人
  3. 允许用户选择特定版本查看内容
  4. 提供回滚到选中版本的功能

示例代码

// 获取文档修订历史
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版本返回更丰富的文档元数据
  • 权限模型有所调整
  • 部分字段名称变更

迁移策略

  1. 检测API版本支持情况
  2. 实现版本兼容的请求处理
  3. 平滑过渡到新版本特性

示例代码

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令牌过期
  • 令牌格式错误
  • 用户权限已被撤销

排查步骤

  1. 检查令牌是否过期
  2. 验证令牌格式是否正确
  3. 确认用户是否具有访问资源的权限

解决方案

// 实现自动令牌刷新机制
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错误)

常见原因

  • 用户没有操作资源的权限
  • 共享链接权限已被撤销
  • 文档已被移动或删除

排查步骤

  1. 检查用户是否具有所需权限
  2. 验证资源是否存在且未被移动
  3. 确认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错误)

常见原因

  • 参数格式不正确
  • 缺少必填参数
  • 参数值超出范围

排查步骤

  1. 检查请求参数是否完整
  2. 验证参数类型和格式
  3. 查看错误详情中的具体字段

解决方案

// 请求参数验证工具
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端点。

配置步骤

  1. 创建一个新的Collection
  2. 设置认证方式为Bearer Token
  3. 添加常用API端点
  4. 创建环境变量管理不同环境的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调试功能:

  1. 打开Chrome开发者工具 (F12)
  2. 切换到Network标签
  3. 筛选XHR/fetch请求
  4. 查看请求详情、响应和时间线

社区贡献接口扩展

Outline作为开源项目,欢迎社区贡献新的API功能。如果你有好的想法,可以通过以下方式参与:

  1. 查看项目源码中的API路由定义:server/routes/
  2. 参考现有API实现,遵循相同的设计模式
  3. 添加新的API端点和相应的测试
  4. 提交Pull Request到官方仓库

在实现新API时,请遵循以下原则:

  • 保持与现有API风格一致
  • 提供完整的参数验证
  • 返回标准化的响应格式
  • 添加详细的API文档
  • 编写单元测试

自测问题

如何设计一个API错误监控系统,能够自动捕获、分类和报告API调用错误?考虑如何区分临时错误和永久错误,并实现相应的重试策略。

登录后查看全文
热门项目推荐
相关项目推荐