opencode-codebuddy-external-auth 1.0.23 → 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.
Files changed (3) hide show
  1. package/README.md +5 -15
  2. package/dist/plugin.js +22 -230
  3. 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/openai-compatible",
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/glm-4.7-ioa"
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. 插件请求 `/v2/plugin/auth/state` 获取认证状态和 URL
99
+ 1. 插件请求 `/plugin/auth/state` 获取认证状态和 URL
107
100
  2. 用户在浏览器中打开 URL 完成 IOA 登录
108
- 3. 插件轮询 `/v2/plugin/auth/token` 获取访问令牌
109
- 4. Token 到期前自动通过 `/v2/plugin/auth/token/refresh` 刷新
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 路径(优先尝试 /v2/chat/completions)
12
- chatCompletionsPaths: ["/v2/chat/completions", "/v2/plugin/chat/completions"],
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
- let cachedTenantId = "";
148
- let cachedEnterpriseId = "";
149
- let cachedUserId = "";
150
- let cachedModelIds = [];
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 (encodedUserId)
318
- headers["X-User-Id"] = encodedUserId;
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("未设置模型,请在 OpenCode 选择模型");
534
- }
535
- if (supportedModels.length && !supportedModels.includes(resolvedModel)) {
536
- throw new Error(`[codebuddy-external] 模型 ${resolvedModel} 不在可用列表,请更换模型`);
348
+ throw new Error("未设置模型,请设置 CODEBUDDY_DEFAULT_MODEL 或在 OpenCode 选择模型");
537
349
  }
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, path) => {
546
- const headers = await buildAuthHeaders(token);
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
- const requestWithFallback = async (token) => {
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, accessToken);
366
+ const refreshed = await refreshAccessToken(auth.refresh);
568
367
  if (refreshed?.accessToken) {
569
368
  accessToken = refreshed.accessToken;
570
- response = await requestWithFallback(accessToken);
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, accessToken) {
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, auth.access);
629
+ const tokenData = await refreshAccessToken(auth.refresh);
838
630
  if (!tokenData) {
839
631
  return null;
840
632
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codebuddy-external-auth",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "OpenCode plugin for CodeBuddy External (IOA) authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",