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.
- package/dist/plugin.js +158 -67
- 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
|
-
//
|
|
12
|
-
|
|
11
|
+
// 真实对话 API 路径
|
|
12
|
+
chatCompletionsPath: "/v2/chat/completions",
|
|
13
13
|
// 平台标识
|
|
14
14
|
platform: "CLI",
|
|
15
15
|
appVersion: "2.37.20",
|
|
16
|
-
|
|
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
|
|
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 (
|
|
232
|
-
* 支持流式响应
|
|
339
|
+
* Execute via Auth HTTP API (真实对话接口)
|
|
233
340
|
*/
|
|
234
|
-
async function
|
|
235
|
-
|
|
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
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
|
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", // 占位符,实际认证由
|
|
528
|
-
baseURL:
|
|
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
|
-
//
|
|
587
|
-
output.options.baseURL =
|
|
677
|
+
// 交由 fetch 处理,baseURL 仅用于 SDK 记录
|
|
678
|
+
output.options.baseURL = CONFIG.serverUrl;
|
|
588
679
|
},
|
|
589
680
|
};
|
|
590
681
|
};
|