mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-07-03 14:09:06 +02:00
151 lines
5.0 KiB
Go
151 lines
5.0 KiB
Go
// SiYuan - Refactor your thinking
|
||
// Copyright (c) 2020-present, b3log.org
|
||
//
|
||
// This program is free software: you can redistribute it and/or modify
|
||
// it under the terms of the GNU Affero General Public License as published by
|
||
// the Free Software Foundation, either version 3 of the License, or
|
||
// (at your option) any later version.
|
||
//
|
||
// This program is distributed in the hope that it will be useful,
|
||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
// GNU Affero General Public License for more details.
|
||
//
|
||
// You should have received a copy of the GNU Affero General Public License
|
||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
package conf
|
||
|
||
import (
|
||
"encoding/hex"
|
||
"regexp"
|
||
|
||
"github.com/siyuan-note/siyuan/kernel/util"
|
||
)
|
||
|
||
// Secret 是一条命名密钥,Name 为引用名(如 weread_key),Value 在运行时为明文,落盘时由 Secrets.Encrypt 加密。
|
||
type Secret struct {
|
||
Name string `json:"name"`
|
||
Value string `json:"value"`
|
||
}
|
||
|
||
// Secrets 是全局密钥库,脱离 AI 配置独立存在,供智能体 http_request 工具、MCP 服务 headers 等以
|
||
// {{secret:名字}} 形式引用。落盘时 Value 经 AES 加密,运行时为明文。
|
||
type Secrets struct {
|
||
Items []*Secret `json:"items"`
|
||
}
|
||
|
||
func NewSecrets() *Secrets {
|
||
return &Secrets{Items: []*Secret{}}
|
||
}
|
||
|
||
// Encrypt 把内存明文加密为密文,供 AppConf.Save() 序列化前调用。
|
||
func (s *Secrets) Encrypt() {
|
||
if s == nil {
|
||
return
|
||
}
|
||
for _, item := range s.Items {
|
||
if item == nil || item.Value == "" {
|
||
continue
|
||
}
|
||
item.Value = util.AESEncrypt(item.Value)
|
||
}
|
||
}
|
||
|
||
// Decrypt 把密文解密回明文。util.AESDecrypt 返回的是 hex 文本,必须再做一次 hex.DecodeString
|
||
// 才是真正的明文,照搬 conf.AI.DecryptAPIKeys 的双 hex 模式。
|
||
func (s *Secrets) Decrypt() {
|
||
if s == nil {
|
||
return
|
||
}
|
||
for _, item := range s.Items {
|
||
if item == nil || item.Value == "" {
|
||
continue
|
||
}
|
||
dec := util.AESDecrypt(item.Value)
|
||
if dec == nil {
|
||
continue
|
||
}
|
||
if plain, err := hex.DecodeString(string(dec)); err == nil {
|
||
item.Value = string(plain)
|
||
}
|
||
}
|
||
}
|
||
|
||
// secretPlaceholder 匹配 {{secrets.NAME}} 形式的占位符,NAME 部分不含 } 字符。
|
||
var secretPlaceholder = regexp.MustCompile(`\{\{secrets\.([^}]+)\}\}`)
|
||
|
||
// Resolve 把字符串里的 {{secrets.NAME}} 占位符替换为对应明文密钥,并处理无前缀的
|
||
// $NAME、${NAME}(仅在密钥库存在对应名字时才替换)。找不到对应名字时保留原文,
|
||
// 便于调用方/LLM 发现尚未配置的密钥。
|
||
// 必须在内存明文状态下(InitConf 解密后或 AppConf.Save 的 defer 还原后)调用。
|
||
func (s *Secrets) Resolve(in string) string {
|
||
if s == nil {
|
||
return in
|
||
}
|
||
in = secretPlaceholder.ReplaceAllStringFunc(in, func(match string) string {
|
||
sub := secretPlaceholder.FindStringSubmatch(match)
|
||
if len(sub) < 2 {
|
||
return match
|
||
}
|
||
name := sub[1]
|
||
for _, item := range s.Items {
|
||
if item != nil && item.Name == name {
|
||
return item.Value
|
||
}
|
||
}
|
||
return match
|
||
})
|
||
return resolveDollar(in, s.lookup)
|
||
}
|
||
|
||
// lookup 按名查找密钥值,返回值及是否存在。
|
||
func (s *Secrets) lookup(name string) (string, bool) {
|
||
if s == nil {
|
||
return "", false
|
||
}
|
||
for _, item := range s.Items {
|
||
if item != nil && item.Name == name {
|
||
return item.Value, true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
// dollarPlaceholder 匹配无前缀的 shell 风格变量引用:${NAME} 与 $NAME。
|
||
// NAME 限定为字母/数字/下划线,避免误匹配 $100、正则等。
|
||
var dollarPlaceholder = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)`)
|
||
|
||
// resolveDollar 替换字符串里的 $NAME、${NAME}(shell 风格)。按顺序尝试 lookups,
|
||
// 首个命中的值生效;都不命中则保留原文。仅在能查到名字时才替换,因此 $100、正则等
|
||
// 不相关内容不受影响。
|
||
func resolveDollar(in string, lookups ...func(string) (string, bool)) string {
|
||
return dollarPlaceholder.ReplaceAllStringFunc(in, func(match string) string {
|
||
sub := dollarPlaceholder.FindStringSubmatch(match)
|
||
// sub[1] 为 ${NAME} 捕获组,sub[2] 为 $NAME 捕获组。
|
||
name := sub[1]
|
||
if name == "" {
|
||
name = sub[2]
|
||
}
|
||
if name == "" {
|
||
return match
|
||
}
|
||
for _, lk := range lookups {
|
||
if v, ok := lk(name); ok {
|
||
return v
|
||
}
|
||
}
|
||
return match
|
||
})
|
||
}
|
||
|
||
// ResolveSecretsVars 替换 {{secrets.NAME}}、{{vars.NAME}} 与无前缀的 $NAME、${NAME}。
|
||
// Secrets.Resolve / Variables.Resolve 已各自处理显式语法与无前缀引用(仅查各自库),
|
||
// 串行调用后即覆盖「$NAME 先密钥后变量」的优先级。仅在库中存在对应名字时才替换,
|
||
// 找不到保留原文——因此 $100、正则等不相关内容不受影响。
|
||
// 供智能体 http_request 工具、MCP 服务 headers 等统一消费密钥与变量。
|
||
func ResolveSecretsVars(secrets *Secrets, vars *Variables, in string) string {
|
||
in = secrets.Resolve(in)
|
||
return vars.Resolve(in)
|
||
}
|