feat(prompts): v2 DB 控制面热切换 —— 版本留存 + 不重启即生效
在 v1(注册表+文件覆盖)上加 DB 管理层与热切换,镜像 model-config 控制面: - store: sundynix_prompt 表(key/version/content/active) + ActivePrompts/ListPrompts/ CreateVersion/Activate/Deactivate - 控制面: ServePrompts/RequestActivePrompts(+Retry)/PublishPromptsUpdated/SubscribePromptsUpdated; prompts.ApplyOverrides 整体替换覆盖集(DB 激活集为权威) - gateway API: GET/POST /api/v1/prompts、version/activate/deactivate;激活/撤销即广播 - dispatcher/mcp-go: 启动拉激活集 + 订阅热更新(不重启) - live: 建版本→激活→mcp-go 图谱抽取 2→0→回滚 2(全程不重启);deactivate 回退代码默认;版本可回溯 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -588,6 +588,75 @@ func (b *Bus) SubscribeConfigUpdated(kind string, onUpdate func(*contract.ModelC
|
||||
return sub.Unsubscribe, nil
|
||||
}
|
||||
|
||||
// ---- Prompt 控制面(core NATS request-reply + broadcast,镜像 config)----
|
||||
|
||||
// ServePrompts 让控制面响应「取全部激活 prompt」请求;provide 返回 key→content 映射(可空)。
|
||||
// 队列组:多网关副本下每个请求只由一个副本应答。
|
||||
func (b *Bus) ServePrompts(provide func() map[string]string) (unsub func() error, err error) {
|
||||
sub, err := b.nc.QueueSubscribe(contract.SubjectPromptsGet, contract.QueueGateway, func(m *nats.Msg) {
|
||||
data, _ := json.Marshal(provide())
|
||||
_ = m.Respond(data)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("serve prompts: %w", err)
|
||||
}
|
||||
return sub.Unsubscribe, nil
|
||||
}
|
||||
|
||||
// RequestActivePrompts 向控制面请求当前全部激活 prompt(key→content)。无人应答/空集返回 (nil,nil),调用方用内置默认。
|
||||
func (b *Bus) RequestActivePrompts(ctx context.Context) (map[string]string, error) {
|
||||
msg, err := b.nc.RequestWithContext(ctx, contract.SubjectPromptsGet, nil)
|
||||
if err != nil || len(msg.Data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal(msg.Data, &m); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal prompts: %w", err)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// RequestPromptsWithRetry 后台重试拉取初始激活 prompt 集(容忍消费方早于网关启动),拿到非空即 apply。
|
||||
func (b *Bus) RequestPromptsWithRetry(ctx context.Context, apply func(map[string]string)) {
|
||||
for i := 0; i < 60; i++ {
|
||||
cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
m, _ := b.RequestActivePrompts(cctx)
|
||||
cancel()
|
||||
if len(m) > 0 {
|
||||
apply(m)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PublishPromptsUpdated 广播激活集变更(携带全量 key→content,消费方据此整体热更新)。
|
||||
func (b *Bus) PublishPromptsUpdated(m map[string]string) error {
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.nc.Publish(contract.SubjectPromptsUpdated, data)
|
||||
}
|
||||
|
||||
// SubscribePromptsUpdated 订阅激活集变更,回调拿到全量 key→content。
|
||||
func (b *Bus) SubscribePromptsUpdated(onUpdate func(map[string]string)) (unsub func() error, err error) {
|
||||
sub, err := b.nc.Subscribe(contract.SubjectPromptsUpdated, func(m *nats.Msg) {
|
||||
var mp map[string]string
|
||||
if json.Unmarshal(m.Data, &mp) == nil {
|
||||
onUpdate(mp)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("subscribe prompts updated: %w", err)
|
||||
}
|
||||
return sub.Unsubscribe, nil
|
||||
}
|
||||
|
||||
// TaskHandler 处理一个消费到的任务。
|
||||
type TaskHandler func(ctx context.Context, t *contract.Task) error
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package contract
|
||||
|
||||
// Prompt 控制面:DB 存各 prompt 的多版本,激活某版 → 经控制面热下发给各服务,
|
||||
// 覆盖内置默认(不重启即生效)。镜像 model-config 控制面(ConfigGetSubject/ConfigUpdatedSubject)。
|
||||
const (
|
||||
SubjectPromptsGet = "sundynix.prompts.get" // request-reply:取全部激活 prompt(map key→content)
|
||||
SubjectPromptsUpdated = "sundynix.prompts.updated" // 广播:激活集变更,消费方据此热更新
|
||||
)
|
||||
@@ -20,6 +20,9 @@ const (
|
||||
MemoryExtract = "memory.extract" // dispatcher:长期记忆对账
|
||||
)
|
||||
|
||||
// Known 是平台受管的全部 prompt key(供管理端列出可配项;网关进程不登记默认,靠它枚举)。
|
||||
var Known = []string{GraphExtract, EvalQuality, EvalRefine, GuardJailbreak, CoordinatorLead, MemoryExtract}
|
||||
|
||||
// Registry 持有默认与覆盖;Get 取覆盖优先、否则默认。并发安全。
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
@@ -53,6 +56,21 @@ func (r *Registry) SetOverride(key, text string) {
|
||||
r.overrides[key] = text
|
||||
}
|
||||
|
||||
// ApplyOverrides 用给定集合整体替换当前覆盖集(控制面热下发:DB 激活集为权威)。
|
||||
// 空值条目忽略;传空 map 即清空所有覆盖、全回退内置默认。
|
||||
func ApplyOverrides(m map[string]string) { global.ApplyOverrides(m) }
|
||||
func (r *Registry) ApplyOverrides(m map[string]string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
next := make(map[string]string, len(m))
|
||||
for k, v := range m {
|
||||
if v != "" {
|
||||
next[k] = v
|
||||
}
|
||||
}
|
||||
r.overrides = next
|
||||
}
|
||||
|
||||
// Get 取生效内容:有覆盖用覆盖,否则用默认(都没有则空串)。
|
||||
func Get(key string) string { return global.Get(key) }
|
||||
func (r *Registry) Get(key string) string {
|
||||
|
||||
Reference in New Issue
Block a user