fix(aibox): make --model effective in WebSocket server mode (H3)
--model was accepted by parse_args but only consumed inside handle_once_mode, so `python aibox.py --model kimi` (server mode) silently ignored the override. The model now flows as a process-wide override: main() -> EnhancedAIManager(model=...) -> DeepSeekChat(model=...) in all three AI creation paths: add_ai (main dialogue), WebSocketHandler._create_session_ai (per-connection sessions), and TaskScheduler._create_task_ai (scheduled tasks). Manager logs "CLI 模型覆盖已启用" at startup for observability. --once mode plumbing is unchanged; --model=None keeps config-driven behavior. Tests: new TestH3ModelPlumbing (6 cases) mocking DeepSeekChat to assert the constructor kwarg across manager/session/task paths plus parse_args; full suite 288 passed.
This commit is contained in:
1 parent
0e9efe4895
commit
84a3f6dd5e
3 files changed
+111
-8
No files matched your search
+2
-2
@@ -552,7 +552,7 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i
|
|||||||
| `reasoning_enabled` | 档案级思考模式开关(缺省回退 `api.reasoning_enabled`) |
|
| `reasoning_enabled` | 档案级思考模式开关(缺省回退 `api.reasoning_enabled`) |
|
||||||
| `timeout` | 请求超时秒数 |
|
| `timeout` | 请求超时秒数 |
|
||||||
|
|
||||||
**选择优先级**:`--model <档案名>`(命中 `models` 节)→ `api.default_model` 指向档案 → 回退旧 `api` 扁平节。
|
**选择优先级**:`--model <档案名>`(命中 `models` 节)→ `api.default_model` 指向档案 → 回退旧 `api` 扁平节。`--model` 为**进程级覆盖**:WebSocket 服务模式(主对话、每个会话实例、定时任务实例)与 `--once` 一次性模式均生效。
|
||||||
|
|
||||||
`--model` 传**裸模型名**(未命中档案)时,沿用默认端点与 Key,仅覆盖模型名——即 "同端点换模型" 语义。
|
`--model` 传**裸模型名**(未命中档案)时,沿用默认端点与 Key,仅覆盖模型名——即 "同端点换模型" 语义。
|
||||||
|
|
||||||
@@ -657,7 +657,7 @@ python aibox.py [选项]
|
|||||||
--show-reasoning 一次性模式输出思考流(默认静默,走 stderr)
|
--show-reasoning 一次性模式输出思考流(默认静默,走 stderr)
|
||||||
--no-save-memory 一次性模式禁用记忆工具(干净跳过,不进请求 schema)
|
--no-save-memory 一次性模式禁用记忆工具(干净跳过,不进请求 schema)
|
||||||
--worker-only 一次性模式纯执行面(worker profile,无人格/记忆工具)
|
--worker-only 一次性模式纯执行面(worker profile,无人格/记忆工具)
|
||||||
--model NAME 模型档案名(models 节)或裸模型名(同端点换模型)
|
--model NAME 模型档案名(models 节)或裸模型名(同端点换模型);服务模式与 --once 均生效(进程级覆盖)
|
||||||
--temperature T 覆盖配置的对话温度
|
--temperature T 覆盖配置的对话温度
|
||||||
--once-timeout SEC 一次性模式全局超时(默认 300 秒)
|
--once-timeout SEC 一次性模式全局超时(默认 300 秒)
|
||||||
```
|
```
|
||||||
|
|||||||
+18
-6
@@ -116,7 +116,9 @@ class TaskScheduler:
|
|||||||
# 每个任务使用独立的命令执行器,互不共享,避免 cancel() 误杀其他任务的子进程
|
# 每个任务使用独立的命令执行器,互不共享,避免 cancel() 误杀其他任务的子进程
|
||||||
command_executor=SubprocessCommandExecutor(self.logger),
|
command_executor=SubprocessCommandExecutor(self.logger),
|
||||||
prompt_manager=self.ai_manager.prompt_manager if self.ai_manager else None,
|
prompt_manager=self.ai_manager.prompt_manager if self.ai_manager else None,
|
||||||
skill_manager=self._get_shared_skill_manager()
|
skill_manager=self._get_shared_skill_manager(),
|
||||||
|
# H3:CLI --model 为进程级覆盖,定时任务实例同样生效
|
||||||
|
model=getattr(self.ai_manager, "model", None) if self.ai_manager else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_websocket_handler(self, handler):
|
def set_websocket_handler(self, handler):
|
||||||
@@ -1938,7 +1940,9 @@ class WebSocketHandler:
|
|||||||
# 每个连接使用独立的命令执行器,互不共享,避免 cancel() 误杀其他用户/任务正在运行的子进程
|
# 每个连接使用独立的命令执行器,互不共享,避免 cancel() 误杀其他用户/任务正在运行的子进程
|
||||||
command_executor=SubprocessCommandExecutor(self.logger),
|
command_executor=SubprocessCommandExecutor(self.logger),
|
||||||
prompt_manager=self.ai_manager.prompt_manager,
|
prompt_manager=self.ai_manager.prompt_manager,
|
||||||
skill_manager=self._get_shared_skill_manager()
|
skill_manager=self._get_shared_skill_manager(),
|
||||||
|
# H3:CLI --model 进程级覆盖传导到每个会话实例
|
||||||
|
model=getattr(self.ai_manager, "model", None)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_session(self, websocket) -> dict:
|
def _get_session(self, websocket) -> dict:
|
||||||
@@ -2569,18 +2573,24 @@ class WebSocketHandler:
|
|||||||
class EnhancedAIManager:
|
class EnhancedAIManager:
|
||||||
"""增强的AI对话管理器 - 集成任务调度器"""
|
"""增强的AI对话管理器 - 集成任务调度器"""
|
||||||
|
|
||||||
def __init__(self, debug_level: int = None, load_history: bool = None, command_executor: CommandExecutor = None):
|
def __init__(self, debug_level: int = None, load_history: bool = None, command_executor: CommandExecutor = None,
|
||||||
|
model: str = None):
|
||||||
self.ai: Optional[DeepSeekChat] = None
|
self.ai: Optional[DeepSeekChat] = None
|
||||||
self.dialogue_history: List[Dict] = []
|
self.dialogue_history: List[Dict] = []
|
||||||
self.config = ConfigManager()
|
self.config = ConfigManager()
|
||||||
self.command_executor = command_executor or SubprocessCommandExecutor()
|
self.command_executor = command_executor or SubprocessCommandExecutor()
|
||||||
self.prompt_manager = PromptManager() # 创建提示词管理器
|
self.prompt_manager = PromptManager() # 创建提示词管理器
|
||||||
self.scheduler: Optional[TaskScheduler] = None # 任务调度器
|
self.scheduler: Optional[TaskScheduler] = None # 任务调度器
|
||||||
|
# H3:CLI --model 进程级覆盖(档案名或裸模型名),传导到本进程创建的所有 AI 实例
|
||||||
|
# (主对话 add_ai / WebSocket 会话 / 定时任务),此前服务模式该参数被静默忽略
|
||||||
|
self.model = model
|
||||||
|
|
||||||
if debug_level is None:
|
if debug_level is None:
|
||||||
debug_level = self.config.get("system.debug_level", 1)
|
debug_level = self.config.get("system.debug_level", 1)
|
||||||
|
|
||||||
self.logger = Logger("Manager", debug_level)
|
self.logger = Logger("Manager", debug_level)
|
||||||
|
if model:
|
||||||
|
self.logger.info(f"CLI 模型覆盖已启用: {model}")
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
# 确保 ~/.aibox 目录存在
|
# 确保 ~/.aibox 目录存在
|
||||||
@@ -2628,7 +2638,8 @@ class EnhancedAIManager:
|
|||||||
memory_db=self.memory_db,
|
memory_db=self.memory_db,
|
||||||
memo_db=self.memo_db,
|
memo_db=self.memo_db,
|
||||||
command_executor=self.command_executor,
|
command_executor=self.command_executor,
|
||||||
prompt_manager=self.prompt_manager
|
prompt_manager=self.prompt_manager,
|
||||||
|
model=self.model # H3:--model 进程级覆盖
|
||||||
)
|
)
|
||||||
|
|
||||||
self.ai = ai
|
self.ai = ai
|
||||||
@@ -2852,7 +2863,7 @@ def parse_args():
|
|||||||
parser.add_argument('--no-save-memory', action='store_true', help='一次性对话不注册记忆工具,禁止写入记忆数据库')
|
parser.add_argument('--no-save-memory', action='store_true', help='一次性对话不注册记忆工具,禁止写入记忆数据库')
|
||||||
parser.add_argument('--show-reasoning', action='store_true', help='一次性对话在 stderr 显示思考过程流(默认不显示)')
|
parser.add_argument('--show-reasoning', action='store_true', help='一次性对话在 stderr 显示思考过程流(默认不显示)')
|
||||||
parser.add_argument('--worker-only', action='store_true', help='一次性对话以 worker 纯执行面模式运行(7 种执行工具,无记忆/人格/派生)')
|
parser.add_argument('--worker-only', action='store_true', help='一次性对话以 worker 纯执行面模式运行(7 种执行工具,无记忆/人格/派生)')
|
||||||
parser.add_argument('--model', type=str, metavar='NAME', help='覆盖配置中的模型名(如 deepseek-chat / deepseek-reasoner)')
|
parser.add_argument('--model', type=str, metavar='NAME', help='覆盖配置中的模型:档案名(models 节,如 kimi)或裸模型名(同端点换模型)。服务模式与 --once 均生效(进程级覆盖)')
|
||||||
parser.add_argument('--temperature', type=float, metavar='T', help='覆盖默认采样温度(0.0~2.0,越低越确定)')
|
parser.add_argument('--temperature', type=float, metavar='T', help='覆盖默认采样温度(0.0~2.0,越低越确定)')
|
||||||
|
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
@@ -3142,7 +3153,8 @@ def main():
|
|||||||
|
|
||||||
manager = EnhancedAIManager(
|
manager = EnhancedAIManager(
|
||||||
debug_level=args.debug,
|
debug_level=args.debug,
|
||||||
command_executor=command_executor
|
command_executor=command_executor,
|
||||||
|
model=args.model # H3:--model 服务模式同样生效(进程级覆盖)
|
||||||
)
|
)
|
||||||
|
|
||||||
if handle_check_commands(manager):
|
if handle_check_commands(manager):
|
||||||
|
|||||||
@@ -297,6 +297,97 @@ class TestH2SchemaValidation:
|
|||||||
create_llm_client(_cfg(models=models, default_model=None), model="kimi")
|
create_llm_client(_cfg(models=models, default_model=None), model="kimi")
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== H3:--model 服务模式传导 ====================
|
||||||
|
|
||||||
|
class TestH3ModelPlumbing:
|
||||||
|
"""H3:--model 此前仅在 --once 模式生效,WebSocket 服务模式被静默忽略。
|
||||||
|
|
||||||
|
验证进程级覆盖传导链:main() → EnhancedAIManager(model) →
|
||||||
|
add_ai / WebSocketHandler._create_session_ai / TaskScheduler._create_task_ai
|
||||||
|
三类 AI 实例的 DeepSeekChat(model=...) 构造参数。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _stub_logger(self):
|
||||||
|
return types.SimpleNamespace(level=0, info=lambda *a, **k: None,
|
||||||
|
warning=lambda *a, **k: None,
|
||||||
|
error=lambda *a, **k: None)
|
||||||
|
|
||||||
|
def test_manager_stores_and_passes_model_to_add_ai(self):
|
||||||
|
"""EnhancedAIManager(model) → add_ai() → DeepSeekChat(model=...)"""
|
||||||
|
from aibox import EnhancedAIManager
|
||||||
|
mgr = EnhancedAIManager.__new__(EnhancedAIManager) # 跳过重初始化,只测传导
|
||||||
|
mgr.model = "kimi"
|
||||||
|
mgr.logger = self._stub_logger()
|
||||||
|
mgr.history_manager = None
|
||||||
|
mgr.memory_db = None
|
||||||
|
mgr.memo_db = None
|
||||||
|
mgr.command_executor = None
|
||||||
|
mgr.prompt_manager = None
|
||||||
|
with mock.patch("aibox.DeepSeekChat") as mc:
|
||||||
|
mgr.add_ai()
|
||||||
|
assert mc.call_args.kwargs.get("model") == "kimi"
|
||||||
|
|
||||||
|
def test_manager_default_model_is_none(self):
|
||||||
|
"""未指定 --model → model=None,DeepSeekChat 走配置默认"""
|
||||||
|
from aibox import EnhancedAIManager
|
||||||
|
mgr = EnhancedAIManager.__new__(EnhancedAIManager)
|
||||||
|
mgr.model = None
|
||||||
|
mgr.logger = self._stub_logger()
|
||||||
|
mgr.history_manager = None
|
||||||
|
mgr.memory_db = None
|
||||||
|
mgr.memo_db = None
|
||||||
|
mgr.command_executor = None
|
||||||
|
mgr.prompt_manager = None
|
||||||
|
with mock.patch("aibox.DeepSeekChat") as mc:
|
||||||
|
mgr.add_ai()
|
||||||
|
assert mc.call_args.kwargs.get("model") is None
|
||||||
|
|
||||||
|
def test_session_ai_receives_model(self):
|
||||||
|
"""WebSocketHandler._create_session_ai → DeepSeekChat(model=...)"""
|
||||||
|
from aibox import WebSocketHandler
|
||||||
|
handler = WebSocketHandler.__new__(WebSocketHandler)
|
||||||
|
handler.ai_manager = types.SimpleNamespace(
|
||||||
|
model="kimi", memory_db=None, memo_db=None, prompt_manager=None)
|
||||||
|
handler.logger = self._stub_logger()
|
||||||
|
handler._shared_skill_manager = object() # 非 None 即复用,不触发扫描
|
||||||
|
with mock.patch("aibox.DeepSeekChat") as mc, \
|
||||||
|
mock.patch("aibox.SubprocessCommandExecutor"):
|
||||||
|
handler._create_session_ai()
|
||||||
|
assert mc.call_args.kwargs.get("model") == "kimi"
|
||||||
|
|
||||||
|
def test_task_ai_receives_model(self):
|
||||||
|
"""TaskScheduler._create_task_ai → DeepSeekChat(model=...)"""
|
||||||
|
from aibox import TaskScheduler
|
||||||
|
sched = TaskScheduler.__new__(TaskScheduler)
|
||||||
|
sched.ai_manager = types.SimpleNamespace(
|
||||||
|
model="kimi", memory_db=None, memo_db=None, prompt_manager=None)
|
||||||
|
sched.logger = self._stub_logger()
|
||||||
|
sched._shared_skill_manager = object()
|
||||||
|
with mock.patch("aibox.DeepSeekChat") as mc, \
|
||||||
|
mock.patch("aibox.SubprocessCommandExecutor"):
|
||||||
|
sched._create_task_ai()
|
||||||
|
assert mc.call_args.kwargs.get("model") == "kimi"
|
||||||
|
|
||||||
|
def test_task_ai_without_manager_passes_none(self):
|
||||||
|
"""调度器未挂 manager(ai_manager=None)→ model=None 不报错"""
|
||||||
|
from aibox import TaskScheduler
|
||||||
|
sched = TaskScheduler.__new__(TaskScheduler)
|
||||||
|
sched.ai_manager = None
|
||||||
|
sched.logger = self._stub_logger()
|
||||||
|
sched._shared_skill_manager = object()
|
||||||
|
with mock.patch("aibox.DeepSeekChat") as mc, \
|
||||||
|
mock.patch("aibox.SubprocessCommandExecutor"):
|
||||||
|
sched._create_task_ai()
|
||||||
|
assert mc.call_args.kwargs.get("model") is None
|
||||||
|
|
||||||
|
def test_parse_args_model(self):
|
||||||
|
"""CLI 解析:--model kimi → args.model"""
|
||||||
|
from aibox import parse_args
|
||||||
|
with mock.patch("sys.argv", ["aibox.py", "--model", "kimi"]):
|
||||||
|
args = parse_args()
|
||||||
|
assert args.model == "kimi"
|
||||||
|
|
||||||
|
|
||||||
# ==================== DeepSeekChat 档案级 reasoning 读取 ====================
|
# ==================== DeepSeekChat 档案级 reasoning 读取 ====================
|
||||||
|
|
||||||
class TestChatReasoningFlag:
|
class TestChatReasoningFlag:
|
||||||
|
|||||||
Reference in new issue
Block a user