@melodyoftears/opencode-qwen-auth 1.3.1
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/LICENSE +21 -0
- package/README.md +194 -0
- package/README.zh-CN.md +165 -0
- package/dist/cli.js +83 -0
- package/dist/index.js +394 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 melodyoftears
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# 🤖 Qwen Code OAuth Plugin for OpenCode
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="assets/screenshot.png" alt="OpenCode with Qwen Code" width="800">
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
**Authenticate OpenCode CLI with your qwen.ai account.** This plugin enables you to use Qwen OAuth models (`coder-model` and `vision-model`) with **2,000 free requests per day** - no API key or credit card required!
|
|
12
|
+
|
|
13
|
+
[🇨🇳 中文文档](./README.zh-CN.md)
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- 🔐 **OAuth Device Flow** - Secure browser-based authentication (RFC 8628)
|
|
18
|
+
- ⚡ **Automatic Polling** - No need to press Enter after authorizing
|
|
19
|
+
- 🆓 **2,000 req/day free** - Generous free tier with no credit card
|
|
20
|
+
- 🧠 **1M context window** - Models with 1 million token context
|
|
21
|
+
- 🔄 **Auto-refresh** - Tokens renewed automatically before expiration
|
|
22
|
+
- 🔗 **qwen-code compatible** - Reuses credentials from `~/.qwen/oauth_creds.json`
|
|
23
|
+
|
|
24
|
+
## 📋 Prerequisites
|
|
25
|
+
|
|
26
|
+
- [OpenCode CLI](https://opencode.ai) installed
|
|
27
|
+
- A [qwen.ai](https://chat.qwen.ai) account (free to create)
|
|
28
|
+
|
|
29
|
+
## 🚀 Installation
|
|
30
|
+
|
|
31
|
+
### 1. Install the plugin
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd ~/.opencode && npm install @melodyoftears/opencode-qwen-auth
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Enable the plugin
|
|
38
|
+
|
|
39
|
+
Edit `~/.opencode/opencode.jsonc`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"plugin": ["@melodyoftears/opencode-qwen-auth"]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 🔑 Usage
|
|
48
|
+
|
|
49
|
+
### 1. Login
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
opencode auth login
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Select Provider
|
|
56
|
+
|
|
57
|
+
Choose **"Other"** and type `qwen-code`
|
|
58
|
+
|
|
59
|
+
### 3. Authenticate
|
|
60
|
+
|
|
61
|
+
Select **"Qwen Code (qwen.ai OAuth)"**
|
|
62
|
+
|
|
63
|
+
- A browser window will open for you to authorize
|
|
64
|
+
- The plugin automatically detects when you complete authorization
|
|
65
|
+
- No need to copy/paste codes or press Enter!
|
|
66
|
+
|
|
67
|
+
> [!TIP]
|
|
68
|
+
> In the OpenCode TUI (graphical interface), the **Qwen Code** provider appears automatically in the provider list.
|
|
69
|
+
|
|
70
|
+
## 🎯 Available Models
|
|
71
|
+
|
|
72
|
+
| Model | ID | Input | Output | Context | Max Output | Cost |
|
|
73
|
+
|-------|----|-------|--------|---------|------------|------|
|
|
74
|
+
| Qwen Coder (Qwen 3.5 Plus) | `coder-model` | text | text | 1M tokens | 65,536 tokens | Free |
|
|
75
|
+
| Qwen VL Plus (Vision) | `vision-model` | text, image | text | 128K tokens | 8,192 tokens | Free |
|
|
76
|
+
|
|
77
|
+
### Using a specific model
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
opencode --provider qwen-code --model coder-model
|
|
81
|
+
opencode --provider qwen-code --model vision-model
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## ⚙️ How It Works
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
88
|
+
│ OpenCode CLI │────▶│ qwen.ai OAuth │────▶│ Qwen Models │
|
|
89
|
+
│ │◀────│ (Device Flow) │◀────│ API │
|
|
90
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
1. **Device Flow (RFC 8628)**: Opens your browser to `chat.qwen.ai` for authentication
|
|
94
|
+
2. **Automatic Polling**: Detects authorization completion automatically
|
|
95
|
+
3. **Token Storage**: Saves credentials to `~/.qwen/oauth_creds.json`
|
|
96
|
+
4. **Auto-refresh**: Renews tokens 30 seconds before expiration
|
|
97
|
+
|
|
98
|
+
## 📊 Usage Limits
|
|
99
|
+
|
|
100
|
+
| Plan | Rate Limit | Daily Limit |
|
|
101
|
+
|------|------------|-------------|
|
|
102
|
+
| Free (OAuth) | 60 req/min | 2,000 req/day |
|
|
103
|
+
|
|
104
|
+
> [!NOTE]
|
|
105
|
+
> Limits reset at midnight UTC. For higher limits, consider using an API key from [DashScope](https://dashscope.aliyun.com).
|
|
106
|
+
|
|
107
|
+
## 🔧 Troubleshooting
|
|
108
|
+
|
|
109
|
+
### Token expired
|
|
110
|
+
|
|
111
|
+
The plugin automatically renews tokens. If issues persist:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Remove old credentials
|
|
115
|
+
rm ~/.qwen/oauth_creds.json
|
|
116
|
+
|
|
117
|
+
# Re-authenticate
|
|
118
|
+
opencode auth login
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Provider not showing in `auth login`
|
|
122
|
+
|
|
123
|
+
The `qwen-code` provider is added via plugin. In the `opencode auth login` command:
|
|
124
|
+
|
|
125
|
+
1. Select **"Other"**
|
|
126
|
+
2. Type `qwen-code`
|
|
127
|
+
|
|
128
|
+
### Rate limit exceeded (429 errors)
|
|
129
|
+
|
|
130
|
+
- Wait until midnight UTC for quota reset
|
|
131
|
+
- Switch to another Qwen account and login again if quota is exhausted
|
|
132
|
+
- Consider [DashScope API](https://dashscope.aliyun.com) for higher limits
|
|
133
|
+
|
|
134
|
+
## 🛠️ Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Clone the repository
|
|
138
|
+
git clone https://github.com/1579364808/opencode-qwen-auth.git
|
|
139
|
+
cd opencode-qwencode-auth
|
|
140
|
+
|
|
141
|
+
# Install dependencies
|
|
142
|
+
bun install
|
|
143
|
+
|
|
144
|
+
# Type check
|
|
145
|
+
bun run typecheck
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Local testing
|
|
149
|
+
|
|
150
|
+
Edit `~/.opencode/package.json`:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"dependencies": {
|
|
155
|
+
"@melodyoftears/opencode-qwen-auth": "file:///absolute/path/to/opencode-qwencode-auth"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Then reinstall:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
cd ~/.opencode && npm install
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## 📁 Project Structure
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
src/
|
|
170
|
+
├── constants.ts # OAuth endpoints, models config
|
|
171
|
+
├── types.ts # TypeScript interfaces
|
|
172
|
+
├── index.ts # Main plugin entry point
|
|
173
|
+
├── qwen/
|
|
174
|
+
│ └── oauth.ts # OAuth Device Flow + PKCE
|
|
175
|
+
└── plugin/
|
|
176
|
+
├── auth.ts # Credentials management
|
|
177
|
+
└── utils.ts # Helper utilities
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 🔗 Related Projects
|
|
181
|
+
|
|
182
|
+
- [qwen-code](https://github.com/QwenLM/qwen-code) - Official Qwen coding CLI
|
|
183
|
+
- [OpenCode](https://opencode.ai) - AI-powered CLI for development
|
|
184
|
+
- [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) - Similar plugin for Google Gemini
|
|
185
|
+
|
|
186
|
+
## 📄 License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
<p align="center">
|
|
193
|
+
Made with ❤️ for the OpenCode community
|
|
194
|
+
</p>
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# 🤖 OpenCode 的 Qwen OAuth 插件
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="assets/screenshot.png" alt="OpenCode with Qwen Code" width="800">
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
**使用你的 qwen.ai 账号为 OpenCode CLI 登录。** 该插件可让你使用 Qwen OAuth 模型(`coder-model` 与 `vision-model`),享受 **每天 2,000 次免费请求**,无需 API Key 或信用卡。
|
|
12
|
+
|
|
13
|
+
[🇺🇸 English](./README.md)
|
|
14
|
+
|
|
15
|
+
## ✨ 功能
|
|
16
|
+
|
|
17
|
+
- 🔐 **OAuth Device Flow**:基于浏览器的安全登录(RFC 8628)
|
|
18
|
+
- ⚡ **自动轮询**:授权完成后自动检测,无需手动回车
|
|
19
|
+
- 🆓 **免费额度**:每天 2,000 次请求
|
|
20
|
+
- 🧠 **超长上下文**:支持最高 1M 上下文模型
|
|
21
|
+
- 🔄 **自动续期**:Token 过期前自动刷新
|
|
22
|
+
- 🔗 **兼容 qwen-code**:复用 `~/.qwen/oauth_creds.json` 凭据
|
|
23
|
+
|
|
24
|
+
## 📋 前置条件
|
|
25
|
+
|
|
26
|
+
- 已安装 [OpenCode CLI](https://opencode.ai)
|
|
27
|
+
- 一个 [qwen.ai](https://chat.qwen.ai) 账号(可免费注册)
|
|
28
|
+
|
|
29
|
+
## 🚀 安装
|
|
30
|
+
|
|
31
|
+
### 1) 安装插件
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd ~/.opencode && npm install @melodyoftears/opencode-qwen-auth
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2) 启用插件
|
|
38
|
+
|
|
39
|
+
编辑 `~/.opencode/opencode.jsonc`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"plugin": ["@melodyoftears/opencode-qwen-auth"]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 🔑 使用
|
|
48
|
+
|
|
49
|
+
### 1) 登录
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
opencode auth login
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2) 选择 Provider
|
|
56
|
+
|
|
57
|
+
选择 **"Other"**,输入 `qwen-code`
|
|
58
|
+
|
|
59
|
+
### 3) 完成授权
|
|
60
|
+
|
|
61
|
+
选择 **"Qwen Code (qwen.ai OAuth)"**
|
|
62
|
+
|
|
63
|
+
- 浏览器会自动打开授权页
|
|
64
|
+
- 授权完成后插件会自动检测并保存
|
|
65
|
+
|
|
66
|
+
> [!TIP]
|
|
67
|
+
> 在 OpenCode TUI 中,**Qwen Code** provider 会自动出现在 provider 列表。
|
|
68
|
+
|
|
69
|
+
## 🎯 可用模型
|
|
70
|
+
|
|
71
|
+
| 模型 | ID | 输入 | 输出 | 上下文 | 最大输出 | 费用 |
|
|
72
|
+
|------|----|------|------|--------|----------|------|
|
|
73
|
+
| Qwen Coder (Qwen 3.5 Plus) | `coder-model` | text | text | 1M tokens | 65,536 tokens | Free |
|
|
74
|
+
| Qwen VL Plus (Vision) | `vision-model` | text, image | text | 128K tokens | 8,192 tokens | Free |
|
|
75
|
+
|
|
76
|
+
### 指定模型运行
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
opencode --provider qwen-code --model coder-model
|
|
80
|
+
opencode --provider qwen-code --model vision-model
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## ⚙️ 工作原理
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
87
|
+
│ OpenCode CLI │────▶│ qwen.ai OAuth │────▶│ Qwen Models │
|
|
88
|
+
│ │◀────│ (Device Flow) │◀────│ API │
|
|
89
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
1. Device Flow 打开 `chat.qwen.ai` 授权页面
|
|
93
|
+
2. 插件自动轮询授权结果
|
|
94
|
+
3. 凭据保存到 `~/.qwen/oauth_creds.json`
|
|
95
|
+
4. Access Token 在到期前自动刷新
|
|
96
|
+
|
|
97
|
+
## 📊 使用限制
|
|
98
|
+
|
|
99
|
+
| 计划 | 频率限制 | 每日限制 |
|
|
100
|
+
|------|----------|----------|
|
|
101
|
+
| 免费(OAuth) | 60 req/min | 2,000 req/day |
|
|
102
|
+
|
|
103
|
+
> [!NOTE]
|
|
104
|
+
> 配额通常按 UTC 零点重置。若需更高额度,可考虑使用 [DashScope](https://dashscope.aliyun.com) API Key。
|
|
105
|
+
|
|
106
|
+
## 🔧 故障排查
|
|
107
|
+
|
|
108
|
+
### Token 过期或异常
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# 删除旧凭据
|
|
112
|
+
rm ~/.qwen/oauth_creds.json
|
|
113
|
+
|
|
114
|
+
# 重新登录
|
|
115
|
+
opencode auth login
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `auth login` 中看不到 provider
|
|
119
|
+
|
|
120
|
+
`qwen-code` 是插件注入的 provider。请在 `opencode auth login` 中:
|
|
121
|
+
|
|
122
|
+
1. 选择 **"Other"**
|
|
123
|
+
2. 输入 `qwen-code`
|
|
124
|
+
|
|
125
|
+
### 遇到 429 限流
|
|
126
|
+
|
|
127
|
+
- 等待 UTC 零点重置
|
|
128
|
+
- 配额耗尽时切换账号并重新登录
|
|
129
|
+
- 需要更高额度可使用 DashScope API
|
|
130
|
+
|
|
131
|
+
## 🛠️ 开发
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# 克隆仓库
|
|
135
|
+
git clone https://github.com/1579364808/opencode-qwen-auth.git
|
|
136
|
+
cd opencode-qwencode-auth
|
|
137
|
+
|
|
138
|
+
# 安装依赖
|
|
139
|
+
bun install
|
|
140
|
+
|
|
141
|
+
# 类型检查
|
|
142
|
+
bun run typecheck
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 本地联调
|
|
146
|
+
|
|
147
|
+
编辑 `~/.opencode/package.json`:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"dependencies": {
|
|
152
|
+
"@melodyoftears/opencode-qwen-auth": "file:///absolute/path/to/opencode-qwencode-auth"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
然后重新安装:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
cd ~/.opencode && npm install
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 📄 License
|
|
164
|
+
|
|
165
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
var CREDS_PATH = join(homedir(), ".qwen", "oauth_creds.json");
|
|
9
|
+
var rl = createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout
|
|
12
|
+
});
|
|
13
|
+
function question(prompt) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
rl.question(prompt, resolve);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async function main() {
|
|
19
|
+
console.log(`
|
|
20
|
+
Qwen Auth CLI Helper
|
|
21
|
+
`);
|
|
22
|
+
console.log(`This tool helps you set up Qwen authentication manually.
|
|
23
|
+
`);
|
|
24
|
+
if (existsSync(CREDS_PATH)) {
|
|
25
|
+
const data = JSON.parse(readFileSync(CREDS_PATH, "utf-8"));
|
|
26
|
+
console.log("Existing credentials found at:", CREDS_PATH);
|
|
27
|
+
if (data.access_token) {
|
|
28
|
+
console.log("Access token: Present");
|
|
29
|
+
console.log("Email:", data.email || "Not set");
|
|
30
|
+
console.log("Updated at:", data.updated_at ? new Date(data.updated_at).toISOString() : "Unknown");
|
|
31
|
+
const overwrite = await question(`
|
|
32
|
+
Overwrite existing credentials? (y/N): `);
|
|
33
|
+
if (overwrite.toLowerCase() !== "y") {
|
|
34
|
+
console.log(`
|
|
35
|
+
Keeping existing credentials. Exiting.`);
|
|
36
|
+
rl.close();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.log(`
|
|
42
|
+
Instructions:`);
|
|
43
|
+
console.log("1. Open https://chat.qwen.ai in your browser");
|
|
44
|
+
console.log("2. Sign in with your account");
|
|
45
|
+
console.log("3. Open Developer Tools (F12) -> Network tab");
|
|
46
|
+
console.log("4. Make any chat request");
|
|
47
|
+
console.log("5. Find a request to chat.qwen.ai");
|
|
48
|
+
console.log('6. Copy the "Authorization" header value (starts with "Bearer ...")');
|
|
49
|
+
console.log("");
|
|
50
|
+
const token = await question('Paste your Bearer token (or just the token without "Bearer "): ');
|
|
51
|
+
if (!token.trim()) {
|
|
52
|
+
console.log("No token provided. Exiting.");
|
|
53
|
+
rl.close();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let accessToken = token.trim();
|
|
57
|
+
if (accessToken.toLowerCase().startsWith("bearer ")) {
|
|
58
|
+
accessToken = accessToken.slice(7);
|
|
59
|
+
}
|
|
60
|
+
const email = await question("Email (optional, press Enter to skip): ");
|
|
61
|
+
const dir = join(homedir(), ".qwen");
|
|
62
|
+
if (!existsSync(dir)) {
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
const credentials = {
|
|
66
|
+
access_token: accessToken,
|
|
67
|
+
email: email.trim() || undefined,
|
|
68
|
+
updated_at: Date.now()
|
|
69
|
+
};
|
|
70
|
+
writeFileSync(CREDS_PATH, JSON.stringify(credentials, null, 2));
|
|
71
|
+
console.log(`
|
|
72
|
+
Credentials saved to:`, CREDS_PATH);
|
|
73
|
+
console.log(`
|
|
74
|
+
You can now use OpenCode with Qwen models:`);
|
|
75
|
+
console.log(" opencode --provider qwen-code --model coder-model");
|
|
76
|
+
console.log(" opencode --provider qwen-code --model vision-model");
|
|
77
|
+
rl.close();
|
|
78
|
+
}
|
|
79
|
+
main().catch((error) => {
|
|
80
|
+
console.error("Error:", error);
|
|
81
|
+
rl.close();
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
// src/constants.ts
|
|
5
|
+
var QWEN_PROVIDER_ID = "qwen-code";
|
|
6
|
+
var QWEN_OAUTH_CONFIG = {
|
|
7
|
+
baseUrl: "https://chat.qwen.ai",
|
|
8
|
+
deviceCodeEndpoint: "https://chat.qwen.ai/api/v1/oauth2/device/code",
|
|
9
|
+
tokenEndpoint: "https://chat.qwen.ai/api/v1/oauth2/token",
|
|
10
|
+
clientId: "f0304373b74a44d2b584a3fb70ca9e56",
|
|
11
|
+
scope: "openid profile email model.completion",
|
|
12
|
+
grantType: "urn:ietf:params:oauth:grant-type:device_code"
|
|
13
|
+
};
|
|
14
|
+
var QWEN_API_CONFIG = {
|
|
15
|
+
defaultBaseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
16
|
+
portalBaseUrl: "https://portal.qwen.ai/v1",
|
|
17
|
+
chatEndpoint: "/chat/completions",
|
|
18
|
+
modelsEndpoint: "/models",
|
|
19
|
+
baseUrl: "https://portal.qwen.ai/v1"
|
|
20
|
+
};
|
|
21
|
+
var QWEN_MODELS = {
|
|
22
|
+
"coder-model": {
|
|
23
|
+
id: "coder-model",
|
|
24
|
+
name: "Qwen Coder (Qwen 3.5 Plus)",
|
|
25
|
+
contextWindow: 1048576,
|
|
26
|
+
maxOutput: 65536,
|
|
27
|
+
description: "Text-to-text coding model with 1M context and 65,536 max output",
|
|
28
|
+
reasoning: true,
|
|
29
|
+
cost: { input: 0, output: 0 }
|
|
30
|
+
},
|
|
31
|
+
"vision-model": {
|
|
32
|
+
id: "vision-model",
|
|
33
|
+
name: "Qwen VL Plus (Vision)",
|
|
34
|
+
contextWindow: 131072,
|
|
35
|
+
maxOutput: 8192,
|
|
36
|
+
description: "Text and image input model with 128K context and 8,192 max output",
|
|
37
|
+
reasoning: false,
|
|
38
|
+
cost: { input: 0, output: 0 }
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/plugin/auth.ts
|
|
43
|
+
import { homedir } from "node:os";
|
|
44
|
+
import { join } from "node:path";
|
|
45
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
46
|
+
function getCredentialsPath() {
|
|
47
|
+
const homeDir = homedir();
|
|
48
|
+
return join(homeDir, ".qwen", "oauth_creds.json");
|
|
49
|
+
}
|
|
50
|
+
function saveCredentials(credentials) {
|
|
51
|
+
const credPath = getCredentialsPath();
|
|
52
|
+
const dir = join(homedir(), ".qwen");
|
|
53
|
+
if (!existsSync(dir)) {
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
const data = {
|
|
57
|
+
access_token: credentials.accessToken,
|
|
58
|
+
token_type: credentials.tokenType || "Bearer",
|
|
59
|
+
refresh_token: credentials.refreshToken,
|
|
60
|
+
resource_url: credentials.resourceUrl,
|
|
61
|
+
expiry_date: credentials.expiryDate,
|
|
62
|
+
scope: credentials.scope
|
|
63
|
+
};
|
|
64
|
+
writeFileSync(credPath, JSON.stringify(data, null, 2));
|
|
65
|
+
}
|
|
66
|
+
function loadCredentials() {
|
|
67
|
+
const credPath = getCredentialsPath();
|
|
68
|
+
if (!existsSync(credPath)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const raw = JSON.parse(readFileSync(credPath, "utf-8"));
|
|
73
|
+
const accessToken = raw.access_token || raw.accessToken;
|
|
74
|
+
if (!accessToken || typeof accessToken !== "string") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
accessToken,
|
|
79
|
+
tokenType: raw.token_type || raw.tokenType || "Bearer",
|
|
80
|
+
refreshToken: raw.refresh_token || raw.refreshToken,
|
|
81
|
+
resourceUrl: raw.resource_url || raw.resourceUrl,
|
|
82
|
+
expiryDate: raw.expiry_date || raw.expiryDate,
|
|
83
|
+
scope: raw.scope
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/qwen/oauth.ts
|
|
91
|
+
import { randomBytes, createHash, randomUUID } from "node:crypto";
|
|
92
|
+
|
|
93
|
+
// src/errors.ts
|
|
94
|
+
var REAUTH_HINT = 'Execute "opencode auth login" e selecione "Qwen Code (qwen.ai OAuth)" para autenticar.';
|
|
95
|
+
var AUTH_MESSAGES = {
|
|
96
|
+
token_expired: `[Qwen] Token expirado. ${REAUTH_HINT}`,
|
|
97
|
+
refresh_failed: `[Qwen] Falha ao renovar token. ${REAUTH_HINT}`,
|
|
98
|
+
auth_required: `[Qwen] Autenticacao necessaria. ${REAUTH_HINT}`
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
class QwenAuthError extends Error {
|
|
102
|
+
kind;
|
|
103
|
+
technicalDetail;
|
|
104
|
+
constructor(kind, technicalDetail) {
|
|
105
|
+
super(AUTH_MESSAGES[kind]);
|
|
106
|
+
this.name = "QwenAuthError";
|
|
107
|
+
this.kind = kind;
|
|
108
|
+
this.technicalDetail = technicalDetail;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function logTechnicalDetail(detail) {
|
|
112
|
+
if (process.env.OPENCODE_QWEN_DEBUG === "1") {
|
|
113
|
+
console.debug("[Qwen Debug]", detail);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/qwen/oauth.ts
|
|
118
|
+
class SlowDownError extends Error {
|
|
119
|
+
constructor() {
|
|
120
|
+
super("slow_down: server requested increased polling interval");
|
|
121
|
+
this.name = "SlowDownError";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function generatePKCE() {
|
|
125
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
126
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
127
|
+
return { verifier, challenge };
|
|
128
|
+
}
|
|
129
|
+
function objectToUrlEncoded(data) {
|
|
130
|
+
return Object.keys(data).map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`).join("&");
|
|
131
|
+
}
|
|
132
|
+
async function requestDeviceAuthorization(codeChallenge) {
|
|
133
|
+
const bodyData = {
|
|
134
|
+
client_id: QWEN_OAUTH_CONFIG.clientId,
|
|
135
|
+
scope: QWEN_OAUTH_CONFIG.scope,
|
|
136
|
+
code_challenge: codeChallenge,
|
|
137
|
+
code_challenge_method: "S256"
|
|
138
|
+
};
|
|
139
|
+
const response = await fetch(QWEN_OAUTH_CONFIG.deviceCodeEndpoint, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
143
|
+
Accept: "application/json",
|
|
144
|
+
"x-request-id": randomUUID()
|
|
145
|
+
},
|
|
146
|
+
body: objectToUrlEncoded(bodyData)
|
|
147
|
+
});
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
const errorData = await response.text();
|
|
150
|
+
logTechnicalDetail(`Device auth HTTP ${response.status}: ${errorData}`);
|
|
151
|
+
throw new QwenAuthError("auth_required", `HTTP ${response.status}: ${errorData}`);
|
|
152
|
+
}
|
|
153
|
+
const result = await response.json();
|
|
154
|
+
if (!result.device_code || !result.user_code) {
|
|
155
|
+
throw new Error("Invalid device authorization response");
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
async function pollDeviceToken(deviceCode, codeVerifier) {
|
|
160
|
+
const bodyData = {
|
|
161
|
+
grant_type: QWEN_OAUTH_CONFIG.grantType,
|
|
162
|
+
client_id: QWEN_OAUTH_CONFIG.clientId,
|
|
163
|
+
device_code: deviceCode,
|
|
164
|
+
code_verifier: codeVerifier
|
|
165
|
+
};
|
|
166
|
+
const response = await fetch(QWEN_OAUTH_CONFIG.tokenEndpoint, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: {
|
|
169
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
170
|
+
Accept: "application/json"
|
|
171
|
+
},
|
|
172
|
+
body: objectToUrlEncoded(bodyData)
|
|
173
|
+
});
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
const responseText = await response.text();
|
|
176
|
+
try {
|
|
177
|
+
const errorData = JSON.parse(responseText);
|
|
178
|
+
if (response.status === 400 && errorData.error === "authorization_pending") {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
if (response.status === 429 && errorData.error === "slow_down") {
|
|
182
|
+
throw new SlowDownError;
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`Token poll failed: ${errorData.error || "Unknown error"} - ${errorData.error_description || responseText}`);
|
|
185
|
+
} catch (parseError) {
|
|
186
|
+
if (parseError instanceof SyntaxError) {
|
|
187
|
+
throw new Error(`Token poll failed: ${response.status} ${response.statusText}. Response: ${responseText}`);
|
|
188
|
+
}
|
|
189
|
+
throw parseError;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return await response.json();
|
|
193
|
+
}
|
|
194
|
+
function tokenResponseToCredentials(tokenResponse) {
|
|
195
|
+
return {
|
|
196
|
+
accessToken: tokenResponse.access_token,
|
|
197
|
+
tokenType: tokenResponse.token_type || "Bearer",
|
|
198
|
+
refreshToken: tokenResponse.refresh_token,
|
|
199
|
+
resourceUrl: tokenResponse.resource_url,
|
|
200
|
+
expiryDate: Date.now() + tokenResponse.expires_in * 1000,
|
|
201
|
+
scope: tokenResponse.scope
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function refreshAccessToken(refreshToken) {
|
|
205
|
+
const bodyData = {
|
|
206
|
+
grant_type: "refresh_token",
|
|
207
|
+
refresh_token: refreshToken,
|
|
208
|
+
client_id: QWEN_OAUTH_CONFIG.clientId
|
|
209
|
+
};
|
|
210
|
+
const response = await fetch(QWEN_OAUTH_CONFIG.tokenEndpoint, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: {
|
|
213
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
214
|
+
Accept: "application/json"
|
|
215
|
+
},
|
|
216
|
+
body: objectToUrlEncoded(bodyData)
|
|
217
|
+
});
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
const errorText = await response.text();
|
|
220
|
+
logTechnicalDetail(`Token refresh HTTP ${response.status}: ${errorText}`);
|
|
221
|
+
throw new QwenAuthError("refresh_failed", `HTTP ${response.status}: ${errorText}`);
|
|
222
|
+
}
|
|
223
|
+
const data = await response.json();
|
|
224
|
+
return {
|
|
225
|
+
accessToken: data.access_token,
|
|
226
|
+
tokenType: data.token_type || "Bearer",
|
|
227
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
228
|
+
resourceUrl: data.resource_url,
|
|
229
|
+
expiryDate: Date.now() + data.expires_in * 1000,
|
|
230
|
+
scope: data.scope
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/index.ts
|
|
235
|
+
function openBrowser(url) {
|
|
236
|
+
try {
|
|
237
|
+
const platform = process.platform;
|
|
238
|
+
const command = platform === "darwin" ? "open" : platform === "win32" ? "rundll32" : "xdg-open";
|
|
239
|
+
const args = platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
240
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
241
|
+
child.unref?.();
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
function normalizeVerificationUrl(candidate) {
|
|
245
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
246
|
+
try {
|
|
247
|
+
const parsed = new URL(candidate);
|
|
248
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
249
|
+
return parsed.toString();
|
|
250
|
+
}
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
return "https://chat.qwen.ai";
|
|
254
|
+
}
|
|
255
|
+
async function getValidAccessToken(getAuth) {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const localCreds = loadCredentials();
|
|
258
|
+
if (localCreds?.accessToken) {
|
|
259
|
+
const isExpired = typeof localCreds.expiryDate === "number" && now > localCreds.expiryDate - 60000;
|
|
260
|
+
if (!isExpired) {
|
|
261
|
+
return localCreds.accessToken;
|
|
262
|
+
}
|
|
263
|
+
if (localCreds.refreshToken) {
|
|
264
|
+
try {
|
|
265
|
+
const refreshed = await refreshAccessToken(localCreds.refreshToken);
|
|
266
|
+
saveCredentials(refreshed);
|
|
267
|
+
return refreshed.accessToken;
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const auth = await getAuth();
|
|
272
|
+
if (!auth || auth.type !== "oauth") {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
let accessToken = auth.access;
|
|
276
|
+
if (accessToken && auth.expires && now > auth.expires - 60000 && auth.refresh) {
|
|
277
|
+
try {
|
|
278
|
+
const refreshed = await refreshAccessToken(auth.refresh);
|
|
279
|
+
accessToken = refreshed.accessToken;
|
|
280
|
+
saveCredentials(refreshed);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
283
|
+
logTechnicalDetail(`Token refresh falhou: ${detail}`);
|
|
284
|
+
accessToken = undefined;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return accessToken ?? null;
|
|
288
|
+
}
|
|
289
|
+
var QwenAuthPlugin = async (_input) => {
|
|
290
|
+
return {
|
|
291
|
+
auth: {
|
|
292
|
+
provider: QWEN_PROVIDER_ID,
|
|
293
|
+
loader: async (getAuth, provider) => {
|
|
294
|
+
if (provider?.models) {
|
|
295
|
+
for (const model of Object.values(provider.models)) {
|
|
296
|
+
if (model)
|
|
297
|
+
model.cost = { input: 0, output: 0 };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const accessToken = await getValidAccessToken(getAuth);
|
|
301
|
+
if (!accessToken)
|
|
302
|
+
return null;
|
|
303
|
+
return {
|
|
304
|
+
apiKey: accessToken,
|
|
305
|
+
baseURL: QWEN_API_CONFIG.baseUrl
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
methods: [
|
|
309
|
+
{
|
|
310
|
+
type: "oauth",
|
|
311
|
+
label: "Qwen Code (qwen.ai OAuth)",
|
|
312
|
+
authorize: async () => {
|
|
313
|
+
const { verifier, challenge } = generatePKCE();
|
|
314
|
+
try {
|
|
315
|
+
const deviceAuth = await requestDeviceAuthorization(challenge);
|
|
316
|
+
const verificationUrl = normalizeVerificationUrl(deviceAuth.verification_uri_complete || deviceAuth.verification_uri);
|
|
317
|
+
openBrowser(verificationUrl);
|
|
318
|
+
const POLLING_MARGIN_MS = 3000;
|
|
319
|
+
return {
|
|
320
|
+
url: verificationUrl,
|
|
321
|
+
instructions: `Codigo: ${deviceAuth.user_code}`,
|
|
322
|
+
method: "auto",
|
|
323
|
+
callback: async () => {
|
|
324
|
+
const startTime = Date.now();
|
|
325
|
+
const timeoutMs = deviceAuth.expires_in * 1000;
|
|
326
|
+
let interval = 5000;
|
|
327
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, interval + POLLING_MARGIN_MS));
|
|
329
|
+
try {
|
|
330
|
+
const tokenResponse = await pollDeviceToken(deviceAuth.device_code, verifier);
|
|
331
|
+
if (tokenResponse) {
|
|
332
|
+
const credentials = tokenResponseToCredentials(tokenResponse);
|
|
333
|
+
saveCredentials(credentials);
|
|
334
|
+
return {
|
|
335
|
+
type: "success",
|
|
336
|
+
access: credentials.accessToken,
|
|
337
|
+
refresh: credentials.refreshToken ?? "",
|
|
338
|
+
expires: credentials.expiryDate || Date.now() + 3600000
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
} catch (e) {
|
|
342
|
+
if (e instanceof SlowDownError) {
|
|
343
|
+
interval = Math.min(interval + 5000, 15000);
|
|
344
|
+
} else if (!(e instanceof Error) || !e.message.includes("authorization_pending")) {
|
|
345
|
+
return { type: "failed" };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return { type: "failed" };
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
} catch (e) {
|
|
353
|
+
const msg = e instanceof Error ? e.message : "Erro desconhecido";
|
|
354
|
+
return {
|
|
355
|
+
url: "https://chat.qwen.ai",
|
|
356
|
+
instructions: `Erro: ${msg}`,
|
|
357
|
+
method: "auto",
|
|
358
|
+
callback: async () => ({ type: "failed" })
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
},
|
|
365
|
+
config: async (config) => {
|
|
366
|
+
const providers = config.provider || {};
|
|
367
|
+
providers[QWEN_PROVIDER_ID] = {
|
|
368
|
+
npm: "@ai-sdk/openai-compatible",
|
|
369
|
+
name: "Qwen Code",
|
|
370
|
+
options: { baseURL: QWEN_API_CONFIG.baseUrl },
|
|
371
|
+
models: Object.fromEntries(Object.entries(QWEN_MODELS).map(([id, m]) => [
|
|
372
|
+
id,
|
|
373
|
+
{
|
|
374
|
+
id: m.id,
|
|
375
|
+
name: m.name,
|
|
376
|
+
reasoning: m.reasoning,
|
|
377
|
+
limit: { context: m.contextWindow, output: m.maxOutput },
|
|
378
|
+
cost: m.cost,
|
|
379
|
+
modalities: {
|
|
380
|
+
input: id === "vision-model" ? ["text", "image"] : ["text"],
|
|
381
|
+
output: ["text"]
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
]))
|
|
385
|
+
};
|
|
386
|
+
config.provider = providers;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
var src_default = QwenAuthPlugin;
|
|
391
|
+
export {
|
|
392
|
+
src_default as default,
|
|
393
|
+
QwenAuthPlugin
|
|
394
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@melodyoftears/opencode-qwen-auth",
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Qwen OAuth authentication plugin for OpenCode - Access Qwen AI models (Coder, Vision) with your qwen.ai account",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node --format esm && bun build ./src/cli.ts --outdir ./dist --target node --format esm",
|
|
10
|
+
"dev": "bun run --watch src/index.ts",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"opencode",
|
|
15
|
+
"qwen",
|
|
16
|
+
"qwen-code",
|
|
17
|
+
"coder-model",
|
|
18
|
+
"vision-model",
|
|
19
|
+
"oauth",
|
|
20
|
+
"authentication",
|
|
21
|
+
"ai",
|
|
22
|
+
"llm",
|
|
23
|
+
"opencode-plugins"
|
|
24
|
+
],
|
|
25
|
+
"author": "melodyoftears",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/1579364808/opencode-qwen-auth.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/1579364808/opencode-qwen-auth#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/1579364808/opencode-qwen-auth/issues"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@opencode-ai/plugin": "^1.1.48",
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"bun-types": "^1.1.0",
|
|
39
|
+
"typescript": "^5.6.0"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md",
|
|
44
|
+
"README.zh-CN.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=20.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|