seeyonjf-ai-tool 0.1.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/README.md +107 -0
- package/docs/chat-completions-gateway-config.md +299 -0
- package/docs/gateway-config.html +487 -0
- package/package.json +26 -0
- package/tools/configure-chat-gateway.mjs +470 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# seeyonjf-ai-tool
|
|
2
|
+
|
|
3
|
+
`seeyonjf-ai-tool` 是一个用于配置 AI 网关的 Node.js CLI。目前聚焦 `chat/completions` 模式,支持自动配置 `opencode` 的用户级配置文件。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- 配置 `opencode` 使用 `seeyonjf-ai` 网关。
|
|
8
|
+
- `Codex` 相关入口暂保留为提示项,待协议支持建设完成后再开放配置。
|
|
9
|
+
- 写入前自动备份原配置文件。
|
|
10
|
+
- 支持交互式恢复备份。
|
|
11
|
+
- 配置前检查 `opencode` 和 `codex` 命令是否已在 `PATH` 中。
|
|
12
|
+
- 网关基础地址由用户运行时输入,不在包内内置固定地址。
|
|
13
|
+
- 纯 Node.js 标准库实现,无需安装第三方依赖。
|
|
14
|
+
- 支持 Windows、macOS、Linux。
|
|
15
|
+
|
|
16
|
+
## 环境要求
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
Node.js 18+
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 快速使用
|
|
23
|
+
|
|
24
|
+
交互式配置会先检查 `opencode` 和 `codex` 是否已安装,然后引导你选择配置对象。当前仅 `opencode` 会继续输入 chat/completions 网关基础地址和 token,并自动写入模型配置;`Codex` 相关选项会提示待协议支持建设中,不会修改配置。
|
|
25
|
+
|
|
26
|
+
不安装,直接运行:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx seeyonjf-ai-tool@latest configure
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
全局安装后运行:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g seeyonjf-ai-tool
|
|
36
|
+
seeyonjf-ai-tool configure
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
仅配置 `opencode`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx seeyonjf-ai-tool@latest configure --tool opencode --base-url "你的网关基础地址"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`Codex` 暂不可配置:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx seeyonjf-ai-tool@latest configure --tool codex --base-url "你的网关基础地址"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
该命令只会提示待协议支持建设中,不会写入配置。
|
|
52
|
+
|
|
53
|
+
恢复备份:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx seeyonjf-ai-tool@latest restore
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
列出备份:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx seeyonjf-ai-tool@latest list-backups
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
查看帮助:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx seeyonjf-ai-tool@latest --help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 文档
|
|
72
|
+
|
|
73
|
+
- `docs/chat-completions-gateway-config.md`:手动配置和自动配置工具说明。
|
|
74
|
+
- `docs/gateway-config.html`:面向用户的静态配置指南页面。
|
|
75
|
+
|
|
76
|
+
## 网关地址
|
|
77
|
+
|
|
78
|
+
运行配置时需要输入你的 chat/completions 网关基础地址。基础地址不要追加 `/chat/completions`,客户端会自动拼接实际请求路径。
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
https://你的网关域名/v1/你的路径
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
支持模型:
|
|
85
|
+
|
|
86
|
+
- `gpt-5.5`
|
|
87
|
+
- `qwen3.7-max`
|
|
88
|
+
- `deepseek-v4-pro`
|
|
89
|
+
|
|
90
|
+
## 开发验证
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm run check
|
|
94
|
+
npm run help
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 发布
|
|
98
|
+
|
|
99
|
+
发布需要先设置带 publish 权限且可 bypass 2FA 的 npm token:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
export NPM_TOKEN="你的 npm token"
|
|
103
|
+
npm run publish:npm:dry-run
|
|
104
|
+
npm run publish:npm
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
发布脚本会临时生成 `.npmrc` 使用 `NPM_TOKEN`,结束后自动删除,不会把 token 写入项目文件。
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# chat/completions 网关配置说明
|
|
2
|
+
|
|
3
|
+
本文说明如何在 `opencode` 和 `Codex` 中使用 AI 网关的 `chat/completions` 模式,并提供自动配置工具用于写入用户级配置文件。
|
|
4
|
+
|
|
5
|
+
## 1. 网关信息
|
|
6
|
+
|
|
7
|
+
chat/completions 基础地址:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
https://你的网关域名/v1/你的路径
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
实际请求路径通常由客户端自动拼接为:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
https://你的网关域名/v1/你的路径/chat/completions
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
认证方式:
|
|
20
|
+
|
|
21
|
+
```http
|
|
22
|
+
Authorization: Bearer <你的 chat token>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
支持模型:
|
|
26
|
+
|
|
27
|
+
| 模型 ID | 文本 | 流式 | 工具调用 | 图片 | 文件块 |
|
|
28
|
+
|---|---|---|---|---|---|
|
|
29
|
+
| `gpt-5.5` | 支持 | 支持 | 支持,含强制工具调用 | 支持 | 不建议,实测未读取到附件 |
|
|
30
|
+
| `qwen3.7-max` | 支持 | 支持 | 自动工具调用支持,强制 `tool_choice` object 不支持 | 不支持 | 不支持 |
|
|
31
|
+
| `deepseek-v4-pro` | 支持 | 支持 | 支持,含强制工具调用 | 不建议,实测模型表现为未真正看到图片 | 不建议,实测未读取到附件 |
|
|
32
|
+
|
|
33
|
+
使用建议:
|
|
34
|
+
|
|
35
|
+
| 场景 | 推荐模型 |
|
|
36
|
+
|---|---|
|
|
37
|
+
| 默认主模型 | `gpt-5.5` |
|
|
38
|
+
| opencode 小模型、标题、轻量任务 | `qwen3.7-max` |
|
|
39
|
+
| DeepSeek 风格推理任务 | `deepseek-v4-pro` |
|
|
40
|
+
| 图片输入 | 只建议 `gpt-5.5` |
|
|
41
|
+
| 文件处理 | 让工具读取文件内容后作为文本发送,不依赖 chat file block |
|
|
42
|
+
|
|
43
|
+
## 2. opencode 手动配置
|
|
44
|
+
|
|
45
|
+
opencode 用户级配置文件位置:
|
|
46
|
+
|
|
47
|
+
| 系统 | 路径 |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Windows | `%USERPROFILE%\.config\opencode\opencode.json` |
|
|
50
|
+
| macOS/Linux | `~/.config/opencode/opencode.json` |
|
|
51
|
+
|
|
52
|
+
参考配置:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"$schema": "https://opencode.ai/config.json",
|
|
57
|
+
"model": "seeyonjf-ai/gpt-5.5",
|
|
58
|
+
"small_model": "seeyonjf-ai/qwen3.7-max",
|
|
59
|
+
"provider": {
|
|
60
|
+
"seeyonjf-ai": {
|
|
61
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
62
|
+
"name": "SeeyonJF-AI",
|
|
63
|
+
"options": {
|
|
64
|
+
"baseURL": "https://你的网关域名/v1/你的路径",
|
|
65
|
+
"apiKey": "替换为你的 chat token",
|
|
66
|
+
"timeout": false
|
|
67
|
+
},
|
|
68
|
+
"models": {
|
|
69
|
+
"gpt-5.5": {
|
|
70
|
+
"name": "gpt-5.5",
|
|
71
|
+
"tool_call": true,
|
|
72
|
+
"reasoning": true,
|
|
73
|
+
"attachment": true,
|
|
74
|
+
"modalities": {
|
|
75
|
+
"input": ["text", "image"],
|
|
76
|
+
"output": ["text"]
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"qwen3.7-max": {
|
|
80
|
+
"name": "qwen3.7-max",
|
|
81
|
+
"tool_call": true,
|
|
82
|
+
"reasoning": true,
|
|
83
|
+
"interleaved": {
|
|
84
|
+
"field": "reasoning_content"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"deepseek-v4-pro": {
|
|
88
|
+
"name": "deepseek-v4-pro",
|
|
89
|
+
"tool_call": true,
|
|
90
|
+
"reasoning": true,
|
|
91
|
+
"interleaved": {
|
|
92
|
+
"field": "reasoning_content"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
关键配置项说明:
|
|
102
|
+
|
|
103
|
+
| 配置项 | 说明 |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `model` | opencode 默认模型,格式必须是 `provider/model-id` |
|
|
106
|
+
| `small_model` | opencode 用于标题、摘要等轻量任务的小模型 |
|
|
107
|
+
| `provider.seeyonjf-ai.npm` | 使用 OpenAI-compatible provider |
|
|
108
|
+
| `provider.seeyonjf-ai.options.baseURL` | 填基础地址,不要手动追加 `/chat/completions` |
|
|
109
|
+
| `provider.seeyonjf-ai.options.apiKey` | chat token |
|
|
110
|
+
| `provider.seeyonjf-ai.options.timeout` | `false` 表示不限制完整请求超时,适合长任务 |
|
|
111
|
+
| `models.gpt-5.5.modalities.input` | 标记 `gpt-5.5` 支持文本和图片输入 |
|
|
112
|
+
| `interleaved.field` | 告诉客户端推理内容字段是 `reasoning_content` |
|
|
113
|
+
|
|
114
|
+
注意事项:
|
|
115
|
+
|
|
116
|
+
| 项目 | 说明 |
|
|
117
|
+
|---|---|
|
|
118
|
+
| 配置生效 | 修改 `opencode.json` 后需要重启 opencode |
|
|
119
|
+
| 图片 | 只建议使用 `gpt-5.5` |
|
|
120
|
+
| 文件 | 不建议依赖模型附件能力,优先让 opencode 读取文件后作为文本上下文发送 |
|
|
121
|
+
| token 安全 | 不建议把带 token 的配置文件提交到 Git 仓库 |
|
|
122
|
+
|
|
123
|
+
## 3. Codex 手动配置
|
|
124
|
+
|
|
125
|
+
Codex 用户级配置文件位置:
|
|
126
|
+
|
|
127
|
+
| 系统 | 路径 |
|
|
128
|
+
|---|---|
|
|
129
|
+
| Windows | `%USERPROFILE%\.codex\config.toml` |
|
|
130
|
+
| macOS/Linux | `~/.codex/config.toml` |
|
|
131
|
+
|
|
132
|
+
chat/completions 模式参考配置:
|
|
133
|
+
|
|
134
|
+
```toml
|
|
135
|
+
model = "gpt-5.5"
|
|
136
|
+
model_provider = "seeyonjf-ai"
|
|
137
|
+
|
|
138
|
+
[model_providers.seeyonjf-ai]
|
|
139
|
+
name = "SeeyonJF-AI"
|
|
140
|
+
base_url = "https://你的网关域名/v1/你的路径"
|
|
141
|
+
wire_api = "chat"
|
|
142
|
+
experimental_bearer_token = "替换为你的 chat token"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
关键配置项说明:
|
|
146
|
+
|
|
147
|
+
| 配置项 | 说明 |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `model` | Codex 默认模型 ID,使用网关支持的模型 ID |
|
|
150
|
+
| `model_provider` | 指向下方 `[model_providers.<name>]` 的 provider 名称 |
|
|
151
|
+
| `base_url` | 填基础地址,不要追加 `/chat/completions` |
|
|
152
|
+
| `wire_api` | 这里必须使用 `chat`,表示按 Chat Completions 协议请求 |
|
|
153
|
+
| `experimental_bearer_token` | 用 Bearer token 认证,等价于 `Authorization: Bearer <token>` |
|
|
154
|
+
|
|
155
|
+
Codex 下的模型建议:
|
|
156
|
+
|
|
157
|
+
| 模型 | 建议 |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `gpt-5.5` | chat 模式下最稳,推荐默认使用 |
|
|
160
|
+
| `deepseek-v4-pro` | 可作为备用推理模型,但不建议处理图片/附件 |
|
|
161
|
+
| `qwen3.7-max` | 可做普通问答或轻量任务,不建议作为 Codex 主力 Agent 模型 |
|
|
162
|
+
|
|
163
|
+
如果希望把 token 放到环境变量中,需要确认本机 Codex 版本支持 `env_key`。支持时可改成:
|
|
164
|
+
|
|
165
|
+
```toml
|
|
166
|
+
model = "gpt-5.5"
|
|
167
|
+
model_provider = "seeyonjf-ai"
|
|
168
|
+
|
|
169
|
+
[model_providers.seeyonjf-ai]
|
|
170
|
+
name = "SeeyonJF-AI"
|
|
171
|
+
base_url = "https://你的网关域名/v1/你的路径"
|
|
172
|
+
wire_api = "chat"
|
|
173
|
+
env_key = "SEEYONJF_AI_CHAT_TOKEN"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
启动前设置环境变量:
|
|
177
|
+
|
|
178
|
+
Windows PowerShell:
|
|
179
|
+
|
|
180
|
+
```powershell
|
|
181
|
+
$env:SEEYONJF_AI_CHAT_TOKEN = "你的 chat token"
|
|
182
|
+
codex
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
macOS/Linux:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
export SEEYONJF_AI_CHAT_TOKEN="你的 chat token"
|
|
189
|
+
codex
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 4. 自动配置工具
|
|
193
|
+
|
|
194
|
+
包名:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
seeyonjf-ai-tool
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
该工具会完成:
|
|
201
|
+
|
|
202
|
+
| 功能 | 说明 |
|
|
203
|
+
|---|---|
|
|
204
|
+
| 环境检查 | 配置前检查 `opencode` 和 `codex` 命令是否已在 `PATH` 中 |
|
|
205
|
+
| 选择工具 | 当前支持配置 `opencode`;`codex` 和同时配置两者会提示待协议支持建设中 |
|
|
206
|
+
| 输入网关 | 交互输入或通过 `--base-url` 传入 chat/completions 网关基础地址 |
|
|
207
|
+
| 输入 token | 交互输入或通过 `--key` 传入网关 token |
|
|
208
|
+
| 写入配置 | 自动写入用户级配置文件 |
|
|
209
|
+
| 自动备份 | 写入前备份源配置文件,备份名为 `.bak-时间戳` |
|
|
210
|
+
| 恢复配置 | 支持通过 `restore` 子命令交互恢复备份 |
|
|
211
|
+
| 跨平台 | 使用 Node.js 标准库实现,支持 Windows、macOS、Linux |
|
|
212
|
+
|
|
213
|
+
运行要求:
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
Node.js 18+
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
交互式运行:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
npx seeyonjf-ai-tool@latest configure
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
全局安装后运行:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npm install -g seeyonjf-ai-tool
|
|
229
|
+
seeyonjf-ai-tool configure
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
非交互式配置 opencode:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npx seeyonjf-ai-tool@latest configure --tool opencode --base-url "https://你的网关域名/v1/你的路径"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Codex 暂不可配置:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
npx seeyonjf-ai-tool@latest configure --tool codex --base-url "https://你的网关域名/v1/你的路径"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
该命令只会提示待协议支持建设中,不会写入配置。
|
|
245
|
+
|
|
246
|
+
同时配置两者暂不可用:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
npx seeyonjf-ai-tool@latest configure --tool both --base-url "https://你的网关域名/v1/你的路径"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
该命令包含 Codex,也只会提示待协议支持建设中,不会写入配置。
|
|
253
|
+
|
|
254
|
+
指定默认模型:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
npx seeyonjf-ai-tool@latest configure --tool opencode --base-url "https://你的网关域名/v1/你的路径" --model gpt-5.5 --small-model qwen3.7-max
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
查看帮助:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
npx seeyonjf-ai-tool@latest --help
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
恢复备份:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
npx seeyonjf-ai-tool@latest restore
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
列出备份:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
npx seeyonjf-ai-tool@latest list-backups
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
手动恢复方式:
|
|
279
|
+
|
|
280
|
+
| 工具 | 原配置 | 备份文件格式 |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| opencode | `~/.config/opencode/opencode.json` | `opencode.json.bak-YYYYMMDD-HHMMSS` |
|
|
283
|
+
| Codex | `~/.codex/config.toml` | `config.toml.bak-YYYYMMDD-HHMMSS` |
|
|
284
|
+
|
|
285
|
+
把对应备份文件复制回原配置路径即可恢复。
|
|
286
|
+
|
|
287
|
+
## 5. 兼容性建议
|
|
288
|
+
|
|
289
|
+
推荐把工具设计为跨平台 Node.js CLI,而不是只支持 Windows。
|
|
290
|
+
|
|
291
|
+
原因:
|
|
292
|
+
|
|
293
|
+
| 方案 | 评价 |
|
|
294
|
+
|---|---|
|
|
295
|
+
| 只做 Windows `.bat`/PowerShell | Windows 用户方便,但 macOS/Linux 不能复用 |
|
|
296
|
+
| 做 Node.js CLI | Windows、macOS、Linux 都可执行,维护成本低 |
|
|
297
|
+
| 打包成 exe | 对非技术用户友好,但发布和杀软误报处理成本更高 |
|
|
298
|
+
|
|
299
|
+
当前脚本是纯 Node.js 标准库实现,不依赖 npm install,适合先作为内部配置工具使用。后续如需面向普通用户分发,可以再增加 npm 发布配置或使用 `pkg`/`nexe` 打包。
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>SeeyonJF-AI 网关配置指南</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f7f3ea;
|
|
10
|
+
--panel: #fffaf1;
|
|
11
|
+
--panel-strong: #f0e6d6;
|
|
12
|
+
--text: #221f1a;
|
|
13
|
+
--muted: #6f6659;
|
|
14
|
+
--line: #ded2c1;
|
|
15
|
+
--accent: #c96442;
|
|
16
|
+
--accent-dark: #9c4329;
|
|
17
|
+
--ok: #367a55;
|
|
18
|
+
--warn: #9b6a1d;
|
|
19
|
+
--code: #2d2923;
|
|
20
|
+
--code-bg: #eee5d7;
|
|
21
|
+
--shadow: 0 24px 80px rgba(59, 42, 24, 0.12);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
* { box-sizing: border-box; }
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
margin: 0;
|
|
28
|
+
color: var(--text);
|
|
29
|
+
background:
|
|
30
|
+
radial-gradient(circle at 15% 0%, rgba(201, 100, 66, 0.16), transparent 34rem),
|
|
31
|
+
radial-gradient(circle at 90% 10%, rgba(95, 72, 46, 0.10), transparent 28rem),
|
|
32
|
+
var(--bg);
|
|
33
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
34
|
+
line-height: 1.65;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.page {
|
|
38
|
+
width: min(1120px, calc(100% - 40px));
|
|
39
|
+
margin: 0 auto;
|
|
40
|
+
padding: 48px 0 72px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.hero {
|
|
44
|
+
margin-bottom: 28px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.hero-card, .card {
|
|
48
|
+
background: rgba(255, 250, 241, 0.86);
|
|
49
|
+
border: 1px solid rgba(222, 210, 193, 0.9);
|
|
50
|
+
border-radius: 24px;
|
|
51
|
+
box-shadow: var(--shadow);
|
|
52
|
+
backdrop-filter: blur(16px);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.hero-card { padding: 36px; }
|
|
56
|
+
.card { padding: 28px; margin-top: 22px; }
|
|
57
|
+
|
|
58
|
+
.eyebrow {
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 8px;
|
|
62
|
+
margin-bottom: 16px;
|
|
63
|
+
padding: 6px 10px;
|
|
64
|
+
border: 1px solid var(--line);
|
|
65
|
+
border-radius: 999px;
|
|
66
|
+
color: var(--accent-dark);
|
|
67
|
+
background: rgba(240, 230, 214, 0.74);
|
|
68
|
+
font-size: 13px;
|
|
69
|
+
font-weight: 650;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
h1, h2, h3 { line-height: 1.16; margin: 0; letter-spacing: -0.03em; }
|
|
73
|
+
h1 { max-width: 720px; font-size: clamp(38px, 6vw, 72px); }
|
|
74
|
+
h2 { font-size: clamp(26px, 3vw, 38px); }
|
|
75
|
+
h3 { font-size: 19px; margin-bottom: 10px; }
|
|
76
|
+
|
|
77
|
+
.lead {
|
|
78
|
+
max-width: 920px;
|
|
79
|
+
margin: 20px 0 0;
|
|
80
|
+
color: var(--muted);
|
|
81
|
+
font-size: 18px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.aside {
|
|
85
|
+
padding: 26px;
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-direction: column;
|
|
88
|
+
justify-content: space-between;
|
|
89
|
+
gap: 18px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.meta-grid {
|
|
93
|
+
display: grid;
|
|
94
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
95
|
+
gap: 12px;
|
|
96
|
+
margin-top: 26px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.meta {
|
|
100
|
+
padding: 14px;
|
|
101
|
+
border: 1px solid var(--line);
|
|
102
|
+
border-radius: 16px;
|
|
103
|
+
background: rgba(255, 255, 255, 0.34);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.meta span, .label {
|
|
107
|
+
display: block;
|
|
108
|
+
margin-bottom: 5px;
|
|
109
|
+
color: var(--muted);
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
font-weight: 700;
|
|
112
|
+
letter-spacing: 0.05em;
|
|
113
|
+
text-transform: uppercase;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.meta strong { font-size: 15px; }
|
|
117
|
+
|
|
118
|
+
.notice {
|
|
119
|
+
padding: 16px 18px;
|
|
120
|
+
border: 1px solid rgba(155, 106, 29, 0.35);
|
|
121
|
+
border-radius: 18px;
|
|
122
|
+
background: rgba(255, 245, 218, 0.78);
|
|
123
|
+
color: #5d4218;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.notice strong { color: #442f0f; }
|
|
127
|
+
|
|
128
|
+
.gateway-box {
|
|
129
|
+
display: grid;
|
|
130
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
131
|
+
gap: 12px;
|
|
132
|
+
align-items: center;
|
|
133
|
+
margin-top: 16px;
|
|
134
|
+
padding: 14px;
|
|
135
|
+
border: 1px solid var(--line);
|
|
136
|
+
border-radius: 18px;
|
|
137
|
+
background: #fffdf7;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
code, pre {
|
|
141
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
code {
|
|
145
|
+
color: var(--code);
|
|
146
|
+
background: var(--code-bg);
|
|
147
|
+
border-radius: 7px;
|
|
148
|
+
padding: 2px 6px;
|
|
149
|
+
font-size: 0.92em;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pre {
|
|
153
|
+
overflow-x: auto;
|
|
154
|
+
margin: 14px 0 0;
|
|
155
|
+
padding: 18px;
|
|
156
|
+
color: #f7efe2;
|
|
157
|
+
background: #2f2921;
|
|
158
|
+
border-radius: 18px;
|
|
159
|
+
font-size: 13px;
|
|
160
|
+
line-height: 1.55;
|
|
161
|
+
white-space: pre;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.copy-row {
|
|
165
|
+
position: relative;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.copy-row .copy {
|
|
169
|
+
position: absolute;
|
|
170
|
+
top: 12px;
|
|
171
|
+
right: 12px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
button.copy {
|
|
175
|
+
border: 1px solid rgba(156, 67, 41, 0.25);
|
|
176
|
+
border-radius: 999px;
|
|
177
|
+
padding: 8px 12px;
|
|
178
|
+
color: #fff;
|
|
179
|
+
background: var(--accent);
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
font-weight: 700;
|
|
182
|
+
transition: transform 0.15s ease, background 0.15s ease;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
button.copy:hover { background: var(--accent-dark); transform: translateY(-1px); }
|
|
186
|
+
button.copy:focus-visible { outline: 3px solid rgba(201, 100, 66, 0.28); outline-offset: 2px; }
|
|
187
|
+
|
|
188
|
+
.section-title {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: flex-end;
|
|
191
|
+
justify-content: space-between;
|
|
192
|
+
gap: 16px;
|
|
193
|
+
margin-bottom: 16px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.section-title p {
|
|
197
|
+
max-width: 900px;
|
|
198
|
+
margin: 0;
|
|
199
|
+
color: var(--muted);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.steps {
|
|
203
|
+
display: grid;
|
|
204
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
205
|
+
gap: 14px;
|
|
206
|
+
margin-top: 18px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.step {
|
|
210
|
+
padding: 18px;
|
|
211
|
+
border: 1px solid var(--line);
|
|
212
|
+
border-radius: 18px;
|
|
213
|
+
background: rgba(255, 255, 255, 0.42);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.step-num {
|
|
217
|
+
display: inline-grid;
|
|
218
|
+
width: 28px;
|
|
219
|
+
height: 28px;
|
|
220
|
+
place-items: center;
|
|
221
|
+
margin-bottom: 12px;
|
|
222
|
+
border-radius: 50%;
|
|
223
|
+
color: #fff;
|
|
224
|
+
background: var(--accent);
|
|
225
|
+
font-size: 13px;
|
|
226
|
+
font-weight: 800;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.two-col {
|
|
230
|
+
display: grid;
|
|
231
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
232
|
+
gap: 18px;
|
|
233
|
+
margin-top: 18px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
ul { margin: 12px 0 0; padding-left: 20px; }
|
|
237
|
+
li + li { margin-top: 8px; }
|
|
238
|
+
|
|
239
|
+
.pill-list {
|
|
240
|
+
display: flex;
|
|
241
|
+
flex-wrap: wrap;
|
|
242
|
+
gap: 10px;
|
|
243
|
+
margin-top: 16px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.pill {
|
|
247
|
+
padding: 7px 10px;
|
|
248
|
+
border: 1px solid var(--line);
|
|
249
|
+
border-radius: 999px;
|
|
250
|
+
background: rgba(255, 255, 255, 0.45);
|
|
251
|
+
font-size: 13px;
|
|
252
|
+
font-weight: 700;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.status {
|
|
256
|
+
display: inline-flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: 8px;
|
|
259
|
+
color: var(--warn);
|
|
260
|
+
font-weight: 800;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.footer {
|
|
264
|
+
margin-top: 32px;
|
|
265
|
+
color: var(--muted);
|
|
266
|
+
font-size: 13px;
|
|
267
|
+
text-align: center;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@media (max-width: 860px) {
|
|
271
|
+
.page { width: min(100% - 24px, 1120px); padding-top: 24px; }
|
|
272
|
+
.two-col, .steps { grid-template-columns: 1fr; }
|
|
273
|
+
.hero-card, .card, .aside { padding: 22px; border-radius: 20px; }
|
|
274
|
+
.meta-grid { grid-template-columns: 1fr; }
|
|
275
|
+
.gateway-box { grid-template-columns: 1fr; }
|
|
276
|
+
.copy-row .copy { position: static; margin-top: 10px; }
|
|
277
|
+
pre { font-size: 12px; }
|
|
278
|
+
}
|
|
279
|
+
</style>
|
|
280
|
+
</head>
|
|
281
|
+
<body>
|
|
282
|
+
<main class="page">
|
|
283
|
+
<section class="hero">
|
|
284
|
+
<div class="hero-card">
|
|
285
|
+
<div class="eyebrow">SeeyonJF-AI Gateway</div>
|
|
286
|
+
<h1>AI 服务网关配置指南</h1>
|
|
287
|
+
<p class="lead">当前网关聚焦 <code>chat/completions</code> 模式,面向AI工具提供统一的模型访问入口。</p>
|
|
288
|
+
<div class="meta-grid">
|
|
289
|
+
<div class="meta"><span>当前协议</span><strong>OpenAI-compatible chat/completions</strong></div>
|
|
290
|
+
<div class="meta"><span>支持模型</span><strong>gpt-5.5 / qwen3.7-max / deepseek-v4-pro</strong></div>
|
|
291
|
+
<div class="meta"><span>后续规划</span><strong>Codex / Claude Code 协议适配</strong></div>
|
|
292
|
+
</div>
|
|
293
|
+
<div style="margin-top: 22px;">
|
|
294
|
+
<span class="label">网关基础地址</span>
|
|
295
|
+
<div class="copy-row">
|
|
296
|
+
<button class="copy" data-copy-target="gateway-url">复制地址</button>
|
|
297
|
+
<pre id="gateway-url">https://aigateway.edgecloudapp.com/v1/a9b63c35216bb24537b1644f64957654/seeyonkekai-chat</pre>
|
|
298
|
+
</div>
|
|
299
|
+
<p style="margin: 10px 0 0; color: var(--muted); font-size: 13px;">基础地址不要追加 <code>/chat/completions</code>,客户端会自动拼接实际请求路径。</p>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
</section>
|
|
304
|
+
|
|
305
|
+
<section class="card">
|
|
306
|
+
<div class="section-title">
|
|
307
|
+
<div>
|
|
308
|
+
<h2>防泄漏提醒</h2>
|
|
309
|
+
<p>配置过程中会涉及网关地址和认证 key,请区分可共享信息和敏感信息。</p>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="notice">
|
|
313
|
+
<strong>不要把认证 key 粘贴到公开仓库、Issue、聊天截图或共享文档。</strong><br>
|
|
314
|
+
网关地址可用于配置说明;认证 key 必须联系平台管理员或业务负责人单独获取、单独保管。如需分享配置说明,请使用占位符 <code><你的认证 key></code>,不要包含真实 key。
|
|
315
|
+
</div>
|
|
316
|
+
</section>
|
|
317
|
+
|
|
318
|
+
<section class="card">
|
|
319
|
+
<div class="section-title">
|
|
320
|
+
<div>
|
|
321
|
+
<h2>方式一:使用工具自动配置</h2>
|
|
322
|
+
<p>适合大多数用户。工具会先检查环境,再引导输入网关地址和认证 key,并在写入前备份原配置。</p>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div class="copy-row">
|
|
327
|
+
<button class="copy" data-copy-target="auto-command">复制命令</button>
|
|
328
|
+
<pre id="auto-command">npx --registry https://registry.npmjs.org/ seeyonjf-ai-tool@latest configure</pre>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div class="steps">
|
|
332
|
+
<div class="step">
|
|
333
|
+
<div class="step-num">1</div>
|
|
334
|
+
<h3>选择工具</h3>
|
|
335
|
+
<p>当前请选择 <code>opencode</code>。<code>Codex</code> 和 <code>Claude Code</code> 因协议差异,待网关侧协议支持完善后开放。</p>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="step">
|
|
338
|
+
<div class="step-num">2</div>
|
|
339
|
+
<h3>填写网关地址</h3>
|
|
340
|
+
<p>输入页面上方的网关基础地址,不要追加 <code>/chat/completions</code>。</p>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="step">
|
|
343
|
+
<div class="step-num">3</div>
|
|
344
|
+
<h3>填写认证 key</h3>
|
|
345
|
+
<p>认证 key 请联系平台管理员或业务负责人获取。工具会写入本机用户配置文件,请勿提交到代码仓库。</p>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</section>
|
|
349
|
+
|
|
350
|
+
<section class="card">
|
|
351
|
+
<div class="section-title">
|
|
352
|
+
<div>
|
|
353
|
+
<h2>方式二:把协议说明交给工具处理</h2>
|
|
354
|
+
<p>如果你使用的 AI 工具支持读取本地配置并理解 OpenAI-compatible chat/completions 协议,可以复制下面这段通用说明,让工具分析自身配置方式并完成配置。认证 key 请单独输入,不要写进共享文本。</p>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<div class="copy-row">
|
|
359
|
+
<button class="copy" data-copy-target="prompt-template">复制说明</button>
|
|
360
|
+
<pre id="prompt-template">请根据当前工具自身的配置机制,帮我接入 SeeyonJF-AI 网关。
|
|
361
|
+
|
|
362
|
+
请先判断当前工具是否支持 OpenAI-compatible chat/completions 协议,或者是否支持 OpenAI-compatible provider/baseURL/apiKey 形式的配置。若支持,请完成配置;若不支持,请明确说明原因,不要强行写入错误配置。
|
|
363
|
+
|
|
364
|
+
关键信息如下:
|
|
365
|
+
- 协议:OpenAI-compatible chat/completions
|
|
366
|
+
- provider id:seeyonjf-ai
|
|
367
|
+
- provider 显示名:SeeyonJF-AI
|
|
368
|
+
- 网关基础地址:https://aigateway.edgecloudapp.com/v1/a9b63c35216bb24537b1644f64957654/seeyonkekai-chat
|
|
369
|
+
- 默认模型:gpt-5.5
|
|
370
|
+
- 小模型/轻量任务模型:qwen3.7-max
|
|
371
|
+
- 其他可用模型:qwen3.7-max、deepseek-v4-pro
|
|
372
|
+
- 请求路径说明:配置时填写基础地址,不要手动追加 /chat/completions
|
|
373
|
+
|
|
374
|
+
安全要求:
|
|
375
|
+
- 认证 key 请单独向我询问,不要写入任何仓库、文档、日志或共享文件。
|
|
376
|
+
- 写入用户配置文件前请先备份原配置。
|
|
377
|
+
- 只修改当前工具所需的用户级配置,不要改动项目源码。
|
|
378
|
+
- 配置完成后提醒我重启或重新打开对应工具,使配置生效。</pre>
|
|
379
|
+
</div>
|
|
380
|
+
</section>
|
|
381
|
+
|
|
382
|
+
<section class="card">
|
|
383
|
+
<div class="section-title">
|
|
384
|
+
<div>
|
|
385
|
+
<h2>opencode 配置示例</h2>
|
|
386
|
+
<p>以下为参考示例。请把 <code>baseURL</code> 替换为实际网关基础地址,把 <code>apiKey</code> 替换为你单独获取的认证 key。</p>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div class="copy-row">
|
|
391
|
+
<button class="copy" data-copy-target="opencode-json">复制示例</button>
|
|
392
|
+
<pre id="opencode-json">{
|
|
393
|
+
"$schema": "https://opencode.ai/config.json",
|
|
394
|
+
"model": "seeyonjf-ai/gpt-5.5",
|
|
395
|
+
"small_model": "seeyonjf-ai/qwen3.7-max",
|
|
396
|
+
"provider": {
|
|
397
|
+
"seeyonjf-ai": {
|
|
398
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
399
|
+
"name": "SeeyonJF-AI",
|
|
400
|
+
"options": {
|
|
401
|
+
"baseURL": "https://你的网关域名/v1/你的路径",
|
|
402
|
+
"apiKey": "替换为你的认证 key",
|
|
403
|
+
"timeout": false
|
|
404
|
+
},
|
|
405
|
+
"models": {
|
|
406
|
+
"gpt-5.5": {
|
|
407
|
+
"name": "gpt-5.5",
|
|
408
|
+
"tool_call": true,
|
|
409
|
+
"reasoning": true,
|
|
410
|
+
"attachment": true,
|
|
411
|
+
"modalities": {
|
|
412
|
+
"input": ["text", "image"],
|
|
413
|
+
"output": ["text"]
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
"qwen3.7-max": {
|
|
417
|
+
"name": "qwen3.7-max",
|
|
418
|
+
"tool_call": true,
|
|
419
|
+
"reasoning": true,
|
|
420
|
+
"interleaved": {
|
|
421
|
+
"field": "reasoning_content"
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
"deepseek-v4-pro": {
|
|
425
|
+
"name": "deepseek-v4-pro",
|
|
426
|
+
"tool_call": true,
|
|
427
|
+
"reasoning": true,
|
|
428
|
+
"interleaved": {
|
|
429
|
+
"field": "reasoning_content"
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}</pre>
|
|
436
|
+
</div>
|
|
437
|
+
</section>
|
|
438
|
+
|
|
439
|
+
<section class="card">
|
|
440
|
+
<div class="section-title">
|
|
441
|
+
<div>
|
|
442
|
+
<h2>协议支持状态</h2>
|
|
443
|
+
<p>不同工具依赖的协议不同,当前只开放已验证的 chat/completions 配置能力。</p>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
<div class="two-col">
|
|
447
|
+
<div class="step">
|
|
448
|
+
<h3>当前可用</h3>
|
|
449
|
+
<div class="pill-list">
|
|
450
|
+
<span class="pill">chat/completions</span>
|
|
451
|
+
<span class="pill">OpenAI-compatible</span>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
<div class="step">
|
|
455
|
+
<h3>建设中</h3>
|
|
456
|
+
<p class="status">Codex / Claude Code 待协议支持完善</p>
|
|
457
|
+
<p>Codex 当前不再适配本工具现有的 chat 配置方式;Claude Code 需要 Anthropic Messages 等协议能力。待网关侧协议建设完成后再开放自动配置。</p>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
</section>
|
|
461
|
+
|
|
462
|
+
<p class="footer">SeeyonJF-AI Gateway · 请妥善保管认证 key,配置文件仅保存在本机用户目录。</p>
|
|
463
|
+
</main>
|
|
464
|
+
|
|
465
|
+
<script>
|
|
466
|
+
const buttons = document.querySelectorAll("[data-copy-target]");
|
|
467
|
+
for (const button of buttons) {
|
|
468
|
+
button.addEventListener("click", async () => {
|
|
469
|
+
const target = document.getElementById(button.dataset.copyTarget);
|
|
470
|
+
const text = target ? target.innerText.trim() : "";
|
|
471
|
+
try {
|
|
472
|
+
await navigator.clipboard.writeText(text);
|
|
473
|
+
const previous = button.textContent;
|
|
474
|
+
button.textContent = "已复制";
|
|
475
|
+
setTimeout(() => { button.textContent = previous; }, 1400);
|
|
476
|
+
} catch {
|
|
477
|
+
const range = document.createRange();
|
|
478
|
+
range.selectNodeContents(target);
|
|
479
|
+
const selection = window.getSelection();
|
|
480
|
+
selection.removeAllRanges();
|
|
481
|
+
selection.addRange(range);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
</script>
|
|
486
|
+
</body>
|
|
487
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "seeyonjf-ai-tool",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "SeeyonJF-AI chat/completions gateway configuration helper for opencode and Codex.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"seeyonjf-ai-tool": "tools/configure-chat-gateway.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"README.md",
|
|
11
|
+
"docs/chat-completions-gateway-config.md",
|
|
12
|
+
"docs/gateway-config.html",
|
|
13
|
+
"tools/configure-chat-gateway.mjs"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"configure": "node tools/configure-chat-gateway.mjs configure",
|
|
17
|
+
"check": "node --check tools/configure-chat-gateway.mjs",
|
|
18
|
+
"help": "node tools/configure-chat-gateway.mjs --help",
|
|
19
|
+
"publish:npm": "node tools/publish-npm.mjs",
|
|
20
|
+
"publish:npm:dry-run": "node tools/publish-npm.mjs --dry-run"
|
|
21
|
+
},
|
|
22
|
+
"license": "UNLICENSED",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import * as readline from "node:readline/promises"
|
|
7
|
+
import { stdin as input, stdout as output } from "node:process"
|
|
8
|
+
|
|
9
|
+
const CHAT_MODELS = ["gpt-5.5", "qwen3.7-max", "deepseek-v4-pro"]
|
|
10
|
+
const DEFAULT_MODEL = "gpt-5.5"
|
|
11
|
+
const DEFAULT_SMALL_MODEL = "qwen3.7-max"
|
|
12
|
+
const PROVIDER_ID = "seeyonjf-ai"
|
|
13
|
+
const PACKAGE_NAME = "seeyonjf-ai-tool"
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2)
|
|
16
|
+
const colorEnabled = Boolean(output.isTTY && !process.env.NO_COLOR)
|
|
17
|
+
|
|
18
|
+
function color(code, text) {
|
|
19
|
+
return colorEnabled ? `\x1b[${code}m${text}\x1b[0m` : text
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ui = {
|
|
23
|
+
title: (text) => color("1;36", text),
|
|
24
|
+
info: (text) => color("36", text),
|
|
25
|
+
success: (text) => color("32", text),
|
|
26
|
+
warning: (text) => color("33", text),
|
|
27
|
+
error: (text) => color("31", text),
|
|
28
|
+
muted: (text) => color("2", text),
|
|
29
|
+
value: (text) => color("1", text),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function section(title) {
|
|
33
|
+
output.write(`${ui.title(`== ${title} ==`)}\n`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function usage() {
|
|
37
|
+
return `SeeyonJF-AI chat/completions 网关配置工具
|
|
38
|
+
|
|
39
|
+
用法:
|
|
40
|
+
seeyonjf-ai-tool configure
|
|
41
|
+
seeyonjf-ai-tool configure --tool opencode --base-url <url>
|
|
42
|
+
seeyonjf-ai-tool restore
|
|
43
|
+
seeyonjf-ai-tool list-backups
|
|
44
|
+
|
|
45
|
+
npx 用法:
|
|
46
|
+
npx ${PACKAGE_NAME}@latest configure
|
|
47
|
+
npx ${PACKAGE_NAME}@latest configure --tool opencode --base-url <url>
|
|
48
|
+
|
|
49
|
+
选项:
|
|
50
|
+
--tool <opencode|codex|both> 需要配置的工具;codex/both 待协议支持建设中
|
|
51
|
+
--base-url <url> chat/completions 网关基础地址,不要追加 /chat/completions
|
|
52
|
+
--key <token> chat/completions 网关 token
|
|
53
|
+
--model <model> 默认模型,默认 ${DEFAULT_MODEL}
|
|
54
|
+
--small-model <model> opencode small_model,默认 ${DEFAULT_SMALL_MODEL}
|
|
55
|
+
--help 显示帮助
|
|
56
|
+
--version 显示版本
|
|
57
|
+
|
|
58
|
+
支持模型:
|
|
59
|
+
${CHAT_MODELS.join(", ")}
|
|
60
|
+
`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseArgs(argv) {
|
|
64
|
+
const parsed = { command: "configure" }
|
|
65
|
+
let start = 0
|
|
66
|
+
if (argv[0] && !argv[0].startsWith("-")) {
|
|
67
|
+
parsed.command = argv[0]
|
|
68
|
+
start = 1
|
|
69
|
+
}
|
|
70
|
+
for (let i = start; i < argv.length; i += 1) {
|
|
71
|
+
const arg = argv[i]
|
|
72
|
+
if (arg === "--help" || arg === "-h") parsed.help = true
|
|
73
|
+
else if (arg === "--version" || arg === "-v") parsed.version = true
|
|
74
|
+
else if (arg === "--tool") parsed.tool = argv[++i]
|
|
75
|
+
else if (arg === "--base-url") parsed.baseUrl = argv[++i]
|
|
76
|
+
else if (arg === "--key") parsed.key = argv[++i]
|
|
77
|
+
else if (arg === "--model") parsed.model = argv[++i]
|
|
78
|
+
else if (arg === "--small-model") parsed.smallModel = argv[++i]
|
|
79
|
+
else throw new Error(`未知参数: ${arg}`)
|
|
80
|
+
}
|
|
81
|
+
return parsed
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function packageVersion() {
|
|
85
|
+
const packagePath = new URL("../package.json", import.meta.url)
|
|
86
|
+
const raw = fs.readFileSync(packagePath, "utf8")
|
|
87
|
+
return JSON.parse(raw).version
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function configPaths() {
|
|
91
|
+
return {
|
|
92
|
+
opencode: path.join(os.homedir(), ".config", "opencode", "opencode.json"),
|
|
93
|
+
codex: path.join(os.homedir(), ".codex", "config.toml"),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ensureDir(filePath) {
|
|
98
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function commandExists(command) {
|
|
102
|
+
const pathValue = process.env.PATH ?? ""
|
|
103
|
+
const pathExts = process.platform === "win32"
|
|
104
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
|
|
105
|
+
: [""]
|
|
106
|
+
for (const dir of pathValue.split(path.delimiter)) {
|
|
107
|
+
if (!dir) continue
|
|
108
|
+
for (const ext of pathExts) {
|
|
109
|
+
const commandPath = path.join(dir, `${command}${ext}`)
|
|
110
|
+
try {
|
|
111
|
+
fs.accessSync(commandPath, fs.constants.X_OK)
|
|
112
|
+
return commandPath
|
|
113
|
+
} catch {
|
|
114
|
+
// Continue searching other PATH entries.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checkTools() {
|
|
122
|
+
return {
|
|
123
|
+
opencode: commandExists("opencode"),
|
|
124
|
+
codex: commandExists("codex"),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function printToolCheck(tools) {
|
|
129
|
+
section("环境检查")
|
|
130
|
+
for (const [tool, commandPath] of Object.entries(tools)) {
|
|
131
|
+
if (commandPath) output.write(`${ui.success("✓")} ${tool}: 已安装 ${ui.muted(`(${commandPath})`)}\n`)
|
|
132
|
+
else output.write(`${ui.warning("!")} ${tool}: 未在 PATH 中找到,仍可先写入配置。\n`)
|
|
133
|
+
}
|
|
134
|
+
output.write("\n")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function timestamp() {
|
|
138
|
+
const date = new Date()
|
|
139
|
+
const pad = (value) => String(value).padStart(2, "0")
|
|
140
|
+
return [
|
|
141
|
+
date.getFullYear(),
|
|
142
|
+
pad(date.getMonth() + 1),
|
|
143
|
+
pad(date.getDate()),
|
|
144
|
+
"-",
|
|
145
|
+
pad(date.getHours()),
|
|
146
|
+
pad(date.getMinutes()),
|
|
147
|
+
pad(date.getSeconds()),
|
|
148
|
+
].join("")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function backupFile(filePath) {
|
|
152
|
+
if (!fs.existsSync(filePath)) return null
|
|
153
|
+
const backupPath = `${filePath}.bak-${timestamp()}`
|
|
154
|
+
fs.copyFileSync(filePath, backupPath)
|
|
155
|
+
return backupPath
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readJsonIfExists(filePath) {
|
|
159
|
+
if (!fs.existsSync(filePath)) return {}
|
|
160
|
+
const raw = fs.readFileSync(filePath, "utf8").trim()
|
|
161
|
+
if (!raw) return {}
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(raw)
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new Error(`${filePath} 不是有效 JSON,已停止写入。请先修复或手动备份后重试。\n${error.message}`)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function mergeOpenCodeConfig(existing, token, baseUrl, model, smallModel) {
|
|
170
|
+
return {
|
|
171
|
+
...existing,
|
|
172
|
+
$schema: existing.$schema ?? "https://opencode.ai/config.json",
|
|
173
|
+
model: `${PROVIDER_ID}/${model}`,
|
|
174
|
+
small_model: `${PROVIDER_ID}/${smallModel}`,
|
|
175
|
+
provider: {
|
|
176
|
+
...(existing.provider ?? {}),
|
|
177
|
+
[PROVIDER_ID]: {
|
|
178
|
+
...((existing.provider ?? {})[PROVIDER_ID] ?? {}),
|
|
179
|
+
npm: "@ai-sdk/openai-compatible",
|
|
180
|
+
name: "SeeyonJF-AI",
|
|
181
|
+
options: {
|
|
182
|
+
...(((existing.provider ?? {})[PROVIDER_ID] ?? {}).options ?? {}),
|
|
183
|
+
baseURL: baseUrl,
|
|
184
|
+
apiKey: token,
|
|
185
|
+
timeout: false,
|
|
186
|
+
},
|
|
187
|
+
models: {
|
|
188
|
+
...((((existing.provider ?? {})[PROVIDER_ID] ?? {}).models) ?? {}),
|
|
189
|
+
"gpt-5.5": {
|
|
190
|
+
name: "gpt-5.5",
|
|
191
|
+
tool_call: true,
|
|
192
|
+
reasoning: true,
|
|
193
|
+
attachment: true,
|
|
194
|
+
modalities: {
|
|
195
|
+
input: ["text", "image"],
|
|
196
|
+
output: ["text"],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
"qwen3.7-max": {
|
|
200
|
+
name: "qwen3.7-max",
|
|
201
|
+
tool_call: true,
|
|
202
|
+
reasoning: true,
|
|
203
|
+
interleaved: {
|
|
204
|
+
field: "reasoning_content",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
"deepseek-v4-pro": {
|
|
208
|
+
name: "deepseek-v4-pro",
|
|
209
|
+
tool_call: true,
|
|
210
|
+
reasoning: true,
|
|
211
|
+
interleaved: {
|
|
212
|
+
field: "reasoning_content",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function configureOpenCode(token, baseUrl, model, smallModel) {
|
|
222
|
+
const filePath = configPaths().opencode
|
|
223
|
+
ensureDir(filePath)
|
|
224
|
+
const backupPath = backupFile(filePath)
|
|
225
|
+
const existing = readJsonIfExists(filePath)
|
|
226
|
+
const next = mergeOpenCodeConfig(existing, token, baseUrl, model, smallModel)
|
|
227
|
+
fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8")
|
|
228
|
+
return { filePath, backupPath }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function tomlString(value) {
|
|
232
|
+
return JSON.stringify(value)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function stripManagedCodexBlock(raw) {
|
|
236
|
+
const begin = "# >>> seeyonjf-ai managed >>>"
|
|
237
|
+
const end = "# <<< seeyonjf-ai managed <<<"
|
|
238
|
+
const start = raw.indexOf(begin)
|
|
239
|
+
if (start === -1) return raw.trimEnd()
|
|
240
|
+
const finish = raw.indexOf(end, start)
|
|
241
|
+
if (finish === -1) {
|
|
242
|
+
throw new Error("Codex 配置中存在未闭合的 seeyonjf-ai managed block,请手动处理后重试。")
|
|
243
|
+
}
|
|
244
|
+
return `${raw.slice(0, start)}${raw.slice(finish + end.length)}`.trimEnd()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function stripCodexTopLevelSelection(raw) {
|
|
248
|
+
const lines = raw.split(/\r?\n/)
|
|
249
|
+
let inTable = false
|
|
250
|
+
return lines.filter((line) => {
|
|
251
|
+
if (/^\s*\[/.test(line)) inTable = true
|
|
252
|
+
if (inTable) return true
|
|
253
|
+
return !/^\s*(model|model_provider)\s*=/.test(line)
|
|
254
|
+
}).join("\n").trimEnd()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function codexManagedBlock(token, baseUrl, model) {
|
|
258
|
+
return `# >>> seeyonjf-ai managed >>>
|
|
259
|
+
# 由 tools/configure-chat-gateway.mjs 自动生成。重新运行工具会替换本段。
|
|
260
|
+
model = ${tomlString(model)}
|
|
261
|
+
model_provider = ${tomlString(PROVIDER_ID)}
|
|
262
|
+
|
|
263
|
+
[model_providers.${PROVIDER_ID}]
|
|
264
|
+
name = "SeeyonJF-AI"
|
|
265
|
+
base_url = ${tomlString(baseUrl)}
|
|
266
|
+
wire_api = "chat"
|
|
267
|
+
experimental_bearer_token = ${tomlString(token)}
|
|
268
|
+
# <<< seeyonjf-ai managed <<<`
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function configureCodex(token, baseUrl, model) {
|
|
272
|
+
const filePath = configPaths().codex
|
|
273
|
+
ensureDir(filePath)
|
|
274
|
+
const backupPath = backupFile(filePath)
|
|
275
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : ""
|
|
276
|
+
const preserved = stripCodexTopLevelSelection(stripManagedCodexBlock(existing))
|
|
277
|
+
const next = [codexManagedBlock(token, baseUrl, model), preserved].filter(Boolean).join("\n\n")
|
|
278
|
+
fs.writeFileSync(filePath, `${next}\n`, "utf8")
|
|
279
|
+
return { filePath, backupPath }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function listBackups(filePath) {
|
|
283
|
+
const dir = path.dirname(filePath)
|
|
284
|
+
const base = path.basename(filePath)
|
|
285
|
+
if (!fs.existsSync(dir)) return []
|
|
286
|
+
return fs.readdirSync(dir)
|
|
287
|
+
.filter((entry) => entry.startsWith(`${base}.bak-`))
|
|
288
|
+
.map((entry) => path.join(dir, entry))
|
|
289
|
+
.sort()
|
|
290
|
+
.reverse()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function promptChoice(rl, question, choices) {
|
|
294
|
+
section(question)
|
|
295
|
+
choices.forEach((choice, index) => {
|
|
296
|
+
const suffix = choice.disabled ? ` ${ui.warning("[待协议支持]")}` : ""
|
|
297
|
+
output.write(` ${ui.value(`${index + 1}.`)} ${choice.label}${suffix}\n`)
|
|
298
|
+
if (choice.description) output.write(` ${ui.muted(choice.description)}\n`)
|
|
299
|
+
})
|
|
300
|
+
while (true) {
|
|
301
|
+
const answer = (await rl.question("请输入序号: ")).trim()
|
|
302
|
+
const index = Number(answer) - 1
|
|
303
|
+
if (Number.isInteger(index) && choices[index]) return choices[index].value
|
|
304
|
+
output.write(`${ui.warning("输入无效,请重新输入。")}\n`)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isCodexUnavailable(tool) {
|
|
309
|
+
return tool === "codex" || tool === "both"
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function printCodexUnavailable(tool) {
|
|
313
|
+
section("暂不支持")
|
|
314
|
+
const target = tool === "both" ? "opencode + codex" : "Codex"
|
|
315
|
+
output.write(`${ui.warning("!")} 当前选择: ${target}\n`)
|
|
316
|
+
output.write("Codex 当前不再支持本工具现有的 chat/completions 配置方式。\n")
|
|
317
|
+
output.write("Codex 配置需等待网关侧协议支持建设完成后再开放。\n")
|
|
318
|
+
output.write("本次不会写入任何配置,也不会修改现有文件。\n\n")
|
|
319
|
+
output.write(`如需配置当前可用能力,请选择 ${ui.value("opencode")}。\n`)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function restoreFlow() {
|
|
323
|
+
const rl = readline.createInterface({ input, output })
|
|
324
|
+
try {
|
|
325
|
+
const tool = await promptChoice(rl, "请选择需要恢复的工具", [
|
|
326
|
+
{ label: "opencode", value: "opencode" },
|
|
327
|
+
{ label: "codex", value: "codex" },
|
|
328
|
+
])
|
|
329
|
+
const targetPath = configPaths()[tool]
|
|
330
|
+
const backups = listBackups(targetPath)
|
|
331
|
+
if (backups.length === 0) {
|
|
332
|
+
output.write(`未找到备份文件: ${targetPath}.bak-*\n`)
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
const backupPath = await promptChoice(rl, "请选择要恢复的备份", backups.map((backup) => ({ label: backup, value: backup })))
|
|
336
|
+
const currentBackup = backupFile(targetPath)
|
|
337
|
+
ensureDir(targetPath)
|
|
338
|
+
fs.copyFileSync(backupPath, targetPath)
|
|
339
|
+
output.write(`已恢复 ${targetPath}\n`)
|
|
340
|
+
output.write(`来源备份: ${backupPath}\n`)
|
|
341
|
+
if (currentBackup) output.write(`恢复前的当前配置已再次备份: ${currentBackup}\n`)
|
|
342
|
+
} finally {
|
|
343
|
+
rl.close()
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function validateTool(tool) {
|
|
348
|
+
if (!["opencode", "codex", "both"].includes(tool)) {
|
|
349
|
+
throw new Error("--tool 只能是 opencode、codex 或 both")
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function validateModel(model, label) {
|
|
354
|
+
if (!CHAT_MODELS.includes(model)) {
|
|
355
|
+
throw new Error(`${label} 不在支持列表中: ${CHAT_MODELS.join(", ")}`)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function validateBaseUrl(baseUrl) {
|
|
360
|
+
const normalized = baseUrl.trim().replace(/\/+$/, "")
|
|
361
|
+
let url
|
|
362
|
+
try {
|
|
363
|
+
url = new URL(normalized)
|
|
364
|
+
} catch {
|
|
365
|
+
throw new Error("--base-url 必须是有效 URL")
|
|
366
|
+
}
|
|
367
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
368
|
+
throw new Error("--base-url 只支持 http 或 https URL")
|
|
369
|
+
}
|
|
370
|
+
if (url.pathname.replace(/\/+$/, "").endsWith("/chat/completions")) {
|
|
371
|
+
throw new Error("--base-url 请填写基础地址,不要追加 /chat/completions")
|
|
372
|
+
}
|
|
373
|
+
return normalized
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function printBackups() {
|
|
377
|
+
const paths = configPaths()
|
|
378
|
+
for (const [tool, filePath] of Object.entries(paths)) {
|
|
379
|
+
const backups = listBackups(filePath)
|
|
380
|
+
output.write(`${tool}: ${filePath}\n`)
|
|
381
|
+
if (backups.length === 0) {
|
|
382
|
+
output.write(" 未找到备份。\n")
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
for (const backup of backups) output.write(` ${backup}\n`)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function configureFlow(parsed) {
|
|
390
|
+
const rl = readline.createInterface({ input, output })
|
|
391
|
+
try {
|
|
392
|
+
printToolCheck(checkTools())
|
|
393
|
+
|
|
394
|
+
const tool = parsed.tool ?? await promptChoice(rl, "请选择需要配置的工具", [
|
|
395
|
+
{ label: "opencode", value: "opencode", description: "当前支持 chat/completions 网关配置。" },
|
|
396
|
+
{ label: "codex", value: "codex", disabled: true, description: "待协议支持建设中,当前不会写入配置。" },
|
|
397
|
+
{ label: "opencode + codex", value: "both", disabled: true, description: "包含 Codex,待协议支持建设中,当前不会写入配置。" },
|
|
398
|
+
])
|
|
399
|
+
validateTool(tool)
|
|
400
|
+
if (isCodexUnavailable(tool)) {
|
|
401
|
+
printCodexUnavailable(tool)
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const model = parsed.model ?? DEFAULT_MODEL
|
|
406
|
+
const smallModel = parsed.smallModel ?? DEFAULT_SMALL_MODEL
|
|
407
|
+
validateModel(model, "默认模型")
|
|
408
|
+
validateModel(smallModel, "small_model")
|
|
409
|
+
|
|
410
|
+
let baseUrl = parsed.baseUrl
|
|
411
|
+
if (!baseUrl) baseUrl = (await rl.question("请输入 chat/completions 网关基础地址: ")).trim()
|
|
412
|
+
if (!baseUrl) throw new Error("网关基础地址不能为空")
|
|
413
|
+
baseUrl = validateBaseUrl(baseUrl)
|
|
414
|
+
|
|
415
|
+
let token = parsed.key
|
|
416
|
+
if (!token) token = (await rl.question("请输入 chat/completions 网关 token: ")).trim()
|
|
417
|
+
if (!token) throw new Error("token 不能为空")
|
|
418
|
+
|
|
419
|
+
const results = []
|
|
420
|
+
results.push({ tool: "opencode", ...configureOpenCode(token, baseUrl, model, smallModel) })
|
|
421
|
+
|
|
422
|
+
output.write("\n")
|
|
423
|
+
section("配置完成")
|
|
424
|
+
output.write(`${ui.success("✓")} 网关基础地址: ${ui.value(baseUrl)}\n`)
|
|
425
|
+
output.write(`${ui.success("✓")} 默认模型: ${ui.value(model)}\n`)
|
|
426
|
+
output.write(`${ui.success("✓")} opencode small_model: ${ui.value(smallModel)}\n`)
|
|
427
|
+
for (const result of results) {
|
|
428
|
+
output.write(`${ui.success("✓")} ${result.tool}: ${result.filePath}\n`)
|
|
429
|
+
if (result.backupPath) output.write(` 备份文件: ${result.backupPath}\n`)
|
|
430
|
+
else output.write(` ${ui.muted("原配置不存在,本次未生成备份。")}\n`)
|
|
431
|
+
}
|
|
432
|
+
output.write("\n")
|
|
433
|
+
section("恢复方式")
|
|
434
|
+
output.write("- 交互恢复: seeyonjf-ai-tool restore\n")
|
|
435
|
+
output.write("- 手动恢复: 将对应 .bak-时间戳 文件复制回原配置路径。\n")
|
|
436
|
+
output.write(`\n${ui.warning("提示:")} opencode 配置修改后需要重启 opencode。\n`)
|
|
437
|
+
} finally {
|
|
438
|
+
rl.close()
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function main() {
|
|
443
|
+
const parsed = parseArgs(args)
|
|
444
|
+
if (parsed.version) {
|
|
445
|
+
output.write(`${packageVersion()}\n`)
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
if (parsed.help) {
|
|
449
|
+
output.write(usage())
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
if (parsed.command === "configure") {
|
|
453
|
+
await configureFlow(parsed)
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
if (parsed.command === "restore") {
|
|
457
|
+
await restoreFlow()
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
if (parsed.command === "list-backups") {
|
|
461
|
+
printBackups()
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
throw new Error(`未知命令: ${parsed.command}`)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
main().catch((error) => {
|
|
468
|
+
console.error(`错误: ${error.message}`)
|
|
469
|
+
process.exitCode = 1
|
|
470
|
+
})
|