首页
/ 深入 Cline SDK 的 @cline/shared 共享原语包:路径解析、会话配置、日志契约与跨客户端 DTO

深入 Cline SDK 的 @cline/shared 共享原语包:路径解析、会话配置、日志契约与跨客户端 DTO

2026-09-06 17:56:38作者:凤尚柏Louis

本文基于 Cline 仓库中 @cline/shared 包的官方说明文档,系统讲解这个实验性(experimental)共享包的核心职责:它如何以 ./storage 等子路径导出 Node 端文件系统路径解析器、如何定义跨客户端统一的 BasicLogger 日志契约、如何集中 AgentMode/SessionPromptConfig 等会话配置原语、以及它是如何承载 @cline/cli 与宿主应用之间共享的聊天/Provider RPC 载荷 DTO 的。读完本文,你可以在基于 Cline SDK 构建宿主、运行时或 IDE 扩展时,正确复用这些跨包契约,避免重复定义类型与重复推导数据目录。

包定位:跨包原语的统一出口

@cline/sharedsdk/packages/shared/README.md 中被标注为 experimental 包,其定位是"own shared cross-package primitives",即持有跨包共享的原语(session common types/utilities)。它不是某个具体功能的实现包,而是 Cline 各包(core、agents、CLI、宿主应用)之间的契约层与工具层:日志类型、会话配置形状、hook 会话上下文、运行时载荷 DTO 都在这里集中定义,避免每个宿主重复定义相似字段。

sdk/packages/shared/package.json 可以看到该包的工程形态:

  • 多子路径导出(exports 字段)@cline/shared 按子路径拆分为多个独立入口,宿主可以只引入自己需要的部分:
    • .(根入口,同时区分 browser 条件导出 ./dist/index.browser.js 与 Node 入口 ./dist/index.js
    • ./browser./types./storage./db./node./automation./remote-config
  • 运行环境engines.node >= 22,构建脚本为 BUILD_MODE=package bun bun.mts,单元测试走 vitest run --config vitest.config.ts
  • 依赖极窄:仅 aws4fetchjsonrepairzodzod-to-json-schema 四个运行时依赖,符合"共享原语包"不引入重依赖的设计取向。

README 中的两条中央文档入口也值得保留:包级总览见 sdk/packages/README.md,架构与交互见 sdk/ARCHITECTURE.md

storage 子路径导出:Node-only 文件系统路径解析器

README 明确指出:Node-only 的文件系统路径解析器位于 @cline/shared/storage 子路径导出下,代表性函数为 resolveClineDataDirresolveDbDataDirresolveSessionDataDirresolveTeamDataDir。之所以单独走 ./storage 子路径而不是根入口,正是因为这些函数依赖 node:fs/node:path 等 Node 运行时能力,不能进入浏览器构建(对应 package.json 中 . 入口的 browser 条件导出走的是 index.browser.js,而 ./storage 只声明了 types + import 两个条件)。

实现位于 sdk/packages/shared/src/storage/paths.ts,其核心模式高度一致:优先读取显式环境变量,否则从 Cline 数据根目录派生。以四个代表性函数为例(paths.ts L179-L245):

export function resolveClineDataDir(): string {
	const explicitDir = process.env.CLINE_DATA_DIR?.trim();
	if (explicitDir) {
		return explicitDir;
	}
	return join(resolveClineDir(), "data");
}

export function resolveSessionDataDir(): string {
	const explicitDir = process.env.CLINE_SESSION_DATA_DIR?.trim();
	if (explicitDir) {
		return explicitDir;
	}
	return join(resolveClineDataDir(), "sessions");
}
// resolveTeamDataDir -> CLINE_TEAM_DATA_DIR,默认 <data>/teams
// resolveDbDataDir   -> CLINE_DB_DATA_DIR,默认 <data>/db

整理成一张可操作的参数表:

函数 环境变量覆盖 默认值
resolveClineDataDir() CLINE_DATA_DIR <ClineDir>/data
resolveSessionDataDir() CLINE_SESSION_DATA_DIR <data>/sessions
resolveTeamDataDir() CLINE_TEAM_DATA_DIR <data>/teams
resolveDbDataDir() CLINE_DB_DATA_DIR <data>/db
resolveConnectorDataDir() CLINE_CONNECTOR_DATA_DIR <data>/connectors

这套"环境变量可覆盖 + 目录层级派生"的设计有一个直接收益:CLI、hub 守护进程、IDE 宿主等所有客户端只要遵守同一组环境变量约定,就能落到同一份数据目录上,无需各自实现路径推导。sdk/packages/shared/src/storage/paths.test.ts 中的用例印证了这一点,例如设置 CLINE_DATA_DIR=/tmp/cline-data 后断言 resolveClineDataDir()/tmp/cline-dataresolveSessionDataDir()/tmp/cline-data/sessionsresolveTeamDataDir()/tmp/cline-data/teamsresolveDbDataDir()/tmp/cline-data/db

paths.ts 中还有一组围绕 connector 的路径解析,例如 resolveConnectorLogPath(channel, instanceKey) 会把非法字符替换为 _ 后落到 <data>/logs/connectors/<channel>/<key>.log——源码注释解释了它放在共享包的原因:CLI(直接 spawn 分离 connector)与 hub supervisor(spawn 并回收 connector)必须对该日志路径达成一致,所以它"住在两边都不属于的公共位置"。

附赠能力:容错 Unicode 文件名解析

同目录下的 sdk/packages/shared/src/storage/path-resolution.ts 提供了一个值得注意的健壮性工具 resolveExistingFilePath:macOS(Sonoma 及以后)在截屏文件名 AM/PM 前插入 U+202F 窄不换行空格,当路径经过剪贴板、终端、粘贴解码层后被归一化成普通空格时,字面 fs.stat 会抛 ENOENT。该函数按顺序尝试 macOS AM/PM 变体、NFD 归一化变体、弯引号(U+2019)变体,最后回退到父目录枚举做"规范空白折叠"匹配,从而把被"打伤"的路径恢复到真实磁盘条目。对需要在多种客户端间传递文件路径的 Cline 宿主来说,这是一个典型的"共享包收编边角坑位"的例子。

BasicLogger:跨客户端日志契约

README 的第二块内容是跨客户端日志契约:@cline/shared 导出 BasicLogger,让 runtime、SDK 与宿主应用共享同一个 logger 类型。实现只有 45 行,位于 sdk/packages/shared/src/logging/logger.ts

export interface BasicLogger {
	/** 冗长诊断;宿主应在非调试模式下 no-op 或过滤 */
	debug: (message: string, metadata?: BasicLogMetadata) => void;
	/** 运维消息(取代旧版 info / 非 error warn 的拆分) */
	log: (message: string, metadata?: BasicLogMetadata) => void;
	error?: (
		message: string,
		metadata?: BasicLogMetadata & { error?: unknown },
	) => void;
}

设计要点有三:

  1. 刻意保持"最小接口"debug/log 必填,error 可选——源码注释说明,宿主如果没有独立 error 通道,可以走 log 并在 BasicLogMetadata.severity 中标 "error"。这解释了为什么 metadata 里有一个 severity?: "info" | "warn" | "error" 字段:它是给那些把单一 log 方法映射到多级输出(如 Pino 的 info vs warn)的后端做消歧用的。
  2. BasicLogMetadata 约定了一批跨组件查询键sessionIdrunIdproviderIdtoolNamedurationMs,同时继承 Record<string, unknown> 允许宿主自由扩展。
  3. noopBasicLogger 作为全 no-op 安全默认值,注入缺失时可直接兜底。

这种"接口 + 约定字段 + no-op 兜底"的三件套,使得宿主可以把自己的 logger(pino、VS Code 的日志 API 等)适配进来,而 SDK 内部代码永远只面向 BasicLogger 编程。

会话配置原语:AgentMode 与三个 Session 配置接口

README 强调:会话配置原语被集中在此,"so hosts/runtimes can compose one base shape instead of redefining similar fields repeatedly"(宿主/运行时组合出一个基础形状,而不是反复重定义相似字段)。实现位于 sdk/packages/shared/src/session/runtime-config.ts,四个原语在 sdk/packages/shared/src/index.ts 的 L568-L581 统一从根入口导出。

export type AgentMode = "act" | "plan" | "yolo" | "zen";

export interface SessionPromptConfig {
	mode?: AgentMode;
	systemPrompt?: string;
	rules?: string;
	maxIterations?: number;
}

export interface SessionWorkspaceConfig {
	cwd: string;
	workspaceRoot?: string;
}

export interface SessionExecutionConfig {
	enableTools: boolean;
	teamName?: number | undefined extends undefined ? string : never; // 实际为 teamName?: string
	missionLogIntervalSteps?: number;
	missionLogIntervalMs?: number;
	maxConsecutiveMistakes?: number;
	toolPolicies?: Record<string, ToolPolicy>;
}

(上面 SessionExecutionConfig 按源码原样为 teamName?: stringtoolPolicies 即 README 所说的"canonical ToolPolicy map shape",ToolPolicy 类型来自 sdk/packages/shared/src/llms/tools.ts。)

三个接口分工清晰:

  • SessionPromptConfig:提示词维度——模式、系统提示词、rules、最大迭代数;
  • SessionWorkspaceConfig:工作区维度——cwd 必填、workspaceRoot 可选;
  • SessionExecutionConfig:执行维度——工具开关(唯一必填的 enableTools)、团队名、任务日志节奏、连续错误上限、按工具名的策略表。

同一文件还定义了运行期"配置扩展"原语,供宿主裁剪默认加载的扩展类型:

export type RuntimeConfigExtensionKind =
	| "rules" | "skills" | "workflows" | "plugins" | "hooks";

export const DEFAULT_RUNTIME_CONFIG_EXTENSIONS =
	RUNTIME_CONFIG_EXTENSION_KINDS; // 默认全部启用

并配套 isRuntimeConfigExtensionKind(类型守卫)、parseRuntimeConfigExtensions(从未知输入过滤去重出合法 kind 数组)、hasRuntimeConfigExtension(未显式指定时按"默认全开"判断)三个辅助函数。

会话标识方面,sdk/packages/shared/src/session/index.ts 提供 createSessionId(prefix, suffix):用 nanoid 的小写字母数字字母表生成 5 位随机串,拼上毫秒时间戳,形如 <prefix><ts>_<nanoid5><suffix>,时间戳在前保证可排序,随机后缀避免并发碰撞。

Hook 会话上下文原语

README 声明该包还导出 hook 会话上下文原语,"used across agents/core/CLI":HookSessionContextresolveHookSessionContext(...)resolveRootSessionId(...)resolveHookLogPath(...)

当前源码中前两者的实现在 sdk/packages/shared/src/session/hook-context.ts,并由 sdk/packages/shared/src/index.ts L552-L560 导出:

export interface HookSessionContext {
	rootSessionId?: string;
}

export type HookSessionContextProvider =
	| HookSessionContext
	| ((input?: HookSessionContextLookup) => HookSessionContext | undefined);

export function resolveHookSessionContext(
	provider?: HookSessionContextProvider,
	input?: HookSessionContextLookup,
): HookSessionContext | undefined

resolveHookSessionContext 的设计点在于:provider 既可以是静态上下文对象,也可以是一个函数(由调用方在 hook 触发时惰性求值,HookSessionContextLookup 提供 hookName/conversationId/agentId/parentAgentId 供求值使用);解析结果会对 rootSessionId 做 trim 归一化,空值统一收敛为 undefinedresolveRootSessionId 则是从上下文中取根会话 ID 的轻量辅助。

需要如实说明的是:在检索当前仓库源码时,resolveHookLogPath 仅在 sdk/packages/shared/README.md 中出现,当前 session/hook-context.ts 与根入口导出表中未见同名函数——从源码结构看,该函数可能已被移除或改名,而 hook 日志路径目前以事件载荷字段的形式存在:sdk/packages/shared/src/hooks/events.ts L277-L282 的 HookEventPayloadSchema 中,sessionContext 对象包含可选的 rootSessionIdhookLogPath 两个字符串字段。也就是说,"hook 写日志的位置"这一信息通过 hook 事件 schema 的 sessionContext.hookLogPath 在 hook 执行侧传递。

跨客户端运行时载荷 DTO:rpc/runtime 契约

README 后半部分说明:@cline/shared 还导出跨客户端运行时载荷 DTO,供多个宿主(@cline/cli@cline/code)复用,"so request/response contracts are not duplicated outside transport wiring"(请求/响应契约不在传输接线之外重复定义)。实现集中在 sdk/packages/shared/src/rpc/runtime.ts,共 468 行,根入口 L412-L456 将其类型全部转出。

聊天运行时载荷

export interface ChatRuntimeConfig extends SessionPromptConfig {
	cwd?: string;
	apiKey?: string;
	logger?: RuntimeLoggerConfig;
	enableTools: boolean;
	enableSpawn?: boolean;
	enableTeams?: boolean;
	disableMcpSettingsTools?: boolean;
	autoApproveTools?: boolean;
	missionStepInterval?: number;
	missionTimeIntervalMs?: number;
	timeoutSeconds?: number;
	toolPolicies?: SessionExecutionConfig["toolPolicies"];
	toolExecutors?: HubToolExecutorName[];
	configExtensions?: RuntimeConfigExtensionKind[];
}

export interface ChatStartSessionRequest extends ChatRuntimeConfig {
	sessionId?: string;
	workspaceRoot: string;
	provider: string;
	model: string;
	source?: string;
	interactive?: boolean;
}

可以看到 RPC 层与上文会话原语是组合关系ChatRuntimeConfig extends SessionPromptConfig(因此继承 mode/systemPrompt/rules/maxIterations),toolPolicies 直接复用 SessionExecutionConfig["toolPolicies"] 的类型,configExtensions 复用 RuntimeConfigExtensionKind[]。这正是 README 所说"compose one base shape"的落地:会话配置原语在 RPC 契约中被二次组合,而非复制。

README 提到的 initialMessages、可选 toolPolicies、用于默认系统提示词组装的可选 rules,以及让宿主把序列化日志配置跨传输边界传过去的可选 loggerRuntimeLoggerConfig),在源码中均可对应到字段:

export interface RuntimeLoggerConfig {
	enabled?: boolean;
	level?: "trace" | "debug" | "info" | "warn" | "error" | "fatal" | "silent";
	destination?: string;
	name?: string;
	bindings?: Record<string, string | number | boolean>;
}

其中 bindings 是 README 特别点名的能力:宿主可以为所有运行时日志记录附加稳定的上下文字段(例如 clientIdclientTypeclientApp),从而让远端 runtime 产出的日志在宿主侧可按客户端维度检索。

回合与结果侧同样有完整契约:ChatRunTurnRequest 携带 config(一个完整的 ChatStartSessionRequest)、prompt、可选 attachmentsuserImages: string[]userFiles: ChatAttachmentFile[])以及 delivery?: "queue" | "steer"ChatTurnResult 返回 text、带缓存 token 与成本信息的 usageinputTokens/outputTokens/cacheReadTokens?/cacheWriteTokens?/totalCost?)、iterationsfinishReason 以及每次工具调用的明细数组 toolCalls: ChatToolCallResult[](含 name/input/output/error/durationMs)。同一文件还定义了 ChatStartSessionArtifactssessionId/manifestPath/messagesPath),说明启动会话会落盘 manifest 与消息文件并返回其路径。

Provider 运行时载荷

Provider 侧契约覆盖目录查询、模型操作与增改 Provider:ProviderActionRequestProviderCatalogResponseProviderOAuthLoginResponse,以及 README 点名的细粒度复用契约 AddProviderActionRequestSaveProviderSettingsActionRequestProviderCapability(由 ProviderCapabilitySchema 推导)与 OAuth 相关的 Provider ID 类型。它们集中在 runtime.ts L271-L414 附近,ProviderSettingsActionRequest 等联合类型把目录/模型操作与 settings 宿主的 add/save 操作统一在一棵类型树下——这意味着一个实现 ProviderClient 的宿主可以处理整个 Provider 操作面,而无需为每个 action 单独定义 DTO。

使用建议:把 @cline/shared 当作契约层消费

基于以上源码事实,对要在 Cline 生态里构建宿主或 SDK 集成的开发者,有三条可直接落地的实践:

  1. 数据目录不要自己拼路径。凡是涉及会话、团队、数据库、connector 落盘,一律调用 @cline/shared/storageresolve* 函数并遵守 CLINE_DATA_DIR 等环境变量约定,保证多客户端数据一致(实现见 sdk/packages/shared/src/storage/paths.ts,回归见 paths.test.ts)。
  2. 日志注入统一实现 BasicLogger。宿主把 pino/VS Code logger 适配成 { debug, log, error? },缺省用 noopBasicLogger;跨传输边界传日志配置时用 RuntimeLoggerConfig,并善用 bindings 打上 clientId 等稳定维度。
  3. RPC 契约不要本地复刻。跨客户端的请求/响应一律引用 ChatStartSessionRequestChatRunTurnRequestChatTurnResultProviderActionRequest 等导出类型;需要裁剪默认加载的扩展时用 parseRuntimeConfigExtensions/hasRuntimeConfigExtension,保持与 core 相同的"默认全开"语义。

需要说明的适用前提:该包在 README 中自标 experimental,当前版本为 package.json 中的 0.0.82,要求 Node 22+;对 resolveHookLogPath 等 README 与源码存在出入的导出,以仓库当前 src/ 与根入口的实际导出为准。包级全景(core/llms/agents 各包职责与交互)可继续阅读 sdk/packages/README.mdsdk/ARCHITECTURE.md

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