opencode-codebuddy-external-auth 1.0.7 → 1.0.9

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 +215 -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,216 @@ 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
+ const id = `chatcmpl-${Date.now()}`;
57
+ const created = Math.floor(Date.now() / 1000);
58
+ return new ReadableStream({
59
+ start(controller) {
60
+ // First chunk with role
61
+ const roleChunk = {
62
+ id,
63
+ object: "chat.completion.chunk",
64
+ created,
65
+ model,
66
+ choices: [
67
+ {
68
+ index: 0,
69
+ delta: { role: "assistant" },
70
+ finish_reason: null,
71
+ },
72
+ ],
73
+ };
74
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(roleChunk)}\n\n`));
75
+ // Content chunk
76
+ const contentChunk = {
77
+ id,
78
+ object: "chat.completion.chunk",
79
+ created,
80
+ model,
81
+ choices: [
82
+ {
83
+ index: 0,
84
+ delta: { content: text },
85
+ finish_reason: null,
86
+ },
87
+ ],
88
+ };
89
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(contentChunk)}\n\n`));
90
+ // Done chunk
91
+ const doneChunk = {
92
+ id,
93
+ object: "chat.completion.chunk",
94
+ created,
95
+ model,
96
+ choices: [
97
+ {
98
+ index: 0,
99
+ delta: {},
100
+ finish_reason: "stop",
101
+ },
102
+ ],
103
+ };
104
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(doneChunk)}\n\n`));
105
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
106
+ controller.close();
107
+ },
108
+ });
109
+ }
110
+ /**
111
+ * Create a non-streaming response in OpenAI format
112
+ */
113
+ function createOpenAIResponse(text, model) {
114
+ return JSON.stringify({
115
+ id: `chatcmpl-${Date.now()}`,
116
+ object: "chat.completion",
117
+ created: Math.floor(Date.now() / 1000),
118
+ model: model,
119
+ choices: [
120
+ {
121
+ index: 0,
122
+ message: {
123
+ role: "assistant",
124
+ content: text,
125
+ },
126
+ finish_reason: "stop",
127
+ },
128
+ ],
129
+ usage: {
130
+ prompt_tokens: 0,
131
+ completion_tokens: 0,
132
+ total_tokens: 0,
133
+ },
134
+ });
135
+ }
136
+ /**
137
+ * Execute codebuddy CLI command and return result
138
+ */
139
+ async function executeCodeBuddyCLI(prompt, model, systemPrompt) {
140
+ const { spawn } = await import("child_process");
141
+ return new Promise((resolve, reject) => {
142
+ const args = ["-p", "--output-format", "text"];
143
+ if (model) {
144
+ args.push("--model", model);
145
+ }
146
+ if (systemPrompt) {
147
+ args.push("--system-prompt", systemPrompt);
148
+ }
149
+ args.push(prompt);
150
+ console.log(`[codebuddy-external] Executing: codebuddy ${args.join(" ").substring(0, 100)}...`);
151
+ const child = spawn("codebuddy", args, {
152
+ env: { ...process.env },
153
+ stdio: ["pipe", "pipe", "pipe"],
154
+ });
155
+ let stdout = "";
156
+ let stderr = "";
157
+ child.stdout.on("data", (data) => {
158
+ stdout += data.toString();
159
+ });
160
+ child.stderr.on("data", (data) => {
161
+ stderr += data.toString();
162
+ });
163
+ child.on("close", (code) => {
164
+ if (code === 0) {
165
+ resolve(stdout.trim());
166
+ }
167
+ else {
168
+ reject(new Error(`CodeBuddy CLI exited with code ${code}: ${stderr}`));
169
+ }
170
+ });
171
+ child.on("error", (err) => {
172
+ reject(new Error(`Failed to spawn codebuddy: ${err.message}`));
173
+ });
174
+ // Timeout after 5 minutes
175
+ setTimeout(() => {
176
+ child.kill();
177
+ reject(new Error("CodeBuddy CLI timeout after 5 minutes"));
178
+ }, 5 * 60 * 1000);
179
+ });
180
+ }
181
+ /**
182
+ * Creates a fetch function that invokes codebuddy CLI
183
+ * and converts between OpenAI and CodeBuddy formats
184
+ */
185
+ function createProxyFetch() {
29
186
  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));
187
+ const urlStr = url.toString();
188
+ // Check if this is a chat completions request
189
+ if (urlStr.includes("/chat/completions") || urlStr.includes("/v1/chat/completions")) {
190
+ try {
191
+ // Parse the OpenAI request
192
+ const body = init?.body;
193
+ if (!body) {
194
+ return new Response(JSON.stringify({ error: "Missing request body" }), {
195
+ status: 400,
196
+ headers: { "Content-Type": "application/json" },
197
+ });
198
+ }
199
+ const openaiRequest = JSON.parse(typeof body === "string" ? body : await new Response(body).text());
200
+ // Convert to CodeBuddy format
201
+ const { prompt, systemPrompt } = convertMessagesToPrompt(openaiRequest.messages);
202
+ console.log(`[codebuddy-external] Invoking CodeBuddy CLI`);
203
+ console.log(`[codebuddy-external] Model: ${openaiRequest.model}, Prompt length: ${prompt.length}`);
204
+ // Execute codebuddy CLI
205
+ const resultText = await executeCodeBuddyCLI(prompt, openaiRequest.model, systemPrompt);
206
+ // Convert response to OpenAI format
207
+ if (openaiRequest.stream) {
208
+ return new Response(createOpenAIStreamResponse(resultText, openaiRequest.model), {
209
+ headers: {
210
+ "Content-Type": "text/event-stream",
211
+ "Cache-Control": "no-cache",
212
+ "Connection": "keep-alive",
213
+ },
214
+ });
215
+ }
216
+ else {
217
+ return new Response(createOpenAIResponse(resultText, openaiRequest.model), {
218
+ headers: { "Content-Type": "application/json" },
219
+ });
220
+ }
221
+ }
222
+ catch (error) {
223
+ console.error(`[codebuddy-external] CLI error:`, error);
224
+ return new Response(JSON.stringify({ error: `CodeBuddy CLI error: ${error}` }), {
225
+ status: 500,
226
+ headers: { "Content-Type": "application/json" },
227
+ });
228
+ }
36
229
  }
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 });
230
+ // For non-chat-completions requests, pass through normally
231
+ return fetch(url, init);
42
232
  };
43
233
  }
44
234
  // ============================================================================
@@ -165,10 +355,12 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
165
355
  async loader(getAuth) {
166
356
  const auth = await getAuth();
167
357
  if (auth.type === "oauth" && auth.access) {
358
+ // 返回代理 fetch 函数,通过 codebuddy CLI 处理请求
359
+ // baseURL 可以是任意值,因为 fetch 会拦截并调用 CLI
168
360
  return {
169
- apiKey: auth.access,
170
- baseURL: CONFIG.apiBaseUrl,
171
- fetch: createAuthenticatedFetch(auth.access, auth.userId),
361
+ apiKey: "cli-proxy", // 占位符,实际认证由 codebuddy CLI 处理
362
+ baseURL: "http://localhost", // 占位符,fetch 会拦截请求
363
+ fetch: createProxyFetch(),
172
364
  };
173
365
  }
174
366
  return {};
@@ -225,7 +417,8 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
225
417
  if (input.model.providerID !== PROVIDER_ID) {
226
418
  return;
227
419
  }
228
- output.options.baseURL = CONFIG.apiBaseUrl;
420
+ // 占位符,实际请求由 fetch 函数拦截并通过 CLI 处理
421
+ output.options.baseURL = "http://localhost";
229
422
  },
230
423
  };
231
424
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codebuddy-external-auth",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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": [