oh-my-agent-web 1.0.1 → 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.
Files changed (74) hide show
  1. package/README.en.md +14 -7
  2. package/README.md +14 -7
  3. package/dist/server/cli.js +79 -15
  4. package/dist/server/engine/builtin.js +2 -0
  5. package/dist/server/engine/codex.js +10 -8
  6. package/dist/server/engine/dsh-prompt-patch.js +48 -0
  7. package/dist/server/engine/pi-sdk.js +20 -3
  8. package/dist/server/engine/suggestion-guidance.js +22 -9
  9. package/dist/server/engine/wiring.js +11 -0
  10. package/dist/server/index.js +44 -4
  11. package/dist/server/session/settings.js +15 -0
  12. package/dist/server/update-check.js +127 -0
  13. package/dist/server/util/log.js +1 -1
  14. package/package.json +1 -1
  15. package/web/dist/assets/{arc-zmD1IRwE.js → arc-Df9SI9yh.js} +1 -1
  16. package/web/dist/assets/{architecture-WOLXFQ4H-C5B92zMK.js → architecture-WOLXFQ4H-iO49E8Y1.js} +1 -1
  17. package/web/dist/assets/{blockDiagram-RJAE7QA3-BOZKv8NT.js → blockDiagram-RJAE7QA3-D8d1ukIs.js} +1 -1
  18. package/web/dist/assets/channel-C2-OT56h.js +1 -0
  19. package/web/dist/assets/{chunk-55XCU76K-Cqr92VtU.js → chunk-55XCU76K-C5wOvqDb.js} +1 -1
  20. package/web/dist/assets/{chunk-7S6XSTFO-DDqM0lw8.js → chunk-7S6XSTFO-WIrWcDbG.js} +1 -1
  21. package/web/dist/assets/{chunk-AQDTSKZB-CeUIfGt5.js → chunk-AQDTSKZB-OkzYqFsw.js} +1 -1
  22. package/web/dist/assets/{chunk-DSNLSHJZ-Bf4nxMnG.js → chunk-DSNLSHJZ-CHEA9kYd.js} +1 -1
  23. package/web/dist/assets/{chunk-HNE4Q36P-CP_5bmzn.js → chunk-HNE4Q36P-CPsq34CI.js} +1 -1
  24. package/web/dist/assets/{chunk-VQ5W62QI-C4KlhrWA.js → chunk-VQ5W62QI-CamPnEuF.js} +1 -1
  25. package/web/dist/assets/{chunk-WQQL4CDK-OBr867RS.js → chunk-WQQL4CDK-gNypBVAM.js} +1 -1
  26. package/web/dist/assets/{chunk-ZAUX2LTX-BGf21XAP.js → chunk-ZAUX2LTX--4WyRBRV.js} +1 -1
  27. package/web/dist/assets/{chunk-ZCHMQSOR-B1v2_vIA.js → chunk-ZCHMQSOR-umv3Lkpn.js} +1 -1
  28. package/web/dist/assets/{classDiagram-v2-4MYWR3UA-D73WliGc.js → classDiagram-v2-4MYWR3UA-C1XldpO1.js} +1 -1
  29. package/web/dist/assets/{cose-bilkent-JH36ORCC-BHjex_ed.js → cose-bilkent-JH36ORCC-BvugZzFU.js} +1 -1
  30. package/web/dist/assets/{cynefin-EF2NZ3EQ-BkwUbDxE.js → cynefin-EF2NZ3EQ-6qXB70s4.js} +1 -1
  31. package/web/dist/assets/{dagre-NK4GIZVX-HBESSwxd.js → dagre-NK4GIZVX-DDdlABYw.js} +1 -1
  32. package/web/dist/assets/{diagram-I5XIIV2D-B0x4b3TO.js → diagram-I5XIIV2D-T8oWy5CY.js} +1 -1
  33. package/web/dist/assets/{diagram-RJA3I4VV-D5I3zPSl.js → diagram-RJA3I4VV-CkdHTtfV.js} +1 -1
  34. package/web/dist/assets/{elk-QONTBCTQ-Bf1nltwr.js → elk-QONTBCTQ-BiEUbJJX.js} +1 -1
  35. package/web/dist/assets/{erDiagram-KYQIGQAA-DPncMSdU.js → erDiagram-KYQIGQAA-q3WwhyzH.js} +1 -1
  36. package/web/dist/assets/{eventmodeling-K75KTNOO-DMA5HThh.js → eventmodeling-K75KTNOO-BO5gW_NC.js} +1 -1
  37. package/web/dist/assets/{flowDiagram-IUGLLWBU-tz3GJD8_.js → flowDiagram-IUGLLWBU-DubjP6UK.js} +1 -1
  38. package/web/dist/assets/{ganttDiagram-R7GPNTJY-BdLbjwpy.js → ganttDiagram-R7GPNTJY-lKNRH8CI.js} +1 -1
  39. package/web/dist/assets/{gitGraph-VSP46ZUC-D-EmEDwq.js → gitGraph-VSP46ZUC-Cwlv3omT.js} +1 -1
  40. package/web/dist/assets/{gitGraphDiagram-PDYL7XHY-C1MNufrt.js → gitGraphDiagram-PDYL7XHY-Bo9GpFvW.js} +1 -1
  41. package/web/dist/assets/index-CeYhxqkk.js +113 -0
  42. package/web/dist/assets/index-GJgJwZ23.css +1 -0
  43. package/web/dist/assets/{info-OHQRW6UA-Dnj11J5n.js → info-OHQRW6UA-g3MbTRmN.js} +1 -1
  44. package/web/dist/assets/{infoDiagram-MVDRKAX5-CR1Pc-ET.js → infoDiagram-MVDRKAX5-CmILYLoB.js} +1 -1
  45. package/web/dist/assets/{journeyDiagram-BOSHW7O7-BaQQ5bT0.js → journeyDiagram-BOSHW7O7-Av8T0U1C.js} +1 -1
  46. package/web/dist/assets/{kanban-definition-FNSA4HC5-CryJiCse.js → kanban-definition-FNSA4HC5-B9NyWQYN.js} +1 -1
  47. package/web/dist/assets/{line-DAqohcwr.js → line-BUrll1My.js} +1 -1
  48. package/web/dist/assets/{linear-Aj6QlF4i.js → linear-DjET0phX.js} +1 -1
  49. package/web/dist/assets/{math-OLjDjiS-.js → math-BJAjkhlu.js} +1 -1
  50. package/web/dist/assets/{mermaid-parser.core-BfmVJIf8.js → mermaid-parser.core-CaTdkhkS.js} +3 -3
  51. package/web/dist/assets/{mermaid.core-DcmL9PWM.js → mermaid.core-Cktx937U.js} +4 -4
  52. package/web/dist/assets/{packet-JDAUHWVQ-DmHJYz0u.js → packet-JDAUHWVQ-DhG6l3PG.js} +1 -1
  53. package/web/dist/assets/{pie-XZMESJXO-DyIYlEiZ.js → pie-XZMESJXO-YPcOx7s0.js} +1 -1
  54. package/web/dist/assets/{pieDiagram-GNMPY74T-CbmNjslV.js → pieDiagram-GNMPY74T-DesiZKUl.js} +1 -1
  55. package/web/dist/assets/{quadrantDiagram-D57K4MC3-b8t3m46P.js → quadrantDiagram-D57K4MC3-Cn1kBVgu.js} +1 -1
  56. package/web/dist/assets/{radar-ABXABTNO-BRduUljF.js → radar-ABXABTNO-CtuRfYSz.js} +1 -1
  57. package/web/dist/assets/{railroad-I3PHUGI6-CS79uNF5.js → railroad-I3PHUGI6-D5Gnyy0R.js} +1 -1
  58. package/web/dist/assets/{railroad-abnf-LNEFI6M7-80I8zkTd.js → railroad-abnf-LNEFI6M7-Kw4pmJEm.js} +1 -1
  59. package/web/dist/assets/{railroad-ebnf-DWYD2UWJ-CvMLODGQ.js → railroad-ebnf-DWYD2UWJ-D8Mk5dIe.js} +1 -1
  60. package/web/dist/assets/{railroad-peg-JR7BVNR7-CDA3EuEX.js → railroad-peg-JR7BVNR7-R5Jwzs8F.js} +1 -1
  61. package/web/dist/assets/{requirementDiagram-EQYSW5RA-Bp5ukALL.js → requirementDiagram-EQYSW5RA-B9RDq-cL.js} +1 -1
  62. package/web/dist/assets/{sankeyDiagram-SY24JHO5-DWEy27rM.js → sankeyDiagram-SY24JHO5-DoEatOm6.js} +1 -1
  63. package/web/dist/assets/{sequenceDiagram-BLEHNHQU-Hz6IqSZC.js → sequenceDiagram-BLEHNHQU-DGE5Z9lT.js} +1 -1
  64. package/web/dist/assets/{src-DtpyLxw7.js → src-BnEL7pZw.js} +1 -1
  65. package/web/dist/assets/{stateDiagram-v2-A6IESKWZ-BXLpOvku.js → stateDiagram-v2-A6IESKWZ-_vn8W-k_.js} +1 -1
  66. package/web/dist/assets/{timeline-definition-JXYKGLGB-COLhHvFy.js → timeline-definition-JXYKGLGB-BaSLTtgR.js} +1 -1
  67. package/web/dist/assets/{treeView-D4JQ5SDB-Bv_XbrZv.js → treeView-D4JQ5SDB-DZElyFgL.js} +1 -1
  68. package/web/dist/assets/{treemap-SAJKECNS-DiIJdbav.js → treemap-SAJKECNS-DWDO4K4G.js} +1 -1
  69. package/web/dist/assets/{wardley-7MLQ67FV-9RG35vNe.js → wardley-7MLQ67FV-DTOUsk6Q.js} +1 -1
  70. package/web/dist/assets/{xychartDiagram-4URNHDU4-D_SC3zbf.js → xychartDiagram-4URNHDU4-DiaCfgAB.js} +1 -1
  71. package/web/dist/index.html +2 -2
  72. package/web/dist/assets/channel-DVBc3OcZ.js +0 -1
  73. package/web/dist/assets/index-DxuhX7Zj.css +0 -1
  74. package/web/dist/assets/index-Zo1mtXrj.js +0 -111
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()) {
@@ -31,6 +31,7 @@ export function createBuiltinEngineRegistry(options) {
31
31
  options.onNotice(text, level);
32
32
  },
33
33
  ...(options.skillPaths ? { skillPaths: options.skillPaths } : {}),
34
+ ...(options.thinkCount ? { thinkCount: options.thinkCount } : {}),
34
35
  }),
35
36
  },
36
37
  ...(options.codex
@@ -63,6 +64,7 @@ export function createBuiltinEngineRegistry(options) {
63
64
  client: options.codex?.client,
64
65
  log: options.onLog,
65
66
  ...(options.codex?.subagentPolicy ? { subagentPolicy: options.codex.subagentPolicy } : {}),
67
+ ...(options.thinkCount ? { thinkCount: options.thinkCount } : {}),
66
68
  }),
67
69
  },
68
70
  ]
@@ -3,6 +3,7 @@ import { createCodexClient } from "./codex-transport.js";
3
3
  import { codexMultiAgentConfig, codexSubagentGuidance, createCodexSubagents } from "./codex-subagents.js";
4
4
  import { registerCodexApprovals } from "./codex-approvals.js";
5
5
  import { sanitizeHistoricalActions } from "../markers/actions.js";
6
+ import { suggestionGuidance } from "./suggestion-guidance.js";
6
7
  import { ensureCodexProject } from "./codex-sessions.js";
7
8
  import { mkdtemp, rm } from "node:fs/promises";
8
9
  import { tmpdir } from "node:os";
@@ -295,14 +296,15 @@ export function createCodexEngine(options = {}) {
295
296
  }))
296
297
  : [],
297
298
  }),
298
- ...(codexSubagentGuidance({ enabled: plan.enabled, profiles: plan.profiles })
299
- ? {
300
- developerInstructions: codexSubagentGuidance({
301
- enabled: plan.enabled,
302
- profiles: plan.profiles,
303
- }),
304
- }
305
- : {}),
299
+ // 「下一步建议」的写法一直带(ADR-0022,否则 codex 永远出不了建议按钮);
300
+ // 开了子代理就把委派说明接在后面。两者都是 **thread 级**覆盖,
301
+ // 只对这条新会话生效 —— 不写用户的 `~/.codex/config.toml`,Codex Desktop 不受影响。
302
+ developerInstructions: [
303
+ suggestionGuidance(options.thinkCount?.() ?? null),
304
+ codexSubagentGuidance({ enabled: plan.enabled, profiles: plan.profiles }),
305
+ ]
306
+ .filter((part) => part !== null)
307
+ .join("\n\n"),
306
308
  });
307
309
  const threadId = response?.thread?.id;
308
310
  if (!threadId)
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 把「下一步建议」的写法带进 dsh(ADR-0022 的 dsh 版)。
3
+ *
4
+ * 为什么只能走 patch 文件:ACP 是**唯一**入口,而它的 `session/new` 只有
5
+ * `cwd / additionalDirectories / mcpServers / _meta`(schema 实测),没有系统提示字段;
6
+ * dsh 的系统提示由插件栈里的 `@deepseek-ai/dsh-system-prompt` 组装,其中
7
+ * `personaSuffix` 排在系统提示末尾 —— 正好是这段说明该待的位置。
8
+ *
9
+ * 两个必须写清的约束:
10
+ * 1. patch 的 `config` 是**整块替换**(不是深合并):只写 `personaSuffix` 会把 acp 模板
11
+ * 原本的 `personaPrefix` 冲掉(实测 dump 里会整个消失)。所以两句 persona 都带上,
12
+ * 取值照抄 `@deepseek-ai/dsh-acp-app` 的默认值;dsh 改这两句默认值时这里要跟着改。
13
+ * 2. 它是**进程级**的:dsh 子进程启动时才 compose 配置栈,所以改完「想想」条数要
14
+ * 重启工作台(引擎)才生效 —— 不像 pi / codex 那样每条新会话现读。
15
+ */
16
+ import { mkdirSync, writeFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ /** `@deepseek-ai/dsh-acp-app` 给 acp profile 的默认 persona(照抄,别改措辞) */
19
+ const PERSONA_PREFIX = "You are a coding agent powered by the {{model}} model.";
20
+ const PERSONA_SUFFIX = "Your working directory is {{cwd}}.";
21
+ /** YAML 块标量的缩进:`personaSuffix: |` 下的内容比 key 深两级 */
22
+ function indentBlock(text) {
23
+ return text
24
+ .split("\n")
25
+ .map((line) => (line === "" ? "" : ` ${line}`))
26
+ .join("\n");
27
+ }
28
+ /** 纯函数:生成 patch 文件内容(喂给 `dsh --patch <file>`) */
29
+ export function dshSuggestionPatch(guidance) {
30
+ return [
31
+ "# 由 oh-my-agent-web 生成(不要手改):给 dsh 的 system prompt 追加「下一步建议」写法。",
32
+ "# 改条数请改设置里的「想想」,然后重启工作台 —— dsh 的配置栈是进程启动时 compose 的。",
33
+ "- id: system-prompt",
34
+ " config:",
35
+ " personaPrefix: |",
36
+ indentBlock(PERSONA_PREFIX),
37
+ " personaSuffix: |",
38
+ indentBlock(`${PERSONA_SUFFIX}\n\n${guidance}`),
39
+ "",
40
+ ].join("\n");
41
+ }
42
+ /** 写到一个临时目录里(不污染用户的 `dsh-patches` 目录),返回 patch 文件路径 */
43
+ export function writeDshSuggestionPatch(dir, guidance) {
44
+ mkdirSync(dir, { recursive: true });
45
+ const file = join(dir, "dsh-suggestion-patch.yml");
46
+ writeFileSync(file, dshSuggestionPatch(guidance), "utf8");
47
+ return file;
48
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 与 SDK 的对接方式(M3 侦察结论):
8
8
  */
9
- import { DefaultResourceLoader, SessionManager, createAgentSession, getAgentDir, } from "@earendil-works/pi-coding-agent";
9
+ import { DefaultResourceLoader, SessionManager, createAgentSession, getAgentDir, initTheme, } from "@earendil-works/pi-coding-agent";
10
10
  import { dirname } from "node:path";
11
11
  import { getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
12
12
  import { classifyScope } from "../approval/scope.js";
@@ -131,13 +131,26 @@ function toAuthEvent(event) {
131
131
  /** 教模型怎么写「下一步建议」(ADR-0022):刻意写得短(每次请求都付 token) */
132
132
  import { reloadModelRegistry } from "./model-registry-reload.js";
133
133
  import { NARRATE_GUIDANCE } from "./narrate-guidance.js";
134
- import { SUGGESTION_GUIDANCE } from "./suggestion-guidance.js";
134
+ import { SUGGESTION_GUIDANCE, suggestionGuidance } from "./suggestion-guidance.js";
135
135
  import { SUBAGENT_GUIDANCE } from "./subagent-guidance.js";
136
136
  export { NARRATE_GUIDANCE, SUGGESTION_GUIDANCE, SUBAGENT_GUIDANCE };
137
137
  import { createPiThinking } from "./pi-thinking.js";
138
138
  import { piSdkVersion } from "./pi-version.js";
139
139
  import { renameSessionIn } from "./pi-rename.js";
140
140
  export function createPiSdkEngine(options = {}) {
141
+ /**
142
+ * pi 的 `theme` 是个全局 Proxy:没调过 `initTheme()` 就抛
143
+ * `Theme not initialized. Call initTheme() first.`(import 本身**不会**初始化它)。
144
+ *
145
+ * TUI 启动时自己会初始化,而这个无界面的工作台不会 —— 但扩展里那些「给终端画东西」的代码
146
+ * (`getMarkdownTheme().heading()`、`new Text(..., theme.fg(...))`)照样会被调用,尤其是
147
+ * 塞在 `setTimeout` 里的渲染:那个异常没人接,**直接把这个服务进程带走**(同事机器上真踩过:
148
+ * 扩展 `token-speed.ts` 的定时渲染崩了整个 agent-web)。
149
+ *
150
+ * 所以宿主这边先初始化一次,让这类渲染退化成「画了个没人看的东西」,而不是崩服务。
151
+ * 顺带也是所有 `@earendil-works/*` 导入的唯一出口(本文件)。
152
+ */
153
+ initTheme();
141
154
  const sessions = new Map();
142
155
  const thinking = createPiThinking(sessions);
143
156
  let approvalHandler = null;
@@ -234,7 +247,11 @@ export function createPiSdkEngine(options = {}) {
234
247
  extensionFactories,
235
248
  // 教模型「下一步建议」的写法(ADR-0022):纯文本标记,不需要调用任何工具
236
249
  // 「动手前先说话」:过程默认折叠,不写正文用户就不知道它在干什么
237
- appendSystemPrompt: [SUGGESTION_GUIDANCE, NARRATE_GUIDANCE, ...(subagentReady ? [SUBAGENT_GUIDANCE] : [])],
250
+ appendSystemPrompt: [
251
+ suggestionGuidance(options.thinkCount?.() ?? null),
252
+ NARRATE_GUIDANCE,
253
+ ...(subagentReady ? [SUBAGENT_GUIDANCE] : []),
254
+ ],
238
255
  });
239
256
  await resourceLoader.reload();
240
257
  const { session } = await createAgentSession({
@@ -1,9 +1,22 @@
1
- /** 教模型「下一步建议」的写法(ADR-0022):纯文本标记,不需要调用任何工具 */
2
- export const SUGGESTION_GUIDANCE = [
3
- "## Suggested next actions",
4
- "When the user must choose what to do next, end the reply with 4 to 5 lines of:",
5
- "[[action:suggest:<button text>]]",
6
- "Clicking a button sends that text as the next user message.",
7
- "Use [[action:suggest:<short label>,prompt=<text to send>]] when the label should differ from the sent text.",
8
- "Put them on their own lines at the very end, and never mention this syntax in prose.",
9
- ].join("\n");
1
+ /**
2
+ * 教模型「下一步建议」的写法(ADR-0022):纯文本标记,不需要调用任何工具。
3
+ *
4
+ * 条数由调用方给(设置里「想想」那一个 `settings.thinkCount`,取值 1~5):
5
+ * 界面上「再给一批」按几条问,模型结尾就写几条 —— 两边用同一个数,不然按钮数和
6
+ * 「想想」的结果数对不上。拿不到就退回默认区间 4~5(与 ADR-0039 一致)。
7
+ */
8
+ export function suggestionGuidance(count) {
9
+ const wanted = typeof count === "number" && Number.isInteger(count) && count >= 1 && count <= 5 ? count : null;
10
+ const amount = wanted === null ? "4 to 5" : String(wanted);
11
+ const unit = wanted === 1 ? "line" : "lines";
12
+ return [
13
+ "## Suggested next actions",
14
+ `When the user must choose what to do next, end the reply with ${amount} ${unit} of:`,
15
+ "[[action:suggest:<button text>]]",
16
+ "Clicking a button sends that text as the next user message.",
17
+ "Use [[action:suggest:<short label>,prompt=<text to send>]] when the label should differ from the sent text.",
18
+ "Put them on their own lines at the very end, and never mention this syntax in prose.",
19
+ ].join("\n");
20
+ }
21
+ /** 默认版(4~5 条):给拿不到设置的地方用(测试、临时会话、预热) */
22
+ export const SUGGESTION_GUIDANCE = suggestionGuidance();
@@ -9,6 +9,7 @@
9
9
  * codex 客户端构造时不 spawn:机器上没装 codex 也能正常启动,只有在切到它时才报错。
10
10
  */
11
11
  import { existsSync, readFileSync, readdirSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
12
13
  import { join } from "node:path";
13
14
  import { createBuiltinEngineRegistry } from "./builtin.js";
14
15
  import { enabledSubagentSpecs, readSubagentSettings } from "../subagents/profile-store.js";
@@ -18,6 +19,8 @@ import { createClaudeClient } from "./claude-transport.js";
18
19
  import { createCodexSessionsProvider } from "./codex-sessions.js";
19
20
  import { createCodexClient } from "./codex-transport.js";
20
21
  import { createDshClient } from "./dsh-transport.js";
22
+ import { writeDshSuggestionPatch } from "./dsh-prompt-patch.js";
23
+ import { suggestionGuidance } from "./suggestion-guidance.js";
21
24
  import { createDshSessionsProvider } from "./dsh-sessions.js";
22
25
  import { createOpencodeSessionsProvider } from "./opencode-sessions.js";
23
26
  import { readOpencodeSessionDirs } from "./opencode-projects.js";
@@ -59,7 +62,14 @@ export function createEngineWiring(options) {
59
62
  * 顺序 = 环境变量里显式给的 → 用户目录里的 `*.yml`(按文件名)。
60
63
  */
61
64
  const patchDir = options.dshPatchDir;
65
+ /**
66
+ * 「下一步建议」的写法:dsh 只能靠 patch 叠加层带进去(ACP 的 session/new 没有系统提示字段)。
67
+ * 写在工作台自己的临时目录,**不**混进用户的 `dsh-patches`;排在最前面,
68
+ * 用户自己写的 patch 仍有最终决定权。
69
+ */
70
+ const suggestionPatchFile = writeDshSuggestionPatch(join(tmpdir(), "oh-my-agent-web"), suggestionGuidance(options.thinkCount?.() ?? null));
62
71
  const patchFiles = [
72
+ suggestionPatchFile,
63
73
  ...(options.dshPatches ?? []),
64
74
  ...(patchDir && existsSync(patchDir)
65
75
  ? readdirSync(patchDir)
@@ -154,6 +164,7 @@ export function createEngineWiring(options) {
154
164
  onNotice: options.onNotice,
155
165
  ...(options.skillPaths ? { skillPaths: options.skillPaths } : {}),
156
166
  onLog: (text) => options.onLog(text),
167
+ ...(options.thinkCount ? { thinkCount: options.thinkCount } : {}),
157
168
  codex: { client: codexClient, bin: options.codexBin, subagentPolicy: codexSubagentPolicy },
158
169
  dsh: { client: dshClient, bin: options.dshBin, model: defaultModel },
159
170
  opencode: {
@@ -13,7 +13,7 @@ import { existsSync, mkdirSync } from "node:fs";
13
13
  import { PROTOCOL_VERSION } from "./protocol.js";
14
14
  import { acquireLock, describeHolder, releaseLock } from "./session/lock.js";
15
15
  import { createRuntime } from "./session/runtime.js";
16
- import { createFileSettingsStore } from "./session/settings.js";
16
+ import { createFileSettingsStore, readSettingsSync } from "./session/settings.js";
17
17
  import { createSender } from "./snapshot/sender.js";
18
18
  import { createPiSessionsProvider, piAgentDir } from "./session/pi-sessions-provider.js";
19
19
  import { createUploadStore, uploadsDir } from "./uploads/upload-store.js";
@@ -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
  });
@@ -79,6 +89,18 @@ process.on("unhandledRejection", (reason) => {
79
89
  message: reason instanceof Error ? (reason.stack ?? reason.message) : String(reason),
80
90
  });
81
91
  });
92
+ // 同一个口径的另外半边:**同步抛出**的异常(最典型的是扩展塞在 setTimeout 里的渲染,
93
+ // 比如 `token-speed.ts` 调 pi 的 theme 时抛 `Theme not initialized`)。
94
+ // 没人 catch 的 timer 异常在 Node 里等于「杀进程」—— 一条扩展的渲染错就能把整个工作台带走,
95
+ // 用户看到的就是服务突然没了、终端回到提示符。这里记下来、继续跑。
96
+ //
97
+ // 取舍:单用户本地工作台,宁可带着可能的脏状态继续(顶多某个界面块不刷新),
98
+ // 也不要一按就死 —— 死了连日志面板都打不开。真到了没法自救的地步,用户重启就是了。
99
+ process.on("uncaughtException", (error) => {
100
+ logger.error("未捕获的异常(已记录,进程继续)", {
101
+ message: error instanceof Error ? (error.stack ?? error.message) : String(error),
102
+ });
103
+ });
82
104
  /** 局域网地址自动放行;域名仍要显式配 */
83
105
  const localAddresses = Object.values(networkInterfaces())
84
106
  .flat()
@@ -119,10 +141,15 @@ const engineWiring = createEngineWiring({
119
141
  skillPaths: [defaultSkillsDir()],
120
142
  // 子代理模板目录:pi 与 codex 共用同一份(codex 的 spawn 类型就是这些模板)
121
143
  subagentAgentDir: piAgentDir(),
144
+ // 「下一步建议」要几条 = 设置里「想想」的那个数:pi / codex 建会话时现读(读的是盘上的
145
+ // settings.json,改了设置下一条会话就生效);dsh 只能在启动时取一次(进程级 patch)
146
+ thinkCount: () => readSettingsSync(config.dataDir).thinkCount,
122
147
  workspace: config.workspace,
123
148
  piSessions,
124
149
  onNotice: (text, level) => runtimeRef.current?.notify(text, level),
125
- onLog: (text, meta) => logger.warn(text, meta),
150
+ // 引擎子进程的 stderr / 诊断输出:正常运行时它是噪声(codex、dsh 都会吐),
151
+ // 真正要用户知道的事走 onNotice(界面提示)。所以记 info 光进文件,别占终端。
152
+ onLog: (text, meta) => logger.info(text, meta),
126
153
  });
127
154
  const engineCatalog = engineWiring.catalog;
128
155
  const resolved = engineWiring.resolved;
@@ -391,6 +418,19 @@ async function main() {
391
418
  void releaseLock(config.dataDir, process.pid).finally(() => process.exit(1));
392
419
  });
393
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
+ }
394
434
  logger.info("agent-web 已启动", {
395
435
  url: `http://${config.host}:${config.port}`,
396
436
  workspace: config.workspace,
@@ -5,6 +5,7 @@
5
5
  * 归一化放在纯函数里(`normalizeQuickPhrases`),因为「用户能贴进来什么」永远想不到 ——
6
6
  * 单条超长、重复、空白、几百条,都必须在上盘前收敛。
7
7
  */
8
+ import { readFileSync } from "node:fs";
8
9
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
9
10
  import { dirname, isAbsolute, join } from "node:path";
10
11
  const SETTINGS_FILE = "settings.json";
@@ -311,6 +312,20 @@ function parseSettings(text) {
311
312
  export function settingsPath(dataDir) {
312
313
  return join(dataDir, SETTINGS_FILE);
313
314
  }
315
+ /**
316
+ * 同步读设置(缺文件/坏文件 → 默认值,与 `createFileSettingsStore().load()` 同口径)。
317
+ *
318
+ * 只给「进程启动时就要定型」的场景用(目前只有一处:dsh 的 system prompt patch)。
319
+ * 平时请走异步的 SettingsStore —— 这里每调一次就读一次盘。
320
+ */
321
+ export function readSettingsSync(dataDir) {
322
+ try {
323
+ return parseSettings(readFileSync(settingsPath(dataDir), "utf8"));
324
+ }
325
+ catch {
326
+ return defaultSettings();
327
+ }
328
+ }
314
329
  export function createFileSettingsStore(dataDir) {
315
330
  return {
316
331
  async load() {