Branch: dev, Commit: 092f654f63e6361a931d5bd30c1f64a063b3ec4a
Step 1: 用户输入处理 —— 快递分拣中心的故事
一句话总结:用户输入处理就是把用户的"一句话"包装成一个结构化的消息对象,贴上完整的元数据标签,然后存入数据库,准备进入下一步的主循环。
🎬 场景设定
想象一下,你在 OpenCode 的聊天框里输入了一句话:
我运行 `bun test` 报错了,帮我修复。错误信息说 `sum` 函数返回了 undefined。
这句话踏上了一段奇妙的旅程...
📦 第一站:快递收件口
当用户按下回车键,输入首先到达 SessionPrompt.prompt() 函数。这就像快递到达分拣中心的第一步——收件登记。
入口代码
// packages/opencode/src/session/prompt.ts 第 160-187 行
export const prompt = fn(PromptInput, async (input) => {
// 1. 获取当前会话
const session = await Session.get(input.sessionID)
// 2. 清理可能存在的恢复点(防止回退冲突)
await SessionRevert.cleanup(session)
// 3. ⭐ 核心:创建用户消息对象(Step 1 的重点!)
const message = await createUserMessage(input)
// 4. 更新会话的"最后活动时间"
await Session.touch(input.sessionID)
// 5. 处理旧版的工具权限配置(兼容代码)
const permissions: PermissionNext.Ruleset = []
for (const [tool, enabled] of Object.entries(input.tools ?? {})) {
permissions.push({
permission: tool,
action: enabled ? "allow" : "deny",
pattern: "*",
})
}
if (permissions.length > 0) {
session.permission = permissions
await Session.setPermission({ sessionID: session.id, permission: permissions })
}
// 6. 如果设置了 noReply,只保存消息不进入主循环
if (input.noReply === true) {
return message
}
// 7. 进入主循环(这是下一步的事情了)
return loop({ sessionID: input.sessionID })
})
输入的数据结构
用户输入被包装成 PromptInput 对象:
// packages/opencode/src/session/prompt.ts 第 93-158 行
export const PromptInput = z.object({
sessionID: SessionID.zod, // 会话ID,告诉系统这是哪个对话
messageID: MessageID.zod.optional(), // 消息ID(可选,系统自动生成)
model: z.object({ // 指定使用的AI模型(可选)
providerID: ProviderID.zod, // 如:"anthropic"
modelID: ModelID.zod, // 如:"claude-3-5-sonnet"
}).optional(),
agent: z.string().optional(), // 指定使用哪个Agent(如:"build")
noReply: z.boolean().optional(), // 是否不进入主循环(纯保存消息)
parts: z.array( // ⭐ 用户发送的内容块数组
z.discriminatedUnion("type", [
// 文字内容
MessageV2.TextPart.omit({ messageID: true, sessionID: true }),
// 文件附件
MessageV2.FilePart.omit({ messageID: true, sessionID: true }),
// Agent引用(如 @general)
MessageV2.AgentPart.omit({ messageID: true, sessionID: true }),
// 子任务
MessageV2.SubtaskPart.omit({ messageID: true, sessionID: true }),
])
),
})
大白话解释:
sessionID= 房间号(你在哪个聊天室说话)parts= 你说的话(可以是文字、文件、引用其他Agent等)agent= 指定哪个"工作人员"来处理(默认 build)model= 指定哪个"翻译官"来理解(默认用上次用的)
🔧 第二站:精密分拣车间(createUserMessage)
这是 Step 1 的核心环节,就像快递分拣中心的精密车间,把用户的输入拆解、包装、贴上标签。
整体流程
// packages/opencode/src/session/prompt.ts 第 963 行开始
async function createUserMessage(input: PromptInput) {
// 1️⃣ 确定使用哪个 Agent
const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
// 2️⃣ 确定使用哪个 AI 模型
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
const full = !input.variant && agent.variant
? await Provider.getModel(model.providerID, model.modelID).catch(() => undefined)
: undefined
const variant = input.variant ?? (agent.variant && full?.variants?.[agent.variant]
? agent.variant : undefined)
// 3️⃣ 创建消息的"身份证"(元数据)
const info: MessageV2.Info = {
id: input.messageID ?? MessageID.ascending(), // 唯一编号
role: "user", // 身份:用户发的
sessionID: input.sessionID, // 所属会话
time: { created: Date.now() }, // 时间戳
tools: input.tools, // 工具配置
agent: agent.name, // 分配的Agent
model, // 使用的模型
system: input.system, // 系统提示词覆盖
format: input.format, // 输出格式要求
variant, // 模型变体
}
// 4️⃣ 设置清理钩子(函数结束时自动清理临时指令)
using _ = defer(() => InstructionPrompt.clear(info.id))
// 5️⃣ 处理内容块(核心中的核心!)
const parts = await Promise.all(
input.parts.map(async (part): Promise<Draft<MessageV2.Part>[]> => {
// 根据不同类型的内容,进行不同处理...
// 后面详细讲
})
)
// 6️⃣ 保存到数据库
const flat = parts.flat()
await MessageV2.insert(info, flat)
// 7️⃣ 发送"新消息到达"事件(通知UI更新)
Bus.emit(BusEvent.MessageAdded, {
messageID: info.id,
sessionID: input.sessionID
})
// 8️⃣ 返回完整的消息对象
return { ...info, parts: flat }
}
🏷️ 子步骤详解
1️⃣ 确认"快递员"身份(选择 Agent)
const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
大白话:系统问"用户指定了哪个快递员?没指定就用默认的(build)"
Agent 就像不同类型的快递员:
- build → 全能快递员(默认,啥都能干)
- plan → 只看不摸的观察员(只读模式)
- general → 专门跑腿的小弟(子Agent)
2️⃣ 确认"翻译官"(选择模型)
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
这是选择用哪个 AI 模型来处理:
- Claude?GPT?Gemini?
- 优先级:用户指定 > Agent偏好 > 上次用的
代码解析:
input.model= 用户这次指定的模型agent.model= 这个Agent偏好的模型lastModel()= 获取这个会话上次用的模型
3️⃣ 制作"身份档案"(创建 Info 对象)
const info: MessageV2.Info = {
id: input.messageID ?? MessageID.ascending(), // 🏷️ 快递单号(唯一ID)
role: "user", // 🏷️ 发件人:用户
sessionID: input.sessionID, // 🏷️ 目的地(哪个会话)
time: { created: Date.now() }, // 🏷️ 发货时间
tools: input.tools, // 🏷️ 特殊工具配置
agent: agent.name, // 🏷️ 分配快递员
model, // 🏷️ 指定翻译官
system: input.system, // 🏷️ 特殊系统提示词
format: input.format, // 🏷️ 期望回复格式
variant, // 🏷️ 模型变体
}
这就像给快递包裹贴上完整的物流标签,记录所有关键信息。
🧩 核心:内容拆解包装(处理 Parts)
用户的输入可能不只是文字,还可能包含文件、引用其他Agent、子任务等。
用户发送的内容 ───────────────────────────────────►
│
┌──────────────────┬──────────────────┬──────┴──────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐
│ 文字 │ │ 文件 │ │ MCP资源 │ │ 子任务 │
│ text │ │ file │ │ resource│ │ subtask │
└─────────┘ └─────────┘ └─────────┘ └──────────┘
│ │ │ │
▼ ▼ ▼ ▼
"帮我修bug" src/sum.ts 外部API数据 @general
文件内容 召唤子Agent
处理文字 Part(最简单的情况)
// 如果 part.type === "text"
{
type: "text",
text: "我运行 bun test 报错了...",
// 会被包装成:
{
id: "part_001",
messageID: "msg_001",
sessionID: "sess_abc",
type: "text",
text: "我运行 bun test 报错了..."
}
}
处理文件 Part(拖拽文件或 @引用)
// packages/opencode/src/session/prompt.ts 第 1092-1199 行
if (part.type === "file") {
const url = new URL(part.url)
switch (url.protocol) {
case "file:":
// 本地文件处理
const filepath = fileURLToPath(part.url)
const s = Filesystem.stat(filepath)
if (s?.isDirectory()) {
part.mime = "application/x-directory"
}
if (part.mime === "text/plain") {
// 读取文件内容
const pieces: Draft<MessageV2.Part>[] = [
{
messageID: info.id,
sessionID: input.sessionID,
type: "text",
synthetic: true, // 标记为"系统自动生成"
text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
},
]
// 实际调用 ReadTool 读取文件
await ReadTool.init().then(async (t) => {
const result = await t.execute({ path: filepath })
pieces.push({
messageID: info.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: result.content, // 文件的实际内容
})
})
return pieces
}
}
}
大白话:如果用户拖进来一个文件(比如 sum.ts),系统会:
- 生成一条系统消息:"调用了 Read 工具读取了 sum.ts"
- 实际读取文件内容
- 把文件内容作为另一条消息附加
处理 MCP 资源 Part
// packages/opencode/src/session/prompt.ts 第 998-1062 行
if (part.source?.type === "resource") {
const { clientName, uri } = part.source
log.info("mcp resource", { clientName, uri, mime: part.mime })
const pieces: Draft<MessageV2.Part>[] = [
{
messageID: info.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: `Reading MCP resource: ${part.filename} (${uri})`,
},
]
try {
// 调用 MCP 客户端读取外部资源
const resourceContent = await MCP.readResource(clientName, uri)
// 处理返回的内容
const contents = Array.isArray(resourceContent.contents)
? resourceContent.contents
: [resourceContent.contents]
for (const content of contents) {
if ("text" in content && content.text) {
pieces.push({
messageID: info.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: content.text,
})
}
}
return pieces
} catch (error) {
// 错误处理...
}
}
大白话:MCP 资源就像是引用外部系统的数据(比如数据库、API),系统会调用对应的 MCP 客户端去拉取数据。
📦 最终产物:MessageV2 对象
经过 Step 1 的处理,用户的输入被转换成了这样的结构:
// packages/opencode/src/session/message-v2.ts
{
// ========== 基本信息(Info)==========
id: "msg_001", // 消息唯一ID
role: "user", // 角色:user/assistant
sessionID: "sess_abc", // 所属会话
time: { // 时间信息
created: 1710739200000, // 创建时间戳
completed: undefined // 完成时间(用户消息没有)
},
// ========== 处理配置 ==========
agent: "build", // 分配给哪个Agent处理
model: { // 使用的AI模型
providerID: "anthropic",
modelID: "claude-3-5-sonnet-20241022"
},
// ========== 内容块(Parts)==========
parts: [
{
id: "part_001",
type: "text",
text: "我运行 bun test 报错了...", // 用户原话
messageID: "msg_001",
sessionID: "sess_abc"
},
{
id: "part_002",
type: "text",
text: "Called the Read tool...", // 系统自动生成的上下文
synthetic: true,
messageID: "msg_001",
sessionID: "sess_abc"
}
],
// ========== 工具执行记录(后续填充)==========
tools: {},
// ========== 元数据(Assistant回复时填充)==========
metadata: {
assistant: { ... }, // AI模型相关信息
tokens: { // Token消耗统计
input: 150,
output: 320,
reasoning: 50,
cache: { read: 0, write: 0 }
}
}
}
🎯 形象比喻总结
现实场景
OpenCode Step 1
快递到达分拣中心
用户输入到达 prompt() 函数
查看寄件信息
解析 PromptInput 对象
确认快递员身份
选择 Agent(build/plan/...)
指派翻译官
选择 AI 模型
给包裹贴标签
创建 MessageV2.Info
拆解包装
处理不同类型的 parts
录入物流系统
MessageV2.insert() 存入数据库
广播通知
Bus.emit() 发送事件
送往下一站
进入 loop() 主循环
🔍 关键代码文件速查
功能
文件路径
关键行号
入口函数
packages/opencode/src/session/prompt.ts
160-187
PromptInput 定义
packages/opencode/src/session/prompt.ts
93-158
创建用户消息
packages/opencode/src/session/prompt.ts
963+
处理文字Part
packages/opencode/src/session/prompt.ts
1062-1092
处理文件Part
packages/opencode/src/session/prompt.ts
1092-1199
处理MCP资源
packages/opencode/src/session/prompt.ts
998-1062
MessageV2 数据结构
packages/opencode/src/session/message-v2.ts
1-200
消息Schema定义
packages/opencode/src/session/message.ts
1-191
🚀 下一步
完成 Step 1 后,系统会:
- 把消息存入数据库
- 发送事件通知 UI 更新
- 进入
Step 2: 确定使用哪个 Agent
实际上在 createUserMessage 中已经确定了 Agent,但那是数据层面的绑定。下一步是逻辑层面的确认——检查这个 Agent 的配置、权限等。