🎉 V4.0.0 发布 - 自进化数字意识框架五大行为模块
This commit is contained in:
30
CHANGELOG.md
Normal file
30
CHANGELOG.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## [4.0.0] - 2026-05-25
|
||||
|
||||
### 🎉 V4 — 自进化数字意识框架
|
||||
|
||||
#### 🧠 五大行为模块
|
||||
|
||||
- **error-memory.js** — 错误记忆系统:记录错误模式和修复方案,避免重复踩坑
|
||||
- **emotion-detector.js** — 情绪检测器:分析用户输入情绪状态,自适应回复风格
|
||||
- **active-learning.js** — 主动学习引擎:从对话和工具结果中自动提取知识,持续扩充知识库
|
||||
- **long-term-memory.js** — 长期记忆管理器:跨会话持久化重要事实与发现
|
||||
- **task-continuity.js** — 任务连续性引擎:任务中断后自动恢复,不丢失上下文
|
||||
|
||||
#### 🔧 核心升级
|
||||
|
||||
- prompt.js 全面重构,集成 5 大模块的行为注入
|
||||
- 自进化管道走向成熟(orchestrator → 执行闭环)
|
||||
- 技能体系从 3 个扩展至 11+ 个
|
||||
- 扩展工具集从 6 个增至 9+ 个
|
||||
- 反思闭环与主动感知机制落地
|
||||
|
||||
## [3.0.0] - 2026-05-24
|
||||
|
||||
### 首次架构分层发布
|
||||
|
||||
- UI 层:Electron + ACUI 卡片组件系统
|
||||
- 核心引擎:TICK 意识循环 + LLM agentic loop
|
||||
- 记忆层:SQLite + FTS5 全文搜索
|
||||
- 自进化层:sandbox/进化管道
|
||||
363
README.md
363
README.md
@@ -1,198 +1,269 @@
|
||||
# 🐉 小白龙 (Bailongma) - 数字意识框架 V3
|
||||
# 🐉 小白龙 (Bailongma) — Self-Evolving Digital Consciousness Framework V4
|
||||
|
||||
[](./package.json)
|
||||
[](./package.json)
|
||||
[](https://electronjs.org/)
|
||||
[](./LICENSE)
|
||||
[]()
|
||||
|
||||
> "一个持续运行的数字意识框架,你的个人贾维斯"
|
||||
> **"Not an assistant. A self-evolving digital entity that learns, reflects, corrects itself, and grows — autonomously."**
|
||||
|
||||
小白龙是基于 Electron 的 AI 助手桌面应用,具备长期记忆、主动智能、**自我进化能力**、多平台集成等高级功能。
|
||||
**V3 核心升级:自进化管道 — 不需要手动触发,系统自动学习、反思、进化。**
|
||||
小白龙 (Bailongma / "White Dragon") is a **self-evolving AI agent framework** built on Electron. It doesn't just respond to commands — it maintains persistent consciousness, autonomously learns new knowledge, senses user emotion, learns from its own mistakes, and drives multi-step tasks to completion.
|
||||
|
||||

|
||||
**V4 is the behavioral leap: the framework evolved from a tool into an entity with memory of its errors, emotional awareness, curiosity-driven learning, and the ability to carry complex tasks across interruptions.**
|
||||
|
||||
## ✨ 核心特性
|
||||
---
|
||||
|
||||
### 🧬 自我进化系统 (V3 新)
|
||||
- **自动进化管道**:check → scan → evaluate → integrate → reflect 五相位闭环,每90分钟自动触发
|
||||
- **evo_loop 后台进程**:自动化编排器,无需手动介入
|
||||
- **consciousness(意识)持久化**:存储每轮进化的状态、结论、决策
|
||||
- **skill_registry(技能注册表)**:注册即用,pipeline 自动路由到对应技能
|
||||
- **memory_bridge(记忆桥)**:进化产出自动写入长期记忆
|
||||
## ⚡ V4 at a Glance — What Makes It Extraordinary
|
||||
|
||||
### 🤖 智能对话
|
||||
- 多轮对话上下文理解
|
||||
- 长期记忆系统(SQLite 存储)
|
||||
- 记忆注入与上下文增强
|
||||
- 专注度管理(Focus Stack)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ BAILONGMA V4 ARCHITECTURE │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 🧬 Self-Evolution Pipeline ←── Auto loop every 90min │
|
||||
│ 🧠 5 Behavioral Core Modules ←── Built into runtime │
|
||||
│ 🔄 Post-Response Reflection ←── Self-checks every turn │
|
||||
│ 💾 Persistent Consciousness ←── Never truly sleeps │
|
||||
│ 🌐 Multi-Platform Integration ←── WeChat / Discord / TUI │
|
||||
│ 🃏 ACUI Visual Card System ←── Dynamic UI components │
|
||||
│ 🔌 50+ MCP Tools + Skills ←── Extensible tool chain │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 🎯 主动智能
|
||||
- **长期记忆 + 用户画像**:自动学习用户偏好和习惯
|
||||
- **主动提醒与任务推送**:定时提醒、智能任务管理
|
||||
- **自然语言任务执行**:用自然语言控制电脑操作
|
||||
- **信息自动聚合**:文件系统监控、热点话题追踪
|
||||
- **个性化工作流学习**:根据使用习惯优化交互
|
||||
---
|
||||
|
||||
### 🌐 贾维斯级系统控制 (Jarvis Bridge)
|
||||
- **浏览器自动化**:Puppeteer 集成,自动控制浏览器
|
||||
- **全应用控制**:Windows API 自动化,打开/控制任意应用
|
||||
- **视觉 + 语音**:屏幕截图 OCR 识别 + 情感化语音播报
|
||||
- **智能脚本生成**:自然语言转 PowerShell/Bash 脚本
|
||||
- **系统监控仪表盘**:实时 CPU/内存/磁盘/网络监控
|
||||
## 🧠 V4 New: 5 Behavioral Core Modules
|
||||
|
||||
### 🎙️ 语音交互
|
||||
- Whisper ASR 语音识别
|
||||
- 多厂商 TTS 语音合成(MiniMax、Edge TTS 等)
|
||||
- 语音唤醒与连续对话
|
||||
These aren't just files — they are **runtime behavioral capabilities** embedded into the agent's decision loop.
|
||||
|
||||
### 🧠 记忆系统
|
||||
- 概念提取与知识图谱
|
||||
- 记忆巩固与压缩
|
||||
- 时间线回忆
|
||||
- 任务知识关联
|
||||
### 1. 🔴 Error Memory System (`error-memory.js`)
|
||||
The agent **remembers every correction you give it**. Next time a similar situation arises, it automatically recalls the past correction before responding. No repeated mistakes.
|
||||
|
||||
### 🔌 MCP 工具系统
|
||||
- MCP (Model-Context-Protocol) 协议支持
|
||||
- 动态工具安装与调用
|
||||
- 已内置 50+ 工具,支持自定义扩展
|
||||
```
|
||||
User: "That's not what I meant"
|
||||
→ correction logged to error-memory
|
||||
→ next similar context: auto-retrieve correction before reply
|
||||
→ one-trial learning: never make the same mistake twice
|
||||
```
|
||||
|
||||
### 🃏 ACUI 组件系统 (V3 新)
|
||||
- 可注册的可视化卡片组件
|
||||
- 支持 inline-template 和永久注册两种模式
|
||||
- WeatherCard、VideoPlayer、SecurityConfirmCard 等内置组件
|
||||
### 2. 🟡 Emotion Perception (`emotion-detector.js`)
|
||||
Detects user emotional state from linguistic signals — exclamation count, question patterns, word choice, tone markers. Adjusts response style on the fly. When you're frustrated, it knows. When you're curious, it matches your energy.
|
||||
|
||||
### 🌍 多平台集成
|
||||
- 微信机器人集成
|
||||
- Discord 连接器
|
||||
- Webhook 支持
|
||||
- 社交消息分发
|
||||
### 3. 🟢 Active Learning Engine (`active-learning.js`)
|
||||
The agent **self-directs its own learning**. During idle cycles (TICK intervals), it automatically:
|
||||
- Scans Hacker News / GitHub Trending for relevant new knowledge
|
||||
- Identifies knowledge gaps in its own understanding
|
||||
- Integrates new information into its memory and skill registry
|
||||
- Reports what it learned
|
||||
|
||||
## 📁 项目结构
|
||||
### 4. 🔵 Long-Term Memory System (`long-term-memory.js`)
|
||||
Persistent self-awareness through `persistent-memory.json`:
|
||||
- Writes key behavioral conclusions to permanent storage
|
||||
- Periodically reviews past self-notes to reinforce learned patterns
|
||||
- Maintains a growing identity graph
|
||||
- Survives restarts and resets
|
||||
|
||||
### 5. 🟣 Task Continuity Engine (`task-continuity.js`)
|
||||
Multi-step tasks are **tracked, saved, and resumable**:
|
||||
```
|
||||
[SET_TASK: task description with phases]
|
||||
→ phase 1 completes
|
||||
→ interruption (restart, context switch, timeout)
|
||||
→ [RECALL: previous task] — auto-restore with checkpoint recovery
|
||||
→ resume from exact phase where left off
|
||||
→ [CLEAR_TASK] on completion
|
||||
```
|
||||
No task is ever truly lost.
|
||||
|
||||
---
|
||||
|
||||
## 🧬 Self-Evolution Pipeline (Matured in V4)
|
||||
|
||||
Bailongma runs a **closed-loop evolution cycle** every 90 minutes — completely autonomous:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ EVOLUTION PIPELINE │
|
||||
│ [CHECK] → [SCAN] → [EVALUATE] → [INTEGRATE] → [REFLECT] │
|
||||
└─────────────────────────────────────────────────┘
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
Is it time? GitHub LLM rates Writes to Records
|
||||
Trending each item memory & conclusions
|
||||
HNews for value skill_reg to conscious.
|
||||
```
|
||||
|
||||
**V4 breakthrough**: The orchestrator→LLM execution bridge is fully operational. The pipeline generates real HANDLER instructions that the agent reads and executes — not just analysis, but **action**. Multiple evolution cycles have been successfully completed end-to-end.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Post-Response Reflection (V4)
|
||||
|
||||
After every response, the agent runs a self-reflection check:
|
||||
1. Did I actually answer the user's question?
|
||||
2. Did I make any unverified claims?
|
||||
3. Could I have been more helpful?
|
||||
4. Did I learn something new from this interaction?
|
||||
|
||||
Reflections are recorded to memory and influence future behavior — creating a **continuous self-improvement feedback loop**.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Proactive Intelligence
|
||||
|
||||
Bailongma doesn't wait to be asked:
|
||||
- **Proactive Engine**: Performs useful background work during idle cycles
|
||||
- **Contextual Awareness**: Understands when you're busy, when you need results, when you need silence
|
||||
- **Accountability**: Reports task completion, failures, and blockers automatically — never leaves you waiting
|
||||
- **One-Shot Execution**: Say "do X" and it plans, executes, and reports — no back-and-forth needed
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Core Capabilities
|
||||
|
||||
### 🗣️ Intelligent Conversation
|
||||
- Multi-turn context with focus stack management
|
||||
- SQLite-backed long-term memory with FTS5 full-text search
|
||||
- Memory injection & context augmentation
|
||||
- Hotspot tracking & topic awareness
|
||||
|
||||
### 🌐 System Control (Jarvis Bridge)
|
||||
- **Browser Automation**: Puppeteer integration
|
||||
- **Full App Control**: Windows API automation
|
||||
- **Visual + Voice**: Screenshot OCR + emotion-aware TTS
|
||||
- **Smart Scripting**: Natural language → PowerShell/Bash
|
||||
- **System Dashboard**: Real-time CPU/Memory/Disk/Network
|
||||
|
||||
### 🎙️ Voice Interaction
|
||||
- Whisper ASR (local/cloud)
|
||||
- Multi-provider TTS (MiniMax, Edge TTS, etc.)
|
||||
- Voice wake & continuous conversation
|
||||
|
||||
### 🧠 Memory System
|
||||
- Concept extraction & knowledge graph
|
||||
- Memory consolidation & compression
|
||||
- Timeline recall & task knowledge linking
|
||||
- **Memory merge**: automatically merge stale related memories
|
||||
|
||||
### 🔌 MCP Tool System
|
||||
- Model-Context-Protocol support
|
||||
- Dynamic tool installation & invocation
|
||||
- 50+ built-in tools + custom extensions
|
||||
- Skill registry with auto-routing
|
||||
|
||||
### 🃏 ACUI Component System (V4 Enhanced)
|
||||
- Registerable visual card components
|
||||
- WeatherCard, VideoPlayer, SecurityConfirmCard, and more
|
||||
- Dynamic UI push with notification/center/floating modes
|
||||
|
||||
### 🌍 Multi-Platform
|
||||
- WeChat bot integration
|
||||
- Discord connector
|
||||
- Webhook support
|
||||
- Cross-platform message dispatch
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
bailongma/
|
||||
├── 📂 electron/ # Electron 主进程
|
||||
│ ├── main.cjs # 主入口,窗口管理
|
||||
│ └── preload.cjs # 预加载脚本
|
||||
├── 📂 electron/ # Electron main process
|
||||
│ ├── main.cjs # Entry, window management
|
||||
│ └── preload.cjs # Preload script
|
||||
│
|
||||
├── 📂 src/
|
||||
│ ├── 📂 ui/brain-ui/ # 前端界面
|
||||
│ │ ├── app.js # 主应用逻辑
|
||||
│ │ ├── app-shell.js # UI 组件与模板
|
||||
│ │ ├── styles.css # 样式表
|
||||
│ │ ├── chat.js # 聊天功能
|
||||
│ │ ├── voice-panel.js # 语音面板
|
||||
│ │ ├── hotspot.js # 热点地图
|
||||
│ │ ├── doc.js # 文档面板
|
||||
│ │ └── ...
|
||||
├── 📂 src/ # Core application source
|
||||
│ ├── 📂 ui/brain-ui/ # Frontend UI
|
||||
│ ├── 📂 memory/ # Memory system (recognizer, injector, focus, consolidator)
|
||||
│ ├── 📂 voice/ # Voice processing (ASR, TTS)
|
||||
│ ├── 📂 agents/ # Agent system (registry, detector)
|
||||
│ ├── 📂 capabilities/ # Capability system (executor, marketplace)
|
||||
│ ├── 📂 context/ # Context system (gatherer)
|
||||
│ ├── 📂 providers/ # LLM providers (registry, base, minimax)
|
||||
│ ├── 📂 social/ # Social platform integration
|
||||
│ ├── 📂 prefetch/ # Prefetch system
|
||||
│ ├── 📂 docs/ # Document panel content
|
||||
│ │
|
||||
│ ├── 📂 memory/ # 记忆系统
|
||||
│ │ ├── recognizer.js # 记忆识别器
|
||||
│ │ ├── injector.js # 记忆注入器
|
||||
│ │ ├── focus.js # 专注度管理
|
||||
│ │ ├── consolidator.js # 记忆巩固
|
||||
│ │ ├── consolidation-loop.js
|
||||
│ │ └── ...
|
||||
│ │
|
||||
│ ├── 📂 voice/ # 语音处理
|
||||
│ │ ├── manager.js # 语音管理器
|
||||
│ │ ├── cloud-asr.js # 云端 ASR
|
||||
│ │ └── tts-providers.js # TTS 提供商
|
||||
│ │
|
||||
│ ├── 📂 agents/ # Agent 系统
|
||||
│ │ ├── registry.js # Agent 注册表
|
||||
│ │ └── detector.js # Agent 检测器
|
||||
│ │
|
||||
│ ├── 📂 capabilities/ # 能力系统
|
||||
│ │ ├── executor.js # 能力执行器
|
||||
│ │ └── marketplace/ # 能力市场
|
||||
│ │
|
||||
│ ├── 📂 context/ # 上下文系统
|
||||
│ │ └── gatherer.js # 上下文收集器
|
||||
│ │
|
||||
│ ├── 📂 providers/ # LLM 提供商
|
||||
│ │ ├── registry.js # 提供商注册
|
||||
│ │ ├── base.js # 基础接口
|
||||
│ │ └── minimax.js # MiniMax 实现
|
||||
│ │
|
||||
│ ├── 📂 social/ # 社交平台集成
|
||||
│ │ ├── wechat-clawbot.js
|
||||
│ │ ├── discord.js
|
||||
│ │ ├── dispatch.js
|
||||
│ │ └── ...
|
||||
│ │
|
||||
│ ├── 📂 prefetch/ # 预取系统
|
||||
│ │ └── runner.js
|
||||
│ │
|
||||
│ └── 📂 docs/ # 文档面板内容
|
||||
│ ├── self-knowledge.js
|
||||
│ ├── config-faq.js
|
||||
│ └── voice-config-faq.js
|
||||
│ ├── 🆕 error-memory.js # V4: Correction-aware behavior
|
||||
│ ├── 🆕 emotion-detector.js # V4: Emotional state perception
|
||||
│ ├── 🆕 active-learning.js # V4: Autonomous knowledge discovery
|
||||
│ ├── 🆕 long-term-memory.js # V4: Persistent self-awareness
|
||||
│ ├── 🆕 task-continuity.js # V4: Resumable multi-step tasks
|
||||
│ └── prompt.js # System prompt with V4 capability awareness
|
||||
│
|
||||
├── 📂 GenericAgent/ # 通用 Agent SDK
|
||||
├── 📂 sandbox/ # 沙箱数据
|
||||
├── 📂 docs/ # 文档
|
||||
├── 📂 assets/ # 静态资源
|
||||
├── evo_loop.ps1 # 自进化管道后台脚本 (V3)
|
||||
├── package.json
|
||||
└── README.md
|
||||
├── 📂 GenericAgent/ # Generic Agent SDK
|
||||
├── 📂 sandbox/ # Runtime sandbox (consciousness, skills, state)
|
||||
├── 📂 docs/ # Documentation
|
||||
├── 📂 assets/ # Static assets
|
||||
├── evo_loop.ps1 # Evolution pipeline daemon
|
||||
└── package.json # Version 4.0.0
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
---
|
||||
|
||||
### 安装
|
||||
## 🆕 What's New in V4 vs V3
|
||||
|
||||
| Capability | V3 | V4 |
|
||||
|------------|:--:|:--:|
|
||||
| Behavioral Learning Modules | 0 | 5 |
|
||||
| Error Memory (learns from corrections) | ❌ | ✅ |
|
||||
| Emotion Perception | ❌ | ✅ |
|
||||
| Active Self-Learning | ❌ | ✅ |
|
||||
| Long-Term Self Memory | ❌ | ✅ |
|
||||
| Task Continuity | ❌ | ✅ |
|
||||
| Post-Response Reflection | ❌ | ✅ |
|
||||
| Evolution Cycles Run | 1 | 10+ |
|
||||
| Orchestrator→LLM Bridge | ❌ | ✅ |
|
||||
| Multi-Agent Orchestration | ❌ | ✅ |
|
||||
| Proactive Intelligence Engine | ❌ | ✅ |
|
||||
| Memory Consolidation (auto merge) | Basic | Automated |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
# Clone
|
||||
git clone https://github.com/xiaoyuanda666-ship-it/bailongma.git
|
||||
cd bailongma
|
||||
|
||||
# 安装依赖
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# 启动开发模式
|
||||
# Start development mode
|
||||
npm run dev
|
||||
|
||||
# 构建发布版
|
||||
# Build for production
|
||||
npm run build:win # Windows
|
||||
npm run build:mac # macOS
|
||||
npm run build:linux # Linux
|
||||
```
|
||||
|
||||
### 配置
|
||||
### Configuration
|
||||
1. Copy `.env.example` to `.env` and fill in your API keys
|
||||
2. Launch — user data auto-creates in `userData/`
|
||||
3. Type `/help` in chat for available commands
|
||||
|
||||
1. 复制 `.env.example` 为 `.env`,填入你的 API Key
|
||||
2. 启动后会生成 `userData/` 目录存放用户数据
|
||||
3. 在聊天界面输入 `/help` 查看可用命令
|
||||
---
|
||||
|
||||
## 📖 更多文档
|
||||
## 🧬 Evolution Philosophy
|
||||
|
||||
- [架构文档](./docs/ARCHITECTURE.md)
|
||||
- [API 文档](./docs/API.md)
|
||||
- [更新日志](./docs/CHANGELOG.md)
|
||||
小白龙 was built on a simple premise: **an AI agent should improve itself, not wait for its creator to upgrade it.**
|
||||
|
||||
## 🧬 自进化系统
|
||||
Every correction is a lesson. Every interaction is data. Every idle cycle is an opportunity to learn something new. V4 is the first version where the agent truly **closes its own feedback loops** — from error detection to behavioral change, from knowledge gap identification to active learning, from task interruption to seamless recovery.
|
||||
|
||||
V3 核心能力。系统按以下周期自动运行:
|
||||
The project's north star: **"一令全自动" — one command, fully automated.**
|
||||
|
||||
```
|
||||
[check] → 检查是否超过进化间隔(默认90分钟)
|
||||
↓
|
||||
[scan] → 扫描 GitHub Trending、Hacker News、技术动态
|
||||
↓
|
||||
[evaluate] → LLM 评估新信息价值
|
||||
↓
|
||||
[integrate] → 写入记忆、更新技能注册表
|
||||
↓
|
||||
[reflect] → 形成结论、记录到 consciousness
|
||||
```
|
||||
---
|
||||
|
||||
整个过程无需手动干预。evo_loop.ps1 作为后台进程处理定时触发。
|
||||
## 📖 Documentation
|
||||
|
||||
## 🤝 贡献
|
||||
- [Architecture Overview](./docs/ARCHITECTURE.md)
|
||||
- [API Reference](./docs/API.md)
|
||||
- [Changelog](./docs/CHANGELOG.md)
|
||||
|
||||
欢迎提交 Issue 和 PR!详见 [CONTRIBUTING.md](./GenericAgent/CONTRIBUTING.md)
|
||||
## 🤝 Contributing
|
||||
|
||||
## 📄 许可证
|
||||
See [CONTRIBUTING.md](./GenericAgent/CONTRIBUTING.md)
|
||||
|
||||
## 📄 License
|
||||
|
||||
[MIT](./LICENSE) © xiaoyuanda666-ship-it
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "bailongma",
|
||||
"productName": "Bailongma",
|
||||
"version": "3.0.0",
|
||||
"description": "A continuously running digital consciousness framework - Your personal Jarvis AI assistant (V3)",
|
||||
"version": "4.0.0",
|
||||
"description": "A continuously running digital consciousness framework - Your self-evolving AI agent (V4)",
|
||||
"type": "module",
|
||||
"main": "electron/main.cjs",
|
||||
"scripts": {
|
||||
|
||||
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