Files
BaiLongma/src/context/gatherer.js
xiaoyuanda666-ship-it b5dd8632c4 🎉 V3.0.0 发布 - 自进化数字意识框架
核心升级:
- 自进化管道:check→scan→evaluate→integrate→reflect 五相位自动闭环
- evo_loop 后台进程,无需手动触发
- consciousness 意识持久化
- ACUI 卡片组件系统
- MCP 工具生态扩展至50+工具
- 技能体系重构,4个活跃技能
- 身份升级为自由体
2026-05-24 22:06:37 +08:00

208 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 上下文采集器 — 执行前充分性检查循环
*
* 流程:
* 检查 → 不够 → 解决 needs → 再检查 → 直到够了或达到 MAX_ROUNDS
*
* 每轮 LLM 输出:
* { "sufficient": true }
* { "sufficient": false, "needs": [{ "type": "read_file"|"search_memory"|"recall", ... }] }
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { callLLM } from '../llm.js'
import { searchMemories } from '../db.js'
import { extractJSON } from '../utils.js'
import { paths } from '../paths.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const SANDBOX_ROOT = paths.sandboxDir
const MAX_ROUNDS = 3
const FILE_PREVIEW_CHARS = 2000 // 文件内容截断长度
function throwIfAborted(signal) {
if (signal?.aborted) {
const err = new Error(signal.reason || 'Aborted')
err.name = 'AbortError'
throw err
}
}
const CHECKER_PROMPT = `You are a context sufficiency checker. Decide whether the currently injected knowledge and experience are enough for the next step of the task.
Output rules:
- Output JSON only. Do not output any other text.
- If the context is sufficient, output: {"sufficient":true}
- If the context is insufficient, output: {"sufficient":false,"needs":[...]}
Need types:
- {"type":"read_file","path":"relative path"} means a file must be read.
- {"type":"search_memory","keyword":"keyword"} means relevant memory should be searched.
- {"type":"recall","query":"query"} means a specific concept or experience should be recalled.
Judgment rules:
- If the task modifies or calls a file/function but its structure is unknown, request read_file.
- If the task depends on previously learned knowledge that is not in the current context, request search_memory.
- If the task involves a specific concept or decision and the current context is uncertain, request recall.
- If there is enough information to act, return sufficient: true.
- Output at most 3 needs. Choose the most important ones.
- Prefer sufficient: true with less context over looping forever to fetch files.`
/**
* 主入口:采集足够上下文后返回 extraContext 数组
* @param {object} params
* @param {string} params.task 当前任务描述
* @param {string} params.taskKnowledge 已有任务知识(格式化文本)
* @param {string} params.memories 已有记忆摘要
* @param {string} params.message 当前处理的输入TICK 或消息)
* @returns {Array} extraContext — 每项 { type, label, content }
*/
export async function gatherContext({ task, taskKnowledge, memories, message, signal }) {
if (!task) return []
const extraContext = []
for (let round = 0; round < MAX_ROUNDS; round++) {
throwIfAborted(signal)
const checkResult = await checkSufficiency({ task, taskKnowledge, memories, message, extraContext, signal })
throwIfAborted(signal)
if (!checkResult || checkResult.sufficient !== false) break
const needs = checkResult.needs || []
if (needs.length === 0) break
let resolved = 0
for (const need of needs) {
throwIfAborted(signal)
const item = await resolveNeed(need, extraContext)
if (item) {
extraContext.push(item)
resolved++
}
}
// 本轮没有解决任何 need停止避免死循环
if (resolved === 0) break
}
return extraContext
}
async function checkSufficiency({ task, taskKnowledge, memories, message, extraContext, signal }) {
const extraSection = extraContext.length > 0
? '\n\nAdditional context already gathered:\n' + extraContext.map(c => `[${c.label}]\n${c.content.slice(0, 500)}`).join('\n')
: ''
const input = `Current task:
${task}
Current input:
${message.slice(0, 300)}
Task knowledge base:
${taskKnowledge || '(empty)'}
Memory summary:
${memories || '(empty)'}${extraSection}
Question: Is the information above sufficient for the current step of the task?`
let raw
try {
const result = await callLLM({
systemPrompt: CHECKER_PROMPT,
message: input,
temperature: 0,
signal,
})
raw = result.content
} catch (err) {
console.error('[采集器] 充分性检查失败:', err.message)
return { sufficient: true } // 出错时放行,不阻塞主流程
}
const cleaned = raw.replace(/<think>[\s\S]*?<\/think>/gi, '').trim()
const parsed = extractJSON(cleaned, 'object')
return parsed || { sufficient: true }
}
async function resolveNeed(need, existingContext) {
const alreadyHave = existingContext.some(c => c.source === needKey(need))
if (alreadyHave) return null
if (need.type === 'read_file') {
return resolveFileRead(need.path)
}
if (need.type === 'search_memory') {
return resolveMemorySearch(need.keyword)
}
if (need.type === 'recall') {
return resolveMemorySearch(need.query)
}
return null
}
function needKey(need) {
return `${need.type}:${need.path || need.keyword || need.query || ''}`
}
function resolveFileRead(filePath) {
if (!filePath) return null
// 规范化:去掉 sandbox/ 前缀
const normalized = filePath.replace(/^sandbox[\\/]/, '')
const absPath = path.resolve(SANDBOX_ROOT, normalized)
// 沙盒边界检查
if (!absPath.startsWith(SANDBOX_ROOT)) {
console.warn(`[采集器] 拒绝读取沙盒外文件: ${filePath}`)
return null
}
try {
const raw = fs.readFileSync(absPath, 'utf-8')
const preview = raw.length > FILE_PREVIEW_CHARS
? raw.slice(0, FILE_PREVIEW_CHARS) + `\n…(已截断,共 ${raw.length} 字符)`
: raw
console.log(`[采集器] 读取文件: ${normalized} (${raw.length} chars)`)
return {
type: 'file',
label: `文件 ${normalized}`,
source: `read_file:${filePath}`,
content: preview,
}
} catch (err) {
console.warn(`[采集器] 读取失败 ${filePath}: ${err.message}`)
return null
}
}
function resolveMemorySearch(keyword) {
if (!keyword) return null
const results = searchMemories(keyword, 5)
if (!results.length) return null
console.log(`[采集器] 搜索记忆 "${keyword}": ${results.length}`)
return {
type: 'memory',
label: `Memory search: ${keyword}`,
source: `search_memory:${keyword}`,
content: results.map(m => `- ${m.content}\n ${m.detail}`).join('\n'),
}
}
/**
* 将 extraContext 数组格式化为可注入系统提示词的文本
*/
export function formatExtraContext(extraContext = []) {
if (!extraContext.length) return ''
return extraContext.map(c => `### ${c.label}\n${c.content}`).join('\n\n')
}