@@ -22,19 +22,23 @@ import (
type StreamEventType string
const (
StreamEventTypeThought StreamEventType = "thought" // LLM 思考过程
StreamEventTypeToolCall StreamEventType = "tool_call" // 工具调用请求
StreamEventTypeToolResult StreamEventType = "tool_result" // 工具执行结果
StreamEventTypeFinal StreamEventType = "final" // 最终答案
StreamEventTypeError StreamEventType = "error" // 错误信息
StreamEventTypeThought StreamEventType = "thought" // LLM 思考过程
StreamEventTypeToolCall StreamEventType = "tool_call" // 工具调用请求
StreamEventTypeToolResult StreamEventType = "tool_result" // 工具执行结果
StreamEventTypeFinal StreamEventType = "final" // 最终答案
StreamEventTypeError StreamEventType = "error" // 错误信息
StreamEventTypeWorkspaceStart StreamEventType = "workspace_start" // 工具渲染开始
StreamEventTypeWorkspaceDelta StreamEventType = "workspace_delta" // 工具渲染增量内容
StreamEventTypeWorkspaceEnd StreamEventType = "workspace_end" // 工具渲染结束
)
// StreamEvent 代表流式输出中的一个事件
type StreamEvent struct {
Type StreamEventType ` json:"type" `
Content string ` json:"content" `
Step int ` json:"step,omitempty" `
ToolName string ` json:"tool_name,omitempty" `
Type StreamEventType ` json:"type" `
Content string ` json:"content" `
Step int ` json:"step,omitempty" `
ToolName string ` json:"tool_name,omitempty" `
WorkspaceTitle string ` json:"workspace_title,omitempty" `
}
// StreamEventCallback 是流式事件回调函数类型,用于推送事件到客户端
@@ -42,23 +46,26 @@ type StreamEventCallback func(event StreamEvent) error
// Orchestrator 负责协调和组合业务逻辑,包含 LLM 计算、上下文管理、技能匹配计算和工具调用。
type Orchestrator struct {
llm llm . Client
routerLLM llm . Client // 可选:轻量路由模型,用于技能意图路由;为 nil 则仅用关键词匹配
store * memory . SQLiteStore
tools * tools . Registry
soul string
skills [ ] knowledge . Skill
skillSummaries [ ] knowledge . SkillSummary
skillsDir string
autoSkillDir string
gapDraftTriggerCount int
gapLookbackDuration time . Duration
reactMaxStep int
enableCapabilityGap bool
log * logger . Logger
skillsMu sync . RWMutex
pendingFilesMu sync . Mutex
pendingFiles map [ string ] [ ] pendingFileRef
llm llm . Client
routerLLM llm . Client // 可选:轻量路由模型,用于技能意图路由;为 nil 则仅用关键词匹配
store * memory . SQLiteStore
tools * tools . Registry
soul string
skills [ ] knowledge . Skill
skillSummaries [ ] knowledge . SkillSummary
skillsDir string
autoSkillDir string
gapDraftTriggerCount int
gapLookbackDuration time . Duration
reactMaxStep int
enableCapabilityGap bool
autoSkillDraftEnabled bool
log * logger . Logger
skillsMu sync . RW Mutex
pendingFilesMu sync . Mutex
pendingFiles map [ string ] [ ] pendingFileRef
planningSessionsMu sync . Mutex
planningSessions map [ string ] planningSessionState
}
type pendingFileRef struct {
@@ -67,6 +74,15 @@ type pendingFileRef struct {
MimeType string
}
type planningSessionState struct {
Active bool
LastArtifact string
AwaitingConfirm bool
UpdatedAt time . Time
}
const planningSessionTTL = 2 * time . Hour
// NewOrchestrator 创建一个新的编排器对象,初始化关键路径和超时控制等。
func NewOrchestrator (
llmClient llm . Client ,
@@ -97,21 +113,23 @@ func NewOrchestrator(
autoSkillDir = skillsDir
}
return & Orchestrator {
llm : llmClient ,
routerLLM : routerLLM ,
store : store ,
tools : registry ,
soul : soul ,
skills : skills ,
skillSummaries : copySkillSummaries ( skillSummaries ) ,
skillsDir : skillsDir ,
autoSkillDir : autoSkillDir ,
gapDraftTriggerCount : gapDraftTriggerCount ,
gapLookbackDuration : gapLookbackDuration ,
reactMaxStep : reactMaxStep ,
enableCapabilityGap : enableCapabilityGap ,
log : log ,
pendingFiles : make ( map [ string ] [ ] pendingFileRef ) ,
llm : llmClient ,
routerLLM : routerLLM ,
store : store ,
tools : registry ,
soul : soul ,
skills : skills ,
skillSummaries : copySkillSummaries ( skillSummaries ) ,
skillsDir : skillsDir ,
autoSkillDir : autoSkillDir ,
gapDraftTriggerCount : gapDraftTriggerCount ,
gapLookbackDuration : gapLookbackDuration ,
reactMaxStep : reactMaxStep ,
enableCapabilityGap : enableCapabilityGap ,
autoSkillDraftEnabled : false ,
log : log ,
pendingFiles : make ( map [ string ] [ ] pendingFileRef ) ,
planningSessions : make ( map [ string ] planningSessionState ) ,
}
}
@@ -166,6 +184,11 @@ func (o *Orchestrator) HandleMessageStreamWithFiles(ctx context.Context, chatID,
return o . handleMessageStreamInternal ( ctx , chatID , userID , text , callback )
}
// GetHistory 获取指定会话的历史记录。
func ( o * Orchestrator ) GetHistory ( chatID string , limit int ) ( [ ] memory . Message , error ) {
return o . store . LoadRecent ( chatID , limit )
}
// UploadAndCacheFiles 上传文件到 LLM 并缓存 file_id, 供后续同会话文本问答复用。
func ( o * Orchestrator ) UploadAndCacheFiles ( ctx context . Context , chatID , userID string , files [ ] llm . InputFile ) ( [ ] string , error ) {
if len ( files ) == 0 {
@@ -342,7 +365,7 @@ func (o *Orchestrator) handleMessageStreamInternal(ctx context.Context, chatID,
// buildUnifiedSystemPrompt 构建统一 ReAct 循环的 system prompt。
// 工具定义通过 API 的 tools 字段传递;此处只需包含人格、技能、运行环境和思考指引。
// routedSkills 为 LLM 路由预选的技能列表;如果为 nil, 则回退到关键词匹配。
func ( o * Orchestrator ) buildUnifiedSystemPrompt ( userInput string , routedSkills [ ] knowledge . Skill ) string {
func ( o * Orchestrator ) buildUnifiedSystemPrompt ( userInput string , recentMessages [ ] memory . Message , routedSkills [ ] knowledge . Skill , planningMode bool ) string {
skillMetaDoc := o . formatSkillSummariesForPrompt ( )
var relevantSkillsDoc string
if routedSkills != nil {
@@ -352,6 +375,18 @@ func (o *Orchestrator) buildUnifiedSystemPrompt(userInput string, routedSkills [
}
runtimeDoc := formatRuntimeContextForPrompt ( )
// 提取最近的 Artifact
artifactDoc := extractLastArtifact ( recentMessages )
planningModeDoc := "当前未处于 PI 规划编辑模式。"
if planningMode {
planningModeDoc = strings . Join ( [ ] string {
"当前处于 PI 规划编辑模式。" ,
"- 用户的修订意见必须基于现有 Artifact 继续迭代。" ,
"- 需要继续调用 safe_pi_planning / publish_pi_plan 相关流程生成更新版本。" ,
"- 不要仅给普通文本答复替代蓝图更新。" ,
} , "\n" )
}
return strings . Join ( [ ] string {
"你是一个个人自动化助手,必须遵循如下人格设定并保持一致:" ,
o . soul ,
@@ -373,6 +408,12 @@ func (o *Orchestrator) buildUnifiedSystemPrompt(userInput string, routedSkills [
"===== 运行环境 =====" ,
runtimeDoc ,
"" ,
"===== 已有结果 (Artifact) =====" ,
artifactDoc ,
"" ,
"===== PI 规划模式 =====" ,
planningModeDoc ,
"" ,
"===== 可用技能概览 =====" ,
skillMetaDoc ,
"" ,
@@ -383,6 +424,21 @@ func (o *Orchestrator) buildUnifiedSystemPrompt(userInput string, routedSkills [
} , "\n" )
}
// extractLastArtifact 从消息历史中提取最后一个 Artifact
func extractLastArtifact ( messages [ ] memory . Message ) string {
for i := len ( messages ) - 1 ; i >= 0 ; i -- {
msg := messages [ i ]
if strings . Contains ( msg . Content , "<artifact" ) {
start := strings . Index ( msg . Content , "<artifact" )
end := strings . LastIndex ( msg . Content , "</artifact>" )
if end > start {
return "\n当前正在处理的 Artifact 内容如下:\n" + msg . Content [ start : end + 11 ] + "\n如果你需要更新此 Artifact, 请输出完整的更新后版本并确保包裹在 <artifact> 标签中。"
}
}
}
return "当前没有正在处理的 Artifact。"
}
// routeSkillsWithLLM 使用轻量 LLM 模型对用户输入进行语义路由,判断是否需要加载技能以及选择哪些技能。
// 返回匹配到的技能列表( 可能为空切片表示不需要技能, nil 表示调用失败应回退)。
func ( o * Orchestrator ) routeSkillsWithLLM ( ctx context . Context , userInput string ) ( [ ] knowledge . Skill , error ) {
@@ -497,9 +553,26 @@ func (o *Orchestrator) runUnifiedReAct(ctx context.Context, chatID, userID, comp
traceID := logger . TraceIDFromContext ( ctx )
traceLogPrefix := "trace_id=" + traceID
// ===== LLM 意图路由:使用轻量模型判断是否需要加载技能 =====
planningMode := false
if session , ok := o . getPlanningSession ( chatID , userID ) ; ok && session . Active {
if shouldExitPlanningMode ( userInput ) {
o . clearPlanningSession ( chatID , userID )
if o . log != nil {
o . log . Infof ( "%s planning mode exited chat_id=%s" , traceLogPrefix , chatID )
}
} else {
planningMode = true
}
}
// ===== LLM 意图路由:使用轻量模型判断是否需要加载技能(规划模式下使用粘性技能) =====
var routedSkills [ ] knowledge . Skill
if o . routerLLM != nil {
if planningMode {
routedSkills = o . getSafePIPlanningSkills ( )
if o . log != nil {
o . log . Infof ( "%s planning mode sticky skill activated chat_id=%s matched=%d" , traceLogPrefix , chatID , len ( routedSkills ) )
}
} else if o . routerLLM != nil {
routed , routeErr := o . routeSkillsWithLLM ( ctx , userInput )
if routeErr != nil {
if o . log != nil {
@@ -517,8 +590,12 @@ func (o *Orchestrator) runUnifiedReAct(ctx context.Context, chatID, userID, comp
}
}
}
if containsSafePIPlanningSkill ( routedSkills ) {
planningMode = true
o . activatePlanningSession ( chatID , userID , "" , false )
}
systemPrompt := o . buildUnifiedSystemPrompt ( userInput , routedSkills )
systemPrompt := o . buildUnifiedSystemPrompt ( userInput , nil , routedSkills , planningMode )
if o . log != nil {
o . log . Infof ( "%s unified react start" , traceLogPrefix )
@@ -544,6 +621,18 @@ func (o *Orchestrator) runUnifiedReAct(ctx context.Context, chatID, userID, comp
// 加入当前用户消息
messages = append ( messages , llm . PromptMessage { Role : "user" , Content : userInput } )
// 获取最近消息以提取 Artifact
recent , _ := o . store . LoadRecent ( chatID , 16 )
systemPrompt = o . buildUnifiedSystemPrompt ( userInput , recent , routedSkills , planningMode )
// 更新 system message
messages [ 0 ] . Content = systemPrompt
if o . log != nil {
o . log . Infof ( "%s unified react start" , traceLogPrefix )
o . log . Debugf ( "%s system_prompt_len=%d" , traceLogPrefix , len ( systemPrompt ) )
}
// 构建工具定义列表(通过 API tools 字段传递)
toolDefs := o . buildToolDefinitions ( )
if o . log != nil {
@@ -598,6 +687,16 @@ func (o *Orchestrator) runUnifiedReAct(ctx context.Context, chatID, userID, comp
for _ , tc := range completion . ToolCalls {
toolName := strings . ToLower ( strings . TrimSpace ( tc . Function . Name ) )
toolInput := extractToolInput ( tc . Function . Arguments )
if planningMode && toolName == "create_gitea_ticket" && ! isPlanningConfirmation ( userInput ) {
obs := formatToolErrorObservation ( "WAIT_USER_CONFIRM" , toolName , "需用户确认后才能创建工单,请先征求用户确认" )
messages = append ( messages , llm . PromptMessage {
Role : "tool" ,
ToolCallID : tc . ID ,
Name : tc . Function . Name ,
Content : obs ,
} )
continue
}
tool , ok := o . tools . Get ( toolName )
if ! ok {
@@ -624,6 +723,14 @@ func (o *Orchestrator) runUnifiedReAct(ctx context.Context, chatID, userID, comp
if obs == "" {
obs = "(empty output)"
}
if isPIPlanningToolName ( toolName ) && toolErr == nil && strings . TrimSpace ( obs ) != "" {
o . activatePlanningSession ( chatID , userID , obs , true )
if err := o . store . SaveMessage ( chatID , userID , "assistant" , wrapPIArtifact ( obs ) ) ; err != nil {
if o . log != nil {
o . log . Warnf ( "%s save planning artifact failed chat_id=%s err=%v" , traceLogPrefix , chatID , err )
}
}
}
if toolErr != nil {
obs = formatToolErrorObservation ( "TOOL_EXEC_ERROR" , toolName , toolErr . Error ( ) ) + "\nOUTPUT:\n" + obs
o . emitCapabilityGap ( chatID , userID , userInput , "tool_call_failed:" + toolName )
@@ -657,9 +764,26 @@ func (o *Orchestrator) runUnifiedReActStream(ctx context.Context, chatID, userID
traceID := logger . TraceIDFromContext ( ctx )
traceLogPrefix := "trace_id=" + traceID
// ===== LLM 意图路由:使用轻量模型判断是否需要加载技能 =====
planningMode := false
if session , ok := o . getPlanningSession ( chatID , userID ) ; ok && session . Active {
if shouldExitPlanningMode ( userInput ) {
o . clearPlanningSession ( chatID , userID )
if o . log != nil {
o . log . Infof ( "%s planning mode exited chat_id=%s" , traceLogPrefix , chatID )
}
} else {
planningMode = true
}
}
// ===== LLM 意图路由:使用轻量模型判断是否需要加载技能(规划模式下使用粘性技能) =====
var routedSkills [ ] knowledge . Skill
if o . routerLLM != nil {
if planningMode {
routedSkills = o . getSafePIPlanningSkills ( )
if o . log != nil {
o . log . Infof ( "%s planning mode sticky skill activated chat_id=%s matched=%d" , traceLogPrefix , chatID , len ( routedSkills ) )
}
} else if o . routerLLM != nil {
routed , routeErr := o . routeSkillsWithLLM ( ctx , userInput )
if routeErr != nil {
if o . log != nil {
@@ -676,8 +800,20 @@ func (o *Orchestrator) runUnifiedReActStream(ctx context.Context, chatID, userID
}
}
}
if containsSafePIPlanningSkill ( routedSkills ) {
planningMode = true
o . activatePlanningSession ( chatID , userID , "" , false )
}
routedToSafePIPlanning := false
for _ , sk := range routedSkills {
name := strings . ToLower ( strings . TrimSpace ( sk . Name ) )
if strings . Contains ( name , "safe" ) || strings . Contains ( name , "pi 规划" ) || strings . Contains ( name , "pi planning" ) {
routedToSafePIPlanning = true
break
}
}
systemPrompt := o . buildUnifiedSystemPrompt ( userInput , routedSkills )
systemPrompt := o . buildUnifiedSystemPrompt ( userInput , nil , routedSkills , planningMode )
if o . log != nil {
o . log . Infof ( "%s unified react stream start" , traceLogPrefix )
@@ -699,6 +835,18 @@ func (o *Orchestrator) runUnifiedReActStream(ctx context.Context, chatID, userID
messages = append ( messages , parseCompressedHistoryMessages ( compressedContext ) ... )
messages = append ( messages , llm . PromptMessage { Role : "user" , Content : userInput } )
// 获取最近消息以提取 Artifact
recent , _ := o . store . LoadRecent ( chatID , 16 )
systemPrompt = o . buildUnifiedSystemPrompt ( userInput , recent , routedSkills , planningMode )
// 更新 system message
messages [ 0 ] . Content = systemPrompt
if o . log != nil {
o . log . Infof ( "%s unified react stream start" , traceLogPrefix )
o . log . Debugf ( "%s system_prompt_len=%d" , traceLogPrefix , len ( systemPrompt ) )
}
// 构建工具定义列表
toolDefs := o . buildToolDefinitions ( )
if o . log != nil {
@@ -709,6 +857,7 @@ func (o *Orchestrator) runUnifiedReActStream(ctx context.Context, chatID, userID
o . log . Debugf ( "%s tool_defs_count=%d names=%v" , traceLogPrefix , len ( toolDefs ) , toolNames )
}
workspaceSentThisTurn := false
const maxSteps = 20
for step := 1 ; step <= maxSteps ; step ++ {
if o . log != nil {
@@ -729,17 +878,23 @@ func (o *Orchestrator) runUnifiedReActStream(ctx context.Context, chatID, userID
}
}
// 推送思考过程事件
// 推送思考过程事件(剥离 artifact 标签,避免前端重复渲染工作区)
if completion . Content != "" {
// 分割内容为逐步推送的片段
segments := splitContentIntoSegments ( completion . Content , 50 ) // 每段50字符
for _ , segm ent : = range segments {
if err := callback ( StreamEvent {
Type : StreamEventTypeThought ,
Content : segment ,
Step : step ,
} ) ; err ! = nil {
return "" , fmt . Errorf ( "callback error: %w" , err )
displayContent := completion . Content
if planningMode {
displayCont ent = stripArtifactTags ( displayContent )
}
if displayContent != "" {
// 分割内容为逐步推送的片段
segments := splitContentIntoSegments ( displayContent , 50 ) // 每段50字符
for _ , segment : = range segments {
if err := callback ( StreamEvent {
Type : StreamEventTypeThought ,
Content : segment ,
Step : step ,
} ) ; err != nil {
return "" , fmt . Errorf ( "callback error: %w" , err )
}
}
}
}
@@ -750,6 +905,45 @@ func (o *Orchestrator) runUnifiedReActStream(ctx context.Context, chatID, userID
if finalText == "" {
finalText = "已完成处理。"
}
if planningMode && ! workspaceSentThisTurn {
// 提取 artifact 标签内的文档内容作为工作区内容
workspaceContent := extractArtifactContent ( finalText )
if workspaceContent == "" {
workspaceContent = stripArtifactTags ( finalText )
}
if workspaceContent == "" {
workspaceContent = finalText
}
o . activatePlanningSession ( chatID , userID , workspaceContent , true )
if err := o . store . SaveMessage ( chatID , userID , "assistant" , wrapPIArtifact ( workspaceContent ) ) ; err != nil {
if o . log != nil {
o . log . Warnf ( "%s save planning artifact failed chat_id=%s err=%v" , traceLogPrefix , chatID , err )
}
}
if err := callback ( StreamEvent {
Type : StreamEventTypeWorkspaceStart ,
WorkspaceTitle : "PI Planning Document" ,
} ) ; err != nil {
return "" , fmt . Errorf ( "callback error: %w" , err )
}
if err := callback ( StreamEvent {
Type : StreamEventTypeWorkspaceDelta ,
Content : workspaceContent ,
} ) ; err != nil {
return "" , fmt . Errorf ( "callback error: %w" , err )
}
if err := callback ( StreamEvent { Type : StreamEventTypeWorkspaceEnd } ) ; err != nil {
return "" , fmt . Errorf ( "callback error: %w" , err )
}
workspaceSentThisTurn = true
finalText = "我已根据你的意见更新 PI 规划,请查看右侧工作区;如确认无误,请回复“确认”进入工单创建。"
} else if planningMode {
// 工作区已在本轮通过工具调用发送,剥离 final 内容中的 artifact 标签
finalText = stripArtifactTags ( finalText )
if finalText == "" {
finalText = "已完成处理。"
}
}
if o . log != nil {
o . log . Debugf ( "%s react stream final at step=%d answer_len=%d" , traceLogPrefix , step , len ( finalText ) )
}
@@ -817,11 +1011,66 @@ func (o *Orchestrator) runUnifiedReActStream(ctx context.Context, chatID, userID
o . log . Debugf ( "%s react stream step=%d tool=%s input=%q" , traceLogPrefix , step , toolName , toolInput )
}
if planningMode && toolName == "create_gitea_ticket" && ! isPlanningConfirmation ( userInput ) {
obs := formatToolErrorObservation ( "WAIT_USER_CONFIRM" , toolName , "需用户确认后才能创建工单,请先征求用户确认" )
if err := callback ( StreamEvent {
Type : StreamEventTypeToolResult ,
Content : obs ,
Step : step ,
ToolName : toolName ,
} ) ; err != nil {
return "" , fmt . Errorf ( "callback error: %w" , err )
}
messages = append ( messages , llm . PromptMessage {
Role : "tool" ,
ToolCallID : tc . ID ,
Name : tc . Function . Name ,
Content : obs ,
} )
continue
}
// 当路由命中 SAFe PI 规划技能或调用 publish_pi_plan 工具时,触发 workspace 事件。
isArtifactTool := strings . Contains ( toolName , "safe_pi_planning" ) || toolName == "publish_pi_plan" || ( routedToSafePIPlanning && strings . Contains ( toolName , "publish_pi_plan" ) )
if isArtifactTool {
if err := callback ( StreamEvent {
Type : StreamEventTypeWorkspaceStart ,
WorkspaceTitle : "PI Planning Document" ,
ToolName : toolName ,
} ) ; err != nil {
return "" , fmt . Errorf ( "callback error: %w" , err )
}
workspaceSentThisTurn = true
}
toolOut , toolErr := tool . Call ( ctx , toolInput )
obs := strings . TrimSpace ( toolOut )
if obs == "" {
obs = "(empty output)"
}
if isArtifactTool && toolErr == nil {
o . activatePlanningSession ( chatID , userID , obs , true )
if err := o . store . SaveMessage ( chatID , userID , "assistant" , wrapPIArtifact ( obs ) ) ; err != nil {
if o . log != nil {
o . log . Warnf ( "%s save planning artifact failed chat_id=%s err=%v" , traceLogPrefix , chatID , err )
}
}
// 将工具输出通过 workspace_delta 发送给前端
if err := callback ( StreamEvent {
Type : StreamEventTypeWorkspaceDelta ,
Content : obs ,
} ) ; err != nil {
return "" , fmt . Errorf ( "callback error: %w" , err )
}
if err := callback ( StreamEvent {
Type : StreamEventTypeWorkspaceEnd ,
} ) ; err != nil {
return "" , fmt . Errorf ( "callback error: %w" , err )
}
workspaceSentThisTurn = true
}
if toolErr != nil {
obs = formatToolErrorObservation ( "TOOL_EXEC_ERROR" , toolName , toolErr . Error ( ) ) + "\nOUTPUT:\n" + obs
o . emitCapabilityGap ( chatID , userID , userInput , "tool_call_failed:" + toolName )
@@ -1171,6 +1420,118 @@ func defaultIfEmpty(v, fallback string) string {
return v
}
func shouldExitPlanningMode ( userInput string ) bool {
text := strings . ToLower ( strings . TrimSpace ( userInput ) )
if text == "" {
return false
}
markers := [ ] string { "退出规划" , "结束规划" , "关闭分屏" , "结束分屏" , "停止规划" , "cancel planning" , "exit planning" }
for _ , marker := range markers {
if strings . Contains ( text , marker ) {
return true
}
}
return false
}
func isPlanningConfirmation ( userInput string ) bool {
text := strings . ToLower ( strings . TrimSpace ( userInput ) )
if text == "" {
return false
}
markers := [ ] string {
"确认" , "同意" , "可以创建" , "开始创建" , "继续创建" , "执行下一步" , "没问题,继续" ,
"confirm" , "approved" , "go ahead" , "proceed" ,
}
for _ , marker := range markers {
if strings . Contains ( text , marker ) {
return true
}
}
return false
}
func containsSafePIPlanningSkill ( skills [ ] knowledge . Skill ) bool {
for _ , sk := range skills {
if isSafePIPlanningSkill ( sk ) {
return true
}
}
return false
}
func isSafePIPlanningSkill ( sk knowledge . Skill ) bool {
name := strings . ToLower ( strings . TrimSpace ( sk . Name ) )
source := strings . ToLower ( strings . TrimSpace ( sk . Source ) )
if strings . Contains ( name , "safe" ) || strings . Contains ( name , "pi 规划" ) || strings . Contains ( name , "pi planning" ) {
return true
}
if strings . Contains ( source , "safe_pi_planning" ) {
return true
}
return false
}
func ( o * Orchestrator ) getSafePIPlanningSkills ( ) [ ] knowledge . Skill {
all := o . getSkillsSnapshot ( )
out := make ( [ ] knowledge . Skill , 0 , 1 )
for _ , sk := range all {
if isSafePIPlanningSkill ( sk ) {
out = append ( out , sk )
}
}
return out
}
func isPIPlanningToolName ( toolName string ) bool {
t := strings . ToLower ( strings . TrimSpace ( toolName ) )
return strings . Contains ( t , "safe_pi_planning" ) || t == "publish_pi_plan"
}
func wrapPIArtifact ( markdown string ) string {
content := strings . TrimSpace ( markdown )
if content == "" {
return ""
}
return "<artifact type=\"safe_pi_planning\" title=\"PI Planning Document\">\n" + content + "\n</artifact>"
}
func ( o * Orchestrator ) activatePlanningSession ( chatID , userID , artifact string , awaitingConfirm bool ) {
key := pendingFileKey ( chatID , userID )
o . planningSessionsMu . Lock ( )
defer o . planningSessionsMu . Unlock ( )
state := o . planningSessions [ key ]
state . Active = true
if strings . TrimSpace ( artifact ) != "" {
state . LastArtifact = strings . TrimSpace ( artifact )
}
state . AwaitingConfirm = awaitingConfirm
state . UpdatedAt = time . Now ( ) . UTC ( )
o . planningSessions [ key ] = state
}
func ( o * Orchestrator ) clearPlanningSession ( chatID , userID string ) {
key := pendingFileKey ( chatID , userID )
o . planningSessionsMu . Lock ( )
defer o . planningSessionsMu . Unlock ( )
delete ( o . planningSessions , key )
}
func ( o * Orchestrator ) getPlanningSession ( chatID , userID string ) ( planningSessionState , bool ) {
key := pendingFileKey ( chatID , userID )
o . planningSessionsMu . Lock ( )
defer o . planningSessionsMu . Unlock ( )
state , ok := o . planningSessions [ key ]
if ! ok {
return planningSessionState { } , false
}
if time . Since ( state . UpdatedAt ) > planningSessionTTL {
delete ( o . planningSessions , key )
return planningSessionState { } , false
}
return state , true
}
// formatRelevantSkillsForPrompt 返回与当前用户问题最相关的技能内容。
func ( o * Orchestrator ) formatSelectedSkillsForPrompt ( userInput string , selected [ ] knowledge . Skill ) string {
skills := selected
@@ -1331,6 +1692,14 @@ func (o *Orchestrator) emitCapabilityGap(chatID, userID, intent, reason string)
return
}
// 暂时关闭基于历史能力缺口的自动技能草稿生成。
if ! o . autoSkillDraftEnabled {
if o . log != nil {
o . log . Debugf ( "auto skill draft generation disabled temporarily" )
}
return
}
// 提取出高频率缺口并在超出阈值后进行 draft 生成
clusters , err := o . store . TopCapabilityGapClusters ( 20 , time . Now ( ) . UTC ( ) . Add ( - o . gapLookbackDuration ) )
if err != nil {
@@ -1573,6 +1942,50 @@ func sanitizeUserFacingAnswer(raw string) string {
return strings . TrimSpace ( strings . Join ( cleaned , "\n" ) )
}
// stripArtifactTags removes <artifact ...>...</artifact> blocks from text,
// returning only the surrounding non-artifact content.
func stripArtifactTags ( s string ) string {
for {
start := strings . Index ( s , "<artifact" )
if start == - 1 {
break
}
end := strings . Index ( s [ start : ] , "</artifact>" )
if end == - 1 {
s = strings . TrimSpace ( s [ : start ] )
break
}
end += start + len ( "</artifact>" )
before := strings . TrimSpace ( s [ : start ] )
after := strings . TrimSpace ( s [ end : ] )
if before != "" && after != "" {
s = before + "\n\n" + after
} else {
s = before + after
}
}
return strings . TrimSpace ( s )
}
// extractArtifactContent returns the text inside the first <artifact ...>...</artifact> block.
// Returns empty string if no artifact tags are found.
func extractArtifactContent ( s string ) string {
start := strings . Index ( s , "<artifact" )
if start == - 1 {
return ""
}
tagEnd := strings . Index ( s [ start : ] , ">" )
if tagEnd == - 1 {
return ""
}
contentStart := start + tagEnd + 1
end := strings . Index ( s , "</artifact>" )
if end == - 1 {
return strings . TrimSpace ( s [ contentStart : ] )
}
return strings . TrimSpace ( s [ contentStart : end ] )
}
// splitContentIntoSegments splits a string into smaller segments of the specified size (by rune count).
func splitContentIntoSegments ( content string , segmentSize int ) [ ] string {
runes := [ ] rune ( content )