Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/gateway-rpc-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,27 @@ type ResolvePermissionParams struct {

---

## Method: gateway.userQuestionAnswer

- Stability: Beta
- Auth Required: Yes
- Request Schema:

```go
type UserQuestionAnswerParams struct {
RequestID string `json:"request_id"` // MUST
Status string `json:"status,omitempty"` // answered|skipped,默认 answered
Values []string `json:"values,omitempty"` // 可选:选择值
Message string `json:"message,omitempty"` // 可选:文本回答
}
```

- Response Schema: `ack`(提交成功)或标准 `error`
- Observation:
- `gateway_requests_total{method="gateway.userQuestionAnswer",...}`

---

## Method: gateway.listProviders

- Stability: Stable
Expand Down Expand Up @@ -656,6 +677,14 @@ type GetRuntimeSnapshotParams struct {

- Response Schema:
- Success: `ack` + `payload.snapshot`(runtime facts、decision、todo snapshot)
- `payload.snapshot.pending_user_question`(可选):
- `request_id`
- `question_id`
- `title` / `description`
- `kind`(`text|single_choice|multi_choice`)
- `options`
- `required` / `allow_skip`
- `max_choices` / `timeout_sec`
- Failure: 标准 `error`
- Observation:
- `gateway_requests_total{method="runtime.snapshot.get",...}`
19 changes: 18 additions & 1 deletion docs/guides/feishu-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- `gateway.bindStream`
- `gateway.run`
- `gateway.resolvePermission`
- `gateway.userQuestionAnswer`
- `gateway.event`
- 会话与运行 ID 保持实现一致:
- `session_id = "feishu_" + stableHash(chat_id)`
Expand Down Expand Up @@ -86,7 +87,7 @@ SDK 模式下不要求公网回调地址,不要求 `adapter.listen/event_path/
## 6. 幂等与重试

- 消息去重键:`event_id + message_id`
- 卡片去重键:`request_id + decision`
- 卡片去重键:优先 `event_id`,无 `event_id` 时回退到动作键(如 `request_id + decision/status`)
- 仅当 `gateway.run` 成功受理后才标记成功;
- 若 `run` 失败会释放去重状态,Webhook 返回 `HTTP 500`,SDK 长连接回调返回失败 ACK,允许飞书重试恢复。

Expand All @@ -109,6 +110,22 @@ SDK 模式下不要求公网回调地址,不要求 `adapter.listen/event_path/

以上两种路径都复用 `gateway.resolvePermission`,不新增 Gateway action。

## 7.1 ask_user 统一交互(permission + user_question)

飞书端卡片回调已升级为通用 `action_type`:

- `action_type=permission` -> `gateway.resolvePermission`
- `action_type=user_question` -> `gateway.userQuestionAnswer`

V1 交互策略:

- 单选/跳过:优先卡片按钮提交
- 文本/多选:回退文本指令提交
- `回答 <request_id> <内容>`
- `跳过 <request_id>`

`webhook` 与 `sdk` 两条 ingress 使用同一动作映射与幂等规则,保证行为一致。

## 8. 安全要求

- 默认启用签名校验(Webhook);
Expand Down
9 changes: 9 additions & 0 deletions internal/cli/feishu_adapter_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ func (s *stubFeishuGatewayClient) Run(context.Context, string, string, string) e
func (s *stubFeishuGatewayClient) ResolvePermission(context.Context, string, string) error {
return nil
}
func (s *stubFeishuGatewayClient) ResolveUserQuestion(context.Context, string, string, []string, string) error {
return nil
}
func (s *stubFeishuGatewayClient) Ping(context.Context) error { return nil }
func (s *stubFeishuGatewayClient) Notifications() <-chan feishuadapter.GatewayNotification {
ch := make(chan feishuadapter.GatewayNotification)
Expand All @@ -234,6 +237,12 @@ func (stubFeishuMessenger) SendPermissionCard(context.Context, string, feishuada
func (stubFeishuMessenger) UpdatePermissionCard(context.Context, string, feishuadapter.ResolvedPermissionCardPayload) error {
return nil
}
func (stubFeishuMessenger) SendUserQuestionCard(context.Context, string, feishuadapter.UserQuestionCardPayload) (string, error) {
return "", nil
}
func (stubFeishuMessenger) UpdateUserQuestionCard(context.Context, string, feishuadapter.ResolvedUserQuestionCardPayload) error {
return nil
}
func (stubFeishuMessenger) SendStatusCard(context.Context, string, feishuadapter.StatusCardPayload) (string, error) {
return "", nil
}
Expand Down
22 changes: 22 additions & 0 deletions internal/cli/gateway_runtime_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -1722,6 +1722,28 @@ func convertRuntimeSnapshot(snapshot agentruntime.RuntimeSnapshot) gateway.Runti
"completed_count": snapshot.SubAgents.CompletedCount,
"failed_count": snapshot.SubAgents.FailedCount,
},
PendingUserQuestion: convertRuntimePendingUserQuestion(snapshot.PendingUserQuestion),
}
}

// convertRuntimePendingUserQuestion 把 runtime ask_user 待答快照映射为 gateway 契约结构。
func convertRuntimePendingUserQuestion(
payload *agentruntime.UserQuestionRequestedPayload,
) *gateway.PendingUserQuestionSnapshot {
if payload == nil {
return nil
}
return &gateway.PendingUserQuestionSnapshot{
RequestID: strings.TrimSpace(payload.RequestID),
QuestionID: strings.TrimSpace(payload.QuestionID),
Title: strings.TrimSpace(payload.Title),
Description: strings.TrimSpace(payload.Description),
Kind: strings.TrimSpace(payload.Kind),
Options: append([]any(nil), payload.Options...),
Required: payload.Required,
AllowSkip: payload.AllowSkip,
MaxChoices: payload.MaxChoices,
TimeoutSec: payload.TimeoutSec,
}
}

Expand Down
58 changes: 41 additions & 17 deletions internal/cli/gateway_runtime_bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,24 @@ import (
)

type runtimeStub struct {
submitInput agentruntime.PrepareInput
submitErr error
askInput agentruntime.AskInput
askErr error
deleteAskInput agentruntime.DeleteAskSessionInput
deleteAskResult bool
deleteAskErr error
compactInput agentruntime.CompactInput
compactResult agentruntime.CompactResult
compactErr error
systemToolInput agentruntime.SystemToolInput
systemToolRes tools.ToolResult
systemToolErr error
permissionInput agentruntime.PermissionResolutionInput
permissionErr error
activateSession struct {
submitInput agentruntime.PrepareInput
submitErr error
askInput agentruntime.AskInput
askErr error
deleteAskInput agentruntime.DeleteAskSessionInput
deleteAskResult bool
deleteAskErr error
compactInput agentruntime.CompactInput
compactResult agentruntime.CompactResult
compactErr error
systemToolInput agentruntime.SystemToolInput
systemToolRes tools.ToolResult
systemToolErr error
permissionInput agentruntime.PermissionResolutionInput
permissionErr error
userQuestionInput agentruntime.UserQuestionResolutionInput
userQuestionErr error
activateSession struct {
sessionID string
skillID string
}
Expand Down Expand Up @@ -129,7 +131,8 @@ func (s *runtimeStub) ResolvePermission(_ context.Context, input agentruntime.Pe
}

func (s *runtimeStub) ResolveUserQuestion(_ context.Context, input agentruntime.UserQuestionResolutionInput) error {
return nil
s.userQuestionInput = input
return s.userQuestionErr
}

func (s *runtimeStub) CancelActiveRun() bool {
Expand Down Expand Up @@ -856,6 +859,21 @@ func TestGatewayRuntimePortBridgeRuntimeMethods(t *testing.T) {
if stub.permissionInput.RequestID != "request-1" || string(stub.permissionInput.Decision) != "allow_session" {
t.Fatalf("permission input = %#v, want trimmed request id and allow_session", stub.permissionInput)
}
if err := bridge.ResolveUserQuestion(context.Background(), gateway.UserQuestionAnswerInput{
SubjectID: testBridgeSubjectID,
RequestID: " ask-1 ",
Status: " answered ",
Values: []string{"A", "B"},
Message: " final answer ",
}); err != nil {
t.Fatalf("resolve_user_question: %v", err)
}
if stub.userQuestionInput.RequestID != "ask-1" || stub.userQuestionInput.Status != "answered" {
t.Fatalf("user question input = %#v, want trimmed request id and status", stub.userQuestionInput)
}
if stub.userQuestionInput.Message != "final answer" || len(stub.userQuestionInput.Values) != 2 {
t.Fatalf("user question input = %#v, want message and values forwarded", stub.userQuestionInput)
}

canceled, err := bridge.CancelRun(context.Background(), gateway.CancelInput{
SubjectID: testBridgeSubjectID,
Expand Down Expand Up @@ -1059,6 +1077,7 @@ func TestGatewayRuntimePortBridgeRuntimeMethodErrors(t *testing.T) {
compactErr: errors.New("compact failed"),
systemToolErr: errors.New("system tool failed"),
permissionErr: errors.New("permission failed"),
userQuestionErr: errors.New("user question failed"),
activateSessionErr: errors.New("activate skill failed"),
deactivateSessionErr: errors.New("deactivate skill failed"),
sessionSkillsErr: errors.New("list session skills failed"),
Expand Down Expand Up @@ -1115,6 +1134,11 @@ func TestGatewayRuntimePortBridgeRuntimeMethodErrors(t *testing.T) {
}); err == nil {
t.Fatal("expected resolve_permission error from runtime")
}
if err := bridge.ResolveUserQuestion(context.Background(), gateway.UserQuestionAnswerInput{
SubjectID: testBridgeSubjectID,
}); err == nil {
t.Fatal("expected resolve_user_question error from runtime")
}
if _, err := bridge.ListSessions(context.Background()); err == nil {
t.Fatal("expected list_sessions error from runtime")
}
Expand Down
Loading
Loading