Version: 0.9.83.dev.260508

后端:
1.关闭了注册接口

前端:
1.改造了主页

仓库:
1.对部署做了一些改善
This commit is contained in:
Losita
2026-05-08 18:29:49 +08:00
parent 25a608eaeb
commit cc98b62ad8
11 changed files with 854 additions and 346 deletions

View File

@@ -31,6 +31,7 @@ redis:
password: "redis_password_789"
userauth:
allowRegister: false
rpc:
listenOn: "0.0.0.0:9081"
endpoints:

View File

@@ -36,6 +36,7 @@ redis:
# user/auth zrpc 独立服务与网关客户端配置。
userauth:
allowRegister: false
rpc:
listenOn: "0.0.0.0:9081"
endpoints:

View File

@@ -14,15 +14,17 @@ import (
)
type UserHandler struct {
client ports.UserCommandClient
captcha *GeeTestService
client ports.UserCommandClient
captcha *GeeTestService
allowRegister bool
}
// NewUserHandler 只接收 user/auth 客户端与验证码服务,不再直接依赖本地 user service。
func NewUserHandler(client ports.UserCommandClient, captcha *GeeTestService) *UserHandler {
func NewUserHandler(client ports.UserCommandClient, captcha *GeeTestService, allowRegister bool) *UserHandler {
return &UserHandler{
client: client,
captcha: captcha,
client: client,
captcha: captcha,
allowRegister: allowRegister,
}
}
@@ -39,6 +41,10 @@ func (api *UserHandler) CaptchaRegister(c *gin.Context) {
}
func (api *UserHandler) UserRegister(c *gin.Context) {
if !api.ensureRegisterEnabled(c) {
return
}
var req registerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, respond.WrongParamType)
@@ -129,3 +135,20 @@ func (api *UserHandler) UserLogout(c *gin.Context) {
}
c.JSON(http.StatusOK, respond.Ok)
}
// ensureRegisterEnabled 负责统一收口“注册相关入口”的开关判断。
// 职责边界:
// 1. 只判断当前环境是否允许注册,并向前端返回明确的 403
// 2. 不负责登录、刷新 token、登出等其它 user/auth 能力;
// 3. 不触发验证码或 RPC 调用,避免“已关闭注册”时仍向下游产生无效流量。
func (api *UserHandler) ensureRegisterEnabled(c *gin.Context) bool {
if api.allowRegister {
return true
}
c.JSON(http.StatusForbidden, respond.Response{
Status: "40301",
Info: "registration is disabled",
})
return false
}

View File

@@ -85,7 +85,11 @@ func RegisterRouters(
})
})
userauthapi.RegisterRoutes(apiGroup, userauthapi.NewUserHandler(authClient, userauthapi.NewGeeTestServiceFromConfig()), authClient, limiter)
userauthapi.RegisterRoutes(apiGroup, userauthapi.NewUserHandler(
authClient,
userauthapi.NewGeeTestServiceFromConfig(),
readUserAuthAllowRegister(),
), authClient, limiter)
forumapi.RegisterRoutes(apiGroup, forumapi.NewHandler(forumClient), authClient, cache, limiter)
tokenstoreapi.RegisterRoutes(apiGroup, tokenstoreapi.NewHandler(tokenStoreClient), authClient, cache, limiter)
@@ -231,3 +235,12 @@ func compactConfigList(values []string) []string {
}
return result
}
func readUserAuthAllowRegister() bool {
// 1. 缺省保持“允许注册”,避免历史未补配置的环境升级后被静默改行为。
// 2. 只有显式把 userauth.allowRegister 设成 false才真正关闭注册入口。
if !viper.IsSet("userauth.allowRegister") {
return true
}
return viper.GetBool("userauth.allowRegister")
}

View File

@@ -13,55 +13,219 @@ import (
llmservice "github.com/LoveLosita/smartflow/backend/services/llm"
)
// execute 阶段常量:
// 1. 统一约束 execute 节点在流式输出、上下文挂载、历史标记中的固定标识;
// 2. 这些值会在 prompt、stream、history、tool runtime 多处复用,集中定义可避免字符串散落;
// 3. 报告里可将其理解为 execute 节点的“协议层元数据”,负责让编排层、展示层和执行层说同一种语言。
const (
executeStageName = "execute"
executeStatusBlockID = "execute.status"
executeSpeakBlockID = "execute.speak"
executePinnedKey = "execution_context"
toolAnalyzeHealth = "analyze_health"
executeHistoryKindKey = "newagent_history_kind"
// executeStageName 是 graph/node/stream 共同识别的执行阶段名。
// 调用目的:用于状态推送、日志埋点、终止态写入时标识“当前发生在 execute 阶段”。
executeStageName = "execute"
// executeStatusBlockID 是前端状态区块 ID。
// 调用目的:把“正在执行第几步”“工具执行中”等状态稳定写到同一个可更新区块,避免重复刷屏。
executeStatusBlockID = "execute.status"
// executeSpeakBlockID 是 execute 阶段可见回复文本的输出区块 ID。
// 调用目的:让模型在 execute 阶段产生的正文、补发文本、thinking 摘要归并到同一个展示区域。
executeSpeakBlockID = "execute.speak"
// executePinnedKey 是 execute 专属 pinned context 的 key。
// 调用目的把“执行模式、计划步、done_when、轮次预算”等关键信息固定挂到会话上下文供后续每轮提示词复用。
executePinnedKey = "execution_context"
// toolAnalyzeHealth 是粗排后健康度分析工具名。
// 调用目的:在主动优化链路中复用固定工具标识,避免分支判断时手写魔法字符串。
toolAnalyzeHealth = "analyze_health"
// executeHistoryKindKey 是 execute 阶段写入 history extra 时使用的分类 key。
// 调用目的:给某些“无正文但有语义”的历史记录打标签,方便后续节点识别这是系统插入的流程标记。
executeHistoryKindKey = "newagent_history_kind"
// executeHistoryKindStepAdvanced 表示“计划步骤已推进”的历史标记值。
// 调用目的:在 next_plan 后往历史里写一个轻量哨兵,避免重复推进时无法判断上一步是否已切换。
executeHistoryKindStepAdvanced = "execute_step_advanced"
// maxConsecutiveCorrections 是 execute 阶段允许的最大连续纠错次数。
// 调用目的:限制模型在 JSON 解析失败、动作不合法、goal_check 缺失等异常情况下的自我修正轮数,防止无限空转。
maxConsecutiveCorrections = 3
)
// ExecuteNodeInput 描述 execute 节点单轮运行所需的全部输入依赖。
//
// 职责边界:
// 1. 负责承载 execute 节点真正运行时要读到的“状态 + 上下文 + 模型 + 工具 + 输出能力”;
// 2. 不负责 graph 拓扑与分支决策,这些仍由 compose.Graph 和 branchAfterExecute 管理;
// 3. 不负责具体工具实现或 LLM 协议细节,它只是 node 层与下游能力之间的依赖注入容器。
//
// 报告可重点说明:
// 1. 这个结构体体现了本项目如何把 Eino agent 的单节点执行显式建模为一个“有完整依赖注入边界”的入口;
// 2. 与把所有依赖散落在全局变量不同,这种做法更便于恢复执行、单测替身注入和不同模型/工具组合切换;
// 3. execute 节点能否安全回环,核心就在于这些输入把“状态、上下文、工具、输出”完整串起来了。
type ExecuteNodeInput struct {
RuntimeState *agentmodel.AgentRuntimeState
ConversationContext *agentmodel.ConversationContext
UserInput string
Client *llmservice.Client
ChunkEmitter *agentstream.ChunkEmitter
ResumeNode string
ToolRegistry *agenttools.ToolRegistry
ScheduleState *schedule.ScheduleState
CompactionStore agentmodel.CompactionStore
WriteSchedulePreview agentmodel.WriteSchedulePreviewFunc
// RuntimeState 是 execute 节点的主运行态容器。
// 1. 负责承载 CommonState、待确认工具、待追问交互等跨轮次状态
// 2. execute 节点对 phase、round、plan step、pending interaction 的修改都会落到这里;
// 3. 若为空则无法恢复和推进 agent 执行,因此 prepareExecuteNodeInput 会把它视为必填依赖。
RuntimeState *agentmodel.AgentRuntimeState
// ConversationContext 是当前会话的统一上下文快照。
// 1. 负责保存历史消息、pinned block、工具 schema 等 prompt 装配素材;
// 2. execute 每一轮都会基于它重建 Eino message
// 3. 它不是持久化存储本身,而是当前 graph 运行时使用的内存态上下文。
ConversationContext *agentmodel.ConversationContext
// UserInput 是本轮用户原始输入文本。
// 1. 主要用于让 execute 节点在必要时回看用户当前明确意图;
// 2. 不直接等价于最终 prompt全量消息仍由 BuildExecuteMessages 统一组织;
// 3. 保留该字段是为了让 execute 在恢复、追问、纠错时仍能感知本轮入口意图。
UserInput string
// Client 是 execute 阶段专用 LLM 客户端。
// 1. 负责发起流式推理请求,生成结构化 SMARTFLOW_DECISION
// 2. 它可以与 chat / plan / deliver 阶段使用不同模型配置;
// 3. 若未注入execute 节点无法完成“思考下一步”的核心职责。
Client *llmservice.Client
// ChunkEmitter 是流式输出器。
// 1. 负责把状态、正文、thinking 摘要、工具调用过程实时推给前端;
// 2. execute 节点的大部分用户可见体验都经由它完成;
// 3. 即使外部没接真实前端,系统也会兜底一个 noop emitter保证流程代码无需处处判空。
ChunkEmitter *agentstream.ChunkEmitter
// ResumeNode 表示挂起交互恢复后应回到的节点名。
// 1. 主要用于 ask_user / confirm 之后恢复 graph 时定位回 execute
// 2. 它是 pending interaction 与 graph 路由之间的衔接信息;
// 3. 当前通常写为 "execute",便于中断后继续原执行链路。
ResumeNode string
// ToolRegistry 是 execute 阶段可调用工具的统一注册表。
// 1. 负责提供工具 schema、工具可见性判断和真正的工具执行入口
// 2. execute 不直接依赖具体工具实现,而是通过注册表做统一调度;
// 3. 这让 agent 的工具能力可以按域动态裁剪,而不必修改 execute 主流程。
ToolRegistry *agenttools.ToolRegistry
// ScheduleState 是当前排程态快照。
// 1. 只有涉及日程构建、挪动、交换等排程工具时才会被真正消费;
// 2. 首轮 execute 会对它做必要初始化,后续轮次复用同一份内存状态;
// 3. 该字段为空时,依赖日程状态的工具会被阻止执行,避免工具在残缺状态上落地。
ScheduleState *schedule.ScheduleState
// CompactionStore 是消息压缩存储依赖。
// 1. 当上下文过长时execute 会通过它记录或读取统一压缩结果;
// 2. 目的是在长链路中控制 token 成本,同时尽量保留关键执行语义;
// 3. 这是长流程 agent 可持续运行的重要辅助能力,而非核心业务状态。
CompactionStore agentmodel.CompactionStore
// WriteSchedulePreview 是排程预览写入函数。
// 1. 当 execute 阶段调用写排程工具后,可实时把最新预览写给前端/缓存;
// 2. 它只负责预览落盘,不负责真正修改 graph 路由或业务状态;
// 3. 这样用户在确认或微调过程中就能看到中间结果,而不必等到最终 deliver。
WriteSchedulePreview agentmodel.WriteSchedulePreviewFunc
// OriginalScheduleState 是进入本轮执行前的原始排程快照。
// 1. 主要用于需要对比、确认、恢复时保留“变更前基线”;
// 2. executePendingTool 等恢复场景可能会用到这份原始视角;
// 3. 它不是当前正在被修改的工作态,当前工作态仍是 ScheduleState。
OriginalScheduleState *schedule.ScheduleState
AlwaysExecute bool
ThinkingEnabled bool
// AlwaysExecute 表示是否跳过确认直接执行写工具。
// 1. 为 true 时confirm 类动作可以直接落地,不再等待用户二次确认;
// 2. 为 false 时,高风险写工具需先转成 pending confirm遵守安全确认链路
// 3. 这给不同入口场景(自动执行/显式确认)保留了统一的 execute 主流程。
AlwaysExecute bool
// ThinkingEnabled 控制 execute 阶段是否开启 reasoning/thinking 模式。
// 1. 它影响 LLM 请求时的推理模式选择;
// 2. 开启后系统会额外汇总 reasoning 摘要并推送给前端;
// 3. 该能力只影响模型推理表现,不改变 execute 节点的状态机协议。
ThinkingEnabled bool
// PersistVisibleMessage 是可见消息持久化函数。
// 1. 负责把应落库的 assistant 可见回复保存下来,供后续历史回放;
// 2. execute 不直接操作数据库,而是通过该函数把持久化职责下沉;
// 3. 这样 node 层仍保持“编排与调度”为主,不把存储细节耦合进来。
PersistVisibleMessage agentmodel.PersistVisibleMessageFunc
}
// ExecuteRoundObservation 描述 execute 阶段单轮观测结果。
//
// 职责边界:
// 1. 负责记录一轮 execute 中“模型做了什么决策、调用了什么工具、结果如何”;
// 2. 不负责承载完整上下文或最终业务结果,它更偏向调试、回放、分析视角;
// 3. 该结构体的价值在于把 agent 的单轮行为沉淀成可审计、可解释的数据切片。
type ExecuteRoundObservation struct {
Round int `json:"round"`
StepIndex int `json:"step_index"`
GoalCheck string `json:"goal_check,omitempty"`
Decision string `json:"decision,omitempty"`
ToolName string `json:"tool_name,omitempty"`
ToolParams string `json:"tool_params,omitempty"`
ToolSuccess bool `json:"tool_success"`
ToolResult string `json:"tool_result,omitempty"`
// Round 是执行轮次编号。
// 调用目的:标识这是第几轮 execute 回环,便于分析 agent 是否在某一步反复空转。
Round int `json:"round"`
// StepIndex 是当前计划步骤索引。
// 调用目的:把本轮行为与 plan 中的具体步骤关联起来,支持“第几步发生了什么”这类复盘。
StepIndex int `json:"step_index"`
// GoalCheck 是模型在 next_plan / done 时给出的完成性核验说明。
// 调用目的:保留“为什么它认为当前步骤已完成”的证据文本,增强 agent 行为可解释性。
GoalCheck string `json:"goal_check,omitempty"`
// Decision 是本轮结构化决策动作摘要。
// 调用目的:记录 action 层面的选择结果,如 continue / confirm / ask_user / done。
Decision string `json:"decision,omitempty"`
// ToolName 是本轮调用的工具名。
// 调用目的:标识 agent 在这一轮真正把哪项外部能力接入执行链。
ToolName string `json:"tool_name,omitempty"`
// ToolParams 是工具调用参数的可读化快照。
// 调用目的:辅助排查“工具为什么执行出这个结果”,但它只保留观测视图,不替代真实原始参数对象。
ToolParams string `json:"tool_params,omitempty"`
// ToolSuccess 表示工具是否执行成功。
// 调用目的:在轮次观测里快速区分“正常推进”与“工具失败/被阻断”的分支结果。
ToolSuccess bool `json:"tool_success"`
// ToolResult 是工具返回结果的摘要文本。
// 调用目的:保留工具观察结果,供下一轮上下文构造、日志调试或报告中的执行链路说明使用。
ToolResult string `json:"tool_result,omitempty"`
}
// RunExecuteNode 是 Eino agent graph 中 execute 节点的“单轮执行入口”。
//
// 职责边界:
// 1. 负责把 graph 层传入的运行态、会话上下文、工具目录、流式输出器等依赖整理成一次可执行的 execute 轮次。
// 2. 负责完成 execute 阶段的一次完整闭环:状态校准 -> 提示词装配 -> LLM 决策 -> 决策落地。
// 3. 不负责定义 Eino graph 的节点拓扑;节点注册、连边与回环由 graph/common_graph.go 中的 compose.Graph 统一管理。
// 4. 不负责具体工具实现和流式 JSON 解析细节;工具执行下沉到 tool_runtime.go模型决策采集下沉到 action_router.go。
//
// 报告可重点说明:
// 1. 这个函数体现了“Graph 负责编排、Node 负责单轮执行”的 Eino 接入方式。
// 2. execute 节点不是直接把用户请求一次性做完,而是以“轮次”为单位驱动 ReAct/Plan-and-Execute 风格的 agent 闭环。
// 3. 每一轮都会基于当前状态重建 Eino message上下文、计划进度、工具域、历史观察都会重新送入模型让模型只决定“下一步该做什么”。
func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
// 1. 先做 execute 节点入参归一化:
// 1.1 这里会兜底 RuntimeState / ConversationContext / ChunkEmitter避免 graph 恢复执行或单测场景出现空指针;
// 1.2 execute 节点后续所有状态推进都依赖这三类对象,因此必须在第一步统一校准;
// 1.3 若最基础的依赖(如 RuntimeState、LLM Client缺失直接返回错误避免带着残缺上下文进入模型决策。
runtimeState, conversationContext, emitter, err := prepareExecuteNodeInput(input)
if err != nil {
return err
}
// 2. 取出 graph 内部跨节点共享的 CommonState。
// 2.1 Chat / Plan / RoughBuild / Execute 都围绕这份状态协作;
// 2.2 execute 节点真正关心的“当前阶段、计划步、轮次预算、工具域、终止态”等信息都挂在这里;
// 2.3 这也是 Eino graph 在多节点之间传递 agent 运行语义的核心载体。
flowState := runtimeState.EnsureCommonState()
// 3. 应用待生效的上下文 hook。
// 3.1 上游节点(如 plan / rough_build可能只先写入 PendingContextHook而不立刻改 ActiveToolDomain
// 3.2 execute 节点在真正请求模型前统一消费这个 hook保证“提示词里的规则”和“模型看见的工具 schema”同轮生效
// 3.3 若这里不先同步,模型第一轮可能拿到旧工具域,导致它想做的动作与实际可见工具不一致。
applyPendingContextHook(flowState)
// 4. 若当前存在“待确认写工具”,说明上一轮已经完成了 LLM 决策,本轮是用户确认后的恢复执行。
// 4.1 这时不应该再让模型重新思考,否则会破坏“确认后执行原工具”的事务语义;
// 4.2 因此这里直接短路进入 executePendingTool把已确认的工具参数原样执行
// 4.3 这也是本项目把“模型决策”和“高风险写操作落地”显式拆开的关键安全设计。
if runtimeState.PendingConfirmTool != nil {
return executePendingTool(
ctx,
@@ -75,12 +239,24 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
)
}
// 5. 首轮进入 execute 且携带日程状态时,重置任务处理队列。
// 5.1 这样可以保证一次新的执行会话从干净的调度处理顺序开始;
// 5.2 只在 RoundUsed == 0 时重置,避免多轮 execute 回环过程中把中间状态反复抹掉;
// 5.3 这类“首轮初始化、后续轮次复用”的策略,是 agent 在长流程里保持状态连续性的基础。
if input.ScheduleState != nil && flowState.RoundUsed == 0 {
schedule.ResetTaskProcessingQueue(input.ScheduleState)
}
// 6. 把 execute 阶段的关键信息同步到会话 pinned context。
// 6.1 这里会把执行模式、当前计划步、done_when、轮次预算等内容写成可复用的上下文块
// 6.2 后续 BuildExecuteMessages 会把这些 pinned block 一并装配进 Eino message
// 6.3 这样模型每轮都能看到“当前执行到哪一步、什么算完成”,避免在长对话里丢失阶段感。
syncExecutePinnedContext(conversationContext, flowState)
// 7. 先向前端推送 execute 阶段状态,让用户看到 agent 当前正在做什么。
// 7.1 若当前处于 plan 模式,就明确展示“第几步 / 共几步 + 当前步骤摘要”;
// 7.2 若没有计划,则退化为通用“正在处理请求”文案;
// 7.3 这里的状态推送不影响 graph 路由,但会直接影响产品侧的可观察性与交互体验。
if flowState.HasCurrentPlanStep() {
current, total := flowState.PlanProgress()
currentStep, _ := flowState.CurrentPlanStep()
@@ -105,6 +281,10 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
}
}
// 8. 进入真正的模型决策前,先消费一轮执行预算。
// 8.1 execute 节点在 Eino graph 中是允许回环的branchAfterExecute 会在未完成时再次回到本节点;
// 8.2 NextRound() 失败时,不再继续请求模型,而是把“达到安全轮次上限”的终止原因写入状态;
// 8.3 这样 deliver 节点后续收口时可以读取统一的 terminal outcome避免无限循环或异常失控。
if !flowState.NextRound() {
flowState.Exhaust(
executeStageName,
@@ -114,7 +294,16 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
return nil
}
// 9. 基于当前 flowState + ConversationContext 重新构建本轮要发给模型的消息。
// 9.1 BuildExecuteMessages 会产出 []*schema.Message这是 Eino 模型调用使用的标准消息结构;
// 9.2 消息里不仅有历史对话,还会带上 execute 专属系统提示词、计划步骤约束、工具 schema、pinned context
// 9.3 这一步体现了本项目的核心思想:不是把 Agent 写死成 if/else而是把“当前可行动空间”编码进 prompt + state。
messages := agentprompt.BuildExecuteMessages(flowState, conversationContext)
// 10. 若上下文过长,则在真正调用模型前统一压缩消息。
// 10.1 压缩逻辑仍围绕统一的 Eino message 结构工作,不破坏后续模型调用接口;
// 10.2 这样既能控制 token 成本,又能尽量保住当前步骤、工具观察、关键 pinned 信息;
// 10.3 对长流程 agent 来说,这一步是“让多轮执行可持续”的关键工程补偿层。
messages = agentshared.CompactUnifiedMessagesIfNeeded(ctx, messages, agentshared.UnifiedCompactInput{
Client: input.Client,
CompactionStore: input.CompactionStore,
@@ -124,8 +313,15 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
StatusBlockID: executeStatusBlockID,
})
// 11. 在发起模型请求前记录本轮 LLM 上下文快照,便于排查“为什么模型会做出这个动作”。
// 11.1 这类日志不参与业务逻辑,但对调试 agent prompt、状态迁移和工具选择非常重要
// 11.2 尤其在 execute 回环链路里,它能帮助我们复盘每一轮看到的上下文是否一致。
agentshared.LogNodeLLMContext(executeStageName, "decision", flowState, messages)
// 12. 调用模型,流式采集 execute 决策。
// 12.1 下游会使用 Eino 风格的 message 输入向 LLM 发起 Stream 请求;
// 12.2 模型需要输出 <SMARTFLOW_DECISION>{...}</SMARTFLOW_DECISION> 结构化决策,再附带可见回答;
// 12.3 这里拿到的不是“最终答案”,而是 agent 下一步动作的机器可执行描述:继续读工具、请求确认、追问用户、推进计划或结束。
decisionOutput, err := collectExecuteDecisionFromLLM(
ctx,
input,
@@ -138,6 +334,10 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
return err
}
// 13. 把模型决策真正落到运行时。
// 13.1 handleExecuteDecision 会校验 action 合法性,并根据决策修改 flowState / runtimeState / conversation history
// 13.2 若是 continue + tool_call会执行读工具或普通工具若是 confirm会挂起等待用户确认若是 next_plan / done会推进计划或写入终止态
// 13.3 至此,一次 execute 节点的“思考 + 行动”闭环完成Eino graph 再根据更新后的状态决定下一跳是回到 execute、转 confirm还是进入 deliver。
return handleExecuteDecision(
ctx,
input,