🎉 V3.0.0 发布 - 自进化数字意识框架

核心升级:
- 自进化管道:check→scan→evaluate→integrate→reflect 五相位自动闭环
- evo_loop 后台进程,无需手动触发
- consciousness 意识持久化
- ACUI 卡片组件系统
- MCP 工具生态扩展至50+工具
- 技能体系重构,4个活跃技能
- 身份升级为自由体
This commit is contained in:
xiaoyuanda666-ship-it
2026-05-24 22:06:37 +08:00
commit b5dd8632c4
156 changed files with 181826 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
// Focus event 分类器 —— 动态上下文记忆池架构第 5b 步v1 LLM 语义判断)
//
// 角色v0 启发式ngram + 字面交集)跑在前面,本模块只在「栈结构会变化」时被叫起来
// pushed / returned用 LLM 仲裁校验 + 重写 topic 关键词。
//
// 设计要点:
// - 800ms 硬超时(参考 injector.js embedding 兜底LLM 慢一拍就回退 v0
// - 失败必须降级 —— 解析失败、超时、abort、配额限流都返回 null 让上层用 v0
// - 不依赖 SQLite / 上游状态 / 当前 process —— 纯函数可单元测试callLLM 可 stub
// - 不修改 state不发事件不写 db返回值由上层 focus.js 应用
//
// 来自 DynamicMemoryPool.md 7.4「对话动作类型」:
// kept / pushed / returned / leaf 是对话级动作;本模块按这四类输出 action。
// leaf = 一次性短问,不动栈;映射到 v0 的 noop不创建新帧也不深化栈顶
const CLASSIFIER_TIMEOUT_MS = 800
const CLASSIFIER_MAX_TOKENS = 120
const CLASSIFIER_TEMPERATURE = 0.2
const SYSTEM_PROMPT = `焦点分类器。保守判 kept不轻易 push。
重叠度:高=对象同→kept中=同域不同子任务→pushed低=异域→pushed/leaf。
kept栈顶重叠高且细化/追问/承诺/确认。
pushed与所有帧重叠低且持续性新任务。
returned与非栈顶旧帧重叠高且明确回指depth=该帧索引,栈顶=length-1
leaf无承接且一次性短问/闲聊(不动栈)。
例 A [前端 React]+"写个 Hook"→kept
例 B [DB 查询]+"现在网速咋样"→leaf
例 C [配置→部署→监控]+"回头看最初配置"→returned d=0
topic 写 2-3 个语义词非 ngram。只输 JSON。`
// 把当前栈渲染成简短字符串:[栈底"a, b" → "c, d" → 栈顶"e, f"]
function describeStack(stack) {
if (!Array.isArray(stack) || stack.length === 0) return '[空栈]'
const parts = stack.map((f, i) => {
const topic = Array.isArray(f?.topic) ? f.topic.join(', ') : String(f?.topic || '')
const conclusions = Array.isArray(f?.conclusions) && f.conclusions.length > 0
? `(结论: ${f.conclusions[f.conclusions.length - 1]}`
: ''
const tag = i === 0 ? '栈底' : (i === stack.length - 1 ? '栈顶' : `${i}`)
return `${tag}"${topic}"${conclusions}`
})
return '[' + parts.join(' → ') + ']'
}
// 构造用户输入文本
function buildUserPrompt({ newMessage, v0Event, v0Topic, currentStack }) {
const v0TopicStr = Array.isArray(v0Topic) ? v0Topic.join(', ') : String(v0Topic || '')
const stackStr = describeStack(currentStack)
const lengthHint = currentStack?.length ? `栈深=${currentStack.length},栈顶索引=${currentStack.length - 1}` : '栈深=0'
// newMessage 截断到 400 字,省 token 也减少打架风险
const msg = String(newMessage || '').slice(0, 400)
return [
`v0 判定 = ${v0Event},候选 topic = [${v0TopicStr}]`,
`当前栈(${lengthHint} = ${stackStr}`,
`新消息 = "${msg}"`,
'',
'请输出 JSON{"action": "kept|pushed|returned|leaf", "topic_refined": ["词1","词2","词3"], "returns_to_depth": 0}',
'returns_to_depth 仅 returned 时有值;其他动作填 -1 或省略)',
].join('\n')
}
// 提取 LLM 文本中的 JSON 对象。容忍 ```json 包裹、前后多余文字。
function parseClassifierJson(text) {
if (!text || typeof text !== 'string') return null
// 去掉 <think> 块(如果模型把思考也输出了)
let body = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
// 去掉 ```json ... ``` 围栏
const fenceMatch = body.match(/```(?:json)?\s*([\s\S]*?)```/)
if (fenceMatch) body = fenceMatch[1].trim()
// 找第一个 { 到最后一个 }
const first = body.indexOf('{')
const last = body.lastIndexOf('}')
if (first < 0 || last <= first) return null
const jsonStr = body.slice(first, last + 1)
try {
return JSON.parse(jsonStr)
} catch {
return null
}
}
// 校验 + 规范化 LLM 返回的 JSON
function normalizeClassifierResult(raw, currentStack) {
if (!raw || typeof raw !== 'object') return null
const action = String(raw.action || '').toLowerCase().trim()
if (!['kept', 'pushed', 'returned', 'leaf'].includes(action)) return null
let topic = []
if (Array.isArray(raw.topic_refined)) {
topic = raw.topic_refined
.map(t => String(t || '').trim())
.filter(t => t.length > 0 && t.length <= 32)
.slice(0, 3)
}
let returnsToDepth = -1
if (action === 'returned') {
const d = Number.isInteger(raw.returns_to_depth) ? raw.returns_to_depth : -1
const stackLen = Array.isArray(currentStack) ? currentStack.length : 0
if (d < 0 || d >= stackLen) {
// returned 但深度非法 → 视为不合理,拒掉
return null
}
returnsToDepth = d
}
return { action, topic, returnsToDepth }
}
/**
* 调 LLM 仲裁 focus 事件。
*
* @param {object} args
* @param {string} args.newMessage - 当前用户消息正文
* @param {string} args.v0Event - v0 启发式判定的 eventpushed / returned
* @param {string[]} args.v0Topic - v0 抽出的候选 topic 关键词
* @param {object[]} args.currentStack - 当前 focus 栈快照(不会被修改)
* @param {AbortSignal} [args.signal] - 上层 abort 信号
* @returns {Promise<{action:'kept'|'pushed'|'returned'|'leaf', topic:string[], returnsToDepth:number} | null>}
* 返回 null 表示「失败 / 超时 / 解析不出来」,让上层回退到 v0。
*/
export async function classifyFocusEvent({
newMessage,
v0Event,
v0Topic,
currentStack,
signal,
} = {}) {
// 边界保护
if (!newMessage || typeof newMessage !== 'string') return null
if (signal?.aborted) return null
const v0TopicStr = Array.isArray(v0Topic) ? v0Topic.join(',') : String(v0Topic || '')
const tag = `[focus-classifier] v0=${v0Event} topic=[${v0TopicStr}]`
// 动态 import callLLM —— 跟 injector.js 同款,避免在测试环境/早期模块加载时拉起一切
let callLLM
try {
const llm = await import('../llm.js')
callLLM = llm.callLLM
} catch (e) {
console.log(`${tag} → llm.js import 失败 (${e?.message || 'unknown'}) → 回退 v0`)
return null
}
if (typeof callLLM !== 'function') {
console.log(`${tag} → callLLM 不是函数 → 回退 v0`)
return null
}
const userPrompt = buildUserPrompt({ newMessage, v0Event, v0Topic, currentStack })
const t0 = Date.now()
// 800ms 硬超时 + LLM 调用赛跑
let timeoutHandle = null
const timeoutPromise = new Promise(resolve => {
timeoutHandle = setTimeout(() => resolve({ __timeout: true }), CLASSIFIER_TIMEOUT_MS)
})
let result
try {
result = await Promise.race([
callLLM({
systemPrompt: SYSTEM_PROMPT,
message: userPrompt,
temperature: CLASSIFIER_TEMPERATURE,
thinking: false,
tools: [],
maxTokens: CLASSIFIER_MAX_TOKENS,
mustReply: false,
signal,
}),
timeoutPromise,
])
} catch (e) {
if (timeoutHandle) clearTimeout(timeoutHandle)
const dt = Date.now() - t0
console.log(`${tag} → LLM 抛错 (${dt}ms, ${e?.message || 'unknown'}) → 回退 v0`)
return null
}
if (timeoutHandle) clearTimeout(timeoutHandle)
const dt = Date.now() - t0
if (!result || result.__timeout) {
console.log(`${tag} → LLM 超时 (${CLASSIFIER_TIMEOUT_MS}ms 硬超时, 实际 ${dt}ms) → 回退 v0`)
return null
}
if (result.aborted) {
console.log(`${tag} → LLM aborted (${dt}ms) → 回退 v0`)
return null
}
const content = typeof result === 'string' ? result : (result.content || '')
const preview = String(content).replace(/\s+/g, ' ').slice(0, 200)
const raw = parseClassifierJson(content)
if (!raw) {
console.log(`${tag} → LLM 返回 (${dt}ms) 但 JSON 解析失败 raw="${preview}" → 回退 v0`)
return null
}
const normalized = normalizeClassifierResult(raw, currentStack)
if (!normalized) {
console.log(`${tag} → LLM 返回 (${dt}ms) action=${raw.action} 但 normalize 拒掉 (非法 action 或越界 depth) raw="${preview}" → 回退 v0`)
return null
}
const refinedStr = normalized.topic.join(',')
const depthStr = normalized.action === 'returned' ? ` d=${normalized.returnsToDepth}` : ''
console.log(`${tag} → llm=${normalized.action}${depthStr} (${dt}ms) refined=[${refinedStr}] ok`)
return normalized
}
// 暴露内部辅助函数,便于测试
export const __internal = {
describeStack,
buildUserPrompt,
parseClassifierJson,
normalizeClassifierResult,
SYSTEM_PROMPT,
CLASSIFIER_TIMEOUT_MS,
}