fix: v2.0.0 - 修复 MCP 加载 + HOME 路径 JSON 转义

P0: mcpServers 已合并到 settings.template.json (settings.local.json 不被加载)
P2: {{HOME}} 替换改用正斜杠, 避免 \U \l 等非法 JSON 转义
新增: diagnose-tooluse.js 中转站工具调用诊断脚本

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
bookworm 2026-04-10 18:01:40 +08:00
parent 844419b951
commit 7d0e4edd16
2 changed files with 186 additions and 5 deletions

View File

@ -16,7 +16,7 @@ param(
$ErrorActionPreference = "Stop"
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
$BWVersion = "1.9.0"
$BWVersion = "2.0.0"
# ─── B4: 单实例保护 (防止双击两次导致竞态) ─────────
$mutexCreated = $false
@ -1081,8 +1081,10 @@ if (Test-Path $templateFile) {
$content = Get-Content $templateFile -Raw
$content = $content -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot
$content = $content -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\')
$content = $content -replace '\{\{PWSH_PATH\}\}', ($pwshJsonPath -replace '\\', '\\')
# HOME 必须用正斜杠: C:\Users\x 中 \U 是非法 JSON 转义序列
$homeForward = $homeDir.Replace('\', '/')
$content = $content -replace '\{\{HOME\}\}', $homeForward
$content = $content -replace '\{\{PWSH_PATH\}\}', $pwshJsonPath
Set-Content $settingsFile -Value $content -Encoding UTF8
Log-OK "settings.json 已渲染 (ROOT=$claudeRoot, SHELL=$pwshJsonPath)"
@ -1093,9 +1095,9 @@ if (Test-Path $templateFile) {
if (Test-Path $localTpl) {
$lc = Get-Content $localTpl -Raw
$lc = $lc -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot
$lc = $lc -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\')
$lc = $lc -replace '\{\{HOME\}\}', $homeForward
$lc = $lc -replace '\{\{USERNAME\}\}', $env:USERNAME
$lc = $lc -replace '\{\{PWSH_PATH\}\}', ($pwshJsonPath -replace '\\', '\\')
$lc = $lc -replace '\{\{PWSH_PATH\}\}', $pwshJsonPath
Set-Content $localSet -Value $lc -Encoding UTF8
Log-OK "settings.local.json 已渲染"
}

179
diagnose-tooluse.js Normal file
View File

@ -0,0 +1,179 @@
#!/usr/bin/env node
/**
* Bookworm tool_use 诊断脚本
* 测试中转站是否正确透传 Anthropic tool_use 协议
* 用法: node diagnose-tooluse.js
*/
const https = require('https');
const http = require('http');
const API_KEY = process.env.ANTHROPIC_API_KEY;
const BASE_URL = process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com';
if (!API_KEY) {
console.error('[FAIL] ANTHROPIC_API_KEY 未设置');
process.exit(1);
}
console.log(`[INFO] 中转站: ${BASE_URL}`);
console.log(`[INFO] API Key: ${API_KEY.substring(0, 8)}...`);
console.log('');
// 测试 1: 非流式 tool_use
async function testToolUse() {
const url = new URL('/v1/messages', BASE_URL);
const isHttps = url.protocol === 'https:';
const lib = isHttps ? https : http;
const body = JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 200,
tools: [{
name: 'get_disk_info',
description: 'Get disk space information',
input_schema: {
type: 'object',
properties: { drive: { type: 'string', description: 'Drive letter' } },
required: ['drive']
}
}],
tool_choice: { type: 'tool', name: 'get_disk_info' },
messages: [{ role: 'user', content: 'Check disk C:' }]
});
return new Promise((resolve, reject) => {
const req = lib.request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
'anthropic-version': '2023-06-01'
}
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve({ status: res.statusCode, body: json });
} catch (e) {
resolve({ status: res.statusCode, raw: data.substring(0, 500) });
}
});
});
req.on('error', reject);
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
req.write(body);
req.end();
});
}
// 测试 2: 流式 tool_use
async function testStreamToolUse() {
const url = new URL('/v1/messages', BASE_URL);
const isHttps = url.protocol === 'https:';
const lib = isHttps ? https : http;
const body = JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 200,
stream: true,
tools: [{
name: 'get_disk_info',
description: 'Get disk space information',
input_schema: {
type: 'object',
properties: { drive: { type: 'string', description: 'Drive letter' } },
required: ['drive']
}
}],
tool_choice: { type: 'tool', name: 'get_disk_info' },
messages: [{ role: 'user', content: 'Check disk C:' }]
});
return new Promise((resolve, reject) => {
const req = lib.request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
'anthropic-version': '2023-06-01'
}
}, (res) => {
let data = '';
let hasToolUseStart = false;
let hasInputJsonDelta = false;
res.on('data', chunk => {
const text = chunk.toString();
data += text;
if (text.includes('"type":"tool_use"') || text.includes('"type": "tool_use"')) hasToolUseStart = true;
if (text.includes('input_json_delta')) hasInputJsonDelta = true;
});
res.on('end', () => {
resolve({
status: res.statusCode,
hasToolUseStart,
hasInputJsonDelta,
sample: data.substring(0, 800)
});
});
});
req.on('error', reject);
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
req.write(body);
req.end();
});
}
(async () => {
// 测试 1: 非流式
console.log('=== 测试 1: 非流式 tool_use ===');
try {
const r = await testToolUse();
if (r.status !== 200) {
console.log(`[FAIL] HTTP ${r.status}`);
console.log(JSON.stringify(r.body || r.raw, null, 2).substring(0, 300));
} else {
const content = r.body?.content || [];
const toolUseBlock = content.find(b => b.type === 'tool_use');
if (toolUseBlock) {
console.log(`[PASS] 收到 tool_use block: name=${toolUseBlock.name}, id=${toolUseBlock.id}`);
console.log(` input: ${JSON.stringify(toolUseBlock.input)}`);
} else {
console.log('[FAIL] 未收到 tool_use block!');
console.log(' content types:', content.map(b => b.type).join(', ') || '(empty)');
console.log(' stop_reason:', r.body?.stop_reason);
if (content[0]?.text) console.log(' text:', content[0].text.substring(0, 200));
}
}
} catch (e) {
console.log(`[FAIL] 请求失败: ${e.message}`);
}
console.log('');
// 测试 2: 流式
console.log('=== 测试 2: 流式 tool_use (Claude Code 实际使用模式) ===');
try {
const r = await testStreamToolUse();
if (r.status !== 200) {
console.log(`[FAIL] HTTP ${r.status}`);
console.log(r.sample?.substring(0, 300));
} else {
console.log(` tool_use block in stream: ${r.hasToolUseStart ? '[PASS]' : '[FAIL]'}`);
console.log(` input_json_delta events: ${r.hasInputJsonDelta ? '[PASS]' : '[FAIL]'}`);
if (!r.hasToolUseStart) {
console.log(' stream sample:', r.sample?.substring(0, 400));
}
}
} catch (e) {
console.log(`[FAIL] 请求失败: ${e.message}`);
}
console.log('');
console.log('=== 诊断结论 ===');
console.log('如果两项测试均 PASS: 中转站 tool_use 正常');
console.log('如果非流式 PASS 流式 FAIL: 中转站流式 SSE 处理有 bug, 联系管理员升级 NewAPI');
console.log('如果均 FAIL: 中转站不支持 Anthropic tool_use, 需开启原生 Anthropic 透传模式');
})();