opencode-codebuddy-external-auth 1.0.14 → 1.0.15

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 +140 -67
  2. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -8,13 +8,22 @@ const PROVIDER_ID = "codebuddy-external";
8
8
  const CONFIG = {
9
9
  // IOA 版本使用 copilot.tencent.com 进行认证
10
10
  serverUrl: "https://copilot.tencent.com",
11
- // 本地 codebuddy --serve 代理地址
12
- localServeUrl: "http://127.0.0.1:3000",
11
+ // 真实对话 API 路径
12
+ chatCompletionsPath: "/v2/chat/completions",
13
13
  // 平台标识
14
14
  platform: "CLI",
15
15
  appVersion: "2.37.20",
16
- // 使用 HTTP API 模式还是 CLI 模式
16
+ ideName: "CLI",
17
+ ideType: "CLI",
18
+ domain: "tencent.sso.copilot.tencent.com",
19
+ product: "SaaS",
20
+ agentIntent: "craft",
21
+ // 使用 HTTP Auth API 模式还是 CLI 模式
17
22
  useHttpApi: true,
23
+ // 可通过环境变量注入(从零启动时必需)
24
+ tenantId: process.env.CODEBUDDY_TENANT_ID || "",
25
+ enterpriseId: process.env.CODEBUDDY_ENTERPRISE_ID || "",
26
+ userId: process.env.CODEBUDDY_USER_ID || "",
18
27
  };
19
28
  // ============================================================================
20
29
  // Utility Functions
@@ -22,6 +31,97 @@ const CONFIG = {
22
31
  function sleep(ms) {
23
32
  return new Promise((resolve) => setTimeout(resolve, ms));
24
33
  }
34
+ function generateUuid() {
35
+ if (globalThis.crypto?.randomUUID) {
36
+ return globalThis.crypto.randomUUID();
37
+ }
38
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
39
+ }
40
+ function decodeJwtPayload(token) {
41
+ try {
42
+ const parts = token.split(".");
43
+ if (parts.length < 2)
44
+ return null;
45
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
46
+ const pad = "=".repeat((4 - (payload.length % 4)) % 4);
47
+ const json = Buffer.from(payload + pad, "base64").toString("utf8");
48
+ return JSON.parse(json);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function extractTenantIdFromIss(iss) {
55
+ if (!iss)
56
+ return "";
57
+ const match = iss.match(/realms\/sso-([^/]+)$/);
58
+ return match?.[1] || "";
59
+ }
60
+ function resolveTenantId(accessToken) {
61
+ if (CONFIG.tenantId)
62
+ return CONFIG.tenantId;
63
+ const payload = decodeJwtPayload(accessToken);
64
+ return (payload?.tenant_id ||
65
+ payload?.tenantId ||
66
+ extractTenantIdFromIss(payload?.iss));
67
+ }
68
+ function resolveEnterpriseId(accessToken) {
69
+ if (CONFIG.enterpriseId)
70
+ return CONFIG.enterpriseId;
71
+ const payload = decodeJwtPayload(accessToken);
72
+ return (payload?.enterprise_id ||
73
+ payload?.enterpriseId ||
74
+ payload?.ent_id ||
75
+ payload?.entId ||
76
+ "");
77
+ }
78
+ function resolveUserId(accessToken) {
79
+ if (CONFIG.userId)
80
+ return CONFIG.userId;
81
+ const payload = decodeJwtPayload(accessToken);
82
+ return payload?.user_id || payload?.userId || payload?.uid || payload?.sub || "";
83
+ }
84
+ function buildAuthHeaders(accessToken) {
85
+ const tenantId = resolveTenantId(accessToken);
86
+ const enterpriseId = resolveEnterpriseId(accessToken);
87
+ const userId = resolveUserId(accessToken);
88
+ if (!tenantId) {
89
+ console.warn("[codebuddy-external] 未获取到 X-Tenant-Id,请设置 CODEBUDDY_TENANT_ID");
90
+ }
91
+ if (!enterpriseId) {
92
+ console.warn("[codebuddy-external] 未获取到 X-Enterprise-Id,请设置 CODEBUDDY_ENTERPRISE_ID");
93
+ }
94
+ if (!userId) {
95
+ console.warn("[codebuddy-external] 未获取到 X-User-Id,请设置 CODEBUDDY_USER_ID");
96
+ }
97
+ const conversationId = generateUuid();
98
+ const messageId = generateUuid();
99
+ const requestId = messageId;
100
+ const headers = {
101
+ "Accept": "application/json",
102
+ "Content-Type": "application/json",
103
+ "X-Requested-With": "XMLHttpRequest",
104
+ "Authorization": `Bearer ${accessToken}`,
105
+ "X-Conversation-ID": conversationId,
106
+ "X-Conversation-Message-ID": messageId,
107
+ "X-Conversation-Request-ID": requestId,
108
+ "X-Request-ID": requestId,
109
+ "X-Agent-Intent": CONFIG.agentIntent,
110
+ "X-IDE-Type": CONFIG.ideType,
111
+ "X-IDE-Name": CONFIG.ideName,
112
+ "X-IDE-Version": CONFIG.appVersion,
113
+ "X-Domain": CONFIG.domain,
114
+ "X-Product": CONFIG.product,
115
+ "User-Agent": `CLI/${CONFIG.appVersion} CodeBuddy/${CONFIG.appVersion}`,
116
+ };
117
+ if (tenantId)
118
+ headers["X-Tenant-Id"] = tenantId;
119
+ if (enterpriseId)
120
+ headers["X-Enterprise-Id"] = enterpriseId;
121
+ if (userId)
122
+ headers["X-User-Id"] = userId;
123
+ return headers;
124
+ }
25
125
  /**
26
126
  * Convert OpenAI chat messages to a single prompt string
27
127
  */
@@ -187,7 +287,7 @@ async function executeCodeBuddyCLI(prompt, model, systemPrompt) {
187
287
  * Creates a fetch function that invokes codebuddy CLI
188
288
  * and converts between OpenAI and CodeBuddy formats
189
289
  */
190
- function createProxyFetch() {
290
+ function createProxyFetch(auth) {
191
291
  return async (url, init) => {
192
292
  const urlStr = url.toString();
193
293
  // Check if this is a chat completions request
@@ -202,18 +302,13 @@ function createProxyFetch() {
202
302
  });
203
303
  }
204
304
  const openaiRequest = JSON.parse(typeof body === "string" ? body : await new Response(body).text());
205
- // Convert to CodeBuddy format
206
- const { prompt, systemPrompt } = convertMessagesToPrompt(openaiRequest.messages);
207
- // 调试日志
208
- console.log(`[codebuddy-external] Messages count: ${openaiRequest.messages.length}`);
209
- console.log(`[codebuddy-external] Prompt: ${prompt.substring(0, 100)}...`);
210
305
  // 根据配置选择 HTTP API 模式或 CLI 模式
211
306
  if (CONFIG.useHttpApi) {
212
- return await executeViaHttpApi(openaiRequest, prompt, systemPrompt);
213
- }
214
- else {
215
- return await executeViaCli(openaiRequest, prompt, systemPrompt);
307
+ return await executeViaAuthApi(openaiRequest, auth);
216
308
  }
309
+ // CLI 模式:将消息拼接为 prompt
310
+ const { prompt, systemPrompt } = convertMessagesToPrompt(openaiRequest.messages);
311
+ return await executeViaCli(openaiRequest, prompt, systemPrompt);
217
312
  }
218
313
  catch (error) {
219
314
  console.error(`[codebuddy-external] Error:`, error);
@@ -228,61 +323,39 @@ function createProxyFetch() {
228
323
  };
229
324
  }
230
325
  /**
231
- * Execute via HTTP API (codebuddy --serve)
232
- * 支持流式响应
326
+ * Execute via Auth HTTP API (真实对话接口)
233
327
  */
234
- async function executeViaHttpApi(openaiRequest, prompt, systemPrompt) {
235
- // 构建 CodeBuddy /agent 请求
236
- // 暂时禁用流式模式,先确保基础功能正常
237
- const useStream = false; // openaiRequest.stream
238
- const agentRequest = {
239
- prompt,
240
- model: openaiRequest.model,
241
- print: true, // 关键:非交互模式,返回结果
242
- outputFormat: useStream ? "stream-json" : "text",
328
+ async function executeViaAuthApi(openaiRequest, auth) {
329
+ if (auth.type !== "oauth" || !auth.access) {
330
+ throw new Error("缺少 access token,无法调用 CodeBuddy API");
331
+ }
332
+ let accessToken = auth.access;
333
+ const requestBody = {
334
+ ...openaiRequest,
335
+ response_format: openaiRequest.response_format || { type: "text" },
336
+ stream: openaiRequest.stream ?? true,
337
+ };
338
+ const doRequest = async (token) => {
339
+ const response = await fetch(`${CONFIG.serverUrl}${CONFIG.chatCompletionsPath}`, {
340
+ method: "POST",
341
+ headers: buildAuthHeaders(token),
342
+ body: JSON.stringify(requestBody),
343
+ });
344
+ return response;
243
345
  };
244
- if (systemPrompt) {
245
- agentRequest.systemPrompt = systemPrompt;
346
+ let response = await doRequest(accessToken);
347
+ if ((response.status === 401 || response.status === 403) && auth.refresh) {
348
+ const refreshed = await refreshAccessToken(auth.refresh);
349
+ if (refreshed?.accessToken) {
350
+ accessToken = refreshed.accessToken;
351
+ response = await doRequest(accessToken);
352
+ }
246
353
  }
247
- // 调用本地 codebuddy --serve
248
- const response = await fetch(`${CONFIG.localServeUrl}/agent`, {
249
- method: "POST",
250
- headers: {
251
- "Content-Type": "application/json",
252
- },
253
- body: JSON.stringify(agentRequest),
254
- });
255
354
  if (!response.ok) {
256
355
  const errorText = await response.text();
257
- throw new Error(`HTTP API error: ${response.status} - ${errorText}`);
356
+ throw new Error(`Auth API error: ${response.status} - ${errorText}`);
258
357
  }
259
- // 处理流式响应
260
- if (useStream && response.body) {
261
- return new Response(transformCodeBuddyStreamToOpenAI(response.body, openaiRequest.model), {
262
- headers: {
263
- "Content-Type": "text/event-stream",
264
- "Cache-Control": "no-cache",
265
- "Connection": "keep-alive",
266
- },
267
- });
268
- }
269
- // 非流式响应 - 等待完整结果
270
- const responseText = await response.text();
271
- let resultText = responseText.trim();
272
- // 返回 OpenAI 格式响应
273
- // 如果客户端请求流式,返回伪流式响应
274
- if (openaiRequest.stream) {
275
- return new Response(createOpenAIStreamResponse(resultText, openaiRequest.model), {
276
- headers: {
277
- "Content-Type": "text/event-stream",
278
- "Cache-Control": "no-cache",
279
- "Connection": "keep-alive",
280
- },
281
- });
282
- }
283
- return new Response(createOpenAIResponse(resultText, openaiRequest.model), {
284
- headers: { "Content-Type": "application/json" },
285
- });
358
+ return response;
286
359
  }
287
360
  /**
288
361
  * Transform CodeBuddy SSE stream to OpenAI SSE stream
@@ -524,9 +597,9 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
524
597
  // 返回代理 fetch 函数,通过 codebuddy CLI 处理请求
525
598
  // baseURL 可以是任意值,因为 fetch 会拦截并调用 CLI
526
599
  return {
527
- apiKey: "cli-proxy", // 占位符,实际认证由 codebuddy CLI 处理
528
- baseURL: "http://localhost", // 占位符,fetch 会拦截请求
529
- fetch: createProxyFetch(),
600
+ apiKey: "cli-proxy", // 占位符,实际认证由 fetch 处理
601
+ baseURL: CONFIG.serverUrl,
602
+ fetch: createProxyFetch(auth),
530
603
  };
531
604
  }
532
605
  return {};
@@ -583,8 +656,8 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
583
656
  if (input.model.providerID !== PROVIDER_ID) {
584
657
  return;
585
658
  }
586
- // 占位符,实际请求由 fetch 函数拦截并通过 CLI 处理
587
- output.options.baseURL = "http://localhost";
659
+ // 交由 fetch 处理,baseURL 仅用于 SDK 记录
660
+ output.options.baseURL = CONFIG.serverUrl;
588
661
  },
589
662
  };
590
663
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codebuddy-external-auth",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "OpenCode plugin for CodeBuddy External (IOA) authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",