opencode-codebuddy-external-auth 1.0.13 → 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 -64
  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,15 +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
305
  // 根据配置选择 HTTP API 模式或 CLI 模式
208
306
  if (CONFIG.useHttpApi) {
209
- return await executeViaHttpApi(openaiRequest, prompt, systemPrompt);
210
- }
211
- else {
212
- return await executeViaCli(openaiRequest, prompt, systemPrompt);
307
+ return await executeViaAuthApi(openaiRequest, auth);
213
308
  }
309
+ // CLI 模式:将消息拼接为 prompt
310
+ const { prompt, systemPrompt } = convertMessagesToPrompt(openaiRequest.messages);
311
+ return await executeViaCli(openaiRequest, prompt, systemPrompt);
214
312
  }
215
313
  catch (error) {
216
314
  console.error(`[codebuddy-external] Error:`, error);
@@ -225,61 +323,39 @@ function createProxyFetch() {
225
323
  };
226
324
  }
227
325
  /**
228
- * Execute via HTTP API (codebuddy --serve)
229
- * 支持流式响应
326
+ * Execute via Auth HTTP API (真实对话接口)
230
327
  */
231
- async function executeViaHttpApi(openaiRequest, prompt, systemPrompt) {
232
- // 构建 CodeBuddy /agent 请求
233
- // 暂时禁用流式模式,先确保基础功能正常
234
- const useStream = false; // openaiRequest.stream
235
- const agentRequest = {
236
- prompt,
237
- model: openaiRequest.model,
238
- print: true, // 关键:非交互模式,返回结果
239
- 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;
240
345
  };
241
- if (systemPrompt) {
242
- 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
+ }
243
353
  }
244
- // 调用本地 codebuddy --serve
245
- const response = await fetch(`${CONFIG.localServeUrl}/agent`, {
246
- method: "POST",
247
- headers: {
248
- "Content-Type": "application/json",
249
- },
250
- body: JSON.stringify(agentRequest),
251
- });
252
354
  if (!response.ok) {
253
355
  const errorText = await response.text();
254
- throw new Error(`HTTP API error: ${response.status} - ${errorText}`);
356
+ throw new Error(`Auth API error: ${response.status} - ${errorText}`);
255
357
  }
256
- // 处理流式响应
257
- if (useStream && response.body) {
258
- return new Response(transformCodeBuddyStreamToOpenAI(response.body, openaiRequest.model), {
259
- headers: {
260
- "Content-Type": "text/event-stream",
261
- "Cache-Control": "no-cache",
262
- "Connection": "keep-alive",
263
- },
264
- });
265
- }
266
- // 非流式响应 - 等待完整结果
267
- const responseText = await response.text();
268
- let resultText = responseText.trim();
269
- // 返回 OpenAI 格式响应
270
- // 如果客户端请求流式,返回伪流式响应
271
- if (openaiRequest.stream) {
272
- return new Response(createOpenAIStreamResponse(resultText, openaiRequest.model), {
273
- headers: {
274
- "Content-Type": "text/event-stream",
275
- "Cache-Control": "no-cache",
276
- "Connection": "keep-alive",
277
- },
278
- });
279
- }
280
- return new Response(createOpenAIResponse(resultText, openaiRequest.model), {
281
- headers: { "Content-Type": "application/json" },
282
- });
358
+ return response;
283
359
  }
284
360
  /**
285
361
  * Transform CodeBuddy SSE stream to OpenAI SSE stream
@@ -521,9 +597,9 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
521
597
  // 返回代理 fetch 函数,通过 codebuddy CLI 处理请求
522
598
  // baseURL 可以是任意值,因为 fetch 会拦截并调用 CLI
523
599
  return {
524
- apiKey: "cli-proxy", // 占位符,实际认证由 codebuddy CLI 处理
525
- baseURL: "http://localhost", // 占位符,fetch 会拦截请求
526
- fetch: createProxyFetch(),
600
+ apiKey: "cli-proxy", // 占位符,实际认证由 fetch 处理
601
+ baseURL: CONFIG.serverUrl,
602
+ fetch: createProxyFetch(auth),
527
603
  };
528
604
  }
529
605
  return {};
@@ -580,8 +656,8 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
580
656
  if (input.model.providerID !== PROVIDER_ID) {
581
657
  return;
582
658
  }
583
- // 占位符,实际请求由 fetch 函数拦截并通过 CLI 处理
584
- output.options.baseURL = "http://localhost";
659
+ // 交由 fetch 处理,baseURL 仅用于 SDK 记录
660
+ output.options.baseURL = CONFIG.serverUrl;
585
661
  },
586
662
  };
587
663
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codebuddy-external-auth",
3
- "version": "1.0.13",
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",