llm-api-gateway-cli 1.0.3 → 1.0.5
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.en.md +232 -0
- package/README.md +39 -16
- package/lib/hub.js +19 -8
- package/lib/launcher.js +1 -1
- package/package.json +2 -1
- package/public/app.js +18 -12
- package/public/index.html +15 -6
- package/public/manual.html +42 -7
- package/public/shell-bridge.js +100 -0
- package/public/shell.css +242 -0
- package/public/shell.html +73 -0
- package/public/shell.js +412 -0
- package/public/styles.css +81 -15
- package/public/task.css +70 -0
- package/public/task.html +19 -1
- package/public/task.js +217 -13
- package/public/theme.js +20 -0
- package/public/tint.js +5 -3
package/README.en.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# LLM API Gateway CLI — English entry
|
|
2
|
+
|
|
3
|
+
> **Language:** English (this page) · [中文(权威全文)](README.md)
|
|
4
|
+
>
|
|
5
|
+
> [`README.md`](README.md) is the authoritative, always-current document (Chinese, far more detail:
|
|
6
|
+
> every flag, every API route, Docker publishing, troubleshooting).
|
|
7
|
+
> This page is the English entry point: what the tool is, how to install it, and how to run it.
|
|
8
|
+
|
|
9
|
+
A local toolbox for an **OpenAI-compatible LLM gateway** (default `http://127.0.0.1:9000`).
|
|
10
|
+
It gives you five ways to talk to the gateway from your own machine, plus a task mode where the
|
|
11
|
+
model actually reads and writes files in a directory you pick:
|
|
12
|
+
|
|
13
|
+
| Mode | Entry point | What it does | Extra deps |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| 1 | `gateway-openai` (`cli-openai.js`) | OpenAI-compatible chat against `/v1/chat/completions` | `openai` SDK |
|
|
16
|
+
| 2 | `gateway-anthropic` (`cli-anthropic.js`) | Anthropic-compatible calls against `/v1/messages` | `@anthropic-ai/sdk` |
|
|
17
|
+
| 3 | `gateway-claude-code` (`cli-claude-code.js`) | Pipes through a locally installed `claude` CLI | local `claude` |
|
|
18
|
+
| 4 + 5 | `gateway-web` (`server.js`) · `gateway-task` (`task-server.js`) | Local Web UI: chat, task mode and manual in one page | none |
|
|
19
|
+
| 6 | `gateway-agent` (`cli-agent.js`) | Native agent CLI with tool calls (recommended) | none |
|
|
20
|
+
|
|
21
|
+
Modes 4, 5 and 6 are plain Node.js with no additional dependencies — the two SDKs above are only
|
|
22
|
+
needed by modes 1 and 2. Modes 4 and 5 run on **one port** (`3100`) as one service.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- **Node.js >= 18** (`node -v`). The installers check this and stop with a clear message; they never
|
|
27
|
+
install a runtime for you.
|
|
28
|
+
- A gateway you can reach, plus an API key for it. There is no bundled model — this is a client.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
Three routes, all producing the same artifact and the same commands.
|
|
33
|
+
|
|
34
|
+
### A. One-liner script (macOS / Linux / WSL)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Pin a release tag (recommended: the tag never moves)
|
|
38
|
+
curl -fsSL https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/<version>/scripts/install.sh | bash
|
|
39
|
+
|
|
40
|
+
# Or track the newest script from main (script and release contract may drift)
|
|
41
|
+
curl -fsSL https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/main/scripts/install.sh | bash
|
|
42
|
+
|
|
43
|
+
# Passing arguments through a pipe needs -s --
|
|
44
|
+
curl -fsSL https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/<version>/scripts/install.sh \
|
|
45
|
+
| bash -s -- --version v1.0.0 --prefix "$HOME/.llm-api-gateway"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### B. One-liner script (Windows PowerShell 5.1 / 7)
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
irm https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/<version>/scripts/install.ps1 | iex
|
|
52
|
+
|
|
53
|
+
# Passing arguments needs the scriptblock form
|
|
54
|
+
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/<version>/scripts/install.ps1))) -Version v1.0.0 -DryRun
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### C. npm
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install -g llm-api-gateway-cli
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The installers put everything under `~/.llm-api-gateway` (Windows: `$HOME\.llm-api-gateway`), add the
|
|
64
|
+
bin directory to your `PATH`, are idempotent, and need no admin rights. Useful flags:
|
|
65
|
+
`--prefix`, `--no-modify-path`, `--dry-run`, `--force`, `--base` (mirror).
|
|
66
|
+
|
|
67
|
+
### Already have the sources
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/boonya-hrgk/llm-api-gateway-cli
|
|
71
|
+
cd llm-api-gateway-cli
|
|
72
|
+
npm install
|
|
73
|
+
npm link # symlink, so edits take effect without reinstalling
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
You can also skip installing entirely inside the repo: `node cli-agent.js`, `node server.js`.
|
|
77
|
+
|
|
78
|
+
## First-time setup
|
|
79
|
+
|
|
80
|
+
You do **not** need to hand-write `.env` any more. Either run the guided setup:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
gateway-agent setup # asks for gateway URL, then the key (hidden input)
|
|
84
|
+
gateway-agent setup --base-url http://127.0.0.1:9000 --key sk-xxx # non-interactive
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
or set the key from the Web UI (`Settings -> gateway key`) after starting the service — the server
|
|
88
|
+
**starts without a key on purpose**, so the page is reachable and you have somewhere to paste it.
|
|
89
|
+
|
|
90
|
+
The key is written to `~/.llm-api-gateway-cli/credentials.json` (owner-readable only) and is
|
|
91
|
+
**never stored in `config.json`**. Sources are resolved in this order, first hit wins:
|
|
92
|
+
|
|
93
|
+
1. `--key sk-xxx` on the command line
|
|
94
|
+
2. environment variables: `GATEWAY_KEY`, `SK`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`
|
|
95
|
+
3. `credentials.json` (what the UI / `config set key` writes)
|
|
96
|
+
4. `.env` in the install directory, then in the current working directory
|
|
97
|
+
|
|
98
|
+
When the key comes from the environment, `credentials.json` is ignored — the startup banner always
|
|
99
|
+
prints which source is in effect, so you never have to guess.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
gateway-agent config set baseUrl http://127.0.0.1:9000
|
|
103
|
+
gateway-agent config set model qwen3:8b
|
|
104
|
+
gateway-agent config list # every value and where it comes from
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Quick start
|
|
108
|
+
|
|
109
|
+
### Command line
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
gateway-agent -p "explain this stack trace" # one-shot
|
|
113
|
+
gateway-agent -i # interactive REPL
|
|
114
|
+
gateway-agent # bare command: asks chat/task, CLI/web
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Bare `llm-api-gateway-cli` (or `gateway-agent`) with no arguments asks two questions — what to do
|
|
118
|
+
(chat / task) and where to run it (terminal / Web UI) — instead of silently picking one. Passing
|
|
119
|
+
`-p`, `-i`, or piping stdin skips the menu.
|
|
120
|
+
|
|
121
|
+
Slash commands are available inside the REPL (`/help` lists them; the CLI REPL has 10 of them).
|
|
122
|
+
|
|
123
|
+
### Web UI — one page, three tabs
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
gateway-web # or: node server.js
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
| Page | URL | Notes |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| Tab shell (recommended entry) | `http://127.0.0.1:3100/` | Chat / Task / Manual switch in one browser tab |
|
|
132
|
+
| Chat panel | `http://127.0.0.1:3100/chat` | Pure conversation, no file system access |
|
|
133
|
+
| Task panel | `http://127.0.0.1:3100/task` | Pick a directory, the model really reads/writes it |
|
|
134
|
+
| Manual | `http://127.0.0.1:3100/manual` | Operation manual (Chinese) generated from the live command tables |
|
|
135
|
+
|
|
136
|
+
The shell owns the single top bar (tabs, per-panel actions, theme, reload); panels are loaded
|
|
137
|
+
lazily on first activation and keep their state when you switch away. `Alt+1/2/3` switches tabs.
|
|
138
|
+
Each panel also opens standalone, which is what external systems embed.
|
|
139
|
+
|
|
140
|
+
Useful flags: `--port`, `--host`, `--store <dir>`, `--mode manual|auto|plan`, `--max-steps`,
|
|
141
|
+
`--allow-bash`, `--allow-remote-fs`, `--verbose` (`node server.js --help`).
|
|
142
|
+
|
|
143
|
+
### Task mode in one minute
|
|
144
|
+
|
|
145
|
+
1. Open the Task tab and pick a **working directory** — the model's file tools are confined to it.
|
|
146
|
+
2. Describe the job, press `Enter`. The model lists directories, reads files and searches content.
|
|
147
|
+
3. In **manual** mode every write shows an approval card first; click approve/reject (or `/approve`,
|
|
148
|
+
`/reject`). **auto** executes writes directly. **plan** gives the model read-only tools, writes a
|
|
149
|
+
plan document into `docs/` inside your working directory, and waits for you to confirm.
|
|
150
|
+
4. `/stop` or the Stop button cancels the current run; other tasks keep going in the background.
|
|
151
|
+
|
|
152
|
+
Tools the model can use: `list_dir`, `read_file`, `search_files`, `glob`, `grep`, `write_file`,
|
|
153
|
+
`apply_patch`. `bash` exists but is **off by default** — enable it with `--allow-bash` (dangerous:
|
|
154
|
+
in auto mode commands are not confirmed one by one). The task page has 14 slash commands; the manual
|
|
155
|
+
page lists all of them side by side with the CLI.
|
|
156
|
+
|
|
157
|
+
Task data is stored on disk by the server (not in the browser): `<data dir>/tasks/<id>.json` plus a
|
|
158
|
+
model-side session per task. Default retention is a sliding **15 days / at most 20 tasks**.
|
|
159
|
+
|
|
160
|
+
## Configuration and storage
|
|
161
|
+
|
|
162
|
+
| Env var | Purpose |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `GATEWAY_BASE_URL` | Gateway base URL (default `http://127.0.0.1:9000`) |
|
|
165
|
+
| `GATEWAY_MODEL` | Default model (built-in default `qwen3:8b`) |
|
|
166
|
+
| `GATEWAY_KEY` / `SK` | Gateway API key |
|
|
167
|
+
| `WEB_PORT` / `TASK_WEB_PORT` | Web UI port (default `3100`) |
|
|
168
|
+
| `TASK_MODE` | Default approval mode for new tasks: `manual` / `auto` / `plan` |
|
|
169
|
+
| `TASK_MAX_STEPS` | Max model turns per task |
|
|
170
|
+
| `TASK_STORE_DIR` | Where task records live |
|
|
171
|
+
| `TASK_HISTORY_MAX_CHARS` | History budget sent to the model |
|
|
172
|
+
| `TASK_SESSION_MAX_BYTES` | Cap for one task's model-side session file |
|
|
173
|
+
| `LLM_GATEWAY_DATA_DIR` | Move the whole data root (config + credentials + tasks) |
|
|
174
|
+
|
|
175
|
+
| File | Contents |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `~/.llm-api-gateway-cli/config.json` | Settings (never the key) |
|
|
178
|
+
| `~/.llm-api-gateway-cli/credentials.json` | The key, owner-readable only |
|
|
179
|
+
| `~/.llm-api-gateway-cli/tasks/` | Task records + per-task sessions |
|
|
180
|
+
|
|
181
|
+
Precedence everywhere is **command-line flag > environment variable > `.env` > `config.json` >
|
|
182
|
+
built-in default**, and every surface (CLI, REPL, Web UI) reads and writes the same files.
|
|
183
|
+
|
|
184
|
+
## Embedding and theming
|
|
185
|
+
|
|
186
|
+
Any page can be embedded in another system, and you can hand it the host's background colour so it
|
|
187
|
+
does not look pasted on:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
http://127.0.0.1:3100/task?bg=%23f0f4ff light host background
|
|
191
|
+
http://127.0.0.1:3100/task?bg=%23111a2b dark host background
|
|
192
|
+
http://127.0.0.1:3100/task?bg=none back to the page's own palette
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The theme is derived from that one colour (panels, borders, text and accent all follow), and the
|
|
196
|
+
theme switcher hides itself because the host now decides.
|
|
197
|
+
|
|
198
|
+
## Docker
|
|
199
|
+
|
|
200
|
+
A `Dockerfile`, `docker-compose.yml` and `docker.ps1` helper are included: one image containing the
|
|
201
|
+
Web UI, with your project mounted as the agent's working directory and task history on a named
|
|
202
|
+
volume. See the [Chinese README](README.md#docker-容器化部署) for the full walkthrough
|
|
203
|
+
(`.\docker.ps1 up`, publishing to ACR / Docker Hub, troubleshooting).
|
|
204
|
+
|
|
205
|
+
## Security notes
|
|
206
|
+
|
|
207
|
+
- The API key stays on the server: the browser never receives it (the config endpoint returns a mask
|
|
208
|
+
and its source only).
|
|
209
|
+
- Directory browsing, task records and the settings API are **only served when the service is bound
|
|
210
|
+
to loopback**. Binding `0.0.0.0` requires the explicit `--allow-remote-fs` flag.
|
|
211
|
+
- The agent's sandbox is the working directory you picked: path traversal, absolute paths and
|
|
212
|
+
symlink escapes are rejected, and `bash` is off unless you pass `--allow-bash`.
|
|
213
|
+
|
|
214
|
+
## Tests
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
npm test # offline suite (no gateway needed)
|
|
218
|
+
npm run test:all # offline + tests that call a real gateway on 127.0.0.1:9000
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Where to read more
|
|
222
|
+
|
|
223
|
+
- **`/manual`** in the running Web UI — install, first-time setup, task workflow, every slash command
|
|
224
|
+
on all three surfaces (its command tables are generated from `lib/commands.js` and
|
|
225
|
+
`public/task-slash.js`, so they cannot go stale).
|
|
226
|
+
- [`README.md`](README.md) — the authoritative Chinese document: all flags, every API route, storage
|
|
227
|
+
internals, Docker publishing, FAQ.
|
|
228
|
+
- [`docs/`](docs/) — iteration plans and release steps (Chinese).
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# LLM API Gateway CLI 编程工具
|
|
2
2
|
|
|
3
|
+
> 🌐 **English entry:** [`README.en.md`](README.en.md) —— 英文入口(是什么 / 怎么装 / 怎么跑)。
|
|
4
|
+
> 本页是**权威全文**(中文,最全):所有参数、全部接口、存储内部细节、Docker 发布与排错。
|
|
5
|
+
|
|
3
6
|
验证 `llm-api-gateway`(`http://127.0.0.1:9000`)除了 Web 对话页 / SDK 之外,**CLI 方式同样可用**。多个脚本覆盖多种接入方言,其中**原生 Agent(`cli-agent.js`)是终端里干活的推荐方式** —— 自带工具循环,无需安装 Claude Code。
|
|
4
7
|
|
|
5
8
|
| 脚本 | 模式 | 走网关的端点 | 依赖 |
|
|
@@ -7,7 +10,7 @@
|
|
|
7
10
|
| `cli-openai.js` | 模式一:OpenAI 兼容 | `/v1/chat/completions` | `openai` SDK |
|
|
8
11
|
| `cli-anthropic.js` | 模式二:Anthropic 兼容 | `/v1/messages` | `@anthropic-ai/sdk` |
|
|
9
12
|
| `cli-claude-code.js` | 模式三:Claude Code | `/v1/messages`(透传 claude CLI) | 本机已装 `claude` |
|
|
10
|
-
| `server.js` | 本地 Web UI
|
|
13
|
+
| `server.js` | 本地 Web UI 入口:聊天 / 任务 / 手册在**同一个页面**里用顶部 Tab 切换(`/`),三个面板也能各自单独打开 | `/v1/chat/completions`(浏览器 → 本机服务 → 网关)+ 任务侧的工具调用 | 无(零新增依赖) |
|
|
11
14
|
| `task-server.js` | 同一个服务的别名入口,直接落到 `/task` | 同左 | 无(零新增依赖) |
|
|
12
15
|
| `cli-agent.js` | 模式六:原生 Agent CLI(推荐) | `/v1/chat/completions` + 工具调用 | 无(零新增依赖) |
|
|
13
16
|
|
|
@@ -188,12 +191,21 @@ node cli-claude-code.js --config # 打印 CC Switch / CC GUI 可粘
|
|
|
188
191
|
|
|
189
192
|
`server.js` 用 `node:http` 起一个**只监听本机**的小服务,浏览器打开即是一个聊天界面:多轮对话、流式打字机输出、Markdown 渲染、模型切换、系统提示词、token 用量统计。请求链路是「浏览器 → 本机 3100 → 网关 9000」,`sk-` 密钥只留在服务端,不下发到页面。
|
|
190
193
|
|
|
191
|
-
|
|
194
|
+
**聊天、任务、操作手册现在是一个服务、一个端口、一个页面**:打开 `http://127.0.0.1:3100/` 就是顶部 Tab 外壳,
|
|
195
|
+
在**聊天 / 任务 / 手册**之间切换(不再新开标签页,切走的面板状态不丢,地址栏同步 `#chat` / `#task` / `#manual`);
|
|
196
|
+
三个面板也都能单独打开(外部系统 iframe 嵌入、`?bg=` 底色照旧):
|
|
192
197
|
|
|
193
|
-
| 页面 | 地址 |
|
|
194
|
-
| --- | --- |
|
|
195
|
-
|
|
|
196
|
-
|
|
|
198
|
+
| 页面 | 地址 | 说明 |
|
|
199
|
+
| --- | --- | --- |
|
|
200
|
+
| Tab 外壳(推荐入口) | http://127.0.0.1:3100/ | 聊天 / 任务 / 手册 同页切换;面板**第一次切过去才加载**(懒加载),后台在跑的任务在 Tab 上有脉冲点 |
|
|
201
|
+
| 聊天(模式四) | http://127.0.0.1:3100/chat | 纯对话,不碰文件系统(老链接 `/index.html` 也还是它) |
|
|
202
|
+
| 任务(模式五) | http://127.0.0.1:3100/task | 选一个目录,模型真读写 |
|
|
203
|
+
| 操作手册 | http://127.0.0.1:3100/manual | 同一份手册,见下文「操作手册页」 |
|
|
204
|
+
|
|
205
|
+
外壳顶栏就是唯一一条顶栏(品牌 / Tab / 当前面板的操作按钮 / 主题 / ⟳ 重载当前面板):面板自带的顶栏在嵌入时收起,
|
|
206
|
+
那些按钮(☰ 侧栏开关、工作目录、设置、新对话)仍是面板里的真按钮,外壳只负责「代点」。
|
|
207
|
+
快捷键 `Alt+1/2/3` 切面板。三个面板各自完整、互不干扰(同源 iframe),所以两边都能同时用;
|
|
208
|
+
主题切换在同源文档间即时同步(`theme.js` 监听 `storage`)。
|
|
197
209
|
|
|
198
210
|
`npm run web` 和 `npm run task`(`server.js` / `task-server.js`)起的是**同一个服务**,两个命令等价;保留两个名字只是为了不打断已有习惯和脚本。旧的 3101 不再使用。
|
|
199
211
|
|
|
@@ -489,8 +501,10 @@ node task-server.js --mode auto # 默认审批模式:manual / auto
|
|
|
489
501
|
|
|
490
502
|
| 接口 | 作用 |
|
|
491
503
|
| --- | --- |
|
|
492
|
-
| `GET /` |
|
|
493
|
-
| `GET /
|
|
504
|
+
| `GET /` | Tab 外壳:聊天 / 任务 / 手册在同一个页面里用顶部 Tab 切换(`/shell.html` 同页) |
|
|
505
|
+
| `GET /chat` | 聊天面板(单独打开;老链接 `/index.html` 也是它) |
|
|
506
|
+
| `GET /task` | 任务面板(单独打开,选目录、模型真读写) |
|
|
507
|
+
| `GET /manual` | 操作手册面板(安装与启动 + 任务流程 + 三端斜杠指令对照) |
|
|
494
508
|
| `GET /api/commands` | 三端(命令行 / 本机聊天页 / 任务页)指令的机器可读清单,手册页据此生成,不手抄 |
|
|
495
509
|
| `GET /api/cost?prompt=&completion=&model=` | 费用粗估(与 CLI `/cost` 同一份单价表 `lib/pricing.js`) |
|
|
496
510
|
| `GET /api/config` | 网关地址、模型、工具清单、上限、密钥是否就绪(只回脱敏值 + 来源 + 密钥文件路径) |
|
|
@@ -605,14 +619,22 @@ node task-server.js --mode auto # 默认审批模式:manual / auto
|
|
|
605
619
|
|
|
606
620
|
> **安全边界**:任务记录里含工具读过的文件正文,属于本机数据,所以存储接口和目录浏览一样**只在服务绑定本机时开放**,绑定 `0.0.0.0` 需显式 `--allow-remote-fs`。另外,如果你把工作目录正好选成了存储目录所在的位置(比如回退到 `.tasks/` 时选了本项目),模型的文件工具就能读到任务历史 —— 服务端检测到这种情况会在状态栏提示。
|
|
607
621
|
|
|
608
|
-
> 与聊天的区别:聊天没有工作目录、不碰文件系统,是纯对话;任务是「选目录 →
|
|
622
|
+
> 与聊天的区别:聊天没有工作目录、不碰文件系统,是纯对话;任务是「选目录 → 干活」。两者是 `http://127.0.0.1:3100/` 这个 Tab 外壳里的两个面板(也能用 `/chat`、`/task` 单独打开),可以同时用,顶部 Tab 互相切换。
|
|
623
|
+
|
|
624
|
+
三个面板之间不用记端口,也不用新开页面:
|
|
625
|
+
|
|
626
|
+
- 顶栏 Tab **聊天 / 任务 / 手册** 直接切(`Alt+1/2/3`);
|
|
627
|
+
- **导航只归外壳**:聊天面板里不再自带通往任务页的入口(原先那个「+ 新任务 ↗」已删除),要开任务就点外壳的「任务」Tab;
|
|
628
|
+
- 任务页顶栏仍保留「聊天 ↗」入口,方便 `/task` 单开(外部 iframe 嵌入)时用。
|
|
609
629
|
|
|
610
|
-
|
|
630
|
+
### 任务多了 / 历史长了:懒加载
|
|
611
631
|
|
|
612
|
-
-
|
|
613
|
-
-
|
|
632
|
+
- **侧边栏**默认只渲染前 30 条任务,滚到底自动续载,也可以点「加载更多(还有 N 条)」;被截断的目录分组会标注「本组还有 N 条」;
|
|
633
|
+
- 侧边栏搜索框按**任务标题 / 工作目录**过滤(纯客户端,不发请求),`Esc` 清空;
|
|
634
|
+
- **消息历史**默认只渲染最近 30 条,往上翻点顶部「载入更早的消息」**前插**(视口不跳、斜杠指令输出不会被清掉);
|
|
635
|
+
- 磁盘上的任务与消息始终是完整的:懒加载只少建 DOM,不改落盘、发送给模型的内容与批准流程。
|
|
614
636
|
|
|
615
|
-
|
|
637
|
+
任务页顶栏那个「聊天 ↗」用同源相对路径,通过局域网 IP 访问时不会跳回 `127.0.0.1`;端口若改过,可用 `?chat=http://host:port/` 覆盖它。
|
|
616
638
|
|
|
617
639
|
## 模式六 · 原生 Agent CLI(推荐,不依赖 Claude Code)
|
|
618
640
|
|
|
@@ -706,7 +728,7 @@ node cli-agent.js -i --resume <会话id> # 切到指定
|
|
|
706
728
|
|
|
707
729
|
模式五(Web)与模式六(CLI)共用同一套实现,避免两处各写一遍后行为漂移:
|
|
708
730
|
|
|
709
|
-
- `lib/hub.js`:**唯一的 Web 服务实现** —— 静态文件(`/` →
|
|
731
|
+
- `lib/hub.js`:**唯一的 Web 服务实现** —— 静态文件(`/` → Tab 外壳、`/chat` → 聊天面板、`/task` → 任务面板、`/manual` → 操作手册面板)、两页共用的 `/api/config` 与 `/api/models`、手册页数据 `/api/commands` 与 `/api/cost`、聊天的 `/api/chat`、任务的全部接口都在这里;`server.js` 与 `task-server.js` 只是它的两个薄入口。
|
|
710
732
|
- `lib/tools.js`:八个工具(`list_dir` / `read_file` / `search_files` / `glob` / `grep` / `apply_patch` / `write_file` / `bash`)+ 沙箱(`realpath` 符号链接逃逸检查)+ 写入预览 `writePreview`;`bash` 由 `setBashEnabled` 控制开关,未开启时 `availableTools()` 直接把它摘掉。
|
|
711
733
|
- `lib/agent.js`:模型 → 工具 → 回填 → 再问的循环、SSE 解析、按 `index` 拼接 `tool_calls`、挂起/批准/续跑、带退避的 `fetchWithRetry`;
|
|
712
734
|
- `lib/runner.js`:把上面两者包成「建会话 → 跑一轮 → 遇写入挂起 → 等批准 → 断点续跑」的传输无关流程,Web 与 CLI 都基于它(`createSession` / `restoreSession` / `pushUser` / `runTurn` / `resumePendingTurn` / `buildTaskSystemPrompt`);
|
|
@@ -755,7 +777,8 @@ npm run test:all # 先离线再联网
|
|
|
755
777
|
| `tests/taskstore.test.mjs` | 磁盘存储(`lib/taskstore.js`):路径探测与降级、id 防目录穿越、增删改查、15 天/20 条清理、原子写入、索引损坏重建、任务文件损坏容错、脏数据清洗、重开进程后数据仍在;以及**没改东西的保存不算更新**(时间戳不动、文件不重写,真变化才更新,任务文件丢了会补写) |
|
|
756
778
|
| `tests/task-store.test.mjs` | 任务页客户端逻辑:调接口的路径与方法对不对、草稿态不建任务、`ensureTask` 幂等、自动命名与重命名保护、从浏览器旧数据迁移、接口报错时有可读提示 |
|
|
757
779
|
| `tests/page-runtime.test.mjs` | 把 `task.js` 放进最小 DOM 垫片 + 假服务端里真跑一遍:`init()` 不抛异常、侧边栏渲染、**按工作目录分组**(同目录多任务、分组 `+` 新建、折叠/展开、切过去自动展开)、**模型下拉的选项来自 `/api/models`**(配置里的模型不在列表里时保留并选中、拉不到列表时只剩默认+当前值并说明原因)、存储路径展示、点「新任务」/切换/删除的完整请求路径、迁移浏览器旧数据、服务端报错时页面不崩;用「卡住的流」验证**切走不中断**(后台跑完并落盘到它自己的历史)、**切回走缓存不重新拉**、看别的任务时输入不被禁用;**点标题只换视图**(切走与切回都不产生 PUT、时间戳原封不动,内容真变了才落盘、同内容连存两次只发一次请求) |
|
|
758
|
-
| `tests/theme.test.mjs` | 主题与单端口契约:两套 CSS 里没有裸颜色、浅色覆盖了深色的**每一个**颜色变量、`task.css`
|
|
780
|
+
| `tests/theme.test.mjs` | 主题与单端口契约:两套 CSS 里没有裸颜色、浅色覆盖了深色的**每一个**颜色变量、`task.css` 与 `shell.css` 用到的变量都有定义、四个页面都在 `<head>` 里内联防闪白脚本且 key 与 `theme.js` 一致、`theme.js` 在 DOM 垫片里真跑(跟随系统 / 切换 / 记住 / 非法值容错 / 点按钮生效 / 系统变化在跟随模式下生效、明确选过后不再被覆盖、**监听 `storage` 让同源的面板与多标签页跟着变**)、`server.js` 与 `task-server.js` 是同一服务的薄入口、`/` 是 Tab 外壳而三个面板各有独立路由、两套接口都在同一个 handler 里、存储路径只在放行时才进配置 |
|
|
781
|
+
| `tests/shell.test.mjs` | Tab 外壳:外壳页结构(三个 Tab、唯一顶栏、面板区为空容器 = iframe 懒加载)、`shell.js` 的懒加载与桥协议(用到才建 iframe、切走只 hidden、只认同源 origin、只认已登记的面板窗口、代点 `{type:click}` 与 `{type:cmd}`、没就绪先排队、`#tab` 同步、`Alt+1/2/3`)、**用 DOM 垫片真跑一遍**(懒加载顺序、点外壳按钮发出正确的代点消息、面板上行状态驱动外壳顶栏、跨源/未登记窗口的消息被忽略、load 后补发排队指令)、`shell-bridge.js` 只在 `?embed=1` 且真在 iframe 里才生效(单独打开时一行都不动)、三个面板的 `data-embed` 适配、任务侧边栏与消息历史懒加载的源码契约,以及真起服务后 `/`、`/chat`、`/task`、`/manual`、`/shell.html`、`/shell.css`、`/shell-bridge.js` 各自可达 |
|
|
759
782
|
| `tests/tint.test.mjs` | 外部集成配色(`?bg=`):两页都在首次绘制前阻塞接上 `tint.js`、颜色解析(hex / rgb / hsl / 具名色,以及 `;`、`url()`、`var()` 这类注入写法一律拒绝)、**传默认深/浅底推导结果贴住默认那一套配色**、任意底色下的层次关系(深底加亮、浅底更亮、描边始终可见)、垫片里真跑(参数优先 / 同标签页记忆 / `?bg=none` 清除 / 认不出就忽略 / `sessionStorage` 不可用也能用 / 页内跳转带参数)、`theme.js` 遇到底色让位 |
|
|
760
783
|
| `tests/chat.test.mjs` | 聊天端到端:静态资源、目录穿越防护、真实流式对话(含思维链)、错误处理、配置不下发真实密钥与本机路径 |
|
|
761
784
|
| `tests/live.test.mjs` | 任务端到端:真实模型调用工具读写真实文件、批准后落盘、拒绝后不落盘、过期运行态返回 410 |
|
|
@@ -847,7 +870,7 @@ history.maxChars 60000 内置默认
|
|
|
847
870
|
|
|
848
871
|
## Docker 容器化部署
|
|
849
872
|
|
|
850
|
-
把整个应用打成一个镜像,用 Docker Desktop
|
|
873
|
+
把整个应用打成一个镜像,用 Docker Desktop 起一个容器就能用:Tab 外壳 `/`(聊天 / 任务 / 手册同页切换,也能用 `/chat`、`/task` 单独打开)就在里面,Agent 的「工作目录」映射到宿主机的某个目录。**本节命令全部是 Windows PowerShell 写法。**
|
|
851
874
|
|
|
852
875
|
| 新增文件 | 作用 |
|
|
853
876
|
| --- | --- |
|
package/lib/hub.js
CHANGED
|
@@ -126,10 +126,13 @@ const STORE_SOURCE_TEXT = {
|
|
|
126
126
|
|
|
127
127
|
const HELP = `用法:node server.js [选项]
|
|
128
128
|
|
|
129
|
-
本地 Web UI
|
|
129
|
+
本地 Web UI:聊天(模式四)、任务(模式五)、操作手册跑在同一个端口、同一个页面里,
|
|
130
|
+
顶部 Tab 切换(不再新开标签页);三个面板也能各自单独打开。
|
|
130
131
|
|
|
131
|
-
http://127.0.0.1:${DEFAULT_PORT}/ 聊天
|
|
132
|
-
http://127.0.0.1:${DEFAULT_PORT}/
|
|
132
|
+
http://127.0.0.1:${DEFAULT_PORT}/ 聊天 / 任务 / 手册(Tab 外壳)
|
|
133
|
+
http://127.0.0.1:${DEFAULT_PORT}/chat 只打开聊天面板
|
|
134
|
+
http://127.0.0.1:${DEFAULT_PORT}/task 只打开任务面板(选目录,模型真读写)
|
|
135
|
+
http://127.0.0.1:${DEFAULT_PORT}/manual 只打开操作手册
|
|
133
136
|
|
|
134
137
|
选项:
|
|
135
138
|
--port <端口> 监听端口(默认 ${DEFAULT_PORT},也可用 WEB_PORT / TASK_WEB_PORT)
|
|
@@ -189,9 +192,17 @@ const MIME = {
|
|
|
189
192
|
'.png': 'image/png',
|
|
190
193
|
};
|
|
191
194
|
|
|
192
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* 路径 → public/ 下的文件。每个页面各有自己的入口。
|
|
197
|
+
*
|
|
198
|
+
* `/` 从 20260922 起是 **Tab 外壳**(shell.html):聊天 / 任务 / 手册在一个页面里切换,
|
|
199
|
+
* 不再新开标签页。三个面板本身仍是完整页面,`/chat`、`/task`、`/manual` 单独打开时的
|
|
200
|
+
* 行为与以前一模一样(外部系统 iframe 嵌入、`?bg=` 底色都照旧)。
|
|
201
|
+
*/
|
|
193
202
|
function pageFile(urlPath) {
|
|
194
|
-
if (urlPath === '/' || urlPath === '/
|
|
203
|
+
if (urlPath === '/' || urlPath === '/shell' || urlPath === '/shell.html') return 'shell.html';
|
|
204
|
+
if (urlPath === '/chat' || urlPath === '/chat/' || urlPath === '/chat.html') return 'index.html';
|
|
205
|
+
if (urlPath === '/index.html') return 'index.html'; // 老链接:直接给聊天页
|
|
195
206
|
if (urlPath === '/task' || urlPath === '/task/' || urlPath === '/task.html') return 'task.html';
|
|
196
207
|
if (urlPath === '/manual' || urlPath === '/manual/' || urlPath === '/manual.html') return 'manual.html';
|
|
197
208
|
return null;
|
|
@@ -1460,8 +1471,8 @@ export function runHub(argv, { entry = 'web' } = {}) {
|
|
|
1460
1471
|
server.listen(port, host, () => {
|
|
1461
1472
|
const s = store.stats();
|
|
1462
1473
|
console.log('LLM API Gateway · 本地 Web UI');
|
|
1463
|
-
console.log(`
|
|
1464
|
-
console.log(`
|
|
1474
|
+
console.log(` 页面 http://${host}:${port}/(聊天 / 任务 / 手册,顶部 Tab 切换)`);
|
|
1475
|
+
console.log(` 面板 /chat 聊天 · /task 任务 · /manual 手册(都能单独打开)`);
|
|
1465
1476
|
// 网关 / 模型也要带来源:新机器上「有值」不等于「配过」——默认值、当前目录的 .env、
|
|
1466
1477
|
// config.json 三者长得很像,不说清来源就只能靠猜(与 CLI 横幅同一套文案)
|
|
1467
1478
|
console.log(` 网关 ${baseUrl}(${settingSourceText(settings, 'baseUrl')})`);
|
|
@@ -1490,7 +1501,7 @@ export function runHub(argv, { entry = 'web' } = {}) {
|
|
|
1490
1501
|
console.log(` 轮次 单个任务最多 ${cfg.maxSteps} 轮模型调用(--max-steps 可调);撞到上限会先收尾给结论,再给「继续执行」`);
|
|
1491
1502
|
console.log(' 写入按当前模式处理:手动逐个批准 / 自动直接执行 / 计划只读先出方案。');
|
|
1492
1503
|
if (entry === 'task') {
|
|
1493
|
-
console.log(' 提示
|
|
1504
|
+
console.log(' 提示 / 是 Tab 外壳(聊天 / 任务 / 手册同页切换),/task 直接落到任务面板。');
|
|
1494
1505
|
}
|
|
1495
1506
|
});
|
|
1496
1507
|
|
package/lib/launcher.js
CHANGED
|
@@ -25,7 +25,7 @@ export const TOPICS = [
|
|
|
25
25
|
|
|
26
26
|
export const SURFACES = [
|
|
27
27
|
{ value: 'cli', label: '命令行窗口', desc: '就在这个终端里继续' },
|
|
28
|
-
{ value: 'web', label: '网页 UI', desc: `起本机网页 http://127.0.0.1:${WEB_PORT}
|
|
28
|
+
{ value: 'web', label: '网页 UI', desc: `起本机网页 http://127.0.0.1:${WEB_PORT}(聊天 / 任务 / 手册,顶部 Tab 切换)` },
|
|
29
29
|
];
|
|
30
30
|
|
|
31
31
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-api-gateway-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "CLI 测试工具:验证 LLM API Gateway(http://127.0.0.1:9000)的 OpenAI / Anthropic / 原生 Agent / Claude Code 多种接入方式",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"cli-claude-code.js",
|
|
44
44
|
"server.js",
|
|
45
45
|
"task-server.js",
|
|
46
|
+
"README.en.md",
|
|
46
47
|
"lib/",
|
|
47
48
|
"!lib/.tasks/",
|
|
48
49
|
"public/",
|
package/public/app.js
CHANGED
|
@@ -44,7 +44,6 @@ const dom = {
|
|
|
44
44
|
send: document.getElementById('btn-send'),
|
|
45
45
|
stop: document.getElementById('btn-stop'),
|
|
46
46
|
clear: document.getElementById('btn-clear'),
|
|
47
|
-
btnOpenTask: document.getElementById('btn-open-task'),
|
|
48
47
|
status: document.getElementById('status'),
|
|
49
48
|
dot: document.getElementById('status-dot'),
|
|
50
49
|
meta: document.getElementById('gateway-meta'),
|
|
@@ -93,6 +92,8 @@ function setStatus(text, kind = '') {
|
|
|
93
92
|
|
|
94
93
|
function setDot(kind) {
|
|
95
94
|
dom.dot.className = `dot${kind ? ' ' + kind : ''}`;
|
|
95
|
+
// 被 Tab 外壳嵌着时,状态点也在外壳顶栏上(本页顶栏是收起的),顺手报一份过去
|
|
96
|
+
if (window.lgwBridge) window.lgwBridge.dot(kind || '');
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/**
|
|
@@ -485,9 +486,21 @@ function startDraft() {
|
|
|
485
486
|
dom.input.focus();
|
|
486
487
|
}
|
|
487
488
|
|
|
489
|
+
/** 侧边栏当前是不是开着:桌面看 .collapsed,窄屏看 .open(口径只写这一处) */
|
|
490
|
+
function sidebarOpen() {
|
|
491
|
+
const mobile = window.matchMedia('(max-width: 820px)').matches;
|
|
492
|
+
return mobile ? dom.sidebar.classList.contains('open') : !dom.sidebar.classList.contains('collapsed');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** 被 Tab 外壳嵌着(/)时,外壳顶栏上的 ☰ 要有按下态;单独打开本页时这行是空操作 */
|
|
496
|
+
function notifySidebar() {
|
|
497
|
+
if (window.lgwBridge) window.lgwBridge.sidebar(sidebarOpen());
|
|
498
|
+
}
|
|
499
|
+
|
|
488
500
|
function toggleSidebar() {
|
|
489
501
|
if (window.matchMedia('(max-width: 820px)').matches) dom.sidebar.classList.toggle('open');
|
|
490
502
|
else dom.sidebar.classList.toggle('collapsed');
|
|
503
|
+
notifySidebar();
|
|
491
504
|
}
|
|
492
505
|
|
|
493
506
|
/* ---------- 渲染 ---------- */
|
|
@@ -588,6 +601,8 @@ function setBusy(busy) {
|
|
|
588
601
|
state.streaming = busy;
|
|
589
602
|
dom.send.disabled = busy;
|
|
590
603
|
dom.stop.hidden = !busy;
|
|
604
|
+
// 切到别的 Tab 也能看出「聊天这边还在跑」(外壳 Tab 上的脉冲点)
|
|
605
|
+
if (window.lgwBridge) window.lgwBridge.busy(busy);
|
|
591
606
|
}
|
|
592
607
|
|
|
593
608
|
/* ---------- 发送消息 ---------- */
|
|
@@ -981,16 +996,6 @@ async function loadModels() {
|
|
|
981
996
|
|
|
982
997
|
/* ---------- 事件绑定 ---------- */
|
|
983
998
|
|
|
984
|
-
/** 任务页和聊天页现在是同一个服务,同源相对路径就能跳过去。
|
|
985
|
-
* 需要指向别处时用 ?task=http://host:port/task 覆盖。 */
|
|
986
|
-
const TASK_PATH = '/task';
|
|
987
|
-
|
|
988
|
-
function wireTaskEntry() {
|
|
989
|
-
if (!dom.btnOpenTask) return;
|
|
990
|
-
const override = new URLSearchParams(location.search).get('task');
|
|
991
|
-
dom.btnOpenTask.href = override || TASK_PATH;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
999
|
function autoGrow() {
|
|
995
1000
|
dom.input.style.height = 'auto';
|
|
996
1001
|
dom.input.style.height = `${Math.min(dom.input.scrollHeight, 220)}px`;
|
|
@@ -1002,7 +1007,6 @@ function init() {
|
|
|
1002
1007
|
applySettingsToUI();
|
|
1003
1008
|
renderSessions();
|
|
1004
1009
|
renderAll();
|
|
1005
|
-
wireTaskEntry();
|
|
1006
1010
|
loadConfig().then(loadModels);
|
|
1007
1011
|
|
|
1008
1012
|
if (state.prunedCount) setStatus(`已自动清理 ${state.prunedCount} 个过期会话(保留 ${TTL_DAYS} 天 / 最多 ${MAX_SESSIONS} 个)`, '');
|
|
@@ -1101,6 +1105,8 @@ function init() {
|
|
|
1101
1105
|
|
|
1102
1106
|
autoGrow();
|
|
1103
1107
|
dom.input.focus();
|
|
1108
|
+
// 初值也报一份:外壳顶栏上的 ☰ 不能等用户点一次才有正确的按下态
|
|
1109
|
+
notifySidebar();
|
|
1104
1110
|
}
|
|
1105
1111
|
|
|
1106
1112
|
init();
|
package/public/index.html
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
<title>LLM API Gateway · 聊天</title>
|
|
7
7
|
<link rel="stylesheet" href="/styles.css" />
|
|
8
8
|
<script>
|
|
9
|
-
// 防闪白:必须在首次绘制前把主题定下来,所以内联在这里(逻辑与 /theme.js
|
|
9
|
+
// 防闪白:必须在首次绘制前把主题定下来,所以内联在这里(逻辑与 /theme.js 一致,四个页面都一样)。
|
|
10
|
+
// 顺手把「是不是被 Tab 外壳嵌着」也定下来:带 ?embed=1 时 <html> 上挂 data-embed,
|
|
11
|
+
// styles.css 据此收起本页自己的顶栏(顶栏归外壳,免得出现两条横栏)。
|
|
12
|
+
// 放在首帧之前是为了不闪一下再收起来。
|
|
10
13
|
(() => {
|
|
11
14
|
try {
|
|
12
15
|
let t = localStorage.getItem('lgw.theme');
|
|
@@ -18,6 +21,13 @@
|
|
|
18
21
|
} catch (e) {
|
|
19
22
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
20
23
|
}
|
|
24
|
+
try {
|
|
25
|
+
if (new URLSearchParams(location.search).get('embed') === '1') {
|
|
26
|
+
document.documentElement.setAttribute('data-embed', '1');
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
/* 地址解析不了就当作没嵌入,页面照常独立使用 */
|
|
30
|
+
}
|
|
21
31
|
})();
|
|
22
32
|
</script>
|
|
23
33
|
<!-- 外部集成:地址上带 ?bg=<底色> 时按底色推导整套配色。必须是阻塞脚本(排在 theme.js 之前),
|
|
@@ -29,11 +39,8 @@
|
|
|
29
39
|
<div class="app">
|
|
30
40
|
<aside class="sidebar" id="sidebar">
|
|
31
41
|
<div class="sidebar-head">
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<span>+ 新任务</span>
|
|
35
|
-
<span class="ext-mark" aria-hidden="true">↗</span>
|
|
36
|
-
</a>
|
|
42
|
+
<!-- 这里原先有一个「+ 新任务 ↗」跳到任务页。20260922 起导航归 Tab 外壳(/ 顶部那排 Tab),
|
|
43
|
+
聊天页不再自己留一条通往任务页的入口 —— 免得和外壳的「任务」Tab 重复。 -->
|
|
37
44
|
<button class="btn primary block" id="btn-new-session" type="button">+ 新对话</button>
|
|
38
45
|
</div>
|
|
39
46
|
<div class="session-list" id="session-list"></div>
|
|
@@ -170,6 +177,8 @@
|
|
|
170
177
|
|
|
171
178
|
<script src="/render.js"></script>
|
|
172
179
|
<script src="/models.js"></script>
|
|
180
|
+
<!-- 面板 ↔ Tab 外壳(/)的桥:单独打开本页时它什么都不做,见 public/shell-bridge.js 的说明 -->
|
|
181
|
+
<script src="/shell-bridge.js"></script>
|
|
173
182
|
<script src="/app.js"></script>
|
|
174
183
|
</body>
|
|
175
184
|
</html>
|