fix hermes provider command dispatch

This commit is contained in:
Haitao Pan 2026-04-22 11:51:07 +08:00
parent 5b5ffa86cd
commit bf1fcac528
8 changed files with 169 additions and 3 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
build/
.env
xworkmate-go-core-linux

View File

@ -183,7 +183,7 @@ HTTP 与 WebSocket 统一使用 **JSON-RPC 2.0** 作为默认通信协议。为
说明:
- `bridgeOrigin` 读取 `BRIDGE_SERVER_URL`
- `bridgeOrigin` 反映 bootstrap 阶段的 bridge metadata
- 默认值:`https://xworkmate-bridge.svc.plus`
## 4. Bridge JSON-RPC Methods
@ -838,7 +838,7 @@ bridge 在 session 执行期间会通过 `session.update` 推送通知,统一
| `ACP_ALLOWED_ORIGINS` | `https://xworkmate.svc.plus,http://localhost:*,http://127.0.0.1:*` | bridge allowed origins |
| `ACP_MULTI_AGENT_ENABLED` | `true` | `acp.capabilities` 中的 `multiAgent` 开关 |
| `ACP_MULTI_AGENT_MODEL` | `gpt-4o` | multi-agent 默认模型 |
| `BRIDGE_SERVER_URL` | `https://xworkmate-bridge.svc.plus` | bootstrap health 与外部调用统一使用的 bridge base URL |
| `BRIDGE_SERVER_URL` | `https://xworkmate-bridge.svc.plus` | bootstrap health 的 metadata 默认值,不作为 app runtime 真源 |
| `INTERNAL_SERVICE_TOKEN` | 空 | upstream provider / gateway 优先使用的内部服务 token |
| `BRIDGE_AUTH_TOKEN` | 空 | bridge 入站 bearer token`INTERNAL_SERVICE_TOKEN` 为空时也作为 upstream forwarding token |
| `IMAGE` | 空 | `/api/ping` 版本信息来源 |

View File

@ -16,7 +16,7 @@ See also:
- `assistant` surface 进入 ACP control-plane`acp.capabilities`、`xworkmate.routing.resolve`、`session.*`
- `settings` surface 进入 gateway runtime / connection flow`acp.capabilities`、`xworkmate.gateway.*`
不管 bridge 内部还保留哪些 provider / gateway mode / capability flagapp-facing 公共入口都只有 bridge origin。
不管 bridge 内部还保留哪些 provider / gateway mode / capability flagapp-facing 公共入口都只有 bridge origin`/acp-server/*` 和 `/gateway/openclaw` 属于 bridge-owned routing facts不是 app-owned truth
## Topology

View File

@ -144,6 +144,17 @@ func ResolveProviderCommand(
"-p",
prompt,
}
case "hermes":
binary := strings.TrimSpace(EnvOrDefault("ACP_HERMES_BIN", "hermes"))
if strings.TrimSpace(model) == "" {
return binary, []string{"-p", prompt}
}
return binary, []string{
"--model",
strings.TrimSpace(model),
"-p",
prompt,
}
default:
return "", nil
}

View File

@ -36,3 +36,19 @@ func TestNormalizeProviderWorkingDirectorySkipsUnknownProvider(t *testing.T) {
t.Fatalf("expected unknown provider to keep dir, got %q %q", got, effective)
}
}
func TestResolveProviderCommandSupportsHermes(t *testing.T) {
t.Setenv("ACP_HERMES_BIN", "/usr/local/bin/hermes")
command, args := ResolveProviderCommand("hermes", "sonnet", "hello world", "/tmp/work")
if command != "/usr/local/bin/hermes" {
t.Fatalf("expected hermes binary override, got %q", command)
}
if len(args) != 4 {
t.Fatalf("expected hermes args with model and prompt, got %#v", args)
}
if args[0] != "--model" || args[1] != "sonnet" || args[2] != "-p" || args[3] != "hello world" {
t.Fatalf("unexpected hermes args: %#v", args)
}
}

View File

@ -0,0 +1,78 @@
#!/usr/bin/env bash
# scripts/ci/verify_api_interface_contract.sh
set -euo pipefail
BRIDGE_SERVER_URL="${BRIDGE_SERVER_URL:-https://xworkmate-bridge.svc.plus}"
BRIDGE_AUTH_TOKEN="${BRIDGE_AUTH_TOKEN:-}"
if [[ -z "${BRIDGE_AUTH_TOKEN}" ]]; then
echo "Error: BRIDGE_AUTH_TOKEN is required" >&2
exit 1
fi
echo "--- Verifying API Interface Contract for $BRIDGE_SERVER_URL ---"
check_endpoint() {
local name=$1
local path=$2
local expected_status=$3
local content_type=$4
echo -n "Checking $name ($path)... "
local response_info
response_info=$(curl -s -o /tmp/resp.body -w "%{http_code} %{content_type}" \
-H "Authorization: Bearer $BRIDGE_AUTH_TOKEN" \
"$BRIDGE_SERVER_URL$path")
local status=$(echo "$response_info" | cut -d' ' -f1)
local actual_ct=$(echo "$response_info" | cut -d' ' -f2-)
if [[ "$status" == "$expected_status" ]]; then
if [[ "$actual_ct" == *"$content_type"* ]]; then
# 验证是否为有效的 JSON 且包含 ok: true
if jq -e '.ok == true' /tmp/resp.body >/dev/null 2>&1; then
echo "✅ OK ($status, application/json)"
else
echo "❌ Failed (Invalid Bridge Response Structure)"
cat /tmp/resp.body
return 1
fi
else
echo "❌ Failed: Wrong Content-Type (Expected $content_type, got $actual_ct)"
return 1
fi
else
echo "❌ Failed (Expected $expected_status, got $status)"
return 1
fi
}
# 现在的架构下,所有路径都应该由 Bridge 统一处理并返回 200 JSON
check_endpoint "OpenClaw" "/gateway/openclaw" "200" "application/json"
check_endpoint "OpenCode" "/acp-server/opencode" "200" "application/json"
check_endpoint "Codex" "/acp-server/codex" "200" "application/json"
check_endpoint "Gemini" "/acp-server/gemini" "200" "application/json"
check_endpoint "Hermes" "/acp-server/hermes" "200" "application/json"
# 6. Aggregate RPC Endpoint
echo -n "Checking Aggregate RPC (/acp/rpc)... "
rpc_status=$(curl -s -o /tmp/rpc.resp -w "%{http_code}" \
-X POST -H "Authorization: Bearer $BRIDGE_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"acp.capabilities","params":{},"id":1}' \
"$BRIDGE_SERVER_URL/acp/rpc")
if [[ "$rpc_status" == "200" ]]; then
if jq -e '.ok == true' /tmp/rpc.resp >/dev/null 2>&1; then
echo "✅ OK (200 + Valid JSON-RPC Result)"
else
echo "❌ Failed (Invalid JSON-RPC Response)"
cat /tmp/rpc.resp
exit 1
fi
else
echo "❌ Failed ($rpc_status)"
exit 1
fi
echo "Interface contract verification completed."

View File

@ -0,0 +1,60 @@
#!/usr/bin/env bash
# scripts/ci/verify_api_scenario_contract.sh
set -euo pipefail
BRIDGE_SERVER_URL="${BRIDGE_SERVER_URL:-https://xworkmate-bridge.svc.plus}"
BRIDGE_AUTH_TOKEN="${BRIDGE_AUTH_TOKEN:-}"
if [[ -z "${BRIDGE_AUTH_TOKEN}" ]]; then
echo "Error: BRIDGE_AUTH_TOKEN is required" >&2
exit 1
fi
echo "--- Verifying API Scenario Contract for $BRIDGE_SERVER_URL ---"
# Scenario: Discovery -> Initialize Session
# 1. Discover capabilities and find a provider
echo "Step 1: Discovery"
CAPS=$(curl -s -X POST -H "Authorization: Bearer $BRIDGE_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"acp.capabilities","params":{},"id":"scen-1"}' \
"$BRIDGE_SERVER_URL/acp/rpc")
FIRST_PROVIDER=$(echo "$CAPS" | python3 -c 'import json, sys; d=json.load(sys.stdin); print(d.get("result", {}).get("providerCatalog", [{}])[0].get("providerId", ""))')
if [[ -z "$FIRST_PROVIDER" ]]; then
echo "❌ Error: No providers found in catalog"
exit 1
fi
echo "✅ Found provider: $FIRST_PROVIDER"
# 2. Attempt session.start
echo "Step 2: Session Initialization"
SESSION_ID="test-scenario-$(date +%s)"
START_RESP=$(curl -s -X POST -H "Authorization: Bearer $BRIDGE_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"session.start\",\"params\":{\"sessionId\":\"$SESSION_ID\",\"routing\":{\"routingMode\":\"explicit\",\"explicitExecutionTarget\":\"singleAgent\",\"explicitProviderId\":\"$FIRST_PROVIDER\"}},\"id\":\"scen-2\"}" \
"$BRIDGE_SERVER_URL/acp/rpc")
# Check if response is valid JSON-RPC
if ! echo "$START_RESP" | jq . >/dev/null 2>&1; then
echo "❌ Error: session.start returned invalid JSON"
echo "$START_RESP"
exit 1
fi
# Analyze result
SUCCESS=$(echo "$START_RESP" | python3 -c 'import json, sys; d=json.load(sys.stdin); print(d.get("result", {}).get("success", "false"))')
ERROR_MSG=$(echo "$START_RESP" | python3 -c 'import json, sys; d=json.load(sys.stdin); print(d.get("error", {}).get("message", d.get("result", {}).get("error", "")))')
if [[ "$SUCCESS" == "True" ]]; then
echo "✅ Session started successfully"
elif [[ "$ERROR_MSG" == *"connection refused"* || "$ERROR_MSG" == *"ROUTING_REQUIRED"* ]]; then
echo "✅ Scenario logic verified (Gateway correctly routed but backend agent is unreachable: $ERROR_MSG)"
else
echo "❌ Session start failed with unexpected error: $ERROR_MSG"
echo "$START_RESP"
exit 1
fi
echo "Scenario contract verification completed."

Binary file not shown.