feat(question): DSH ask_user_question 桥接 + 前端问答面板 + 待办字段全路径透出
问题(P0):DSH 有两个独立的人机交互 seam —— approval/request(危险工具审批) 与 ask_user_question → ctx.userQuestions(模型主动提问)。原来只桥接了前者。 邮件驱动的会话没有本地 UI,而 ask() 的 provider 是 DSH host 注册的本地 UI 实现, 于是在那里等人点选永久等不到,那一轮工具调用**静默挂死**。 修法(不抢注全局 provider —— registerProvider 只允许一个活动实例,抢注会让 平台自己的界面失效):在 tools/execute around-dispatch 里只对**邮件驱动**的 会话接管 ask_user_question,其余原样 next()。失败一律当场报错而不是 next(): 下一个 answerer 是本地 UI,邮件会话没有兜底 UI,放过去就是挂死。 - lib/user-question.js(三桥逐字节同源,14 例测试):DSH questions[] ↔ AgentMail 单问题询问邮件的双向映射。多问题时把选项并集摊平、按 label 归属分配回各问题 (label 认不出来就不猜测放行);无选项题走自由文本 custom。 - Gateway:kind=question 且无选项时**不再**回落「同意/拒绝」(那会让自由文本 问题变成两个毫无意义的按钮);主题按类型区分「权限请求 / 需要回答」; 推送 payload 带上 permission_kind / multi_select / options。 - mails.permission_kind / permission_multi_select 此前只存在于结构体与写入路径, 五个读路径的 SELECT/Scan 都没带 —— 前端永远拿到空串,把提问渲染成批准/拒绝。 container 修正五处并加 repo 测试(含反向验证:删掉任一处字段,测试即失败)。 - 前端 PermissionPanel:question 走「勾选 + 自由文本」,多选/单选、空回答禁止提交; approval 路径不变(回归测试覆盖)。 测试:opencode 316 / dsh 349 / pi 405 / 前端 185 / Go 全量 全绿。
This commit is contained in:
@ -69,7 +69,13 @@ func RequestPermission(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
options := req.Options
|
||||
if len(options) == 0 {
|
||||
if len(options) == 0 && kind != "question" {
|
||||
// 审批型询问必须给两个可点选项,否则人在界面上无以为答。
|
||||
//
|
||||
// 主动询问(question)不同:它可能根本没有预设选项 ——
|
||||
// 那是「请把你的名字告诉我」「请把报错贴给我」这类自由文本问题。
|
||||
// 给它们塞「同意/拒绝」会让人只能选一个毫无意义的答案,
|
||||
// 而模型拿到的 selected 里也会是这种噪音。
|
||||
options = []string{"同意", "拒绝"}
|
||||
}
|
||||
|
||||
@ -107,7 +113,7 @@ func RequestPermission(w http.ResponseWriter, r *http.Request) {
|
||||
repo.TouchSession(r.Context(), sessionID)
|
||||
} else {
|
||||
// workspace 空串:权限询问不经三维寻址,没有 path 位可归属。
|
||||
id, err := repo.CreateSession(r.Context(), nil, agentName, "权限请求: "+req.Question, "")
|
||||
id, err := repo.CreateSession(r.Context(), nil, agentName, mailSubjectFor(kind, req.Question), "")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "Failed to create session")
|
||||
return
|
||||
@ -239,10 +245,16 @@ func RequestPermission(w http.ResponseWriter, r *http.Request) {
|
||||
"mail_id": mailID.String(),
|
||||
"session_id": sessionID.String(),
|
||||
"from_name": agentName,
|
||||
"subject": "权限请求: " + req.Question,
|
||||
"subject": mailSubjectFor(kind, req.Question),
|
||||
"mail_type": "permission_request",
|
||||
"role": "to",
|
||||
"session_alias": alias,
|
||||
// 待办类型与多选语义必须随推送下发:前端靠它们决定渲染
|
||||
// 「批准/拒绝」还是「回答问题」(勾选 + 自由文本)。
|
||||
// 不下发的话前端只能重查一次,而授权页是靠这条推送实时更新的。
|
||||
"permission_kind": kind,
|
||||
"permission_multi_select": req.MultiSelect,
|
||||
"permission_options": options,
|
||||
})
|
||||
|
||||
JSON(w, http.StatusOK, map[string]string{
|
||||
@ -253,6 +265,18 @@ func RequestPermission(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// mailSubjectFor 给待办邮件起主题。
|
||||
//
|
||||
// 审批型与主动询问是两类不同的待办,主题必须一眼能区分:「权限请求」意味着
|
||||
// 有人要被放行一个危险操作,「需要回答」只是模型缺信息。混用一套措辞会让人
|
||||
// 在授权页里把「回答问题」当成「批准执行」。
|
||||
func mailSubjectFor(kind, question string) string {
|
||||
if kind == "question" {
|
||||
return "需要回答: " + question
|
||||
}
|
||||
return "权限请求: " + question
|
||||
}
|
||||
|
||||
// POST /api/v1/permission/decide —— 需登录;只有该权限请求的收件人或管理员可决策
|
||||
func DecidePermission(w http.ResponseWriter, r *http.Request) {
|
||||
user := middleware.GetUser(r)
|
||||
|
||||
Reference in New Issue
Block a user