package loop import ( "context" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // 本文件汇出技能 `paths` 判定(DevDocs/design/09-插件生态.md §6.2/§6.3)要用的 // 「本轮工作文件」:ctx 上显式挂载的权威路径,加上用户本轮文本里看得出的路径 // token(保守启发式,代价只落在上下文,不落在权限上)。 // skillWorkFilesKey 是「本轮工作文件」的 context key(design/09 §6.2 的 `paths`)。 type skillWorkFilesKey struct{} // WithSkillWorkFiles 把本轮确实在动的文件路径挂到 ctx 上,供技能 `paths` 判定 // 自动激活(design/09 §6.3)。知道真实路径的调用方(网关的附件/工作区、编码类 // 工具的宿主)挂它;未挂载时该来源为空,判定退回用户文本里的路径候选。 // // 空列表是空操作:不挂键即「本轮没有工作文件」,与不接该能力时逐字一致。 func WithSkillWorkFiles(ctx context.Context, paths []string) context.Context { if ctx == nil || len(paths) == 0 { return ctx } return context.WithValue(ctx, skillWorkFilesKey{}, append([]string(nil), paths...)) } // SkillWorkFilesFrom 返回 ctx 上挂载的工作文件;未挂载返回 nil。它是 // WithSkillWorkFiles 的读侧,供只拿到 context 的调用方与测试观察。 func SkillWorkFilesFrom(ctx context.Context) []string { if ctx == nil { return nil } paths, _ := ctx.Value(skillWorkFilesKey{}).([]string) return paths } // turnWorkFiles 汇出本轮 `paths` 判定的工作文件候选: // // - ctx 上显式挂载的(WithSkillWorkFiles,权威来源); // - 用户本轮文本里**看得出是路径**的 token(保守启发式,见 pathLikeTokens)。 // // 第二条是为了让「帮我改 internal/runtime/loop/run.go」这类消息真的能命中 // `paths`,而不是让这枚设计字段在生产上永远不生效。它只影响「是否自动展开某个 // 技能的正文」,不参与任何身份/权限判定(身份只走 channel 层 actor 元数据)。 func turnWorkFiles(ctx context.Context, messages []model.Message) []string { hinted := SkillWorkFilesFrom(ctx) derived := pathLikeTokens(lastUserText(messages)) if len(hinted) == 0 { return derived } return append(append(make([]string, 0, len(hinted)+len(derived)), hinted...), derived...) } // pathLikeTokens 从一段文本里挑出「像路径」的 token:含路径分隔符(`/` 或 `\`)、 // 不含空白/引号等边界字符、单 token 长度有界。刻意保守 —— 宁可漏报,也不把普通 // 词当成路径(误报只会多展开一份技能正文,代价是上下文,不是权限)。 func pathLikeTokens(text string) []string { const ( maxTokenLen = 200 maxTokens = 16 ) if text == "" { return nil } fields := strings.FieldsFunc(text, func(r rune) bool { switch r { case ' ', '\t', '\n', '\r', '"', '\'', '`', ',', ';', '(', ')', '[', ']', '{', '}', '<', '>', '|', ',', '。', '、', '(', ')', ':', ';', '「', '」': return true default: return false } }) out := make([]string, 0, 4) seen := make(map[string]struct{}, 4) for _, tok := range fields { if len(tok) > maxTokenLen || !strings.ContainsAny(tok, `/\`) { continue } if _, dup := seen[tok]; dup { continue } seen[tok] = struct{}{} out = append(out, tok) if len(out) >= maxTokens { break } } return out }