直连接口测试成功

This commit is contained in:
2026-04-23 04:36:57 +08:00
parent 5bc69bcd5b
commit a1587b8d12
15 changed files with 2694 additions and 7 deletions

221
src/direct-chat.ts Normal file
View File

@@ -0,0 +1,221 @@
import { randomBytes, randomUUID } from 'node:crypto';
import { existsSync, readFileSync } from 'node:fs';
import { createInterface } from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import { gzipSync } from 'node:zlib';
type ChatMessage = {
role: 'system' | 'user' | 'assistant';
content: MessageContent;
};
type MessageContent = string | TextBlock[];
type TextBlock = {
type: 'text';
text: string;
};
const endpoint = process.env.CODEBUDDY_DIRECT_ENDPOINT ?? 'https://copilot.tencent.com/v2/chat/completions';
const model = process.env.CODEBUDDY_MODEL ?? 'minimax-m2.7';
const apiKey = loadApiKey();
const systemPrompt = loadSystemPrompt();
const cliUserContextBlocks = loadCliUserContextBlocks();
const messages: ChatMessage[] = [];
async function main(): Promise<void> {
const rl = createInterface({ input, output });
console.log(`CodeBuddy direct chat demo. model=${model}`);
console.log('输入内容回车发送,/exit 退出,/clear 清空上下文。');
try {
while (true) {
const text = (await question(rl, '\n你> '))?.trim();
if (text === undefined) break;
if (!text) continue;
if (text === '/exit' || text === '/quit') break;
if (text === '/clear') {
messages.length = 0;
console.log('上下文已清空。');
continue;
}
messages.push({ role: 'user', content: buildUserContent(text) });
process.stdout.write('\n助手> ');
const answer = await sendChat(messages);
messages.push({ role: 'assistant', content: answer });
process.stdout.write('\n');
}
} finally {
rl.close();
}
}
async function question(rl: ReturnType<typeof createInterface>, prompt: string): Promise<string | undefined> {
try {
return await rl.question(prompt);
} catch (error) {
if (error instanceof Error && /readline was closed/i.test(error.message)) return undefined;
throw error;
}
}
async function sendChat(history: ChatMessage[]): Promise<string> {
const response = await fetch(endpoint, {
method: 'POST',
headers: headers(),
body: gzipSync(JSON.stringify({
model,
messages: buildRequestMessages(history),
stream: true,
stream_options: { include_usage: true },
temperature: Number(process.env.CODEBUDDY_TEMPERATURE ?? 1),
max_tokens: Number(process.env.CODEBUDDY_MAX_TOKENS ?? 48000),
reasoning_effort: process.env.CODEBUDDY_REASONING_EFFORT ?? 'medium',
verbosity: process.env.CODEBUDDY_VERBOSITY ?? 'high',
reasoning_summary: process.env.CODEBUDDY_REASONING_SUMMARY ?? 'auto',
})),
});
if (!response.ok || !response.body) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const decoder = new TextDecoder();
let buffer = '';
let answer = '';
for await (const chunk of response.body) {
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split(/\r?\n/);
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.startsWith('data:')) continue;
const data = line.slice(5).trim();
if (!data || data === '[DONE]') continue;
const delta = parseDelta(data);
if (delta) {
answer += delta;
process.stdout.write(delta);
}
}
}
return answer;
}
function buildRequestMessages(history: ChatMessage[]): ChatMessage[] {
return systemPrompt ? [{ role: 'system', content: systemPrompt }, ...history] : history;
}
function buildUserContent(text: string): MessageContent {
if (process.env.CODEBUDDY_DISABLE_USER_QUERY_WRAP === '1') return text;
return [
...cliUserContextBlocks,
{ type: 'text', text: `<user_query>${text}</user_query>` },
];
}
function parseDelta(data: string): string {
try {
const event = JSON.parse(data) as {
choices?: Array<{ delta?: { content?: string } }>;
};
return event.choices?.[0]?.delta?.content ?? '';
} catch {
return '';
}
}
function headers(): Record<string, string> {
const traceId = randomBytes(16).toString('hex');
const spanId = randomBytes(8).toString('hex');
return {
'content-type': 'application/json',
'content-encoding': 'gzip',
accept: 'application/json',
'x-requested-with': 'XMLHttpRequest',
authorization: `Bearer ${apiKey}`,
'x-api-key': apiKey,
'x-conversation-id': randomUUID(),
'x-conversation-request-id': randomBytes(16).toString('hex'),
'x-conversation-message-id': randomBytes(16).toString('hex'),
'x-agent-intent': 'craft',
'x-ide-type': 'CLI',
'x-ide-name': 'CLI',
'x-ide-version': '2.93.3',
'user-agent': process.env.CODEBUDDY_USER_AGENT
?? 'CLI/2.93.3 CodeBuddy/2.93.3 CodeBuddy Agent SDK/0.3.28 (Node.js/25.2.1) CodeBuddy Code/2.93.3',
'x-trace-id': traceId,
'x-request-id': traceId,
b3: `${traceId}-${spanId}-1-`,
'x-b3-traceid': traceId,
'x-b3-parentspanid': '',
'x-b3-spanid': spanId,
'x-b3-sampled': '1',
'x-codebuddy-request': '1',
'x-user-id': `anonymous_${apiKey.slice(-8)}`,
'x-product': 'SaaS',
};
}
function loadApiKey(): string {
const fromEnv = process.env.CODEBUDDY_API_KEY?.trim();
if (fromEnv) return fromEnv;
const file = process.env.CODEBUDDY_APIKEY_FILE ?? 'apikey';
if (!existsSync(file)) {
throw new Error(`Missing API key. Set CODEBUDDY_API_KEY or create ${file}.`);
}
const key = readFileSync(file, 'utf8')
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line && !line.startsWith('#'));
if (!key) throw new Error(`${file} has no usable API key line.`);
return key;
}
function loadSystemPrompt(): string {
if (process.env.CODEBUDDY_DISABLE_SYSTEM_PROMPT === '1') return '';
const inline = process.env.CODEBUDDY_SYSTEM_PROMPT?.trim();
if (inline) return inline;
const file = process.env.CODEBUDDY_SYSTEM_PROMPT_FILE ?? 'captures/codebuddy-system-prompt.txt';
if (!existsSync(file)) return '';
return readFileSync(file, 'utf8').trim();
}
function loadCliUserContextBlocks(): TextBlock[] {
if (process.env.CODEBUDDY_DISABLE_CLI_USER_CONTEXT === '1') return [];
const file = process.env.CODEBUDDY_CLI_CAPTURE_FILE ?? 'captures/codebuddy-chat-completion-full.redacted.json';
if (!existsSync(file)) return [];
try {
const capture = JSON.parse(readFileSync(file, 'utf8')) as {
request_body?: { messages?: Array<{ role?: string; content?: unknown }> };
};
const userMessage = capture.request_body?.messages?.find((message) => message.role === 'user');
if (!Array.isArray(userMessage?.content)) return [];
return userMessage.content
.filter(isTextBlock)
.filter((block) => !block.text.includes('<user_query>'))
.map((block) => ({ type: 'text', text: block.text }));
} catch {
return [];
}
}
function isTextBlock(value: unknown): value is TextBlock {
if (!value || typeof value !== 'object') return false;
const block = value as Partial<TextBlock>;
return block.type === 'text' && typeof block.text === 'string';
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});