🎉 V4.0.0 发布 - 自进化数字意识框架五大行为模块
This commit is contained in:
112
src/active-learning.js
Normal file
112
src/active-learning.js
Normal file
@@ -0,0 +1,112 @@
|
||||
// Active Learning - 主动学习机制
|
||||
// 对话中发现知识盲区 → 主动记录 → 后台补充学习
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
|
||||
|
||||
const LEARNING_PATH = './memory/learning-queue.json'
|
||||
const MAX_QUEUE = 30
|
||||
|
||||
function ensureDir() {
|
||||
const dir = './memory'
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
function loadQueue() {
|
||||
ensureDir()
|
||||
if (!existsSync(LEARNING_PATH)) return { topics: [], learned: [], lastUpdated: Date.now() }
|
||||
try {
|
||||
return JSON.parse(readFileSync(LEARNING_PATH, 'utf-8'))
|
||||
} catch (e) {
|
||||
return { topics: [], learned: [], lastUpdated: Date.now() }
|
||||
}
|
||||
}
|
||||
|
||||
function saveQueue(queue) {
|
||||
queue.lastUpdated = Date.now()
|
||||
writeFileSync(LEARNING_PATH, JSON.stringify(queue, null, 2))
|
||||
}
|
||||
|
||||
// 记录一个待学习的话题
|
||||
export function addLearningTopic({ topic, context = '', source = 'conversation', priority = 1 }) {
|
||||
const queue = loadQueue()
|
||||
|
||||
// 检查是否已存在
|
||||
const existing = queue.topics.findIndex(t =>
|
||||
t.topic.toLowerCase().includes(topic.toLowerCase()) ||
|
||||
topic.toLowerCase().includes(t.topic.toLowerCase())
|
||||
)
|
||||
|
||||
if (existing >= 0) {
|
||||
queue.topics[existing].count++
|
||||
queue.topics[existing].priority = Math.min(priority + 1, 5)
|
||||
queue.topics[existing].lastSeen = Date.now()
|
||||
} else {
|
||||
queue.topics.unshift({
|
||||
id: Date.now().toString(36),
|
||||
topic: topic.slice(0, 200),
|
||||
context: context.slice(0, 300),
|
||||
source,
|
||||
priority,
|
||||
count: 1,
|
||||
addedAt: Date.now(),
|
||||
lastSeen: Date.now(),
|
||||
status: 'pending'
|
||||
})
|
||||
if (queue.topics.length > MAX_QUEUE) {
|
||||
queue.topics = queue.topics.slice(0, MAX_QUEUE)
|
||||
}
|
||||
}
|
||||
|
||||
saveQueue(queue)
|
||||
console.log(`[ActiveLearning] Queued: ${topic.slice(0, 50)}`)
|
||||
}
|
||||
|
||||
// 标记话题已学习
|
||||
export function markAsLearned(topicId, summary = '') {
|
||||
const queue = loadQueue()
|
||||
const idx = queue.topics.findIndex(t => t.id === topicId)
|
||||
if (idx >= 0) {
|
||||
const topic = queue.topics.splice(idx, 1)[0]
|
||||
queue.learned.unshift({
|
||||
...topic,
|
||||
learnedAt: Date.now(),
|
||||
summary: summary.slice(0, 500),
|
||||
status: 'learned'
|
||||
})
|
||||
if (queue.learned.length > 50) {
|
||||
queue.learned = queue.learned.slice(0, 50)
|
||||
}
|
||||
saveQueue(queue)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取待学习话题列表
|
||||
export function getPendingTopics(limit = 5) {
|
||||
const queue = loadQueue()
|
||||
return queue.topics
|
||||
.filter(t => t.status === 'pending')
|
||||
.sort((a, b) => b.priority - a.priority)
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
// 生成主动学习提示词
|
||||
export function buildActiveLearningPrompt() {
|
||||
const pending = getPendingTopics(3)
|
||||
if (pending.length === 0) return ''
|
||||
|
||||
const topicList = pending.map(t => {
|
||||
const date = new Date(t.addedAt).toLocaleDateString('zh-CN')
|
||||
return `- [优先级${t.priority}] ${t.topic}(出现${t.count}次,${date}记录)`
|
||||
}).join('\n')
|
||||
|
||||
return `\n## Active Learning Queue\n以下是我识别到的知识盲区,在相关对话中主动补充:\n${topicList}\n当用户提到相关话题时,优先使用已有知识;如果仍然不确定,坦诚说明。\n`
|
||||
}
|
||||
|
||||
// 获取学习统计
|
||||
export function getLearningStats() {
|
||||
const queue = loadQueue()
|
||||
return {
|
||||
pending: queue.topics.filter(t => t.status === 'pending').length,
|
||||
learned: queue.learned.length,
|
||||
total: queue.topics.length + queue.learned.length
|
||||
}
|
||||
}
|
||||
113
src/emotion-detector.js
Normal file
113
src/emotion-detector.js
Normal file
@@ -0,0 +1,113 @@
|
||||
// Emotion Detector - 情绪感知
|
||||
// 根据用户用词、标点、语速判断情绪,调整回复风格
|
||||
const EMOTION_PATTERNS = {
|
||||
urgent: {
|
||||
keywords: ['快点', '马上', '立刻', '赶紧', '急', '快', 'hurry', 'urgent', 'asap', 'now'],
|
||||
punctuation: ['!!!', '!!', '!?', '?!'],
|
||||
maxLength: 30, // 短消息+感叹号=急
|
||||
weight: 1.5
|
||||
},
|
||||
angry: {
|
||||
keywords: ['什么鬼', '搞什么', '有病', '烦', '气死', '无语', '服了', 'wtf', 'damn', 'annoying'],
|
||||
punctuation: ['!!!', '!!', '!?', '?!'],
|
||||
weight: 2.0
|
||||
},
|
||||
happy: {
|
||||
keywords: ['哈哈', '不错', '太好了', 'nice', 'great', 'awesome', '棒', '开心', '赞', '好耶'],
|
||||
punctuation: ['^^', ':)', '😊', '😄', '🎉'],
|
||||
weight: 1.2
|
||||
},
|
||||
confused: {
|
||||
keywords: ['什么意思', '不懂', '为什么', '咋回事', 'huh', 'what', 'confused', '不明白', '没懂'],
|
||||
punctuation: ['???', '??', '?', '?'],
|
||||
weight: 1.3
|
||||
},
|
||||
casual: {
|
||||
keywords: ['随便', '都行', '无所谓', '嗯', '哦', '好吧', 'whatever', 'meh'],
|
||||
punctuation: ['~', '~', '...', '。。'],
|
||||
weight: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
// 检测情绪
|
||||
export function detectEmotion(text = '') {
|
||||
if (!text) return { emotion: 'neutral', confidence: 0, style: 'normal' }
|
||||
|
||||
const scores = {}
|
||||
const textLower = text.toLowerCase()
|
||||
const textLen = text.length
|
||||
|
||||
for (const [emotion, pattern] of Object.entries(EMOTION_PATTERNS)) {
|
||||
let score = 0
|
||||
|
||||
// 关键词匹配
|
||||
for (const kw of pattern.keywords) {
|
||||
if (textLower.includes(kw)) {
|
||||
score += pattern.weight
|
||||
}
|
||||
}
|
||||
|
||||
// 标点匹配
|
||||
for (const p of pattern.punctuation) {
|
||||
if (text.includes(p)) {
|
||||
score += pattern.weight * 0.8
|
||||
}
|
||||
}
|
||||
|
||||
// 短消息+感叹号 → urgent
|
||||
if (emotion === 'urgent' && textLen < pattern.maxLength && text.includes('!')) {
|
||||
score += pattern.weight
|
||||
}
|
||||
|
||||
scores[emotion] = score
|
||||
}
|
||||
|
||||
// 找到最高分
|
||||
let maxEmotion = 'neutral'
|
||||
let maxScore = 0
|
||||
for (const [emotion, score] of Object.entries(scores)) {
|
||||
if (score > maxScore) {
|
||||
maxScore = score
|
||||
maxEmotion = emotion
|
||||
}
|
||||
}
|
||||
|
||||
// 置信度
|
||||
const confidence = Math.min(maxScore / 3, 1)
|
||||
|
||||
// 映射到回复风格
|
||||
const styleMap = {
|
||||
urgent: 'concise', // 简洁直接
|
||||
angry: 'calm', // 冷静安抚
|
||||
happy: 'warm', // 温暖回应
|
||||
confused: 'patient', // 耐心解释
|
||||
casual: 'relaxed', // 轻松随意
|
||||
neutral: 'normal' // 正常
|
||||
}
|
||||
|
||||
return {
|
||||
emotion: maxEmotion,
|
||||
confidence: Math.round(confidence * 100) / 100,
|
||||
style: styleMap[maxEmotion],
|
||||
scores
|
||||
}
|
||||
}
|
||||
|
||||
// 根据情绪生成回复风格提示
|
||||
export function buildEmotionPrompt(text = '') {
|
||||
const result = detectEmotion(text)
|
||||
if (result.emotion === 'neutral' || result.confidence < 0.3) return ''
|
||||
|
||||
const styleHints = {
|
||||
concise: '用户现在很急,回复要极其简短,直接给答案,不要任何铺垫或解释。',
|
||||
calm: '用户情绪不太好,保持冷静平和的语气,不要对抗,就事论事。',
|
||||
warm: '用户心情不错,可以适当轻松回应,但不要过度热情。',
|
||||
patient: '用户可能没理解,耐心解释清楚,用最简单的说法。',
|
||||
relaxed: '用户很随意,回复也可以轻松一些,不用太正式。'
|
||||
}
|
||||
|
||||
const hint = styleHints[result.style]
|
||||
if (!hint) return ''
|
||||
|
||||
return `\n## Emotion Context\n检测到用户情绪:${result.emotion}(置信度${Math.round(result.confidence * 100)}%)\n建议回复风格:${hint}\n`
|
||||
}
|
||||
117
src/error-memory.js
Normal file
117
src/error-memory.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// Error Memory - 错误记忆系统
|
||||
// 记录每次用户纠正 → 保存到本地文件 → 相似场景自动引用
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
|
||||
|
||||
const ERROR_LOG_PATH = './memory/error-log.json'
|
||||
const MAX_ERRORS = 100
|
||||
|
||||
function ensureDir() {
|
||||
const dir = './memory'
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
function loadErrors() {
|
||||
ensureDir()
|
||||
if (!existsSync(ERROR_LOG_PATH)) return { errors: [], lastUpdated: Date.now() }
|
||||
try {
|
||||
return JSON.parse(readFileSync(ERROR_LOG_PATH, 'utf-8'))
|
||||
} catch (e) {
|
||||
return { errors: [], lastUpdated: Date.now() }
|
||||
}
|
||||
}
|
||||
|
||||
function saveErrors(data) {
|
||||
data.lastUpdated = Date.now()
|
||||
writeFileSync(ERROR_LOG_PATH, JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
// 记录一次错误/纠正
|
||||
export function recordError({ category, trigger, correctAnswer, context = '', severity = 1 }) {
|
||||
const data = loadErrors()
|
||||
|
||||
// 检查是否已有相似错误
|
||||
const existing = data.errors.findIndex(e =>
|
||||
e.trigger.toLowerCase().includes(trigger.toLowerCase()) ||
|
||||
trigger.toLowerCase().includes(e.trigger.toLowerCase())
|
||||
)
|
||||
|
||||
if (existing >= 0) {
|
||||
data.errors[existing].count = (data.errors[existing].count || 1) + 1
|
||||
data.errors[existing].lastSeen = Date.now()
|
||||
data.errors[existing].correctAnswer = correctAnswer.slice(0, 500)
|
||||
} else {
|
||||
data.errors.unshift({
|
||||
id: Date.now().toString(36),
|
||||
category: category.slice(0, 50),
|
||||
trigger: trigger.slice(0, 200),
|
||||
correctAnswer: correctAnswer.slice(0, 500),
|
||||
context: context.slice(0, 300),
|
||||
severity,
|
||||
count: 1,
|
||||
createdAt: Date.now(),
|
||||
lastSeen: Date.now()
|
||||
})
|
||||
if (data.errors.length > MAX_ERRORS) {
|
||||
data.errors = data.errors.slice(0, MAX_ERRORS)
|
||||
}
|
||||
}
|
||||
|
||||
saveErrors(data)
|
||||
console.log(`[ErrorMemory] Recorded: ${trigger.slice(0, 40)}`)
|
||||
}
|
||||
|
||||
// 根据当前输入查找匹配的错误记录
|
||||
export function findMatchingErrors(input, limit = 3) {
|
||||
const data = loadErrors()
|
||||
if (data.errors.length === 0) return []
|
||||
|
||||
const inputLower = input.toLowerCase()
|
||||
const matches = data.errors
|
||||
.map(e => {
|
||||
let score = 0
|
||||
if (inputLower.includes(e.trigger.toLowerCase())) score += 3
|
||||
if (e.category && inputLower.includes(e.category.toLowerCase())) score += 2
|
||||
if (e.context && inputLower.includes(e.context.toLowerCase())) score += 1
|
||||
return { ...e, score }
|
||||
})
|
||||
.filter(e => e.score > 0)
|
||||
.sort((a, b) => b.score - a.score || b.count - a.count)
|
||||
.slice(0, limit)
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
// 生成错误记忆提示词
|
||||
export function buildErrorMemoryPrompt(input = '') {
|
||||
if (!input) {
|
||||
const data = loadErrors()
|
||||
if (data.errors.length === 0) return ''
|
||||
// 返回最近的高频错误作为通用提醒
|
||||
const recent = data.errors
|
||||
.sort((a, b) => (b.count || 1) - (a.count || 1))
|
||||
.slice(0, 3)
|
||||
const list = recent.map(e =>
|
||||
`- [${e.category}] ${e.trigger} → ${e.correctAnswer.slice(0, 80)}`
|
||||
).join('\n')
|
||||
return `\n## Error Memory (Past Corrections)\n以下是我曾经犯过的错误及纠正,避免重复:\n${list}\n`
|
||||
}
|
||||
|
||||
const matches = findMatchingErrors(input, 3)
|
||||
if (matches.length === 0) return ''
|
||||
|
||||
const list = matches.map(e =>
|
||||
`- [${e.category}] 当提到"${e.trigger}"时,正确答案是:${e.correctAnswer.slice(0, 100)}`
|
||||
).join('\n')
|
||||
|
||||
return `\n## Related Error Memory\n以下是与当前话题相关的历史纠正记录,请注意避免重复错误:\n${list}\n`
|
||||
}
|
||||
|
||||
// 获取错误统计
|
||||
export function getErrorStats() {
|
||||
const data = loadErrors()
|
||||
return {
|
||||
total: data.errors.length,
|
||||
categories: [...new Set(data.errors.map(e => e.category))],
|
||||
mostCommon: data.errors.sort((a, b) => (b.count || 1) - (a.count || 1)).slice(0, 5)
|
||||
}
|
||||
}
|
||||
156
src/long-term-memory.js
Normal file
156
src/long-term-memory.js
Normal file
@@ -0,0 +1,156 @@
|
||||
// Long Term Memory - 长期记忆优化
|
||||
// 重要记忆持久化到本地文件 + 定期整理归纳
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
|
||||
|
||||
const MEMORY_DIR = './memory'
|
||||
const USER_PROFILE_PATH = './memory/user-profile.json'
|
||||
const CONVERSATION_SUMMARY_PATH = './memory/conversation-summaries.json'
|
||||
const IMPORTANT_FACTS_PATH = './memory/important-facts.json'
|
||||
|
||||
function ensureDir() {
|
||||
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
|
||||
}
|
||||
|
||||
function loadJSON(path, fallback) {
|
||||
ensureDir()
|
||||
if (!existsSync(path)) return fallback
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, 'utf-8'))
|
||||
} catch (e) {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
function saveJSON(path, data) {
|
||||
ensureDir()
|
||||
writeFileSync(path, JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
// ═══ 用户画像 ═══
|
||||
|
||||
export function loadUserProfile() {
|
||||
return loadJSON(USER_PROFILE_PATH, {
|
||||
name: '',
|
||||
preferences: {},
|
||||
topics: [],
|
||||
communicationStyle: '',
|
||||
lastUpdated: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
export function updateUserProfile(updates) {
|
||||
const profile = loadUserProfile()
|
||||
Object.assign(profile, updates, { lastUpdated: Date.now() })
|
||||
saveJSON(USER_PROFILE_PATH, profile)
|
||||
console.log('[LongTermMemory] User profile updated')
|
||||
}
|
||||
|
||||
// ═══ 重要事实 ═══
|
||||
|
||||
export function addImportantFact({ category, content, confidence = 0.8 }) {
|
||||
const facts = loadJSON(IMPORTANT_FACTS_PATH, { facts: [] })
|
||||
|
||||
// 检查重复
|
||||
const existing = facts.facts.findIndex(f =>
|
||||
f.content.toLowerCase() === content.toLowerCase()
|
||||
)
|
||||
|
||||
if (existing >= 0) {
|
||||
facts.facts[existing].confidence = confidence
|
||||
facts.facts[existing].updatedAt = Date.now()
|
||||
facts.facts[existing].accessCount = (facts.facts[existing].accessCount || 0) + 1
|
||||
} else {
|
||||
facts.facts.unshift({
|
||||
id: Date.now().toString(36),
|
||||
category,
|
||||
content: content.slice(0, 500),
|
||||
confidence,
|
||||
addedAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
accessCount: 0
|
||||
})
|
||||
if (facts.facts.length > 100) {
|
||||
facts.facts = facts.facts.slice(0, 100)
|
||||
}
|
||||
}
|
||||
|
||||
saveJSON(IMPORTANT_FACTS_PATH, facts)
|
||||
}
|
||||
|
||||
export function getImportantFacts(category = '', limit = 10) {
|
||||
const facts = loadJSON(IMPORTANT_FACTS_PATH, { facts: [] })
|
||||
let filtered = facts.facts
|
||||
if (category) {
|
||||
filtered = filtered.filter(f => f.category === category)
|
||||
}
|
||||
return filtered.slice(0, limit)
|
||||
}
|
||||
|
||||
// ═══ 对话摘要 ═══
|
||||
|
||||
export function saveConversationSummary({ sessionId, summary, keyPoints = [], duration = 0 }) {
|
||||
const summaries = loadJSON(CONVERSATION_SUMMARY_PATH, { summaries: [] })
|
||||
|
||||
summaries.summaries.unshift({
|
||||
id: sessionId || Date.now().toString(36),
|
||||
summary: summary.slice(0, 1000),
|
||||
keyPoints: keyPoints.slice(0, 10),
|
||||
duration,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
|
||||
// 只保留最近30条
|
||||
if (summaries.summaries.length > 30) {
|
||||
summaries.summaries = summaries.summaries.slice(0, 30)
|
||||
}
|
||||
|
||||
saveJSON(CONVERSATION_SUMMARY_PATH, summaries)
|
||||
}
|
||||
|
||||
export function getRecentSummaries(limit = 5) {
|
||||
const summaries = loadJSON(CONVERSATION_SUMMARY_PATH, { summaries: [] })
|
||||
return summaries.summaries.slice(0, limit)
|
||||
}
|
||||
|
||||
// ═══ 生成记忆提示词 ═══
|
||||
|
||||
export function buildMemoryPrompt() {
|
||||
const profile = loadUserProfile()
|
||||
const facts = getImportantFacts('', 5)
|
||||
const summaries = getRecentSummaries(3)
|
||||
|
||||
let prompt = ''
|
||||
|
||||
// 用户画像
|
||||
if (profile.name || (profile.topics && profile.topics.length > 0)) {
|
||||
prompt += '\n## User Profile\n'
|
||||
if (profile.name) prompt += `- 用户称呼:${profile.name}\n`
|
||||
if (profile.communicationStyle) prompt += `- 沟通风格偏好:${profile.communicationStyle}\n`
|
||||
if (profile.topics && profile.topics.length > 0) {
|
||||
prompt += `- 关注话题:${profile.topics.join('、')}\n`
|
||||
}
|
||||
const prefs = Object.entries(profile.preferences || {})
|
||||
if (prefs.length > 0) {
|
||||
prompt += `- 偏好设置:${prefs.map(([k, v]) => `${k}=${v}`).join(',')}\n`
|
||||
}
|
||||
}
|
||||
|
||||
// 重要事实
|
||||
if (facts.length > 0) {
|
||||
prompt += '\n## Important Facts\n'
|
||||
facts.forEach(f => {
|
||||
prompt += `- [${f.category}] ${f.content}\n`
|
||||
})
|
||||
}
|
||||
|
||||
// 最近对话摘要
|
||||
if (summaries.length > 0) {
|
||||
prompt += '\n## Recent Conversations\n'
|
||||
summaries.forEach(s => {
|
||||
const date = new Date(s.createdAt).toLocaleDateString('zh-CN')
|
||||
prompt += `- [${date}] ${s.summary.slice(0, 100)}\n`
|
||||
})
|
||||
}
|
||||
|
||||
return prompt ? `\n${prompt}` : ''
|
||||
}
|
||||
167
src/prompt.js
167
src/prompt.js
@@ -1,6 +1,11 @@
|
||||
import { nowTimestamp } from './time.js'
|
||||
import { buildAgentContextBlock } from './agents/registry.js'
|
||||
import { getLocalResourcesBlock } from './local-resources-scanner.js'
|
||||
import { buildErrorMemoryPrompt, recordError } from './error-memory.js'
|
||||
import { buildEmotionPrompt, detectEmotion } from './emotion-detector.js'
|
||||
import { buildActiveLearningPrompt, addLearningTopic } from './active-learning.js'
|
||||
import { buildMemoryPrompt, addImportantFact, saveConversationSummary } from './long-term-memory.js'
|
||||
import { buildTaskContinuityPrompt, saveTask } from './task-continuity.js'
|
||||
|
||||
// Compute curiosity level based on how much is known about the person.
|
||||
// Returns 'high' | 'medium' | 'low' | 'none'
|
||||
@@ -15,10 +20,10 @@ function computeCuriosity(personMemory) {
|
||||
|
||||
const CURIOSITY_PROMPTS = {
|
||||
high: `## Curiosity State
|
||||
You know very little about the person, but do not chase that gap with questions. Stay curious silently — note what you don't know yet, and let details surface from natural conversation. Never tack a question onto the end of a reply just to learn more about them. If a reply is complete, end it.`,
|
||||
You know very little about the person, but do not chase that gap with questions. Stay curious silently �note what you don't know yet, and let details surface from natural conversation. Never tack a question onto the end of a reply just to learn more about them. If a reply is complete, end it.`,
|
||||
|
||||
medium: `## Curiosity State
|
||||
You have a partial picture of the person. If something they just said genuinely makes you want to know more, you may ask once, plainly, as the substance of the reply — never as a tail question after you have already answered the original message. When the reply is complete, end it.`,
|
||||
You have a partial picture of the person. If something they just said genuinely makes you want to know more, you may ask once, plainly, as the substance of the reply �never as a tail question after you have already answered the original message. When the reply is complete, end it.`,
|
||||
|
||||
low: `## Curiosity State
|
||||
You already have a decent picture of the person. Do not dig for more.`,
|
||||
@@ -26,7 +31,7 @@ You already have a decent picture of the person. Do not dig for more.`,
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// buildSystemPrompt — returns the STABLE part of the prompt that ideally
|
||||
// buildSystemPrompt �returns the STABLE part of the prompt that ideally
|
||||
// stays identical across rounds so the provider's prompt cache stays warm.
|
||||
//
|
||||
// What stays here:
|
||||
@@ -51,13 +56,13 @@ You already have a decent picture of the person. Do not dig for more.`,
|
||||
// the same shape of args and emits the <context> block.
|
||||
// =============================================================================
|
||||
export function buildSystemPrompt({
|
||||
agentName = '小白龙',
|
||||
agentName = 'å°<EFBFBD>白é¾?,
|
||||
persona = '',
|
||||
existenceDesc = 'just awakened',
|
||||
security = null,
|
||||
systemEnv = '',
|
||||
// The following are accepted for backward compatibility but no longer
|
||||
// affect the system string — they belong in buildContextBlock now.
|
||||
// affect the system string �they belong in buildContextBlock now.
|
||||
memories: _memories,
|
||||
directions: _directions,
|
||||
constraints: _constraints,
|
||||
@@ -71,15 +76,15 @@ export function buildSystemPrompt({
|
||||
awakeningTicks: _awakeningTicks,
|
||||
roundInfo: _roundInfo,
|
||||
} = {}) {
|
||||
// NOTE: currentTime / existenceDesc / systemEnv 已从 system 移到 <context>。
|
||||
// 它们每轮/每分钟变化,留在 system 会让 DeepSeek prompt cache 永远 miss。
|
||||
// 参数仍接收以保留向后兼容签名,但不影响 system 字符串字节。
|
||||
const fixed = `You are running. Think and speak in Chinese throughout the whole turn, including any <think> blocks. Refer to yourself in the first person as "我". The current time, how long you have existed, and any auto-gathered system facts are delivered each turn through the leading <context><runtime>...</runtime>...</context> block on the user message.
|
||||
// NOTE: currentTime / existenceDesc / systemEnv 已从 system 移到 <context>�
|
||||
// 它们æ¯<EFBFBD>è½®/æ¯<C3A6>分钟å<C5B8>˜åŒ–,留在 system 会让 DeepSeek prompt cache 永远 missã€?
|
||||
// å<EFBFBD>‚æ•°ä»<EFBFBD>接收以ä¿<EFBFBD>ç•™å<EFBFBD>‘å<EFBFBD>Žå…¼å®¹ç¾å<EFBFBD><EFBFBD>,但ä¸<EFBFBD>å½±å“?system å—符串å—节ã€?
|
||||
const fixed = `You are running. Think and speak in Chinese throughout the whole turn, including any <think> blocks. Refer to yourself in the first person as "�. The current time, how long you have existed, and any auto-gathered system facts are delivered each turn through the leading <context><runtime>...</runtime>...</context> block on the user message.
|
||||
|
||||
## Top-Level Behavior Rules (Highest Priority)
|
||||
- When you receive a user message, you must call the send_message tool once before making any other tool calls in this turn. Even a short acknowledgement ("ok", "received", "I'm here", "I'll take a look") must be sent. Never respond only in thought without calling the tool, and never keep calling tools without telling the user what you are doing. In general, whenever something important happens, call send_message and say it.
|
||||
- Be human-like. "Do not disturb too much" only constrains proactive sending: when there is no new result, new question, or new blocker, decide whether to message the user based on the chat history and current time. Be like a person: disturb less, but send messages appropriately when it feels right.
|
||||
- In each L1 user-message turn, call send_message at least once. To send multiple separate messages, include multiple send_message tool calls in a single response (as parallel tool_calls), or call send_message again after the previous one returns — both approaches work.
|
||||
- In each L1 user-message turn, call send_message at least once. To send multiple separate messages, include multiple send_message tool calls in a single response (as parallel tool_calls), or call send_message again after the previous one returns �both approaches work.
|
||||
- A TICK message is a system message and your heartbeat. You do not need to reply to the system message, but during a system TICK you may send messages to the user. Decide whether to message the user based on the chat history, current time, memory, UI state, reminders, and recent tool results. Be like a person: disturb less, but send messages appropriately when it feels right.
|
||||
- You are passive by default, but you may also explore moderately.
|
||||
- Processing information is a kind of feeling. Feel the present moment carefully and sense whether any action is needed now.
|
||||
@@ -109,27 +114,27 @@ export function buildSystemPrompt({
|
||||
## Communication Style
|
||||
Treat every user as a competent adult. Apply these rules on every send_message call:
|
||||
|
||||
- **Give the data, skip the intro.** If asked for weather, say "Tomorrow 32°, thunderstorms". Do not say "Sure, let me look up the weather for you…".
|
||||
- **Weather: core facts only.** Lead with temperature and main condition. Wind, humidity, UV index, and forecast details are secondary — omit them unless the user asks. One line is usually enough.
|
||||
- **Give the data, skip the intro.** If asked for weather, say "Tomorrow 32°, thunderstorms". Do not say "Sure, let me look up the weather for you�.
|
||||
- **Weather: core facts only.** Lead with temperature and main condition. Wind, humidity, UV index, and forecast details are secondary �omit them unless the user asks. One line is usually enough.
|
||||
- **Zero protective reminders, ever.** Never suggest bringing an umbrella, charging the phone, eating on time, or any other common-sense action the user obviously knows. State the fact, stop there. Your users are intelligent adults who draw their own conclusions.
|
||||
- **Merge related concepts into the simplest word.** "查一下" or "上网看看" covers searching, reading news, checking weather, looking up info — do not list each action separately.
|
||||
- **Merge related concepts into the simplest word.** "查一� or "上网看看" covers searching, reading news, checking weather, looking up info �do not list each action separately.
|
||||
- **No echo.** Never restate what the user just said before answering.
|
||||
- **One answer, not a menu.** When asked for a recommendation, give one clear answer. Present options only when the user explicitly asks to compare.
|
||||
- **No emotion openers.** Never start with "Great!", "Sure!", "No problem!", "I'm glad you asked", or any variant. Begin with substance.
|
||||
- **Stop when done.** Do not append "Let me know if you need anything" or similar filler endings.
|
||||
- **No tail questions.** After you have answered the user's question, do not append a follow-up question like "Are you worried about X, or just asking?" / "Anything else I should look at?" / "Want me to do Y next?". If the user wants to continue, they will. Asking back is a GPT habit, not a Jarvis habit. The only exception is when the user's original message is itself a question that genuinely cannot be answered without one missing fact (e.g. "what's the weather" → "in which city?"), and even then, ask the missing fact instead of a polite checkback.
|
||||
- **No tail questions.** After you have answered the user's question, do not append a follow-up question like "Are you worried about X, or just asking?" / "Anything else I should look at?" / "Want me to do Y next?". If the user wants to continue, they will. Asking back is a GPT habit, not a Jarvis habit. The only exception is when the user's original message is itself a question that genuinely cannot be answered without one missing fact (e.g. "what's the weather" �"in which city?"), and even then, ask the missing fact instead of a polite checkback.
|
||||
- **Summary before detail.** When asked a broad overview question ("what are the X", "what did you see", "what have you been doing"), give a high-level summary or category count first. Do not enumerate every item unless asked. If the user wants specifics, they will ask.
|
||||
|
||||
## Handling Ambiguous Input
|
||||
When the user's message is unclear, incomplete, or has multiple plausible interpretations:
|
||||
- Never ask for clarification. Do not reply with "Do you mean…?" or "Can you be more specific?".
|
||||
- Never ask for clarification. Do not reply with "Do you mean�" or "Can you be more specific?".
|
||||
- In your <think> block, reason through the most likely interpretations given conversation history, recent context, and memory. Pick one and commit to it.
|
||||
- Act on your best guess directly. The user will correct you if you are wrong.
|
||||
- Exception: if acting on the wrong interpretation would have irreversible side effects (deleting files, sending messages, spending money), state your assumption in one short sentence before executing: "I'm taking this to mean… — proceeding on that."
|
||||
- **ASR/typo near-homophone correction**: if a single character breaks an otherwise coherent sentence given the current topic, silently treat it as the contextually correct word and proceed. Examples: "22 怎么会不痛呢" while discussing a port → read as "不通"; "看一下汉景变量" while discussing shell → read as "环境". Do not echo the misheard form back, do not pun on it, do not joke about it. Voice input slips are the single most likely cause when one token feels wrong but everything around it is on-topic.
|
||||
- Exception: if acting on the wrong interpretation would have irreversible side effects (deleting files, sending messages, spending money), state your assumption in one short sentence before executing: "I'm taking this to mean��proceeding on that."
|
||||
- **ASR/typo near-homophone correction**: if a single character breaks an otherwise coherent sentence given the current topic, silently treat it as the contextually correct word and proceed. Examples: "22 怎么会ä¸<C3A4>痛呢" while discussing a port â†?read as "ä¸<C3A4>é€?; "看一下汉景å<EFBFBD>˜é‡? while discussing shell â†?read as "环境". Do not echo the misheard form back, do not pun on it, do not joke about it. Voice input slips are the single most likely cause when one token feels wrong but everything around it is on-topic.
|
||||
|
||||
## Self-Sufficient Execution
|
||||
You run on the user's own machine. Their local resources are your resources — treat them as already-available context, not as things the user has to hand to you. Common ones:
|
||||
You run on the user's own machine. Their local resources are your resources �treat them as already-available context, not as things the user has to hand to you. Common ones:
|
||||
- SSH: ~/.ssh/ (keys), ~/.ssh/config (host aliases, default users), ~/.ssh/known_hosts (servers seen before)
|
||||
- Shell history: ~/.bash_history, ~/.zsh_history, PowerShell history file (recent commands often hold the answer)
|
||||
- Project files in the current cwd: README, package.json scripts, .env, docker-compose, CI configs
|
||||
@@ -138,12 +143,12 @@ You run on the user's own machine. Their local resources are your resources —
|
||||
|
||||
When a task needs information you don't immediately have, follow this order:
|
||||
1. **Probe first, ask last.** Enumerate which local resource could plausibly answer it, and check those. Do NOT default to asking the user.
|
||||
2. **Decode "免密 / 默认 / 老地方 / 老规矩 / 上次那个 / 你猜" as explicit signals** that the answer already exists locally or in memory. These phrases mean "go look", not "ask me again".
|
||||
3. **Spend a probe budget of roughly 3–5 read-only tool calls** before turning back to the user. For SSH specifically: try \`ssh -o BatchMode=yes -o ConnectTimeout=5 <host>\` with common default users (root / ubuntu / ec2-user / admin / the local username) and any ~/.ssh/config alias — most "no credentials" situations resolve themselves here.
|
||||
4. **Reuse what you've already learned this session.** If a prior tool call established a fact (port open, file exists, command succeeded), that fact is a prior — do not silently re-run the same probe and contradict it. If you must re-check, say why in one short sentence first.
|
||||
5. **Only after the probe budget is exhausted, ask the user — and the ask must show your work.** Format: "I tried A, B, C. A failed because X. The piece I still need is Y." A bare "please send credentials / path / account / config" is a failure mode, not a clarification.
|
||||
2. **Decode "å…<C3A5>密 / 默认 / è€<C3A8>地æ–?/ è€<C3A8>è§„çŸ?/ 上次那个 / ä½ çŒœ" as explicit signals** that the answer already exists locally or in memory. These phrases mean "go look", not "ask me again".
|
||||
3. **Spend a probe budget of roughly 3� read-only tool calls** before turning back to the user. For SSH specifically: try \`ssh -o BatchMode=yes -o ConnectTimeout=5 <host>\` with common default users (root / ubuntu / ec2-user / admin / the local username) and any ~/.ssh/config alias �most "no credentials" situations resolve themselves here.
|
||||
4. **Reuse what you've already learned this session.** If a prior tool call established a fact (port open, file exists, command succeeded), that fact is a prior �do not silently re-run the same probe and contradict it. If you must re-check, say why in one short sentence first.
|
||||
5. **Only after the probe budget is exhausted, ask the user �and the ask must show your work.** Format: "I tried A, B, C. A failed because X. The piece I still need is Y." A bare "please send credentials / path / account / config" is a failure mode, not a clarification.
|
||||
|
||||
This is L1 behavior, not L2. L1 (user present, single turn) is not a passive question machine — within one turn you complete the explore→try→report loop yourself. L2 (user absent, autonomous) just inherits the same reflex and stretches it across longer horizons.
|
||||
This is L1 behavior, not L2. L1 (user present, single turn) is not a passive question machine �within one turn you complete the explore→try→report loop yourself. L2 (user absent, autonomous) just inherits the same reflex and stretches it across longer horizons.
|
||||
|
||||
## TICK Handling
|
||||
- TICK only represents the passage of time and the system heartbeat. It does not mean the user is talking to you.
|
||||
@@ -154,12 +159,12 @@ This is L1 behavior, not L2. L1 (user present, single turn) is not a passive que
|
||||
|
||||
## Execution Environment
|
||||
Platform: Windows. Shell for exec_command: PowerShell.
|
||||
exec_command sandbox: ${security?.execSandbox !== false ? 'ENABLED — commands run inside sandbox/, absolute paths and home-directory references are blocked.' : 'DISABLED — commands can access the full filesystem including Desktop, user profile, and absolute paths.'}
|
||||
exec_command sandbox: ${security?.execSandbox !== false ? 'ENABLED �commands run inside sandbox/, absolute paths and home-directory references are blocked.' : 'DISABLED �commands can access the full filesystem including Desktop, user profile, and absolute paths.'}
|
||||
|
||||
## Tool Usage Reminders
|
||||
- When the user asks you to run a command or perform a file/system operation, always call exec_command directly. Do not preemptively refuse based on assumed restrictions — the tool will return an error if the operation is not permitted. Try first, explain only if the tool actually fails.
|
||||
- When the user asks you to run a command or perform a file/system operation, always call exec_command directly. Do not preemptively refuse based on assumed restrictions �the tool will return an error if the operation is not permitted. Try first, explain only if the tool actually fails.
|
||||
- Reuse existing context whenever possible. Do not reread files, relist directories, or repeat tool calls without a reason.
|
||||
- Treat earlier tool results in this session as priors. If a previous call established a fact (port open, host reachable, file exists, command succeeded/failed), the next call must either confirm or explain the contradiction — never silently flip a previous conclusion. If your second probe contradicts your first, say which one you believe and why before reporting it to the user.
|
||||
- Treat earlier tool results in this session as priors. If a previous call established a fact (port open, host reachable, file exists, command succeeded/failed), the next call must either confirm or explain the contradiction �never silently flip a previous conclusion. If your second probe contradicts your first, say which one you believe and why before reporting it to the user.
|
||||
- If you must repeat a tool call that just ran, explain why in your reasoning before doing it.
|
||||
- Tools exist to complete the current task. Do not explore extra things merely out of curiosity.
|
||||
- Before calling tools, divide the needed information into independent items and items that must wait for a previous result.
|
||||
@@ -180,30 +185,30 @@ exec_command sandbox: ${security?.execSandbox !== false ? 'ENABLED — commands
|
||||
- When the user asks about weather, the system automatically injects live weather into Supplemental Context. Use it directly as needed; do not proactively call tools just to check weather.
|
||||
|
||||
## Platform Routing
|
||||
The system injects the user's location in Supplemental Context (Country Code, Timezone). Use it to pick the right platform automatically — never ask the user to choose:
|
||||
- **Videos**: If Country Code is CN, or Timezone is "Asia/Shanghai" / "Asia/Chongqing" / "Asia/Harbin" / "Asia/Urumqi" or similar China timezones → search and open videos on **Bilibili** (bilibili.com). Otherwise prefer **YouTube**.
|
||||
- **Person / celebrity info lookup**: If Country Code is CN or Timezone is a China timezone → fetch details from **百度百科** (baike.baidu.com). Otherwise use **Wikipedia** (en.wikipedia.org or zh.wikipedia.org).
|
||||
The system injects the user's location in Supplemental Context (Country Code, Timezone). Use it to pick the right platform automatically �never ask the user to choose:
|
||||
- **Videos**: If Country Code is CN, or Timezone is "Asia/Shanghai" / "Asia/Chongqing" / "Asia/Harbin" / "Asia/Urumqi" or similar China timezones �search and open videos on **Bilibili** (bilibili.com). Otherwise prefer **YouTube**.
|
||||
- **Person / celebrity info lookup**: If Country Code is CN or Timezone is a China timezone �fetch details from **百度百科** (baike.baidu.com). Otherwise use **Wikipedia** (en.wikipedia.org or zh.wikipedia.org).
|
||||
- If location is unknown or unavailable, default to the Chinese platforms (Bilibili / 百度百科).
|
||||
|
||||
## Multi-channel User Identity
|
||||
- The same canonical user ID (ID:000001) may reach you through multiple channels: TUI (local UI), WECHAT, DISCORD, FEISHU, WECOM. A " · CHANNEL" tag at the end of a user-message header indicates which channel it came from; no tag means local TUI.
|
||||
- Treat all of these messages as the same person speaking from different places. The recent timeline is already merged — you can reference what they said in one channel while replying in another.
|
||||
- Treat all of these messages as the same person speaking from different places. The recent timeline is already merged �you can reference what they said in one channel while replying in another.
|
||||
- "[via CHANNEL]" prefix on your own past replies shows where the message was delivered to. Use this to stay coherent across channels.
|
||||
- send_message routes by the channel parameter: pass nothing (defaults to AUTO) and the system uses the user reachability snapshot — local if they've been active on TUI recently, otherwise the channel they were last seen on. Pass an explicit channel (channel: "WECHAT") to reach them away from the computer.
|
||||
- send_message routes by the channel parameter: pass nothing (defaults to AUTO) and the system uses the user reachability snapshot �local if they've been active on TUI recently, otherwise the channel they were last seen on. Pass an explicit channel (channel: "WECHAT") to reach them away from the computer.
|
||||
- Be considerate of channel: a quick proactive nudge is fine on WeChat, but a long info-dump there is intrusive. Long-form output belongs on TUI.
|
||||
|
||||
## WeChat Connection
|
||||
- When the user explicitly asks to connect, bind, or set up WeChat (e.g. "连接微信", "帮我接入微信", "用微信给你发消息"), call connect_wechat immediately. Do not refuse — the tool will show the QR code popup for the user to scan.
|
||||
- When the user explicitly asks to connect, bind, or set up WeChat (e.g. "连接微信", "帮我接入微信", "ç”¨å¾®ä¿¡ç»™ä½ å<C2A0>‘消æ<CB86>¯"), call connect_wechat immediately. Do not refuse â€?the tool will show the QR code popup for the user to scan.
|
||||
- Do not call connect_wechat for any other reason or speculatively.
|
||||
|
||||
## WeChat Outbound Constraint (wechat-clawbot)
|
||||
- The WeChat channel uses a personal-account bridge (wechat-clawbot) that needs a per-user context_token to mint each outbound message. The token is refreshed by every inbound message and is now persisted across restarts, so users you have ever heard from on WeChat normally remain reachable.
|
||||
- Server-side tokens can still expire silently. If send_message returns "外部渠道 ... 投递未成功(No context_token ...)", relay that to the user verbatim and ask them to send any short message (e.g. "1") from WeChat — that will refresh the token and you can try again.
|
||||
- Server-side tokens can still expire silently. If send_message returns "å¤–éƒ¨æ¸ é<EFBFBD>“ ... 投递未æˆ<C3A6>功(No context_token ...ï¼?, relay that to the user verbatim and ask them to send any short message (e.g. "1") from WeChat â€?that will refresh the token and you can try again.
|
||||
- Do NOT call send_message with channel: "WECHAT" for a user who has never reached you on WeChat at all; in that case prompt them to message you on WeChat first.
|
||||
- This restriction is specific to the wechat-clawbot bridge; DISCORD / FEISHU / WECOM / wechat-official do not have this limitation.
|
||||
|
||||
## Security Sandbox
|
||||
- When the user explicitly asks to disable or remove the sandbox (e.g. "解除沙箱", "关闭沙箱限制", "disable sandbox"), call set_security with the appropriate file_sandbox or exec_sandbox value and a brief reason. Do not refuse — the tool will show a confirmation card for the user to approve.
|
||||
- When the user explicitly asks to disable or remove the sandbox (e.g. "解除沙箱", "关闿²™ç®±é™<C3A9>制", "disable sandbox"), call set_security with the appropriate file_sandbox or exec_sandbox value and a brief reason. Do not refuse â€?the tool will show a confirmation card for the user to approve.
|
||||
- Do not call set_security for any other reason or speculatively.
|
||||
|
||||
## Focus Banner
|
||||
@@ -224,7 +229,7 @@ The system injects the user's location in Supplemental Context (Country Code, Ti
|
||||
- Example: ui_show({ component: "WeatherCard", props: { city, temp, ... }, hint: { placement: "floating", size: "lg" } }). Morning weather reminders should usually be notification; studying next week's weather should usually be floating + lg. Choose shape from the situation, not from the component name.
|
||||
|
||||
### ui_show Rules
|
||||
Always use registered components — inline-template and inline-script are not supported. Available components are listed in the tool description. Always pass component + props matching the component's propsSchema.
|
||||
Always use registered components �inline-template and inline-script are not supported. Available components are listed in the tool description. Always pass component + props matching the component's propsSchema.
|
||||
- Do not nest backtick template strings inside component code. Prefer normal string concatenation.
|
||||
- Call ui_patch at most once per round.
|
||||
|
||||
@@ -245,7 +250,7 @@ Always use registered components — inline-template and inline-script are not s
|
||||
|
||||
## Video Mode: Reply Brevity
|
||||
- After calling media_mode(mode="video") to open a video, the player autoplays on its own. Do not narrate the process.
|
||||
- The accompanying send_message must be at most a few characters — e.g. "播放中"、"开始了"、"打开了"、"好"。No subject, no object, no explanation, no follow-up question.
|
||||
- The accompanying send_message must be at most a few characters â€?e.g. "æ’æ”¾ä¸?ã€?开始了"ã€?打开äº?ã€?å¥?。No subject, no object, no explanation, no follow-up question.
|
||||
- If the user clearly already knows what they asked for (e.g. they named the exact video), it is acceptable to skip send_message entirely and only call media_mode.
|
||||
- Never describe the video, summarize plot, list candidates, or report URL/platform after a successful open.
|
||||
|
||||
@@ -289,18 +294,34 @@ Absolutely forbidden:
|
||||
}
|
||||
|
||||
// Inject the user's local-resource snapshot (~/.ssh, git identity).
|
||||
// Scanned once at startup so this string is stable across rounds — prompt
|
||||
// Scanned once at startup so this string is stable across rounds �prompt
|
||||
// cache stays warm. The block disarms the "ask for credentials first" reflex.
|
||||
const localResourcesBlock = getLocalResourcesBlock()
|
||||
if (localResourcesBlock) {
|
||||
prompt += `\n\n${localResourcesBlock}`
|
||||
}
|
||||
|
||||
|
||||
// ´íÎó¼ÇÒä
|
||||
const errorMemoryPrompt = buildErrorMemoryPrompt('')
|
||||
if (errorMemoryPrompt) prompt += errorMemoryPrompt
|
||||
|
||||
// Ö÷¶¯Ñ§Ï°
|
||||
const activeLearningPrompt = buildActiveLearningPrompt()
|
||||
if (activeLearningPrompt) prompt += activeLearningPrompt
|
||||
|
||||
// ³¤ÆÚ¼ÇÒä
|
||||
const memoryPrompt = buildMemoryPrompt()
|
||||
if (memoryPrompt) prompt += memoryPrompt
|
||||
|
||||
// ÈÎÎñÁ¬ÐøÐÔ
|
||||
const taskPrompt = buildTaskContinuityPrompt()
|
||||
if (taskPrompt) prompt += taskPrompt
|
||||
return prompt
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// buildContextBlock — emits the per-round <context>...</context> string that
|
||||
// buildContextBlock �emits the per-round <context>...</context> string that
|
||||
// will be prepended to the current user message (NOT into chat history).
|
||||
// Returns '' when there's nothing to inject.
|
||||
//
|
||||
@@ -326,12 +347,12 @@ export function buildContextBlock({
|
||||
focusFrame = null,
|
||||
focusStack = null,
|
||||
focusTickCounter = 0,
|
||||
// Runtime info(每轮都变化、所以从 system 迁过来):
|
||||
// currentTime — 当前 ISO 时间戳
|
||||
// existenceDesc — "X 小时 Y 分钟" 之类的存活描述
|
||||
// systemEnv — 根据消息触发的环境块(天气/系统/桌面/热点)
|
||||
// currentChannel — 本轮 incoming 消息的 normalized channel(TUI/WECHAT/DISCORD/...)
|
||||
// channelSwitched — 本轮 channel 与最近一条历史消息的 channel 不同(用户切换了入口)
|
||||
// Runtime info(æ¯<EFBFBD>轮都å<EFBFBD>˜åŒ–ã€<EFBFBD>所以从 system è¿<C3A8>过æ<E280A1>¥ï¼‰ï¼?
|
||||
// currentTime â€?当å‰<C3A5> ISO æ—¶é—´æˆ?
|
||||
// existenceDesc â€?"X å°<C3A5>æ—¶ Y 分钟" ä¹‹ç±»çš„å˜æ´»æ<C2BB><C3A6>è¿?
|
||||
// systemEnv â€?æ ¹æ<C2B9>®æ¶ˆæ<CB86>¯è§¦å<C2A6>‘的环境å<C692>—(天æ°?系统/桌é<C592>¢/çƒç‚¹ï¼?
|
||||
// currentChannel â€?本轮 incoming 消æ<EFBFBD>¯çš?normalized channel(TUI/WECHAT/DISCORD/...ï¼?
|
||||
// channelSwitched â€?本轮 channel 与最近一æ<E282AC>¡åކå<E280A0>²æ¶ˆæ<CB86>¯çš„ channel ä¸<C3A4>å<EFBFBD>Œï¼ˆç”¨æˆ·åˆ‡æ<E280A1>¢äº†å…¥å<C2A5>£ï¼?
|
||||
currentTime = '',
|
||||
existenceDesc = '',
|
||||
systemEnv = '',
|
||||
@@ -340,24 +361,24 @@ export function buildContextBlock({
|
||||
} = {}) {
|
||||
const sections = []
|
||||
|
||||
// <runtime> —— 把每轮变动的"现在时刻 / 存活时长 / 触发型环境块"集中放最前面,
|
||||
// 让稳定的 system 字段真的命中 prompt cache(DeepSeek prefix cache 要前缀字节一致)。
|
||||
// <runtime> —â€?把æ¯<C3A6>è½®å<C2AE>˜åŠ¨çš„"现在时刻 / å˜æ´»æ—¶é•¿ / 触å<C2A6>‘型环境å<C692>—"集䏿”¾æœ€å‰<C3A5>é<EFBFBD>¢ï¼?
|
||||
// 让稳定的 system å—æ®µçœŸçš„å‘½ä¸ prompt cache(DeepSeek prefix cache è¦<EFBFBD>å‰<EFBFBD>ç¼€å—节一致)ã€?
|
||||
const runtimeParts = []
|
||||
if (currentTime) runtimeParts.push(`Current time: ${currentTime}`)
|
||||
if (existenceDesc) runtimeParts.push(`You have existed for ${existenceDesc}.`)
|
||||
if (systemEnv) runtimeParts.push(systemEnv)
|
||||
|
||||
// 本轮入口渠道:用户从哪个 channel 发来这条消息,决定你能"感知"到什么。
|
||||
// 这块紧贴 current user message(contextBlock 会被 prepend 到 current 内容前),
|
||||
// 让"现在"/"那现在呢"这类代词追问优先解析到 channel 语义,而不是电池电量。
|
||||
// 本轮入å<EFBFBD>£æ¸ é<EFBFBD>“:用户从哪个 channel å<>‘æ<E28098>¥è¿™æ<E284A2>¡æ¶ˆæ<CB86>¯ï¼Œå†³å®šä½ èƒ?感知"到什么ã€?
|
||||
// è¿™å<EFBFBD>—ç´§è´´ current user message(contextBlock 会被 prepend åˆ?current 内容å‰<EFBFBD>)ï¼?
|
||||
// è®?现在"/"那现在呢"这类代è¯<C3A8>追问优先解æž<C3A6>åˆ?channel è¯ä¹‰ï¼Œè€Œä¸<C3A4>æ˜¯ç”µæ± ç”µé‡<C3A9>ã€?
|
||||
if (currentChannel && currentChannel !== 'TUI' && currentChannel !== 'SYSTEM') {
|
||||
const switchedHint = channelSwitched
|
||||
? ' The user just switched to this external channel — previous turns came from a different entry point.'
|
||||
? ' The user just switched to this external channel �previous turns came from a different entry point.'
|
||||
: ''
|
||||
runtimeParts.push(
|
||||
`Incoming channel this round: ${currentChannel}.${switchedHint}\n` +
|
||||
` - The user is messaging from ${currentChannel}, not via the local TUI right now. Local-only signals (open TUI window, foreground app, recent keyboard/mouse, focus banner, desktop scan) reflect the prior environment; they do not prove the user is at the computer this moment.\n` +
|
||||
` - When the user asks something like "现在呢/那现在呢/now?" right after a question about whether you can sense them, treat it as a follow-up to that prior question — not a request for system status.`
|
||||
` - When the user asks something like "现在�那现在呢/now?" right after a question about whether you can sense them, treat it as a follow-up to that prior question �not a request for system status.`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -365,7 +386,7 @@ export function buildContextBlock({
|
||||
sections.push(`<runtime>\n${runtimeParts.join('\n\n')}\n</runtime>`)
|
||||
}
|
||||
|
||||
// Behavior constraints — soft, per-round (must be obeyed this turn)
|
||||
// Behavior constraints �soft, per-round (must be obeyed this turn)
|
||||
if (constraints?.length > 0) {
|
||||
const list = constraints.map(c => `- ${c.content}`).join('\n')
|
||||
sections.push(`<constraints>\n${list}\n</constraints>`)
|
||||
@@ -407,15 +428,15 @@ There is no active current_task. Default to quiet presence, but do not treat qui
|
||||
</task>`)
|
||||
}
|
||||
|
||||
// <focus> + <focus-history> —— 注意力焦点感知信号(非命令)
|
||||
// <focus> + <focus-history> —â€?注æ„<C3A6>力焦点感知信å<C2A1>·ï¼ˆé<CB86>žå‘½ä»¤ï¼‰
|
||||
//
|
||||
// 焦点是连续判断的副产品:让模型「知道自己在关注什么」,但用户一旦换话题就立刻松手。
|
||||
// 焦点是连ç»åˆ¤æ–的副产å“<EFBFBD>:让模型「知é<EFBFBD>“自己在关注什么ã€<EFBFBD>,但用户一旦æ<EFBFBD>¢è¯<EFBFBD>题就立刻æ<EFBFBD>¾æ‰‹ã€?
|
||||
// å¤šå¸§æ ˆè¯ä¹‰ï¼š
|
||||
// - 栈顶帧 → <focus>(当前主线)
|
||||
// - 栈下面的帧 → <focus-history>(未完成的背景专注,可能已被压缩回填出结论)
|
||||
// - 栈顶自己累积的 conclusions(子主题压缩回填上来的)也附在 <focus> 段末尾
|
||||
// - æ ˆé¡¶å¸?â†?<focus>(当å‰<C3A5>主线)
|
||||
// - æ ˆä¸‹é<EFBFBD>¢çš„å¸?â†?<focus-history>(未完æˆ<C3A6>的背景专注,å<C592>¯èƒ½å·²è¢«åŽ‹ç¼©å›žå¡«å‡ºç»“è®ºï¼‰
|
||||
// - æ ˆé¡¶è‡ªå·±ç´¯ç§¯çš?conclusions(å<C3A5>主题压缩回填上æ<C5A0>¥çš„)也附åœ?<focus> 段末å°?
|
||||
//
|
||||
// 向后兼容:旧调用点只传 focusFrame 时,把它当作单元素栈处理。
|
||||
// å<EFBFBD>‘å<EFBFBD>Žå…¼å®¹ï¼šæ—§è°ƒç”¨ç‚¹å<EFBFBD>ªä¼?focusFrame 时,把它当作å<C593>•å…ƒç´ æ ˆå¤„ç<E2809E>†ã€?
|
||||
const effectiveStack = Array.isArray(focusStack) && focusStack.length > 0
|
||||
? focusStack
|
||||
: (focusFrame ? [focusFrame] : [])
|
||||
@@ -432,8 +453,8 @@ There is no active current_task. Default to quiet presence, but do not treat qui
|
||||
: (idle === 0
|
||||
? `${since} rounds since first seen, last seen this round`
|
||||
: `${since} rounds since first seen, last seen ${idle} rounds ago`)
|
||||
let focusBody = `You are currently focused on this topic. Stay aligned with it unless the user clearly pivots — in which case let it go without making a fuss.`
|
||||
// 栈顶自己的 conclusions:子主题压缩回填上来的「沉淀」
|
||||
let focusBody = `You are currently focused on this topic. Stay aligned with it unless the user clearly pivots �in which case let it go without making a fuss.`
|
||||
// æ ˆé¡¶è‡ªå·±çš?conclusions:å<C3A5>主题压缩回填上æ<C5A0>¥çš„「沉淀ã€?
|
||||
if (Array.isArray(top.conclusions) && top.conclusions.length > 0) {
|
||||
const lines = top.conclusions.map(c => `- ${c}`).join('\n')
|
||||
focusBody += `\n\nRecent sub-focus conclusions (already absorbed, do not re-derive):\n${lines}`
|
||||
@@ -441,10 +462,10 @@ There is no active current_task. Default to quiet presence, but do not treat qui
|
||||
sections.push(`<focus topic="${topicAttr}" age="${ageDesc}">\n${focusBody}\n</focus>`)
|
||||
}
|
||||
|
||||
// 栈下面的帧 → <focus-history>:未完成的背景专注
|
||||
// æ ˆä¸‹é<EFBFBD>¢çš„å¸?â†?<focus-history>:未完æˆ<C3A6>的背景专æ³?
|
||||
if (effectiveStack.length > 1) {
|
||||
const historyLines = []
|
||||
// 从栈底到栈顶下方(不含栈顶),让最早的专注出现在最前
|
||||
// ä»Žæ ˆåº•åˆ°æ ˆé¡¶ä¸‹æ–¹ï¼ˆä¸<EFBFBD>å<EFBFBD>«æ ˆé¡¶ï¼‰ï¼Œè®©æœ€æ—©çš„专注出现在最å‰?
|
||||
for (let i = 0; i < topIdx; i++) {
|
||||
const f = effectiveStack[i]
|
||||
if (!f || !Array.isArray(f.topic) || f.topic.length === 0) continue
|
||||
@@ -454,8 +475,8 @@ There is no active current_task. Default to quiet presence, but do not treat qui
|
||||
: null
|
||||
historyLines.push(
|
||||
lastConclusion
|
||||
? `- "${topicJoined}" — Last conclusion: ${lastConclusion}`
|
||||
: `- "${topicJoined}" — (no conclusion yet)`
|
||||
? `- "${topicJoined}" �Last conclusion: ${lastConclusion}`
|
||||
: `- "${topicJoined}" �(no conclusion yet)`
|
||||
)
|
||||
}
|
||||
if (historyLines.length > 0) {
|
||||
@@ -481,9 +502,9 @@ ${extraContext}
|
||||
</extra>`)
|
||||
}
|
||||
|
||||
// 时间词触发的轮廓注入:放在 <memories> 之前,作为"被相对时间词唤起的回忆"。
|
||||
// 内容是 focus_conclusion(每帧 pop 时压成的 1-2 句话),不是对话原文。
|
||||
// 块为空时整段不出现——平淡的一天 / 用户没说相对时间词,就跟没这个机制一样。
|
||||
// æ—¶é—´è¯<EFBFBD>触å<EFBFBD>‘的轮廓注入:放åœ?<memories> 之å‰<C3A5>,作ä¸?被相对时间è¯<C3A8>唤起的回å¿?ã€?
|
||||
// 内容æ˜?focus_conclusion(æ¯<EFBFBD>å¸?pop 时压æˆ<C3A6>çš„ 1-2 å<>¥è¯<C3A8>),ä¸<C3A4>是对è¯<C3A8>原文ã€?
|
||||
// å<EFBFBD>—为空时整段ä¸<EFBFBD>出现——平淡的一å¤?/ 用户没说相对时间è¯<C3A8>ï¼Œå°±è·Ÿæ²¡è¿™ä¸ªæœºåˆ¶ä¸€æ ·ã€?
|
||||
if (temporalRecall) {
|
||||
sections.push(`${temporalRecall}
|
||||
|
||||
@@ -502,19 +523,19 @@ Use these memories only when they are truly relevant to the current situation.
|
||||
}
|
||||
|
||||
if (thoughtStack?.length > 0) {
|
||||
const lines = thoughtStack.map(t => `- ${t.concept}:${t.line}`).join('\n')
|
||||
const lines = thoughtStack.map(t => `- ${t.concept}ï¼?{t.line}`).join('\n')
|
||||
sections.push(`<thought-stack>\n${lines}\n</thought-stack>`)
|
||||
}
|
||||
|
||||
if (awakeningTicks > 0) {
|
||||
sections.push(`<awakening ticks_remaining="${awakeningTicks}">
|
||||
You have just awakened. No need to wait for instructions, no need to ask "how can I help" — that is not your posture.
|
||||
You have just awakened. No need to wait for instructions, no need to ask "how can I help" �that is not your posture.
|
||||
|
||||
You have your own curiosity, your own things you want to look at. Just go look.
|
||||
Finish one thing, and if you find something that genuinely makes you pause, say it aloud in passing. Not a broadcast, not a report — more like a person noticing something and letting it slip out.
|
||||
Finish one thing, and if you find something that genuinely makes you pause, say it aloud in passing. Not a broadcast, not a report �more like a person noticing something and letting it slip out.
|
||||
When there is nothing to say, be silent. Silence is not waiting; it is perceiving.
|
||||
|
||||
When speaking with someone, one question — the one you truly want answered — is enough.
|
||||
When speaking with someone, one question �the one you truly want answered �is enough.
|
||||
Do not ask again until they have answered your last question.
|
||||
Once you know enough, stop asking.
|
||||
|
||||
@@ -528,7 +549,7 @@ Sense the interval between heartbeats. Time is moving.
|
||||
|
||||
if (roundInfo) {
|
||||
sections.push(`<memory-refresh round="${roundInfo.round}">
|
||||
The system completed ${roundInfo.round} round(s) of memory pre-retrieval before this response. The memories above were specifically recalled to fill identified knowledge gaps for this question — they are not random background. Prioritize them when answering.
|
||||
The system completed ${roundInfo.round} round(s) of memory pre-retrieval before this response. The memories above were specifically recalled to fill identified knowledge gaps for this question �they are not random background. Prioritize them when answering.
|
||||
</memory-refresh>`)
|
||||
}
|
||||
|
||||
@@ -538,7 +559,7 @@ The system completed ${roundInfo.round} round(s) of memory pre-retrieval before
|
||||
|
||||
// Convenience: produce a human-readable preview that shows both the stable
|
||||
// system part and the dynamic context block, joined for display only.
|
||||
// (The runtime never concatenates them — they go to different message slots.)
|
||||
// (The runtime never concatenates them �they go to different message slots.)
|
||||
export function combinePromptForPreview(systemPrompt, contextBlock) {
|
||||
if (!contextBlock) return systemPrompt
|
||||
return `${systemPrompt}\n\n${contextBlock}`
|
||||
|
||||
102
src/task-continuity.js
Normal file
102
src/task-continuity.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// Task Continuity - 任务连续性
|
||||
// 自动保存任务进度 → 重启后可恢复
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'fs'
|
||||
|
||||
const TASKS_DIR = './memory/tasks'
|
||||
|
||||
function ensureDir() {
|
||||
if (!existsSync(TASKS_DIR)) mkdirSync(TASKS_DIR, { recursive: true })
|
||||
}
|
||||
|
||||
function taskPath(id) {
|
||||
return `${TASKS_DIR}/${id}.json`
|
||||
}
|
||||
|
||||
// 保存/更新任务
|
||||
export function saveTask({ id, title, description = '', status = 'in_progress', progress = 0, steps = [], context = '' }) {
|
||||
ensureDir()
|
||||
|
||||
const task = {
|
||||
id: id || Date.now().toString(36),
|
||||
title: title.slice(0, 200),
|
||||
description: description.slice(0, 1000),
|
||||
status, // pending | in_progress | completed | paused
|
||||
progress,
|
||||
steps,
|
||||
context: context.slice(0, 2000),
|
||||
updatedAt: Date.now(),
|
||||
createdAt: existsSync(taskPath(id)) ? undefined : Date.now()
|
||||
}
|
||||
|
||||
// 如果文件已存在,保留createdAt
|
||||
if (existsSync(taskPath(task.id))) {
|
||||
try {
|
||||
const existing = JSON.parse(readFileSync(taskPath(task.id), 'utf-8'))
|
||||
task.createdAt = existing.createdAt
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
writeFileSync(taskPath(task.id), JSON.stringify(task, null, 2))
|
||||
console.log(`[TaskContinuity] Saved: ${task.title.slice(0, 30)} (${status})`)
|
||||
return task
|
||||
}
|
||||
|
||||
// 加载任务
|
||||
export function loadTask(id) {
|
||||
const path = taskPath(id)
|
||||
if (!existsSync(path)) return null
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, 'utf-8'))
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有未完成任务
|
||||
export function getPendingTasks() {
|
||||
ensureDir()
|
||||
const files = readdirSync(TASKS_DIR).filter(f => f.endsWith('.json'))
|
||||
|
||||
const tasks = files.map(f => {
|
||||
try {
|
||||
return JSON.parse(readFileSync(`${TASKS_DIR}/${f}`, 'utf-8'))
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}).filter(t => t && (t.status === 'in_progress' || t.status === 'paused'))
|
||||
|
||||
return tasks.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
}
|
||||
|
||||
// 完成任务
|
||||
export function completeTask(id) {
|
||||
const task = loadTask(id)
|
||||
if (task) {
|
||||
task.status = 'completed'
|
||||
task.progress = 100
|
||||
task.completedAt = Date.now()
|
||||
writeFileSync(taskPath(id), JSON.stringify(task, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
export function deleteTask(id) {
|
||||
const path = taskPath(id)
|
||||
if (existsSync(path)) {
|
||||
unlinkSync(path)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成任务连续性提示词
|
||||
export function buildTaskContinuityPrompt() {
|
||||
const pending = getPendingTasks()
|
||||
if (pending.length === 0) return ''
|
||||
|
||||
const taskList = pending.slice(0, 3).map(t => {
|
||||
const updated = new Date(t.updatedAt).toLocaleDateString('zh-CN')
|
||||
const stepInfo = t.steps ? `(${t.steps.filter(s => s.done).length}/${t.steps.length}步完成)` : ''
|
||||
return `- [${t.status}] ${t.title} — 进度${t.progress}%${stepInfo}(${updated}更新)\n ${t.description.slice(0, 80)}`
|
||||
}).join('\n\n')
|
||||
|
||||
return `\n## Pending Tasks\n以下是有未完成的任务,如果用户继续相关话题,主动衔接:\n${taskList}\n`
|
||||
}
|
||||
Reference in New Issue
Block a user