直连接口测试成功
This commit is contained in:
221
src/direct-chat.ts
Normal file
221
src/direct-chat.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user