opencode-codebuddy-external-auth 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/plugin.js +198 -22
  2. package/package.json +2 -1
package/dist/plugin.js CHANGED
@@ -6,12 +6,8 @@ exports.CodeBuddyExternalAuthPlugin = void 0;
6
6
  // ============================================================================
7
7
  const PROVIDER_ID = "codebuddy-external";
8
8
  const CONFIG = {
9
- // IOA 版本使用 copilot.tencent.com
9
+ // IOA 版本使用 copilot.tencent.com 进行认证
10
10
  serverUrl: "https://copilot.tencent.com",
11
- // API 端点 - CodeBuddy IOA 直接使用 endpoint 作为 baseURL
12
- // OpenAI SDK 会自动追加 /chat/completions
13
- // 注意:不需要 /v1 前缀,CodeBuddy 服务端直接接收 /chat/completions
14
- apiBaseUrl: "https://copilot.tencent.com",
15
11
  // 平台标识
16
12
  platform: "CLI",
17
13
  appVersion: "2.37.20",
@@ -23,22 +19,199 @@ function sleep(ms) {
23
19
  return new Promise((resolve) => setTimeout(resolve, ms));
24
20
  }
25
21
  /**
26
- * Creates an authenticated fetch function with CodeBuddy headers
22
+ * Convert OpenAI chat messages to a single prompt string
27
23
  */
28
- function createAuthenticatedFetch(accessToken, userId) {
24
+ function convertMessagesToPrompt(messages) {
25
+ let systemPrompt;
26
+ const conversationParts = [];
27
+ for (const msg of messages) {
28
+ if (msg.role === "system") {
29
+ systemPrompt = msg.content;
30
+ }
31
+ else if (msg.role === "user") {
32
+ conversationParts.push(`User: ${msg.content}`);
33
+ }
34
+ else if (msg.role === "assistant") {
35
+ conversationParts.push(`Assistant: ${msg.content}`);
36
+ }
37
+ }
38
+ // If there's only one user message, use it directly
39
+ const userMessages = messages.filter((m) => m.role === "user");
40
+ if (userMessages.length === 1 && conversationParts.length === 1) {
41
+ return {
42
+ prompt: userMessages[0].content,
43
+ systemPrompt,
44
+ };
45
+ }
46
+ return {
47
+ prompt: conversationParts.join("\n\n"),
48
+ systemPrompt,
49
+ };
50
+ }
51
+ /**
52
+ * Create a streaming response in OpenAI format from CodeBuddy response
53
+ */
54
+ function createOpenAIStreamResponse(text, model) {
55
+ const encoder = new TextEncoder();
56
+ return new ReadableStream({
57
+ start(controller) {
58
+ // Send the content in a single chunk
59
+ const chunk = {
60
+ id: `chatcmpl-${Date.now()}`,
61
+ object: "chat.completion.chunk",
62
+ created: Math.floor(Date.now() / 1000),
63
+ model: model,
64
+ choices: [
65
+ {
66
+ index: 0,
67
+ delta: { content: text },
68
+ finish_reason: null,
69
+ },
70
+ ],
71
+ };
72
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
73
+ // Send done signal
74
+ const doneChunk = {
75
+ id: `chatcmpl-${Date.now()}`,
76
+ object: "chat.completion.chunk",
77
+ created: Math.floor(Date.now() / 1000),
78
+ model: model,
79
+ choices: [
80
+ {
81
+ index: 0,
82
+ delta: {},
83
+ finish_reason: "stop",
84
+ },
85
+ ],
86
+ };
87
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(doneChunk)}\n\n`));
88
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
89
+ controller.close();
90
+ },
91
+ });
92
+ }
93
+ /**
94
+ * Create a non-streaming response in OpenAI format
95
+ */
96
+ function createOpenAIResponse(text, model) {
97
+ return JSON.stringify({
98
+ id: `chatcmpl-${Date.now()}`,
99
+ object: "chat.completion",
100
+ created: Math.floor(Date.now() / 1000),
101
+ model: model,
102
+ choices: [
103
+ {
104
+ index: 0,
105
+ message: {
106
+ role: "assistant",
107
+ content: text,
108
+ },
109
+ finish_reason: "stop",
110
+ },
111
+ ],
112
+ usage: {
113
+ prompt_tokens: 0,
114
+ completion_tokens: 0,
115
+ total_tokens: 0,
116
+ },
117
+ });
118
+ }
119
+ /**
120
+ * Execute codebuddy CLI command and return result
121
+ */
122
+ async function executeCodeBuddyCLI(prompt, model, systemPrompt) {
123
+ const { spawn } = await import("child_process");
124
+ return new Promise((resolve, reject) => {
125
+ const args = ["-p", "--output-format", "text"];
126
+ if (model) {
127
+ args.push("--model", model);
128
+ }
129
+ if (systemPrompt) {
130
+ args.push("--system-prompt", systemPrompt);
131
+ }
132
+ args.push(prompt);
133
+ console.log(`[codebuddy-external] Executing: codebuddy ${args.join(" ").substring(0, 100)}...`);
134
+ const child = spawn("codebuddy", args, {
135
+ env: { ...process.env },
136
+ stdio: ["pipe", "pipe", "pipe"],
137
+ });
138
+ let stdout = "";
139
+ let stderr = "";
140
+ child.stdout.on("data", (data) => {
141
+ stdout += data.toString();
142
+ });
143
+ child.stderr.on("data", (data) => {
144
+ stderr += data.toString();
145
+ });
146
+ child.on("close", (code) => {
147
+ if (code === 0) {
148
+ resolve(stdout.trim());
149
+ }
150
+ else {
151
+ reject(new Error(`CodeBuddy CLI exited with code ${code}: ${stderr}`));
152
+ }
153
+ });
154
+ child.on("error", (err) => {
155
+ reject(new Error(`Failed to spawn codebuddy: ${err.message}`));
156
+ });
157
+ // Timeout after 5 minutes
158
+ setTimeout(() => {
159
+ child.kill();
160
+ reject(new Error("CodeBuddy CLI timeout after 5 minutes"));
161
+ }, 5 * 60 * 1000);
162
+ });
163
+ }
164
+ /**
165
+ * Creates a fetch function that invokes codebuddy CLI
166
+ * and converts between OpenAI and CodeBuddy formats
167
+ */
168
+ function createProxyFetch() {
29
169
  return async (url, init) => {
30
- const headers = new Headers(init?.headers);
31
- // Bearer token authentication
32
- headers.set("Authorization", `Bearer ${accessToken}`);
33
- // User identification (URL encoded)
34
- if (userId) {
35
- headers.set("X-User-Id", encodeURIComponent(userId));
170
+ const urlStr = url.toString();
171
+ // Check if this is a chat completions request
172
+ if (urlStr.includes("/chat/completions") || urlStr.includes("/v1/chat/completions")) {
173
+ try {
174
+ // Parse the OpenAI request
175
+ const body = init?.body;
176
+ if (!body) {
177
+ return new Response(JSON.stringify({ error: "Missing request body" }), {
178
+ status: 400,
179
+ headers: { "Content-Type": "application/json" },
180
+ });
181
+ }
182
+ const openaiRequest = JSON.parse(typeof body === "string" ? body : await new Response(body).text());
183
+ // Convert to CodeBuddy format
184
+ const { prompt, systemPrompt } = convertMessagesToPrompt(openaiRequest.messages);
185
+ console.log(`[codebuddy-external] Invoking CodeBuddy CLI`);
186
+ console.log(`[codebuddy-external] Model: ${openaiRequest.model}, Prompt length: ${prompt.length}`);
187
+ // Execute codebuddy CLI
188
+ const resultText = await executeCodeBuddyCLI(prompt, openaiRequest.model, systemPrompt);
189
+ // Convert response to OpenAI format
190
+ if (openaiRequest.stream) {
191
+ return new Response(createOpenAIStreamResponse(resultText, openaiRequest.model), {
192
+ headers: {
193
+ "Content-Type": "text/event-stream",
194
+ "Cache-Control": "no-cache",
195
+ "Connection": "keep-alive",
196
+ },
197
+ });
198
+ }
199
+ else {
200
+ return new Response(createOpenAIResponse(resultText, openaiRequest.model), {
201
+ headers: { "Content-Type": "application/json" },
202
+ });
203
+ }
204
+ }
205
+ catch (error) {
206
+ console.error(`[codebuddy-external] CLI error:`, error);
207
+ return new Response(JSON.stringify({ error: `CodeBuddy CLI error: ${error}` }), {
208
+ status: 500,
209
+ headers: { "Content-Type": "application/json" },
210
+ });
211
+ }
36
212
  }
37
- // Request tracing
38
- headers.set("X-B3-TraceId", crypto.randomUUID().replace(/-/g, ""));
39
- headers.set("X-B3-SpanId", crypto.randomUUID().substring(0, 16));
40
- headers.set("X-B3-Sampled", "1");
41
- return fetch(url, { ...init, headers });
213
+ // For non-chat-completions requests, pass through normally
214
+ return fetch(url, init);
42
215
  };
43
216
  }
44
217
  // ============================================================================
@@ -165,10 +338,12 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
165
338
  async loader(getAuth) {
166
339
  const auth = await getAuth();
167
340
  if (auth.type === "oauth" && auth.access) {
341
+ // 返回代理 fetch 函数,通过 codebuddy CLI 处理请求
342
+ // baseURL 可以是任意值,因为 fetch 会拦截并调用 CLI
168
343
  return {
169
- apiKey: auth.access,
170
- baseURL: CONFIG.apiBaseUrl,
171
- fetch: createAuthenticatedFetch(auth.access, auth.userId),
344
+ apiKey: "cli-proxy", // 占位符,实际认证由 codebuddy CLI 处理
345
+ baseURL: "http://localhost", // 占位符,fetch 会拦截请求
346
+ fetch: createProxyFetch(),
172
347
  };
173
348
  }
174
349
  return {};
@@ -225,7 +400,8 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
225
400
  if (input.model.providerID !== PROVIDER_ID) {
226
401
  return;
227
402
  }
228
- output.options.baseURL = CONFIG.apiBaseUrl;
403
+ // 占位符,实际请求由 fetch 函数拦截并通过 CLI 处理
404
+ output.options.baseURL = "http://localhost";
229
405
  },
230
406
  };
231
407
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codebuddy-external-auth",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "OpenCode plugin for CodeBuddy External (IOA) authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,6 +24,7 @@
24
24
  "devDependencies": {
25
25
  "@opencode-ai/plugin": "^1.0.168",
26
26
  "@opencode-ai/sdk": "^1.0.168",
27
+ "@types/node": "^25.0.10",
27
28
  "typescript": "^5.7.2"
28
29
  },
29
30
  "files": [