diff --git a/backend/api/server.py b/backend/api/server.py index 0075477..2a38fd0 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -2283,15 +2283,27 @@ def _feishu_paths(state: AgentState, account_id: str | None) -> dict[str, Path]: def _feishu_command() -> list[str]: return [ - 'npx', + _feishu_npx_executable(), '-y', f'--registry={FEISHU_NPM_REGISTRY}', FEISHU_MCP_PACKAGE, ] +def _feishu_npx_executable() -> str: + explicit = os.environ.get('CLAW_NPX_BIN') + if explicit and Path(explicit).expanduser().exists(): + return str(Path(explicit).expanduser()) + node_bin_dir = os.environ.get('CLAW_NODE_BIN_DIR') + if node_bin_dir: + candidate = Path(node_bin_dir).expanduser() / 'npx' + if candidate.exists(): + return str(candidate) + return shutil.which('npx') or 'npx' + + def _feishu_env(paths: dict[str, Path]) -> dict[str, str]: - return { + env = { 'HOME': str(paths['home']), 'XDG_CONFIG_HOME': str(paths['config']), 'npm_config_cache': str(paths['npm_cache']), @@ -2299,6 +2311,10 @@ def _feishu_env(paths: dict[str, Path]) -> dict[str, str]: 'NO_UPDATE_NOTIFIER': 'true', 'FEISHU_LOGIN_MODE': 'devicecode', } + node_bin_dir = os.environ.get('CLAW_NODE_BIN_DIR') + if node_bin_dir: + env['PATH'] = f'{node_bin_dir}{os.pathsep}{os.environ.get("PATH", "")}' + return env def _run_feishu_cli( diff --git a/scripts/start-backend.sh b/scripts/start-backend.sh index 89d229c..3f7049d 100755 --- a/scripts/start-backend.sh +++ b/scripts/start-backend.sh @@ -20,6 +20,15 @@ export OPENAI_API_KEY export OPENAI_BASE_URL="${OPENAI_BASE_URL:-http://model.mify.ai.srv/v1}" export OPENAI_MODEL="${OPENAI_MODEL:-xiaomi/mimo-v2-flash}" +# 后端也会调用 Node 生态工具,例如 feishu-mcp-pro。systemd 不读取交互式 +# shell 的 nvm 配置,所以需要把部署时记录的 Node 路径显式传给 Python 进程。 +if [[ -n "${CLAW_NODE_BIN_DIR:-}" ]]; then + export PATH="${CLAW_NODE_BIN_DIR}:${PATH}" +fi +export CLAW_NPM_BIN="${CLAW_NPM_BIN:-}" +export CLAW_NODE_BIN="${CLAW_NODE_BIN:-}" +export CLAW_NODE_BIN_DIR="${CLAW_NODE_BIN_DIR:-}" + BACKEND_HOST="${CLAW_BACKEND_HOST:-127.0.0.1}" BACKEND_PORT="${CLAW_BACKEND_PORT:-8765}" PYTHON_BIN="${CLAW_PYTHON_BIN:-${ROOT_DIR}/.venv/bin/python}" diff --git a/tests/test_feishu_integration.py b/tests/test_feishu_integration.py index c4c9a98..d174902 100644 --- a/tests/test_feishu_integration.py +++ b/tests/test_feishu_integration.py @@ -114,6 +114,28 @@ class FeishuIntegrationTests(unittest.TestCase): self.assertEqual(url, 'https://mi.feishu.cn/docx/CZIldpM3QofbbXxxAq1cEininTd') + def test_feishu_command_uses_deployed_node_bin_dir(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir, patch.dict( + gui_server.os.environ, + {'CLAW_NODE_BIN_DIR': tmp_dir, 'PATH': ''}, + clear=False, + ): + npx_path = Path(tmp_dir) / 'npx' + npx_path.write_text('#!/usr/bin/env node\n', encoding='utf-8') + npx_path.chmod(0o755) + + command = gui_server._feishu_command() + env = gui_server._feishu_env( + { + 'home': Path(tmp_dir) / 'home', + 'config': Path(tmp_dir) / 'config', + 'npm_cache': Path(tmp_dir) / 'npm-cache', + } + ) + + self.assertEqual(command[0], str(npx_path)) + self.assertTrue(env['PATH'].startswith(tmp_dir)) + def test_create_online_doc_rejects_jsonl_for_now(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: client, state = _build_client(Path(tmp_dir))