@ailuntz/minilink 0.9.0 → 0.9.2
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 +16 -23
- package/bin/minilink.mjs +6 -2
- package/examples/providers/.env.example +2 -0
- package/examples/providers/README.md +12 -0
- package/examples/providers/deepseek-models.json +83 -0
- package/examples/providers/deepseek.config.toml +9 -0
- package/examples/providers/doubao-models.json +68 -0
- package/examples/providers/doubao.config.toml +9 -0
- package/package.json +2 -1
- package/src/picker.mjs +11 -3
- package/src/proxy.mjs +14 -1
- package/src/terminal.mjs +10 -8
package/README.md
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# MiniLink
|
|
2
2
|
|
|
3
|
-
在 ChatGPT / Codex Desktop
|
|
4
|
-
|
|
5
|
-
读取 `~/.codex/*.config.toml`:有 `model_catalog_json` 就使用该目录,否则继承原生模型列表。支持 API key 的 Responses 连接还会追加 `-Chat`,用于不带工具和工作区上下文的纯文字聊天。
|
|
3
|
+
在 ChatGPT / Codex Desktop 的原生模型菜单中使用自定义模型。
|
|
6
4
|
|
|
7
5
|
## 安装
|
|
8
6
|
|
|
9
|
-
macOS
|
|
7
|
+
支持 macOS,需 Node.js ≥22.16。
|
|
10
8
|
|
|
11
9
|
```sh
|
|
12
10
|
npm install -g @ailuntz/minilink
|
|
11
|
+
minilink on
|
|
13
12
|
```
|
|
14
13
|
|
|
15
|
-
完全退出并重开 Desktop
|
|
14
|
+
完全退出并重开 Desktop。
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
minilink on
|
|
19
|
-
```
|
|
16
|
+
## 命令
|
|
20
17
|
|
|
21
18
|
| 命令 | 用途 |
|
|
22
19
|
| --- | --- |
|
|
23
|
-
| `on` |
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
20
|
+
| `minilink on` | 开启 |
|
|
21
|
+
| `minilink link` | 选择模型与推理强度 |
|
|
22
|
+
| `minilink off` | 关闭 |
|
|
23
|
+
| `minilink doctor` | 检查配置与运行状态 |
|
|
24
|
+
|
|
25
|
+
## 配置
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
读取 `~/.codex/*.config.toml`。通过 `model_catalog_json` 指定模型目录;未指定时继承原生目录。密钥使用提供商配置的环境变量,也可保存在 `~/.codex/.env`。
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
支持 Responses API。符合条件的连接提供 `-Chat` 选项,用于纯文字聊天。
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
供应商示例:[DeepSeek / 豆包](examples/providers/)。
|
|
32
32
|
|
|
33
33
|
## 卸载
|
|
34
34
|
|
|
@@ -36,11 +36,4 @@ minilink on
|
|
|
36
36
|
npm uninstall -g @ailuntz/minilink
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
## 注意
|
|
42
|
-
|
|
43
|
-
- 跨连接切换需要任务已保存且空闲;临时任务、尚无历史或含子代理的任务请新建。
|
|
44
|
-
- Chat 仅支持纯文字历史,不支持附件、中途追加和上下文压缩。
|
|
45
|
-
- 切换模型保留当前任务权限。
|
|
46
|
-
- 密钥使用提供商声明的环境变量,也可放在 `~/.codex/.env`。支持 `CODEX_HOME`。
|
|
39
|
+
卸载后完全退出并重开 Desktop。
|
package/bin/minilink.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { selectModel, text, language } from '../src/terminal.mjs';
|
|
|
9
9
|
import { installation, environmentStatus } from '../src/install.mjs';
|
|
10
10
|
|
|
11
11
|
const self = fileURLToPath(import.meta.url), args = process.argv.slice(2);
|
|
12
|
-
const commands = ['on', 'off', 'doctor'];
|
|
12
|
+
const commands = ['on', 'link', 'off', 'doctor'];
|
|
13
13
|
const output = value => console.log(typeof value === 'string' ? value : JSON.stringify(value, null, 2));
|
|
14
14
|
|
|
15
15
|
async function main() {
|
|
@@ -21,7 +21,11 @@ async function main() {
|
|
|
21
21
|
if (command === 'on' || command === 'off') {
|
|
22
22
|
if (command === 'on') discoverProfiles();
|
|
23
23
|
atomicJSON(configPath(), { version: 1, enabled: command === 'on' });
|
|
24
|
-
return output(
|
|
24
|
+
return output(text(command));
|
|
25
|
+
}
|
|
26
|
+
if (command === 'link') {
|
|
27
|
+
if (!readConfig().enabled) throw new Error(text('disabled'));
|
|
28
|
+
return output(await selectModel(pickerEndpoints(codexHome())));
|
|
25
29
|
}
|
|
26
30
|
if (command === 'doctor') {
|
|
27
31
|
const profiles = discoverProfiles(), env = childEnvironment(profiles);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 供应商配置
|
|
2
|
+
|
|
3
|
+
将所需的 `.config.toml` 和 `-models.json` 文件放入 `~/.codex/`,按 `.env.example` 将密钥补充到 `~/.codex/.env`,然后重启 Desktop。
|
|
4
|
+
|
|
5
|
+
| 供应商 | 模型 |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| DeepSeek | V4.1 Flash、V4 Pro |
|
|
8
|
+
| 豆包 | Seed 2.1 Pro、Seed 2.1 Lite(260915) |
|
|
9
|
+
|
|
10
|
+
MiniLink 的 Chat 模式仅支持文字。
|
|
11
|
+
|
|
12
|
+
模型核对:2026-09-24。[DeepSeek](https://api-docs.deepseek.com/quick_start/pricing/) · [豆包](https://docs.volcengine.com/docs/ark/model-list?lang=zh)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"models": [
|
|
3
|
+
{
|
|
4
|
+
"slug": "deepseek-flash",
|
|
5
|
+
"display_name": "DeepSeek V4.1 Flash",
|
|
6
|
+
"description": "DeepSeek V4.1 Flash",
|
|
7
|
+
"default_reasoning_level": "low",
|
|
8
|
+
"supported_reasoning_levels": [
|
|
9
|
+
{
|
|
10
|
+
"effort": "low",
|
|
11
|
+
"description": "low"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"effort": "high",
|
|
15
|
+
"description": "high"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"effort": "max",
|
|
19
|
+
"description": "max"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"shell_type": "unified_exec",
|
|
23
|
+
"visibility": "list",
|
|
24
|
+
"supported_in_api": true,
|
|
25
|
+
"priority": 0,
|
|
26
|
+
"upgrade": null,
|
|
27
|
+
"base_instructions": "You are a helpful coding assistant. Follow the user instructions, inspect the workspace when needed, and explain results clearly.",
|
|
28
|
+
"support_verbosity": false,
|
|
29
|
+
"supports_reasoning_summaries": false,
|
|
30
|
+
"default_reasoning_summary": "none",
|
|
31
|
+
"supports_parallel_tool_calls": false,
|
|
32
|
+
"truncation_policy": {
|
|
33
|
+
"mode": "bytes",
|
|
34
|
+
"limit": 10000
|
|
35
|
+
},
|
|
36
|
+
"experimental_supported_tools": [],
|
|
37
|
+
"context_window": 1000000,
|
|
38
|
+
"input_modalities": [
|
|
39
|
+
"text",
|
|
40
|
+
"image"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"slug": "deepseek-v4-pro",
|
|
45
|
+
"display_name": "DeepSeek V4 Pro",
|
|
46
|
+
"description": "DeepSeek V4 Pro",
|
|
47
|
+
"default_reasoning_level": "low",
|
|
48
|
+
"supported_reasoning_levels": [
|
|
49
|
+
{
|
|
50
|
+
"effort": "low",
|
|
51
|
+
"description": "low"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"effort": "high",
|
|
55
|
+
"description": "high"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"effort": "max",
|
|
59
|
+
"description": "max"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"shell_type": "unified_exec",
|
|
63
|
+
"visibility": "list",
|
|
64
|
+
"supported_in_api": true,
|
|
65
|
+
"priority": 0,
|
|
66
|
+
"upgrade": null,
|
|
67
|
+
"base_instructions": "You are a helpful coding assistant. Follow the user instructions, inspect the workspace when needed, and explain results clearly.",
|
|
68
|
+
"support_verbosity": false,
|
|
69
|
+
"supports_reasoning_summaries": false,
|
|
70
|
+
"default_reasoning_summary": "none",
|
|
71
|
+
"supports_parallel_tool_calls": false,
|
|
72
|
+
"truncation_policy": {
|
|
73
|
+
"mode": "bytes",
|
|
74
|
+
"limit": 10000
|
|
75
|
+
},
|
|
76
|
+
"experimental_supported_tools": [],
|
|
77
|
+
"context_window": 1000000,
|
|
78
|
+
"input_modalities": [
|
|
79
|
+
"text"
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"models": [
|
|
3
|
+
{
|
|
4
|
+
"slug": "doubao-seed-2-1-pro-260915",
|
|
5
|
+
"display_name": "Doubao Seed 2.1 Pro",
|
|
6
|
+
"description": "Doubao Seed 2.1 Pro",
|
|
7
|
+
"default_reasoning_level": "low",
|
|
8
|
+
"supported_reasoning_levels": [
|
|
9
|
+
{
|
|
10
|
+
"effort": "low",
|
|
11
|
+
"description": "low"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"shell_type": "unified_exec",
|
|
15
|
+
"visibility": "list",
|
|
16
|
+
"supported_in_api": true,
|
|
17
|
+
"priority": 0,
|
|
18
|
+
"upgrade": null,
|
|
19
|
+
"base_instructions": "You are a helpful coding assistant. Follow the user instructions, inspect the workspace when needed, and explain results clearly.",
|
|
20
|
+
"support_verbosity": false,
|
|
21
|
+
"supports_reasoning_summaries": false,
|
|
22
|
+
"default_reasoning_summary": "none",
|
|
23
|
+
"supports_parallel_tool_calls": false,
|
|
24
|
+
"truncation_policy": {
|
|
25
|
+
"mode": "bytes",
|
|
26
|
+
"limit": 10000
|
|
27
|
+
},
|
|
28
|
+
"experimental_supported_tools": [],
|
|
29
|
+
"context_window": 1048576,
|
|
30
|
+
"input_modalities": [
|
|
31
|
+
"text",
|
|
32
|
+
"image"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"slug": "doubao-seed-2-1-lite-260915",
|
|
37
|
+
"display_name": "Doubao Seed 2.1 Lite",
|
|
38
|
+
"description": "Doubao Seed 2.1 Lite",
|
|
39
|
+
"default_reasoning_level": "low",
|
|
40
|
+
"supported_reasoning_levels": [
|
|
41
|
+
{
|
|
42
|
+
"effort": "low",
|
|
43
|
+
"description": "low"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"shell_type": "unified_exec",
|
|
47
|
+
"visibility": "list",
|
|
48
|
+
"supported_in_api": true,
|
|
49
|
+
"priority": 0,
|
|
50
|
+
"upgrade": null,
|
|
51
|
+
"base_instructions": "You are a helpful coding assistant. Follow the user instructions, inspect the workspace when needed, and explain results clearly.",
|
|
52
|
+
"support_verbosity": false,
|
|
53
|
+
"supports_reasoning_summaries": false,
|
|
54
|
+
"default_reasoning_summary": "none",
|
|
55
|
+
"supports_parallel_tool_calls": false,
|
|
56
|
+
"truncation_policy": {
|
|
57
|
+
"mode": "bytes",
|
|
58
|
+
"limit": 10000
|
|
59
|
+
},
|
|
60
|
+
"experimental_supported_tools": [],
|
|
61
|
+
"context_window": 1048576,
|
|
62
|
+
"input_modalities": [
|
|
63
|
+
"text",
|
|
64
|
+
"image"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ailuntz/minilink",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Use your Codex profiles in the Desktop model menu.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT AND Apache-2.0",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"bin/",
|
|
15
15
|
"src/",
|
|
16
|
+
"examples/",
|
|
16
17
|
"README.md",
|
|
17
18
|
"LICENSE"
|
|
18
19
|
],
|
package/src/picker.mjs
CHANGED
|
@@ -10,8 +10,8 @@ import { additionalModels } from './model-menu.mjs';
|
|
|
10
10
|
// Keep the selected model when Desktop substitutes its reserve model at quota.
|
|
11
11
|
// A pending choice belongs to the next user task that starts or sends a message.
|
|
12
12
|
export class Picker {
|
|
13
|
-
constructor({ bridge, getConfig, serialize, chats = [] } = {}) {
|
|
14
|
-
Object.assign(this, { bridge, getConfig, serialize, chats });
|
|
13
|
+
constructor({ bridge, getConfig, serialize, configRpc, chats = [] } = {}) {
|
|
14
|
+
Object.assign(this, { bridge, getConfig, serialize, configRpc, chats });
|
|
15
15
|
this.pins = new Map(); this.pending = new Map(); this.next = null;
|
|
16
16
|
}
|
|
17
17
|
request(message) {
|
|
@@ -52,7 +52,8 @@ export class Picker {
|
|
|
52
52
|
async state() {
|
|
53
53
|
this.request(null);
|
|
54
54
|
const models = await this.catalog();
|
|
55
|
-
|
|
55
|
+
const { config } = await this.configRpc('config/read', { includeLayers: false });
|
|
56
|
+
return { next: this.next, selected: { model: config.model, effort: config.model_reasoning_effort }, models: models.map(m => ({ model: m.model, name: m.displayName || m.model,
|
|
56
57
|
effort: m.defaultReasoningEffort, efforts: m.supportedReasoningEfforts || [] })) };
|
|
57
58
|
}
|
|
58
59
|
async choose({ model, effort }) {
|
|
@@ -62,6 +63,13 @@ export class Picker {
|
|
|
62
63
|
const supported = entry.supportedReasoningEfforts || [];
|
|
63
64
|
effort = supported.length ? (effort ?? entry.defaultReasoningEffort) : null;
|
|
64
65
|
if (supported.length && !supported.some(e => e.reasoningEffort === effort)) throw new Error(text('invalidEffort'));
|
|
66
|
+
await this.configRpc('config/batchWrite', {
|
|
67
|
+
edits: [
|
|
68
|
+
{ keyPath: 'model', value: model, mergeStrategy: 'upsert' },
|
|
69
|
+
{ keyPath: 'model_reasoning_effort', value: effort, mergeStrategy: 'upsert' }
|
|
70
|
+
],
|
|
71
|
+
reloadUserConfig: true
|
|
72
|
+
});
|
|
65
73
|
this.next = { model, effort };
|
|
66
74
|
return { ok: true };
|
|
67
75
|
}
|
package/src/proxy.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setting } from './legacy.mjs';
|
|
2
2
|
import { runtimeDirectory } from './runtime.mjs';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { spawn } from 'node:child_process';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
6
|
import { rmSync, existsSync, readFileSync } from 'node:fs';
|
|
@@ -102,7 +103,19 @@ export async function runProxy(args, selfPath) {
|
|
|
102
103
|
catch { route.error = `Profile ${route.profile} 的模型目录无法加载,请检查 model_catalog_json。`; }
|
|
103
104
|
}
|
|
104
105
|
};
|
|
105
|
-
|
|
106
|
+
const configRpc = async (method, params) => {
|
|
107
|
+
// Use the same alias translation and saved provider identity as Desktop.
|
|
108
|
+
const id = `minilink-picker-${randomUUID()}`;
|
|
109
|
+
const request = menu.request({ id, method, params });
|
|
110
|
+
try {
|
|
111
|
+
const result = await switches.rpc(method, request.params);
|
|
112
|
+
return menu.response({ id, result }).result;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
menu.response({ id, error: { message: error.message } });
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
Object.assign(picker, { bridge: switches, getConfig, serialize, refreshCatalogs, configRpc });
|
|
106
119
|
for (const chat of chats) Object.assign(chat, { bridge: switches, menu });
|
|
107
120
|
present = async message => {
|
|
108
121
|
picker.response(message);
|
package/src/terminal.mjs
CHANGED
|
@@ -2,18 +2,19 @@ import { createInterface } from 'node:readline/promises';
|
|
|
2
2
|
|
|
3
3
|
export const language = (env = process.env) => /^zh(?:[_-]|$)/i.test(env.LC_ALL || env.LC_MESSAGES || env.LANG || '') ? 'zh' : 'en';
|
|
4
4
|
const messages = {
|
|
5
|
-
help: ['minilink\n\n on
|
|
5
|
+
help: ['minilink\n\n on 开启 MiniLink\n link 选择模型和推理强度\n off 关闭额外选项和模型接管\n doctor 检查配置和运行状态\n\n读取现有 profiles。其他 Codex 参数原样透传。', 'minilink\n\n on Enable MiniLink\n link Select a model and reasoning effort\n off Disable extra models and overrides\n doctor Check configuration and status\n\nUses existing profiles. Other arguments are forwarded to Codex.'],
|
|
6
|
+
on: ['minilink on 已开启,请完全退出并重开 Desktop。', 'MiniLink is enabled. Fully quit and reopen Desktop.'],
|
|
6
7
|
off: ['已关闭。已有任务保留当前提供商。', 'Disabled. Existing tasks keep their current provider.'],
|
|
7
|
-
unavailable: ['
|
|
8
|
-
interactive: ['
|
|
8
|
+
unavailable: ['未连接到 Desktop。请在运行 Desktop 的电脑上完全退出并重开 Desktop,然后运行 minilink link。', 'No Desktop connection. On the computer hosting Desktop, fully quit and reopen Desktop, then run minilink link.'],
|
|
9
|
+
interactive: ['请在交互终端运行 minilink link;SSH 请使用 ssh -t。', 'Run minilink link in an interactive terminal; use ssh -t over SSH.'],
|
|
9
10
|
multiple: ['检测到多个 Desktop 连接。请只保留一个连接后重试。', 'Multiple Desktop connections are running. Keep one connection open and try again.'],
|
|
10
11
|
model: ['模型', 'Model'], effort: ['推理强度', 'Reasoning effort'],
|
|
11
12
|
prompt: ['输入编号', 'Enter a number'], invalid: ['请输入列表中的编号。', 'Enter a number from the list.'],
|
|
12
13
|
cancelled: ['已取消,未更改模型。', 'Cancelled. Model unchanged.'],
|
|
13
|
-
done: ['
|
|
14
|
+
done: ['已保存到 config.toml。请完全退出并重开 Desktop,让模型菜单加载已保存的选择。', 'Saved to config.toml. Fully quit and reopen Desktop to load the saved selection in the model menu.'],
|
|
14
15
|
empty: ['暂无可用模型,请检查连接后重试。', 'No models available. Check your connection and try again.'],
|
|
15
|
-
disconnected: ['连接已结束,请重新运行 minilink
|
|
16
|
-
expired: ['连接已失效,请重新运行 minilink
|
|
16
|
+
disconnected: ['连接已结束,请重新运行 minilink link。', 'Connection closed. Run minilink link again.'],
|
|
17
|
+
expired: ['连接已失效,请重新运行 minilink link。', 'Connection expired. Run minilink link again.'],
|
|
17
18
|
catalogLarge: ['模型目录过大,请检查配置。', 'Model catalog is too large. Check your configuration.'],
|
|
18
19
|
disabled: ['请先运行 minilink on。', 'Run minilink on first.'],
|
|
19
20
|
missingModel: ['模型已不在目录中,请重新选择。', 'Model is no longer available. Select again.'],
|
|
@@ -44,6 +45,7 @@ export async function selectModel(endpoints, { input = process.stdin, output = p
|
|
|
44
45
|
};
|
|
45
46
|
const state = await api('/state');
|
|
46
47
|
if (!state.models.length) throw new Error(t('empty'));
|
|
48
|
+
const preferred = state.next ?? state.selected;
|
|
47
49
|
const rl = createInterface({ input, output });
|
|
48
50
|
const abort = new AbortController();
|
|
49
51
|
rl.on('SIGINT', () => abort.abort());
|
|
@@ -62,10 +64,10 @@ export async function selectModel(endpoints, { input = process.stdin, output = p
|
|
|
62
64
|
throw new DOMException('Cancelled', 'AbortError');
|
|
63
65
|
};
|
|
64
66
|
try {
|
|
65
|
-
const model = await select(t('model'), state.models.map(m => ({ value: m.model, label: m.name })),
|
|
67
|
+
const model = await select(t('model'), state.models.map(m => ({ value: m.model, label: m.name })), preferred?.model);
|
|
66
68
|
const entry = state.models.find(m => m.model === model);
|
|
67
69
|
const efforts = entry.efforts || [];
|
|
68
|
-
const effort = efforts.length > 1 ? await select(t('effort'), efforts.map(e => ({ value: e.reasoningEffort, label: t(e.reasoningEffort) })),
|
|
70
|
+
const effort = efforts.length > 1 ? await select(t('effort'), efforts.map(e => ({ value: e.reasoningEffort, label: t(e.reasoningEffort) })), preferred?.model === model ? preferred.effort : entry.effort) : efforts[0]?.reasoningEffort ?? null;
|
|
69
71
|
if (abort.signal.aborted) return t('cancelled');
|
|
70
72
|
await api('/choose', { model, effort });
|
|
71
73
|
return t('done');
|