mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
Merge branch 'fix/proc-extra-frame-passthrough' into refactor/block-union-payload
This commit is contained in:
331
internal/plugin/proc/extra_bag_test.go
Normal file
331
internal/plugin/proc/extra_bag_test.go
Normal file
@ -0,0 +1,331 @@
|
||||
package proc
|
||||
|
||||
// Extra 通用透传的判据(2026-10-12,fix/proc-extra-frame-passthrough)。
|
||||
//
|
||||
// 背景:shm 桥此前只透传 4 个具名 Extra 键(media_blocks/media_type/
|
||||
// input_source/output_channel),插件自建的键(如 qq 的 qq_auth 绑帧身份)
|
||||
// 在 stage 之间静默丢失 —— 权限门绑帧机制在 proc 插件下整体失效。
|
||||
// 修复:4 键之外的 Extra 键装入 fExtraBag(JSON),跨 stage 与跨进程往返。
|
||||
//
|
||||
// 判据即契约:
|
||||
// 1. 具名 4 键走原字段、原语义(含 nil/未设置区分),bag 不碰它们。
|
||||
// 2. 任意 JSON 可序列化的插件键经 WriteAll→ReadInto 往返后值等价
|
||||
// (map/bool/string/数字/嵌套)。
|
||||
// 3. 不可序列化的键被跳过并返回错误——宁可显式失败也不静默丢键
|
||||
// (丢键对 qq 是权限失效,静默等于把安全退化成运气)。
|
||||
// 4. WriteDirty:bag 变了才写;没变零写入(lost update 防线不破)。
|
||||
// 5. 插件写回的 bag 在内核 ctx.Extra 里以 map 形态存活,下一个 stage
|
||||
// WriteAll 原样下发(帧态跨 stage 存活)。
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
|
||||
)
|
||||
|
||||
func newTestSeg(t *testing.T) *Segment {
|
||||
t.Helper()
|
||||
buf := make([]byte, 1<<20)
|
||||
seg, err := NewSegment(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSegment: %v", err)
|
||||
}
|
||||
return seg
|
||||
}
|
||||
|
||||
// 判据 2:插件自建键(qq_auth 形态:bool+int64 混合 map)完整往返。
|
||||
func TestExtraBag_PluginKeyRoundTrip(t *testing.T) {
|
||||
seg := newTestSeg(t)
|
||||
src := &pubsdk.StageContext{
|
||||
RawMessage: "hello",
|
||||
Extra: map[string]interface{}{
|
||||
"input_source": "qq", // 具名键,语义不变
|
||||
"qq_auth": map[string]interface{}{
|
||||
"active": true,
|
||||
"owner": true,
|
||||
"message_id": int64(1125391573),
|
||||
"user_id": int64(2198972886),
|
||||
"group_id": int64(0),
|
||||
"is_group": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := seg.WriteAll(src); err != nil {
|
||||
t.Fatalf("WriteAll: %v", err)
|
||||
}
|
||||
var dst pubsdk.StageContext
|
||||
if err := seg.ReadInto(&dst); err != nil {
|
||||
t.Fatalf("ReadInto: %v", err)
|
||||
}
|
||||
if dst.Extra["input_source"] != "qq" {
|
||||
t.Fatalf("具名键 input_source 应原样往返,got %v", dst.Extra["input_source"])
|
||||
}
|
||||
auth, ok := dst.Extra["qq_auth"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("qq_auth 应以 map 形态往返,got %T", dst.Extra["qq_auth"])
|
||||
}
|
||||
if auth["active"] != true || auth["owner"] != true {
|
||||
t.Fatalf("qq_auth bool 字段失真: %+v", auth)
|
||||
}
|
||||
// JSON 数字统一 float64 —— 插件侧按此解码
|
||||
if id, ok := auth["user_id"].(float64); !ok || int64(id) != 2198972886 {
|
||||
t.Fatalf("qq_auth user_id 失真: %v (%T)", auth["user_id"], auth["user_id"])
|
||||
}
|
||||
if id, ok := auth["message_id"].(float64); !ok || int64(id) != 1125391573 {
|
||||
t.Fatalf("qq_auth message_id 失真: %v", auth["message_id"])
|
||||
}
|
||||
if auth["is_group"] != false {
|
||||
t.Fatalf("qq_auth is_group 失真(false 不应丢失): %v", auth["is_group"])
|
||||
}
|
||||
}
|
||||
|
||||
// 判据 3:不可序列化的键显式报错,不静默丢。
|
||||
func TestExtraBag_UnserializableKeyFails(t *testing.T) {
|
||||
seg := newTestSeg(t)
|
||||
src := &pubsdk.StageContext{
|
||||
Extra: map[string]interface{}{
|
||||
"func_val": func() {}, // 函数不可 JSON
|
||||
},
|
||||
}
|
||||
if err := seg.WriteAll(src); err == nil {
|
||||
t.Fatal("不可序列化的 Extra 值应使 WriteAll 报错,而不是静默丢键")
|
||||
}
|
||||
}
|
||||
|
||||
// 判据 1:具名 4 键的 nil/空 语义不受 bag 影响(回归既有行为)。
|
||||
func TestExtraBag_NamedKeysUnaffected(t *testing.T) {
|
||||
seg := newTestSeg(t)
|
||||
// 无 Extra:全部未设置
|
||||
if err := seg.WriteAll(&pubsdk.StageContext{RawMessage: "x"}); err != nil {
|
||||
t.Fatalf("WriteAll: %v", err)
|
||||
}
|
||||
var dst pubsdk.StageContext
|
||||
if err := seg.ReadInto(&dst); err != nil {
|
||||
t.Fatalf("ReadInto: %v", err)
|
||||
}
|
||||
if dst.Extra != nil {
|
||||
t.Fatalf("无 Extra 时应保持 nil,got %v", dst.Extra)
|
||||
}
|
||||
// 只有具名键:与旧行为一致
|
||||
if err := seg.WriteAll(&pubsdk.StageContext{
|
||||
Extra: map[string]interface{}{"output_channel": "qq"},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteAll: %v", err)
|
||||
}
|
||||
dst = pubsdk.StageContext{}
|
||||
if err := seg.ReadInto(&dst); err != nil {
|
||||
t.Fatalf("ReadInto: %v", err)
|
||||
}
|
||||
if dst.Extra["output_channel"] != "qq" || len(dst.Extra) != 1 {
|
||||
t.Fatalf("具名键行为变化: %v", dst.Extra)
|
||||
}
|
||||
}
|
||||
|
||||
// 判据 4:bag 未变时 WriteDirty 零写入;变了才写。
|
||||
func TestExtraBag_WriteDirty(t *testing.T) {
|
||||
seg := newTestSeg(t)
|
||||
base := &pubsdk.StageContext{
|
||||
Extra: map[string]interface{}{"qq_auth": map[string]interface{}{"active": true, "owner": true}},
|
||||
}
|
||||
if err := seg.WriteAll(base); err != nil {
|
||||
t.Fatalf("WriteAll: %v", err)
|
||||
}
|
||||
var local pubsdk.StageContext
|
||||
if err := seg.ReadInto(&local); err != nil {
|
||||
t.Fatalf("ReadInto: %v", err)
|
||||
}
|
||||
snap := TakeSnapshot(&local)
|
||||
|
||||
// 只读插件:bag 没动 → 零写入
|
||||
n, err := seg.WriteDirty(&local, snap)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteDirty: %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Fatalf("只读插件应零写入,got %d 字段", n)
|
||||
}
|
||||
|
||||
// 改了身份(降权):bag 变 → 恰好 1 个字段
|
||||
local.Lock()
|
||||
local.Extra["qq_auth"] = map[string]interface{}{"active": true, "owner": false}
|
||||
local.Unlock()
|
||||
n, err = seg.WriteDirty(&local, snap)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteDirty: %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("bag 变更应恰好写回 1 个字段,got %d", n)
|
||||
}
|
||||
var dst pubsdk.StageContext
|
||||
if err := seg.ReadInto(&dst); err != nil {
|
||||
t.Fatalf("ReadInto: %v", err)
|
||||
}
|
||||
auth, ok := dst.Extra["qq_auth"].(map[string]interface{})
|
||||
if !ok || auth["owner"] != false {
|
||||
t.Fatalf("降权应往返成功,got %v", dst.Extra["qq_auth"])
|
||||
}
|
||||
}
|
||||
|
||||
// 判据 5:帧态跨 stage 存活 —— 内核从共享段读回后,下一次 WriteAll
|
||||
// 把插件写的 bag 原样下发(这是绑帧能成立的必要条件)。
|
||||
func TestExtraBag_FrameSurvivesAcrossStages(t *testing.T) {
|
||||
seg := newTestSeg(t)
|
||||
// stage1: 内核只有 input_source;插件 on_input 绑了 qq_auth
|
||||
if err := seg.WriteAll(&pubsdk.StageContext{
|
||||
Extra: map[string]interface{}{"input_source": "qq"},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteAll: %v", err)
|
||||
}
|
||||
var pluginCtx pubsdk.StageContext
|
||||
if err := seg.ReadInto(&pluginCtx); err != nil {
|
||||
t.Fatalf("ReadInto: %v", err)
|
||||
}
|
||||
// 快照在 handler 执行前抓(真实 stage 顺序);之后插件 on_input 绑身份。
|
||||
snap := TakeSnapshot(&pluginCtx)
|
||||
pluginCtx.Lock()
|
||||
if pluginCtx.Extra == nil {
|
||||
pluginCtx.Extra = map[string]interface{}{}
|
||||
}
|
||||
pluginCtx.Extra["qq_auth"] = map[string]interface{}{"active": true, "owner": true, "user_id": int64(2198972886)}
|
||||
pluginCtx.Unlock()
|
||||
if n, err := seg.WriteDirty(&pluginCtx, snap); err != nil || n == 0 {
|
||||
t.Fatalf("插件写回 qq_auth: n=%d err=%v", n, err)
|
||||
}
|
||||
|
||||
// 内核回读到自己的 ctx(coordinator.finish 语义)
|
||||
var kernelCtx pubsdk.StageContext
|
||||
if err := seg.ReadInto(&kernelCtx); err != nil {
|
||||
t.Fatalf("内核回读: %v", err)
|
||||
}
|
||||
if _, ok := kernelCtx.Extra["qq_auth"]; !ok {
|
||||
t.Fatal("内核回读后 qq_auth 必须存活(否则绑帧失效)")
|
||||
}
|
||||
|
||||
// stage2: 内核把同一 ctx 写下去(新段模拟下一 stage 的 WriteAll)
|
||||
if err := seg.WriteAll(&kernelCtx); err != nil {
|
||||
t.Fatalf("stage2 WriteAll: %v", err)
|
||||
}
|
||||
var pluginCtx2 pubsdk.StageContext
|
||||
if err := seg.ReadInto(&pluginCtx2); err != nil {
|
||||
t.Fatalf("stage2 ReadInto: %v", err)
|
||||
}
|
||||
auth, ok := pluginCtx2.Extra["qq_auth"].(map[string]interface{})
|
||||
if !ok || auth["owner"] != true {
|
||||
t.Fatalf("下一 stage 插件必须还能读到帧身份(绑帧核心判据),got %v", pluginCtx2.Extra["qq_auth"])
|
||||
}
|
||||
if pluginCtx2.Extra["input_source"] != "qq" {
|
||||
t.Fatalf("具名键跨 stage 也应存活: %v", pluginCtx2.Extra)
|
||||
}
|
||||
}
|
||||
|
||||
// bag 的 JSON 编解码等价性(float64 数字约定)——qq 插件解码侧依赖它。
|
||||
func TestExtraBag_JSONNumberConvention(t *testing.T) {
|
||||
raw, err := json.Marshal(map[string]interface{}{"user_id": int64(2198972886)})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
var out map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if f, ok := out["user_id"].(float64); !ok || int64(f) != 2198972886 {
|
||||
t.Fatalf("int64 经 JSON 应变 float64 且无损: %v %T", out["user_id"], out["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// 判据 1b:bag 里混入具名键时,具名字段必须赢(ReadInto 的覆盖顺序契约)。
|
||||
// 直接往共享段写原始 JSON 构造冲突值,绕过 WriteAll 的过滤 —— 模拟
|
||||
// "写侧 bug / 旧版本内核把具名键也装袋" 的现实场景。
|
||||
func TestExtraBag_NamedKeyConflict_BagLoses(t *testing.T) {
|
||||
seg := newTestSeg(t)
|
||||
if err := seg.WriteAll(&pubsdk.StageContext{
|
||||
Extra: map[string]interface{}{"input_source": "qq"},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteAll: %v", err)
|
||||
}
|
||||
// 绕过 captureLocal,手工把 bag 描述符指向伪造的 JSON(含同名异值具名键)。
|
||||
bagJSON, _ := json.Marshal(map[string]interface{}{
|
||||
"input_source": "bogus-from-bag", // 具名键污染
|
||||
"qq_auth": map[string]interface{}{"active": true},
|
||||
})
|
||||
sl, err := seg.write(bagJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
seg.setDesc(fExtraBag, sl)
|
||||
|
||||
var dst pubsdk.StageContext
|
||||
if err := seg.ReadInto(&dst); err != nil {
|
||||
t.Fatalf("ReadInto: %v", err)
|
||||
}
|
||||
if dst.Extra["input_source"] != "qq" {
|
||||
t.Fatalf("具名键必须覆盖 bag 污染值: got %v", dst.Extra["input_source"])
|
||||
}
|
||||
if _, ok := dst.Extra["qq_auth"]; !ok {
|
||||
t.Fatal("bag 的正常键(qq_auth)应存活")
|
||||
}
|
||||
}
|
||||
|
||||
// 判据 4b:WriteAll 后 bag 描述符里不得出现具名键(写侧过滤契约,
|
||||
// 用 raw JSON 检查——语义判据测不到"键从未进袋")。
|
||||
func TestExtraBag_WriteAll_NeverPutsNamedKeysInBag(t *testing.T) {
|
||||
seg := newTestSeg(t)
|
||||
src := &pubsdk.StageContext{
|
||||
Extra: map[string]interface{}{
|
||||
"input_source": "qq",
|
||||
"media_blocks": "x",
|
||||
"qq_auth": map[string]interface{}{"active": true},
|
||||
"custom": 123,
|
||||
},
|
||||
}
|
||||
if err := seg.WriteAll(src); err != nil {
|
||||
t.Fatalf("WriteAll: %v", err)
|
||||
}
|
||||
desc := seg.getDesc(fExtraBag)
|
||||
if desc.IsUnset() {
|
||||
t.Fatal("有自建键时 bag 描述符不应为空")
|
||||
}
|
||||
raw, err := seg.read(desc)
|
||||
if err != nil {
|
||||
t.Fatalf("read bag: %v", err)
|
||||
}
|
||||
var bag map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &bag); err != nil {
|
||||
t.Fatalf("bag 不是合法 JSON: %v (%s)", err, raw)
|
||||
}
|
||||
for k := range bag {
|
||||
if namedExtraKeys[k] {
|
||||
t.Fatalf("具名键 %s 混进了 bag(写侧过滤失效)", k)
|
||||
}
|
||||
}
|
||||
if bag["qq_auth"] == nil || bag["custom"] == nil {
|
||||
t.Fatalf("自建键应全在 bag: %v", bag)
|
||||
}
|
||||
}
|
||||
|
||||
// 变异哨兵:TakeSnapshot/WirteDirty 对 bag 的脏检测(bag 变了必须计 1 字段)。
|
||||
func TestExtraBag_SnapshotDetectsBagChange(t *testing.T) {
|
||||
seg := newTestSeg(t)
|
||||
if err := seg.WriteAll(&pubsdk.StageContext{
|
||||
Extra: map[string]interface{}{"qq_auth": map[string]interface{}{"active": true, "owner": false}},
|
||||
}); err != nil {
|
||||
t.Fatalf("WriteAll: %v", err)
|
||||
}
|
||||
var local pubsdk.StageContext
|
||||
if err := seg.ReadInto(&local); err != nil {
|
||||
t.Fatalf("ReadInto: %v", err)
|
||||
}
|
||||
snap := TakeSnapshot(&local)
|
||||
local.Lock()
|
||||
// 模拟 qq on_input 绑帧/合并降权:owner 翻转
|
||||
local.Extra["qq_auth"] = map[string]interface{}{"active": true, "owner": true}
|
||||
local.Unlock()
|
||||
n, err := seg.WriteDirty(&local, snap)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteDirty: %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("bag 身份翻转应恰好脏 1 字段(WriteDirty 漏检=绑帧写不回), got %d", n)
|
||||
}
|
||||
}
|
||||
@ -18,9 +18,13 @@ import (
|
||||
)
|
||||
|
||||
// 共享段魔数与版本,用于挂载时校验对端布局一致。
|
||||
//
|
||||
// v2(2026-10-12):新增 fExtraBag —— Extra 具名 4 键之外的插件自建键
|
||||
// (如 qq 的 qq_auth 绑帧身份)跨进程/跨 stage 透传。加字段必升版:
|
||||
// AttachSegment 与插件握手对版本失败即拒,旧插件不会静默错读新布局。
|
||||
const (
|
||||
shmMagic uint32 = 0x48415348 // "HASH" — HomeAgent SHared
|
||||
shmVersion uint32 = 1
|
||||
shmVersion uint32 = 2
|
||||
)
|
||||
|
||||
// 段布局(所有偏移均相对**段起始**,arena 内偏移相对 arenaBase):
|
||||
@ -84,10 +88,22 @@ const (
|
||||
fMemory // JSON
|
||||
fTokenUsage // JSON
|
||||
fErrors // JSON
|
||||
fExtraMediaBlocks // JSON —— Extra 的 4 个键提升为具名字段(§3.3 已核实使用点)
|
||||
fExtraMediaBlocks // JSON —— Extra 的 4 个键提升为具名字段
|
||||
fExtraMediaType
|
||||
fExtraInputSource
|
||||
fExtraOutputChannel
|
||||
// fExtraBag 承载 Extra 里**具名 4 键之外的全部插件自建键**(JSON)。
|
||||
//
|
||||
// 背景(2026-10-12 权限失效修复):qq 插件靠 ctx.Extra["qq_auth"] 把
|
||||
// 身份绑到推理帧上(中断抢占/并发合并场景下全局身份不可靠,见该插件
|
||||
// README「身份必须绑帧」)。但此前的桥只透传 4 个具名键,插件自建键
|
||||
// 在 stage 之间静默丢失 → before_toolcall 永远回落到可变全局身份 →
|
||||
// 绑帧(以及建立在它之上的权限门)在 proc 插件下整体失效。
|
||||
//
|
||||
// bag 把 Extra 恢复成「任意键跨进程/跨 stage 存活」的原语义。值以 JSON
|
||||
// 往返:插件侧拿到 map[string]interface{}(数字统一 float64),不能
|
||||
// 类型断言回私有 struct —— 插件侧解码约定见 sdk example/qq。
|
||||
fExtraBag
|
||||
|
||||
stageFieldCount
|
||||
)
|
||||
|
||||
@ -66,6 +66,36 @@ type localCtx struct {
|
||||
ExtraMediaType interface{}
|
||||
ExtraInputSource interface{}
|
||||
ExtraOutputChan interface{}
|
||||
// ExtraBag 是 Extra 里具名 4 键之外的全部键(插件自建,如 qq_auth)。
|
||||
// nil 表示无额外键;非 nil 时整体 JSON 进 fExtraBag。
|
||||
ExtraBag map[string]interface{}
|
||||
}
|
||||
|
||||
// namedExtraKeys 是被提升为具名字段的 4 个 Extra 键。bag 必须排除它们,
|
||||
// 否则同一键会被具名字段与 bag 各带一份,回读时语义冲突。
|
||||
var namedExtraKeys = map[string]bool{
|
||||
ExtraKeyMediaBlocks: true,
|
||||
ExtraKeyMediaType: true,
|
||||
ExtraKeyInputSource: true,
|
||||
ExtraKeyOutputChannel: true,
|
||||
}
|
||||
|
||||
// bagFromExtra 把 Extra 里非具名键收集成 bag(无额外键时返回 nil)。
|
||||
func bagFromExtra(extra map[string]interface{}) map[string]interface{} {
|
||||
if len(extra) == 0 {
|
||||
return nil
|
||||
}
|
||||
var bag map[string]interface{}
|
||||
for k, v := range extra {
|
||||
if namedExtraKeys[k] {
|
||||
continue
|
||||
}
|
||||
if bag == nil {
|
||||
bag = make(map[string]interface{})
|
||||
}
|
||||
bag[k] = v
|
||||
}
|
||||
return bag
|
||||
}
|
||||
|
||||
func captureLocal(sc *pubsdk.StageContext) *localCtx {
|
||||
@ -91,6 +121,7 @@ func captureLocal(sc *pubsdk.StageContext) *localCtx {
|
||||
l.ExtraMediaType = sc.Extra[ExtraKeyMediaType]
|
||||
l.ExtraInputSource = sc.Extra[ExtraKeyInputSource]
|
||||
l.ExtraOutputChan = sc.Extra[ExtraKeyOutputChannel]
|
||||
l.ExtraBag = bagFromExtra(sc.Extra)
|
||||
}
|
||||
return l
|
||||
}
|
||||
@ -166,6 +197,9 @@ func (s *Segment) writeLocal(l *localCtx) error {
|
||||
{fExtraMediaType, l.ExtraMediaType},
|
||||
{fExtraInputSource, l.ExtraInputSource},
|
||||
{fExtraOutputChannel, l.ExtraOutputChan},
|
||||
// bag 空时写成未设置(sliceOrNil);非空时整体 marshal。
|
||||
// 不可序列化的值在这里让 putJSON 显式报错(宁失败不静默丢键)。
|
||||
{fExtraBag, sliceOrNil(len(l.ExtraBag), l.ExtraBag)},
|
||||
}
|
||||
for _, step := range jsonFields {
|
||||
if err := putJSON(step.f, step.v); err != nil {
|
||||
@ -263,6 +297,16 @@ func (s *Segment) ReadInto(sc *pubsdk.StageContext) error {
|
||||
}
|
||||
|
||||
extra := map[string]interface{}{}
|
||||
// bag 先打平,具名键随后覆盖 → 具名键永远赢。
|
||||
// 顺序是防御:即便 bag 里混入具名键(写侧 bug/旧数据),
|
||||
// 也不会用 bag 的陈旧值顶掉具名字段的新值。
|
||||
var bag map[string]interface{}
|
||||
if err := getJSON(fExtraBag, &bag); err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range bag {
|
||||
extra[k] = v
|
||||
}
|
||||
for _, pair := range []struct {
|
||||
f stageField
|
||||
key string
|
||||
@ -281,6 +325,7 @@ func (s *Segment) ReadInto(sc *pubsdk.StageContext) error {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var respPtr *string
|
||||
if s.getFlag(flagResponseSet) {
|
||||
r, err := getStr(fResponse)
|
||||
@ -461,6 +506,7 @@ func newSnapshotFromLocal(l *localCtx) *Snapshot {
|
||||
sn.jsons[fExtraMediaType] = marshal(l.ExtraMediaType)
|
||||
sn.jsons[fExtraInputSource] = marshal(l.ExtraInputSource)
|
||||
sn.jsons[fExtraOutputChannel] = marshal(l.ExtraOutputChan)
|
||||
sn.jsons[fExtraBag] = marshal(sliceOrNil(len(l.ExtraBag), l.ExtraBag))
|
||||
|
||||
if l.Response != nil {
|
||||
sn.response = *l.Response
|
||||
@ -509,6 +555,8 @@ func (f stageField) String() string {
|
||||
return "extra." + ExtraKeyInputSource
|
||||
case fExtraOutputChannel:
|
||||
return "extra." + ExtraKeyOutputChannel
|
||||
case fExtraBag:
|
||||
return "extra.bag"
|
||||
}
|
||||
return fmt.Sprintf("field(%d)", int(f))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user