opencode-codebuddy-external-auth 1.0.14 → 1.0.16

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 +158 -67
  2. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -8,13 +8,24 @@ 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 || "",
27
+ // 强制覆盖模型(避免 OpenCode 仍使用旧模型)
28
+ defaultModel: process.env.CODEBUDDY_DEFAULT_MODEL || "",
18
29
  };
19
30
  // ============================================================================
20
31
  // Utility Functions
@@ -22,6 +33,108 @@ const CONFIG = {
22
33
  function sleep(ms) {
23
34
  return new Promise((resolve) => setTimeout(resolve, ms));
24
35
  }
36
+ function generateUuid() {
37
+ if (globalThis.crypto?.randomUUID) {
38
+ return globalThis.crypto.randomUUID();
39
+ }
40
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
41
+ }
42
+ function decodeJwtPayload(token) {
43
+ try {
44
+ const parts = token.split(".");
45
+ if (parts.length < 2)
46
+ return null;
47
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
48
+ const pad = "=".repeat((4 - (payload.length % 4)) % 4);
49
+ const json = Buffer.from(payload + pad, "base64").toString("utf8");
50
+ return JSON.parse(json);
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ function extractTenantIdFromIss(iss) {
57
+ if (!iss)
58
+ return "";
59
+ const match = iss.match(/realms\/sso-([^/]+)$/);
60
+ return match?.[1] || "";
61
+ }
62
+ let warnedTenantId = false;
63
+ let warnedEnterpriseId = false;
64
+ let warnedUserId = false;
65
+ function resolveTenantId(accessToken) {
66
+ if (CONFIG.tenantId)
67
+ return CONFIG.tenantId;
68
+ const payload = decodeJwtPayload(accessToken);
69
+ return (payload?.tenant_id ||
70
+ payload?.tenantId ||
71
+ extractTenantIdFromIss(payload?.iss));
72
+ }
73
+ function resolveEnterpriseId(accessToken) {
74
+ if (CONFIG.enterpriseId)
75
+ return CONFIG.enterpriseId;
76
+ const payload = decodeJwtPayload(accessToken);
77
+ return (payload?.enterprise_id ||
78
+ payload?.enterpriseId ||
79
+ payload?.ent_id ||
80
+ payload?.entId ||
81
+ "");
82
+ }
83
+ function resolveUserId(accessToken) {
84
+ if (CONFIG.userId)
85
+ return CONFIG.userId;
86
+ const payload = decodeJwtPayload(accessToken);
87
+ return payload?.user_id || payload?.userId || payload?.uid || payload?.sub || "";
88
+ }
89
+ function resolveModel(inputModel) {
90
+ if (CONFIG.defaultModel)
91
+ return CONFIG.defaultModel;
92
+ return inputModel || "";
93
+ }
94
+ function buildAuthHeaders(accessToken) {
95
+ const tenantId = resolveTenantId(accessToken);
96
+ const enterpriseId = resolveEnterpriseId(accessToken);
97
+ const userId = resolveUserId(accessToken);
98
+ if (!tenantId && !warnedTenantId) {
99
+ warnedTenantId = true;
100
+ console.warn("[codebuddy-external] 未获取到 X-Tenant-Id,请设置 CODEBUDDY_TENANT_ID");
101
+ }
102
+ if (!enterpriseId && !warnedEnterpriseId) {
103
+ warnedEnterpriseId = true;
104
+ console.warn("[codebuddy-external] 未获取到 X-Enterprise-Id,请设置 CODEBUDDY_ENTERPRISE_ID");
105
+ }
106
+ if (!userId && !warnedUserId) {
107
+ warnedUserId = true;
108
+ console.warn("[codebuddy-external] 未获取到 X-User-Id,请设置 CODEBUDDY_USER_ID");
109
+ }
110
+ const conversationId = generateUuid();
111
+ const messageId = generateUuid();
112
+ const requestId = messageId;
113
+ const headers = {
114
+ "Accept": "application/json",
115
+ "Content-Type": "application/json",
116
+ "X-Requested-With": "XMLHttpRequest",
117
+ "Authorization": `Bearer ${accessToken}`,
118
+ "X-Conversation-ID": conversationId,
119
+ "X-Conversation-Message-ID": messageId,
120
+ "X-Conversation-Request-ID": requestId,
121
+ "X-Request-ID": requestId,
122
+ "X-Agent-Intent": CONFIG.agentIntent,
123
+ "X-IDE-Type": CONFIG.ideType,
124
+ "X-IDE-Name": CONFIG.ideName,
125
+ "X-IDE-Version": CONFIG.appVersion,
126
+ "X-Domain": CONFIG.domain,
127
+ "X-Product": CONFIG.product,
128
+ "User-Agent": `CLI/${CONFIG.appVersion} CodeBuddy/${CONFIG.appVersion}`,
129
+ };
130
+ if (tenantId)
131
+ headers["X-Tenant-Id"] = tenantId;
132
+ if (enterpriseId)
133
+ headers["X-Enterprise-Id"] = enterpriseId;
134
+ if (userId)
135
+ headers["X-User-Id"] = userId;
136
+ return headers;
137
+ }
25
138
  /**
26
139
  * Convert OpenAI chat messages to a single prompt string
27
140
  */
@@ -187,7 +300,7 @@ async function executeCodeBuddyCLI(prompt, model, systemPrompt) {
187
300
  * Creates a fetch function that invokes codebuddy CLI
188
301
  * and converts between OpenAI and CodeBuddy formats
189
302
  */
190
- function createProxyFetch() {
303
+ function createProxyFetch(auth) {
191
304
  return async (url, init) => {
192
305
  const urlStr = url.toString();
193
306
  // Check if this is a chat completions request
@@ -202,18 +315,13 @@ function createProxyFetch() {
202
315
  });
203
316
  }
204
317
  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
318
  // 根据配置选择 HTTP API 模式或 CLI 模式
211
319
  if (CONFIG.useHttpApi) {
212
- return await executeViaHttpApi(openaiRequest, prompt, systemPrompt);
213
- }
214
- else {
215
- return await executeViaCli(openaiRequest, prompt, systemPrompt);
320
+ return await executeViaAuthApi(openaiRequest, auth);
216
321
  }
322
+ // CLI 模式:将消息拼接为 prompt
323
+ const { prompt, systemPrompt } = convertMessagesToPrompt(openaiRequest.messages);
324
+ return await executeViaCli(openaiRequest, prompt, systemPrompt);
217
325
  }
218
326
  catch (error) {
219
327
  console.error(`[codebuddy-external] Error:`, error);
@@ -228,61 +336,44 @@ function createProxyFetch() {
228
336
  };
229
337
  }
230
338
  /**
231
- * Execute via HTTP API (codebuddy --serve)
232
- * 支持流式响应
339
+ * Execute via Auth HTTP API (真实对话接口)
233
340
  */
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",
243
- };
244
- if (systemPrompt) {
245
- agentRequest.systemPrompt = systemPrompt;
341
+ async function executeViaAuthApi(openaiRequest, auth) {
342
+ if (auth.type !== "oauth" || !auth.access) {
343
+ throw new Error("缺少 access token,无法调用 CodeBuddy API");
246
344
  }
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
- if (!response.ok) {
256
- const errorText = await response.text();
257
- throw new Error(`HTTP API error: ${response.status} - ${errorText}`);
345
+ let accessToken = auth.access;
346
+ const resolvedModel = resolveModel(openaiRequest.model);
347
+ if (!resolvedModel) {
348
+ throw new Error("未设置模型,请设置 CODEBUDDY_DEFAULT_MODEL 或在 OpenCode 选择模型");
258
349
  }
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
- },
350
+ const requestBody = {
351
+ ...openaiRequest,
352
+ model: resolvedModel,
353
+ response_format: openaiRequest.response_format || { type: "text" },
354
+ stream: openaiRequest.stream ?? true,
355
+ };
356
+ const doRequest = async (token) => {
357
+ const response = await fetch(`${CONFIG.serverUrl}${CONFIG.chatCompletionsPath}`, {
358
+ method: "POST",
359
+ headers: buildAuthHeaders(token),
360
+ body: JSON.stringify(requestBody),
267
361
  });
362
+ return response;
363
+ };
364
+ let response = await doRequest(accessToken);
365
+ if ((response.status === 401 || response.status === 403) && auth.refresh) {
366
+ const refreshed = await refreshAccessToken(auth.refresh);
367
+ if (refreshed?.accessToken) {
368
+ accessToken = refreshed.accessToken;
369
+ response = await doRequest(accessToken);
370
+ }
268
371
  }
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
- });
372
+ if (!response.ok) {
373
+ const errorText = await response.text();
374
+ throw new Error(`Auth API error: ${response.status} - ${errorText}`);
282
375
  }
283
- return new Response(createOpenAIResponse(resultText, openaiRequest.model), {
284
- headers: { "Content-Type": "application/json" },
285
- });
376
+ return response;
286
377
  }
287
378
  /**
288
379
  * Transform CodeBuddy SSE stream to OpenAI SSE stream
@@ -524,9 +615,9 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
524
615
  // 返回代理 fetch 函数,通过 codebuddy CLI 处理请求
525
616
  // baseURL 可以是任意值,因为 fetch 会拦截并调用 CLI
526
617
  return {
527
- apiKey: "cli-proxy", // 占位符,实际认证由 codebuddy CLI 处理
528
- baseURL: "http://localhost", // 占位符,fetch 会拦截请求
529
- fetch: createProxyFetch(),
618
+ apiKey: "cli-proxy", // 占位符,实际认证由 fetch 处理
619
+ baseURL: CONFIG.serverUrl,
620
+ fetch: createProxyFetch(auth),
530
621
  };
531
622
  }
532
623
  return {};
@@ -583,8 +674,8 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
583
674
  if (input.model.providerID !== PROVIDER_ID) {
584
675
  return;
585
676
  }
586
- // 占位符,实际请求由 fetch 函数拦截并通过 CLI 处理
587
- output.options.baseURL = "http://localhost";
677
+ // 交由 fetch 处理,baseURL 仅用于 SDK 记录
678
+ output.options.baseURL = CONFIG.serverUrl;
588
679
  },
589
680
  };
590
681
  };
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.16",
4
4
  "description": "OpenCode plugin for CodeBuddy External (IOA) authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",