hermes-web-ui 0.3.4 → 0.3.6

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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/dist/client/assets/{Add-DBcT5hzg.js → Add-CKf6ViXR.js} +1 -1
  3. package/dist/client/assets/{Button-R5s3092y.js → Button-CrrCCorI.js} +1 -1
  4. package/dist/client/assets/ChannelsView-CoREbYTy.css +1 -0
  5. package/dist/client/assets/ChannelsView-D4I7hhZO.js +1 -0
  6. package/dist/client/assets/ChatView-DxyBUK57.js +127 -0
  7. package/dist/client/assets/ChatView-Vfi_jEpI.css +1 -0
  8. package/dist/client/assets/Close-C9xwy-pW.js +45 -0
  9. package/dist/client/assets/{FormItem-ZuVh5lSE.js → FormItem-BgJdrTW0.js} +1 -1
  10. package/dist/client/assets/GatewaysView-Cib2JydO.js +1 -0
  11. package/dist/client/assets/GatewaysView-DtUN_4uh.css +1 -0
  12. package/dist/client/assets/Input-ChENEW-Z.js +234 -0
  13. package/dist/client/assets/{InputNumber-B6xJGyAP.js → InputNumber-Xd6HWSdp.js} +3 -3
  14. package/dist/client/assets/JobsView-C9-go5-X.css +1 -0
  15. package/dist/client/assets/{JobsView-BNGFt0kv.js → JobsView-SnToCbDd.js} +2 -2
  16. package/dist/client/assets/{LoginView-B-UpFAOr.js → LoginView-BZdmMnsf.js} +1 -1
  17. package/dist/client/assets/LoginView-DHPxG3eC.css +1 -0
  18. package/dist/client/assets/{LogsView-BMJ5FVwp.js → LogsView-DblvOJIg.js} +1 -1
  19. package/dist/client/assets/LogsView-VfBZmaM1.css +1 -0
  20. package/dist/client/assets/{MarkdownRenderer-C8-GFOw3.js → MarkdownRenderer-DJLVk7ei.js} +1 -1
  21. package/dist/client/assets/MarkdownRenderer-Dc9VWGMm.css +1 -0
  22. package/dist/client/assets/MemoryView-BZxpGTCE.css +1 -0
  23. package/dist/client/assets/{MemoryView-Rfsn9nzW.js → MemoryView-exXvRwCc.js} +1 -1
  24. package/dist/client/assets/{Modal-DCJNfzf6.js → Modal-B2zvXTrk.js} +6 -6
  25. package/dist/client/assets/{ModelsView-wE2P2H3O.js → ModelsView-DGs47Cj4.js} +1 -1
  26. package/dist/client/assets/ModelsView-jbgZP3YF.css +1 -0
  27. package/dist/client/assets/Popconfirm-BoZc0kKk.js +16 -0
  28. package/dist/client/assets/Popover-Cu52vG3D.js +117 -0
  29. package/dist/client/assets/ProfilesView-1BtKDO_6.css +1 -0
  30. package/dist/client/assets/ProfilesView-D0FY7Jwe.js +440 -0
  31. package/dist/client/assets/Select-BHc7u-Yf.js +340 -0
  32. package/dist/client/assets/SettingRow-Cr1VcUPz.css +1 -0
  33. package/dist/client/assets/{SettingRow-Bztj1Qvd.js → SettingRow-i-UXlco7.js} +1 -1
  34. package/dist/client/assets/SettingsView-BW6ctYG5.js +352 -0
  35. package/dist/client/assets/SettingsView-Dhr2wzAB.css +1 -0
  36. package/dist/client/assets/SkillsView-5K2WjAWZ.css +1 -0
  37. package/dist/client/assets/{SkillsView-DA7GSqMh.js → SkillsView-B5QBaAzi.js} +1 -1
  38. package/dist/client/assets/{Spin-UenY6DjU.js → Spin-DsNCRPk9.js} +3 -3
  39. package/dist/client/assets/Suffix-3xK0KZGt.js +90 -0
  40. package/dist/client/assets/{Switch-D4sdzrF1.js → Switch-Bf63XXgA.js} +2 -2
  41. package/dist/client/assets/{light-UKc15Qzy.js → Tag-Dmbj68Ki.js} +2 -2
  42. package/dist/client/assets/TerminalView-BOiVMGJZ.css +1 -0
  43. package/dist/client/assets/{TerminalView-bdOH27Rz.js → TerminalView-DrJHZ0qI.js} +16 -16
  44. package/dist/client/assets/{Tooltip-shahfZE0.js → Tooltip-CRbZNhG0.js} +1 -1
  45. package/dist/client/assets/{UsageView-LP-yDWfN.js → UsageView-DQ43JasX.js} +1 -1
  46. package/dist/client/assets/UsageView-wje1C97e.css +1 -0
  47. package/dist/client/assets/Warning-kBbRMAif.js +1 -0
  48. package/dist/client/assets/{_plugin-vue_export-helper-B5RrPTMt.js → _plugin-vue_export-helper-CnosYBkx.js} +1 -1
  49. package/dist/client/assets/app-BT9yU6N6.js +1 -0
  50. package/dist/client/assets/app-CjNVVG5x.js +1 -0
  51. package/dist/client/assets/{browser-Cnde4syl.js → browser-Djp4tkp3.js} +1 -1
  52. package/dist/client/assets/chat-DlC9S9DK.js +6 -0
  53. package/dist/client/assets/composables-DCA4Yga5.js +1 -0
  54. package/dist/client/assets/fade-in.cssr-CIVyTG6A.js +12 -0
  55. package/dist/client/assets/index-BEcRccNA.css +1 -0
  56. package/dist/client/assets/index-D12ukDT7.js +284 -0
  57. package/dist/client/assets/{jobs-Bg3B0qd3.js → jobs-CcVaCGMJ.js} +1 -1
  58. package/dist/client/assets/{light-CtcMXxnt.js → light-BF6E9z0k.js} +1 -1
  59. package/dist/client/assets/{light-B9EqymT1.js → light-BJ96fCLC.js} +1 -1
  60. package/dist/client/assets/{light-7AGE9udo.js → light-BPqyaxve.js} +1 -1
  61. package/dist/client/assets/{light-C_iMTPuX.js → light-CSp9-LhE.js} +1 -1
  62. package/dist/client/assets/light-D9G2GshF.js +1 -0
  63. package/dist/client/assets/light-KCEDTUGE.js +23 -0
  64. package/dist/client/assets/{pinia-BizF94YH.js → pinia-iHE5_ZXa.js} +1 -1
  65. package/dist/client/assets/profiles-CJCR84uQ.js +1 -0
  66. package/dist/client/assets/{router-BuVIua3w.js → router-C-NNJUuf.js} +2 -2
  67. package/dist/client/assets/{sessions-CpgDAUKL.js → sessions-C4bnNvzS.js} +1 -1
  68. package/dist/client/assets/{skills-BGUX0Zk7.js → skills-B4slZfeZ.js} +1 -1
  69. package/dist/client/assets/{use-message-DgahoF2f.js → use-message-BIpqgDet.js} +1 -1
  70. package/dist/client/assets/{useTheme-CRVFV8Ud.js → useTheme-B78N9tyz.js} +1 -1
  71. package/dist/client/index.html +29 -29
  72. package/dist/server/index.js +17 -79
  73. package/dist/server/routes/hermes/filesystem.js +3 -1
  74. package/dist/server/routes/hermes/gateways.d.ts +4 -0
  75. package/dist/server/routes/hermes/gateways.js +73 -0
  76. package/dist/server/routes/hermes/index.js +2 -0
  77. package/dist/server/routes/hermes/profiles.js +28 -84
  78. package/dist/server/routes/hermes/proxy-handler.js +17 -1
  79. package/dist/server/routes/hermes/sessions.js +9 -0
  80. package/dist/server/services/hermes/gateway-manager.d.ts +131 -0
  81. package/dist/server/services/hermes/gateway-manager.js +536 -0
  82. package/dist/server/services/hermes/sessions-db.d.ts +24 -0
  83. package/dist/server/services/hermes/sessions-db.js +147 -0
  84. package/package.json +1 -1
  85. package/dist/client/assets/ChannelsView-CH6386MX.js +0 -1
  86. package/dist/client/assets/ChannelsView-DS2AEk-a.css +0 -1
  87. package/dist/client/assets/ChatView-C_mCyb0n.js +0 -127
  88. package/dist/client/assets/ChatView-CtLv9X8G.css +0 -1
  89. package/dist/client/assets/Close-CbcMKiZ9.js +0 -45
  90. package/dist/client/assets/Input-CbOEVtEq.js +0 -234
  91. package/dist/client/assets/JobsView-CVx2Yv-y.css +0 -1
  92. package/dist/client/assets/LoginView-CA9w3oDH.css +0 -1
  93. package/dist/client/assets/LogsView-BG7Bro4j.css +0 -1
  94. package/dist/client/assets/MarkdownRenderer-Ct6cRT2D.css +0 -1
  95. package/dist/client/assets/MemoryView-DEyIgPS8.css +0 -1
  96. package/dist/client/assets/ModelsView-D1p_2trx.css +0 -1
  97. package/dist/client/assets/Popconfirm-pG5fyaNw.js +0 -16
  98. package/dist/client/assets/Popover-BcnZkOJ0.js +0 -117
  99. package/dist/client/assets/ProfilesView-CAOfb8ly.js +0 -440
  100. package/dist/client/assets/ProfilesView-Chffv0-D.css +0 -1
  101. package/dist/client/assets/Scrollbar-B7udJACg.js +0 -77
  102. package/dist/client/assets/Select-B6Xt21kv.js +0 -340
  103. package/dist/client/assets/SettingRow-CwidKv2Q.css +0 -1
  104. package/dist/client/assets/SettingsView-BIEQOPzq.css +0 -1
  105. package/dist/client/assets/SettingsView-Dwx8y_nj.js +0 -352
  106. package/dist/client/assets/SkillsView-D5bivF7Z.css +0 -1
  107. package/dist/client/assets/Suffix-DopnzIdr.js +0 -25
  108. package/dist/client/assets/TerminalView-DPdn9YA7.css +0 -1
  109. package/dist/client/assets/UsageView-CnADqqzf.css +0 -1
  110. package/dist/client/assets/Warning-DT2rnqpH.js +0 -1
  111. package/dist/client/assets/app-B3-jMgcz.js +0 -1
  112. package/dist/client/assets/app-Baqdn89g.js +0 -1
  113. package/dist/client/assets/chat-CCvtq7iz.js +0 -6
  114. package/dist/client/assets/composables-yAWv_nAD.js +0 -1
  115. package/dist/client/assets/fade-in-scale-up.cssr-7cCctcdO.js +0 -1
  116. package/dist/client/assets/index-JeutuTe0.css +0 -1
  117. package/dist/client/assets/index-vZMUiSyM.js +0 -284
  118. package/dist/client/assets/light-87-mk9Yl.js +0 -1
  119. package/dist/client/assets/profiles-DXTLdlvo.js +0 -23
  120. package/dist/client/assets/use-compitable-C1bGazqI.js +0 -1
  121. /package/dist/client/assets/{_common-DgdkN_d5.js → _common-Yp55QE79.js} +0 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * GatewayManager — 多 Profile 网关生命周期管理
3
+ *
4
+ * 核心职责:
5
+ * 1. 启动时检测所有 profile 的网关运行状态(PID、端口、健康检查)
6
+ * 2. 自动发现端口冲突并重新分配
7
+ * 3. 启动/停止网关进程
8
+ *
9
+ * 启动检测流程(detectStatus):
10
+ * ① 读取 gateway.pid → 获取 PID
11
+ * ② 读取 config.yaml (platforms.api_server.extra.port/host) → 获取配置端口
12
+ * ③ PID 存活?
13
+ * - 否 → 标记为 stopped
14
+ * - 是 → 继续
15
+ * ④ 对配置端口做 health check?
16
+ * - 通过 → 配置与运行状态匹配,注册网关
17
+ * - 失败 → 用 lsof 查 PID 实际监听端口
18
+ * ⑤ 实际端口 ≠ 配置端口?
19
+ * - 是 → 更新 config.yaml 到实际端口,重新 health check,通过则注册
20
+ * - 否 → 标记为 stopped
21
+ *
22
+ * 端口分配流程(resolvePort,启动前调用):
23
+ * ① 读取配置端口
24
+ * ② 检查是否被已管理的网关占用
25
+ * ③ 检查是否被外部系统进程占用(TCP bind 测试)
26
+ * ④ 冲突则从 base+1 递增找空闲端口,并写入 config.yaml
27
+ *
28
+ * 启动模式:
29
+ * - 正常系统(macOS/Linux):hermes gateway start/stop(系统服务管理)
30
+ * - WSL / Docker:hermes gateway run(detached 子进程,手动 kill)
31
+ */
32
+ export interface GatewayStatus {
33
+ profile: string;
34
+ port: number;
35
+ host: string;
36
+ url: string;
37
+ running: boolean;
38
+ pid?: number;
39
+ }
40
+ export declare class GatewayManager {
41
+ /** 已注册的网关:profile name → { pid, port, host, url } */
42
+ private gateways;
43
+ /** 本次启动过程中已分配的端口集合(防止并发分配到相同端口) */
44
+ private allocatedPorts;
45
+ /** 当前活跃的 profile(用于代理路由的默认上游) */
46
+ private activeProfile;
47
+ constructor(activeProfile: string);
48
+ /** 获取 profile 的 home 目录路径 */
49
+ private profileDir;
50
+ /**
51
+ * 从 profile 的 config.yaml 读取 api_server 端口和主机
52
+ * 读取路径:platforms.api_server.extra.port / extra.host
53
+ */
54
+ private readProfilePort;
55
+ /** 从 profile 的 gateway.pid 文件读取 PID(JSON 格式 { "pid": 12345 }) */
56
+ private readPidFile;
57
+ /** 检查进程是否存活(发送信号 0,不实际杀死进程) */
58
+ private isProcessAlive;
59
+ /** 请求 /health 端点,判断网关是否真正就绪 */
60
+ private checkHealth;
61
+ /** 尝试绑定端口,检测端口是否被系统级进程占用 */
62
+ private checkPortAvailable;
63
+ /** 从 base 端口开始递增查找空闲端口(上限 65535) */
64
+ private findFreePort;
65
+ /**
66
+ * 将端口和主机写入 profile 的 config.yaml
67
+ * 写入完整结构:
68
+ * platforms:
69
+ * api_server:
70
+ * enabled: true
71
+ * key: ''
72
+ * cors_origins: '*'
73
+ * extra:
74
+ * port: <port>
75
+ * host: <host>
76
+ * 同时清理旧的顶层 port/host(避免 Hermes 读取错误)
77
+ */
78
+ private writeProfilePort;
79
+ /**
80
+ * 为 profile 分配可用端口(启动前调用)
81
+ *
82
+ * 检测顺序:
83
+ * 1. 已管理的网关 + 已分配的端口 → 内存级检查(快)
84
+ * 2. 系统 TCP bind 测试 → 检测外部进程占用
85
+ * 3. 冲突则从 base+1 递增找空闲端口,写入 config.yaml
86
+ */
87
+ private resolvePort;
88
+ /** 获取指定 profile 的网关 URL(代理路由使用) */
89
+ getUpstream(profileName?: string): string;
90
+ getActiveProfile(): string;
91
+ setActiveProfile(name: string): void;
92
+ /** 列出所有已知 profile 名称(通过 hermes CLI 或文件系统扫描) */
93
+ listProfiles(): Promise<string[]>;
94
+ /**
95
+ * 检测单个 profile 的网关状态(只读,不修改任何进程或配置)
96
+ *
97
+ * 流程:
98
+ * ① 读 PID 文件 → 检查进程是否存活
99
+ * ② 读配置端口 → health check
100
+ * ③ 两者都通过 → 匹配,注册
101
+ * ④ 否则 → 标记为未运行(不杀进程,由 startAll 处理)
102
+ */
103
+ detectStatus(name: string): Promise<GatewayStatus>;
104
+ /** 检测所有 profile 的网关状态 */
105
+ listAll(): Promise<GatewayStatus[]>;
106
+ /**
107
+ * 启动单个 profile 的网关
108
+ * 启动前自动调用 resolvePort() 确保端口可用且配置完整
109
+ */
110
+ start(name: string): Promise<GatewayStatus>;
111
+ /** 等待网关健康检查通过,最多 15 秒 */
112
+ private waitForReady;
113
+ /**
114
+ * 停止单个 profile 的网关
115
+ * 正常系统用 "gateway stop",WSL/Docker 直接 kill 进程组
116
+ * 返回前等待 health check 确认网关已真正停止
117
+ */
118
+ stop(name: string, timeoutMs?: number): Promise<void>;
119
+ /** 停止所有已管理的网关(并行执行) */
120
+ stopAll(): Promise<void>;
121
+ /** 扫描所有 profile,检测网关运行状态并注册 */
122
+ detectAllOnStartup(): Promise<void>;
123
+ /**
124
+ * 启动所有未运行的网关
125
+ *
126
+ * 两阶段执行:
127
+ * Phase 1 — 顺序处理:检查状态、清理旧进程、分配端口
128
+ * Phase 2 — 并行启动网关进程
129
+ */
130
+ startAll(): Promise<void>;
131
+ }
@@ -0,0 +1,536 @@
1
+ "use strict";
2
+ /**
3
+ * GatewayManager — 多 Profile 网关生命周期管理
4
+ *
5
+ * 核心职责:
6
+ * 1. 启动时检测所有 profile 的网关运行状态(PID、端口、健康检查)
7
+ * 2. 自动发现端口冲突并重新分配
8
+ * 3. 启动/停止网关进程
9
+ *
10
+ * 启动检测流程(detectStatus):
11
+ * ① 读取 gateway.pid → 获取 PID
12
+ * ② 读取 config.yaml (platforms.api_server.extra.port/host) → 获取配置端口
13
+ * ③ PID 存活?
14
+ * - 否 → 标记为 stopped
15
+ * - 是 → 继续
16
+ * ④ 对配置端口做 health check?
17
+ * - 通过 → 配置与运行状态匹配,注册网关
18
+ * - 失败 → 用 lsof 查 PID 实际监听端口
19
+ * ⑤ 实际端口 ≠ 配置端口?
20
+ * - 是 → 更新 config.yaml 到实际端口,重新 health check,通过则注册
21
+ * - 否 → 标记为 stopped
22
+ *
23
+ * 端口分配流程(resolvePort,启动前调用):
24
+ * ① 读取配置端口
25
+ * ② 检查是否被已管理的网关占用
26
+ * ③ 检查是否被外部系统进程占用(TCP bind 测试)
27
+ * ④ 冲突则从 base+1 递增找空闲端口,并写入 config.yaml
28
+ *
29
+ * 启动模式:
30
+ * - 正常系统(macOS/Linux):hermes gateway start/stop(系统服务管理)
31
+ * - WSL / Docker:hermes gateway run(detached 子进程,手动 kill)
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.GatewayManager = void 0;
35
+ const child_process_1 = require("child_process");
36
+ const path_1 = require("path");
37
+ const os_1 = require("os");
38
+ const fs_1 = require("fs");
39
+ const child_process_2 = require("child_process");
40
+ const util_1 = require("util");
41
+ const net_1 = require("net");
42
+ const execFileAsync = (0, util_1.promisify)(child_process_2.execFile);
43
+ // ============================
44
+ // 常量 & 环境检测
45
+ // ============================
46
+ const HERMES_BASE = (0, path_1.resolve)((0, os_1.homedir)(), '.hermes');
47
+ const HERMES_BIN = process.env.HERMES_BIN?.trim() || 'hermes';
48
+ // WSL / Docker 没有 systemd 或 launchd,需要用 "gateway run" 代替 "gateway start"
49
+ const isWsl = (0, fs_1.existsSync)('/proc/version') && require('fs').readFileSync('/proc/version', 'utf-8').toLowerCase().includes('microsoft');
50
+ const isDocker = (0, fs_1.existsSync)('/.dockerenv');
51
+ const needsRunMode = isWsl || isDocker;
52
+ // ============================
53
+ // GatewayManager
54
+ // ============================
55
+ class GatewayManager {
56
+ /** 已注册的网关:profile name → { pid, port, host, url } */
57
+ gateways = new Map();
58
+ /** 本次启动过程中已分配的端口集合(防止并发分配到相同端口) */
59
+ allocatedPorts = new Set();
60
+ /** 当前活跃的 profile(用于代理路由的默认上游) */
61
+ activeProfile;
62
+ constructor(activeProfile) {
63
+ this.activeProfile = activeProfile;
64
+ }
65
+ // ============================
66
+ // Profile 目录 & 配置读取
67
+ // ============================
68
+ /** 获取 profile 的 home 目录路径 */
69
+ profileDir(name) {
70
+ if (name === 'default')
71
+ return HERMES_BASE;
72
+ return (0, path_1.join)(HERMES_BASE, 'profiles', name);
73
+ }
74
+ /**
75
+ * 从 profile 的 config.yaml 读取 api_server 端口和主机
76
+ * 读取路径:platforms.api_server.extra.port / extra.host
77
+ */
78
+ readProfilePort(name) {
79
+ const configPath = (0, path_1.join)(this.profileDir(name), 'config.yaml');
80
+ if (!(0, fs_1.existsSync)(configPath))
81
+ return { port: 8642, host: '127.0.0.1' };
82
+ try {
83
+ const yaml = require('js-yaml');
84
+ const content = (0, fs_1.readFileSync)(configPath, 'utf-8');
85
+ const cfg = yaml.load(content) || {};
86
+ const extra = cfg?.platforms?.api_server?.extra;
87
+ const rawPort = extra?.port || 8642;
88
+ const port = typeof rawPort === 'number' ? rawPort : parseInt(rawPort, 10) || 8642;
89
+ const host = extra?.host || '127.0.0.1';
90
+ // 端口超出合法范围时回退到默认值
91
+ return { port: port > 0 && port <= 65535 ? port : 8642, host };
92
+ }
93
+ catch {
94
+ return { port: 8642, host: '127.0.0.1' };
95
+ }
96
+ }
97
+ /** 从 profile 的 gateway.pid 文件读取 PID(JSON 格式 { "pid": 12345 }) */
98
+ readPidFile(name) {
99
+ const pidPath = (0, path_1.join)(this.profileDir(name), 'gateway.pid');
100
+ if (!(0, fs_1.existsSync)(pidPath))
101
+ return null;
102
+ try {
103
+ const content = (0, fs_1.readFileSync)(pidPath, 'utf-8').trim();
104
+ const data = JSON.parse(content);
105
+ return typeof data.pid === 'number' ? data.pid : parseInt(data.pid, 10) || null;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ // ============================
112
+ // 进程 & 端口检测工具
113
+ // ============================
114
+ /** 检查进程是否存活(发送信号 0,不实际杀死进程) */
115
+ isProcessAlive(pid) {
116
+ try {
117
+ process.kill(pid, 0);
118
+ return true;
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ }
124
+ /** 请求 /health 端点,判断网关是否真正就绪 */
125
+ async checkHealth(url, timeoutMs = 3000) {
126
+ try {
127
+ const res = await fetch(`${url.replace(/\/$/, '')}/health`, {
128
+ signal: AbortSignal.timeout(timeoutMs),
129
+ });
130
+ return res.ok;
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
136
+ /** 尝试绑定端口,检测端口是否被系统级进程占用 */
137
+ checkPortAvailable(port, host) {
138
+ if (port < 0 || port > 65535)
139
+ return Promise.resolve(false);
140
+ return new Promise((resolve) => {
141
+ const server = (0, net_1.createServer)();
142
+ server.once('error', () => {
143
+ server.close();
144
+ resolve(false);
145
+ });
146
+ server.once('listening', () => {
147
+ server.close();
148
+ resolve(true);
149
+ });
150
+ server.listen(port, host);
151
+ });
152
+ }
153
+ /** 从 base 端口开始递增查找空闲端口(上限 65535) */
154
+ findFreePort(base, host = '127.0.0.1') {
155
+ return new Promise((resolve, reject) => {
156
+ const tryPort = (port) => {
157
+ if (port > 65535) {
158
+ reject(new Error(`No free port found in range ${base}-65535`));
159
+ return;
160
+ }
161
+ const server = (0, net_1.createServer)();
162
+ server.once('error', () => {
163
+ server.close();
164
+ tryPort(port + 1);
165
+ });
166
+ server.once('listening', () => {
167
+ server.close();
168
+ resolve(port);
169
+ });
170
+ server.listen(port, host);
171
+ };
172
+ tryPort(base);
173
+ });
174
+ }
175
+ // ============================
176
+ // 配置写入
177
+ // ============================
178
+ /**
179
+ * 将端口和主机写入 profile 的 config.yaml
180
+ * 写入完整结构:
181
+ * platforms:
182
+ * api_server:
183
+ * enabled: true
184
+ * key: ''
185
+ * cors_origins: '*'
186
+ * extra:
187
+ * port: <port>
188
+ * host: <host>
189
+ * 同时清理旧的顶层 port/host(避免 Hermes 读取错误)
190
+ */
191
+ writeProfilePort(name, port, host) {
192
+ const configPath = (0, path_1.join)(this.profileDir(name), 'config.yaml');
193
+ try {
194
+ const yaml = require('js-yaml');
195
+ const content = (0, fs_1.existsSync)(configPath) ? (0, fs_1.readFileSync)(configPath, 'utf-8') : '';
196
+ const cfg = yaml.load(content) || {};
197
+ if (!cfg.platforms)
198
+ cfg.platforms = {};
199
+ if (!cfg.platforms.api_server)
200
+ cfg.platforms.api_server = {};
201
+ if (!cfg.platforms.api_server.extra)
202
+ cfg.platforms.api_server.extra = {};
203
+ cfg.platforms.api_server.enabled = true;
204
+ cfg.platforms.api_server.key = '';
205
+ cfg.platforms.api_server.cors_origins = '*';
206
+ cfg.platforms.api_server.extra.port = port;
207
+ cfg.platforms.api_server.extra.host = host;
208
+ // 清理旧的顶层 port/host,Hermes 只从 extra 读取
209
+ if (cfg.platforms.api_server.port !== undefined) {
210
+ delete cfg.platforms.api_server.port;
211
+ }
212
+ if (cfg.platforms.api_server.host !== undefined) {
213
+ delete cfg.platforms.api_server.host;
214
+ }
215
+ (0, fs_1.writeFileSync)(configPath, yaml.dump(cfg, { lineWidth: -1 }), 'utf-8');
216
+ console.log(`[GatewayManager] Updated ${configPath}: api_server.extra.port = ${port}`);
217
+ }
218
+ catch (err) {
219
+ console.error(`[GatewayManager] Failed to write config for profile "${name}":`, err);
220
+ }
221
+ }
222
+ // ============================
223
+ // 端口分配
224
+ // ============================
225
+ /**
226
+ * 为 profile 分配可用端口(启动前调用)
227
+ *
228
+ * 检测顺序:
229
+ * 1. 已管理的网关 + 已分配的端口 → 内存级检查(快)
230
+ * 2. 系统 TCP bind 测试 → 检测外部进程占用
231
+ * 3. 冲突则从 base+1 递增找空闲端口,写入 config.yaml
232
+ */
233
+ async resolvePort(name) {
234
+ let { port, host } = this.readProfilePort(name);
235
+ // 收集已占用端口:正在运行的网关 + 本次启动已分配的端口
236
+ const usedPorts = new Set(this.allocatedPorts);
237
+ for (const gw of Array.from(this.gateways.values())) {
238
+ if (gw.host === host && this.isProcessAlive(gw.pid)) {
239
+ usedPorts.add(gw.port);
240
+ }
241
+ }
242
+ if (usedPorts.has(port)) {
243
+ // 已管理端口冲突 → 找空闲端口
244
+ const newPort = await this.findFreePort(port, host);
245
+ console.log(`[GatewayManager] Port ${port} is in use for profile "${name}", reassigning to ${newPort}`);
246
+ this.writeProfilePort(name, newPort, host);
247
+ port = newPort;
248
+ }
249
+ else {
250
+ // 检查系统级端口占用(外部进程)
251
+ const available = await this.checkPortAvailable(port, host);
252
+ if (!available) {
253
+ const newPort = await this.findFreePort(port, host);
254
+ console.log(`[GatewayManager] Port ${port} is occupied by another process for profile "${name}", reassigning to ${newPort}`);
255
+ this.writeProfilePort(name, newPort, host);
256
+ port = newPort;
257
+ }
258
+ else {
259
+ // 端口空闲,写入完整配置(确保 api_server 配置齐全)
260
+ this.writeProfilePort(name, port, host);
261
+ }
262
+ }
263
+ this.allocatedPorts.add(port);
264
+ return { port, host };
265
+ }
266
+ // ============================
267
+ // 公开方法:状态查询
268
+ // ============================
269
+ /** 获取指定 profile 的网关 URL(代理路由使用) */
270
+ getUpstream(profileName) {
271
+ const name = profileName || this.activeProfile;
272
+ const gw = this.gateways.get(name);
273
+ if (gw?.url)
274
+ return gw.url;
275
+ const { port, host } = this.readProfilePort(name);
276
+ return `http://${host}:${port}`;
277
+ }
278
+ getActiveProfile() {
279
+ return this.activeProfile;
280
+ }
281
+ setActiveProfile(name) {
282
+ this.activeProfile = name;
283
+ }
284
+ /** 列出所有已知 profile 名称(通过 hermes CLI 或文件系统扫描) */
285
+ async listProfiles() {
286
+ try {
287
+ const { stdout } = await execFileAsync(HERMES_BIN, ['profile', 'list'], {
288
+ timeout: 10000,
289
+ windowsHide: true,
290
+ });
291
+ const profiles = [];
292
+ for (const line of stdout.trim().split('\n')) {
293
+ if (line.startsWith(' Profile') || line.match(/^ ─/))
294
+ continue;
295
+ const match = line.match(/^\s+(?:◆)?(\S+)\s{2,}/);
296
+ if (match)
297
+ profiles.push(match[1]);
298
+ }
299
+ return profiles;
300
+ }
301
+ catch {
302
+ // CLI 不可用时回退到文件系统扫描
303
+ const profiles = ['default'];
304
+ const profilesDir = (0, path_1.join)(HERMES_BASE, 'profiles');
305
+ const { existsSync, readdirSync } = require('fs');
306
+ if (existsSync(profilesDir)) {
307
+ for (const entry of readdirSync(profilesDir, { withFileTypes: true })) {
308
+ if (entry.isDirectory() && existsSync((0, path_1.join)(profilesDir, entry.name, 'config.yaml'))) {
309
+ profiles.push(entry.name);
310
+ }
311
+ }
312
+ }
313
+ return profiles;
314
+ }
315
+ }
316
+ /**
317
+ * 检测单个 profile 的网关状态(只读,不修改任何进程或配置)
318
+ *
319
+ * 流程:
320
+ * ① 读 PID 文件 → 检查进程是否存活
321
+ * ② 读配置端口 → health check
322
+ * ③ 两者都通过 → 匹配,注册
323
+ * ④ 否则 → 标记为未运行(不杀进程,由 startAll 处理)
324
+ */
325
+ async detectStatus(name) {
326
+ const pid = this.readPidFile(name);
327
+ const { port, host } = this.readProfilePort(name);
328
+ const url = `http://${host}:${port}`;
329
+ if (pid && this.isProcessAlive(pid) && await this.checkHealth(url)) {
330
+ this.gateways.set(name, { pid, port, host, url });
331
+ return { profile: name, port, host, url, running: true, pid };
332
+ }
333
+ // 未运行或端口不匹配
334
+ this.gateways.delete(name);
335
+ return { profile: name, port, host, url, running: false };
336
+ }
337
+ /** 检测所有 profile 的网关状态 */
338
+ async listAll() {
339
+ const profiles = await this.listProfiles();
340
+ const statuses = await Promise.all(profiles.map(name => this.detectStatus(name)));
341
+ return statuses;
342
+ }
343
+ // ============================
344
+ // 公开方法:启动 & 停止
345
+ // ============================
346
+ /**
347
+ * 启动单个 profile 的网关
348
+ * 启动前自动调用 resolvePort() 确保端口可用且配置完整
349
+ */
350
+ async start(name) {
351
+ const { port, host } = await this.resolvePort(name);
352
+ const hermesHome = this.profileDir(name);
353
+ const url = `http://${host}:${port}`;
354
+ if (needsRunMode) {
355
+ // WSL / Docker:无 systemd/launchd,用 "gateway run" 作为 detached 子进程
356
+ return new Promise((resolve, reject) => {
357
+ const env = { ...process.env, HERMES_HOME: hermesHome };
358
+ const child = (0, child_process_1.spawn)(HERMES_BIN, ['gateway', 'run', '--replace'], {
359
+ detached: true,
360
+ stdio: 'ignore',
361
+ windowsHide: true,
362
+ env,
363
+ });
364
+ child.unref();
365
+ const pid = child.pid ?? 0;
366
+ console.log(`[GatewayManager] Starting gateway for profile "${name}" (run mode, PID: ${pid}, port: ${port})`);
367
+ this.waitForReady(name, pid, port, host, url)
368
+ .then(resolve)
369
+ .catch(reject);
370
+ });
371
+ }
372
+ // 正常系统:先 start,失败则 restart(处理服务已运行的情况)
373
+ console.log(`[GatewayManager] Starting gateway for profile "${name}" (start mode, port: ${port})`);
374
+ const env = { ...process.env, HERMES_HOME: hermesHome };
375
+ try {
376
+ const { stdout } = await execFileAsync(HERMES_BIN, ['gateway', 'start'], {
377
+ timeout: 30000,
378
+ env,
379
+ windowsHide: true,
380
+ });
381
+ console.log(`[GatewayManager] gateway start output: ${stdout?.trim()}`);
382
+ }
383
+ catch {
384
+ // start 失败(可能服务已运行),用 restart
385
+ try {
386
+ const { stdout } = await execFileAsync(HERMES_BIN, ['gateway', 'restart'], {
387
+ timeout: 30000,
388
+ env,
389
+ windowsHide: true,
390
+ });
391
+ console.log(`[GatewayManager] gateway restart output: ${stdout?.trim()}`);
392
+ }
393
+ catch (err) {
394
+ console.log(`[GatewayManager] gateway start/restart (non-fatal): ${err.stderr?.trim() || err.message}`);
395
+ }
396
+ }
397
+ return this.waitForReady(name, 0, port, host, url);
398
+ }
399
+ /** 等待网关健康检查通过,最多 15 秒 */
400
+ async waitForReady(name, pid, port, host, url) {
401
+ const deadline = Date.now() + 15000;
402
+ while (Date.now() < deadline) {
403
+ if (pid && !this.isProcessAlive(pid)) {
404
+ throw new Error(`Gateway process exited unexpectedly (PID: ${pid})`);
405
+ }
406
+ if (await this.checkHealth(url, 2000)) {
407
+ // "gateway start" 自行管理进程,重新从 pid 文件读取实际 PID
408
+ const actualPid = this.readPidFile(name) ?? pid;
409
+ this.gateways.set(name, { pid: actualPid, port, host, url });
410
+ return { profile: name, port, host, url, running: true, pid: actualPid || undefined };
411
+ }
412
+ await new Promise(r => setTimeout(r, 500));
413
+ }
414
+ throw new Error(`Gateway health check timed out after 15000ms`);
415
+ }
416
+ /**
417
+ * 停止单个 profile 的网关
418
+ * 正常系统用 "gateway stop",WSL/Docker 直接 kill 进程组
419
+ * 返回前等待 health check 确认网关已真正停止
420
+ */
421
+ async stop(name, timeoutMs = 10000) {
422
+ // 记录当前 URL,用于确认停止
423
+ const gw = this.gateways.get(name);
424
+ const url = gw?.url || (() => {
425
+ const { port, host } = this.readProfilePort(name);
426
+ return `http://${host}:${port}`;
427
+ })();
428
+ if (!needsRunMode) {
429
+ // 正常系统:通过 hermes CLI 停止系统服务
430
+ try {
431
+ const hermesHome = this.profileDir(name);
432
+ const env = { ...process.env, HERMES_HOME: hermesHome };
433
+ await execFileAsync(HERMES_BIN, ['gateway', 'stop'], {
434
+ timeout: 10000,
435
+ env,
436
+ windowsHide: true,
437
+ });
438
+ }
439
+ catch { }
440
+ }
441
+ else {
442
+ // WSL / Docker:直接杀进程组
443
+ let pid = gw?.pid;
444
+ if (!pid) {
445
+ pid = this.readPidFile(name) ?? undefined;
446
+ }
447
+ if (pid) {
448
+ try {
449
+ process.kill(-pid, 'SIGTERM');
450
+ }
451
+ catch {
452
+ try {
453
+ process.kill(pid, 'SIGTERM');
454
+ }
455
+ catch { }
456
+ }
457
+ }
458
+ }
459
+ // 等待 health check 失败,确认网关已真正停止
460
+ const deadline = Date.now() + timeoutMs;
461
+ while (Date.now() < deadline) {
462
+ if (!(await this.checkHealth(url, 1000))) {
463
+ this.gateways.delete(name);
464
+ console.log(`[GatewayManager] Stopped gateway for profile "${name}"`);
465
+ return;
466
+ }
467
+ await new Promise(r => setTimeout(r, 300));
468
+ }
469
+ // 超时也清理
470
+ this.gateways.delete(name);
471
+ console.log(`[GatewayManager] Stopped gateway for profile "${name}" (timeout)`);
472
+ }
473
+ /** 停止所有已管理的网关(并行执行) */
474
+ async stopAll() {
475
+ const entries = Array.from(this.gateways.keys());
476
+ await Promise.allSettled(entries.map(name => this.stop(name)));
477
+ }
478
+ // ============================
479
+ // 批量操作(启动时调用)
480
+ // ============================
481
+ /** 扫描所有 profile,检测网关运行状态并注册 */
482
+ async detectAllOnStartup() {
483
+ console.log('[GatewayManager] Scanning profiles for running gateways...');
484
+ const profiles = await this.listProfiles();
485
+ for (const name of profiles) {
486
+ const status = await this.detectStatus(name);
487
+ if (status.running) {
488
+ console.log(`[GatewayManager] ✓ ${name}: running (PID: ${status.pid}, port: ${status.port})`);
489
+ }
490
+ else {
491
+ console.log(`[GatewayManager] ○ ${name}: stopped`);
492
+ }
493
+ }
494
+ }
495
+ /**
496
+ * 启动所有未运行的网关
497
+ *
498
+ * 两阶段执行:
499
+ * Phase 1 — 顺序处理:检查状态、清理旧进程、分配端口
500
+ * Phase 2 — 并行启动网关进程
501
+ */
502
+ async startAll() {
503
+ const profiles = await this.listProfiles();
504
+ // Phase 1: 顺序处理
505
+ const toStart = [];
506
+ for (const name of profiles) {
507
+ const existing = this.gateways.get(name);
508
+ if (existing && this.isProcessAlive(existing.pid)) {
509
+ console.log(`[GatewayManager] ${name}: already running (PID: ${existing.pid})`);
510
+ continue;
511
+ }
512
+ // 有 PID 文件但进程未在正确端口运行 → 旧进程,先停掉
513
+ const pid = this.readPidFile(name);
514
+ if (pid && this.isProcessAlive(pid)) {
515
+ console.log(`[GatewayManager] ${name}: stale process (PID: ${pid}), stopping`);
516
+ try {
517
+ await this.stop(name);
518
+ }
519
+ catch { }
520
+ }
521
+ await this.resolvePort(name);
522
+ toStart.push(name);
523
+ }
524
+ // Phase 2: 并行启动
525
+ const tasks = toStart.map(async (name) => {
526
+ try {
527
+ await this.start(name);
528
+ }
529
+ catch (err) {
530
+ console.error(`[GatewayManager] ✗ ${name}: failed to start — ${err.message}`);
531
+ }
532
+ });
533
+ await Promise.allSettled(tasks);
534
+ }
535
+ }
536
+ exports.GatewayManager = GatewayManager;
@@ -0,0 +1,24 @@
1
+ export interface HermesSessionRow {
2
+ id: string;
3
+ source: string;
4
+ user_id: string | null;
5
+ model: string;
6
+ title: string | null;
7
+ started_at: number;
8
+ ended_at: number | null;
9
+ end_reason: string | null;
10
+ message_count: number;
11
+ tool_call_count: number;
12
+ input_tokens: number;
13
+ output_tokens: number;
14
+ cache_read_tokens: number;
15
+ cache_write_tokens: number;
16
+ reasoning_tokens: number;
17
+ billing_provider: string | null;
18
+ estimated_cost_usd: number;
19
+ actual_cost_usd: number | null;
20
+ cost_status: string;
21
+ preview: string;
22
+ last_active: number;
23
+ }
24
+ export declare function listSessionSummaries(source?: string, limit?: number): Promise<HermesSessionRow[]>;