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:
parent
844419b951
commit
7d0e4edd16
@ -16,7 +16,7 @@ param(
|
|||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
|
# ─── 版本号 (每次更新递增, build.ps1 自动读取) ──────
|
||||||
$BWVersion = "1.9.0"
|
$BWVersion = "2.0.0"
|
||||||
|
|
||||||
# ─── B4: 单实例保护 (防止双击两次导致竞态) ─────────
|
# ─── B4: 单实例保护 (防止双击两次导致竞态) ─────────
|
||||||
$mutexCreated = $false
|
$mutexCreated = $false
|
||||||
@ -1081,8 +1081,10 @@ if (Test-Path $templateFile) {
|
|||||||
|
|
||||||
$content = Get-Content $templateFile -Raw
|
$content = Get-Content $templateFile -Raw
|
||||||
$content = $content -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot
|
$content = $content -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot
|
||||||
$content = $content -replace '\{\{HOME\}\}', ($homeDir -replace '\\', '\\')
|
# HOME 必须用正斜杠: C:\Users\x 中 \U 是非法 JSON 转义序列
|
||||||
$content = $content -replace '\{\{PWSH_PATH\}\}', ($pwshJsonPath -replace '\\', '\\')
|
$homeForward = $homeDir.Replace('\', '/')
|
||||||
|
$content = $content -replace '\{\{HOME\}\}', $homeForward
|
||||||
|
$content = $content -replace '\{\{PWSH_PATH\}\}', $pwshJsonPath
|
||||||
|
|
||||||
Set-Content $settingsFile -Value $content -Encoding UTF8
|
Set-Content $settingsFile -Value $content -Encoding UTF8
|
||||||
Log-OK "settings.json 已渲染 (ROOT=$claudeRoot, SHELL=$pwshJsonPath)"
|
Log-OK "settings.json 已渲染 (ROOT=$claudeRoot, SHELL=$pwshJsonPath)"
|
||||||
@ -1093,9 +1095,9 @@ if (Test-Path $templateFile) {
|
|||||||
if (Test-Path $localTpl) {
|
if (Test-Path $localTpl) {
|
||||||
$lc = Get-Content $localTpl -Raw
|
$lc = Get-Content $localTpl -Raw
|
||||||
$lc = $lc -replace '\{\{CLAUDE_ROOT\}\}', $claudeRoot
|
$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 '\{\{USERNAME\}\}', $env:USERNAME
|
||||||
$lc = $lc -replace '\{\{PWSH_PATH\}\}', ($pwshJsonPath -replace '\\', '\\')
|
$lc = $lc -replace '\{\{PWSH_PATH\}\}', $pwshJsonPath
|
||||||
Set-Content $localSet -Value $lc -Encoding UTF8
|
Set-Content $localSet -Value $lc -Encoding UTF8
|
||||||
Log-OK "settings.local.json 已渲染"
|
Log-OK "settings.local.json 已渲染"
|
||||||
}
|
}
|
||||||
|
|||||||
179
diagnose-tooluse.js
Normal file
179
diagnose-tooluse.js
Normal 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 透传模式');
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue
Block a user