@newbeebox/newbeebox-app-engine-cli 1.6.0 → 1.7.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/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.0",
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,23 @@ 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 } 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
+
19
25
  // --- app 端口转发 ---
20
26
 
21
- // forwardApp 在本地起 TCP server,把每条连接桥到平台 WS 隧道。前台常驻,Ctrl-C 退出。
22
- export function forwardApp(cfg, appid, opts) {
23
- if (!cfg.token) throw new AuthError('未登录')
27
+ // forwardApp 在本地起 TCP server,把每条连接桥到平台隧道。前台常驻,Ctrl-C 退出。
28
+ export async function forwardApp(cfg, appid, opts) {
29
+ await ensureLoggedIn(cfg)
24
30
  const remotePort = opts.port // undefined → 平台取应用容器端口
31
+ const path = `/apps/${appid}/forward` + (remotePort ? `?port=${remotePort}` : '')
25
32
  let active = 0
26
33
  let up = 0
27
34
  let down = 0
@@ -29,7 +36,7 @@ export function forwardApp(cfg, appid, opts) {
29
36
  return new Promise((resolve, reject) => {
30
37
  const server = net.createServer((sock) => {
31
38
  active++
32
- bridge(cfg, appid, remotePort, sock, {
39
+ bridge(cfg, path, sock, {
33
40
  onUp: (n) => (up += n),
34
41
  onDown: (n) => (down += n),
35
42
  onClose: () => (active = Math.max(0, active - 1)),
@@ -44,25 +51,67 @@ export function forwardApp(cfg, appid, opts) {
44
51
  out.info('前台常驻转发中,Ctrl-C 退出。')
45
52
  })
46
53
 
47
- // 实时流量行(覆盖式单行,写 stderr 不污染可能被重定向的 stdout)。
48
- const timer = setInterval(() => {
49
- process.stderr.write(`\r活动连接 ${active} | ↑ ${humanBytes(up)} | ↓ ${humanBytes(down)} `)
50
- }, 1000)
51
-
52
- process.on('SIGINT', () => {
54
+ const timer = liveLine(() => `活动连接 ${active} | ↑ ${humanBytes(up)} | ↓ ${humanBytes(down)}`)
55
+ onShutdown(() => {
53
56
  clearInterval(timer)
54
57
  process.stderr.write('\n已停止转发。\n')
55
58
  server.close(() => resolve())
56
- // server.close 只停新连接;已建连接由进程退出收尾。
57
59
  process.exit(0)
58
60
  })
59
61
  })
60
62
  }
61
63
 
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, {
64
+ // --- LLM 转发 ---
65
+
66
+ // forwardLLM 在本地起 TCP server,每条连接桥到平台为本人开的 LLM 反代隧道。前台常驻,Ctrl-C 退出。
67
+ // 写全局 env:NAE_LLM_GATEWAY=本地址、NAE_LLM_API_KEY=本地占位令牌;退出即清除。
68
+ // 安全:真实网关令牌只在集群内注入、不出本机;隧道随本命令退出即断,地址 127.0.0.1——key 拿到别处连不上集群。
69
+ export async function forwardLLM(cfg, opts) {
70
+ await ensureLoggedIn(cfg)
71
+ const localKey = 'naelocal_' + randomBytes(18).toString('hex') // 本地占位,真实鉴权在集群内完成
72
+ let active = 0
73
+ let up = 0
74
+ let down = 0
75
+
76
+ return new Promise((resolve, reject) => {
77
+ const server = net.createServer((sock) => {
78
+ active++
79
+ bridge(cfg, '/me/llm/forward', sock, {
80
+ onUp: (n) => (up += n),
81
+ onDown: (n) => (down += n),
82
+ onClose: () => (active = Math.max(0, active - 1)),
83
+ })
84
+ })
85
+
86
+ server.on('error', reject)
87
+ server.listen(opts.local || 0, '127.0.0.1', () => {
88
+ const port = server.address().port
89
+ const gateway = `http://127.0.0.1:${port}`
90
+ applyEnv({ NAE_LLM_GATEWAY: gateway, NAE_LLM_API_KEY: localKey })
91
+ out.info('本地 LLM 转发已就绪(前台常驻,Ctrl-C 退出即停止并清除变量):')
92
+ out.info(` 本地网关 ${gateway}`)
93
+ out.info(` NAE_LLM_GATEWAY = ${gateway}`)
94
+ out.info(` NAE_LLM_API_KEY = ${localKey}`)
95
+ out.info('OpenAI 客户端 base=${NAE_LLM_GATEWAY}/v1;Anthropic SDK base=${NAE_LLM_GATEWAY}。')
96
+ out.info(reloadHint())
97
+ })
98
+
99
+ const timer = liveLine(() => `活动连接 ${active} | ↑ ${humanBytes(up)} | ↓ ${humanBytes(down)}`)
100
+ onShutdown(() => {
101
+ clearInterval(timer)
102
+ clearEnv(['NAE_LLM_GATEWAY', 'NAE_LLM_API_KEY'])
103
+ process.stderr.write('\n已停止 LLM 转发,全局环境变量已清除。\n')
104
+ server.close(() => resolve())
105
+ process.exit(0)
106
+ })
107
+ })
108
+ }
109
+
110
+ // --- 隧道 ---
111
+
112
+ // bridge 一条本地 TCP 连接 ↔ 一条平台 WS 隧道:本地数据缓冲到隧道就绪后发,回包写回本地。
113
+ function bridge(cfg, path, sock, cb) {
114
+ const ws = new WebSocket(wsBase(cfg) + path, {
66
115
  headers: { Authorization: `Bearer ${cfg.token}`, 'X-NAE-CLI-Version': CLI_VERSION },
67
116
  })
68
117
  ws.binaryType = 'nodebuffer'
@@ -105,6 +154,31 @@ function bridge(cfg, appid, remotePort, sock, cb) {
105
154
  })
106
155
  }
107
156
 
157
+ // onShutdown 在所有可捕获的退出信号上跑一次清理(Ctrl-C=SIGINT、Ctrl-Break=SIGBREAK、kill=SIGTERM、挂断=SIGHUP)。
158
+ // 多注册一次只跑一次(once 兜底),尽量不给用户留下指向死端口的残留环境变量。SIGKILL/强杀无法捕获,认栽。
159
+ function onShutdown(fn) {
160
+ let ran = false
161
+ const once = () => {
162
+ if (ran) return
163
+ ran = true
164
+ fn()
165
+ }
166
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP']) {
167
+ try {
168
+ process.on(sig, once)
169
+ } catch {
170
+ // 个别平台不支持某信号,忽略。
171
+ }
172
+ }
173
+ }
174
+
175
+ // liveLine 实时流量行(覆盖式单行,写 stderr 不污染可能被重定向的 stdout)。
176
+ function liveLine(text) {
177
+ return setInterval(() => {
178
+ process.stderr.write(`\r${text()} `)
179
+ }, 1000)
180
+ }
181
+
108
182
  // wsBase 把 apiBase 的 http(s):// 换成 ws(s)://。
109
183
  function wsBase(cfg) {
110
184
  return apiBase(cfg).replace(/^http/, 'ws')
@@ -122,36 +196,17 @@ function humanBytes(n) {
122
196
  return (i === 0 ? v : v.toFixed(1)) + u[i]
123
197
  }
124
198
 
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
- }
199
+ // --- 全局环境变量读写(跨平台)---
147
200
 
148
- // applyEnv 写用户级全局环境变量。Windows: setx;Unix: 重写 profile 内的托管块。
201
+ // applyEnv 写用户级全局环境变量(新开终端生效)。
202
+ // Windows 用 reg add 直写 HKCU\Environment——不用 setx:setx 会同步广播 WM_SETTINGCHANGE 给所有窗口,
203
+ // 某个窗口不响应就把本进程的事件循环卡死,转发随之假死。reg add 不广播、瞬时返回,与 clearEnv 的 reg delete 对称。
149
204
  function applyEnv(pairs) {
150
205
  if (isWin) {
151
206
  for (const [k, v] of Object.entries(pairs)) {
152
- const r = spawnSync('setx', [k, v], { encoding: 'utf8' })
207
+ const r = spawnSync('reg', ['add', 'HKCU\\Environment', '/v', k, '/t', 'REG_SZ', '/d', v, '/f'], { encoding: 'utf8' })
153
208
  if (r.status !== 0) {
154
- throw new Error(`setx ${k} 失败:${(r.stderr || r.stdout || r.error?.message || '').trim()}`)
209
+ throw new Error(`写入环境变量 ${k} 失败:${(r.stderr || r.stdout || r.error?.message || '').trim()}`)
155
210
  }
156
211
  }
157
212
  } else {
@@ -215,6 +270,6 @@ function shellQuote(s) {
215
270
  // reloadHint 提示新值在新终端才生效。
216
271
  function reloadHint() {
217
272
  return isWin
218
- ? '提示:已写入用户级环境变量(setx),新开一个终端窗口生效。'
219
- : `提示:已写入 ${unixProfile()},执行 source 它或新开终端生效。`
273
+ ? '提示:已写入用户级环境变量,在另开的新终端里生效(已打开的终端不会自动刷新)。'
274
+ : `提示:已写入 ${unixProfile()},在另开的新终端或 source 后生效。`
220
275
  }
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