oh-my-agent-web 1.0.2 → 1.0.3

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 CHANGED
@@ -78,15 +78,21 @@ The three commands are completely equivalent; everything below is written as `oh
78
78
 
79
79
  | Command | What it does |
80
80
  |---|---|
81
- | `oh-my-agent-web` | **Start (default)**: runs in the background and opens the browser, handing the terminal straight back; default workspace = current directory |
82
- | `oh-my-agent-web start -f` | Run in the foreground, logs right in the terminal, `Ctrl-C` to quit |
81
+ | `oh-my-agent-web` | **Start (default)**: runs in the foreground and opens the browser, logs right in the terminal, `Ctrl-C` to quit; default workspace = current directory |
82
+ | `oh-my-agent-web start -d` | Run in the background (detached, logs go to `daemon.log`), handing the terminal straight back |
83
83
  | `oh-my-agent-web status` | pid / port / uptime / engine / session count / workspace |
84
- | `oh-my-agent-web restart` | Stop then start (background by default; `-f` for foreground) |
84
+ | `oh-my-agent-web restart` | Stop then start (foreground by default; `-d` for background) |
85
85
  | `oh-my-agent-web stop` | Stop the running instance (foreground or background — it reads the lock file in the data dir instead of guessing a port) |
86
86
  | `oh-my-agent-web version` | Version string (`-v` / `--version` are synonyms) |
87
87
  | `oh-my-agent-web upgrade` | Upgrade to the latest release (it runs `npm -g install oh-my-agent-web@latest`) |
88
88
  | `oh-my-agent-web help` | Show help (`-h` / `--help` are synonyms) |
89
89
 
90
+ Two behavioural rules:
91
+
92
+ - **Starting twice is detected first**: if an instance is already running it only prints the URL (and opens the page) instead of starting a second one — identical for `aw` / `agent-web` / `oh-my-agent-web`.
93
+ - **The foreground run stays quiet**: the terminal only gets one `已启动:…` line plus warn / error; normal logs (session index loading, SSE connections) still go in full to `logs/agent-web.log`. Watch them with `AGENT_WEB_LOG=info aw`; in the background (`-d`) they land in `daemon.log`.
94
+ - **It checks for a newer release**: on start (and on `aw status`) it asks npm once per 24 hours (cached in `update-check.json`) and prints `有新版本 vX(当前 vY)→ 升级:aw upgrade` when one exists. It never upgrades by itself, stays silent when the network is down, and `AGENT_WEB_UPDATE_CHECK=off` turns it off.
95
+
90
96
  ### Flags
91
97
 
92
98
  | Flag | Env var | Default | Notes |
@@ -94,7 +100,7 @@ The three commands are completely equivalent; everything below is written as `oh
94
100
  | `-p, --port` | `AGENT_WEB_PORT` | `25257` | Listen port |
95
101
  | `-H, --host` | `AGENT_WEB_HOST` | `127.0.0.1` | Listen address; only change it for remote access |
96
102
  | `-P, --password` | `AGENT_WEB_PASSWORD` | none | Login password; unset = loopback only (a remote first visit asks you to set one) |
97
- | `-f, --foreground` | — | off (background by default) | Foreground run, logs in the terminal; `-d` / `--detach` is the old spelling and now equals the default |
103
+ | `-d, --detach` | — | off (foreground by default) | Background run, detached, logs go to `~/.oh-my-agent-web/daemon.log`; `-f` / `--foreground` is the old spelling and now equals the default |
98
104
  | `--no-open` | — | on | Don't open the browser |
99
105
  | `--allow-host` | `AGENT_WEB_ALLOWED_HOSTS` | loopback only | Extra allowed hostnames / LAN IPs (repeatable or comma-separated) |
100
106
  | `--data-dir` | `AGENT_WEB_DATA_DIR` | `~/.oh-my-agent-web` | Settings, password, uploads, logs and the lock file live here |
@@ -102,13 +108,14 @@ The three commands are completely equivalent; everything below is written as `oh
102
108
  | — | `AGENT_WEB_ENGINE` | `pi` | Default engine |
103
109
  | — | `AGENT_WEB_APPROVAL_MODE` | `allow` | Default approval mode for new sessions; `ask` confirms every tool call |
104
110
  | — | `AGENT_WEB_TURN_TIMEOUT_MS` | 20 minutes | Per-turn watchdog timeout |
105
- | — | `AGENT_WEB_LOG` | `info` | `debug` / `info` / `warn` / `error` |
111
+ | — | `AGENT_WEB_LOG` | `info` (foreground terminal: warn / error only) | `debug` / `info` / `warn` / `error`; setting it explicitly also prints that level to the terminal |
112
+ | — | `AGENT_WEB_UPDATE_CHECK` | `on` | `off` disables the startup release check (offline / intranet / CI) |
106
113
 
107
114
  ### Common examples
108
115
 
109
116
  ```bash
110
- oh-my-agent-web # background + open the page (the usual one)
111
- oh-my-agent-web -f # foreground, logs in your face, Ctrl-C to quit
117
+ oh-my-agent-web # foreground + open the page (the usual one, Ctrl-C to quit)
118
+ oh-my-agent-web -d # background, terminal handed straight back
112
119
  oh-my-agent-web -p 25258 # different port (when 25257 is taken)
113
120
  oh-my-agent-web status && oh-my-agent-web restart # check, then restart
114
121
  oh-my-agent-web stop # stop it (works for foreground and background)
package/README.md CHANGED
@@ -77,15 +77,21 @@ powershell -c "irm https://raw.githubusercontent.com/duzhenxun/oh-my-agent-web/m
77
77
 
78
78
  | 命令 | 作用 |
79
79
  |---|---|
80
- | `oh-my-agent-web` | **启动(默认)**:后台跑起来 + 打开浏览器,终端立刻还给你;默认工作区 = 当前目录 |
81
- | `oh-my-agent-web start -f` | 前台运行,日志直接打在终端,`Ctrl-C` 退出 |
80
+ | `oh-my-agent-web` | **启动(默认)**:前台跑起来 + 打开浏览器,日志直接打在终端,`Ctrl-C` 退出;默认工作区 = 当前目录 |
81
+ | `oh-my-agent-web start -d` | 后台运行(脱离终端,日志进 `daemon.log`),终端立刻还给你 |
82
82
  | `oh-my-agent-web status` | 看状态:pid / 端口 / 已运行多久 / 引擎 / 会话数 / 工作区 |
83
- | `oh-my-agent-web restart` | 先停再起(默认仍后台;带 `-f` 则前台) |
83
+ | `oh-my-agent-web restart` | 先停再起(默认仍前台;带 `-d` 则后台) |
84
84
  | `oh-my-agent-web stop` | 停掉正在跑的实例(前台、后台都行 —— 读的是数据目录里的锁文件,不靠猜端口) |
85
85
  | `oh-my-agent-web version` | 版本号(`-v` / `--version` 同义) |
86
86
  | `oh-my-agent-web upgrade` | 升级到最新发行版(内部就是 `npm -g install oh-my-agent-web@latest`) |
87
87
  | `oh-my-agent-web help` | 显示帮助(`-h` / `--help` 同义) |
88
88
 
89
+ 两条行为约定:
90
+
91
+ - **重复启动会先检测**:已经在跑就只把地址打给你(并打开页面),不会再起第二个 —— `aw` / `agent-web` / `oh-my-agent-web` 三个名字完全一致。
92
+ - **前台不刷日志**:终端只有一行「已启动:…」和 warn / error;装载会话索引、SSE 连接这类正常日志仍然全量写进 `logs/agent-web.log`。想盯着看就 `AGENT_WEB_LOG=info aw`,后台(`-d`)的输出在 `daemon.log`。
93
+ - **会顺手查新版**:启动时(以及 `aw status`)看一眼 npm 上有没有新版本,24 小时内只问一次(缓存 `update-check.json`);有新版就打一行 `有新版本 vX(当前 vY)→ 升级:aw upgrade`。不自动升级、网络不通就静默跳过,`AGENT_WEB_UPDATE_CHECK=off` 可彻底关掉。
94
+
89
95
  ### 参数
90
96
 
91
97
  | 参数 | 环境变量 | 默认 | 说明 |
@@ -93,7 +99,7 @@ powershell -c "irm https://raw.githubusercontent.com/duzhenxun/oh-my-agent-web/m
93
99
  | `-p, --port` | `AGENT_WEB_PORT` | `25257` | 监听端口 |
94
100
  | `-H, --host` | `AGENT_WEB_HOST` | `127.0.0.1` | 监听地址;对外访问才需要改 |
95
101
  | `-P, --password` | `AGENT_WEB_PASSWORD` | 无 | 访问密码;不设 = 只允许本机(远程首次打开会先让你设) |
96
- | `-f, --foreground` | — | 关(默认后台) | 前台运行,日志打在终端;`-d` / `--detach` 是旧写法,现在等于默认 |
102
+ | `-d, --detach` | — | 关(默认前台) | 后台运行,脱离终端,日志进 `~/.oh-my-agent-web/daemon.log`;`-f` / `--foreground` 是旧写法,现在等于默认 |
97
103
  | `--no-open` | — | 开 | 不在浏览器里打开页面 |
98
104
  | `--allow-host` | `AGENT_WEB_ALLOWED_HOSTS` | 只认 loopback | 额外允许的域名 / 局域网 IP(可多次或逗号分隔) |
99
105
  | `--data-dir` | `AGENT_WEB_DATA_DIR` | `~/.oh-my-agent-web` | 设置、密码、上传、日志、锁文件都放这 |
@@ -101,13 +107,14 @@ powershell -c "irm https://raw.githubusercontent.com/duzhenxun/oh-my-agent-web/m
101
107
  | — | `AGENT_WEB_ENGINE` | `pi` | 默认引擎 |
102
108
  | — | `AGENT_WEB_APPROVAL_MODE` | `allow` | 新会话默认审批模式;`ask` 则逐次确认 |
103
109
  | — | `AGENT_WEB_TURN_TIMEOUT_MS` | 20 分钟 | 单回合看门狗超时 |
104
- | — | `AGENT_WEB_LOG` | `info` | `debug` / `info` / `warn` / `error` |
110
+ | — | `AGENT_WEB_LOG` | `info`(前台终端只打 warn / error) | `debug` / `info` / `warn` / `error`;显式设了就按这个级别打到终端 |
111
+ | — | `AGENT_WEB_UPDATE_CHECK` | `on` | `off` 关掉启动时的升级检查(离线 / 内网 / CI 用) |
105
112
 
106
113
  ### 常用例子
107
114
 
108
115
  ```bash
109
- oh-my-agent-web # 后台起 + 打开页面(最常用)
110
- oh-my-agent-web -f # 前台起,日志直接看,Ctrl-C 退出
116
+ oh-my-agent-web # 前台起 + 打开页面(最常用,Ctrl-C 退出)
117
+ oh-my-agent-web -d # 后台起,终端立刻还给你
111
118
  oh-my-agent-web -p 25258 # 换端口(25257 被占时)
112
119
  oh-my-agent-web status && oh-my-agent-web restart # 看完状态再重启
113
120
  oh-my-agent-web stop # 停掉(前台/后台都行)
@@ -15,11 +15,11 @@
15
15
  * -P, --password <pwd> AGENT_WEB_PASSWORD 非本机访问时的登录密码
16
16
  * --allow-host <h> AGENT_WEB_ALLOWED_HOSTS 额外允许的域名(可重复)
17
17
  * --data-dir <dir> AGENT_WEB_DATA_DIR 默认 ~/.oh-my-agent-web
18
- * -f, --foreground 前台运行(日志直接在终端,Ctrl-C 退出)
19
- * -d, --detach 后台运行(**现在就是默认**,留着给老脚本)
18
+ * -d, --detach 后台运行(脱离终端,日志进 daemon.log)
19
+ * -f, --foreground 前台运行(就是默认,留着给老脚本)
20
20
  * --no-open 不要在浏览器里打开页面
21
21
  *
22
- * 默认行为:**后台起来 + 打开浏览器**(终端立刻还给你);`-f` 才留在前台。
22
+ * 默认行为:**前台起来 + 打开浏览器**(日志直接打在终端,Ctrl-C 退出);`-d` 才丢到后台。
23
23
  * 后台模式只是「spawn 一个脱离终端的自己 + 等它写好 server.lock」:
24
24
  * 真正的事实源仍然是锁文件,所以 stop / restart 对前台、后台一视同仁。
25
25
  *
@@ -36,14 +36,15 @@ import { applyStoredOptions } from "./cli-config.js";
36
36
  import { migrateLegacyDataDir } from "./util/data-dir.js";
37
37
  import { DEFAULT_DATA_DIR_NAME } from "./util/env.js";
38
38
  import { fetchHealth, showStatus, showVersion, upgrade } from "./cli-info.js";
39
- import { defaultIsAlive, readLock } from "./session/lock.js";
39
+ import { checkForUpdate, describeUpdate } from "./update-check.js";
40
+ import { defaultIsAlive, describeHolder, readLock } from "./session/lock.js";
40
41
  const USAGE = `用法: agent-web [命令] [选项] (agent-web / aw / oh-my-agent-web 三个命令完全等价)
41
42
 
42
43
  命令:
43
- start 启动(默认;后台运行并打开页面,终端立刻还给你)
44
- start -f 前台运行(日志直接打在终端,Ctrl-C 退出)
44
+ start 启动(默认前台;已在跑就只把地址给你,不会再起一个)
45
+ start -d 后台运行(脱离终端,日志进 daemon.log,终端立刻还给你)
45
46
  stop 停止正在跑的实例(读 dataDir/server.lock 里的 pid)
46
- restart 先停再起(默认仍后台;带 -f 则前台)
47
+ restart 先停再起(默认仍前台;带 -d 则后台)
47
48
  status 看状态:pid / 端口 / 已运行多久 / 引擎 / 会话数 / 工作区
48
49
  version 看版本号(-v / --version 同义)
49
50
  upgrade 升级全局安装(npm i -g <包名>@latest;private 包会拒绝并提示怎么做)
@@ -55,24 +56,24 @@ const USAGE = `用法: agent-web [命令] [选项] (agent-web / aw / oh
55
56
  -P, --password <pwd> 访问密码 AGENT_WEB_PASSWORD
56
57
  --allow-host <h> 额外允许的域名 AGENT_WEB_ALLOWED_HOSTS(可多次 / 逗号分隔)
57
58
  --data-dir <dir> 数据目录 AGENT_WEB_DATA_DIR(默认 ~/.oh-my-agent-web)
58
- -f, --foreground 前台运行(Ctrl-C 退出)
59
- -d, --detach 后台运行(现在就是默认,留着给老脚本)
59
+ -d, --detach 后台运行(脱离终端;日志见 daemon.log)
60
+ -f, --foreground 前台运行(就是默认,留着给老脚本)
60
61
  --no-open 不要在浏览器里打开页面
61
62
 
62
63
  没给 --password / --allow-host 时,会读数据目录里的 password / allowed-hosts 兜底
63
64
  (设一次就一直在,重启不必重复输入)。远程第一次打开时若还没设密码,页面会让你先设一个。
64
65
 
65
66
  例:
66
- aw # 后台起 + 打开页面
67
- aw -f # 前台起(Ctrl-C 退出)
68
- aw -H 0.0.0.0 --allow-host agent.example.com # 后台起,供外网访问(首次打开会先让你设密码)
67
+ aw # 前台起 + 打开页面(Ctrl-C 退出)
68
+ aw -d # 后台起,终端立刻还给你
69
+ aw -H 0.0.0.0 --allow-host agent.example.com # 供外网访问(首次打开会先让你设密码)
69
70
  aw status && aw restart # 看完状态再重启
70
71
  aw stop # 停掉(前台/后台都行)`;
71
72
  function parseArgs(argv) {
72
73
  const env = {};
73
74
  let command = "start";
74
75
  let help = false;
75
- let foreground = false;
76
+ let foreground = true;
76
77
  let open = true;
77
78
  for (let i = 0; i < argv.length; i += 1) {
78
79
  const arg = argv[i] ?? "";
@@ -334,7 +335,8 @@ async function startDetached(rawArgv, open) {
334
335
  const child = spawn(process.execPath, [script, ...args], {
335
336
  detached: true, // 脱离当前终端:关掉终端不会把它一起收走
336
337
  stdio: ["ignore", fd, fd],
337
- env: process.env,
338
+ // 子进程的 stdout 是 daemon.log:别让 quietConsole 把它的正常日志吞了(那是后台模式唯一的现场)
339
+ env: { ...process.env, AGENT_WEB_FOREGROUND: "0" },
338
340
  cwd: process.cwd(),
339
341
  // Windows 上 detached 会连带开一个 console:不关掉就会闪一下黑框
340
342
  windowsHide: true,
@@ -369,6 +371,48 @@ async function startDetached(rawArgv, open) {
369
371
  console.error(tailOf(log, 20));
370
372
  process.exitCode = 1;
371
373
  }
374
+ /**
375
+ * 起来之前先看锁(ADR-0007):已经有一个在跑,就不起第二个。
376
+ *
377
+ * 为什么放在 CLI 而不是只靠 `index.ts` 里的 `acquireLock`:那边要先把会话索引、价格表
378
+ * 都装完才走到拿锁那一步 —— 用户敲完 `aw` 干等几秒,最后看到一条 error;`-d` 更糟,
379
+ * 父进程发现子进程退出,只能甩一段 daemon.log 出来。
380
+ *
381
+ * 三个命令名(`aw` / `agent-web` / `oh-my-agent-web`)本来就走同一份 cli.js,
382
+ * 所以这一处检测对三者完全一致。
383
+ *
384
+ * @returns true = 已经有一个在跑,本次不再启动
385
+ */
386
+ async function detectRunning(open) {
387
+ const lock = await readLock(dataDir());
388
+ // 无锁 / 锁里的 pid 已经不在(陈旧锁):照常启动,接管交给 index.ts 的 acquireLock
389
+ if (!lock || !defaultIsAlive(lock.pid))
390
+ return false;
391
+ const probeHost = probeHostOf();
392
+ const url = `http://${probeHost}:${lock.port}`;
393
+ const stop = stopHint(process.argv.slice(2));
394
+ if (await fetchHealth(probeHost, lock.port, 1500)) {
395
+ console.log(`已经有一个在跑,不再起第二个:\n ${describeHolder(lock)}\n → 打开 ${url}\n → 停止:${stop}`);
396
+ if (open)
397
+ openBrowser(url);
398
+ return true;
399
+ }
400
+ // 锁在、pid 也在,但端口不响应:可能还在启动(装会话索引),也可能卡死了。
401
+ // 两种情况都不该再起一个 —— 那样只会在拿锁/绑端口时撞得莫名其妙。
402
+ console.error(`已经有一个在跑,但 ${url} 暂时没响应:\n ${describeHolder(lock)}`);
403
+ console.error(` → 还在启动就等一下;卡住了就收掉它:${stop}`);
404
+ process.exitCode = 1;
405
+ return true;
406
+ }
407
+ /** 用户敲的是哪个名字(aw / agent-web / oh-my-agent-web):启动提示里照拄,不硬写 aw */
408
+ function invokedName() {
409
+ const base = (process.argv[1] ?? "").split(/[\\/]/).pop() ?? "";
410
+ // 直接用 node 跑文件(开发、脚本、npx)时 argv[1] 是 cli.js / cli.ts,没有任何命令名可用
411
+ if (base === "" || base === "cli.js" || base === "cli.ts")
412
+ return "aw";
413
+ // Windows 上 npm 的 bin 是 .cmd(aw.cmd)
414
+ return base.replace(/\.(cmd|exe|ps1|bat)$/i, "");
415
+ }
372
416
  /** 是否被当脚本执行(npx / 全局安装时 argv[1] 是 bin 软链,必须比 realpath) */
373
417
  function invokedDirectly() {
374
418
  const entry = process.argv[1];
@@ -394,6 +438,9 @@ async function main() {
394
438
  await applyStoredOptions(process.env, dataDir());
395
439
  if (parsed.command === "status") {
396
440
  await showStatus({ dataDir: dataDir(), probeHost: probeHostOf() });
441
+ const line = describeUpdate(await checkForUpdate({ dataDir: dataDir() }).catch(() => null));
442
+ if (line)
443
+ console.log(` ${line}`);
397
444
  return;
398
445
  }
399
446
  if (parsed.command === "version") {
@@ -413,14 +460,31 @@ async function main() {
413
460
  // 等端口真正释放再起,否则会撞 EADDRINUSE
414
461
  await sleep(400);
415
462
  }
416
- // start / restart:默认**后台**跑(终端立刻还给你),`-f` 才留在前台
463
+ // start / restart:先确认不是「已经在跑了」(三个命令名共用这一处检测)
464
+ if (await detectRunning(parsed.open))
465
+ return;
466
+ // 顺手查一下有没有新版:启动时就并发问,别让它拖启动(缓存 24h,首次最多 1.5s 超时)
467
+ const update = checkForUpdate({ dataDir: dataDir() }).catch(() => null);
468
+ // start / restart:默认**前台**跑(日志直接打在终端,Ctrl-C 退出),`-d` 才丢到后台
417
469
  if (!parsed.foreground) {
418
470
  await startDetached(process.argv.slice(2), parsed.open);
471
+ const line = describeUpdate(await update);
472
+ if (line)
473
+ console.log(line);
419
474
  return;
420
475
  }
421
476
  if (parsed.open)
422
477
  openWhenReady();
478
+ // 告诉服务层「终端在看着」:只留 warn / error,正常日志光进文件(见 index.ts 的 quietConsole)。
479
+ // 必须看 isTTY:后台子进程也走这个分支,但它的 stdout 是 daemon.log —— 那里要保留全部日志。
480
+ if (process.stdout.isTTY)
481
+ process.env.AGENT_WEB_FOREGROUND = "1";
482
+ // 启动提示里的命令名(aw / agent-web / oh-my-agent-web)由实际敲的那个决定
483
+ process.env.AGENT_WEB_CLI_NAME = invokedName();
423
484
  await import("./index.js");
485
+ const line = describeUpdate(await update);
486
+ if (line)
487
+ console.log(line);
424
488
  }
425
489
  // 被当脚本执行时才 main(被 import 时什么都不做)
426
490
  if (invokedDirectly()) {
@@ -64,11 +64,21 @@ const { config, problems: envProblems, notes: envNotes, workspaceFallback, } = r
64
64
  cwd: process.cwd(),
65
65
  });
66
66
  let fileSink = null;
67
+ /**
68
+ * 前台运行(CLI 默认):终端只留 warn / error。
69
+ *
70
+ * 以前后台才是默认,这些 info(装载会话索引、每来一个客户端一条「SSE 已连接」……)都落在
71
+ * daemon.log 里看不见;现在前台成了默认,再照旧打就会把用户自己的输机记录冲没。
72
+ * 它们仍然**全部**进轮转日志文件;显式给了 `AGENT_WEB_LOG=debug|info` 就按用户的意思正常打。
73
+ */
74
+ const foreground = process.env.AGENT_WEB_FOREGROUND === "1";
75
+ const quietConsole = foreground && !process.env.AGENT_WEB_LOG;
67
76
  const logger = createLogger({
68
77
  level: config.logLevel,
69
78
  scope: "app",
70
- sink: (line) => {
71
- process.stdout.write(`${line}\n`);
79
+ sink: (line, level) => {
80
+ if (!quietConsole || level === "warn" || level === "error")
81
+ process.stdout.write(`${line}\n`);
72
82
  fileSink?.write(line);
73
83
  },
74
84
  });
@@ -137,7 +147,9 @@ const engineWiring = createEngineWiring({
137
147
  workspace: config.workspace,
138
148
  piSessions,
139
149
  onNotice: (text, level) => runtimeRef.current?.notify(text, level),
140
- onLog: (text, meta) => logger.warn(text, meta),
150
+ // 引擎子进程的 stderr / 诊断输出:正常运行时它是噪声(codex、dsh 都会吐),
151
+ // 真正要用户知道的事走 onNotice(界面提示)。所以记 info 光进文件,别占终端。
152
+ onLog: (text, meta) => logger.info(text, meta),
141
153
  });
142
154
  const engineCatalog = engineWiring.catalog;
143
155
  const resolved = engineWiring.resolved;
@@ -406,6 +418,19 @@ async function main() {
406
418
  void releaseLock(config.dataDir, process.pid).finally(() => process.exit(1));
407
419
  });
408
420
  await new Promise((resolve) => httpServer.listen(config.port, config.host, resolve));
421
+ if (foreground) {
422
+ // 正常日志已经不在终端刷了,这里给一行「真的跑起来了」的确认 + 最常用的四条命令。
423
+ // 命令名照用户实际敲的那个(aw / agent-web / oh-my-agent-web),由 cli.ts 传进来。
424
+ const shownHost = config.host === "0.0.0.0" || config.host === "::" || config.host === "" ? "127.0.0.1" : config.host;
425
+ const cli = process.env.AGENT_WEB_CLI_NAME || "aw";
426
+ console.log(`已启动:http://${shownHost}:${config.port} (Ctrl-C 退出)`);
427
+ console.log("");
428
+ console.log(` 后台运行:${cli} -d`);
429
+ console.log(` 重启 / 停止:${cli} restart · ${cli} stop`);
430
+ console.log(` 远程访问:${cli} -d -H 0.0.0.0 -P <密码>(用域名访问再加 --allow-host <域名>)`);
431
+ console.log(` 全部选项:${cli} --help`);
432
+ console.log("");
433
+ }
409
434
  logger.info("agent-web 已启动", {
410
435
  url: `http://${config.host}:${config.port}`,
411
436
  workspace: config.workspace,
@@ -0,0 +1,127 @@
1
+ /**
2
+ * 「有没有新版」:启动时顺手看一眼(`aw upgrade` 的自动版)。
3
+ *
4
+ * 为什么要它:默认前台启动之后,用户很少再想起去 `aw status` / `aw upgrade`,
5
+ * 于是装了旧版也不知道 —— 而 upgrade 是**手动**的,不提醒就永远不会发生。
6
+ *
7
+ * 四条纪律:
8
+ * 1. **不挡启动**:整件事异步跑,网络失败 / 超时(1.5 秒)就当没这回事,绝不抛错;
9
+ * 2. **不每次都问 npm**:结果按 24 小时缓存在 `<dataDir>/update-check.json`;
10
+ * 3. **能关**:`AGENT_WEB_UPDATE_CHECK=off`(离线 / 内网 / CI);
11
+ * 4. **只报告,不替用户升级**:升级可能打断正在跑的回合,必须由人来按(`aw upgrade`)。
12
+ */
13
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+ import { packageMeta } from "./util/version.js";
16
+ const CACHE_FILE = "update-check.json";
17
+ /** 缓存 24 小时:够新鲜,也不至于敲一次 `aw` 就问一次 npm */
18
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
19
+ const FETCH_TIMEOUT_MS = 1500;
20
+ /** `v1.2.3` / `1.2.3-rc.1` → 数字段 + 预发布标记 */
21
+ function parseVersion(value) {
22
+ const cleaned = value.trim().replace(/^v/i, "");
23
+ const dash = cleaned.indexOf("-");
24
+ const core = dash === -1 ? cleaned : cleaned.slice(0, dash);
25
+ const pre = dash === -1 ? null : cleaned.slice(dash + 1);
26
+ const numbers = core.split(".").map((part) => {
27
+ const parsed = Number.parseInt(part, 10);
28
+ return Number.isFinite(parsed) ? parsed : 0;
29
+ });
30
+ return { numbers, pre };
31
+ }
32
+ /**
33
+ * `candidate` 是否比 `current` 新(纯函数,好测)。
34
+ *
35
+ * 规则按 semver 够用的部分来:先比数字段(缺的位按 0 补),数字一样时
36
+ * **正式版比预发布版新**(`1.0.0` > `1.0.0-rc.1`),两个都是预发布就按字符串比(`rc.2` > `rc.1`)。
37
+ */
38
+ export function isNewerVersion(candidate, current) {
39
+ const next = parseVersion(candidate);
40
+ const now = parseVersion(current);
41
+ const length = Math.max(next.numbers.length, now.numbers.length);
42
+ for (let index = 0; index < length; index += 1) {
43
+ const a = next.numbers[index] ?? 0;
44
+ const b = now.numbers[index] ?? 0;
45
+ if (a !== b)
46
+ return a > b;
47
+ }
48
+ if (next.pre === now.pre)
49
+ return false;
50
+ if (next.pre === null)
51
+ return true;
52
+ if (now.pre === null)
53
+ return false;
54
+ return next.pre > now.pre;
55
+ }
56
+ function cachePath(dataDir) {
57
+ return join(dataDir, CACHE_FILE);
58
+ }
59
+ function readCache(dataDir) {
60
+ try {
61
+ const parsed = JSON.parse(readFileSync(cachePath(dataDir), "utf8"));
62
+ if (typeof parsed.checkedAt !== "number" || typeof parsed.latest !== "string")
63
+ return null;
64
+ return { checkedAt: parsed.checkedAt, latest: parsed.latest };
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ function writeCache(dataDir, entry) {
71
+ try {
72
+ mkdirSync(dirname(cachePath(dataDir)), { recursive: true });
73
+ writeFileSync(cachePath(dataDir), `${JSON.stringify(entry)}\n`);
74
+ }
75
+ catch {
76
+ // 写不进去(只读目录 / 权限)不影响任何功能,下次再查就是了
77
+ }
78
+ }
79
+ /** 问 npm registry 要 latest 版本号;拿不到就 null(网络不通是常态,不是错误) */
80
+ async function fetchLatestVersion(name) {
81
+ const registry = (process.env.AGENT_WEB_NPM_REGISTRY ?? "https://registry.npmjs.org").replace(/\/+$/, "");
82
+ try {
83
+ const response = await fetch(`${registry}/${name.replace("/", "%2F")}/latest`, {
84
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
85
+ // 只要版本号,别把整个 manifest 拉下来
86
+ headers: { accept: "application/vnd.npm.install-v1+json" },
87
+ });
88
+ if (!response.ok)
89
+ return null;
90
+ const body = (await response.json());
91
+ return typeof body.version === "string" ? body.version : null;
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * 查一次「要不要升级」。返回值 `null` = 不用提示(关掉了 / 没发布 / 网络不通)。
99
+ *
100
+ * `now` 可注入,测试与「缓存过期」判断都靠它。
101
+ */
102
+ export async function checkForUpdate(options) {
103
+ const flag = process.env.AGENT_WEB_UPDATE_CHECK;
104
+ if (flag === "off" || flag === "0" || flag === "false")
105
+ return null;
106
+ const meta = packageMeta();
107
+ // private = 没发布到 npm(`aw upgrade` 也拒绝),版本 unknown = 连 package.json 都没读到
108
+ if (meta.private || meta.version === "unknown")
109
+ return null;
110
+ const now = options.now?.() ?? Date.now();
111
+ const cached = readCache(options.dataDir);
112
+ let latest = cached && now - cached.checkedAt < CACHE_TTL_MS && cached.checkedAt <= now ? cached.latest : null;
113
+ if (!latest) {
114
+ const fetched = await fetchLatestVersion(meta.name);
115
+ if (!fetched)
116
+ return null; // 查不到就不提示,也不写缓存(下次启动再试)
117
+ latest = fetched;
118
+ writeCache(options.dataDir, { checkedAt: now, latest });
119
+ }
120
+ return { current: meta.version, latest, hasUpdate: isNewerVersion(latest, meta.version) };
121
+ }
122
+ /** 一行给人看的提示;`null` = 不用说话 */
123
+ export function describeUpdate(result) {
124
+ if (!result?.hasUpdate)
125
+ return null;
126
+ return `有新版本 v${result.latest}(当前 v${result.current})→ 升级:aw upgrade`;
127
+ }
@@ -40,7 +40,7 @@ export function createLogger(options) {
40
40
  if (LEVEL_ORDER[level] < threshold)
41
41
  return;
42
42
  const safeCtx = ctx ? redactLogContext(ctx) : undefined;
43
- sink(formatLogLine({ ts: now(), level, scope, msg: message, ...(safeCtx ? { ctx: safeCtx } : {}) }));
43
+ sink(formatLogLine({ ts: now(), level, scope, msg: message, ...(safeCtx ? { ctx: safeCtx } : {}) }), level);
44
44
  };
45
45
  return {
46
46
  debug: (message, ctx) => write("debug", message, ctx),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-agent-web",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "bin": {
5
5
  "oh-my-agent-web": "dist/server/cli.js",
6
6
  "aw": "dist/server/cli.js",