feat: 插件-工具合并 — 创建 pkg/plugins 共享模块并移除 tool-engine
- 新增 backend/pkg/plugins/ 共享模块:SDK 接口、PluginManager、ToolRegistry(含环形缓冲区调用日志) - 13 个通用插件从 plugin-manager 迁移至共享模块(import 路径统一) - ai-core 切换至共享 ToolRegistry,进程内执行(零网络开销),包装 6 个专属工具 - plugin-manager 迁移至共享模块,保留管理 REST API - 新增 DevTools 插件管理面板(侧边栏 → 🔌 插件管理) - 移除 tool-engine 服务(从 go.work、DevTools 配置、编译系统) - 工具调用记录 API 从 Tool-Engine 迁至 AI-Core(/api/v1/tools/calls) - ai-core ContextStore 启动时从 PostgreSQL 恢复会话历史 - 清理所有过时引用和备份文件 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,9 @@ import (
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/tools"
|
||||
|
||||
plgManager "github.com/yourname/cyrene-ai/pkg/plugins/manager"
|
||||
plgSDK "github.com/yourname/cyrene-ai/pkg/plugins/sdk"
|
||||
)
|
||||
|
||||
// PendingThought 待推送的后台思考
|
||||
@@ -50,7 +53,7 @@ type Thinker struct {
|
||||
memoryStore *memory.Store
|
||||
|
||||
// 工具调用
|
||||
toolRegistry *tools.Registry
|
||||
toolRegistry *plgManager.ToolRegistry
|
||||
|
||||
// 会话上下文
|
||||
convStore *ctxbuild.ConversationStore
|
||||
@@ -115,6 +118,9 @@ type Thinker struct {
|
||||
// Phase 2: 主动消息决策守卫
|
||||
proactiveGuard *ProactiveGuard
|
||||
|
||||
// 动态调度: 按时间段自动调整思考间隔
|
||||
scheduleLoader *ScheduleLoader
|
||||
|
||||
// Phase 2: 在线状态追踪
|
||||
userOnline bool
|
||||
lastOnlineChange time.Time
|
||||
@@ -149,6 +155,13 @@ func DefaultAutonomousToolPolicy() *AutonomousToolPolicy {
|
||||
}
|
||||
|
||||
// SetMessagePusher 设置主动消息推送回调
|
||||
// SetScheduleLoader sets the dynamic schedule loader for interval calculation.
|
||||
func (t *Thinker) SetScheduleLoader(loader *ScheduleLoader) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.scheduleLoader = loader
|
||||
}
|
||||
|
||||
func (t *Thinker) SetMessagePusher(pusher func(string, string, string)) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
@@ -228,7 +241,7 @@ func NewThinker(
|
||||
toolAdapter *llm.Adapter,
|
||||
iotClient *tools.IoTClient,
|
||||
memoryStore *memory.Store,
|
||||
toolRegistry *tools.Registry,
|
||||
toolRegistry *plgManager.ToolRegistry,
|
||||
convStore *ctxbuild.ConversationStore,
|
||||
adminUserID string,
|
||||
adminSessionID string,
|
||||
@@ -434,8 +447,8 @@ func (t *Thinker) resetSilenceTimer() {
|
||||
|
||||
// periodicThinkLoop 周期性自主思考循环
|
||||
//
|
||||
// 每隔 thinkInterval 触发一次思考,保证昔涟在无用户活动时也能持续进行后台反思。
|
||||
// 每次触发前检查 minThinkGap,避免与事件驱动思考冲突。
|
||||
// 使用动态间隔:若配置了 ScheduleLoader,每次循环根据当前时段计算间隔;
|
||||
// 否则回退到固定的 thinkInterval。
|
||||
func (t *Thinker) periodicThinkLoop() {
|
||||
defer t.wg.Done()
|
||||
defer func() {
|
||||
@@ -444,17 +457,22 @@ func (t *Thinker) periodicThinkLoop() {
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(t.thinkInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Printf("[后台思考] 周期性思考已启动 (间隔=%v)", t.thinkInterval)
|
||||
|
||||
for {
|
||||
// 计算本次等待间隔
|
||||
interval := t.thinkInterval
|
||||
if t.scheduleLoader != nil {
|
||||
if mins := t.scheduleLoader.GetInterval(time.Now()); mins > 0 {
|
||||
interval = time.Duration(mins) * time.Minute
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-t.stopCh:
|
||||
log.Println("[后台思考] 周期性思考已停止")
|
||||
return
|
||||
case <-ticker.C:
|
||||
case <-time.After(interval):
|
||||
t.mu.Lock()
|
||||
sinceLastThink := time.Since(t.lastThinkTime)
|
||||
sinceLastUser := time.Since(t.lastUserMessage)
|
||||
@@ -482,7 +500,7 @@ func (t *Thinker) periodicThinkLoop() {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[后台思考] 周期性触发 (上次思考=%v前, 上次用户消息=%v前)", sinceLastThink.Round(time.Second), sinceLastUser.Round(time.Second))
|
||||
log.Printf("[后台思考] 周期性触发 (间隔=%v, 上次思考=%v前, 上次用户消息=%v前)", interval, sinceLastThink.Round(time.Second), sinceLastUser.Round(time.Second))
|
||||
t.performThink("periodic")
|
||||
}
|
||||
}
|
||||
@@ -653,6 +671,9 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
if execErr != nil {
|
||||
log.Printf("[后台思考] 工具 %s 执行失败: %v", tc.Name, execErr)
|
||||
}
|
||||
if result == nil {
|
||||
result = &plgSDK.ToolResult{ToolName: tc.Name, Success: false, Error: execErr.Error()}
|
||||
}
|
||||
|
||||
resultJSON, _ := json.Marshal(result)
|
||||
messages = append(messages, model.LLMMessage{
|
||||
@@ -741,7 +762,7 @@ func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig
|
||||
- 开拓者说要离开一会儿、去忙、去吃饭
|
||||
- 开拓者明确表示不想被打扰
|
||||
- 对话刚刚自然结束且开拓者没有未完成的事
|
||||
如果对话历史显示以上任何情况,反思中不要写【主动消息】标记。你可以在心里想想他,但不要去打扰。`
|
||||
如果对话历史显示以上任何情况,你只需要在心里默默陪伴,不要输出任何【主动消息】指令行。`
|
||||
|
||||
switch triggerReason {
|
||||
case "post_chat":
|
||||
@@ -768,9 +789,10 @@ func (t *Thinker) buildThinkingSystemPrompt(personaConfig *persona.PersonaConfig
|
||||
|
||||
其他规则:
|
||||
1. 反思部分用第三人称或自言自语的方式,不要直接对开拓者喊话。
|
||||
2. 只有开拓者状态正常且真的有必要时才写【主动消息】,不要硬找话题。
|
||||
3. 【主动消息】的内容必须直接对开拓者说话(用"你"称呼他),像主动找他聊天一样。反思是给自己看的,主动消息是发给他的——语气要区分开。
|
||||
4. 2-4句话即可。`
|
||||
2. 只有开拓者状态正常且真的有必要时,才在独立一行写【主动消息】标记,后面跟你要发给他的话。不要硬找话题。
|
||||
3. 【主动消息】标记必须独占一行开头,内容直接对开拓者说话(用"你"称呼他),像主动找他聊天一样。
|
||||
4. 如果你在反思中提到"主动消息"这个词但不打算发消息,不要使用【主动消息】这个带括号的标记——系统会误解析。
|
||||
5. 2-4句话即可。`
|
||||
|
||||
case "silence":
|
||||
thinkingInstructions = `
|
||||
@@ -840,6 +862,14 @@ func (t *Thinker) buildThinkingUserPrompt(
|
||||
) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// 注入当前现实时间,让模型对时间有感知
|
||||
now := time.Now()
|
||||
weekdayNames := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
|
||||
sb.WriteString(fmt.Sprintf("🕐 现在是 %s %s %02d:%02d。\n",
|
||||
now.Format("2006年1月2日"),
|
||||
weekdayNames[now.Weekday()],
|
||||
now.Hour(), now.Minute()))
|
||||
|
||||
// 根据触发原因使用不同的开场白
|
||||
switch triggerReason {
|
||||
case "post_chat":
|
||||
@@ -886,7 +916,7 @@ func (t *Thinker) buildThinkingUserPrompt(
|
||||
|
||||
// 关键:强调根据对话历史判断用户当前状态
|
||||
if lastUserMsg != "" {
|
||||
sb.WriteString(fmt.Sprintf("\n🔍 **重要**:开拓者最后说的是「%s」。请认真判断:他现在是不是在休息/睡觉/忙?如果是,反思中不要写【主动消息】。\n", lastUserMsg))
|
||||
sb.WriteString(fmt.Sprintf("\n🔍 **重要**:开拓者最后说的是「%s」。请认真判断:他现在是不是在休息/睡觉/忙?如果是,不要输出【主动消息】指令行。\n", lastUserMsg))
|
||||
}
|
||||
|
||||
// 现有记忆
|
||||
@@ -930,9 +960,9 @@ func (t *Thinker) buildThinkingUserPrompt(
|
||||
// 结尾引导
|
||||
sb.WriteString("\n---\n现在请写下你的私人反思。")
|
||||
sb.WriteString("\n记住:这是日记,用第三人称或自言自语的方式。")
|
||||
sb.WriteString("\n⚠️ 如果开拓者正在休息/睡觉/忙碌——不要写【主动消息】。你可以在心里想他,但不要去打扰。")
|
||||
sb.WriteString("\n只有在你确认他现在是醒着、有空、且真的需要关心时,才写【主动消息】。")
|
||||
sb.WriteString("\n❗【主动消息】的内容必须直接对开拓者说话(用\"你\"来称呼他),就像你主动找他聊天一样自然。不要用第三人称或自言自语的方式写主动消息。")
|
||||
sb.WriteString("\n⚠️ 如果开拓者正在休息/睡觉/忙碌——不要输出【主动消息】指令行。你可以在心里想他,但不要去打扰。")
|
||||
sb.WriteString("\n只有在你确认他现在是醒着、有空、且真的需要关心时,才输出一行【主动消息】+ 你要发给他的话。")
|
||||
sb.WriteString("\n❗【主动消息】标记必须独占一行开头,后面紧跟你要对开拓者说的话(用\"你\"称呼),语气自然像主动找他聊天。不要在反思正文中提及\"主动消息\"这个词——如果需要表达这个意思但又不打算发消息,用别的词代替。")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -963,7 +993,7 @@ func (t *Thinker) buildOpenAITools() []llm.OpenAITool {
|
||||
if t.toolRegistry == nil || !t.toolRegistry.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
defs := t.toolRegistry.GetDefinitions()
|
||||
defs := t.toolRegistry.Definitions()
|
||||
if len(defs) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -1066,29 +1096,52 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
|
||||
}
|
||||
}
|
||||
|
||||
// extractProactiveMessage 从思考内容中提取【主动消息】标记的内容
|
||||
// 返回空字符串表示没有主动消息
|
||||
// extractProactiveMessage 从思考内容中提取【主动消息】标记的内容。
|
||||
// 返回空字符串表示没有主动消息。
|
||||
//
|
||||
// 要求标记独立成行(前面只有空白或行首),避免把自然语言中的提及
|
||||
// 当作指令(如 "不需要写【主动消息】" 这类否定表述)。
|
||||
func extractProactiveMessage(content string) string {
|
||||
marker := "【主动消息】"
|
||||
idx := strings.Index(content, marker)
|
||||
if idx < 0 {
|
||||
return ""
|
||||
|
||||
// 扫描每一行,只接受 marker 在行首(忽略前导空白)的行作为指令
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, marker) {
|
||||
continue
|
||||
}
|
||||
// 检查否定语境:标记前面的文字包含否定词
|
||||
markerIdx := strings.Index(line, marker)
|
||||
prefix := strings.TrimSpace(line[:markerIdx])
|
||||
if containsNegation(prefix) {
|
||||
continue
|
||||
}
|
||||
// 提取标记后的内容
|
||||
msg := strings.TrimSpace(trimmed[len(marker):])
|
||||
if msg == "" {
|
||||
continue
|
||||
}
|
||||
// 限制主动消息长度(最多 200 字符,保持简短)
|
||||
runes := []rune(msg)
|
||||
if len(runes) > 200 {
|
||||
msg = string(runes[:200])
|
||||
}
|
||||
return msg
|
||||
}
|
||||
// 提取标记后的内容(到下一个标记或结尾)
|
||||
msg := strings.TrimSpace(content[idx+len(marker):])
|
||||
// 截断到下一个【或换行之前的合理长度
|
||||
if endIdx := strings.Index(msg, "【"); endIdx > 0 {
|
||||
msg = strings.TrimSpace(msg[:endIdx])
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// containsNegation checks if a short prefix string contains negation words
|
||||
// that would nullify the 【主动消息】directive.
|
||||
func containsNegation(prefix string) bool {
|
||||
negations := []string{"不", "别", "不要", "不需要", "不用", "别写", "没", "没有"}
|
||||
for _, n := range negations {
|
||||
if strings.Contains(prefix, n) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 限制主动消息长度(最多 200 字符,保持简短)
|
||||
runes := []rune(msg)
|
||||
if len(runes) > 200 {
|
||||
msg = string(runes[:200])
|
||||
}
|
||||
if msg == "" {
|
||||
return ""
|
||||
}
|
||||
return msg
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user