前面两篇文章分别介绍了如何采用OpenWiki的Personal模式构建属于个人的知识库,以及这个由Wiki页面构成的知识库具有怎样的结构,这篇文章聚焦知识库原始数据的摄取,看看源自如下这些来源的数据是如何被对应的Connector摄取。
- custom-mcp: 摄取任意已配置的HTTP或stdio MCP服务器数据(仅限只读安全工具),提供高度定制化的外部异构系统接入。
- git-repo: 摄取本地版本库数据,读取仓库路径并生成紧凑的清单文件,提供本地代码与资产上下文。
- x: 摄取主页时间线、用户帖子、提及、书签和列表推文,提供个人社交网络与外部关注动态。
- notion: 摄取Notion工作区内的页面与文档,提供个人知识库与结构化笔记上下文。
- google: 摄取最近的邮件通信数据,提供个人日常沟通与事务往来信息。
- web-search: 摄取实时的公开网页搜索结果,提供广泛的外部互联网信息追踪。
- hackernews: 摄取公开的科技新闻与讨论数据,提供科技、创业领域的行业趋势与社区信号。
- slack: 摄取团队工作区内的对话与消息,提供企业内部协作与即时通讯上下文。
1. ConnectorRuntime
Connector在OpenWiki中被表示成如下这个ConnectorRuntime类型,ConnectorDefinition提供了Connector的基本定义,具体的数据摄取操作则体现在ingest方法上。
export type ConnectorRuntime = ConnectorDefinition & {
ingest: (options?: ConnectorIngestOptions) => Promise<ConnectorIngestResult>;
};
1.1 ConnectorDefinition
ConnectorDefinition是OpenWiki连接器子系统的静态元数据契约。它只负责声明一个连接器是什么以及需要什么配置,不包含实际的运行逻辑(运行逻辑由扩展了它的ConnectorRuntime通过ingest方法提供)。
export type ConnectorDefinition = {
backend: ConnectorBackend;
description: string;
displayName: string;
id: ConnectorId;
mode: "code" | "personal";
requiredEnv: string[];
supportsAgenticDiscovery: boolean;
};
export type ConnectorBackend = "direct-api" | "local-git" | "mcp-http" | "mcp-stdio";
各字段说明如下:
- backend:连接器与外部数据源通信的传输方式,对应类型
ConnectorBackend提供了分别代表四种传输方式的字面量; - description:连接器功能的人类可读说明,描述其数据来源与用途;
- displayName:面向用户展示的可读名称,在界面中代替
id呈现; - id:连接器的唯一标识符,用于注册表查找和状态持久化;
- mode:运行模式,
code为仓库文档生成,personal为个人知识库; - requiredEnv:正常工作所需的环境变量名称列表,用于判定配置是否就绪;
- supportsAgenticDiscovery:是否支持Agent自主发现模式,否则采用确定性摄取逻辑。
由于Connector在进行数据摄取的时候都需要提供用户凭证(比如API-KEY),所以只有在独立运行时才能正常工作。由于OpenWiki在code模式是以MCP Server的方式运行在Codeing Agent的进程中(采用stdio协议),无法获取凭证,所以OpenWiki提供的9种Connector都只能应用在personal模型下。实际上code模式下根本就不会有Connector被创建。
1.2 ConnectorIngestOptions
ConnectorRuntime用来完成数据摄取的ingest方法是一个将ConnectorIngestOptions和ConnectorIngestResult作为输入和输出的异步方法。ConnectorIngestOptions是Connector进行数据摄取时的运行时配置选项,用于在单次摄取时传入可选的配置、实例标识、数量/时间范围限制、本地仓库根路径以及要处理的数据流列表,从而让同一连接器按不同参数灵活执行数据拉取。
export type ConnectorIngestOptions = {
connectorConfig?: Record<string, unknown>;
instanceId?: string;
limit?: number;
repoRoot?: string;
streams?: string[];
windowHours?: number;
};
各字段说明如下:
- connectorConfig:连接器的自定义配置项,以键值对形式传入,供具体连接器读取个性化参数;
- instanceId:连接器实例的唯一标识,用于区分同一连接器的多个实例;
- limit:本次摄取的最大条目数量限制;
- repoRoot:仓库根目录路径,供需要访问本地仓库的连接器使用;
- streams:要摄取的数据流名称列表,用于筛选特定来源或频道;
- windowHours:摄取的时间窗口(小时),限定只拉取该时间范围内的数据。
1.3 ConnectorIngestResult
ConnectorRuntime的ingest方法在完成了数据摄取后由ConnectorIngestResult对象返回摄取的结果。ConnectorIngestResult记录一次摄取产生的连接器标识、摘要消息、原始文件列表、运行 ID、状态持久化路径、执行状态(成功/跳过/出错)以及非致命警告,用于追踪、展示和后续处理单次摄取的结果。
export type ConnectorIngestResult = {
connectorId: ConnectorId;
message: string;
rawFiles: string[];
runId: string;
statePath: string;
status: "error" | "skipped" | "success";
warnings: string[];
};
export type ConnectorId =
| "custom-mcp"
| "git-repo"
| "google"
| "hackernews"
| "langsmith"
| "notion"
| "slack"
| "web-search"
| "x";
各字段说明如下:
- connectorId:产生本次结果的连接器标识,取自
ConnectorId联合类型,后者体现了目前默认支持的9种数据来源; - message:本次摄取结果的摘要信息,用于向用户或日志说明执行情况;
- rawFiles:本次摄取生成的原始文件路径列表;
- runId:本次摄取运行的唯一标识,用于追踪和关联状态;
- statePath:本次运行状态持久化的路径;
- status:运行结果状态,
success成功、skipped跳过、error出错; - warnings:本次运行产生的非致命警告信息列表。
2. 配置的读取
Connector通常都具有属于自己的专属配置,并且会根据配置结构定义对应的配置类型。按照约定,配置文件以JSON的形式存放在connectors/{connector-id}/config.json中。OpenWiki定义了如下这个readConnectorConfig函数根据指定的connector-id加载对应的配置文件,并将其反序列化成指定的配置类型T。参数defaultConfig提供默认的配置选项。
export async function readConnectorConfig<T extends object>(
connectorId: ConnectorId,
defaultConfig: T,
): Promise<T> {
await ensureConnectorHome(connectorId);
try {
return {
...defaultConfig,
...(JSON.parse(
await readFile(getConnectorConfigPath(connectorId), "utf8"),
) as T),
};
} catch (error) {
if (isFileNotFoundError(error)) {
return defaultConfig;
}
throw error;
}
}
3. 原始内容的存储
OpenWiki在执行某个Connector的摄取操作前会调用如下这个createRunId函数创建一个标识当前摄取操作的ID,我们将其称为run_id。在完成数据摄取任务后会将得到的内容写入connectors/{connector-id}/raw/{run_id}中目录下。具体的写入工作由如下这个writeRawJson函数完成,该函数返回写入文件的完整路径。
export function createRunId(): string {
return new Date().toISOString().replace(/[:.]/gu, "-");
}
export async function writeRawJson(
connectorId: ConnectorId,
runId: string,
filename: string,
value: unknown,
): Promise<string> {
await ensureConnectorHome(connectorId);
const filePath = path.join(getConnectorRawDir(connectorId), runId, filename);
await writePrivateJson(filePath, value);
return filePath;
}
4. 状态的维护
Connector针对每次数据摄取都会记录在connectors/{connector-id}/state.json这个状态文件中,该文件还会单独记录最后一次执行的时间。OpenWiki定义了如下这个ConnectorState类型专门定义某个Collector的当前状态。
export type ConnectorState = {
lastRunAt?: string;
latestIds?: Record<string, string>;
runs?: ConnectorRunSummary[];
version: 1;
};
各状态字段说明如下:
- lastRunAt: 最后执行数据摄取的时间戳;
- latestIds:标识最终一次数据摄取操作的ID;
- runs:历次数据摄取操作的摘要描述;
- version:当前版本
在完成摄取后会将内容存储一个或者多个源文件中。用来承载每次摄取摘要信息的ConnectorRunSummary分别利用runId和rawFiles字段记录了当前的run_id和写入的文件路径。at、status和warnings字段分别记录了摄取的时间戳、状态("error" | "skipped" | "success")和警告信息。
export type ConnectorRunSummary = {
at: string;
rawFiles: string[];
runId: string;
status: ConnectorIngestResult["status"];
warnings: string[];
};
OpenWiki定义了如下所示的readConnectorState和updateStateWithRun函数实现了针对Collector状态的读写
export async function readConnectorState(
connectorId: ConnectorId,
): Promise<ConnectorState> {
await ensureConnectorHome(connectorId);
try {
return JSON.parse(
await readFile(getConnectorStatePath(connectorId), "utf8"),
) as ConnectorState;
} catch (error) {
if (isFileNotFoundError(error)) {
return { version: 1 };
}
throw error;
}
}
export function updateStateWithRun(
state: ConnectorState,
run: NonNullable<ConnectorState["runs"]>[number],
): ConnectorState {
return {
...state,
lastRunAt: run.at,
runs: [run, ...(state.runs ?? [])].slice(0, 20),
version: 1,
};
}
5. 基于web-search的Connector
OpenWiki默认提供的9种Connector的设计和实现大同小异,它们都提供了用来创建对应ConnectorRuntime的工厂方法,接下来就以web-search这种Connector为例,看看它如何利用Tavily进行网络搜索摄取对应元数据。Tavily是一个专为LLM和AI Agent 设计的搜索API,核心目标是让Agent以更少的token消耗获得结构化、可直接使用的高质量搜索结果。这种Connector对应的工厂函数定义如下:
export function createWebSearchConnector(): ConnectorRuntime {
return {
...definition,
ingest,
};
}
5.1 基础定义
对于createWebSearchConnector工厂函数创建的ConnectorRuntime对象来说,其基础定义体现在definition常量上,该常量定义如下:
const definition: ConnectorDefinition = {
backend: "direct-api",
description:"Fetches web search results with Tavily through the LangChain Tavily integration.",
displayName: "Web Search",
id: "web-search",
mode: "personal",
requiredEnv: [OPENWIKI_TAVILY_API_KEY_ENV_KEY],
supportsAgenticDiscovery: false,
};
export const OPENWIKI_TAVILY_API_KEY_ENV_KEY = "TAVILY_API_KEY";
由于Connector直接调用Tavily API来实施网络搜索,所以其backend字段体现的后端类型为direct-api,并要求利用环境变量TAVILY_API_KEY来提供对应的API-KEY。
5.2 WebSearchConfig
WebSearchConfig是这个Connector的配置类型,用于控制是否启用、搜索范围(包含/排除域名)、结果形态(答案、图片、原始内容、数量)、查询语句、搜索深度、时间范围与主题类别。
type WebSearchConfig = {
enabled?: boolean;
excludeDomains?: string[];
includeAnswer?: boolean;
includeDomains?: string[];
includeImages?: boolean;
includeRawContent?: boolean;
maxResults?: number;
queries?: string[];
searchDepth?: "advanced" | "basic";
timeRange?: "day" | "month" | "week" | "year";
topic?: "general" | "news";
};
各配置字段说明如下:
- enabled:是否启用该web-search配置;
- excludeDomains:搜索时排除的域名列表;
- includeAnswer:是否让搜索服务返回一段汇总答案;
- includeDomains:限定只在这些域名内搜索;
- includeImages:是否在结果中包含图片;
- includeRawContent:是否返回原始页面内容而非摘要;
- maxResults:单次搜索返回的最大结果数量;
- queries:预定义的搜索查询语句列表;
- searchDepth:搜索深度,
basic快速、advanced更深入; - timeRange:结果时间范围,限定为
day、week、month或year内; - topic:搜索主题类别,
general通用或news新闻。
如有没有对配置选项进行显式设置,其默认的兜底配置体现在如下的代码中。
async function ingest(
options: ConnectorIngestOptions = {},
): Promise<ConnectorIngestResult> {
...
const config = {
...(await readConnectorConfig<WebSearchConfig>("web-search", {
enabled: true,
includeAnswer: true,
includeImages: false,
includeRawContent: false,
maxResults: 5,
queries: [],
searchDepth: "basic",
topic: "general",
})),
...((options.connectorConfig ?? {}) as WebSearchConfig),
};
}
LLM Wiki理论与实践-04:使用OpenWiki的Personal模式构建知识库提供的配置文件就是针对这个结构定义的:
{
"queries": [
"美国中期选举",
"US midterm elections",
"2026 midterm elections",
"congressional race 2026"
]
}
5.3 调用Tavily API
OpenWiki将通过调用Tavily API实施网络搜索实现在一个LangChain工具中,工具类型的声明体现为如下这个派生自StructuredTool的TavilySearch类型。
declare class TavilySearch extends StructuredTool<typeof inputSchema> {
static lc_name(): string;
description: string;
name: string;
schema: z.ZodObject<{
query: z.ZodString;
includeDomains: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
excludeDomains: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
searchDepth: z.ZodOptional<z.ZodEnum<["basic", "advanced"]>>;
includeImages: z.ZodOptional<z.ZodBoolean>;
timeRange: z.ZodOptional<z.ZodEnum<["day", "week", "month", "year"]>>;
topic: z.ZodOptional<z.ZodEnum<["general", "news", "finance"]>>;
}, "strip", z.ZodTypeAny, {
query: string;
includeDomains?: string[] | undefined;
excludeDomains?: string[] | undefined;
searchDepth?: "advanced" | "basic" | undefined;
includeImages?: boolean | undefined;
timeRange?: "day" | "month" | "week" | "year" | undefined;
topic?: "finance" | "general" | "news" | undefined;
}, {
query: string;
includeDomains?: string[] | undefined;
excludeDomains?: string[] | undefined;
searchDepth?: "advanced" | "basic" | undefined;
includeImages?: boolean | undefined;
timeRange?: "day" | "month" | "week" | "year" | undefined;
topic?: "finance" | "general" | "news" | undefined;
}>;
apiBaseUrl?: string;
maxResults?: number;
includeImages?: boolean;
includeImageDescriptions?: boolean;
includeAnswer?: boolean;
includeRawContent?: boolean | "markdown" | "text";
includeDomains?: string[];
excludeDomains?: string[];
searchDepth?: SearchDepth;
topic?: TopicType;
days?: number;
timeRange?: TimeRange;
chunksPerSource?: number;
country?: string;
autoParameters?: boolean;
includeFavicon?: boolean;
includeUsage?: boolean;
handleToolError: boolean;
apiWrapper: TavilySearchAPIWrapper;
constructor(params?: TavilySearchAPIRetrieverFields);
_call(input: InferInteropZodOutput<typeof inputSchema>, _runManager?: CallbackManagerForToolRun): Promise<TavilySearchResponse | {
error: string;
}>;
}
5.4 完整的摄取流程
Connector的摄取操作实现在如下这个ingest函数中,具体的流程如下:
- 调用
readConnectorConfig函数读取配置文件并将其反序列化成WebSearchConfig对象; - 调用
readConnectorState函数读取状态文件并返回ConnectorState对象; - 如果配置将Connector设置成禁用状态,返回一个状态为
skipped的ConnectorIngestResult对象; - 提取环境变量
TAVILY_API_KEY,如果失败则返回一个状态为error的ConnectorIngestResult对象; - 如果
WebSearchConfig对象没有利用queries字段提供查询文本,返回一个状态为skipped的ConnectorIngestResult对象; - 创建工具对象
TavilySearch,并利用它调用Tavily API来实施网络搜索,并调用writeRawJson函数将内容写入原始文件; - 调用
writeConnectorState函数记录最新的状态; - 返回一个状态为
success的ConnectorIngestResult对象;
async function ingest(
options: ConnectorIngestOptions = {},
): Promise<ConnectorIngestResult> {
const runId = createRunId();
const config = {
...(await readConnectorConfig<WebSearchConfig>("web-search", {
enabled: true,
includeAnswer: true,
includeImages: false,
includeRawContent: false,
maxResults: 5,
queries: [],
searchDepth: "basic",
topic: "general",
})),
...((options.connectorConfig ?? {}) as WebSearchConfig),
};
const state = await readConnectorState("web-search");
const warnings: string[] = [];
const rawFiles: string[] = [];
if (!config.enabled) {
return {
connectorId: "web-search",
message: `Web Search connector is not enabled. Set enabled=true in ${openWikiConnectorsDisplayPath}/web-search/config.json.`,
rawFiles,
runId,
statePath: `${openWikiConnectorsDisplayPath}/web-search/state.json`,
status: "skipped",
warnings,
};
}
const tavilyApiKey = process.env[OPENWIKI_TAVILY_API_KEY_ENV_KEY];
if (!tavilyApiKey) {
return {
connectorId: "web-search",
message: `${OPENWIKI_TAVILY_API_KEY_ENV_KEY} is required for Web Search ingestion.`,
rawFiles,
runId,
statePath: `${openWikiConnectorsDisplayPath}/web-search/state.json`,
status: "error",
warnings,
};
}
const queries = normalizeStringArray(config.queries);
if (queries.length === 0) {
return {
connectorId: "web-search",
message: `No web search queries configured. Add queries to ${openWikiConnectorsDisplayPath}/web-search/config.json.`,
rawFiles,
runId,
statePath: `${openWikiConnectorsDisplayPath}/web-search/state.json`,
status: "skipped",
warnings,
};
}
const limit = getOptionLimit(options.limit, config.maxResults);
const timeRange = getWindowedTimeRange(config.timeRange, options.windowHours);
const tool = new TavilySearch({
excludeDomains: normalizeStringArray(config.excludeDomains),
includeAnswer: config.includeAnswer ?? true,
includeDomains: normalizeStringArray(config.includeDomains),
includeImages: config.includeImages ?? false,
includeRawContent: config.includeRawContent ?? false,
maxResults: limit,
searchDepth: normalizeSearchDepth(config.searchDepth),
tavilyApiKey,
timeRange,
topic: normalizeTopic(config.topic),
});
const results = [];
for (const query of queries) {
results.push({
query,
response: (await tool.invoke({ query })) as TavilySearchResult,
});
}
rawFiles.push(
await writeRawJson("web-search", runId, "web-search-results.json", {
fetchedAt: new Date().toISOString(),
instanceId: options.instanceId,
maxResults: limit,
queryCount: queries.length,
results,
searchDepth: normalizeSearchDepth(config.searchDepth),
timeRange,
topic: normalizeTopic(config.topic),
windowHours: normalizeWindowHours(options.windowHours),
}),
);
await writeConnectorState(
"web-search",
updateStateWithRun(state, {
at: new Date().toISOString(),
rawFiles,
runId,
status: "success",
warnings,
}),
);
return {
connectorId: "web-search",
message: `Fetched Tavily results for ${queries.length} web search quer${
queries.length === 1 ? "y" : "ies"
}.`,
rawFiles,
runId,
statePath: `${openWikiConnectorsDisplayPath}/web-search/state.json`,
status: "success",
warnings,
};
}
6. Connector的注册
OpenWiki目前提供的8个Connector都提供了类似于createWebSearchConnector的工厂函数,它们分别是:
- custom-mcp:createMcpConnector
- git-repo:createGitRepoConnector
- google:createGmailConnector
- hackernews:createHackerNewsConnector
- langsmith:createLangSmithConnector
- slack:createSlackConnector
- x: createXConnector
- web-search:createWebSearchConnector
至于Notion,由于对应Connector直接连的是Notion发布的MCP服务器,所以直接使用createMcpConnector作为工厂函数。9个Connector通过如下这个createConnectorRegistry函数进行统一注册:
export function createConnectorRegistry(): Record<
ConnectorId,
ConnectorRuntime
> {
return {
"custom-mcp": createMcpConnector({
description: `Generic read-only MCP knowledge source. Point OpenWiki at any MCP server via ${openWikiConnectorsDisplayPath}/custom-mcp/config.json (HTTP or stdio transport). Prefer allowedTools and/or MCP readOnlyHint; do not guess mutating tools.`,
displayName: "Custom MCP",
id: "custom-mcp",
requiredEnv: [],
}),
"git-repo": createGitRepoConnector(),
google: createGmailConnector(),
hackernews: createHackerNewsConnector(),
langsmith: createLangSmithConnector(),
notion: createMcpConnector({
description:
"Notion connector backed by the hosted Notion MCP server or another configured read-only MCP server.",
displayName: "Notion",
id: "notion",
requiredEnv: ["OPENWIKI_NOTION_MCP_ACCESS_TOKEN"],
}),
slack: createSlackConnector(),
"web-search": createWebSearchConnector(),
x: createXConnector(),
};
}
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/JaydenAI/article/details/170411322


![[LLM Wiki理论与实践-06]详细解析OpenWiki如何摄取外部数据源封面图](https://i-blog.csdnimg.cn/direct/4ca4507be5f74d57b8ef824b2b86c42d.png)

