Skip to content

Commit cbfb387

Browse files
authored
feat(bot): add unified VikingBot gateway routing and OpenViking auth (#3119)
* bot opt config\api check * 优化vikingbot的启动链路 * 美化颜色 * docs: add VikingBot gateway routing diagram * fix(bot): harden gateway auth and proxy routing
1 parent a878fc1 commit cbfb387

52 files changed

Lines changed: 4811 additions & 796 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bot/tests/test_agent_loop_outcome.py

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ def test_agent_loop_omits_spawn_tool_when_subagents_disabled(temp_dir: Path, mon
111111
bus = MessageBus()
112112
provider = _RecordingProvider()
113113
config = Config(storage_workspace=str(temp_dir), agents={"subagent_enabled": False})
114-
monkeypatch.setattr("vikingbot.agent.tools.factory.load_config", lambda: config)
115114

116115
loop = AgentLoop(
117116
bus=bus,
@@ -125,6 +124,24 @@ def test_agent_loop_omits_spawn_tool_when_subagents_disabled(temp_dir: Path, mon
125124
assert "spawn" not in loop.tools.tool_names
126125

127126

127+
def test_agent_loop_standalone_omits_openviking_tools(temp_dir: Path, monkeypatch):
128+
monkeypatch.setattr(AgentLoop, "_register_builtin_hooks", lambda self: None)
129+
monkeypatch.setattr("vikingbot.agent.loop.SubagentManager", _FakeSubagentManager)
130+
131+
config = Config(storage_workspace=str(temp_dir))
132+
loop = AgentLoop(
133+
bus=MessageBus(),
134+
provider=_RecordingProvider(),
135+
workspace=temp_dir / "workspace",
136+
config=config,
137+
)
138+
139+
assert config.ov_server.server_url == ""
140+
assert not any(name.startswith("openviking_") for name in loop.tools.tool_names)
141+
session_key = SessionKey(type="cli", channel_id="default", chat_id="standalone")
142+
assert loop._get_ov_tools_enable(session_key) is False
143+
144+
128145
@pytest.mark.asyncio
129146
async def test_context_prompt_omits_subagent_capability_when_disabled(temp_dir: Path):
130147
context = ContextBuilder(workspace=temp_dir / "workspace", enable_subagents=False)
@@ -174,14 +191,6 @@ async def test_agent_loop_makes_final_no_tool_call_when_iteration_limit_reached(
174191
monkeypatch.setattr(AgentLoop, "_register_default_tools", lambda self: None)
175192
monkeypatch.setattr("vikingbot.agent.loop.SubagentManager", _FakeSubagentManager)
176193

177-
class _OVConfig:
178-
exp_write_tools = []
179-
180-
class _LoadedConfig:
181-
ov_server = _OVConfig()
182-
183-
monkeypatch.setattr(loop_module, "load_config", lambda: _LoadedConfig())
184-
185194
class _ToolLimitProvider(LLMProvider):
186195
def __init__(self):
187196
super().__init__()
@@ -458,9 +467,7 @@ async def test_agent_loop_build_prompt_history_uses_ov_context_plus_unsynced_tai
458467
}
459468
)
460469

461-
async def fake_get_ov_client(
462-
self, session_key, openviking_connection=None, actor_peer_id=None
463-
):
470+
async def fake_get_ov_client(self, session_key, openviking_connection=None, actor_peer_id=None):
464471
del session_key, openviking_connection, actor_peer_id
465472
return fake_ov_client
466473

@@ -469,6 +476,7 @@ async def fake_get_ov_client(
469476
bus = MessageBus()
470477
config = Config(
471478
storage_workspace=str(temp_dir),
479+
ov_server={"server_url": "http://127.0.0.1:1933"},
472480
agents={"session_context_enabled": True, "session_context_token_budget": 321},
473481
)
474482
loop = AgentLoop(
@@ -513,9 +521,7 @@ async def test_agent_loop_build_prompt_history_skips_tail_when_sync_cursor_is_pa
513521
context_payload={"messages": [{"role": "user", "content": "OV user turn"}]}
514522
)
515523

516-
async def fake_get_ov_client(
517-
self, session_key, openviking_connection=None, actor_peer_id=None
518-
):
524+
async def fake_get_ov_client(self, session_key, openviking_connection=None, actor_peer_id=None):
519525
del self, session_key, openviking_connection, actor_peer_id
520526
return fake_ov_client
521527

@@ -524,6 +530,7 @@ async def fake_get_ov_client(
524530
bus = MessageBus()
525531
config = Config(
526532
storage_workspace=str(temp_dir),
533+
ov_server={"server_url": "http://127.0.0.1:1933"},
527534
agents={"session_context_enabled": True, "session_context_token_budget": 321},
528535
)
529536
loop = AgentLoop(
@@ -568,6 +575,7 @@ async def fake_execute_hooks(context, **kwargs):
568575
bus = MessageBus()
569576
config = Config(
570577
storage_workspace=str(temp_dir),
578+
ov_server={"server_url": "http://127.0.0.1:1933"},
571579
agents={"session_context_enabled": True},
572580
)
573581
loop = AgentLoop(
@@ -620,9 +628,7 @@ async def fake_execute_hooks(context, **kwargs):
620628
state["last_synced_local_index"] = len(session.messages) - 1
621629
return kwargs
622630

623-
async def fake_get_ov_client(
624-
self, session_key, openviking_connection=None, actor_peer_id=None
625-
):
631+
async def fake_get_ov_client(self, session_key, openviking_connection=None, actor_peer_id=None):
626632
del self, session_key, openviking_connection, actor_peer_id
627633

628634
class _Client:
@@ -651,6 +657,7 @@ async def fake_run_agent_loop(self, **kwargs):
651657
bus = MessageBus()
652658
config = Config(
653659
storage_workspace=str(temp_dir),
660+
ov_server={"server_url": "http://127.0.0.1:1933"},
654661
agents={
655662
"session_context_enabled": True,
656663
"session_context_token_budget": 321,
@@ -725,9 +732,7 @@ async def fake_execute_hooks(context, **kwargs):
725732
session.metadata["openviking"]["last_commit_performed"] = bool(kwargs["force_commit"])
726733
return kwargs
727734

728-
async def fake_get_ov_client(
729-
self, session_key, openviking_connection=None, actor_peer_id=None
730-
):
735+
async def fake_get_ov_client(self, session_key, openviking_connection=None, actor_peer_id=None):
731736
del self, session_key, openviking_connection, actor_peer_id
732737

733738
class _Client:
@@ -756,6 +761,7 @@ async def fake_run_agent_loop(self, **kwargs):
756761
bus = MessageBus()
757762
config = Config(
758763
storage_workspace=str(temp_dir),
764+
ov_server={"server_url": "http://127.0.0.1:1933"},
759765
agents={
760766
"session_context_enabled": True,
761767
"session_context_token_budget": 321,
@@ -887,7 +893,11 @@ async def fake_run_agent_loop(self, **kwargs):
887893
)
888894

889895
bus = MessageBus()
890-
config = Config(storage_workspace=str(temp_dir), agents={"session_context_enabled": True})
896+
config = Config(
897+
storage_workspace=str(temp_dir),
898+
ov_server={"server_url": "http://127.0.0.1:1933"},
899+
agents={"session_context_enabled": True},
900+
)
891901
loop = AgentLoop(
892902
bus=bus,
893903
provider=_FakeProvider(),
@@ -942,7 +952,11 @@ async def fake_run_agent_loop(self, **kwargs):
942952
)
943953

944954
bus = MessageBus()
945-
config = Config(storage_workspace=str(temp_dir), agents={"session_context_enabled": True})
955+
config = Config(
956+
storage_workspace=str(temp_dir),
957+
ov_server={"server_url": "http://127.0.0.1:1933"},
958+
agents={"session_context_enabled": True},
959+
)
946960
loop = AgentLoop(
947961
bus=bus,
948962
provider=_FakeProvider(),
@@ -1001,7 +1015,10 @@ async def build_messages(self, **kwargs):
10011015
monkeypatch.setattr(AgentLoop, "_run_agent_loop", fake_run_agent_loop)
10021016

10031017
bus = MessageBus()
1004-
config = Config(storage_workspace=str(temp_dir))
1018+
config = Config(
1019+
storage_workspace=str(temp_dir),
1020+
ov_server={"server_url": "http://127.0.0.1:1933"},
1021+
)
10051022
loop = AgentLoop(
10061023
bus=bus,
10071024
provider=_FakeProvider(),
@@ -1081,7 +1098,10 @@ async def fake_run_agent_loop(self, **kwargs):
10811098
monkeypatch.setattr(AgentLoop, "_run_agent_loop", fake_run_agent_loop)
10821099

10831100
bus = MessageBus()
1084-
config = Config(storage_workspace=str(temp_dir))
1101+
config = Config(
1102+
storage_workspace=str(temp_dir),
1103+
ov_server={"server_url": "http://127.0.0.1:1933"},
1104+
)
10851105
loop = AgentLoop(
10861106
bus=bus,
10871107
provider=_FakeProvider(),

bot/tests/test_channel_delivery_metadata.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ async def send_callback(msg: OutboundMessage) -> None:
4747
@pytest.mark.asyncio
4848
async def test_spawn_tool_preserves_channel_metadata():
4949
metadata = {"reply_to": "oc_chat", "chat_type": "group", "message_id": "om_old"}
50+
connection = {"api_key": "request-key", "account_id": "acct", "user_id": "alice"}
5051
calls = []
5152

5253
class FakeSubagentManager:
@@ -58,18 +59,21 @@ async def spawn(self, **kwargs):
5859
context = SimpleNamespace(
5960
session_key=SessionKey(type="feishu", channel_id="cli_app", chat_id="oc_chat"),
6061
channel_metadata=metadata,
62+
openviking_connection=connection,
6163
)
6264

6365
result = await tool.execute(context, task="read files", label="read")
6466

6567
assert result == "started"
6668
assert calls[0]["channel_metadata"] == metadata
69+
assert calls[0]["openviking_connection"] is connection
6770

6871

6972
@pytest.mark.asyncio
7073
async def test_subagent_announcement_preserves_channel_metadata(tmp_path):
7174
bus = MessageBus()
7275
metadata = {"reply_to": "oc_chat", "chat_type": "group", "message_id": "om_old"}
76+
connection = {"api_key": "request-key", "account_id": "acct", "user_id": "alice"}
7377
session_key = SessionKey(type="feishu", channel_id="cli_app", chat_id="oc_chat")
7478
manager = SubagentManager(
7579
provider=SimpleNamespace(get_default_model=lambda: "fake-model"),
@@ -86,11 +90,13 @@ async def test_subagent_announcement_preserves_channel_metadata(tmp_path):
8690
session_key,
8791
"ok",
8892
metadata,
93+
connection,
8994
)
9095

9196
inbound = await bus.consume_inbound()
9297
assert inbound.metadata == metadata
9398
assert inbound.metadata is not metadata
99+
assert inbound.openviking_connection is connection
94100

95101

96102
@pytest.mark.asyncio
@@ -100,6 +106,8 @@ async def test_system_message_response_preserves_channel_metadata(tmp_path, monk
100106
monkeypatch.setattr("vikingbot.agent.loop.SubagentManager", lambda **kwargs: SimpleNamespace())
101107

102108
metadata = {"reply_to": "oc_chat", "chat_type": "group", "message_id": "om_old"}
109+
connection = {"api_key": "request-key", "account_id": "acct", "user_id": "alice"}
110+
captured = {}
103111
session_key = SessionKey(type="feishu", channel_id="cli_app", chat_id="oc_chat")
104112
config = Config(storage_workspace=str(tmp_path / "data"))
105113
loop = AgentLoop(
@@ -121,7 +129,15 @@ async def fake_run_agent_loop(**kwargs):
121129
return "summary", None, [], {}, 1
122130

123131
loop._build_prompt_history = fake_build_prompt_history
124-
loop.context = SimpleNamespace(build_messages=fake_build_messages)
132+
133+
class FakeContextBuilder:
134+
def __init__(self, *args, **kwargs):
135+
captured["connection"] = kwargs.get("openviking_connection")
136+
137+
async def build_messages(self, **kwargs):
138+
return await fake_build_messages(**kwargs)
139+
140+
monkeypatch.setattr("vikingbot.agent.context.ContextBuilder", FakeContextBuilder)
125141
loop._run_agent_loop = fake_run_agent_loop
126142

127143
outbound = await loop._process_system_message(
@@ -130,12 +146,14 @@ async def fake_run_agent_loop(**kwargs):
130146
session_key=session_key,
131147
content="subagent done",
132148
metadata=metadata,
149+
openviking_connection=connection,
133150
)
134151
)
135152

136153
assert outbound.content == "summary"
137154
assert outbound.metadata == metadata
138155
assert outbound.metadata is not metadata
156+
assert captured["connection"] is connection
139157

140158

141159
@pytest.mark.asyncio

bot/tests/test_chat_functionality.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from vikingbot.bus.queue import MessageBus
1111
from vikingbot.channels.chat import ChatChannel, ChatChannelConfig
1212
from vikingbot.channels.single_turn import SingleTurnChannel, SingleTurnChannelConfig
13+
from vikingbot.cli.commands import _CHAT_PROMPT_MESSAGE, _CHAT_PROMPT_STYLE
1314
from vikingbot.config.schema import SessionKey
1415
from vikingbot.session.manager import Session
1516

@@ -119,6 +120,13 @@ def test_non_deepseek_history_omits_reasoning_content(self):
119120

120121

121122
class TestChatChannel:
123+
def test_interactive_prompt_uses_terminal_foreground_and_visible_label(self):
124+
assert _CHAT_PROMPT_MESSAGE == [("class:user-label", "You: ")]
125+
rules = dict(_CHAT_PROMPT_STYLE.style_rules)
126+
assert rules[""] == "fg:ansidefault"
127+
assert rules["user-label"] == "bold fg:ansicyan"
128+
assert all("ansiblack" not in style for style in rules.values())
129+
122130
def test_chat_channel_initialization(self, message_bus, temp_workspace):
123131
"""Test that ChatChannel can be initialized correctly."""
124132
config = ChatChannelConfig()

0 commit comments

Comments
 (0)