@hotmanxp/opencode-qwen-login-plugin 1.0.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.
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Qwen-code OAuth 认证配置工具
3
+ *
4
+ * 从 ~/.qwen/oauth_creds.json 读取 OAuth token
5
+ * 用于配置 opencode 使用 Qwen API
6
+ */
7
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+ /**
11
+ * 默认 Qwen API 基础 URL(阿里云 DashScope)
12
+ */
13
+ export const DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
14
+ /**
15
+ * Qwen Code 版本信息
16
+ */
17
+ export const QWEN_CODE_VERSION = "unknown";
18
+ /**
19
+ * 获取当前平台信息
20
+ */
21
+ export function getPlatformInfo() {
22
+ return {
23
+ platform: process.platform === 'darwin' ? 'darwin' : process.platform,
24
+ arch: process.arch === 'arm64' ? 'arm64' : process.arch,
25
+ nodeVersion: process.version.replace('v', '')
26
+ };
27
+ }
28
+ /**
29
+ * 构建默认的 Qwen Code User-Agent
30
+ *
31
+ * 格式:QwenCode/<version> (<platform>; <arch>)
32
+ * 示例:QwenCode/unknown (darwin; arm64)
33
+ */
34
+ export function buildUserAgent() {
35
+ const platformInfo = getPlatformInfo();
36
+ return `QwenCode/${QWEN_CODE_VERSION} (${platformInfo.platform}; ${platformInfo.arch})`;
37
+ }
38
+ /**
39
+ * 构建 Qwen API 的默认请求头
40
+ *
41
+ * @param token OAuth token
42
+ * @returns 请求头对象
43
+ */
44
+ export function buildDefaultHeaders(token) {
45
+ const userAgent = buildUserAgent();
46
+ const platformInfo = getPlatformInfo();
47
+ return {
48
+ 'accept': 'application/json',
49
+ 'content-type': 'application/json',
50
+ 'user-agent': userAgent,
51
+ 'x-stainless-lang': 'js',
52
+ 'x-stainless-package-version': '4.104.0',
53
+ 'x-stainless-os': 'MacOS',
54
+ 'x-stainless-arch': platformInfo.arch,
55
+ 'x-stainless-runtime': 'node',
56
+ 'x-stainless-runtime-version': platformInfo.nodeVersion,
57
+ 'authorization': `Bearer ${token}`,
58
+ 'x-dashscope-cachecontrol': 'enable',
59
+ 'x-dashscope-useragent': userAgent,
60
+ 'x-dashscope-authtype': 'qwen-oauth',
61
+ 'x-stainless-retry-count': '0',
62
+ 'x-stainless-timeout': '120'
63
+ };
64
+ }
65
+ /**
66
+ * 从 resource_url 构建最终的 API endpoint
67
+ *
68
+ * @param resourceUrl resource_url from OAuth credentials (e.g., "portal.qwen.ai")
69
+ * @returns 完整的 API endpoint URL
70
+ */
71
+ export function buildBaseUrl(resourceUrl) {
72
+ // 从 resource_url 构建 endpoint
73
+ // resource_url: "portal.qwen.ai" -> "https://portal.qwen.ai/v1"
74
+ if (resourceUrl) {
75
+ return `https://${resourceUrl}/v1`;
76
+ }
77
+ return DEFAULT_BASE_URL;
78
+ }
79
+ /**
80
+ * 获取 qwen-code OAuth 凭证文件路径
81
+ */
82
+ function getOAuthCredsPath() {
83
+ return join(homedir(), ".qwen", "oauth_creds.json");
84
+ }
85
+ /**
86
+ * 读取 OAuth 凭证
87
+ *
88
+ * @returns OAuth 凭证,如果文件不存在或读取失败则返回 null
89
+ */
90
+ export async function readOAuthCredentials() {
91
+ try {
92
+ const credsPath = getOAuthCredsPath();
93
+ const content = await readFile(credsPath, "utf-8");
94
+ return JSON.parse(content);
95
+ }
96
+ catch (error) {
97
+ // 文件不存在或读取失败
98
+ return null;
99
+ }
100
+ }
101
+ /**
102
+ * 检查 OAuth token 是否过期
103
+ *
104
+ * @param creds OAuth 凭证
105
+ * @returns boolean - 是否有效(未过期)
106
+ */
107
+ export function isTokenValid(creds) {
108
+ const now = Date.now();
109
+ return creds.expiry_date > now;
110
+ }
111
+ /**
112
+ * 从 OAuth 凭证获取 API 配置
113
+ *
114
+ * @param creds OAuth 凭证
115
+ * @returns API 配置(apiKey、baseURL 和 headers)
116
+ */
117
+ export function getApiConfigFromOAuth(creds) {
118
+ const baseURL = buildBaseUrl(creds.resource_url);
119
+ const headers = buildDefaultHeaders(creds.access_token);
120
+ return {
121
+ apiKey: `${creds.token_type} ${creds.access_token}`,
122
+ baseURL: baseURL,
123
+ headers: headers
124
+ };
125
+ }
126
+ /**
127
+ * 获取 Qwen API 配置(从 OAuth)
128
+ *
129
+ * @returns API 配置,如果 OAuth 凭证无效则返回 null
130
+ */
131
+ export async function getQwenConfigFromOAuth() {
132
+ const creds = await readOAuthCredentials();
133
+ if (!creds) {
134
+ return null;
135
+ }
136
+ if (!isTokenValid(creds)) {
137
+ return null;
138
+ }
139
+ return getApiConfigFromOAuth(creds);
140
+ }
141
+ /**
142
+ * 保存配置到 opencode.json
143
+ *
144
+ * @param config API 配置
145
+ */
146
+ export async function saveToOpencodeConfig(config) {
147
+ // 优先使用 ~/.config/opencode/opencode.json (XDG 标准路径)
148
+ let opencodeConfigPath = join(homedir(), ".config", "opencode", "opencode.json");
149
+ // 如果 XDG 路径不存在,使用 macOS 标准路径
150
+ try {
151
+ await readFile(opencodeConfigPath, "utf-8");
152
+ }
153
+ catch {
154
+ // XDG 路径不存在,使用 macOS 路径
155
+ opencodeConfigPath = join(homedir(), "Library", "Application Support", "opencode", "opencode.json");
156
+ }
157
+ try {
158
+ // 确保目录存在
159
+ await mkdir(join(opencodeConfigPath, ".."), { recursive: true });
160
+ // 读取现有配置
161
+ let existingConfig = {};
162
+ try {
163
+ const content = await readFile(opencodeConfigPath, "utf-8");
164
+ existingConfig = JSON.parse(content);
165
+ }
166
+ catch {
167
+ // 配置文件不存在,创建新的
168
+ }
169
+ // 更新配置
170
+ if (!existingConfig.provider) {
171
+ existingConfig.provider = {};
172
+ }
173
+ existingConfig.provider.qwen = {
174
+ name: "Qwen (通义千问)",
175
+ npm: "@ai-sdk/openai-compatible",
176
+ env: [],
177
+ models: {
178
+ "coder-model": {
179
+ name: "Qwen Coder Model",
180
+ tool_call: true,
181
+ reasoning: false,
182
+ attachment: true,
183
+ temperature: true,
184
+ interleaved: true,
185
+ limit: {
186
+ context: 256000,
187
+ output: 8192
188
+ }
189
+ },
190
+ "qwen-plus": {
191
+ name: "Qwen Plus",
192
+ tool_call: true,
193
+ reasoning: false,
194
+ attachment: true,
195
+ temperature: true,
196
+ interleaved: true,
197
+ limit: {
198
+ context: 131072,
199
+ output: 8192
200
+ }
201
+ }
202
+ },
203
+ options: {
204
+ apiKey: config.apiKey,
205
+ baseURL: config.baseURL,
206
+ // 保存自定义请求头
207
+ ...(config.headers ? { headers: config.headers } : {})
208
+ }
209
+ };
210
+ // 不设置默认 model,让用户自己在 opencode.json 中配置
211
+ // 用户可以选择:qwen/coder-model, qwen/qwen-plus 等
212
+ // 保存配置
213
+ await writeFile(opencodeConfigPath, JSON.stringify(existingConfig, null, 2), "utf-8");
214
+ }
215
+ catch (error) {
216
+ throw error;
217
+ }
218
+ }
219
+ /**
220
+ * 主函数:从 qwen-code OAuth 配置 opencode
221
+ *
222
+ * @returns 是否成功配置
223
+ */
224
+ export async function configureOpencodeFromQwenOAuth() {
225
+ try {
226
+ // 1. 读取 OAuth 凭证
227
+ const creds = await readOAuthCredentials();
228
+ if (!creds) {
229
+ return false;
230
+ }
231
+ // 2. 检查 token 是否有效
232
+ if (!isTokenValid(creds)) {
233
+ return false;
234
+ }
235
+ // 3. 获取 API 配置(包含 headers)
236
+ const apiConfig = getApiConfigFromOAuth(creds);
237
+ // 4. 保存到 opencode 配置
238
+ await saveToOpencodeConfig(apiConfig);
239
+ return true;
240
+ }
241
+ catch (error) {
242
+ return false;
243
+ }
244
+ }
245
+ //# sourceMappingURL=qwen-oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qwen-oauth.js","sourceRoot":"","sources":["../src/qwen-oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,123 @@
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
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@hotmanxp/opencode-qwen-login-plugin",
3
+ "version": "1.0.0",
4
+ "description": "通义千问认证插件 - 从 qwen-code OAuth 自动配置 opencode",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "qwen-login": "./dist/cli.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsc --watch",
14
+ "clean": "rm -rf dist",
15
+ "prepublishOnly": "npm run build",
16
+ "start": "node dist/cli.js",
17
+ "test": "node --test dist/*.test.js",
18
+ "test:watch": "node --test --watch dist/*.test.js"
19
+ },
20
+ "keywords": [
21
+ "opencode",
22
+ "plugin",
23
+ "qwen",
24
+ "auth",
25
+ "通义千问",
26
+ "阿里云"
27
+ ],
28
+ "author": "",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/your-username/qwen-login-plugin"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "exports": {
40
+ ".": "./dist/index.js",
41
+ "./cli": "./dist/cli.js"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.10.0",
45
+ "typescript": "^5.8.2"
46
+ }
47
+ }