@hotmanxp/opencode-qwen-login-plugin 1.2.0 → 1.3.0
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 +45 -6
- package/dist/cli.js +0 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -8
- package/dist/index.js.map +1 -1
- package/dist/qwen-oauth.d.ts +26 -3
- package/dist/qwen-oauth.d.ts.map +1 -1
- package/dist/qwen-oauth.js +92 -4
- package/dist/qwen-oauth.js.map +1 -1
- package/package.json +4 -2
- package/dist/config-merge.test.js +0 -188
- package/dist/plugin.d.ts +0 -14
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -83
- package/dist/plugin.js.map +0 -1
- package/dist/qwen-oauth.test.js +0 -123
package/README.md
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
- 将 qwen-code 的 OAuth token 配置到 opencode
|
|
12
12
|
- 无需手动输入 API Key
|
|
13
13
|
|
|
14
|
+
- 🔄 **自动刷新 Token** (新增)
|
|
15
|
+
- 检测 token 即将过期时自动刷新(提前 5 分钟)
|
|
16
|
+
- 使用 refresh_token 获取新的 access_token
|
|
17
|
+
- 自动保存刷新后的凭证
|
|
18
|
+
- 避免 API 调用时出现认证失败
|
|
19
|
+
- **后台定时检查**:每 4 分钟自动检查 token 状态,长时间运行也能保持 token 有效
|
|
20
|
+
|
|
14
21
|
- ⚙️ **一键配置**
|
|
15
22
|
- CLI 工具一键完成配置
|
|
16
23
|
- 插件加载时自动配置
|
|
@@ -18,7 +25,7 @@
|
|
|
18
25
|
|
|
19
26
|
- 🔒 **安全可靠**
|
|
20
27
|
- 使用 qwen-code 的 OAuth token
|
|
21
|
-
- Token
|
|
28
|
+
- Token 过期检测(5 分钟缓冲)
|
|
22
29
|
- 敏感信息不硬编码
|
|
23
30
|
|
|
24
31
|
- 📝 **完整的请求头**
|
|
@@ -148,9 +155,13 @@ opencode --model qwen/qwen-plus
|
|
|
148
155
|
|
|
149
156
|
写入 `~/Library/Application Support/opencode/opencode.json`
|
|
150
157
|
|
|
151
|
-
### 4. Token
|
|
158
|
+
### 4. Token 过期检测与自动刷新
|
|
152
159
|
|
|
153
|
-
|
|
160
|
+
- **过期检测**: 检查 `expiry_date`,使用 5 分钟缓冲
|
|
161
|
+
- **自动刷新**: Token 即将过期时自动调用 refresh_token 刷新
|
|
162
|
+
- **持久化保存**: 刷新后的 token 自动保存到 `~/.qwen/oauth_creds.json`
|
|
163
|
+
- **失败处理**: 刷新失败时返回 null,使用其他认证方式
|
|
164
|
+
- **后台定时检查**: 插件启动后每 4 分钟自动检查一次 token 状态,确保长时间运行时 token 始终有效
|
|
154
165
|
|
|
155
166
|
## 🔧 开发模式
|
|
156
167
|
|
|
@@ -209,7 +220,9 @@ const endpoint = buildBaseUrl(creds.resource_url)
|
|
|
209
220
|
| 函数 | 说明 |
|
|
210
221
|
|------|------|
|
|
211
222
|
| `readOAuthCredentials()` | 读取 OAuth 凭证 |
|
|
212
|
-
| `isTokenValid(creds)` | 检查 token
|
|
223
|
+
| `isTokenValid(creds)` | 检查 token 是否过期(含 5 分钟缓冲) |
|
|
224
|
+
| `refreshOAuthToken(refreshToken)` | 使用 refresh_token 刷新 token |
|
|
225
|
+
| `getValidOAuthCredentials()` | 获取有效凭证(自动刷新) |
|
|
213
226
|
| `getApiConfigFromOAuth(creds)` | 从 OAuth 生成 API 配置 |
|
|
214
227
|
| `getQwenConfigFromOAuth()` | 获取 Qwen API 配置 |
|
|
215
228
|
| `saveToOpencodeConfig(config)` | 保存到 opencode.json |
|
|
@@ -295,12 +308,15 @@ qwen login
|
|
|
295
308
|
|
|
296
309
|
### 2. "Qwen OAuth token has expired"
|
|
297
310
|
|
|
298
|
-
**原因**: OAuth token
|
|
311
|
+
**原因**: OAuth token 已过期且刷新失败
|
|
299
312
|
|
|
300
|
-
**解决**:
|
|
313
|
+
**解决**:
|
|
314
|
+
1. 插件会自动尝试刷新 token(提前 5 分钟)
|
|
315
|
+
2. 如果刷新失败,重新认证:
|
|
301
316
|
```bash
|
|
302
317
|
qwen login
|
|
303
318
|
```
|
|
319
|
+
3. 检查网络连接是否正常
|
|
304
320
|
|
|
305
321
|
### 3. 配置未生效
|
|
306
322
|
|
|
@@ -326,9 +342,32 @@ qwen login
|
|
|
326
342
|
|
|
327
343
|
MIT
|
|
328
344
|
|
|
345
|
+
## 📝 更新日志
|
|
346
|
+
|
|
347
|
+
### v1.3.0 (2026-03-22)
|
|
348
|
+
|
|
349
|
+
**新增功能:**
|
|
350
|
+
- 🔄 **自动 Token 刷新** - 使用 OAuth refresh_token 自动刷新过期的 access_token
|
|
351
|
+
- ⏰ **后台定时检查** - 每 4 分钟自动检查 token 状态,确保长时间运行时 token 始终有效
|
|
352
|
+
- 🛡️ **5 分钟缓冲机制** - 在 token 实际过期前 5 分钟触发刷新,避免 API 调用时认证失败
|
|
353
|
+
- 💾 **持久化保存** - 刷新后的 token 自动保存到 `~/.qwen/oauth_creds.json`
|
|
354
|
+
- 📊 **新增导出函数** - `refreshOAuthToken()` 和 `getValidOAuthCredentials()`
|
|
355
|
+
|
|
356
|
+
**技术改进:**
|
|
357
|
+
- 优化 token 过期检测逻辑,使用 5 分钟缓冲时间
|
|
358
|
+
- 添加定时器 unref() 确保不阻止进程正常退出
|
|
359
|
+
- 完善的错误处理和日志记录
|
|
360
|
+
|
|
361
|
+
### v1.2.0 (2026-03-21)
|
|
362
|
+
|
|
363
|
+
- 支持从 qwen-code OAuth 自动配置 opencode
|
|
364
|
+
- 完整的请求头生成(User-Agent、x-dashscope-* 等)
|
|
365
|
+
- CLI 工具一键配置
|
|
366
|
+
|
|
329
367
|
## 🔗 相关链接
|
|
330
368
|
|
|
331
369
|
- [opencode 文档](https://opencode.ai/docs)
|
|
332
370
|
- [通义千问 API](https://help.aliyun.com/zh/dashscope/)
|
|
333
371
|
- [qwen-code](https://github.com/QwenLM/qwen-code)
|
|
372
|
+
- [更新日志](#-更新日志)
|
|
334
373
|
- [Qwen Portal](https://portal.qwen.ai)
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6CH;;GAEG;AACH,iBAAe,MAAM,CAAC,WAAW,EAAE,GAAG;qBAaX,GAAG;GA2D7B;AAGD,eAAe,MAAM,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,44 @@
|
|
|
4
4
|
* 为 opencode 提供 Qwen 模型认证支持
|
|
5
5
|
* 自动从 qwen-code OAuth 配置导入认证信息
|
|
6
6
|
*/
|
|
7
|
-
import { configureOpencodeFromQwenOAuth,
|
|
7
|
+
import { configureOpencodeFromQwenOAuth, getValidOAuthCredentials, getApiConfigFromOAuth } from "./qwen-oauth.js";
|
|
8
|
+
/**
|
|
9
|
+
* Token 刷新定时器 ID
|
|
10
|
+
*/
|
|
11
|
+
let refreshTimerId = null;
|
|
12
|
+
/**
|
|
13
|
+
* 启动定时 token 刷新检查
|
|
14
|
+
* 每 4 分钟检查一次,如果 token 即将过期(< 5 分钟)则自动刷新
|
|
15
|
+
*/
|
|
16
|
+
function startTokenRefreshTimer() {
|
|
17
|
+
// 清除已有的定时器(如果有)
|
|
18
|
+
if (refreshTimerId) {
|
|
19
|
+
clearInterval(refreshTimerId);
|
|
20
|
+
}
|
|
21
|
+
// 每 4 分钟检查一次
|
|
22
|
+
refreshTimerId = setInterval(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const { getValidOAuthCredentials } = await import("./qwen-oauth.js");
|
|
25
|
+
await getValidOAuthCredentials();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// 静默失败,等待下次检查
|
|
29
|
+
}
|
|
30
|
+
}, 4 * 60 * 1000); // 4 分钟
|
|
31
|
+
// 确保定时器不会阻止进程退出
|
|
32
|
+
if (refreshTimerId && typeof refreshTimerId.unref === 'function') {
|
|
33
|
+
refreshTimerId.unref();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 停止定时 token 刷新检查
|
|
38
|
+
*/
|
|
39
|
+
function stopTokenRefreshTimer() {
|
|
40
|
+
if (refreshTimerId) {
|
|
41
|
+
clearInterval(refreshTimerId);
|
|
42
|
+
refreshTimerId = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
8
45
|
/**
|
|
9
46
|
* Qwen 认证插件主函数
|
|
10
47
|
*/
|
|
@@ -16,20 +53,18 @@ async function plugin(pluginInput) {
|
|
|
16
53
|
catch (error) {
|
|
17
54
|
// 静默失败,使用其他认证方式
|
|
18
55
|
}
|
|
56
|
+
// 启动定时 token 刷新检查
|
|
57
|
+
startTokenRefreshTimer();
|
|
19
58
|
return {
|
|
20
59
|
// 配置钩子 - 修改运行时配置
|
|
21
60
|
config: async (config) => {
|
|
22
61
|
try {
|
|
23
|
-
// 1.
|
|
24
|
-
const creds = await
|
|
62
|
+
// 1. 获取有效的 OAuth 凭证 (自动刷新过期 token)
|
|
63
|
+
const creds = await getValidOAuthCredentials();
|
|
25
64
|
if (!creds) {
|
|
26
65
|
return;
|
|
27
66
|
}
|
|
28
|
-
// 2.
|
|
29
|
-
if (!isTokenValid(creds)) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
// 3. 生成 API 配置
|
|
67
|
+
// 2. 生成 API 配置
|
|
33
68
|
const apiConfig = getApiConfigFromOAuth(creds);
|
|
34
69
|
// 4. 按照 opencode provider schema 配置
|
|
35
70
|
if (!config.provider) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,8BAA8B,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/qwen-oauth.d.ts
CHANGED
|
@@ -30,6 +30,16 @@ export declare const DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatib
|
|
|
30
30
|
* Qwen Code 版本信息
|
|
31
31
|
*/
|
|
32
32
|
export declare const QWEN_CODE_VERSION = "unknown";
|
|
33
|
+
/**
|
|
34
|
+
* Token refresh response interface
|
|
35
|
+
*/
|
|
36
|
+
export interface TokenRefreshResponse {
|
|
37
|
+
access_token: string;
|
|
38
|
+
token_type: string;
|
|
39
|
+
refresh_token: string;
|
|
40
|
+
expires_in: number;
|
|
41
|
+
resource_url: string;
|
|
42
|
+
}
|
|
33
43
|
/**
|
|
34
44
|
* 获取当前平台信息
|
|
35
45
|
*/
|
|
@@ -66,12 +76,25 @@ export declare function buildBaseUrl(resourceUrl: string): string;
|
|
|
66
76
|
*/
|
|
67
77
|
export declare function readOAuthCredentials(): Promise<OAuthCredentials | null>;
|
|
68
78
|
/**
|
|
69
|
-
*
|
|
79
|
+
* Check OAuth token is expired
|
|
70
80
|
*
|
|
71
|
-
* @param creds OAuth
|
|
72
|
-
* @returns boolean -
|
|
81
|
+
* @param creds OAuth credentials
|
|
82
|
+
* @returns boolean - is valid (not expired)
|
|
73
83
|
*/
|
|
74
84
|
export declare function isTokenValid(creds: OAuthCredentials): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Refresh OAuth token using refresh_token
|
|
87
|
+
*
|
|
88
|
+
* @param refreshToken - The refresh token
|
|
89
|
+
* @returns New OAuth credentials, or null if refresh failed
|
|
90
|
+
*/
|
|
91
|
+
export declare function refreshOAuthToken(refreshToken: string): Promise<OAuthCredentials | null>;
|
|
92
|
+
/**
|
|
93
|
+
* Get valid OAuth credentials, refreshing if necessary
|
|
94
|
+
*
|
|
95
|
+
* @returns Valid OAuth credentials, or null if not available
|
|
96
|
+
*/
|
|
97
|
+
export declare function getValidOAuthCredentials(): Promise<OAuthCredentials | null>;
|
|
75
98
|
/**
|
|
76
99
|
* 从 OAuth 凭证获取 API 配置
|
|
77
100
|
*
|
package/dist/qwen-oauth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qwen-oauth.d.ts","sourceRoot":"","sources":["../src/qwen-oauth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,sDAAsD,CAAA;AAEnF;;GAEG;AACH,eAAO,MAAM,iBAAiB,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"qwen-oauth.d.ts","sourceRoot":"","sources":["../src/qwen-oauth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,sDAAsD,CAAA;AAEnF;;GAEG;AACH,eAAO,MAAM,iBAAiB,YAAY,CAAA;AAO1C;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;GAEG;AACH,wBAAgB,eAAe;;;;EAM9B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAGvC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAqBzE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAOxD;AASD;;;;GAIG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAS7E;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAK7D;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAuClC;AAED;;;;GAIG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAwCjF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,GAAG,aAAa,CAS5E;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAY5E;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAa/E;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAG/E;AAiFD;;;;GAIG;AACH,wBAAsB,8BAA8B,IAAI,OAAO,CAAC,OAAO,CAAC,CA2BvE"}
|
package/dist/qwen-oauth.js
CHANGED
|
@@ -15,6 +15,10 @@ export const DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/
|
|
|
15
15
|
* Qwen Code 版本信息
|
|
16
16
|
*/
|
|
17
17
|
export const QWEN_CODE_VERSION = "unknown";
|
|
18
|
+
/**
|
|
19
|
+
* Qwen OAuth token refresh endpoint
|
|
20
|
+
*/
|
|
21
|
+
const QWEN_TOKEN_ENDPOINT = "https://oauth.qwen.ai/oauth/token";
|
|
18
22
|
/**
|
|
19
23
|
* 获取当前平台信息
|
|
20
24
|
*/
|
|
@@ -99,14 +103,98 @@ export async function readOAuthCredentials() {
|
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
/**
|
|
102
|
-
*
|
|
106
|
+
* Check OAuth token is expired
|
|
103
107
|
*
|
|
104
|
-
* @param creds OAuth
|
|
105
|
-
* @returns boolean -
|
|
108
|
+
* @param creds OAuth credentials
|
|
109
|
+
* @returns boolean - is valid (not expired)
|
|
106
110
|
*/
|
|
107
111
|
export function isTokenValid(creds) {
|
|
108
112
|
const now = Date.now();
|
|
109
|
-
|
|
113
|
+
// Use 5-minute buffer like Gemini CLI to refresh before actual expiry
|
|
114
|
+
const bufferMs = 5 * 60 * 1000; // 5 minutes
|
|
115
|
+
return creds.expiry_date > now + bufferMs;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Refresh OAuth token using refresh_token
|
|
119
|
+
*
|
|
120
|
+
* @param refreshToken - The refresh token
|
|
121
|
+
* @returns New OAuth credentials, or null if refresh failed
|
|
122
|
+
*/
|
|
123
|
+
export async function refreshOAuthToken(refreshToken) {
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(QWEN_TOKEN_ENDPOINT, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
grant_type: "refresh_token",
|
|
132
|
+
refresh_token: refreshToken,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
console.error("Token refresh failed:", response.status, response.statusText);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const data = (await response.json());
|
|
140
|
+
// Convert to OAuthCredentials format
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const expiryMs = data.expires_in * 1000;
|
|
143
|
+
return {
|
|
144
|
+
access_token: data.access_token,
|
|
145
|
+
token_type: data.token_type,
|
|
146
|
+
refresh_token: data.refresh_token,
|
|
147
|
+
resource_url: data.resource_url,
|
|
148
|
+
expiry_date: now + expiryMs,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.error("Token refresh error:", error);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get valid OAuth credentials, refreshing if necessary
|
|
158
|
+
*
|
|
159
|
+
* @returns Valid OAuth credentials, or null if not available
|
|
160
|
+
*/
|
|
161
|
+
export async function getValidOAuthCredentials() {
|
|
162
|
+
// Read current credentials
|
|
163
|
+
let creds = await readOAuthCredentials();
|
|
164
|
+
if (!creds) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
// Check if token is still valid (with 5-min buffer)
|
|
168
|
+
if (isTokenValid(creds)) {
|
|
169
|
+
return creds;
|
|
170
|
+
}
|
|
171
|
+
// Token is expired or about to expire, try to refresh
|
|
172
|
+
if (creds.refresh_token) {
|
|
173
|
+
console.log("Refreshing expired OAuth token...");
|
|
174
|
+
const refreshedCreds = await refreshOAuthToken(creds.refresh_token);
|
|
175
|
+
if (refreshedCreds) {
|
|
176
|
+
// Save refreshed credentials
|
|
177
|
+
try {
|
|
178
|
+
const credsPath = getOAuthCredsPath();
|
|
179
|
+
const { mkdir } = await import("node:fs/promises");
|
|
180
|
+
await mkdir(join(credsPath, ".."), { recursive: true });
|
|
181
|
+
await writeFile(credsPath, JSON.stringify(refreshedCreds, null, 2), "utf-8");
|
|
182
|
+
console.log("OAuth token refreshed successfully");
|
|
183
|
+
return refreshedCreds;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.error("Failed to save refreshed credentials:", error);
|
|
187
|
+
// Return refreshed creds anyway even if save failed
|
|
188
|
+
return refreshedCreds;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.error("Failed to refresh OAuth token");
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// No refresh token available, credentials are invalid
|
|
197
|
+
return null;
|
|
110
198
|
}
|
|
111
199
|
/**
|
|
112
200
|
* 从 OAuth 凭证获取 API 配置
|
package/dist/qwen-oauth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qwen-oauth.js","sourceRoot":"","sources":["../src/qwen-oauth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAsBjC;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,mDAAmD,CAAA;AAEnF;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAA;AAE1C;;GAEG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO;QACL,QAAQ,EAAE,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ;QACrE,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI;QACvD,WAAW,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;KAC9C,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,YAAY,GAAG,eAAe,EAAE,CAAA;IACtC,OAAO,YAAY,iBAAiB,KAAK,YAAY,CAAC,QAAQ,KAAK,YAAY,CAAC,IAAI,GAAG,CAAA;AACzF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,MAAM,SAAS,GAAG,cAAc,EAAE,CAAA;IAClC,MAAM,YAAY,GAAG,eAAe,EAAE,CAAA;IAEtC,OAAO;QACL,QAAQ,EAAE,kBAAkB;QAC5B,cAAc,EAAE,kBAAkB;QAClC,YAAY,EAAE,SAAS;QACvB,kBAAkB,EAAE,IAAI;QACxB,6BAA6B,EAAE,SAAS;QACxC,gBAAgB,EAAE,OAAO;QACzB,kBAAkB,EAAE,YAAY,CAAC,IAAI;QACrC,qBAAqB,EAAE,MAAM;QAC7B,6BAA6B,EAAE,YAAY,CAAC,WAAW;QACvD,eAAe,EAAE,UAAU,KAAK,EAAE;QAClC,0BAA0B,EAAE,QAAQ;QACpC,uBAAuB,EAAE,SAAS;QAClC,sBAAsB,EAAE,YAAY;QACpC,yBAAyB,EAAE,GAAG;QAC9B,qBAAqB,EAAE,KAAK;KAC7B,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,WAAmB;IAC9C,6BAA6B;IAC7B,gEAAgE;IAChE,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO,WAAW,WAAW,KAAK,CAAA;IACpC,CAAC;IACD,OAAO,gBAAgB,CAAA;AACzB,CAAC;AAED;;GAEG;AACH,SAAS,iBAAiB;IACxB,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,kBAAkB,CAAC,CAAA;AACrD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,iBAAiB,EAAE,CAAA;QACrC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QAClD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAqB,CAAA;IAChD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,aAAa;QACb,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,KAAuB;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,OAAO,KAAK,CAAC,WAAW,GAAG,GAAG,CAAA;
|
|
1
|
+
{"version":3,"file":"qwen-oauth.js","sourceRoot":"","sources":["../src/qwen-oauth.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmanxp/opencode-qwen-login-plugin",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "通义千问认证插件 - 从 qwen-code OAuth 自动配置 opencode",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "通义千问认证插件 - 从 qwen-code OAuth 自动配置 opencode,支持自动 token 刷新",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"plugin",
|
|
23
23
|
"qwen",
|
|
24
24
|
"auth",
|
|
25
|
+
"oauth",
|
|
26
|
+
"token-refresh",
|
|
25
27
|
"通义千问",
|
|
26
28
|
"阿里云"
|
|
27
29
|
],
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 配置合并测试 - 集成测试
|
|
3
|
-
*
|
|
4
|
-
* 测试 saveToOpencodeConfig 函数的合并行为
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
7
|
-
import { writeFile, mkdir, rm, readFile } from 'node:fs/promises';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import assert from 'node:assert';
|
|
10
|
-
import { saveToOpencodeConfig } from './qwen-oauth.js';
|
|
11
|
-
describe('saveToOpencodeConfig - 配置合并', () => {
|
|
12
|
-
let testConfigDir;
|
|
13
|
-
let testConfigPath;
|
|
14
|
-
let originalHOME;
|
|
15
|
-
beforeEach(async () => {
|
|
16
|
-
originalHOME = process.env.HOME;
|
|
17
|
-
testConfigDir = await mkdtemp('opencode-config-test-');
|
|
18
|
-
testConfigPath = join(testConfigDir, '.config', 'opencode');
|
|
19
|
-
await mkdir(testConfigPath, { recursive: true });
|
|
20
|
-
process.env.HOME = testConfigDir;
|
|
21
|
-
});
|
|
22
|
-
afterEach(async () => {
|
|
23
|
-
process.env.HOME = originalHOME;
|
|
24
|
-
await rm(testConfigDir, { recursive: true, force: true }).catch(() => { });
|
|
25
|
-
});
|
|
26
|
-
it('应该保留现有的 plugin 数组', async () => {
|
|
27
|
-
// 创建初始配置
|
|
28
|
-
const initialConfig = {
|
|
29
|
-
plugin: ['oh-my-opencode', 'another-plugin'],
|
|
30
|
-
agent: {
|
|
31
|
-
build: {
|
|
32
|
-
max_steps: 10
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const configPath = join(testConfigPath, 'opencode.json');
|
|
37
|
-
await writeFile(configPath, JSON.stringify(initialConfig, null, 2));
|
|
38
|
-
// 调用 saveToOpencodeConfig
|
|
39
|
-
await saveToOpencodeConfig({
|
|
40
|
-
apiKey: 'Bearer test-token',
|
|
41
|
-
baseURL: 'https://portal.qwen.ai/v1',
|
|
42
|
-
headers: {
|
|
43
|
-
'user-agent': 'QwenCode/test'
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
// 读取更新后的配置
|
|
47
|
-
const updatedConfig = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
48
|
-
// 验证 plugin 数组被保留
|
|
49
|
-
assert.ok(updatedConfig.plugin.includes('oh-my-opencode'), 'Should contain oh-my-opencode');
|
|
50
|
-
assert.ok(updatedConfig.plugin.includes('another-plugin'), 'Should contain another-plugin');
|
|
51
|
-
// 验证 agent 配置被保留
|
|
52
|
-
assert.ok(updatedConfig.agent, 'Should have agent config');
|
|
53
|
-
assert.ok(updatedConfig.agent.build, 'Should have build agent');
|
|
54
|
-
assert.strictEqual(updatedConfig.agent.build.max_steps, 10);
|
|
55
|
-
// 验证 qwen provider 被添加
|
|
56
|
-
assert.ok(updatedConfig.provider, 'Should have provider config');
|
|
57
|
-
assert.ok(updatedConfig.provider.qwen, 'Should have qwen provider');
|
|
58
|
-
});
|
|
59
|
-
it('应该保留现有的其他 provider', async () => {
|
|
60
|
-
// 创建初始配置,包含其他 provider
|
|
61
|
-
const initialConfig = {
|
|
62
|
-
provider: {
|
|
63
|
-
anthropic: {
|
|
64
|
-
options: {
|
|
65
|
-
apiKey: 'sk-ant-xxx'
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
openai: {
|
|
69
|
-
options: {
|
|
70
|
-
apiKey: 'sk-openai-xxx'
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
model: 'anthropic/claude-sonnet-4-20250514'
|
|
75
|
-
};
|
|
76
|
-
const configPath = join(testConfigPath, 'opencode.json');
|
|
77
|
-
await writeFile(configPath, JSON.stringify(initialConfig, null, 2));
|
|
78
|
-
// 调用 saveToOpencodeConfig
|
|
79
|
-
await saveToOpencodeConfig({
|
|
80
|
-
apiKey: 'Bearer test-token',
|
|
81
|
-
baseURL: 'https://portal.qwen.ai/v1',
|
|
82
|
-
headers: {}
|
|
83
|
-
});
|
|
84
|
-
// 读取更新后的配置
|
|
85
|
-
const updatedConfig = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
86
|
-
// 验证其他 provider 被保留
|
|
87
|
-
assert.ok(updatedConfig.provider.anthropic, "should be defined");
|
|
88
|
-
assert.strictEqual(updatedConfig.provider.anthropic.options.apiKey, 'sk-ant-xxx');
|
|
89
|
-
assert.ok(updatedConfig.provider.openai, "should be defined");
|
|
90
|
-
assert.strictEqual(updatedConfig.provider.openai.options.apiKey, 'sk-openai-xxx');
|
|
91
|
-
// 验证 qwen provider 被添加
|
|
92
|
-
assert.ok(updatedConfig.provider.qwen, "should be defined");
|
|
93
|
-
assert.strictEqual(updatedConfig.provider.qwen.options.apiKey, 'Bearer test-token');
|
|
94
|
-
});
|
|
95
|
-
it('不应该覆盖现有的 model 配置', async () => {
|
|
96
|
-
// 创建初始配置,包含自定义 model
|
|
97
|
-
const initialConfig = {
|
|
98
|
-
provider: {
|
|
99
|
-
anthropic: {
|
|
100
|
-
options: {
|
|
101
|
-
apiKey: 'sk-ant-xxx'
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
model: 'anthropic/claude-sonnet-4-20250514'
|
|
106
|
-
};
|
|
107
|
-
const configPath = join(testConfigPath, 'opencode.json');
|
|
108
|
-
await writeFile(configPath, JSON.stringify(initialConfig, null, 2));
|
|
109
|
-
// 调用 saveToOpencodeConfig
|
|
110
|
-
await saveToOpencodeConfig({
|
|
111
|
-
apiKey: 'Bearer test-token',
|
|
112
|
-
baseURL: 'https://portal.qwen.ai/v1',
|
|
113
|
-
headers: {}
|
|
114
|
-
});
|
|
115
|
-
// 读取更新后的配置
|
|
116
|
-
const updatedConfig = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
117
|
-
// 验证原有 model 被保留(不覆盖)
|
|
118
|
-
assert.strictEqual(updatedConfig.model, 'anthropic/claude-sonnet-4-20250514');
|
|
119
|
-
});
|
|
120
|
-
it('不应该设置默认 model', async () => {
|
|
121
|
-
// 创建初始配置,没有 model
|
|
122
|
-
const initialConfig = {
|
|
123
|
-
plugin: ['test-plugin']
|
|
124
|
-
};
|
|
125
|
-
const configPath = join(testConfigPath, 'opencode.json');
|
|
126
|
-
await writeFile(configPath, JSON.stringify(initialConfig, null, 2));
|
|
127
|
-
// 调用 saveToOpencodeConfig
|
|
128
|
-
await saveToOpencodeConfig({
|
|
129
|
-
apiKey: 'Bearer test-token',
|
|
130
|
-
baseURL: 'https://portal.qwen.ai/v1',
|
|
131
|
-
headers: {}
|
|
132
|
-
});
|
|
133
|
-
// 读取更新后的配置
|
|
134
|
-
const updatedConfig = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
135
|
-
// 验证不设置默认 model(让用户自己配置)
|
|
136
|
-
assert.strictEqual(updatedConfig.model, undefined, 'Should not set default model');
|
|
137
|
-
// 验证 plugin 被保留
|
|
138
|
-
assert.ok(updatedConfig.plugin.includes('test-plugin'), 'Should include test-plugin');
|
|
139
|
-
});
|
|
140
|
-
it('应该保留其他顶层配置', async () => {
|
|
141
|
-
// 创建初始配置,包含各种配置
|
|
142
|
-
const initialConfig = {
|
|
143
|
-
$schema: 'https://opencode.ai/config.json',
|
|
144
|
-
agent: {
|
|
145
|
-
build: { max_steps: 10 },
|
|
146
|
-
plan: { read_only: true }
|
|
147
|
-
},
|
|
148
|
-
mode: {
|
|
149
|
-
default: 'build'
|
|
150
|
-
},
|
|
151
|
-
instructions: ['./instructions.md'],
|
|
152
|
-
plugin: ['plugin-a', 'plugin-b'],
|
|
153
|
-
provider: {
|
|
154
|
-
anthropic: { options: { apiKey: 'sk-ant' } }
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
const configPath = join(testConfigPath, 'opencode.json');
|
|
158
|
-
await writeFile(configPath, JSON.stringify(initialConfig, null, 2));
|
|
159
|
-
// 调用 saveToOpencodeConfig
|
|
160
|
-
await saveToOpencodeConfig({
|
|
161
|
-
apiKey: 'Bearer test-token',
|
|
162
|
-
baseURL: 'https://portal.qwen.ai/v1',
|
|
163
|
-
headers: {}
|
|
164
|
-
});
|
|
165
|
-
// 读取更新后的配置
|
|
166
|
-
const updatedConfig = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
167
|
-
// 验证所有配置都被保留
|
|
168
|
-
assert.strictEqual(updatedConfig.$schema, 'https://opencode.ai/config.json');
|
|
169
|
-
assert.strictEqual(updatedConfig.agent.build.max_steps, 10);
|
|
170
|
-
assert.strictEqual(updatedConfig.agent.plan.read_only, true);
|
|
171
|
-
assert.strictEqual(updatedConfig.mode.default, 'build');
|
|
172
|
-
assert.ok(updatedConfig.instructions.includes('./instructions.md'), "should include");
|
|
173
|
-
assert.ok(updatedConfig.plugin.includes('plugin-a'), "should include");
|
|
174
|
-
assert.ok(updatedConfig.plugin.includes('plugin-b'), "should include");
|
|
175
|
-
assert.strictEqual(updatedConfig.provider.anthropic.options.apiKey, 'sk-ant');
|
|
176
|
-
// 验证 qwen 被添加
|
|
177
|
-
assert.ok(updatedConfig.provider.qwen, "should be defined");
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
// 辅助函数
|
|
181
|
-
async function mkdtemp(template) {
|
|
182
|
-
const fs = await import('node:fs/promises');
|
|
183
|
-
const path = await import('node:path');
|
|
184
|
-
const os = await import('node:os');
|
|
185
|
-
const dir = path.join(os.tmpdir(), `${template}${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
186
|
-
await fs.mkdir(dir, { recursive: true });
|
|
187
|
-
return dir;
|
|
188
|
-
}
|
package/dist/plugin.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Qwen Login Plugin - 通义千问认证插件
|
|
3
|
-
*
|
|
4
|
-
* 为 opencode 提供 Qwen 模型认证支持
|
|
5
|
-
* 自动从 qwen-code OAuth 配置导入认证信息
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Qwen 认证插件主函数
|
|
9
|
-
*/
|
|
10
|
-
declare function plugin(pluginInput: any): Promise<{
|
|
11
|
-
config: (config: any) => Promise<void>;
|
|
12
|
-
}>;
|
|
13
|
-
export default plugin;
|
|
14
|
-
//# sourceMappingURL=plugin.d.ts.map
|
package/dist/plugin.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,iBAAe,MAAM,CAAC,WAAW,EAAE,GAAG;qBAUX,GAAG;GAgE7B;AAGD,eAAe,MAAM,CAAA"}
|
package/dist/plugin.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Qwen Login Plugin - 通义千问认证插件
|
|
3
|
-
*
|
|
4
|
-
* 为 opencode 提供 Qwen 模型认证支持
|
|
5
|
-
* 自动从 qwen-code OAuth 配置导入认证信息
|
|
6
|
-
*/
|
|
7
|
-
import { configureOpencodeFromQwenOAuth, readOAuthCredentials, isTokenValid, getApiConfigFromOAuth } from "./qwen-oauth.js";
|
|
8
|
-
/**
|
|
9
|
-
* Qwen 认证插件主函数
|
|
10
|
-
*/
|
|
11
|
-
async function plugin(pluginInput) {
|
|
12
|
-
// 插件加载时立即写入配置文件
|
|
13
|
-
try {
|
|
14
|
-
await configureOpencodeFromQwenOAuth();
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
// 静默失败,使用其他认证方式
|
|
18
|
-
}
|
|
19
|
-
return {
|
|
20
|
-
// 配置钩子 - 修改运行时配置
|
|
21
|
-
config: async (config) => {
|
|
22
|
-
try {
|
|
23
|
-
// 1. 读取 OAuth 凭证
|
|
24
|
-
const creds = await readOAuthCredentials();
|
|
25
|
-
if (!creds) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
// 2. 检查 token 是否有效
|
|
29
|
-
if (!isTokenValid(creds)) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
// 3. 生成 API 配置
|
|
33
|
-
const apiConfig = getApiConfigFromOAuth(creds);
|
|
34
|
-
// 4. 按照 opencode provider schema 配置
|
|
35
|
-
if (!config.provider) {
|
|
36
|
-
config.provider = {};
|
|
37
|
-
}
|
|
38
|
-
config.provider.qwen = {
|
|
39
|
-
name: "Qwen (通义千问)",
|
|
40
|
-
npm: "@ai-sdk/openai-compatible",
|
|
41
|
-
env: [],
|
|
42
|
-
models: {
|
|
43
|
-
"coder-model": {
|
|
44
|
-
name: "Qwen Coder Model",
|
|
45
|
-
tool_call: true,
|
|
46
|
-
reasoning: false,
|
|
47
|
-
attachment: true,
|
|
48
|
-
temperature: true,
|
|
49
|
-
interleaved: true,
|
|
50
|
-
limit: {
|
|
51
|
-
context: 256000,
|
|
52
|
-
output: 8192
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
"qwen-plus": {
|
|
56
|
-
name: "Qwen Plus",
|
|
57
|
-
tool_call: true,
|
|
58
|
-
reasoning: false,
|
|
59
|
-
attachment: true,
|
|
60
|
-
temperature: true,
|
|
61
|
-
interleaved: true,
|
|
62
|
-
limit: {
|
|
63
|
-
context: 131072,
|
|
64
|
-
output: 8192
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
options: {
|
|
69
|
-
apiKey: apiConfig.apiKey,
|
|
70
|
-
baseURL: apiConfig.baseURL,
|
|
71
|
-
headers: apiConfig.headers
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
// 静默失败
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// 只导出默认函数
|
|
82
|
-
export default plugin;
|
|
83
|
-
//# sourceMappingURL=plugin.js.map
|
package/dist/plugin.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,8BAA8B,EAAE,oBAAoB,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AAE3H;;GAEG;AACH,KAAK,UAAU,MAAM,CAAC,WAAgB;IACpC,gBAAgB;IAChB,IAAI,CAAC;QACH,MAAM,8BAA8B,EAAE,CAAA;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,gBAAgB;IAClB,CAAC;IAED,OAAO;QACL,iBAAiB;QACjB,MAAM,EAAE,KAAK,EAAE,MAAW,EAAE,EAAE;YAC5B,IAAI,CAAC;gBACH,iBAAiB;gBACjB,MAAM,KAAK,GAAG,MAAM,oBAAoB,EAAE,CAAA;gBAE1C,IAAI,CAAC,KAAK,EAAE,CAAC;oBACX,OAAM;gBACR,CAAC;gBAED,mBAAmB;gBACnB,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;oBACzB,OAAM;gBACR,CAAC;gBAED,eAAe;gBACf,MAAM,SAAS,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAA;gBAE9C,oCAAoC;gBACpC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;oBACrB,MAAM,CAAC,QAAQ,GAAG,EAAE,CAAA;gBACtB,CAAC;gBAED,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG;oBACrB,IAAI,EAAE,aAAa;oBACnB,GAAG,EAAE,2BAA2B;oBAChC,GAAG,EAAE,EAAE;oBACP,MAAM,EAAE;wBACN,aAAa,EAAE;4BACb,IAAI,EAAE,kBAAkB;4BACxB,SAAS,EAAE,IAAI;4BACf,SAAS,EAAE,KAAK;4BAChB,UAAU,EAAE,IAAI;4BAChB,WAAW,EAAE,IAAI;4BACjB,WAAW,EAAE,IAAI;4BACjB,KAAK,EAAE;gCACL,OAAO,EAAE,MAAM;gCACf,MAAM,EAAE,IAAI;6BACb;yBACF;wBACD,WAAW,EAAE;4BACX,IAAI,EAAE,WAAW;4BACjB,SAAS,EAAE,IAAI;4BACf,SAAS,EAAE,KAAK;4BAChB,UAAU,EAAE,IAAI;4BAChB,WAAW,EAAE,IAAI;4BACjB,WAAW,EAAE,IAAI;4BACjB,KAAK,EAAE;gCACL,OAAO,EAAE,MAAM;gCACf,MAAM,EAAE,IAAI;6BACb;yBACF;qBACF;oBACD,OAAO,EAAE;wBACP,MAAM,EAAE,SAAS,CAAC,MAAM;wBACxB,OAAO,EAAE,SAAS,CAAC,OAAO;wBAC1B,OAAO,EAAE,SAAS,CAAC,OAAO;qBAC3B;iBACF,CAAA;YAEH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO;YACT,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,UAAU;AACV,eAAe,MAAM,CAAA"}
|
package/dist/qwen-oauth.test.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Qwen Login Plugin - 单元测试
|
|
3
|
-
*
|
|
4
|
-
* 测试配置合并、OAuth 读取等功能
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
7
|
-
import { writeFile, mkdir, rm } from 'node:fs/promises';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { tmpdir } from 'node:os';
|
|
10
|
-
import assert from 'node:assert';
|
|
11
|
-
import { readOAuthCredentials, isTokenValid, getApiConfigFromOAuth, buildBaseUrl, buildDefaultHeaders } from './qwen-oauth.js';
|
|
12
|
-
// 测试用的临时目录
|
|
13
|
-
let testDir;
|
|
14
|
-
let testQwenDir;
|
|
15
|
-
describe('Qwen Login Plugin', () => {
|
|
16
|
-
beforeEach(async () => {
|
|
17
|
-
// 创建临时测试目录
|
|
18
|
-
testDir = await mkdtemp('qwen-login-plugin-test-');
|
|
19
|
-
testQwenDir = join(testDir, '.qwen');
|
|
20
|
-
await mkdir(testQwenDir, { recursive: true });
|
|
21
|
-
});
|
|
22
|
-
afterEach(async () => {
|
|
23
|
-
// 清理临时目录
|
|
24
|
-
await rm(testDir, { recursive: true, force: true }).catch(() => { });
|
|
25
|
-
});
|
|
26
|
-
describe('buildBaseUrl', () => {
|
|
27
|
-
it('应该从 resource_url 构建正确的 endpoint', () => {
|
|
28
|
-
assert.strictEqual(buildBaseUrl('portal.qwen.ai'), 'https://portal.qwen.ai/v1');
|
|
29
|
-
});
|
|
30
|
-
it('应该在没有 resource_url 时使用默认值', () => {
|
|
31
|
-
assert.strictEqual(buildBaseUrl(''), 'https://dashscope.aliyuncs.com/compatible-mode/v1');
|
|
32
|
-
assert.strictEqual(buildBaseUrl(undefined), 'https://dashscope.aliyuncs.com/compatible-mode/v1');
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
describe('buildDefaultHeaders', () => {
|
|
36
|
-
it('应该生成完整的请求头', () => {
|
|
37
|
-
const headers = buildDefaultHeaders('test-token-123');
|
|
38
|
-
assert.ok(headers.authorization, 'Should have authorization header');
|
|
39
|
-
assert.strictEqual(headers.authorization, 'Bearer test-token-123');
|
|
40
|
-
assert.ok(headers['user-agent'], 'Should have user-agent header');
|
|
41
|
-
assert.match(headers['user-agent'], /QwenCode\/.*\(.*;.*\)/);
|
|
42
|
-
assert.ok(headers['x-dashscope-authtype'], 'Should have x-dashscope-authtype header');
|
|
43
|
-
assert.strictEqual(headers['x-dashscope-authtype'], 'qwen-oauth');
|
|
44
|
-
assert.ok(headers['content-type'], 'Should have content-type header');
|
|
45
|
-
assert.strictEqual(headers['content-type'], 'application/json');
|
|
46
|
-
assert.ok(headers['accept'], 'Should have accept header');
|
|
47
|
-
assert.strictEqual(headers['accept'], 'application/json');
|
|
48
|
-
});
|
|
49
|
-
it('应该包含所有 stainless 相关 headers', () => {
|
|
50
|
-
const headers = buildDefaultHeaders('test-token');
|
|
51
|
-
assert.strictEqual(headers['x-stainless-lang'], 'js');
|
|
52
|
-
assert.strictEqual(headers['x-stainless-package-version'], '4.104.0');
|
|
53
|
-
assert.strictEqual(headers['x-stainless-os'], 'MacOS');
|
|
54
|
-
assert.strictEqual(headers['x-stainless-runtime'], 'node');
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
describe('isTokenValid', () => {
|
|
58
|
-
it('应该返回 true 当 token 未过期', () => {
|
|
59
|
-
const futureDate = Date.now() + 1000 * 60 * 60 * 24; // 1 天后
|
|
60
|
-
assert.strictEqual(isTokenValid({ expiry_date: futureDate }), true);
|
|
61
|
-
});
|
|
62
|
-
it('应该返回 false 当 token 已过期', () => {
|
|
63
|
-
const pastDate = Date.now() - 1000 * 60 * 60; // 1 小时前
|
|
64
|
-
assert.strictEqual(isTokenValid({ expiry_date: pastDate }), false);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe('readOAuthCredentials', () => {
|
|
68
|
-
it('应该成功读取 OAuth 凭证', async () => {
|
|
69
|
-
// 模拟 OAuth 凭证文件
|
|
70
|
-
const mockCreds = {
|
|
71
|
-
access_token: 'test-access-token',
|
|
72
|
-
token_type: 'Bearer',
|
|
73
|
-
refresh_token: 'test-refresh-token',
|
|
74
|
-
resource_url: 'portal.qwen.ai',
|
|
75
|
-
expiry_date: Date.now() + 1000000
|
|
76
|
-
};
|
|
77
|
-
await writeFile(join(testQwenDir, 'oauth_creds.json'), JSON.stringify(mockCreds));
|
|
78
|
-
// 临时修改 home 目录
|
|
79
|
-
const originalHome = process.env.HOME;
|
|
80
|
-
process.env.HOME = testDir;
|
|
81
|
-
try {
|
|
82
|
-
const creds = await readOAuthCredentials();
|
|
83
|
-
assert.deepStrictEqual(creds, mockCreds);
|
|
84
|
-
}
|
|
85
|
-
finally {
|
|
86
|
-
process.env.HOME = originalHome;
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
it('应该在文件不存在时返回 null', async () => {
|
|
90
|
-
const originalHome = process.env.HOME;
|
|
91
|
-
process.env.HOME = testDir;
|
|
92
|
-
try {
|
|
93
|
-
const creds = await readOAuthCredentials();
|
|
94
|
-
assert.strictEqual(creds, null);
|
|
95
|
-
}
|
|
96
|
-
finally {
|
|
97
|
-
process.env.HOME = originalHome;
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
describe('getApiConfigFromOAuth', () => {
|
|
102
|
-
it('应该从 OAuth 凭证生成正确的 API 配置', () => {
|
|
103
|
-
const mockCreds = {
|
|
104
|
-
access_token: 'test-token',
|
|
105
|
-
token_type: 'Bearer',
|
|
106
|
-
refresh_token: 'test-refresh',
|
|
107
|
-
resource_url: 'portal.qwen.ai',
|
|
108
|
-
expiry_date: Date.now() + 1000000
|
|
109
|
-
};
|
|
110
|
-
const config = getApiConfigFromOAuth(mockCreds);
|
|
111
|
-
assert.strictEqual(config.apiKey, 'Bearer test-token');
|
|
112
|
-
assert.strictEqual(config.baseURL, 'https://portal.qwen.ai/v1');
|
|
113
|
-
assert.ok(config.headers);
|
|
114
|
-
assert.strictEqual(config.headers?.authorization, 'Bearer test-token');
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
// 辅助函数
|
|
119
|
-
async function mkdtemp(template) {
|
|
120
|
-
const dir = join(tmpdir(), `${template}${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
121
|
-
await mkdir(dir, { recursive: true });
|
|
122
|
-
return dir;
|
|
123
|
-
}
|