llm-api-gateway-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -0
- package/README.md +1127 -0
- package/cli-agent.js +666 -0
- package/cli-anthropic.js +236 -0
- package/cli-claude-code.js +317 -0
- package/cli-openai.js +212 -0
- package/completions/_llm-api-gateway-cli +65 -0
- package/completions/llm-api-gateway-cli.bash +64 -0
- package/completions/llm-api-gateway-cli.fish +43 -0
- package/images/chat.png +0 -0
- package/images/settings.png +0 -0
- package/images/task.png +0 -0
- package/lib/agent.js +607 -0
- package/lib/commands.js +468 -0
- package/lib/common.js +196 -0
- package/lib/config.js +70 -0
- package/lib/configcmd.js +230 -0
- package/lib/hub.js +1494 -0
- package/lib/jsonstore.js +49 -0
- package/lib/mcp.js +375 -0
- package/lib/memory.js +109 -0
- package/lib/plandoc.js +178 -0
- package/lib/pricing.js +52 -0
- package/lib/runner.js +234 -0
- package/lib/runstore.js +96 -0
- package/lib/secrets.js +198 -0
- package/lib/sessionstore.js +269 -0
- package/lib/settings.js +517 -0
- package/lib/tasksession.js +594 -0
- package/lib/taskstore.js +740 -0
- package/lib/tools.js +927 -0
- package/package.json +55 -0
- package/public/app.js +1055 -0
- package/public/index.html +167 -0
- package/public/manual.css +215 -0
- package/public/manual.html +381 -0
- package/public/manual.js +186 -0
- package/public/models.js +121 -0
- package/public/render.js +250 -0
- package/public/styles.css +955 -0
- package/public/task-slash.js +493 -0
- package/public/task.css +739 -0
- package/public/task.html +220 -0
- package/public/task.js +3127 -0
- package/public/theme.js +91 -0
- package/public/tint.js +261 -0
- package/scripts/install.ps1 +537 -0
- package/scripts/install.sh +510 -0
- package/server.js +14 -0
- package/task-server.js +15 -0
|
@@ -0,0 +1,381 @@
|
|
|
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.0" />
|
|
6
|
+
<title>LLM API Gateway · 操作手册</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
8
|
+
<link rel="stylesheet" href="/manual.css" />
|
|
9
|
+
<script>
|
|
10
|
+
// 防闪白:必须在首次绘制前把主题定下来(逻辑与 /theme.js 一致,三个页面都一样)
|
|
11
|
+
(() => {
|
|
12
|
+
try {
|
|
13
|
+
let t = localStorage.getItem('lgw.theme');
|
|
14
|
+
if (t !== 'dark' && t !== 'light') {
|
|
15
|
+
t = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
16
|
+
}
|
|
17
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
18
|
+
document.documentElement.style.colorScheme = t;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
</script>
|
|
24
|
+
<script src="/tint.js"></script>
|
|
25
|
+
<script src="/theme.js" defer></script>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<header class="topbar manual-topbar">
|
|
29
|
+
<div class="brand">
|
|
30
|
+
<span class="dot ok"></span>
|
|
31
|
+
<h1>LLM API Gateway</h1>
|
|
32
|
+
<span class="tag">操作手册</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="topbar-actions">
|
|
35
|
+
<a class="btn ghost" href="/" title="聊天页:纯对话,不碰你的文件">聊天</a>
|
|
36
|
+
<a class="btn ghost" href="/task" title="任务页:选一个目录,模型真读写里面的文件">任务</a>
|
|
37
|
+
<div class="theme-switch" role="group" aria-label="主题">
|
|
38
|
+
<button class="theme-btn" type="button" data-theme-option="system" title="跟随系统" aria-label="跟随系统">◐</button>
|
|
39
|
+
<button class="theme-btn" type="button" data-theme-option="light" title="白天" aria-label="白天">☀</button>
|
|
40
|
+
<button class="theme-btn" type="button" data-theme-option="dark" title="黑夜" aria-label="黑夜">☾</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</header>
|
|
44
|
+
|
|
45
|
+
<main class="manual-main">
|
|
46
|
+
<div class="manual-head">
|
|
47
|
+
<h1>操作手册</h1>
|
|
48
|
+
<p class="manual-lede">
|
|
49
|
+
这套工具怎么装到一台机器上、任务页怎么用、斜杠指令有哪些、三端(命令行 REPL / 本机聊天页 / 任务页)差在哪。
|
|
50
|
+
下面<b>所有指令清单都由服务端接口生成</b>(<code>GET /api/commands</code>),
|
|
51
|
+
与代码里那两张表是同一份数据 —— 不会再出现"文档里少几条"。
|
|
52
|
+
</p>
|
|
53
|
+
<p class="manual-counts" id="manual-counts">正在读取指令清单…</p>
|
|
54
|
+
<p class="manual-status" id="manual-status"></p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<nav class="manual-toc" aria-label="目录">
|
|
58
|
+
<a href="#s-install">安装与启动</a>
|
|
59
|
+
<a href="#s-quick">三分钟上手</a>
|
|
60
|
+
<a href="#s-modes">三种模式</a>
|
|
61
|
+
<a href="#s-workdir">工作目录与边界</a>
|
|
62
|
+
<a href="#s-howto">指令怎么用</a>
|
|
63
|
+
<a href="#s-commands">指令一览</a>
|
|
64
|
+
<a href="#s-faq">常见问题</a>
|
|
65
|
+
</nav>
|
|
66
|
+
|
|
67
|
+
<section id="s-install">
|
|
68
|
+
<h2>一、安装与启动(命令行工具 <code>gateway-agent</code>)</h2>
|
|
69
|
+
<p class="manual-note">
|
|
70
|
+
这一页除了任务页怎么用,也讲<b>这套工具怎么装到一台机器上、装完怎么配才能跑起来</b>。三条路线的产物是<b>同一份</b>
|
|
71
|
+
(<code>npm pack</code> 出来的 <code>.tgz</code>),装完都得到同一个主命令 <code>gateway-agent</code>,
|
|
72
|
+
以及用来起服务的 <code>gateway-web</code> / <code>gateway-task</code>。
|
|
73
|
+
安装器就是仓库里的 <code>scripts/install.sh</code>(macOS / Linux / WSL 的 bash)与
|
|
74
|
+
<code>scripts/install.ps1</code>(Windows PowerShell 5.1 / 7)。
|
|
75
|
+
<b>注意:装完还差一步「配密钥」</b> —— 但这一步<b>不用手写 <code>.env</code></b>:起服务后在界面「设置」里填一次,
|
|
76
|
+
或者 <code>gateway-agent config set key sk-xxx</code>。没配之前页面能打开,只是发消息会被提示去配密钥(见下面「首次配置」)。
|
|
77
|
+
</p>
|
|
78
|
+
<p>
|
|
79
|
+
<b>前置条件只有一个:Node.js ≥ 18</b>(用 <code>node -v</code> 自检)。脚本<b>不会替你装 Node</b> ——
|
|
80
|
+
缺失或版本过低时它明确报错并停下,不动你的系统运行时。
|
|
81
|
+
</p>
|
|
82
|
+
|
|
83
|
+
<h3>路线 A · 一键脚本(macOS / Linux / WSL)</h3>
|
|
84
|
+
<div class="code-block"><code># 把 <版本> 换成 Release 的 tag(例:v1.0.0)—— 装到的就是你看的那个版本
|
|
85
|
+
curl -fsSL https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/<版本>/scripts/install.sh | bash
|
|
86
|
+
|
|
87
|
+
# 想「永远拿最新脚本」就把 <版本> 换成 main(脚本与 Release 的契约可能先后不一致)
|
|
88
|
+
curl -fsSL https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/main/scripts/install.sh | bash
|
|
89
|
+
|
|
90
|
+
# 管道里要传参数,用 -s -- 隔开(否则参数会被 bash 当成它自己的)
|
|
91
|
+
curl -fsSL https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/<版本>/scripts/install.sh \
|
|
92
|
+
| bash -s -- --version v1.0.0 --prefix "$HOME/.llm-api-gateway"</code></div>
|
|
93
|
+
|
|
94
|
+
<h3>路线 B · 一键脚本(Windows PowerShell 5.1 / 7)</h3>
|
|
95
|
+
<div class="code-block"><code>irm https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/<版本>/scripts/install.ps1 | iex
|
|
96
|
+
|
|
97
|
+
# 管道里要传参就用 scriptblock 形式
|
|
98
|
+
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/<版本>/scripts/install.ps1))) -Version v1.0.0 -DryRun</code></div>
|
|
99
|
+
|
|
100
|
+
<h3>路线 C · npm 全局安装</h3>
|
|
101
|
+
<div class="code-block"><code>npm install -g llm-api-gateway-cli</code></div>
|
|
102
|
+
<p class="manual-note">
|
|
103
|
+
与上面两条用<b>同一份产物</b>,不会出现「curl 装到 A 版、npm 装到 B 版」。
|
|
104
|
+
该版本还没 <code>npm publish</code> 时,从
|
|
105
|
+
<a href="https://github.com/boonya-hrgk/llm-api-gateway-cli/releases" target="_blank" rel="noreferrer">Release 页</a>
|
|
106
|
+
下载 <code>.tgz</code> 本地装:<code>npm install -g ./llm-api-gateway-cli-<版本>.tgz</code>。
|
|
107
|
+
</p>
|
|
108
|
+
|
|
109
|
+
<h3>已经有源码:装成全局命令</h3>
|
|
110
|
+
<div class="code-block"><code>cd llm-api-gateway-cli
|
|
111
|
+
npm install
|
|
112
|
+
npm link # 软链到源目录:改代码 / 改 .env 立即生效,无需重装</code></div>
|
|
113
|
+
<p class="manual-note">
|
|
114
|
+
用 <code>npm link</code> 而不是 <code>npm install -g .</code>:后者是<b>拷贝</b>,被 <code>.gitignore</code>
|
|
115
|
+
忽略的 <code>.env</code> 不会跟着走,结果就是「装好了却读不到密钥」。在仓库目录里也完全可以不装:
|
|
116
|
+
直接 <code>node cli-agent.js</code>、<code>node server.js</code> 就能跑。
|
|
117
|
+
</p>
|
|
118
|
+
|
|
119
|
+
<h3>安装器的行为(两份脚本口径一致)</h3>
|
|
120
|
+
<ul>
|
|
121
|
+
<li><b>装到用户私有目录</b>:默认 <code>~/.llm-api-gateway</code>(Windows 是 <code>$HOME\.llm-api-gateway</code>),不碰系统目录、<b>不需要 sudo / 管理员</b>。</li>
|
|
122
|
+
<li><b>把 bin 目录接进 PATH</b>:bash 写 <code>.bashrc</code>、zsh 写 <code>.zshrc</code>、macOS 写 <code>.bash_profile</code>、fish 用 <code>fish_add_path</code>、Windows 写用户 PATH;已经加过就不重复加。不想让它改就加 <code>--no-modify-path</code> / <code>-NoModifyPath</code>,它只打印你要手工加的那一行。</li>
|
|
123
|
+
<li><b>幂等</b>:已经是目标版本就直接跳过(<code>--force</code> / <code>-Force</code> 可强制重装)。</li>
|
|
124
|
+
<li><b>失败不留半成品</b>:临时文件必删;版本号写错(404)与网络不通<b>分开报</b> —— 这两种情况的处理办法不一样。</li>
|
|
125
|
+
</ul>
|
|
126
|
+
|
|
127
|
+
<div class="manual-table-wrap">
|
|
128
|
+
<table class="manual-table">
|
|
129
|
+
<thead>
|
|
130
|
+
<tr><th>用途</th><th>bash(macOS / Linux / WSL)</th><th>PowerShell(Windows)</th><th>说明</th></tr>
|
|
131
|
+
</thead>
|
|
132
|
+
<tbody>
|
|
133
|
+
<tr><td>指定版本</td><td class="cmd">-v / --version</td><td class="cmd">-Version</td><td>装指定 tag(例 <code>v1.0.0</code>);不传则查最新 Release</td></tr>
|
|
134
|
+
<tr><td>安装目录</td><td class="cmd">--prefix</td><td class="cmd">-Prefix</td><td>默认 <code>~/.llm-api-gateway</code></td></tr>
|
|
135
|
+
<tr><td>下载前缀</td><td class="cmd">--base</td><td class="cmd">-Base</td><td>换国内镜像时用</td></tr>
|
|
136
|
+
<tr><td>不改 PATH</td><td class="cmd">--no-modify-path</td><td class="cmd">-NoModifyPath</td><td>只打印要手工加的那一行</td></tr>
|
|
137
|
+
<tr><td>演练</td><td class="cmd">-n / --dry-run</td><td class="cmd">-DryRun</td><td>只打印将要做什么,不下载、不落盘、不改 PATH</td></tr>
|
|
138
|
+
<tr><td>强制重装</td><td class="cmd">-f / --force</td><td class="cmd">-Force</td><td>已是目标版本也重装一遍</td></tr>
|
|
139
|
+
<tr><td>帮助</td><td class="cmd">-h / --help</td><td class="cmd">-Help</td><td>打印参数说明</td></tr>
|
|
140
|
+
</tbody>
|
|
141
|
+
</table>
|
|
142
|
+
</div>
|
|
143
|
+
<p class="manual-note">
|
|
144
|
+
raw / <code>api.github.com</code> 不通时换镜像(API 不通时脚本会退回镜像的
|
|
145
|
+
<code>/releases/latest</code> 重定向来解析版本):
|
|
146
|
+
</p>
|
|
147
|
+
<div class="code-block"><code># bash
|
|
148
|
+
LLM_GATEWAY_INSTALL_BASE=https://your-mirror.example.com bash install.sh
|
|
149
|
+
|
|
150
|
+
# PowerShell
|
|
151
|
+
$env:LLM_GATEWAY_INSTALL_BASE = 'https://your-mirror.example.com'; irm .../install.ps1 | iex</code></div>
|
|
152
|
+
|
|
153
|
+
<h3>首次配置(配一次就能用:不用手写 .env)</h3>
|
|
154
|
+
<p>
|
|
155
|
+
<b>现在装完不用先准备 <code>.env</code></b>:不带密钥也能把服务起起来,页面照常打得开,
|
|
156
|
+
然后<b>在页面上填一次密钥</b>就长期可用。密钥写进
|
|
157
|
+
<code>~/.llm-api-gateway-cli/credentials.json</code>(Windows <code>C:\Users\<你>\.llm-api-gateway-cli\credentials.json</code>)——
|
|
158
|
+
<b>单独一个文件、只给本用户读</b>,<b>不进 <code>config.json</code></b>(那是会被同步、被备份、被贴进 issue 的文件)。
|
|
159
|
+
</p>
|
|
160
|
+
<ol>
|
|
161
|
+
<li>
|
|
162
|
+
<b>路线一(推荐):起服务 → 在界面里填</b>
|
|
163
|
+
<div class="code-block"><code>gateway-web # 或 gateway-task,等价于 node server.js / node task-server.js
|
|
164
|
+
# 打开 http://127.0.0.1:3100/task → 右上「设置」→「网关密钥」→ 粘贴 sk-xxx → 回车/保存</code></div>
|
|
165
|
+
没有密钥时启动横幅会打印「密钥 未配置」,并给出三种填法;页面的信息行显示「未配置」,发消息会被挡下并提示你打开设置
|
|
166
|
+
(聊天页与任务页都有这个输入框,写的是同一个文件)。<b>保存后立即生效,不用重启服务。</b>
|
|
167
|
+
</li>
|
|
168
|
+
<li>
|
|
169
|
+
<b>路线二:命令行填</b>
|
|
170
|
+
<div class="code-block"><code>gateway-agent config set key sk-xxx # 写 credentials.json(不是 config.json)
|
|
171
|
+
gateway-agent config list # 第一行就是 key:已设置 sk-abcd****wxyz / 来源 / 文件路径
|
|
172
|
+
gateway-agent config unset key # 清掉,回到「未配置」</code></div>
|
|
173
|
+
<code>config get key</code> <b>只回掩码,不回明文</b>(命令行输出经常被贴进日志 / issue)。
|
|
174
|
+
</li>
|
|
175
|
+
<li>
|
|
176
|
+
<b>路线三:环境变量或 <code>.env</code>(优先级更高,适合 CI / 多机)</b>:
|
|
177
|
+
<code>--key sk-xxx</code> > 环境变量 <code>SK</code> / <code>GATEWAY_KEY</code>(也认
|
|
178
|
+
<code>OPENAI_API_KEY</code> / <code>ANTHROPIC_API_KEY</code> / <code>ANTHROPIC_AUTH_TOKEN</code>)>
|
|
179
|
+
<code>.env</code> 里写 <code>GATEWAY_KEY=sk-xxx</code> > <code>credentials.json</code>。
|
|
180
|
+
环境里给了密钥时,那个文件<b>不参与</b>(<code>config list</code> 的「来源」列会告诉你是哪一层在生效)。
|
|
181
|
+
<code>.env</code> 的加载顺序是<b>①安装目录 → ②当前工作目录</b>;<b>npm 全局安装后包目录里只有
|
|
182
|
+
<code>.env.example</code></b>,所以要用 <code>.env</code> 就放在<b>你起服务的那个目录</b>:
|
|
183
|
+
<div class="code-block"><code># 在你干活的项目目录里(可选,不是必须)
|
|
184
|
+
printf 'GATEWAY_KEY=sk-xxx\n' > .env # Windows: 'GATEWAY_KEY=sk-xxx' | Set-Content .env</code></div>
|
|
185
|
+
</li>
|
|
186
|
+
<li>
|
|
187
|
+
<b>密钥存在哪、谁能读到</b>:Linux / macOS 上写入时会 <code>chmod 600</code>(只有属主可读),启动时如果发现它对同机其他用户可读会打警告;
|
|
188
|
+
Windows 上 <code>chmod</code> 管不了 ACL,靠的是用户目录本身的权限(<code>C:\Users\<你></code> 默认只有本人与管理员可读)——
|
|
189
|
+
共享机器的多用户场景,建议改用环境变量并把文件删掉(<code>gateway-agent config unset key</code>)。
|
|
190
|
+
</li>
|
|
191
|
+
<li>
|
|
192
|
+
<b>网关地址与模型不是默认值时才要改</b>:地址默认 <code>http://127.0.0.1:9000</code>,模型默认 <code>qwen3:8b</code>
|
|
193
|
+
(网关里没有这个模型就换成你在用的那个)。<b>这两项也能在设置面板里改</b>(模型是从网关拉下来的下拉,只能选不能填),
|
|
194
|
+
改完立即生效;临时用环境变量 <code>GATEWAY_BASE_URL</code> / <code>GATEWAY_MODEL</code> 或 <code>--base-url</code> / <code>--model</code> 也行,
|
|
195
|
+
想长期生效就写进配置文件(见下一条)。
|
|
196
|
+
</li>
|
|
197
|
+
<li>
|
|
198
|
+
<b>其余能落盘的配置写在 <code>~/.llm-api-gateway-cli/config.json</code></b>
|
|
199
|
+
(换地方用 <code>LLM_GATEWAY_CONFIG</code>,打印路径用 <code>gateway-agent config path</code>,它同时打印密钥文件路径):
|
|
200
|
+
<div class="code-block"><code>gateway-agent config list # 所有键 + 当前值 + 来源(被环境变量/flag 盖住时一眼看出)
|
|
201
|
+
gateway-agent config set baseUrl http://127.0.0.1:9000
|
|
202
|
+
gateway-agent config set model qwen3:8b
|
|
203
|
+
gateway-agent config set mode auto # 新任务的默认审批模式</code></div>
|
|
204
|
+
优先级一律是 <code>--flag</code> > 环境变量 > <code>config.json</code> > 内置默认;文件不存在也没事,第一次
|
|
205
|
+
<code>config set</code> 会自动建。Web 设置面板与 REPL 的 <code>/config</code> 读写的是同一份,不会各存一处。
|
|
206
|
+
<b>唯一的例外就是密钥</b>:它有自己那个文件,<code>config.json</code> 里永远没有这个字段。
|
|
207
|
+
</li>
|
|
208
|
+
</ol>
|
|
209
|
+
|
|
210
|
+
<h3>启动与自检</h3>
|
|
211
|
+
<ol>
|
|
212
|
+
<li><b>自检命令可用</b>:<code>gateway-agent --help</code>;再跑一句 <code>gateway-agent -p "用一句话介绍你自己"</code>。
|
|
213
|
+
这一步是命令行 agent,<b>缺密钥它会直接报错并列出三种给法</b>(它没有界面可交互);先按上面「路线一 / 二」配一次即可。</li>
|
|
214
|
+
<li>
|
|
215
|
+
<b>起本机 Web UI 与任务页</b>(同一个服务、同一个端口):<code>gateway-web</code> 或 <code>gateway-task</code>
|
|
216
|
+
(等价于 <code>node server.js</code> / <code>node task-server.js</code>)。<b>没有密钥也能起</b>。
|
|
217
|
+
启动横幅会打印<b>网关 / 模型 / 密钥(脱敏 + 来源)/ 工具 / 存储 / 配置文件与密钥文件路径</b> ——
|
|
218
|
+
先对着这几行核一遍,比在页面上猜快:密钥那行写「未配置」时,同一段还有三条填法。
|
|
219
|
+
</li>
|
|
220
|
+
<li>打开 <code>http://127.0.0.1:3100/task</code> —— 你正在看的这一页就是它。</li>
|
|
221
|
+
<li>
|
|
222
|
+
<b>连不上网关时怎么判断</b>:<code>curl http://127.0.0.1:9000/v1/models</code>(Windows:<code>irm http://127.0.0.1:9000/v1/models</code>)
|
|
223
|
+
能列出模型说明地址与密钥都对;报 401 是密钥不对,连不上是网关地址不对或网关没起。
|
|
224
|
+
</li>
|
|
225
|
+
</ol>
|
|
226
|
+
<p class="manual-note">
|
|
227
|
+
卸载:删掉安装目录(<code>rm -rf ~/.llm-api-gateway</code>,Windows 删 <code>$HOME\.llm-api-gateway</code>),
|
|
228
|
+
再把写进 shell 启动文件 / 用户 PATH 的那一行删掉。数据(任务与配置,在 <code>~/.llm-api-gateway-cli</code>)
|
|
229
|
+
与安装目录是两回事,卸载不会连带删数据。
|
|
230
|
+
</p>
|
|
231
|
+
<p class="manual-note">
|
|
232
|
+
更细的说明(换镜像、源码安装、CI 产物)见仓库
|
|
233
|
+
<a href="https://github.com/boonya-hrgk/llm-api-gateway-cli#readme" target="_blank" rel="noreferrer">README 的「一键安装」一节</a>。
|
|
234
|
+
</p>
|
|
235
|
+
</section>
|
|
236
|
+
|
|
237
|
+
<section id="s-quick">
|
|
238
|
+
<h2>二、三分钟上手(任务页 /task)</h2>
|
|
239
|
+
<ol>
|
|
240
|
+
<li>打开 <a href="/task">/task</a>。第一次会让你选<b>工作目录</b>:模型只能读写这个目录里的东西,目录之外的读不到也写不了。</li>
|
|
241
|
+
<li>在底部输入框里说清楚要做什么,<code>Enter</code> 执行、<code>Shift+Enter</code> 换行。模型会自己列目录、读文件、搜内容。</li>
|
|
242
|
+
<li><b>手动模式</b>下,模型每次写文件前会弹一张卡片,问你批不批。点「批准写入」或「拒绝」,也可以敲 <code>/approve</code>、<code>/reject</code>。</li>
|
|
243
|
+
<li>想先看方案再动手:把模式切到「计划」,或者直接敲 <code>/plan 先看下项目结构再给我改造方案</code>。这一轮模型<b>只有只读工具</b>,不会改文件。</li>
|
|
244
|
+
<li>计划出来后,消息上会出现「按计划执行」/「手动执行」两个按钮;计划本身还会落盘成一份文档,模型接着按它执行并把结果回填进去。</li>
|
|
245
|
+
<li>随时可以点「停止」或敲 <code>/stop</code> 停掉正在跑的这一轮;停只作用于当前这条任务,后台别的任务不受影响。</li>
|
|
246
|
+
</ol>
|
|
247
|
+
</section>
|
|
248
|
+
|
|
249
|
+
<section id="s-modes">
|
|
250
|
+
<h2>三、三种模式(跟任务走,随时可切)</h2>
|
|
251
|
+
<div class="manual-table-wrap">
|
|
252
|
+
<table class="manual-table">
|
|
253
|
+
<thead>
|
|
254
|
+
<tr><th>模式</th><th>行为</th><th>什么时候用</th></tr>
|
|
255
|
+
</thead>
|
|
256
|
+
<tbody>
|
|
257
|
+
<tr>
|
|
258
|
+
<td class="cmd">手动 manual</td>
|
|
259
|
+
<td>每次写入都要你在界面上点确认;工具步骤逐条显示</td>
|
|
260
|
+
<td>碰真实代码库、想看清每一步改了什么</td>
|
|
261
|
+
</tr>
|
|
262
|
+
<tr>
|
|
263
|
+
<td class="cmd">自动 auto</td>
|
|
264
|
+
<td>写入直接执行、不再打断你,界面仍然逐条显示改了什么</td>
|
|
265
|
+
<td>已经想清楚要它干嘛,盯着日志就行</td>
|
|
266
|
+
</tr>
|
|
267
|
+
<tr>
|
|
268
|
+
<td class="cmd">计划 plan</td>
|
|
269
|
+
<td>只给模型只读工具(列目录 / 读文件 / 搜内容),先出计划、不动文件</td>
|
|
270
|
+
<td>大改动之前先要一份方案</td>
|
|
271
|
+
</tr>
|
|
272
|
+
</tbody>
|
|
273
|
+
</table>
|
|
274
|
+
</div>
|
|
275
|
+
<p>
|
|
276
|
+
切法:点模式按钮,或敲 <code>/mode manual|auto|plan</code>;<code>/plan</code> 是「切到计划模式并可选地立刻跑一轮」的快捷方式。
|
|
277
|
+
<b>切模式不影响已经在跑的那一轮</b> —— 要停就点「停止」或 <code>/stop</code>。
|
|
278
|
+
</p>
|
|
279
|
+
</section>
|
|
280
|
+
|
|
281
|
+
<section id="s-workdir">
|
|
282
|
+
<h2>四、工作目录、任务记录与边界</h2>
|
|
283
|
+
<ul>
|
|
284
|
+
<li>模型的一切文件操作都被限制在你选定<b>工作目录</b>之内;换目录用顶栏的目录芯片,或敲 <code>/cwd <路径></code>。</li>
|
|
285
|
+
<li><code>/files [子路径]</code> 列目录、<code>/init</code> 在工作目录里生成 <code>AGENTS.md</code> 项目约定骨架、<code>/instructions</code> 看当前注入给模型的项目记忆(<code>AGENTS.md</code> / <code>CLAUDE.md</code>,超上限截断)。</li>
|
|
286
|
+
<li>任务记录(含工具读过的文件正文)由服务端直写磁盘,换浏览器、清缓存都不会丢;存储位置在设置面板里能看到,也可以用启动参数 <code>--store <目录></code> 换地方。</li>
|
|
287
|
+
<li>侧边栏<b>按工作目录分组</b>:同一个目录下可以挂多条任务,分组行右侧的 <code>+</code> 直接在该目录下再开一条。鼠标移到某条任务上,行尾的 <code>+</code> 可以<b>派生一条子任务</b>(继承父任务的工作目录,第二行以 <code>↳</code> 缩进显示);删父任务不会连坐,子任务会升一层。</li>
|
|
288
|
+
<li>存储目录若正好落在工作目录之内,页面会提醒你:模型能读到任务历史。要避免就把 <code>--store</code> 指到工作目录之外。</li>
|
|
289
|
+
<li><code>bash</code> 工具默认不开(模型跑不了测试);要用请在启动服务时加 <code>--allow-bash</code>。设置面板的「可用工具」一行会写清当前有哪些。</li>
|
|
290
|
+
</ul>
|
|
291
|
+
</section>
|
|
292
|
+
|
|
293
|
+
<section id="s-howto">
|
|
294
|
+
<h2>五、斜杠指令怎么用</h2>
|
|
295
|
+
<ul>
|
|
296
|
+
<li>在任务页输入框敲一个 <code>/</code>,就会弹出<b>全部</b>候选:<code>↑↓</code> 选择 · <code>Tab</code> 补全 · <code>Enter</code> 执行 · <code>Esc</code> 关闭。接着打字母会按前缀收窄(<code>/pl</code> → <code>/plan</code>)。</li>
|
|
297
|
+
<li>指令<b>只在页面本地执行</b>,不发给模型、也不进任务对白(所以它不会污染模型的上下文)。</li>
|
|
298
|
+
<li>想让模型看到这些字面:写成 <code>//plan</code>(去掉一个斜杠),它就会被当成普通任务文本交给模型。</li>
|
|
299
|
+
<li>不认识的 <code>/xxx</code> <b>不会</b>被当成任务发出去,只提示你改用 <code>//</code> —— 在任务页把 <code>/clear</code> 当需求发出去几乎不可能是本意。</li>
|
|
300
|
+
<li>指令带「能力」标记:<code>local</code> 本页即可;<code>api</code> 要用服务端接口;<code>fs</code> 需要服务端放行本机文件访问(服务没绑定在本机地址时会不可用,页面会说明原因)。</li>
|
|
301
|
+
<li><code>/help</code> 列任务页全部指令,<code>/help <指令名></code> 看单条用法(别名、能力、示例)。</li>
|
|
302
|
+
<li><code>/model</code> 改的就是设置面板里的「模型」,这一项<b>只能从网关 <code>/v1/models</code> 拉到的列表里选</b>(设置面板里是个下拉框,打不进字)。不带参数敲 <code>/model</code> 会列出可选模型;想改的模型不在列表里,先点设置面板的「刷新模型」,或检查网关这一侧绑定的上游。</li>
|
|
303
|
+
</ul>
|
|
304
|
+
</section>
|
|
305
|
+
|
|
306
|
+
<section id="s-commands">
|
|
307
|
+
<h2>六、指令一览(自动生成)</h2>
|
|
308
|
+
<p class="manual-note">
|
|
309
|
+
三端对照在前,各端清单在后。<code>命令行 REPL</code> 是终端里的 <code>gateway-agent</code>;
|
|
310
|
+
<code>任务页</code> 是本服务的 <code>/task</code>;<code>本机聊天页</code> 是本服务的 <code>/</code>。
|
|
311
|
+
</p>
|
|
312
|
+
<div id="manual-commands"></div>
|
|
313
|
+
</section>
|
|
314
|
+
|
|
315
|
+
<section id="s-faq">
|
|
316
|
+
<h2>七、常见问题</h2>
|
|
317
|
+
<dl class="manual-faq">
|
|
318
|
+
<dt><code>/files</code> 说"未放行"?</dt>
|
|
319
|
+
<dd>目录浏览、项目记忆、任务记录这类接口会回传本机路径与文件正文,服务只在本机监听(或显式加了 <code>--allow-remote-fs</code>)时才放行。用 <code>--host 0.0.0.0</code> 从别的机器访问时,它们是关着的 —— 这是有意的安全边界。</dd>
|
|
320
|
+
|
|
321
|
+
<dt><code>/init</code> 说"已经有 AGENTS.md 了,没有动它"?</dt>
|
|
322
|
+
<dd>骨架不覆盖已有文件(覆盖别人的约定是破坏性操作)。确实要重写就 <code>/init --force</code>;只想看看现在注入的是什么,用 <code>/instructions</code>。</dd>
|
|
323
|
+
|
|
324
|
+
<dt>模型像是忘了前面读过的文件?</dt>
|
|
325
|
+
<dd>上下文超预算时,更早的<b>工具结果</b>会被折叠成摘要(页面会提示"上下文已压缩")。最近几轮仍是全文;已压缩的文件内容需要时会重新读。想彻底重来:<code>/reset</code>(命令行)或开一条新任务(任务页)。</dd>
|
|
326
|
+
|
|
327
|
+
<dt><code>/cost</code> 的数字和网关账单对不上?</dt>
|
|
328
|
+
<dd>命令行与任务页的 <code>/cost</code> 都是<b>内置单价表的粗估</b>(口径在 <code>lib/pricing.js</code>,任务页通过 <code>GET /api/cost</code> 取同一份),认不出的模型只报 token、不编美元数字。真正的记账口径是网关的 <code>/admin/prices</code>。</dd>
|
|
329
|
+
|
|
330
|
+
<dt>跑到一半停了,说到了轮次上限?</dt>
|
|
331
|
+
<dd>轮次上限(默认 100 轮,可用 <code>--max-steps</code> 调)只是防跑飞的兜底。撞到上限时服务会先自动走一轮不带工具的收尾把结论给你,页面上再给一个「继续执行」按钮 —— 点一下带着全部历史接着跑,不用重新描述一遍。</dd>
|
|
332
|
+
|
|
333
|
+
<dt>npm 全局装完,<code>.env</code> 到底该放哪?为什么起服务就说缺密钥?</dt>
|
|
334
|
+
<dd>
|
|
335
|
+
<b>先分清两件事</b>:<code>gateway-web</code> / <code>gateway-task</code> 现在<b>没有密钥也能起</b>(页面照常打得开,
|
|
336
|
+
启动横幅会写「密钥 未配置」),你可以在界面「设置」里填;而一次性的命令行 agent(<code>gateway-agent -p "…"</code>)
|
|
337
|
+
没有密钥仍然会直接报错退出 —— 它没有可交互的地方让你补。<br>
|
|
338
|
+
<b>要用 <code>.env</code> 的话</b>:加载顺序是<b>安装目录 → 当前工作目录</b>,而 <b>npm 全局安装的包目录里只有
|
|
339
|
+
<code>.env.example</code></b>,所以得放在<b>你运行命令的那个目录</b>(内容 <code>GATEWAY_KEY=sk-xxx</code>);
|
|
340
|
+
服务是在<b>启动那一刻</b>读的,先 <code>cd</code> 过去再起。不想管路径就用
|
|
341
|
+
<code>gateway-agent config set key sk-xxx</code>(写 <code>credentials.json</code>,与界面里填的是同一个文件),
|
|
342
|
+
或者环境变量 <code>SK</code> / <code>GATEWAY_KEY</code>、<code>--key sk-xxx</code> —— 这两层优先级高于那个文件。
|
|
343
|
+
</dd>
|
|
344
|
+
|
|
345
|
+
<dt>密钥到底存在哪?会不会被同步走?</dt>
|
|
346
|
+
<dd>
|
|
347
|
+
你在界面或命令行配的密钥存在 <code><数据根>/credentials.json</code>(默认
|
|
348
|
+
<code>~/.llm-api-gateway-cli/credentials.json</code>,只给本用户读),<b>不进 <code>config.json</code></b> ——
|
|
349
|
+
免得跟着配置一起被同步、被备份、被贴进 issue。要看路径:<code>gateway-agent config path</code>(同时打印配置文件与密钥文件)。
|
|
350
|
+
不用了就 <code>gateway-agent config unset key</code> 删掉,服务会回到「未配置」。
|
|
351
|
+
</dd>
|
|
352
|
+
|
|
353
|
+
<dt>装的时候说「没有 Node」或「Node 版本太低」?</dt>
|
|
354
|
+
<dd>安装器<b>不代装</b>运行时:它只报错并告诉你需要什么(<code>node -v</code> 要 ≥ 18),不动你的系统。装好 Node 再重跑同一条命令即可;中途失败也不会留半成品(临时文件必删)。</dd>
|
|
355
|
+
|
|
356
|
+
<dt>我不想装,只想在仓库里直接用?</dt>
|
|
357
|
+
<dd>在项目目录 <code>npm install</code> 之后,<code>node cli-agent.js</code>(终端 Agent)、<code>node server.js</code>(本机 Web UI 与任务页 <code>http://127.0.0.1:3100</code>)直接就能跑;<code>npm link</code> 只是把同一份东西做成任意目录可用的全局命令。</dd>
|
|
358
|
+
|
|
359
|
+
<dt>装完了,但敲 <code>gateway-agent</code> 说找不到命令?</dt>
|
|
360
|
+
<dd>多半是 PATH 还没生效:重开一个终端,或 <code>source</code> 一下被改的 rc 文件(bash 是 <code>~/.bashrc</code>、zsh 是 <code>~/.zshrc</code>)。安装时用了 <code>--no-modify-path</code> / <code>-NoModifyPath</code> 的话,脚本会打印你要手工加的那一行,照着加即可。</dd>
|
|
361
|
+
|
|
362
|
+
<dt>想自己接这一页的数据?</dt>
|
|
363
|
+
<dd><code>GET /api/commands</code> 返回三端指令的机器可读清单(字段:<code>name / aliases / args / usage / help / group / capability / surfaces</code>);<code>GET /api/cost?prompt=&completion=&model=</code> 返回与命令行同口径的费用粗估。</dd>
|
|
364
|
+
</dl>
|
|
365
|
+
</section>
|
|
366
|
+
|
|
367
|
+
<p class="manual-foot">
|
|
368
|
+
这一页是服务的一部分(<code>/manual</code>),任务页里也可以用 <code>/manual</code> 打开。
|
|
369
|
+
指令清单来自 <code>lib/commands.js</code> 与 <code>public/task-slash.js</code> 两张表,改代码即改这一页;
|
|
370
|
+
安装与首次配置的说明(仓库地址、主命令、最低 Node、两套参数、密钥三种给法、
|
|
371
|
+
<code>credentials.json</code> 与 <code>config.json</code> 的分工、<code>.env</code> 查找顺序、
|
|
372
|
+
<code>config.json</code> 位置与默认网关地址)与
|
|
373
|
+
<code>package.json</code> / <code>scripts/install.sh</code> / <code>scripts/install.ps1</code> /
|
|
374
|
+
<code>lib/common.js</code> / <code>lib/settings.js</code> / <code>lib/secrets.js</code> 的一致性
|
|
375
|
+
由 <code>tests/manual.test.mjs</code> 与 <code>tests/secrets.test.mjs</code> 钉住,改脚本或改默认值漏改这一页会红。
|
|
376
|
+
</p>
|
|
377
|
+
</main>
|
|
378
|
+
|
|
379
|
+
<script src="/manual.js"></script>
|
|
380
|
+
</body>
|
|
381
|
+
</html>
|
package/public/manual.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 操作手册页(/manual)
|
|
3
|
+
*
|
|
4
|
+
* 分工很明确,为的是"清单不会再少几条":
|
|
5
|
+
* · 手册的**正文**(任务页怎么用、指令怎么敲)写在 manual.html 里 —— 那是解释,不是清单;
|
|
6
|
+
* · 指令**清单**一律由 `GET /api/commands` 生成。服务端给的是两张表的机器可读视图:
|
|
7
|
+
* `lib/commands.js`(命令行 REPL)与 `public/task-slash.js`(任务页,与本文件同处一个目录)。
|
|
8
|
+
* 所以这一页看到的就是代码里真实的指令,没有第二份手抄清单可以漂。
|
|
9
|
+
* `tests/manual.test.mjs` 会拿接口数据断言:**表里每一条都能在 buildSections() 的输出里找到**。
|
|
10
|
+
*
|
|
11
|
+
* 结构与 task-slash.js 同构:不碰 DOM 的部分(buildSections / crossRows / surfaceRows)是纯函数,
|
|
12
|
+
* 在 node 里可以直接测;渲染一律走 textContent —— 指令说明也是数据,不该有机会变成 HTML。
|
|
13
|
+
*/
|
|
14
|
+
(function (root, factory) {
|
|
15
|
+
root.Manual = factory();
|
|
16
|
+
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const TICK = '✓';
|
|
20
|
+
const DASH = '—';
|
|
21
|
+
|
|
22
|
+
/** 拉平:一次拿到所有 [端, 指令],省得每处各遍历一遍 */
|
|
23
|
+
function allCommands(data) {
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const s of (data && data.surfaces) || []) {
|
|
26
|
+
for (const c of s.commands || []) out.push({ surface: s, cmd: c });
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 同名指令"在哪几端能用"。
|
|
33
|
+
* 名字相同=语义相同(三端共用的元数据口径),所以这张表就是三端对齐的体检报告:
|
|
34
|
+
* 某条指令只在一端出现,要么是端内特有能力,要么就是漏了 —— 一眼能看出来。
|
|
35
|
+
*/
|
|
36
|
+
function crossRows(data) {
|
|
37
|
+
const surfaces = (data && data.surfaces) || [];
|
|
38
|
+
const names = [];
|
|
39
|
+
for (const { cmd } of allCommands(data)) {
|
|
40
|
+
if (!names.includes(cmd.name)) names.push(cmd.name);
|
|
41
|
+
}
|
|
42
|
+
names.sort();
|
|
43
|
+
return names.map((name) => {
|
|
44
|
+
const on = surfaces.filter((s) => (s.commands || []).some((c) => c.name === name));
|
|
45
|
+
return {
|
|
46
|
+
name: '/' + name,
|
|
47
|
+
count: on.length,
|
|
48
|
+
where: on.map((s) => s.label).join('、'),
|
|
49
|
+
cells: ['/' + name].concat(surfaces.map((s) => (on.indexOf(s) >= 0 ? TICK : DASH))).concat([String(on.length)]),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 一个端的所有指令 */
|
|
55
|
+
function surfaceRows(surface) {
|
|
56
|
+
return (surface.commands || []).map((c) => {
|
|
57
|
+
const aliases = (c.aliases || []).map((a) => '/' + a).join(' ');
|
|
58
|
+
return {
|
|
59
|
+
name: '/' + c.name,
|
|
60
|
+
cells: [c.usage || '/' + c.name, c.args || '—', c.capability || '—', aliases || '—', c.help || ''],
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 纯数据:这一页里所有"表格"部分的内容(node 里断言"一条不漏"就是断言它) */
|
|
66
|
+
function buildSections(data) {
|
|
67
|
+
const surfaces = (data && data.surfaces) || [];
|
|
68
|
+
const sections = [
|
|
69
|
+
{
|
|
70
|
+
id: 'cross',
|
|
71
|
+
title: '同名指令在哪一端能用',
|
|
72
|
+
note: '名字相同的指令在三端语义一致;某端没有这条,下表里就是「—」。',
|
|
73
|
+
columns: ['指令'].concat(surfaces.map((s) => s.label)).concat(['可用端数']),
|
|
74
|
+
rows: crossRows(data),
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
surfaces.forEach((s) => {
|
|
78
|
+
const note = [s.where, s.note].filter(Boolean).join(' ');
|
|
79
|
+
sections.push({
|
|
80
|
+
id: 'surface-' + s.id,
|
|
81
|
+
title: s.label,
|
|
82
|
+
note,
|
|
83
|
+
columns: ['指令', '参数', '能力', '别名', '说明'],
|
|
84
|
+
rows: surfaceRows(s),
|
|
85
|
+
empty: (s.commands || []).length ? '' : '这一端没有斜杠指令。',
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
return sections;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildTable(section) {
|
|
92
|
+
const wrap = document.createElement('div');
|
|
93
|
+
wrap.className = 'manual-table-wrap';
|
|
94
|
+
const table = document.createElement('table');
|
|
95
|
+
table.className = 'manual-table';
|
|
96
|
+
|
|
97
|
+
const thead = document.createElement('thead');
|
|
98
|
+
const hr = document.createElement('tr');
|
|
99
|
+
for (const col of section.columns) {
|
|
100
|
+
const th = document.createElement('th');
|
|
101
|
+
th.textContent = col;
|
|
102
|
+
hr.appendChild(th);
|
|
103
|
+
}
|
|
104
|
+
thead.appendChild(hr);
|
|
105
|
+
table.appendChild(thead);
|
|
106
|
+
|
|
107
|
+
const tbody = document.createElement('tbody');
|
|
108
|
+
for (const row of section.rows) {
|
|
109
|
+
const tr = document.createElement('tr');
|
|
110
|
+
row.cells.forEach((cell, i) => {
|
|
111
|
+
const td = document.createElement('td');
|
|
112
|
+
// 第 0 列是指令名,第 2 列是能力标记:这两列用等宽字体,好扫
|
|
113
|
+
if (i === 0) td.className = 'cmd';
|
|
114
|
+
else if (section.id !== 'cross' && i === 2) td.className = 'cap';
|
|
115
|
+
// 对照表里的 ✓ / — 上色(单元格文本就是标记本身,不做额外结构)
|
|
116
|
+
if (section.id === 'cross' && cell === TICK) td.className = 'yes';
|
|
117
|
+
if (section.id === 'cross' && cell === DASH) td.className = 'no';
|
|
118
|
+
td.textContent = String(cell);
|
|
119
|
+
tr.appendChild(td);
|
|
120
|
+
});
|
|
121
|
+
tbody.appendChild(tr);
|
|
122
|
+
}
|
|
123
|
+
table.appendChild(tbody);
|
|
124
|
+
wrap.appendChild(table);
|
|
125
|
+
return wrap;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function render(container, sections) {
|
|
129
|
+
if (!container) return;
|
|
130
|
+
container.textContent = '';
|
|
131
|
+
for (const section of sections) {
|
|
132
|
+
const h = document.createElement('h3');
|
|
133
|
+
h.textContent = section.title;
|
|
134
|
+
container.appendChild(h);
|
|
135
|
+
if (section.note) {
|
|
136
|
+
const p = document.createElement('p');
|
|
137
|
+
p.className = 'manual-note';
|
|
138
|
+
p.textContent = section.note;
|
|
139
|
+
container.appendChild(p);
|
|
140
|
+
}
|
|
141
|
+
if (section.empty) {
|
|
142
|
+
const p = document.createElement('p');
|
|
143
|
+
p.className = 'manual-note';
|
|
144
|
+
p.textContent = section.empty;
|
|
145
|
+
container.appendChild(p);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
container.appendChild(buildTable(section));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 页头那行「命令行 N 条 · 任务页 M 条」——数字来自接口,不写死 */
|
|
153
|
+
function countsText(data) {
|
|
154
|
+
const parts = ((data && data.surfaces) || []).map((s) => `${s.label}:${(s.commands || []).length} 条`);
|
|
155
|
+
const total = allCommands(data).length;
|
|
156
|
+
return parts.join(' · ') + ` (合计 ${total} 条)`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function boot() {
|
|
160
|
+
const box = document.getElementById('manual-commands');
|
|
161
|
+
const counts = document.getElementById('manual-counts');
|
|
162
|
+
const status = document.getElementById('manual-status');
|
|
163
|
+
try {
|
|
164
|
+
const resp = await fetch('/api/commands');
|
|
165
|
+
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
166
|
+
const data = await resp.json();
|
|
167
|
+
render(box, buildSections(data));
|
|
168
|
+
if (counts) counts.textContent = countsText(data);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
if (counts) counts.textContent = '指令清单没读到';
|
|
171
|
+
if (status) {
|
|
172
|
+
status.textContent =
|
|
173
|
+
'读不到指令清单(' + (e && e.message ? e.message : String(e)) + '):' +
|
|
174
|
+
'请通过本机服务打开这一页(http://127.0.0.1:3100/manual),直接双击 HTML 文件是拿不到数据的。';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { TICK, DASH, allCommands, crossRows, surfaceRows, buildSections, countsText, render, boot };
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 浏览器里自动开跑;node 里 import 它只为拿纯函数(那时没有 document)
|
|
183
|
+
if (typeof document !== 'undefined') {
|
|
184
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => globalThis.Manual.boot());
|
|
185
|
+
else globalThis.Manual.boot();
|
|
186
|
+
}
|