opencode-codebuddy-external-auth 1.0.24 → 1.0.25
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/README.md +5 -15
- package/dist/plugin.js +22 -233
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,12 +18,8 @@ npm install opencode-codebuddy-external-auth
|
|
|
18
18
|
"plugin": ["opencode-codebuddy-external-auth"],
|
|
19
19
|
"provider": {
|
|
20
20
|
"codebuddy-external": {
|
|
21
|
-
"npm": "@ai-sdk/
|
|
21
|
+
"npm": "@ai-sdk/anthropic",
|
|
22
22
|
"name": "CodeBuddy External (IOA)",
|
|
23
|
-
"options": {
|
|
24
|
-
"compatibility": "compatible",
|
|
25
|
-
"baseURL": "https://copilot.tencent.com"
|
|
26
|
-
},
|
|
27
23
|
"models": {
|
|
28
24
|
"claude-4.5": { "name": "Claude Sonnet 4.5 (x2.20)", "contextLength": 176000 },
|
|
29
25
|
"claude-opus-4.5": { "name": "Claude Opus 4.5 (x3.33)", "contextLength": 176000 },
|
|
@@ -45,7 +41,7 @@ npm install opencode-codebuddy-external-auth
|
|
|
45
41
|
}
|
|
46
42
|
}
|
|
47
43
|
},
|
|
48
|
-
"model": "codebuddy-external/
|
|
44
|
+
"model": "codebuddy-external/claude-4.5"
|
|
49
45
|
}
|
|
50
46
|
```
|
|
51
47
|
|
|
@@ -57,9 +53,6 @@ npm install opencode-codebuddy-external-auth
|
|
|
57
53
|
4. 在浏览器中完成 IOA 登录
|
|
58
54
|
5. 返回终端开始使用
|
|
59
55
|
|
|
60
|
-
> 日志会输出实际发送的模型:`[codebuddy-external] 使用模型: <model>`
|
|
61
|
-
> 如模型不在可用列表,直接报错并提示更换(不再自动回退)
|
|
62
|
-
|
|
63
56
|
## 支持的模型
|
|
64
57
|
|
|
65
58
|
根据 CodeBuddy IOA 版本配置(2026-01-24):
|
|
@@ -103,13 +96,10 @@ npm install opencode-codebuddy-external-auth
|
|
|
103
96
|
|
|
104
97
|
## 认证流程
|
|
105
98
|
|
|
106
|
-
1. 插件请求 `/
|
|
99
|
+
1. 插件请求 `/plugin/auth/state` 获取认证状态和 URL
|
|
107
100
|
2. 用户在浏览器中打开 URL 完成 IOA 登录
|
|
108
|
-
3. 插件轮询 `/
|
|
109
|
-
4. Token 到期前自动通过 `/
|
|
110
|
-
|
|
111
|
-
> 插件会自动解析 access token 获取 tenant/user/enterprise;
|
|
112
|
-
> enterprise 缺失时会请求 `/console/enterprises/{tenantId}/config/models` 兜底
|
|
101
|
+
3. 插件轮询 `/plugin/auth/token` 获取访问令牌
|
|
102
|
+
4. Token 到期前自动通过 `/plugin/auth/token/refresh` 刷新
|
|
113
103
|
|
|
114
104
|
## License
|
|
115
105
|
|
package/dist/plugin.js
CHANGED
|
@@ -8,8 +8,8 @@ const PROVIDER_ID = "codebuddy-external";
|
|
|
8
8
|
const CONFIG = {
|
|
9
9
|
// IOA 版本使用 copilot.tencent.com 进行认证
|
|
10
10
|
serverUrl: "https://copilot.tencent.com",
|
|
11
|
-
// 真实对话 API
|
|
12
|
-
|
|
11
|
+
// 真实对话 API 路径
|
|
12
|
+
chatCompletionsPath: "/v2/chat/completions",
|
|
13
13
|
// 平台标识
|
|
14
14
|
platform: "CLI",
|
|
15
15
|
appVersion: "2.37.20",
|
|
@@ -39,35 +39,6 @@ function generateUuid() {
|
|
|
39
39
|
}
|
|
40
40
|
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
41
41
|
}
|
|
42
|
-
function generateHexId(length) {
|
|
43
|
-
const bytes = Math.ceil(length / 2);
|
|
44
|
-
const buffer = new Uint8Array(bytes);
|
|
45
|
-
if (globalThis.crypto?.getRandomValues) {
|
|
46
|
-
globalThis.crypto.getRandomValues(buffer);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
for (let i = 0; i < buffer.length; i += 1) {
|
|
50
|
-
buffer[i] = Math.floor(Math.random() * 256);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return Array.from(buffer)
|
|
54
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
55
|
-
.join("")
|
|
56
|
-
.slice(0, length);
|
|
57
|
-
}
|
|
58
|
-
function buildTraceHeaders() {
|
|
59
|
-
const traceId = generateHexId(32);
|
|
60
|
-
const spanId = generateHexId(16);
|
|
61
|
-
const parentSpanId = generateHexId(16);
|
|
62
|
-
const sampled = "1";
|
|
63
|
-
return {
|
|
64
|
-
"X-B3-TraceId": traceId,
|
|
65
|
-
"X-B3-ParentSpanId": parentSpanId,
|
|
66
|
-
"X-B3-SpanId": spanId,
|
|
67
|
-
"X-B3-Sampled": sampled,
|
|
68
|
-
b3: `${traceId}-${spanId}-${sampled}-${parentSpanId}`,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
42
|
function decodeJwtPayload(token) {
|
|
72
43
|
try {
|
|
73
44
|
const parts = token.split(".");
|
|
@@ -88,16 +59,6 @@ function extractTenantIdFromIss(iss) {
|
|
|
88
59
|
const match = iss.match(/realms\/sso-([^/]+)$/);
|
|
89
60
|
return match?.[1] || "";
|
|
90
61
|
}
|
|
91
|
-
function extractTenantIdFromRoles(roles) {
|
|
92
|
-
if (!roles || roles.length === 0)
|
|
93
|
-
return "";
|
|
94
|
-
for (const role of roles) {
|
|
95
|
-
const match = role.match(/ent-(?:member|plugin-enabled|group):([A-Za-z0-9_-]+)/);
|
|
96
|
-
if (match?.[1])
|
|
97
|
-
return match[1];
|
|
98
|
-
}
|
|
99
|
-
return "";
|
|
100
|
-
}
|
|
101
62
|
let warnedTenantId = false;
|
|
102
63
|
let warnedEnterpriseId = false;
|
|
103
64
|
let warnedUserId = false;
|
|
@@ -105,32 +66,18 @@ function resolveTenantId(accessToken) {
|
|
|
105
66
|
if (CONFIG.tenantId)
|
|
106
67
|
return CONFIG.tenantId;
|
|
107
68
|
const payload = decodeJwtPayload(accessToken);
|
|
108
|
-
const roles = payload?.realm_access?.roles || payload?.resource_access?.account?.roles;
|
|
109
69
|
return (payload?.tenant_id ||
|
|
110
70
|
payload?.tenantId ||
|
|
111
|
-
extractTenantIdFromRoles(roles) ||
|
|
112
71
|
extractTenantIdFromIss(payload?.iss));
|
|
113
72
|
}
|
|
114
|
-
function extractEnterpriseIdFromRoles(roles) {
|
|
115
|
-
if (!roles || roles.length === 0)
|
|
116
|
-
return "";
|
|
117
|
-
for (const role of roles) {
|
|
118
|
-
const match = role.match(/group-admin:([A-Za-z0-9-]+)/);
|
|
119
|
-
if (match?.[1])
|
|
120
|
-
return match[1];
|
|
121
|
-
}
|
|
122
|
-
return "";
|
|
123
|
-
}
|
|
124
73
|
function resolveEnterpriseId(accessToken) {
|
|
125
74
|
if (CONFIG.enterpriseId)
|
|
126
75
|
return CONFIG.enterpriseId;
|
|
127
76
|
const payload = decodeJwtPayload(accessToken);
|
|
128
|
-
const roles = payload?.realm_access?.roles || payload?.resource_access?.account?.roles;
|
|
129
77
|
return (payload?.enterprise_id ||
|
|
130
78
|
payload?.enterpriseId ||
|
|
131
79
|
payload?.ent_id ||
|
|
132
80
|
payload?.entId ||
|
|
133
|
-
extractEnterpriseIdFromRoles(roles) ||
|
|
134
81
|
"");
|
|
135
82
|
}
|
|
136
83
|
function resolveUserId(accessToken) {
|
|
@@ -144,137 +91,10 @@ function resolveModel(inputModel) {
|
|
|
144
91
|
return CONFIG.defaultModel;
|
|
145
92
|
return inputModel || "";
|
|
146
93
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let warnedModelFallback = false;
|
|
152
|
-
let warnedIdSummary = false;
|
|
153
|
-
function extractModelIds(payload) {
|
|
154
|
-
const results = [];
|
|
155
|
-
const collect = (list) => {
|
|
156
|
-
if (!Array.isArray(list))
|
|
157
|
-
return;
|
|
158
|
-
for (const item of list) {
|
|
159
|
-
if (!item)
|
|
160
|
-
continue;
|
|
161
|
-
if (typeof item === "string") {
|
|
162
|
-
results.push(item);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (typeof item === "object") {
|
|
166
|
-
const id = item.id ||
|
|
167
|
-
item.model ||
|
|
168
|
-
item.modelId ||
|
|
169
|
-
item.model_id ||
|
|
170
|
-
item.code ||
|
|
171
|
-
item.name;
|
|
172
|
-
if (typeof id === "string")
|
|
173
|
-
results.push(id);
|
|
174
|
-
if (Array.isArray(item.models))
|
|
175
|
-
collect(item.models);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
const candidates = [
|
|
180
|
-
payload?.data?.models,
|
|
181
|
-
payload?.data?.modelList,
|
|
182
|
-
payload?.data?.availableModels,
|
|
183
|
-
payload?.models,
|
|
184
|
-
payload?.modelList,
|
|
185
|
-
payload?.availableModels,
|
|
186
|
-
payload?.data?.modelGroups,
|
|
187
|
-
payload?.modelGroups,
|
|
188
|
-
];
|
|
189
|
-
for (const list of candidates) {
|
|
190
|
-
collect(list);
|
|
191
|
-
}
|
|
192
|
-
return Array.from(new Set(results)).filter(Boolean);
|
|
193
|
-
}
|
|
194
|
-
async function fetchConfigModels(accessToken, tenantId) {
|
|
195
|
-
if (!tenantId)
|
|
196
|
-
return { enterpriseId: "", models: [] };
|
|
197
|
-
try {
|
|
198
|
-
const url = `${CONFIG.serverUrl}/console/enterprises/${tenantId}/config/models`;
|
|
199
|
-
const response = await fetch(url, {
|
|
200
|
-
method: "GET",
|
|
201
|
-
headers: {
|
|
202
|
-
"Accept": "application/json, text/plain, */*",
|
|
203
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
204
|
-
"Authorization": `Bearer ${accessToken}`,
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
if (!response.ok) {
|
|
208
|
-
return { enterpriseId: "", models: [] };
|
|
209
|
-
}
|
|
210
|
-
const data = await response.json();
|
|
211
|
-
const enterpriseId = data?.data?.enterpriseId || data?.enterpriseId || "";
|
|
212
|
-
const models = extractModelIds(data);
|
|
213
|
-
return { enterpriseId, models };
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
return { enterpriseId: "", models: [] };
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
async function getSupportedModels(accessToken, tenantId) {
|
|
220
|
-
if (cachedModelIds.length > 0)
|
|
221
|
-
return cachedModelIds;
|
|
222
|
-
const config = await fetchConfigModels(accessToken, tenantId);
|
|
223
|
-
if (config.enterpriseId && !cachedEnterpriseId) {
|
|
224
|
-
cachedEnterpriseId = config.enterpriseId;
|
|
225
|
-
}
|
|
226
|
-
if (config.models.length) {
|
|
227
|
-
cachedModelIds = config.models;
|
|
228
|
-
}
|
|
229
|
-
return cachedModelIds;
|
|
230
|
-
}
|
|
231
|
-
function pickFallbackModel(models) {
|
|
232
|
-
if (models.includes("glm-4.7-ioa"))
|
|
233
|
-
return "glm-4.7-ioa";
|
|
234
|
-
return models[0] || "";
|
|
235
|
-
}
|
|
236
|
-
function buildEnterpriseHeaders(accessToken) {
|
|
237
|
-
const tenantId = cachedTenantId || resolveTenantId(accessToken);
|
|
238
|
-
const enterpriseId = cachedEnterpriseId || resolveEnterpriseId(accessToken);
|
|
239
|
-
const resolvedTenantId = tenantId || enterpriseId;
|
|
240
|
-
if (tenantId)
|
|
241
|
-
cachedTenantId = tenantId;
|
|
242
|
-
if (enterpriseId)
|
|
243
|
-
cachedEnterpriseId = enterpriseId;
|
|
244
|
-
const headers = {};
|
|
245
|
-
if (resolvedTenantId)
|
|
246
|
-
headers["X-Tenant-Id"] = resolvedTenantId;
|
|
247
|
-
if (enterpriseId)
|
|
248
|
-
headers["X-Enterprise-Id"] = enterpriseId;
|
|
249
|
-
return headers;
|
|
250
|
-
}
|
|
251
|
-
async function buildAuthHeaders(accessToken) {
|
|
252
|
-
const payload = decodeJwtPayload(accessToken);
|
|
253
|
-
const roles = payload?.realm_access?.roles || payload?.resource_access?.account?.roles;
|
|
254
|
-
let tenantId = cachedTenantId || resolveTenantId(accessToken);
|
|
255
|
-
let enterpriseId = cachedEnterpriseId || resolveEnterpriseId(accessToken);
|
|
256
|
-
const userId = cachedUserId || resolveUserId(accessToken);
|
|
257
|
-
if (!enterpriseId) {
|
|
258
|
-
const config = await fetchConfigModels(accessToken, tenantId);
|
|
259
|
-
enterpriseId = config.enterpriseId || enterpriseId;
|
|
260
|
-
if (config.models.length) {
|
|
261
|
-
cachedModelIds = config.models;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (!tenantId && enterpriseId) {
|
|
265
|
-
tenantId = enterpriseId;
|
|
266
|
-
}
|
|
267
|
-
if (tenantId)
|
|
268
|
-
cachedTenantId = tenantId;
|
|
269
|
-
if (enterpriseId)
|
|
270
|
-
cachedEnterpriseId = enterpriseId;
|
|
271
|
-
if (userId)
|
|
272
|
-
cachedUserId = userId;
|
|
273
|
-
if (!warnedIdSummary) {
|
|
274
|
-
warnedIdSummary = true;
|
|
275
|
-
console.log(`[codebuddy-external] IDs: tenant=${tenantId ? "ok" : "empty"}, enterprise=${enterpriseId ? "ok" : "empty"}, user=${userId ? "ok" : "empty"}`);
|
|
276
|
-
console.log(`[codebuddy-external] Token payload: iss=${payload?.iss ? "ok" : "empty"}, sub=${payload?.sub ? "ok" : "empty"}, roles=${Array.isArray(roles) ? roles.length : 0}`);
|
|
277
|
-
}
|
|
94
|
+
function buildAuthHeaders(accessToken) {
|
|
95
|
+
const tenantId = resolveTenantId(accessToken);
|
|
96
|
+
const enterpriseId = resolveEnterpriseId(accessToken);
|
|
97
|
+
const userId = resolveUserId(accessToken);
|
|
278
98
|
if (!tenantId && !warnedTenantId) {
|
|
279
99
|
warnedTenantId = true;
|
|
280
100
|
console.warn("[codebuddy-external] 未获取到 X-Tenant-Id,请设置 CODEBUDDY_TENANT_ID");
|
|
@@ -290,8 +110,6 @@ async function buildAuthHeaders(accessToken) {
|
|
|
290
110
|
const conversationId = generateUuid();
|
|
291
111
|
const messageId = generateUuid();
|
|
292
112
|
const requestId = messageId;
|
|
293
|
-
const traceHeaders = buildTraceHeaders();
|
|
294
|
-
const encodedUserId = userId ? encodeURIComponent(userId) : "";
|
|
295
113
|
const headers = {
|
|
296
114
|
"Accept": "application/json",
|
|
297
115
|
"Content-Type": "application/json",
|
|
@@ -308,14 +126,13 @@ async function buildAuthHeaders(accessToken) {
|
|
|
308
126
|
"X-Domain": CONFIG.domain,
|
|
309
127
|
"X-Product": CONFIG.product,
|
|
310
128
|
"User-Agent": `CLI/${CONFIG.appVersion} CodeBuddy/${CONFIG.appVersion}`,
|
|
311
|
-
...traceHeaders,
|
|
312
129
|
};
|
|
313
130
|
if (tenantId)
|
|
314
131
|
headers["X-Tenant-Id"] = tenantId;
|
|
315
132
|
if (enterpriseId)
|
|
316
133
|
headers["X-Enterprise-Id"] = enterpriseId;
|
|
317
|
-
if (
|
|
318
|
-
headers["X-User-Id"] =
|
|
134
|
+
if (userId)
|
|
135
|
+
headers["X-User-Id"] = userId;
|
|
319
136
|
return headers;
|
|
320
137
|
}
|
|
321
138
|
/**
|
|
@@ -526,48 +343,30 @@ async function executeViaAuthApi(openaiRequest, auth) {
|
|
|
526
343
|
throw new Error("缺少 access token,无法调用 CodeBuddy API");
|
|
527
344
|
}
|
|
528
345
|
let accessToken = auth.access;
|
|
529
|
-
const tenantId = resolveTenantId(accessToken);
|
|
530
346
|
const resolvedModel = resolveModel(openaiRequest.model);
|
|
531
|
-
const supportedModels = await getSupportedModels(accessToken, tenantId);
|
|
532
347
|
if (!resolvedModel) {
|
|
533
|
-
throw new Error("
|
|
348
|
+
throw new Error("未设置模型,请设置 CODEBUDDY_DEFAULT_MODEL 或在 OpenCode 选择模型");
|
|
534
349
|
}
|
|
535
|
-
if (supportedModels.length && !supportedModels.includes(resolvedModel)) {
|
|
536
|
-
throw new Error(`[codebuddy-external] 模型 ${resolvedModel} 不在可用列表,请更换模型`);
|
|
537
|
-
}
|
|
538
|
-
console.log(`[codebuddy-external] 使用模型: ${resolvedModel}`);
|
|
539
350
|
const requestBody = {
|
|
540
351
|
...openaiRequest,
|
|
541
352
|
model: resolvedModel,
|
|
542
353
|
response_format: openaiRequest.response_format || { type: "text" },
|
|
543
354
|
stream: openaiRequest.stream ?? true,
|
|
544
355
|
};
|
|
545
|
-
const doRequest = async (token
|
|
546
|
-
const
|
|
547
|
-
const response = await fetch(`${CONFIG.serverUrl}${path}`, {
|
|
356
|
+
const doRequest = async (token) => {
|
|
357
|
+
const response = await fetch(`${CONFIG.serverUrl}${CONFIG.chatCompletionsPath}`, {
|
|
548
358
|
method: "POST",
|
|
549
|
-
headers,
|
|
359
|
+
headers: buildAuthHeaders(token),
|
|
550
360
|
body: JSON.stringify(requestBody),
|
|
551
361
|
});
|
|
552
362
|
return response;
|
|
553
363
|
};
|
|
554
|
-
|
|
555
|
-
let lastResponse = null;
|
|
556
|
-
for (const path of CONFIG.chatCompletionsPaths) {
|
|
557
|
-
const response = await doRequest(token, path);
|
|
558
|
-
if (response.status !== 404) {
|
|
559
|
-
return response;
|
|
560
|
-
}
|
|
561
|
-
lastResponse = response;
|
|
562
|
-
}
|
|
563
|
-
return lastResponse || doRequest(token, CONFIG.chatCompletionsPaths[0]);
|
|
564
|
-
};
|
|
565
|
-
let response = await requestWithFallback(accessToken);
|
|
364
|
+
let response = await doRequest(accessToken);
|
|
566
365
|
if ((response.status === 401 || response.status === 403) && auth.refresh) {
|
|
567
|
-
const refreshed = await refreshAccessToken(auth.refresh
|
|
366
|
+
const refreshed = await refreshAccessToken(auth.refresh);
|
|
568
367
|
if (refreshed?.accessToken) {
|
|
569
368
|
accessToken = refreshed.accessToken;
|
|
570
|
-
response = await
|
|
369
|
+
response = await doRequest(accessToken);
|
|
571
370
|
}
|
|
572
371
|
}
|
|
573
372
|
if (!response.ok) {
|
|
@@ -777,22 +576,15 @@ async function pollForToken(state, expiresAt, signal) {
|
|
|
777
576
|
/**
|
|
778
577
|
* Refresh the access token
|
|
779
578
|
*/
|
|
780
|
-
async function refreshAccessToken(refreshToken
|
|
579
|
+
async function refreshAccessToken(refreshToken) {
|
|
781
580
|
try {
|
|
782
|
-
const traceHeaders = buildTraceHeaders();
|
|
783
|
-
const headers = {
|
|
784
|
-
"Content-Type": "application/json",
|
|
785
|
-
"Accept": "application/json",
|
|
786
|
-
"X-Refresh-Token": refreshToken,
|
|
787
|
-
...traceHeaders,
|
|
788
|
-
};
|
|
789
|
-
if (accessToken) {
|
|
790
|
-
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
791
|
-
Object.assign(headers, buildEnterpriseHeaders(accessToken));
|
|
792
|
-
}
|
|
793
581
|
const response = await fetch(`${CONFIG.serverUrl}/v2/plugin/auth/token/refresh`, {
|
|
794
582
|
method: "POST",
|
|
795
|
-
headers
|
|
583
|
+
headers: {
|
|
584
|
+
"Content-Type": "application/json",
|
|
585
|
+
"Accept": "application/json",
|
|
586
|
+
"Authorization": `Bearer ${refreshToken}`,
|
|
587
|
+
},
|
|
796
588
|
});
|
|
797
589
|
if (!response.ok) {
|
|
798
590
|
console.warn(`[codebuddy-external] Token refresh failed: ${response.status}`);
|
|
@@ -834,7 +626,7 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
|
|
|
834
626
|
if (auth.type !== "oauth" || !auth.refresh) {
|
|
835
627
|
return null;
|
|
836
628
|
}
|
|
837
|
-
const tokenData = await refreshAccessToken(auth.refresh
|
|
629
|
+
const tokenData = await refreshAccessToken(auth.refresh);
|
|
838
630
|
if (!tokenData) {
|
|
839
631
|
return null;
|
|
840
632
|
}
|
|
@@ -864,9 +656,6 @@ const CodeBuddyExternalAuthPlugin = async (_input) => {
|
|
|
864
656
|
if (!tokenData) {
|
|
865
657
|
return { type: "failed" };
|
|
866
658
|
}
|
|
867
|
-
if (tokenData.userId && !cachedUserId) {
|
|
868
|
-
cachedUserId = tokenData.userId;
|
|
869
|
-
}
|
|
870
659
|
return {
|
|
871
660
|
type: "success",
|
|
872
661
|
access: tokenData.accessToken,
|