diff --git a/aibox/README.md b/aibox/README.md index b76cc87..447cab8 100644 --- a/aibox/README.md +++ b/aibox/README.md @@ -552,7 +552,7 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i | `reasoning_enabled` | 档案级思考模式开关(缺省回退 `api.reasoning_enabled`) | | `timeout` | 请求超时秒数 | -**选择优先级**:`--model <档案名>`(命中 `models` 节)→ `api.default_model` 指向档案 → 回退旧 `api` 扁平节。 +**选择优先级**:`--model <档案名>`(命中 `models` 节)→ `api.default_model` 指向档案 → 回退旧 `api` 扁平节。`--model` 为**进程级覆盖**:WebSocket 服务模式(主对话、每个会话实例、定时任务实例)与 `--once` 一次性模式均生效。 `--model` 传**裸模型名**(未命中档案)时,沿用默认端点与 Key,仅覆盖模型名——即 "同端点换模型" 语义。 @@ -657,7 +657,7 @@ python aibox.py [选项] --show-reasoning 一次性模式输出思考流(默认静默,走 stderr) --no-save-memory 一次性模式禁用记忆工具(干净跳过,不进请求 schema) --worker-only 一次性模式纯执行面(worker profile,无人格/记忆工具) - --model NAME 模型档案名(models 节)或裸模型名(同端点换模型) + --model NAME 模型档案名(models 节)或裸模型名(同端点换模型);服务模式与 --once 均生效(进程级覆盖) --temperature T 覆盖配置的对话温度 --once-timeout SEC 一次性模式全局超时(默认 300 秒) ``` diff --git a/aibox/aibox.py b/aibox/aibox.py index faa756f..beaa22a 100644 --- a/aibox/aibox.py +++ b/aibox/aibox.py @@ -116,9 +116,11 @@ class TaskScheduler: # 每个任务使用独立的命令执行器,互不共享,避免 cancel() 误杀其他任务的子进程 command_executor=SubprocessCommandExecutor(self.logger), 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): """设置WebSocket处理器,用于广播任务执行状态""" self.ws_handler = handler @@ -1938,7 +1940,9 @@ class WebSocketHandler: # 每个连接使用独立的命令执行器,互不共享,避免 cancel() 误杀其他用户/任务正在运行的子进程 command_executor=SubprocessCommandExecutor(self.logger), 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: @@ -2569,18 +2573,24 @@ class WebSocketHandler: class EnhancedAIManager: """增强的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.dialogue_history: List[Dict] = [] self.config = ConfigManager() self.command_executor = command_executor or SubprocessCommandExecutor() self.prompt_manager = PromptManager() # 创建提示词管理器 self.scheduler: Optional[TaskScheduler] = None # 任务调度器 - + # H3:CLI --model 进程级覆盖(档案名或裸模型名),传导到本进程创建的所有 AI 实例 + # (主对话 add_ai / WebSocket 会话 / 定时任务),此前服务模式该参数被静默忽略 + self.model = model + if debug_level is None: debug_level = self.config.get("system.debug_level", 1) - + self.logger = Logger("Manager", debug_level) + if model: + self.logger.info(f"CLI 模型覆盖已启用: {model}") self.running = True # 确保 ~/.aibox 目录存在 @@ -2628,7 +2638,8 @@ class EnhancedAIManager: memory_db=self.memory_db, memo_db=self.memo_db, command_executor=self.command_executor, - prompt_manager=self.prompt_manager + prompt_manager=self.prompt_manager, + model=self.model # H3:--model 进程级覆盖 ) self.ai = ai @@ -2852,7 +2863,7 @@ def parse_args(): parser.add_argument('--no-save-memory', action='store_true', help='一次性对话不注册记忆工具,禁止写入记忆数据库') parser.add_argument('--show-reasoning', action='store_true', help='一次性对话在 stderr 显示思考过程流(默认不显示)') 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,越低越确定)') return parser.parse_args() @@ -3142,7 +3153,8 @@ def main(): manager = EnhancedAIManager( debug_level=args.debug, - command_executor=command_executor + command_executor=command_executor, + model=args.model # H3:--model 服务模式同样生效(进程级覆盖) ) if handle_check_commands(manager): diff --git a/aibox/tests/test_models_profile.py b/aibox/tests/test_models_profile.py index 5765308..edb4b31 100644 --- a/aibox/tests/test_models_profile.py +++ b/aibox/tests/test_models_profile.py @@ -297,6 +297,97 @@ class TestH2SchemaValidation: 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 读取 ==================== class TestChatReasoningFlag: