@newbeebox/newbeebox-app-engine-cli 1.6.0 → 1.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newbeebox/newbeebox-app-engine-cli",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "NewBee App Engine 命令行客户端(nae)——参数直达、默认人类可读(加 -o json 出 JSON),便于在终端使用与脚本/管道里解析。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/forward.js CHANGED
@@ -1,9 +1,10 @@
1
- // 转发:把内网资源转发到本地调试。
2
- // - app 端口转发:本地 TCP server,每条连接开一条 WS 到平台 /apps/<id>/forward,双向对拷字节。
3
- // 平台 Pod 是唯一外网可达的桥;笔记本够不到 ClusterIP,故经平台中转(类 kubectl port-forward)。
4
- // - LLM 转发:把内置网关经平台 PAT 代理端点暴露回本地,写全局环境变量 NAE_LLM_GATEWAY/NAE_LLM_API_KEY
5
- // 本地代码用同款变量直接调试;off 清除。真实网关令牌不出平台——本地填的是 PAT。
1
+ // 转发:把内网资源转发到本地调试。两条命令同一套机制——本地 TCP server,每条连接桥到平台一条 WS 隧道、
2
+ // 双向对拷字节;都要求当前登录态有效(启动先验活),都前台常驻、Ctrl-C 即停。
3
+ // - app 端口转发:隧道到指定应用的 Service:port
4
+ // - LLM 转发:隧道到平台为本人开的 LLM 反代;写全局 NAE_LLM_GATEWAY(=本地址)/NAE_LLM_API_KEY(=本地占位令牌),退出即清除。
5
+ // 真实网关令牌只在集群内注入、不出本机;隧道随本命令退出即断,地址是 127.0.0.1——key 拿到别处连不上集群。
6
6
  import net from 'node:net'
7
+ import { randomBytes } from 'node:crypto'
7
8
  import { spawnSync } from 'node:child_process'
8
9
  import { homedir, platform } from 'node:os'
9
10
  import { join } from 'node:path'
@@ -11,17 +12,37 @@ import { readFileSync, writeFileSync } from 'node:fs'
11
12
  import WebSocket from 'ws'
12
13
  import { apiBase } from './config.js'
13
14
  import { CLI_VERSION } from './version.js'
14
- import { AuthError } from './http.js'
15
+ import { request, ApiError } from './http.js'
15
16
  import * as out from './output.js'
16
17
 
17
18
  const isWin = platform() === 'win32'
18
19
 
20
+ // ensureLoggedIn 启动前验活:登录态无效(无令牌/过期)直接抛 AuthError,由 run() 提示 nae login,绝不开隧道。
21
+ async function ensureLoggedIn(cfg) {
22
+ await request(cfg, 'GET', '/me')
23
+ }
24
+
25
+ // ensureAppReachable app 转发启动前的一次预检:GET /apps/:id 同时走 AuthRequired(验登录)
26
+ // 与 AppOwnership(验存在+归属)。应用不存在/不归本人 → 抛错,绝不开监听。
27
+ // 一次调用顶「验活 + 验目标」两件事——避免凭空对垃圾 appid 起一个永远连不通的本地监听。
28
+ async function ensureAppReachable(cfg, appid) {
29
+ try {
30
+ await request(cfg, 'GET', `/apps/${encodeURIComponent(appid)}`)
31
+ } catch (e) {
32
+ // 应用不存在/无权访问:给清晰提示,不外泄后端内部错误串。
33
+ // 鉴权(AuthError)/网络(NetworkError)错误原样上抛,交 run() 分类(提示 nae login / 网络)。
34
+ if (e instanceof ApiError) throw new Error(`应用 ${appid} 不存在或无权访问(用 nae apps 查看你的应用)`)
35
+ throw e
36
+ }
37
+ }
38
+
19
39
  // --- app 端口转发 ---
20
40
 
21
- // forwardApp 在本地起 TCP server,把每条连接桥到平台 WS 隧道。前台常驻,Ctrl-C 退出。
22
- export function forwardApp(cfg, appid, opts) {
23
- if (!cfg.token) throw new AuthError('未登录')
41
+ // forwardApp 在本地起 TCP server,把每条连接桥到平台隧道。前台常驻,Ctrl-C 退出。
42
+ export async function forwardApp(cfg, appid, opts) {
43
+ await ensureAppReachable(cfg, appid)
24
44
  const remotePort = opts.port // undefined → 平台取应用容器端口
45
+ const path = `/apps/${appid}/forward` + (remotePort ? `?port=${remotePort}` : '')
25
46
  let active = 0
26
47
  let up = 0
27
48
  let down = 0
@@ -29,7 +50,7 @@ export function forwardApp(cfg, appid, opts) {
29
50
  return new Promise((resolve, reject) => {
30
51
  const server = net.createServer((sock) => {
31
52
  active++
32
- bridge(cfg, appid, remotePort, sock, {
53
+ bridge(cfg, path, sock, {
33
54
  onUp: (n) => (up += n),
34
55
  onDown: (n) => (down += n),
35
56
  onClose: () => (active = Math.max(0, active - 1)),
@@ -44,25 +65,67 @@ export function forwardApp(cfg, appid, opts) {
44
65
  out.info('前台常驻转发中,Ctrl-C 退出。')
45
66
  })
46
67
 
47
- // 实时流量行(覆盖式单行,写 stderr 不污染可能被重定向的 stdout)。
48
- const timer = setInterval(() => {
49
- process.stderr.write(`\r活动连接 ${active} | ↑ ${humanBytes(up)} | ↓ ${humanBytes(down)} `)
50
- }, 1000)
51
-
52
- process.on('SIGINT', () => {
68
+ const timer = liveLine(() => `活动连接 ${active} | ↑ ${humanBytes(up)} | ↓ ${humanBytes(down)}`)
69
+ onShutdown(() => {
53
70
  clearInterval(timer)
54
71
  process.stderr.write('\n已停止转发。\n')
55
72
  server.close(() => resolve())
56
- // server.close 只停新连接;已建连接由进程退出收尾。
57
73
  process.exit(0)
58
74
  })
59
75
  })
60
76
  }
61
77
 
62
- // bridge 一条本地 TCP 连接 ↔ 一条平台 WS 隧道:本地数据缓冲到 WS open 后发,WS 二进制帧写回本地。
63
- function bridge(cfg, appid, remotePort, sock, cb) {
64
- const url = wsBase(cfg) + `/apps/${appid}/forward` + (remotePort ? `?port=${remotePort}` : '')
65
- const ws = new WebSocket(url, {
78
+ // --- LLM 转发 ---
79
+
80
+ // forwardLLM 在本地起 TCP server,每条连接桥到平台为本人开的 LLM 反代隧道。前台常驻,Ctrl-C 退出。
81
+ // 写全局 env:NAE_LLM_GATEWAY=本地址、NAE_LLM_API_KEY=本地占位令牌;退出即清除。
82
+ // 安全:真实网关令牌只在集群内注入、不出本机;隧道随本命令退出即断,地址 127.0.0.1——key 拿到别处连不上集群。
83
+ export async function forwardLLM(cfg, opts) {
84
+ await ensureLoggedIn(cfg)
85
+ const localKey = 'naelocal_' + randomBytes(18).toString('hex') // 本地占位,真实鉴权在集群内完成
86
+ let active = 0
87
+ let up = 0
88
+ let down = 0
89
+
90
+ return new Promise((resolve, reject) => {
91
+ const server = net.createServer((sock) => {
92
+ active++
93
+ bridge(cfg, '/me/llm/forward', sock, {
94
+ onUp: (n) => (up += n),
95
+ onDown: (n) => (down += n),
96
+ onClose: () => (active = Math.max(0, active - 1)),
97
+ })
98
+ })
99
+
100
+ server.on('error', reject)
101
+ server.listen(opts.local || 0, '127.0.0.1', () => {
102
+ const port = server.address().port
103
+ const gateway = `http://127.0.0.1:${port}`
104
+ applyEnv({ NAE_LLM_GATEWAY: gateway, NAE_LLM_API_KEY: localKey })
105
+ out.info('本地 LLM 转发已就绪(前台常驻,Ctrl-C 退出即停止并清除变量):')
106
+ out.info(` 本地网关 ${gateway}`)
107
+ out.info(` NAE_LLM_GATEWAY = ${gateway}`)
108
+ out.info(` NAE_LLM_API_KEY = ${localKey}`)
109
+ out.info('OpenAI 客户端 base=${NAE_LLM_GATEWAY}/v1;Anthropic SDK base=${NAE_LLM_GATEWAY}。')
110
+ out.info(reloadHint())
111
+ })
112
+
113
+ const timer = liveLine(() => `活动连接 ${active} | ↑ ${humanBytes(up)} | ↓ ${humanBytes(down)}`)
114
+ onShutdown(() => {
115
+ clearInterval(timer)
116
+ clearEnv(['NAE_LLM_GATEWAY', 'NAE_LLM_API_KEY'])
117
+ process.stderr.write('\n已停止 LLM 转发,全局环境变量已清除。\n')
118
+ server.close(() => resolve())
119
+ process.exit(0)
120
+ })
121
+ })
122
+ }
123
+
124
+ // --- 隧道 ---
125
+
126
+ // bridge 一条本地 TCP 连接 ↔ 一条平台 WS 隧道:本地数据缓冲到隧道就绪后发,回包写回本地。
127
+ function bridge(cfg, path, sock, cb) {
128
+ const ws = new WebSocket(wsBase(cfg) + path, {
66
129
  headers: { Authorization: `Bearer ${cfg.token}`, 'X-NAE-CLI-Version': CLI_VERSION },
67
130
  })
68
131
  ws.binaryType = 'nodebuffer'
@@ -105,6 +168,31 @@ function bridge(cfg, appid, remotePort, sock, cb) {
105
168
  })
106
169
  }
107
170
 
171
+ // onShutdown 在所有可捕获的退出信号上跑一次清理(Ctrl-C=SIGINT、Ctrl-Break=SIGBREAK、kill=SIGTERM、挂断=SIGHUP)。
172
+ // 多注册一次只跑一次(once 兜底),尽量不给用户留下指向死端口的残留环境变量。SIGKILL/强杀无法捕获,认栽。
173
+ function onShutdown(fn) {
174
+ let ran = false
175
+ const once = () => {
176
+ if (ran) return
177
+ ran = true
178
+ fn()
179
+ }
180
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP']) {
181
+ try {
182
+ process.on(sig, once)
183
+ } catch {
184
+ // 个别平台不支持某信号,忽略。
185
+ }
186
+ }
187
+ }
188
+
189
+ // liveLine 实时流量行(覆盖式单行,写 stderr 不污染可能被重定向的 stdout)。
190
+ function liveLine(text) {
191
+ return setInterval(() => {
192
+ process.stderr.write(`\r${text()} `)
193
+ }, 1000)
194
+ }
195
+
108
196
  // wsBase 把 apiBase 的 http(s):// 换成 ws(s)://。
109
197
  function wsBase(cfg) {
110
198
  return apiBase(cfg).replace(/^http/, 'ws')
@@ -122,36 +210,17 @@ function humanBytes(n) {
122
210
  return (i === 0 ? v : v.toFixed(1)) + u[i]
123
211
  }
124
212
 
125
- // --- LLM 转发(全局环境变量)---
126
-
127
- // forwardLLM on/off:把内置 LLM 网关经平台 PAT 代理端点暴露回本地,写/清全局环境变量。
128
- export function forwardLLM(cfg, action) {
129
- if (action !== 'on' && action !== 'off') {
130
- throw new Error('用法:nae forward llm on|off')
131
- }
132
- const gateway = `${apiBase(cfg)}/me/llm/proxy`
133
- if (action === 'on') {
134
- if (!cfg.token) throw new AuthError('未登录:先 nae login 再开启 LLM 转发')
135
- applyEnv({ NAE_LLM_GATEWAY: gateway, NAE_LLM_API_KEY: cfg.token })
136
- out.info('已开启本地 LLM 转发,全局环境变量已写入:')
137
- out.info(` NAE_LLM_GATEWAY = ${gateway}`)
138
- out.info(` NAE_LLM_API_KEY = ${cfg.token.slice(0, 12)}…`)
139
- out.info(reloadHint())
140
- out.info('本地用 OpenAI 客户端 base=${NAE_LLM_GATEWAY}/v1;Anthropic SDK base=${NAE_LLM_GATEWAY}。')
141
- } else {
142
- clearEnv(['NAE_LLM_GATEWAY', 'NAE_LLM_API_KEY'])
143
- out.info('已关闭本地 LLM 转发,全局环境变量已清除。')
144
- out.info(reloadHint())
145
- }
146
- }
213
+ // --- 全局环境变量读写(跨平台)---
147
214
 
148
- // applyEnv 写用户级全局环境变量。Windows: setx;Unix: 重写 profile 内的托管块。
215
+ // applyEnv 写用户级全局环境变量(新开终端生效)。
216
+ // Windows 用 reg add 直写 HKCU\Environment——不用 setx:setx 会同步广播 WM_SETTINGCHANGE 给所有窗口,
217
+ // 某个窗口不响应就把本进程的事件循环卡死,转发随之假死。reg add 不广播、瞬时返回,与 clearEnv 的 reg delete 对称。
149
218
  function applyEnv(pairs) {
150
219
  if (isWin) {
151
220
  for (const [k, v] of Object.entries(pairs)) {
152
- const r = spawnSync('setx', [k, v], { encoding: 'utf8' })
221
+ const r = spawnSync('reg', ['add', 'HKCU\\Environment', '/v', k, '/t', 'REG_SZ', '/d', v, '/f'], { encoding: 'utf8' })
153
222
  if (r.status !== 0) {
154
- throw new Error(`setx ${k} 失败:${(r.stderr || r.stdout || r.error?.message || '').trim()}`)
223
+ throw new Error(`写入环境变量 ${k} 失败:${(r.stderr || r.stdout || r.error?.message || '').trim()}`)
155
224
  }
156
225
  }
157
226
  } else {
@@ -215,6 +284,6 @@ function shellQuote(s) {
215
284
  // reloadHint 提示新值在新终端才生效。
216
285
  function reloadHint() {
217
286
  return isWin
218
- ? '提示:已写入用户级环境变量(setx),新开一个终端窗口生效。'
219
- : `提示:已写入 ${unixProfile()},执行 source 它或新开终端生效。`
287
+ ? '提示:已写入用户级环境变量,在另开的新终端里生效(已打开的终端不会自动刷新)。'
288
+ : `提示:已写入 ${unixProfile()},在另开的新终端或 source 后生效。`
220
289
  }
package/src/index.js CHANGED
@@ -455,21 +455,21 @@ forward
455
455
  )
456
456
 
457
457
  forward
458
- .command('llm <action>')
459
- .description('本地 LLM 转发开关:on 写全局 env(NAE_LLM_GATEWAY/NAE_LLM_API_KEY) 把内置网关转发回本地,off 清除')
458
+ .command('llm')
459
+ .description('把内置 LLM 网关转发到本地(前台常驻,类 forward app):写全局 env(NAE_LLM_GATEWAY/NAE_LLM_API_KEY),Ctrl-C 退出即停止并清除')
460
+ .option('--local <n>', '本地监听端口(缺省自动选空闲端口)', (v) => parseInt(v, 10))
460
461
  .addHelpText(
461
462
  'after',
462
463
  `
463
464
  示例:
464
- nae forward llm on # 开启:写全局环境变量,新终端里本地代码即可直连内置网关调试
465
- nae forward llm off # 关闭:清除这两个全局环境变量
465
+ nae forward llm # 前台常驻,另开终端里本地代码即可经环境变量调试内置网关
466
466
 
467
- 说明:本地 NAE_LLM_API_KEY 填的是你的 CLI 访问密钥(PAT),真实网关令牌不出平台;
468
- 请求经平台 PAT 代理端点转到内网网关,配额/计量与应用内调用完全一致。`
467
+ 说明:转发到 127.0.0.1,仅本命令运行期间有效,Ctrl-C 退出即失效;
468
+ NAE_LLM_API_KEY 是本次会话的临时本地令牌,只能连本地端点,拿到别处连不上集群;需先登录。`
469
469
  )
470
470
  .action(
471
- run(async (action) => {
472
- forwardLLM(cfg(), action)
471
+ run(async (opts) => {
472
+ await forwardLLM(cfg(), opts)
473
473
  })
474
474
  )
475
475