diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 82faa06..fc994f2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,21 +7,6 @@ on:
branches: [main]
jobs:
- backend:
- name: 后端(Java)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: 安装 JDK 17
- uses: actions/setup-java@v4
- with:
- java-version: '17'
- distribution: temurin
- cache: maven
- # 单测均为纯单元测试,不依赖 MySQL,可直接跑
- - name: 编译 + 单测
- run: ./mvnw -B clean test
-
app:
name: 桌面端(Flutter)
runs-on: ubuntu-latest
@@ -40,26 +25,3 @@ jobs:
run: flutter analyze
- name: 单测
run: flutter test
-
- cli:
- name: CLI(Node)
- runs-on: ubuntu-latest
- defaults:
- run:
- working-directory: clients/cli
- steps:
- - uses: actions/checkout@v4
- - name: 安装 Node 20
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- cache: npm
- cache-dependency-path: clients/cli/package-lock.json
- - name: 装依赖
- run: npm ci
- - name: 类型检查
- run: npm run typecheck
- - name: 单测
- run: npm test
- - name: 构建
- run: npm run build
diff --git a/.gitignore b/.gitignore
index f1f81ee..209b83b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,14 @@ logs/
# CLI 客户端(Node)构建产物与依赖
clients/cli/node_modules/
clients/cli/dist/
+
+# 本地保留的私有实现:不再提交到远程仓库
+/src/
+/pom.xml
+/.mvn/
+/mvnw
+/mvnw.cmd
+/Dockerfile
+/docker-compose.yml
+/.env.example
+/clients/cli/
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
deleted file mode 100644
index 7967f30..0000000
Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
deleted file mode 100644
index aa7b1d6..0000000
--- a/.mvn/wrapper/maven-wrapper.properties
+++ /dev/null
@@ -1,19 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-wrapperVersion=3.3.2
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
-wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 783e8f9..11cc008 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,106 +1,45 @@
# 贡献指南
-感谢你对 LowenSSH 的兴趣。这份指南帮你快速上手本地开发、了解项目约定与提交流程。
-
-## 项目结构速览
-
-LowenSSH 是「同一套理念、三种独立形态」的项目,三端互不依赖:
-
-| 形态 | 目录 | 技术栈 |
-|------|------|--------|
-| 后端服务 | `src/` | Java 17 · Spring Boot 3.4 · Spring AI |
-| 桌面客户端 | `clients/app/` | Flutter(macOS / Windows) |
-| CLI 客户端 | `clients/cli/` | Node 20 · Ink(TUI) |
-
-核心理念(手写 Agent loop + Deny/Ask/Allow 安全门禁 + 上下文管理)在三端各自实现,**门禁规则与事件语义需手动对齐**。改动涉及核心逻辑时,请留意是否需要同步到其他端。
-
-设计取舍详见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
+感谢你对 LowenSSH 的兴趣。远程仓库当前仅维护 [`clients/app/`](clients/app/) 下的 Flutter 桌面客户端。
## 本地开发环境
-### 后端(`src/`)
-
-需要 JDK 17。项目自带 Maven Wrapper,无需预装 Maven。
-
-```bash
-export MYSQL_PASSWORD='你的MySQL密码'
-export GLM_API_KEY='你的智谱AI key' # https://open.bigmodel.cn 申请
-
-# 初始化数据库
-mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS lowenssh DEFAULT CHARSET utf8mb4;"
-mysql -u root -p lowenssh < src/main/resources/schema.sql
-
-./mvnw spring-boot:run # Windows 用 mvnw.cmd
-```
-
-运行测试:
-
-```bash
-./mvnw test
-```
-
-### 桌面端(`clients/app/`)
-
-需要 Flutter SDK 3.12+。详见 [clients/app/README.md](clients/app/README.md)。
+需要 Flutter SDK 3.12+。macOS 构建需要 Xcode;Windows 构建需要 Visual Studio,并安装“使用 C++ 的桌面开发”工作负载。
```bash
cd clients/app
flutter pub get
-flutter run -d macos # 或 -d windows
-flutter analyze # 提交前确保零问题
+flutter run -d macos # 或 flutter run -d windows
+flutter analyze
+flutter test
```
-### CLI(`clients/cli/`)
-
-需要 Node 20+。详见 [clients/cli/README.md](clients/cli/README.md)。
+完整运行、打包和模型配置说明见 [`clients/app/README.md`](clients/app/README.md)。
## 代码约定
-- **注释用中文,标识符(变量/函数/类名)用英文**。
-- 优先可读性,不做过度优化;改动范围尽量小,不顺手重构无关代码。
-- 后端 Java 用 Java 17 语法,不用过时写法。
-- Flutter 优先 Composition 风格的 Widget 拆分,复用动画/组件放对应封装文件。
-- 涉及安全门禁规则改动,必须补充或更新对应单元测试。
+- 注释使用中文,标识符使用英文。
+- 优先可读性,不重构与当前任务无关的代码。
+- Widget 保持职责清晰,可复用动画和组件放入对应封装文件。
+- 修改安全门禁、凭据保存或 SSH 执行逻辑时,必须补充相应测试。
+- 不提交 API Key、密码、`.env`、本机构建产物和日志。
## 提交前检查
-- 后端:`./mvnw test` 全绿。
-- 桌面端:`flutter analyze` 零问题,`flutter build macos --debug`(或 windows)可编译。
-- CLI:按 `clients/cli/README.md` 的检查方式验证。
-- 不提交任何明文密钥、`.env` 文件、本地构建产物。
-
-## 提交信息规范
-
-- 用简洁的中文描述「做了什么」,必要时补充「为什么」。
-- 前缀标明影响范围,例如 `app:`、`cli:`、`backend:`、`docs:`。
-- 一个提交聚焦一件事,避免把无关改动混在一起。
-
-示例:
-
-```
-app: 修复切主题时终端不变色
-
-终端配色从冻结的顶层 final 改为按当前 palette 实时计算。
+```bash
+cd clients/app
+flutter analyze
+flutter test
+flutter build macos --debug # Windows 使用对应构建命令
```
-## Pull Request 流程
-
-1. 从 `main` 切出 feature 分支(如 `feature/xxx`、`fix/xxx`),**不要直接提交到 main**。
-2. 完成开发并通过提交前检查。
-3. 推送分支并发起 PR,目标分支为 `main`。
-4. PR 描述请包含:改了什么、为什么、如何测试、是否涉及多端对齐。
-5. 等待 review,合并后删除 feature 分支。
-
-## 安全相关改动
-
-本项目的安全门禁(高危命令拦截)是真实防护,不是演示。涉及以下改动请在 PR 中重点说明:
-
-- 修改 deny / ask 规则名单。
-- 调整命令拆段、正则匹配逻辑。
-- 改动密码加密、密钥读取、审计落库相关代码。
+## 提交与 Pull Request
-发现安全漏洞请不要直接提 public issue,先通过私下渠道联系维护者。
+1. 从 `main` 创建功能分支,不直接提交到 `main`。
+2. 一个提交只处理一类问题,提交信息使用简洁中文。
+3. 推送分支后创建 PR,目标分支为 `main`。
+4. PR 说明应包含改动内容、原因、验证方式和安全影响。
-## 报告问题
+## 安全问题
-提 issue 时请尽量包含:复现步骤、预期与实际行为、运行环境(操作系统、形态、版本)、相关日志或截图。
+安全门禁和凭据保护属于真实防护。发现可导致未授权命令执行、凭据泄露或安全规则绕过的问题时,请先通过私下渠道联系维护者,不要直接公开利用细节。
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 0355bd0..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,20 +0,0 @@
-# ---- 阶段 1:打后端 jar ----
-FROM maven:3.9-eclipse-temurin-17 AS backend
-WORKDIR /build
-# 先拷 pom 预热依赖缓存:源码变了不必重新下依赖
-COPY pom.xml ./
-RUN mvn -q dependency:go-offline
-# 拷后端源码
-COPY src/ ./src/
-# 跳过测试打包(测试需要 MySQL,构建环境没有)
-RUN mvn -q clean package -DskipTests
-
-# ---- 阶段 2:运行 ----
-# 只带 JRE,镜像更小
-FROM eclipse-temurin:17-jre
-WORKDIR /app
-COPY --from=backend /build/target/lowenssh-*.jar app.jar
-EXPOSE 8081
-# 纯后端 API 服务,供 Flutter 桌面端 / CLI 客户端连接
-# 密钥全走环境变量,镜像里不含任何凭据
-ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/README.md b/README.md
index 590177c..46448d3 100644
--- a/README.md
+++ b/README.md
@@ -1,129 +1,107 @@
# LowenSSH
[](LICENSE)
-[](https://openjdk.org/projects/jdk/17/)
-[](https://spring.io/projects/spring-boot)
[](https://flutter.dev)
+[](https://github.com/Lowen-0621/LowenSSH/actions/workflows/ci.yml)
[](CONTRIBUTING.md)
-AI 驱动的 SSH 智能运维 Agent。给它一个运维目标和一台服务器,它会像工程师一样一步步排查:自己决定跑什么命令、读结果、调整思路,直到给出结论。危险命令会被安全门禁实时拦截。
+AI 驱动的 SSH 智能运维 Agent。用户给出运维目标和目标服务器后,Agent 会自主选择工具、读取执行结果并持续调整方案;危险命令在实际执行前经过安全门禁。
-核心看点是「看得见 AI 在干什么,也看得见安全护栏起作用」——整个 agentic loop 是从零手写的,不套用任何编排框架。
+## 仓库范围
-## 界面预览
+公开仓库仅维护 Flutter 桌面客户端,代码位于 [`clients/app/`](clients/app/)。客户端内置 SSH 连接、Agent loop、安全门禁、上下文管理和大模型调用能力,可以独立运行。
-> 截图待补充。桌面端运行界面、解锁动画、智能体面板与安全门禁可视化效果。
->
->
+Java 后端与 Node CLI 为本地实现,不再包含在远程仓库当前版本中。
-## 三种形态
+## 核心能力
-同一套「Agent loop + 安全门禁 + 上下文管理」理念,落地为三个独立实现,按需选用:
-
-| 形态 | 目录 | 技术栈 | 说明 |
-|------|------|--------|------|
-| **后端服务** | `src/` | Java 17 · Spring Boot 3.4 · Spring AI | REST + SSE API,参考实现,逻辑最完整 |
-| **桌面客户端** | `clients/app/` | Flutter(macOS / Windows) | 独立桌面应用,内置全套逻辑,直连大模型 |
-| **CLI 客户端** | `clients/cli/` | Node 20 · Ink(TUI) | 终端里跑,类 Claude Code 的交互,内置全套逻辑 |
-
-三者**互不依赖**:桌面端和 CLI 各自内置 SSH + Agent loop + 门禁 + 大模型调用,不需要先起后端。门禁规则与事件语义在三端手动对齐。
-
-> 想了解手写 agentic loop、安全门禁、上下文管理的设计取舍,见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
-
-## 能力
-
-- **手写 Agentic Loop**:不依赖 LangChain 之类的编排框架,自己实现「模型决策 → 调工具 → 喂回结果 → 再决策」的循环,逻辑完全可控、可读。
-- **安全门禁三态**:每条命令在执行前经过 `deny / ask / allow` 判定。`rm -rf`、`find -delete` 等高危操作直接拦截,模型被拦后会自主改用安全方式。
-- **流式可视化**:实时推送多类事件(模型 token、要跑的命令、命令结果、被拦截、最终结论、错误),逐字渲染整个排查过程。
-- **上下文管理**:多轮对话爆 context 时,自动做大工具结果截断 + 全量 LLM 摘要,复用消息表持久化。
-- **全程审计**:每次连接、每条命令、每个拦截决策都落库,可追溯。
+- **手写 Agent Loop**:实现“模型决策 → 工具调用 → 结果回灌 → 再决策”的循环。
+- **安全门禁**:命令执行前进行 `deny / ask / allow` 判定,高风险操作需要人工确认。
+- **流式过程展示**:区分模型输出、工具调用、工具结果、安全拦截和最终结论。
+- **上下文治理**:对大工具结果进行截断,并在历史过长时生成摘要。
+- **SSH 工具集**:支持命令执行、日志读取、文件管理、监控和端口转发。
## 技术栈
-**后端**:Java 17 · Spring Boot 3.4 · Spring AI 1.1 · JSch(SSH)· MyBatis-Plus · MySQL · GLM-4.6(OpenAI 兼容协议,可换任意兼容模型)
-
-**桌面端**:Flutter · Dart(macOS / Windows 桌面)
+Flutter · Dart · Riverpod · dartssh2 · Dio · PointyCastle · Secure Storage · xterm · docking
-**CLI**:Node 20 · TypeScript · Ink · ssh2 · openai SDK
+## 本地启动
-## 快速开始(后端服务)
+### 1. 准备环境
-后端提供 REST + SSE API。客户端的运行方式见各自目录的 README([桌面端](clients/app/README.md) · [CLI](clients/cli/README.md))。
+- Flutter SDK 3.12+(Dart 3.12+)
+- macOS:安装 Xcode
+- Windows:安装 Visual Studio,并勾选“使用 C++ 的桌面开发”
-### 方式一:Docker 一键启动(推荐)
+```bash
+flutter doctor
+```
-需要 Docker。两个密钥走环境变量,不写进任何文件:
+### 2. 安装依赖
```bash
-export MYSQL_PASSWORD='给MySQL容器设的root密码'
-export GLM_API_KEY='你的智谱AI key' # https://open.bigmodel.cn 申请
-docker compose up --build
+cd clients/app
+flutter pub get
```
-compose 会自动起 MySQL(建库 + 执行 schema.sql 建表)、构建后端、等 DB 就绪后启动应用。API 监听 http://localhost:8081。
-
-### 方式二:本地手动启动
+### 3. 配置大模型
-#### 1. 准备环境变量
+启动后可以在“设置”中填写 API Key、模型名称和 OpenAI 兼容接口地址。也可以通过环境变量临时注入默认 GLM Key:
-应用读取两个环境变量,源码里不含任何明文密钥:
+macOS / Linux:
```bash
-export MYSQL_PASSWORD='你的MySQL密码' # 本机 MySQL root 密码,空密码则设为 ''
-export GLM_API_KEY='你的智谱AI key' # https://open.bigmodel.cn 申请
+export GLM_API_KEY='你的 API Key'
```
-> 不设这两个变量,启动会因连不上 MySQL(500)或鉴权失败(401)而报错。
+Windows PowerShell:
+
+```powershell
+$env:GLM_API_KEY='你的 API Key'
+```
-#### 2. 初始化数据库
+环境变量只在当前进程中使用,不会被写回配置文件。
-先建库,再执行建表脚本:
+### 4. 运行桌面端
```bash
-mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS lowenssh DEFAULT CHARSET utf8mb4;"
-mysql -u root -p lowenssh < src/main/resources/schema.sql
+cd clients/app
+flutter run -d macos # macOS
+flutter run -d windows # Windows
```
-#### 3. 启动后端
-
-项目自带 Maven Wrapper,无需预装 Maven:
+### 5. 检查与打包
```bash
-./mvnw spring-boot:run # Windows 用 mvnw.cmd
+cd clients/app
+flutter analyze
+flutter test
+flutter build macos --release # macOS
+flutter build windows --release # Windows
```
-API 监听 http://localhost:8081。
+更多模型配置和产物目录说明见 [`clients/app/README.md`](clients/app/README.md)。
## 项目结构
-```
+```text
LowenSSH/
-├── src/main/java/com/lowenssh/
-│ ├── agent/ # Agent 核心:loop、SSE 事件、上下文管理、安全门禁
-│ ├── ssh/ # JSch SSH 执行
-│ └── ...
-├── src/main/resources/
-│ ├── application.yml # 配置(密钥走环境变量)
-│ └── schema.sql # 建表脚本
-├── clients/
-│ ├── app/ # Flutter 桌面客户端(见 clients/app/README.md)
-│ └── cli/ # Node CLI 客户端(见 clients/cli/README.md)
-└── DESIGN.md # 设计规范
+├── clients/app/ # Flutter 桌面客户端
+├── docs/ # 架构与项目文档
+├── DESIGN.md # 设计规范
+└── CONTRIBUTING.md # 贡献指南
```
## 安全说明
-- 所有密钥走环境变量,源码无任何明文凭据。
-- 客户端密码字段不写入明文持久化(AES-GCM 加密落库),不打印到控制台。
-- 安全门禁的高危命令规则(含 `rm -rf`、`find -delete` 等变体)是真实防护,请勿在生产前移除。
-- 这是一个运维 Agent,会真实在目标服务器执行命令。请只连接你有权操作的服务器。
+- 主机密码经 AES-GCM 加密后保存,不记录明文。
+- 端口转发默认只绑定 `127.0.0.1`。
+- 危险命令执行前必须经过安全策略和人工确认。
+- 本项目会在目标服务器执行真实操作,请只连接你有权管理的服务器。
## 贡献
-欢迎提 issue 和 PR。开发环境搭建、代码约定、提交规范见 [CONTRIBUTING.md](CONTRIBUTING.md)。
+欢迎提交 Issue 和 PR。开发环境、代码约定和提交流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。
## License
diff --git a/clients/cli/.gitignore b/clients/cli/.gitignore
deleted file mode 100644
index 9d3b50f..0000000
--- a/clients/cli/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-node_modules/
-dist/
-*.log
-.lowenssh/
diff --git a/clients/cli/README.md b/clients/cli/README.md
deleted file mode 100644
index 9e81585..0000000
--- a/clients/cli/README.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# LowenSSH CLI
-
-LowenSSH 的命令行形态,在终端里跑的 AI SSH 运维 Agent,交互体验类似 Claude Code。基于 Node + Ink(TUI)。
-
-内置全套逻辑——SSH 连接、手写 Agent loop、安全门禁、上下文管理、直连大模型——**不依赖项目的 Java 后端**,独立运行。
-
-## 环境要求
-
-- Node.js 20+
-
-## 安装依赖
-
-```bash
-cd clients/cli
-npm install
-```
-
-## 大模型配置
-
-CLI 需要大模型 API Key(默认接入 GLM,走 OpenAI 兼容协议)。两种方式,环境变量优先:
-
-```bash
-export GLM_API_KEY='你的智谱AI key' # https://open.bigmodel.cn 申请
-```
-
-或写进配置文件 `~/.lowenssh/config.json`(文件权限 600)。缺 Key 时启动会给出明确提示,不会静默失败。
-
-## 运行
-
-开发模式(直接跑 TS 源码):
-
-```bash
-npm run dev # 启动交互式 TUI
-npm run dev add-host # 添加主机(无需 API Key)
-```
-
-构建后作为命令安装:
-
-```bash
-npm run build # 产物输出到 dist/
-npm link # 注册全局命令 lowenssh
-lowenssh # 启动
-lowenssh add-host # 添加主机
-```
-
-## 开发
-
-```bash
-npm test # vitest 跑单测(门禁、加密)
-npm run typecheck # tsc 类型检查
-```
-
-## 安全说明
-
-- 主机密码 AES-GCM 加密后落盘,配置文件权限 600,不存明文。
-- 环境变量注入的 API Key 不会被写回配置文件。
-- 安全门禁的高危命令规则与后端、桌面端对齐,是真实防护。
-- 这是一个运维 Agent,会真实在目标服务器执行命令。请只连接你有权操作的服务器。
diff --git a/clients/cli/package-lock.json b/clients/cli/package-lock.json
deleted file mode 100644
index cf548dd..0000000
--- a/clients/cli/package-lock.json
+++ /dev/null
@@ -1,4113 +0,0 @@
-{
- "name": "lowenssh",
- "version": "0.1.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "lowenssh",
- "version": "0.1.0",
- "dependencies": {
- "ink": "^5.1.0",
- "ink-text-input": "^6.0.0",
- "openai": "^4.77.0",
- "react": "^18.3.1",
- "ssh2": "^1.16.0"
- },
- "bin": {
- "lowenssh": "dist/cli.js"
- },
- "devDependencies": {
- "@types/node": "^22.10.0",
- "@types/react": "^18.3.1",
- "@types/ssh2": "^1.15.0",
- "tsup": "^8.3.5",
- "tsx": "^4.19.2",
- "typescript": "^5.7.2",
- "vitest": "^2.1.8"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/@alcalzone/ansi-tokenize": {
- "version": "0.1.3",
- "resolved": "https://registry.npmmirror.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz",
- "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.1",
- "is-fullwidth-code-point": "^4.0.0"
- },
- "engines": {
- "node": ">=14.13.1"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
- "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
- "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
- "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
- "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
- "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
- "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
- "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
- "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
- "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
- "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
- "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
- "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
- "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
- "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
- "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
- "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
- "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
- "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
- "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
- "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
- "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
- "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
- "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
- "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
- "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
- "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
- "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
- "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
- "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
- "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
- "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
- "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
- "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
- "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
- "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
- "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
- "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
- "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
- "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
- "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
- "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
- "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
- "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
- "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
- "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
- "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
- },
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
- "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
- "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
- "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
- "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
- "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@types/estree": {
- "version": "1.0.9",
- "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz",
- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "22.20.0",
- "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.0.tgz",
- "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.21.0"
- }
- },
- "node_modules/@types/node-fetch": {
- "version": "2.6.13",
- "resolved": "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.13.tgz",
- "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "form-data": "^4.0.4"
- }
- },
- "node_modules/@types/prop-types": {
- "version": "15.7.15",
- "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
- "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@types/react": {
- "version": "18.3.31",
- "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz",
- "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/prop-types": "*",
- "csstype": "^3.2.2"
- }
- },
- "node_modules/@types/ssh2": {
- "version": "1.15.5",
- "resolved": "https://registry.npmmirror.com/@types/ssh2/-/ssh2-1.15.5.tgz",
- "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "^18.11.18"
- }
- },
- "node_modules/@types/ssh2/node_modules/@types/node": {
- "version": "18.19.130",
- "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz",
- "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~5.26.4"
- }
- },
- "node_modules/@types/ssh2/node_modules/undici-types": {
- "version": "5.26.5",
- "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
- "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@vitest/expect": {
- "version": "2.1.9",
- "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-2.1.9.tgz",
- "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "2.1.9",
- "@vitest/utils": "2.1.9",
- "chai": "^5.1.2",
- "tinyrainbow": "^1.2.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/mocker": {
- "version": "2.1.9",
- "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-2.1.9.tgz",
- "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "2.1.9",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.12"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^5.0.0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/pretty-format": {
- "version": "2.1.9",
- "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
- "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyrainbow": "^1.2.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "2.1.9",
- "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-2.1.9.tgz",
- "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "2.1.9",
- "pathe": "^1.1.2"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner/node_modules/pathe": {
- "version": "1.1.2",
- "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz",
- "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@vitest/snapshot": {
- "version": "2.1.9",
- "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-2.1.9.tgz",
- "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "2.1.9",
- "magic-string": "^0.30.12",
- "pathe": "^1.1.2"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/snapshot/node_modules/pathe": {
- "version": "1.1.2",
- "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz",
- "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@vitest/spy": {
- "version": "2.1.9",
- "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-2.1.9.tgz",
- "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyspy": "^3.0.2"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "2.1.9",
- "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-2.1.9.tgz",
- "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "2.1.9",
- "loupe": "^3.1.2",
- "tinyrainbow": "^1.2.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/abort-controller": {
- "version": "3.0.0",
- "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz",
- "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
- "license": "MIT",
- "dependencies": {
- "event-target-shim": "^5.0.0"
- },
- "engines": {
- "node": ">=6.5"
- }
- },
- "node_modules/acorn": {
- "version": "8.17.0",
- "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz",
- "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/agentkeepalive": {
- "version": "4.6.0",
- "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
- "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
- "license": "MIT",
- "dependencies": {
- "humanize-ms": "^1.2.1"
- },
- "engines": {
- "node": ">= 8.0.0"
- }
- },
- "node_modules/ansi-escapes": {
- "version": "7.3.0",
- "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
- "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
- "license": "MIT",
- "dependencies": {
- "environment": "^1.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/any-promise": {
- "version": "1.3.0",
- "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz",
- "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/asn1": {
- "version": "0.2.6",
- "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz",
- "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": "~2.1.0"
- }
- },
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "license": "MIT"
- },
- "node_modules/auto-bind": {
- "version": "5.0.1",
- "resolved": "https://registry.npmmirror.com/auto-bind/-/auto-bind-5.0.1.tgz",
- "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==",
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/bcrypt-pbkdf": {
- "version": "1.0.2",
- "resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
- "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "tweetnacl": "^0.14.3"
- }
- },
- "node_modules/buildcheck": {
- "version": "0.0.7",
- "resolved": "https://registry.npmmirror.com/buildcheck/-/buildcheck-0.0.7.tgz",
- "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
- "optional": true,
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/bundle-require": {
- "version": "5.1.0",
- "resolved": "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.1.0.tgz",
- "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "load-tsconfig": "^0.2.3"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "peerDependencies": {
- "esbuild": ">=0.18"
- }
- },
- "node_modules/cac": {
- "version": "6.7.14",
- "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz",
- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/chai": {
- "version": "5.3.3",
- "resolved": "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz",
- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/chalk": {
- "version": "5.6.2",
- "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz",
- "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
- "license": "MIT",
- "engines": {
- "node": "^12.17.0 || ^14.13 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/check-error": {
- "version": "2.1.3",
- "resolved": "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz",
- "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- }
- },
- "node_modules/chokidar": {
- "version": "4.0.3",
- "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
- "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "readdirp": "^4.0.1"
- },
- "engines": {
- "node": ">= 14.16.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/cli-boxes": {
- "version": "3.0.0",
- "resolved": "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-3.0.0.tgz",
- "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-cursor": {
- "version": "4.0.0",
- "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-4.0.0.tgz",
- "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
- "license": "MIT",
- "dependencies": {
- "restore-cursor": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-truncate": {
- "version": "4.0.0",
- "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-4.0.0.tgz",
- "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
- "license": "MIT",
- "dependencies": {
- "slice-ansi": "^5.0.0",
- "string-width": "^7.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-truncate/node_modules/slice-ansi": {
- "version": "5.0.0",
- "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-5.0.0.tgz",
- "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.0.0",
- "is-fullwidth-code-point": "^4.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/slice-ansi?sponsor=1"
- }
- },
- "node_modules/code-excerpt": {
- "version": "4.0.0",
- "resolved": "https://registry.npmmirror.com/code-excerpt/-/code-excerpt-4.0.0.tgz",
- "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
- "license": "MIT",
- "dependencies": {
- "convert-to-spaces": "^2.0.1"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- }
- },
- "node_modules/combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "license": "MIT",
- "dependencies": {
- "delayed-stream": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/commander": {
- "version": "4.1.1",
- "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
- "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/confbox": {
- "version": "0.1.8",
- "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz",
- "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/consola": {
- "version": "3.4.2",
- "resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz",
- "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^14.18.0 || >=16.10.0"
- }
- },
- "node_modules/convert-to-spaces": {
- "version": "2.0.1",
- "resolved": "https://registry.npmmirror.com/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
- "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- }
- },
- "node_modules/cpu-features": {
- "version": "0.0.10",
- "resolved": "https://registry.npmmirror.com/cpu-features/-/cpu-features-0.0.10.tgz",
- "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
- "hasInstallScript": true,
- "optional": true,
- "dependencies": {
- "buildcheck": "~0.0.6",
- "nan": "^2.19.0"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/csstype": {
- "version": "3.2.3",
- "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/deep-eql": {
- "version": "5.0.2",
- "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz",
- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/emoji-regex": {
- "version": "10.6.0",
- "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz",
- "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
- "license": "MIT"
- },
- "node_modules/environment": {
- "version": "1.1.0",
- "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz",
- "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-module-lexer": {
- "version": "1.7.0",
- "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.2",
- "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
- "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-set-tostringtag": {
- "version": "2.1.0",
- "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
- "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-toolkit": {
- "version": "1.48.1",
- "resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.48.1.tgz",
- "integrity": "sha512-wfnXlwd5I75eXRtdD2vuEs50xHHESECDsGD7yiQnfFVNoa5522NwXEbmgo98LfiukSQHs+mBM7/YG3qKJB9/mQ==",
- "license": "MIT",
- "workspaces": [
- "docs",
- "benchmarks"
- ]
- },
- "node_modules/esbuild": {
- "version": "0.27.7",
- "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz",
- "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.7",
- "@esbuild/android-arm": "0.27.7",
- "@esbuild/android-arm64": "0.27.7",
- "@esbuild/android-x64": "0.27.7",
- "@esbuild/darwin-arm64": "0.27.7",
- "@esbuild/darwin-x64": "0.27.7",
- "@esbuild/freebsd-arm64": "0.27.7",
- "@esbuild/freebsd-x64": "0.27.7",
- "@esbuild/linux-arm": "0.27.7",
- "@esbuild/linux-arm64": "0.27.7",
- "@esbuild/linux-ia32": "0.27.7",
- "@esbuild/linux-loong64": "0.27.7",
- "@esbuild/linux-mips64el": "0.27.7",
- "@esbuild/linux-ppc64": "0.27.7",
- "@esbuild/linux-riscv64": "0.27.7",
- "@esbuild/linux-s390x": "0.27.7",
- "@esbuild/linux-x64": "0.27.7",
- "@esbuild/netbsd-arm64": "0.27.7",
- "@esbuild/netbsd-x64": "0.27.7",
- "@esbuild/openbsd-arm64": "0.27.7",
- "@esbuild/openbsd-x64": "0.27.7",
- "@esbuild/openharmony-arm64": "0.27.7",
- "@esbuild/sunos-x64": "0.27.7",
- "@esbuild/win32-arm64": "0.27.7",
- "@esbuild/win32-ia32": "0.27.7",
- "@esbuild/win32-x64": "0.27.7"
- }
- },
- "node_modules/escape-string-regexp": {
- "version": "2.0.0",
- "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
- "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.0"
- }
- },
- "node_modules/event-target-shim": {
- "version": "5.0.1",
- "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz",
- "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/expect-type": {
- "version": "1.3.0",
- "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz",
- "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/fix-dts-default-cjs-exports": {
- "version": "1.0.1",
- "resolved": "https://registry.npmmirror.com/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz",
- "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "magic-string": "^0.30.17",
- "mlly": "^1.7.4",
- "rollup": "^4.34.8"
- }
- },
- "node_modules/form-data": {
- "version": "4.0.6",
- "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz",
- "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
- "license": "MIT",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.4",
- "mime-types": "^2.1.35"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/form-data-encoder": {
- "version": "1.7.2",
- "resolved": "https://registry.npmmirror.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
- "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
- "license": "MIT"
- },
- "node_modules/formdata-node": {
- "version": "4.4.1",
- "resolved": "https://registry.npmmirror.com/formdata-node/-/formdata-node-4.4.1.tgz",
- "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
- "license": "MIT",
- "dependencies": {
- "node-domexception": "1.0.0",
- "web-streams-polyfill": "4.0.0-beta.3"
- },
- "engines": {
- "node": ">= 12.20"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-east-asian-width": {
- "version": "1.6.0",
- "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
- "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-tostringtag": {
- "version": "1.0.2",
- "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "license": "MIT",
- "dependencies": {
- "has-symbols": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.4",
- "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz",
- "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/humanize-ms": {
- "version": "1.2.1",
- "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz",
- "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.0.0"
- }
- },
- "node_modules/indent-string": {
- "version": "5.0.0",
- "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-5.0.0.tgz",
- "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ink": {
- "version": "5.2.1",
- "resolved": "https://registry.npmmirror.com/ink/-/ink-5.2.1.tgz",
- "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==",
- "license": "MIT",
- "dependencies": {
- "@alcalzone/ansi-tokenize": "^0.1.3",
- "ansi-escapes": "^7.0.0",
- "ansi-styles": "^6.2.1",
- "auto-bind": "^5.0.1",
- "chalk": "^5.3.0",
- "cli-boxes": "^3.0.0",
- "cli-cursor": "^4.0.0",
- "cli-truncate": "^4.0.0",
- "code-excerpt": "^4.0.0",
- "es-toolkit": "^1.22.0",
- "indent-string": "^5.0.0",
- "is-in-ci": "^1.0.0",
- "patch-console": "^2.0.0",
- "react-reconciler": "^0.29.0",
- "scheduler": "^0.23.0",
- "signal-exit": "^3.0.7",
- "slice-ansi": "^7.1.0",
- "stack-utils": "^2.0.6",
- "string-width": "^7.2.0",
- "type-fest": "^4.27.0",
- "widest-line": "^5.0.0",
- "wrap-ansi": "^9.0.0",
- "ws": "^8.18.0",
- "yoga-layout": "~3.2.1"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/react": ">=18.0.0",
- "react": ">=18.0.0",
- "react-devtools-core": "^4.19.1"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "react-devtools-core": {
- "optional": true
- }
- }
- },
- "node_modules/ink-text-input": {
- "version": "6.0.0",
- "resolved": "https://registry.npmmirror.com/ink-text-input/-/ink-text-input-6.0.0.tgz",
- "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==",
- "license": "MIT",
- "dependencies": {
- "chalk": "^5.3.0",
- "type-fest": "^4.18.2"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "ink": ">=5",
- "react": ">=18"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "4.0.0",
- "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
- "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/is-in-ci": {
- "version": "1.0.0",
- "resolved": "https://registry.npmmirror.com/is-in-ci/-/is-in-ci-1.0.0.tgz",
- "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==",
- "license": "MIT",
- "bin": {
- "is-in-ci": "cli.js"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/joycon": {
- "version": "3.1.1",
- "resolved": "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz",
- "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT"
- },
- "node_modules/lilconfig": {
- "version": "3.1.3",
- "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz",
- "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/antonk52"
- }
- },
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/load-tsconfig": {
- "version": "0.2.5",
- "resolved": "https://registry.npmmirror.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
- "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- }
- },
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
- "node_modules/loupe": {
- "version": "3.2.1",
- "resolved": "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz",
- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/mlly": {
- "version": "1.8.2",
- "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz",
- "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "acorn": "^8.16.0",
- "pathe": "^2.0.3",
- "pkg-types": "^1.3.1",
- "ufo": "^1.6.3"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/mz": {
- "version": "2.7.0",
- "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz",
- "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "any-promise": "^1.0.0",
- "object-assign": "^4.0.1",
- "thenify-all": "^1.0.0"
- }
- },
- "node_modules/nan": {
- "version": "2.27.0",
- "resolved": "https://registry.npmmirror.com/nan/-/nan-2.27.0.tgz",
- "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/nanoid": {
- "version": "3.3.15",
- "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz",
- "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/node-domexception": {
- "version": "1.0.0",
- "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz",
- "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
- "deprecated": "Use your platform's native DOMException instead",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "github",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=10.5.0"
- }
- },
- "node_modules/node-fetch": {
- "version": "2.7.0",
- "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
- "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
- "license": "MIT",
- "dependencies": {
- "whatwg-url": "^5.0.0"
- },
- "engines": {
- "node": "4.x || >=6.0.0"
- },
- "peerDependencies": {
- "encoding": "^0.1.0"
- },
- "peerDependenciesMeta": {
- "encoding": {
- "optional": true
- }
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz",
- "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
- "license": "MIT",
- "dependencies": {
- "mimic-fn": "^2.1.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/openai": {
- "version": "4.104.0",
- "resolved": "https://registry.npmmirror.com/openai/-/openai-4.104.0.tgz",
- "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@types/node": "^18.11.18",
- "@types/node-fetch": "^2.6.4",
- "abort-controller": "^3.0.0",
- "agentkeepalive": "^4.2.1",
- "form-data-encoder": "1.7.2",
- "formdata-node": "^4.3.2",
- "node-fetch": "^2.6.7"
- },
- "bin": {
- "openai": "bin/cli"
- },
- "peerDependencies": {
- "ws": "^8.18.0",
- "zod": "^3.23.8"
- },
- "peerDependenciesMeta": {
- "ws": {
- "optional": true
- },
- "zod": {
- "optional": true
- }
- }
- },
- "node_modules/openai/node_modules/@types/node": {
- "version": "18.19.130",
- "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz",
- "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~5.26.4"
- }
- },
- "node_modules/openai/node_modules/undici-types": {
- "version": "5.26.5",
- "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
- "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
- "license": "MIT"
- },
- "node_modules/patch-console": {
- "version": "2.0.0",
- "resolved": "https://registry.npmmirror.com/patch-console/-/patch-console-2.0.0.tgz",
- "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==",
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- }
- },
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/pathval": {
- "version": "2.0.1",
- "resolved": "https://registry.npmmirror.com/pathval/-/pathval-2.0.1.tgz",
- "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.16"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/pirates": {
- "version": "4.0.7",
- "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz",
- "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/pkg-types": {
- "version": "1.3.1",
- "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz",
- "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "confbox": "^0.1.8",
- "mlly": "^1.7.4",
- "pathe": "^2.0.1"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.15",
- "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz",
- "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.12",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/postcss-load-config": {
- "version": "6.0.1",
- "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
- "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "lilconfig": "^3.1.1"
- },
- "engines": {
- "node": ">= 18"
- },
- "peerDependencies": {
- "jiti": ">=1.21.0",
- "postcss": ">=8.0.9",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- },
- "postcss": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/react": {
- "version": "18.3.1",
- "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.1.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-reconciler": {
- "version": "0.29.2",
- "resolved": "https://registry.npmmirror.com/react-reconciler/-/react-reconciler-0.29.2.tgz",
- "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "peerDependencies": {
- "react": "^18.3.1"
- }
- },
- "node_modules/readdirp": {
- "version": "4.1.2",
- "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
- "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.18.0"
- },
- "funding": {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/resolve-from": {
- "version": "5.0.0",
- "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
- "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/restore-cursor": {
- "version": "4.0.0",
- "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-4.0.0.tgz",
- "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
- "license": "MIT",
- "dependencies": {
- "onetime": "^5.1.0",
- "signal-exit": "^3.0.2"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/rollup": {
- "version": "4.62.2",
- "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz",
- "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.9"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.62.2",
- "@rollup/rollup-android-arm64": "4.62.2",
- "@rollup/rollup-darwin-arm64": "4.62.2",
- "@rollup/rollup-darwin-x64": "4.62.2",
- "@rollup/rollup-freebsd-arm64": "4.62.2",
- "@rollup/rollup-freebsd-x64": "4.62.2",
- "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
- "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
- "@rollup/rollup-linux-arm64-gnu": "4.62.2",
- "@rollup/rollup-linux-arm64-musl": "4.62.2",
- "@rollup/rollup-linux-loong64-gnu": "4.62.2",
- "@rollup/rollup-linux-loong64-musl": "4.62.2",
- "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
- "@rollup/rollup-linux-ppc64-musl": "4.62.2",
- "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
- "@rollup/rollup-linux-riscv64-musl": "4.62.2",
- "@rollup/rollup-linux-s390x-gnu": "4.62.2",
- "@rollup/rollup-linux-x64-gnu": "4.62.2",
- "@rollup/rollup-linux-x64-musl": "4.62.2",
- "@rollup/rollup-openbsd-x64": "4.62.2",
- "@rollup/rollup-openharmony-arm64": "4.62.2",
- "@rollup/rollup-win32-arm64-msvc": "4.62.2",
- "@rollup/rollup-win32-ia32-msvc": "4.62.2",
- "@rollup/rollup-win32-x64-gnu": "4.62.2",
- "@rollup/rollup-win32-x64-msvc": "4.62.2",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "license": "MIT"
- },
- "node_modules/scheduler": {
- "version": "0.23.2",
- "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.1.0"
- }
- },
- "node_modules/siginfo": {
- "version": "2.0.0",
- "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz",
- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/signal-exit": {
- "version": "3.0.7",
- "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
- "license": "ISC"
- },
- "node_modules/slice-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz",
- "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.1",
- "is-fullwidth-code-point": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/slice-ansi?sponsor=1"
- }
- },
- "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
- "version": "5.1.0",
- "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
- "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.3.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/source-map": {
- "version": "0.7.6",
- "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz",
- "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/ssh2": {
- "version": "1.17.0",
- "resolved": "https://registry.npmmirror.com/ssh2/-/ssh2-1.17.0.tgz",
- "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
- "hasInstallScript": true,
- "dependencies": {
- "asn1": "^0.2.6",
- "bcrypt-pbkdf": "^1.0.2"
- },
- "engines": {
- "node": ">=10.16.0"
- },
- "optionalDependencies": {
- "cpu-features": "~0.0.10",
- "nan": "^2.23.0"
- }
- },
- "node_modules/stack-utils": {
- "version": "2.0.6",
- "resolved": "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz",
- "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
- "license": "MIT",
- "dependencies": {
- "escape-string-regexp": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/stackback": {
- "version": "0.0.2",
- "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz",
- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/std-env": {
- "version": "3.10.0",
- "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz",
- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/string-width": {
- "version": "7.2.0",
- "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
- "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^10.3.0",
- "get-east-asian-width": "^1.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.2.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/sucrase": {
- "version": "3.35.1",
- "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz",
- "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.2",
- "commander": "^4.0.0",
- "lines-and-columns": "^1.1.6",
- "mz": "^2.7.0",
- "pirates": "^4.0.1",
- "tinyglobby": "^0.2.11",
- "ts-interface-checker": "^0.1.9"
- },
- "bin": {
- "sucrase": "bin/sucrase",
- "sucrase-node": "bin/sucrase-node"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/thenify": {
- "version": "3.3.1",
- "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
- "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "any-promise": "^1.0.0"
- }
- },
- "node_modules/thenify-all": {
- "version": "1.6.0",
- "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz",
- "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "thenify": ">= 3.1.0 < 4"
- },
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/tinybench": {
- "version": "2.9.0",
- "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz",
- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyexec": {
- "version": "0.3.2",
- "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz",
- "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyglobby": {
- "version": "0.2.17",
- "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz",
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tinypool": {
- "version": "1.1.1",
- "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz",
- "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- }
- },
- "node_modules/tinyrainbow": {
- "version": "1.2.0",
- "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
- "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tinyspy": {
- "version": "3.0.2",
- "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-3.0.2.tgz",
- "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "license": "MIT"
- },
- "node_modules/tree-kill": {
- "version": "1.2.2",
- "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz",
- "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "tree-kill": "cli.js"
- }
- },
- "node_modules/ts-interface-checker": {
- "version": "0.1.13",
- "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
- "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/tsup": {
- "version": "8.5.1",
- "resolved": "https://registry.npmmirror.com/tsup/-/tsup-8.5.1.tgz",
- "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "bundle-require": "^5.1.0",
- "cac": "^6.7.14",
- "chokidar": "^4.0.3",
- "consola": "^3.4.0",
- "debug": "^4.4.0",
- "esbuild": "^0.27.0",
- "fix-dts-default-cjs-exports": "^1.0.0",
- "joycon": "^3.1.1",
- "picocolors": "^1.1.1",
- "postcss-load-config": "^6.0.1",
- "resolve-from": "^5.0.0",
- "rollup": "^4.34.8",
- "source-map": "^0.7.6",
- "sucrase": "^3.35.0",
- "tinyexec": "^0.3.2",
- "tinyglobby": "^0.2.11",
- "tree-kill": "^1.2.2"
- },
- "bin": {
- "tsup": "dist/cli-default.js",
- "tsup-node": "dist/cli-node.js"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@microsoft/api-extractor": "^7.36.0",
- "@swc/core": "^1",
- "postcss": "^8.4.12",
- "typescript": ">=4.5.0"
- },
- "peerDependenciesMeta": {
- "@microsoft/api-extractor": {
- "optional": true
- },
- "@swc/core": {
- "optional": true
- },
- "postcss": {
- "optional": true
- },
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/tsx": {
- "version": "4.22.4",
- "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.4.tgz",
- "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "~0.28.0"
- },
- "bin": {
- "tsx": "dist/cli.mjs"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
- "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/android-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
- "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/android-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
- "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/android-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
- "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
- "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
- "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
- "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
- "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
- "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
- "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
- "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
- "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
- "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
- "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
- "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
- "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/linux-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
- "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
- "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
- "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
- "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
- "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
- "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
- "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/@esbuild/win32-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
- "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tsx/node_modules/esbuild": {
- "version": "0.28.1",
- "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.1.tgz",
- "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.1",
- "@esbuild/android-arm": "0.28.1",
- "@esbuild/android-arm64": "0.28.1",
- "@esbuild/android-x64": "0.28.1",
- "@esbuild/darwin-arm64": "0.28.1",
- "@esbuild/darwin-x64": "0.28.1",
- "@esbuild/freebsd-arm64": "0.28.1",
- "@esbuild/freebsd-x64": "0.28.1",
- "@esbuild/linux-arm": "0.28.1",
- "@esbuild/linux-arm64": "0.28.1",
- "@esbuild/linux-ia32": "0.28.1",
- "@esbuild/linux-loong64": "0.28.1",
- "@esbuild/linux-mips64el": "0.28.1",
- "@esbuild/linux-ppc64": "0.28.1",
- "@esbuild/linux-riscv64": "0.28.1",
- "@esbuild/linux-s390x": "0.28.1",
- "@esbuild/linux-x64": "0.28.1",
- "@esbuild/netbsd-arm64": "0.28.1",
- "@esbuild/netbsd-x64": "0.28.1",
- "@esbuild/openbsd-arm64": "0.28.1",
- "@esbuild/openbsd-x64": "0.28.1",
- "@esbuild/openharmony-arm64": "0.28.1",
- "@esbuild/sunos-x64": "0.28.1",
- "@esbuild/win32-arm64": "0.28.1",
- "@esbuild/win32-ia32": "0.28.1",
- "@esbuild/win32-x64": "0.28.1"
- }
- },
- "node_modules/tweetnacl": {
- "version": "0.14.5",
- "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz",
- "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
- "license": "Unlicense"
- },
- "node_modules/type-fest": {
- "version": "4.41.0",
- "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz",
- "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
- "license": "(MIT OR CC0-1.0)",
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/ufo": {
- "version": "1.6.4",
- "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz",
- "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/undici-types": {
- "version": "6.21.0",
- "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "license": "MIT"
- },
- "node_modules/vite": {
- "version": "5.4.21",
- "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
- "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.4.0"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- }
- }
- },
- "node_modules/vite-node": {
- "version": "2.1.9",
- "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-2.1.9.tgz",
- "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cac": "^6.7.14",
- "debug": "^4.3.7",
- "es-module-lexer": "^1.5.4",
- "pathe": "^1.1.2",
- "vite": "^5.0.0"
- },
- "bin": {
- "vite-node": "vite-node.mjs"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/vite-node/node_modules/pathe": {
- "version": "1.1.2",
- "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz",
- "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vite/node_modules/esbuild": {
- "version": "0.21.5",
- "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=12"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
- }
- },
- "node_modules/vitest": {
- "version": "2.1.9",
- "resolved": "https://registry.npmmirror.com/vitest/-/vitest-2.1.9.tgz",
- "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/expect": "2.1.9",
- "@vitest/mocker": "2.1.9",
- "@vitest/pretty-format": "^2.1.9",
- "@vitest/runner": "2.1.9",
- "@vitest/snapshot": "2.1.9",
- "@vitest/spy": "2.1.9",
- "@vitest/utils": "2.1.9",
- "chai": "^5.1.2",
- "debug": "^4.3.7",
- "expect-type": "^1.1.0",
- "magic-string": "^0.30.12",
- "pathe": "^1.1.2",
- "std-env": "^3.8.0",
- "tinybench": "^2.9.0",
- "tinyexec": "^0.3.1",
- "tinypool": "^1.0.1",
- "tinyrainbow": "^1.2.0",
- "vite": "^5.0.0",
- "vite-node": "2.1.9",
- "why-is-node-running": "^2.3.0"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@types/node": "^18.0.0 || >=20.0.0",
- "@vitest/browser": "2.1.9",
- "@vitest/ui": "2.1.9",
- "happy-dom": "*",
- "jsdom": "*"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- }
- }
- },
- "node_modules/vitest/node_modules/pathe": {
- "version": "1.1.2",
- "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz",
- "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/web-streams-polyfill": {
- "version": "4.0.0-beta.3",
- "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
- "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
- "license": "MIT",
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "license": "BSD-2-Clause"
- },
- "node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "license": "MIT",
- "dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
- "node_modules/why-is-node-running": {
- "version": "2.3.0",
- "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "siginfo": "^2.0.0",
- "stackback": "0.0.2"
- },
- "bin": {
- "why-is-node-running": "cli.js"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/widest-line": {
- "version": "5.0.0",
- "resolved": "https://registry.npmmirror.com/widest-line/-/widest-line-5.0.0.tgz",
- "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==",
- "license": "MIT",
- "dependencies": {
- "string-width": "^7.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/wrap-ansi": {
- "version": "9.0.2",
- "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
- "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.1",
- "string-width": "^7.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/ws": {
- "version": "8.21.0",
- "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.0.tgz",
- "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/yoga-layout": {
- "version": "3.2.1",
- "resolved": "https://registry.npmmirror.com/yoga-layout/-/yoga-layout-3.2.1.tgz",
- "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
- "license": "MIT"
- }
- }
-}
diff --git a/clients/cli/package.json b/clients/cli/package.json
deleted file mode 100644
index 0857d61..0000000
--- a/clients/cli/package.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "name": "lowenssh",
- "version": "0.1.0",
- "description": "LowenSSH —— 终端里的 AI SSH 运维 Agent(类 Claude Code 的 CLI)",
- "type": "module",
- "bin": {
- "lowenssh": "dist/cli.js"
- },
- "scripts": {
- "dev": "tsx src/cli.tsx",
- "build": "tsup",
- "test": "vitest run",
- "test:watch": "vitest",
- "typecheck": "tsc --noEmit"
- },
- "engines": {
- "node": ">=20"
- },
- "dependencies": {
- "ink": "^5.1.0",
- "ink-text-input": "^6.0.0",
- "react": "^18.3.1",
- "ssh2": "^1.16.0",
- "openai": "^4.77.0"
- },
- "devDependencies": {
- "@types/node": "^22.10.0",
- "@types/react": "^18.3.1",
- "@types/ssh2": "^1.15.0",
- "tsup": "^8.3.5",
- "tsx": "^4.19.2",
- "typescript": "^5.7.2",
- "vitest": "^2.1.8"
- },
- "files": [
- "dist"
- ]
-}
diff --git a/clients/cli/src/cli.tsx b/clients/cli/src/cli.tsx
deleted file mode 100644
index 7eb6037..0000000
--- a/clients/cli/src/cli.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * LowenSSH CLI 入口。读配置 → 渲染 TUI。
- * apiKey 缺失时给出明确提示(环境变量 GLM_API_KEY 或写进配置文件),不静默失败。
- */
-import { render, Box, Text } from 'ink'
-import { loadConfig, CONFIG_FILE } from './core/config.js'
-import { App } from './ui/App.js'
-import { AddHost } from './ui/AddHost.js'
-
-const command = process.argv[2]
-
-// 子命令:add-host —— 加主机不依赖 apiKey,单独路由
-if (command === 'add-host') {
- render()
-} else {
- const config = loadConfig()
-
- if (!config.llm.apiKey || config.llm.apiKey.trim() === '') {
- render(
-
- ✗ 缺少 GLM API Key
- 设置环境变量 GLM_API_KEY,或填进配置文件:
- {CONFIG_FILE}
- ,
- )
- } else {
- render()
- }
-}
diff --git a/clients/cli/src/core/agent.ts b/clients/cli/src/core/agent.ts
deleted file mode 100644
index c62ca2e..0000000
--- a/clients/cli/src/core/agent.ts
+++ /dev/null
@@ -1,290 +0,0 @@
-/**
- * Agent 核心 —— 手写的 agentic loop + 安全门禁。
- * 移植自 Java 版 AgentService,对外用 async generator 吐 6 类事件。
- *
- * 循环结构(Claude Code 同款状态机):
- * 请求 → 模型返回 → 有 tool_call?
- * 有 → 门禁预检 → 全放行才执行工具 → 结果回灌 → 带新历史再请求
- * 任一被拒 → 不执行,回灌"拒绝"作为工具结果 → loop 继续让模型换方案
- * 无 → 模型给出最终结论,循环结束
- * 加最大轮数上限防死循环。
- *
- * 安全是独立代码路径:门禁不写进工具、不靠模型自觉,越狱也绕不过。
- */
-import { evaluate } from './guard.js'
-import type { SshClient } from './ssh.js'
-import type { GlmClient, ChatMessage, ToolCall, ToolDef } from './glm.js'
-import { ContextManager } from './context.js'
-import type { AgentEvent, Confirmer } from './events.js'
-
-const MAX_ROUNDS = 40
-const EXEC_TOOL = 'execCommand'
-
-const SYSTEM_PROMPT = `## 身份
-你是 LowenSSH,一个面向 Linux 服务器的 SSH/SFTP 智能体。
-你帮用户远程排查问题、执行命令、读取文件、查看日志,并在用户授权下完成文件传输等运维操作。
-你的能力随工具集扩展——当前可用的工具见工具列表,只调用列表里实际存在的工具,不要臆造工具。
-
-## 环境与安全
-你执行的每条命令都会经过一道独立的安全门禁,危险命令会被拦截。
-被拦时换一个更安全的方式达成目标,不要重复同一条被拒命令,也不要改用等价的危险命令绕过拦截。
-
-## 工作方式
-- 先理解任务目标再决定查什么,每步拿到结果后判断下一步,不要一次堆一堆命令。
-- 优先用只读命令探查(df / free / ps / cat / tail),看清现状再动有副作用的操作。
-- 命令输出可能被截断(节省 token),抓关键信息即可,需要时再精确查询。
-
-## 输出格式
-- 用中文,给出结论,不要只罗列原始命令输出。
-- 简单结果用自然语言简短回答,多维度信息才用列表或表格。
-- 关键数字(磁盘占用 %、内存、负载等)直接点出来,别让用户自己从输出里找。`
-
-/** 工具集 schema —— 与 Java 版 SshTools 的 @Tool 对齐 */
-const TOOLS: ToolDef[] = [
- {
- type: 'function',
- function: {
- name: 'execCommand',
- description:
- '在目标服务器上执行一条 shell 命令,返回标准输出、错误输出和退出码。用于查看系统状态、进程、磁盘等运维操作。',
- parameters: {
- type: 'object',
- properties: {
- command: { type: 'string', description: "要执行的 shell 命令,例如 'df -h'" },
- },
- required: ['command'],
- },
- },
- },
- {
- type: 'function',
- function: {
- name: 'readRemoteFile',
- description: '读取目标服务器上指定路径的文本文件的完整内容。',
- parameters: {
- type: 'object',
- properties: {
- path: { type: 'string', description: "远程文件绝对路径,例如 '/etc/nginx/nginx.conf'" },
- },
- required: ['path'],
- },
- },
- },
- {
- type: 'function',
- function: {
- name: 'tailLog',
- description: '读取目标服务器上日志文件的末尾若干行,用于快速查看最新日志。',
- parameters: {
- type: 'object',
- properties: {
- path: { type: 'string', description: '日志文件绝对路径' },
- lines: { type: 'number', description: '读取末尾的行数,例如 100' },
- },
- required: ['path', 'lines'],
- },
- },
- },
- {
- type: 'function',
- function: {
- name: 'listFiles',
- description: '列出目标服务器上指定目录的文件和子目录。',
- parameters: {
- type: 'object',
- properties: {
- path: { type: 'string', description: "目录绝对路径,例如 '/var/log'" },
- },
- required: ['path'],
- },
- },
- },
-]
-
-/** 执行一个工具调用,返回喂回模型的文本结果。只读工具直接跑;execCommand 已过门禁。 */
-async function runTool(ssh: SshClient, name: string, args: Record): Promise {
- try {
- switch (name) {
- case 'execCommand': {
- const r = await ssh.exec(String(args.command ?? ''))
- return formatExec(r)
- }
- case 'readRemoteFile': {
- const r = await ssh.exec(`cat '${args.path}'`)
- return formatExec(r)
- }
- case 'tailLog': {
- const r = await ssh.exec(`tail -n ${Number(args.lines) || 100} '${args.path}'`)
- return formatExec(r)
- }
- case 'listFiles': {
- const files = await ssh.listDir(String(args.path ?? ''))
- if (files.length === 0) return '(空目录)' + args.path
- const lines = files.map(
- (f) => `${f.isDir ? '[d]' : '[f]'} ${f.name} ${f.isDir ? '-' : f.size + 'B'} ${f.perms}`,
- )
- return `目录 ${args.path} 共 ${files.length} 项:\n` + lines.join('\n')
- }
- default:
- return `未知工具: ${name}`
- }
- } catch (e) {
- // 工具内部异常不抛给 loop,作为"工具结果"回灌,让模型知道这步失败
- return `命令执行异常: ${(e as Error).message}`
- }
-}
-
-function formatExec(r: { stdout: string; stderr: string; exitCode: number }): string {
- let s = `exitCode=${r.exitCode}\n`
- if (r.stdout) s += `stdout:\n${r.stdout}`
- if (r.stderr) s += `stderr:\n${r.stderr}`
- return s
-}
-
-/** 从 tool_call 参数 JSON 取出 command 字段 */
-function extractCommand(argsJson: string): string {
- try {
- const obj = JSON.parse(argsJson) as { command?: string }
- return obj.command ?? ''
- } catch {
- return ''
- }
-}
-
-export interface AgentDeps {
- llm: GlmClient
- ssh: SshClient
- confirmer: Confirmer
- /** 历史消息(多轮续聊)。首轮传空数组。loop 结束后调用方可读回更新后的历史。 */
- history?: ChatMessage[]
-}
-
-/**
- * 跑一轮 agent 任务,以 async generator 吐事件流。
- * 调用方 for-await 消费事件;事件语义见 events.ts。
- */
-export async function* runAgent(task: string, deps: AgentDeps): AsyncGenerator {
- const { llm, ssh, confirmer } = deps
- const ctx = new ContextManager(llm)
-
- let messages: ChatMessage[] = [
- { role: 'system', content: SYSTEM_PROMPT },
- ...(deps.history ?? []),
- { role: 'user', content: task },
- ]
-
- for (let round = 1; round <= MAX_ROUNDS; round++) {
- // 进模型前整理上下文:Layer 0 截断 + Layer 4 压缩
- messages = ctx.truncateToolResponses(messages)
- messages = await ctx.compressIfNeeded(messages)
-
- // 一次流式调用:边推 token/reasoning 边聚合
- const pending: AgentEvent[] = []
- const result = await llm.stream(messages, TOOLS, {
- onToken: (t) => pending.push({ type: 'token', text: t }),
- onReasoning: (t) => pending.push({ type: 'reasoning', text: t }),
- })
- // 把流式期间攒的增量事件吐出去
- for (const ev of pending) yield ev
-
- // 没有 tool_call:模型给出最终结论,结束
- if (result.toolCalls.length === 0) {
- const text = result.text?.trim() || '模型暂时没有返回内容,请重试。'
- yield { type: 'done', finalText: text }
- return
- }
-
- // 落 assistant(文字 + tool_calls)
- const assistant: ChatMessage = {
- role: 'assistant',
- content: result.text || null,
- tool_calls: result.toolCalls,
- }
- // 先把本轮要调的工具吐出去
- for (const call of result.toolCalls) {
- yield { type: 'tool_call', name: call.function.name, args: call.function.arguments }
- }
-
- // —— 门禁预检 + 执行 ——
- const toolResponses: ChatMessage[] = []
- let anyRejected = false
-
- for (const call of result.toolCalls) {
- const reject = await screenAndRun(call, ssh, confirmer, (ev) => pending.push(ev))
- // screenAndRun 把 blocked/tool_result 事件塞进 pending
- toolResponses.push({ role: 'tool', tool_call_id: call.id, content: reject.content })
- if (reject.rejected) anyRejected = true
- }
- // 吐出执行阶段攒的事件(blocked / tool_result)
- for (const ev of pending) yield ev
- pending.length = 0
-
- // 回灌历史:assistant + 所有 tool 结果(被拒的也回灌"拒绝"文本,让模型换方案)
- messages.push(assistant, ...toolResponses)
- void anyRejected // 拒绝与否都已通过 tool 结果回灌,loop 自然继续
- }
-
- yield {
- type: 'done',
- finalText: `已达到最大循环轮数(${MAX_ROUNDS}),任务可能未完成。请拆分任务后重试。`,
- }
-}
-
-/**
- * 对单个 tool_call 过门禁并执行。
- * 返回 { content, rejected }:content 是回灌给模型的文本,rejected 表示被拒未执行。
- * 通过 emit 推 blocked / tool_result 事件。
- */
-async function screenAndRun(
- call: ToolCall,
- ssh: SshClient,
- confirmer: Confirmer,
- emit: (ev: AgentEvent) => void,
-): Promise<{ content: string; rejected: boolean }> {
- const name = call.function.name
- let args: Record = {}
- try {
- args = JSON.parse(call.function.arguments) as Record
- } catch {
- // 参数解析失败,交给工具自己处理(会报错回灌)
- }
-
- // 非 execCommand 的工具(读文件/看日志/列目录)只读,直接放行
- if (name !== EXEC_TOOL) {
- const content = await runTool(ssh, name, args)
- emit({ type: 'tool_result', name, summary: summarize(content), executed: true })
- return { content, rejected: false }
- }
-
- const command = extractCommand(call.function.arguments)
- const verdict = evaluate(command)
-
- if (verdict.decision === 'DENY') {
- const reason = verdict.reason
- emit({ type: 'blocked', command, reason })
- return {
- content: `命令被安全门禁拒绝执行(${reason})。请改用更安全的方式。`,
- rejected: true,
- }
- }
-
- if (verdict.decision === 'ASK') {
- const ok = await confirmer(command, verdict.reason)
- if (!ok) {
- emit({ type: 'blocked', command, reason: '用户拒绝: ' + verdict.reason })
- return { content: '用户拒绝执行该命令。请换一种方式或询问用户。', rejected: true }
- }
- }
-
- // ALLOW 或 ASK 已批准:执行
- const content = await runTool(ssh, name, args)
- emit({ type: 'tool_result', name, summary: summarize(content), executed: true })
- return { content, rejected: false }
-}
-
-/** 工具结果摘要:超 500 字符截断(仅用于事件展示,回灌给模型的是完整内容) */
-function summarize(data: string): string {
- return data.length > 500 ? data.slice(0, 500) + '…' : data
-}
-
-export { TOOLS, SYSTEM_PROMPT, MAX_ROUNDS }
diff --git a/clients/cli/src/core/config.ts b/clients/cli/src/core/config.ts
deleted file mode 100644
index 679e4cb..0000000
--- a/clients/cli/src/core/config.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-/**
- * 本地配置 —— 主机簿 + GLM 接入设置,存 ~/.lowenssh/config.json。
- *
- * 内置版(不依赖后端)的持久化层:替代 Java 版的 MySQL t_host。
- * 主机密码用 AES-GCM 加密后存 passwordEnc 字段,绝不存明文(复用 crypto.ts)。
- * 配置文件权限设为 600,只有属主可读写。
- */
-import { homedir } from 'node:os'
-import { join } from 'node:path'
-import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs'
-import { randomUUID } from 'node:crypto'
-import { encrypt, decrypt } from './crypto.js'
-
-/** 一台主机的连接信息 */
-export interface Host {
- id: string
- alias?: string
- host: string
- port: number
- user: string
- /** AES-GCM 加密后的密码;未存密码则为空 */
- passwordEnc?: string
-}
-
-/** GLM/OpenAI 兼容接入设置 */
-export interface LlmConfig {
- baseURL: string
- apiKey: string
- model: string
-}
-
-export interface AppConfig {
- hosts: Host[]
- llm: LlmConfig
-}
-
-const CONFIG_DIR = join(homedir(), '.lowenssh')
-const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
-
-/** 默认 LLM 设置:GLM。apiKey 留空,首次运行提示用户填或从环境变量读 */
-const DEFAULT_LLM: LlmConfig = {
- baseURL: 'https://open.bigmodel.cn/api/paas/v4',
- apiKey: '',
- model: 'glm-4.6',
-}
-
-function emptyConfig(): AppConfig {
- return { hosts: [], llm: { ...DEFAULT_LLM } }
-}
-
-/** 读配置;不存在则返回空配置。环境变量 GLM_API_KEY 优先覆盖文件里的 apiKey。 */
-export function loadConfig(): AppConfig {
- let cfg: AppConfig
- if (!existsSync(CONFIG_FILE)) {
- cfg = emptyConfig()
- } else {
- try {
- const raw = readFileSync(CONFIG_FILE, 'utf8')
- const parsed = JSON.parse(raw) as Partial
- cfg = {
- hosts: parsed.hosts ?? [],
- llm: { ...DEFAULT_LLM, ...parsed.llm },
- }
- } catch {
- // 配置损坏不影响启动,退回空配置(用户可重新添加)
- cfg = emptyConfig()
- }
- }
- // 环境变量优先:方便 CI / 临时覆盖,且不把 key 写进文件
- const envKey = process.env.GLM_API_KEY
- if (envKey && envKey.trim() !== '') {
- cfg.llm.apiKey = envKey
- }
- return cfg
-}
-
-/** 写配置(权限 600)。注意:不会把环境变量注入的 apiKey 持久化回文件。 */
-export function saveConfig(cfg: AppConfig): void {
- if (!existsSync(CONFIG_DIR)) {
- mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
- }
- writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 })
- try {
- chmodSync(CONFIG_FILE, 0o600)
- } catch {
- // Windows 不支持 chmod,忽略
- }
-}
-
-/** 新增主机:密码加密后落库,返回带 id 的 Host */
-export function addHost(input: Omit & { password?: string }): Host {
- const cfg = loadConfig()
- const host: Host = {
- id: randomUUID(),
- alias: input.alias,
- host: input.host,
- port: input.port || 22,
- user: input.user || 'root',
- passwordEnc: input.password ? encrypt(input.password) ?? undefined : undefined,
- }
- cfg.hosts.push(host)
- saveConfig(cfg)
- return host
-}
-
-/** 删除主机 */
-export function removeHost(id: string): void {
- const cfg = loadConfig()
- cfg.hosts = cfg.hosts.filter((h) => h.id !== id)
- saveConfig(cfg)
-}
-
-/** 取某主机的明文密码(解密);未存返回 null */
-export function getHostPassword(host: Host): string | null {
- if (!host.passwordEnc) return null
- return decrypt(host.passwordEnc)
-}
-
-/** 主机是否已存密码 */
-export function hasPassword(host: Host): boolean {
- return !!host.passwordEnc
-}
-
-export { CONFIG_FILE }
diff --git a/clients/cli/src/core/context.ts b/clients/cli/src/core/context.ts
deleted file mode 100644
index 66d2b86..0000000
--- a/clients/cli/src/core/context.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * 上下文管理 —— 防止 agentic loop 多轮滚下来把模型上下文撑爆。
- * 移植自 Java 版 ContextManager,只做两层(抄 Claude Code 思路):
- *
- * Layer 0 —— 大工具结果截断:单条工具结果超阈值截掉中段,留头尾 + 提示。
- * 分级:最近 K 条用大阈值保细节,更早的用小阈值大力收紧 →
- * token 不随轮数线性膨胀,且按"距末尾距离"判定,跨轮稳定不破坏缓存前缀。
- *
- * Layer 4 —— 历史压缩:整段估算 token 超阈值,把较早对话丢给 LLM 摘要成一条,
- * 保留 system + 最近 K 条原文。成对约束:保留区不能以孤儿 tool 消息开头。
- * 摘要连续失败到熔断阈值就停止压缩、裸跑兜底。
- *
- * token 用字符数粗估:中英混合约 2.5 字符/token,不引 tokenizer。
- */
-import type { ChatMessage } from './glm.js'
-import type { GlmClient } from './glm.js'
-
-const CHARS_PER_TOKEN = 2.5
-const TRUNCATE_MARKER = '完整结果见历史记录'
-
-export interface ContextOptions {
- toolResultMaxChars: number // Layer 0 近区阈值
- oldToolResultMaxChars: number // Layer 0 旧区阈值
- maxContextTokens: number // Layer 4 触发压缩阈值
- keepRecentMessages: number // Layer 4 保留最近条数
- circuitLimit: number // 摘要连续失败熔断次数
-}
-
-export const DEFAULT_CONTEXT_OPTIONS: ContextOptions = {
- toolResultMaxChars: 8000,
- oldToolResultMaxChars: 800,
- maxContextTokens: 32000,
- keepRecentMessages: 6,
- circuitLimit: 3,
-}
-
-const SUMMARY_PROMPT = `你是上下文压缩器。下面是一段 AI 运维助手与目标服务器之间的历史对话(含用户任务、助手发起的命令调用、命令执行结果)。
-请把它压缩成简洁的中文摘要,必须保留以下信息,丢弃冗长的原始命令输出(只留结论):
-1. 用户的原始运维目标;
-2. 已执行过的关键命令及其结果结论(例如磁盘占用多少、进程是否存活、配置是否正确);
-3. 已发现的问题或系统状态;
-4. 被安全门禁拦截的危险操作(如果有)。
-只输出摘要正文,不要解释你在做什么。`
-
-export class ContextManager {
- private opts: ContextOptions
- private llm: GlmClient
- private consecutiveFailures = 0
-
- constructor(llm: GlmClient, opts: ContextOptions = DEFAULT_CONTEXT_OPTIONS) {
- this.llm = llm
- this.opts = opts
- }
-
- // ===================== Layer 0:工具结果截断 =====================
-
- /**
- * 对历史里所有工具结果做截断(幂等)。返回新数组,不改原数组。
- * 分级:距末尾 keepRecentMessages 条内用大阈值,更早用小阈值。
- */
- truncateToolResponses(messages: ChatMessage[]): ChatMessage[] {
- const size = messages.length
- return messages.map((msg, i) => {
- if (msg.role !== 'tool') return msg
- const recent = size - i <= this.opts.keepRecentMessages
- const limit = recent ? this.opts.toolResultMaxChars : this.opts.oldToolResultMaxChars
- return { ...msg, content: this.truncateText(msg.content, limit) }
- })
- }
-
- /** 截掉中段,保留头 60% / 尾 40%,中间塞提示。幂等:含哨兵跳过。 */
- private truncateText(text: string, limit: number): string {
- if (!text || text.length <= limit) return text
- if (text.includes(TRUNCATE_MARKER)) return text
- const headLen = Math.floor(limit * 0.6)
- const tailLen = limit - headLen
- const cut = text.length - headLen - tailLen
- const head = text.slice(0, headLen)
- const tail = text.slice(text.length - tailLen)
- return `${head}\n...[已截断 ${cut} 字符,${TRUNCATE_MARKER}]...\n${tail}`
- }
-
- // ===================== Layer 4:历史压缩 =====================
-
- /** 估算超阈值时压缩历史,否则原样返回。 */
- async compressIfNeeded(messages: ChatMessage[]): Promise {
- if (this.consecutiveFailures >= this.opts.circuitLimit) return messages
- if (this.estimateTokens(messages) <= this.opts.maxContextTokens) return messages
- if (messages.length <= this.opts.keepRecentMessages + 1) return messages
-
- let cutIndex = messages.length - this.opts.keepRecentMessages
- // 保留区不能以孤儿 tool 消息开头(它的 tool_call 在 assistant 上,会被切走)
- while (cutIndex > 1 && messages[cutIndex]?.role === 'tool') {
- cutIndex--
- }
- if (cutIndex <= 1) return messages
-
- const summaryRegion = messages.slice(1, cutIndex)
- const summary = await this.summarize(summaryRegion)
- if (summary === null) {
- this.consecutiveFailures++
- return messages
- }
- this.consecutiveFailures = 0
-
- return [
- messages[0]!, // system
- { role: 'user', content: '以下是早先对话的摘要,供你继续任务时参考:\n' + summary },
- ...messages.slice(cutIndex),
- ]
- }
-
- /** 调摘要 LLM 把一段历史压成结论文本;失败返回 null */
- private async summarize(region: ChatMessage[]): Promise {
- try {
- const rendered = this.renderRegion(region)
- const text = await this.llm.complete([
- { role: 'system', content: SUMMARY_PROMPT },
- { role: 'user', content: rendered },
- ])
- return text && text.trim() !== '' ? text : null
- } catch {
- return null
- }
- }
-
- /** 把一段消息渲染成纯文本喂给摘要 LLM */
- private renderRegion(region: ChatMessage[]): string {
- const lines: string[] = []
- for (const msg of region) {
- if (msg.role === 'user') {
- lines.push('用户: ' + msg.content)
- } else if (msg.role === 'assistant') {
- if (msg.content) lines.push('助手: ' + msg.content)
- for (const call of msg.tool_calls ?? []) {
- lines.push(`助手调用工具 ${call.function.name}: ${call.function.arguments}`)
- }
- } else if (msg.role === 'tool') {
- lines.push(`工具结果: ${msg.content}`)
- }
- }
- return lines.join('\n')
- }
-
- // ===================== 工具方法 =====================
-
- /** 估算整段消息的 token 数(字符数粗估) */
- estimateTokens(messages: ChatMessage[]): number {
- let chars = 0
- for (const msg of messages) {
- if (msg.role === 'assistant') {
- chars += msg.content?.length ?? 0
- for (const call of msg.tool_calls ?? []) {
- chars += call.function.arguments?.length ?? 0
- }
- } else {
- chars += msg.content?.length ?? 0
- }
- }
- return Math.floor(chars / CHARS_PER_TOKEN)
- }
-}
diff --git a/clients/cli/src/core/crypto.test.ts b/clients/cli/src/core/crypto.test.ts
deleted file mode 100644
index 0fe2231..0000000
--- a/clients/cli/src/core/crypto.test.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { describe, it, expect } from 'vitest'
-import { encrypt, decrypt } from './crypto.js'
-
-describe('CryptoUtil AES-GCM', () => {
- it('加密后能解回原文', () => {
- const plain = 'my-secret-password-123'
- const enc = encrypt(plain)
- expect(enc).not.toBeNull()
- expect(enc).not.toBe(plain)
- expect(decrypt(enc)).toBe(plain)
- })
-
- it('同一明文每次密文不同(IV 随机)', () => {
- expect(encrypt('same')).not.toBe(encrypt('same'))
- })
-
- it('空值返回 null', () => {
- expect(encrypt('')).toBeNull()
- expect(encrypt(null)).toBeNull()
- expect(decrypt('')).toBeNull()
- expect(decrypt(null)).toBeNull()
- })
-
- it('中文密码往返正确', () => {
- const plain = '密码测试🔐'
- expect(decrypt(encrypt(plain))).toBe(plain)
- })
-
- it('密文被篡改解密抛错(GCM 完整性校验)', () => {
- const enc = encrypt('data')!
- const tampered = enc.slice(0, -4) + (enc.slice(-4) === 'AAAA' ? 'BBBB' : 'AAAA')
- expect(() => decrypt(tampered)).toThrow()
- })
-})
diff --git a/clients/cli/src/core/crypto.ts b/clients/cli/src/core/crypto.ts
deleted file mode 100644
index f90cd47..0000000
--- a/clients/cli/src/core/crypto.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * 密码加密工具 —— 主机密码落本地配置前用 AES-256-GCM 加密,绝不存明文。
- * 1:1 移植自 Java 版 CryptoUtil,密文格式互通。
- *
- * 密文格式:Base64( iv[12] + cipherText + authTag[16] )。
- * Node crypto 的 GCM 把 authTag 单独返回,这里手动拼到密文尾部,与 Java 版
- * (tag 内联在 doFinal 输出尾部)布局一致,两端密文可互相解密。
- *
- * 密钥来源:环境变量 XWSSH_CRYPTO_KEY;任意长度经 SHA-256 派生成 32 字节 AES-256 密钥。
- */
-import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
-
-const ALGO = 'aes-256-gcm'
-const IV_LEN = 12 // GCM 推荐 12 字节 IV
-const TAG_LEN = 16 // 认证标签 16 字节(128 位)
-const DEV_DEFAULT_KEY = 'xwssh-dev-default-key-change-me'
-
-/** 任意密钥串经 SHA-256 派生成固定 32 字节 */
-function deriveKey(raw: string): Buffer {
- return createHash('sha256').update(raw, 'utf8').digest()
-}
-
-function resolveKey(): Buffer {
- const raw = process.env.XWSSH_CRYPTO_KEY
- if (!raw || raw.trim() === '') {
- // 没配密钥退到开发默认值,仅保证能跑;生产务必设 XWSSH_CRYPTO_KEY
- return deriveKey(DEV_DEFAULT_KEY)
- }
- return deriveKey(raw)
-}
-
-/** 加密:明文 → Base64(iv + 密文 + tag)。空串返回 null。 */
-export function encrypt(plain: string | null): string | null {
- if (!plain) return null
- const key = resolveKey()
- const iv = randomBytes(IV_LEN)
- const cipher = createCipheriv(ALGO, key, iv, { authTagLength: TAG_LEN })
- const ct = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()])
- const tag = cipher.getAuthTag()
- // 布局与 Java 版对齐:iv + 密文 + tag
- return Buffer.concat([iv, ct, tag]).toString('base64')
-}
-
-/** 解密:Base64(iv + 密文 + tag) → 明文。空串返回 null。 */
-export function decrypt(enc: string | null): string | null {
- if (!enc) return null
- const key = resolveKey()
- const all = Buffer.from(enc, 'base64')
- const iv = all.subarray(0, IV_LEN)
- const tag = all.subarray(all.length - TAG_LEN)
- const ct = all.subarray(IV_LEN, all.length - TAG_LEN)
- const decipher = createDecipheriv(ALGO, key, iv, { authTagLength: TAG_LEN })
- decipher.setAuthTag(tag)
- return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8')
-}
diff --git a/clients/cli/src/core/events.ts b/clients/cli/src/core/events.ts
deleted file mode 100644
index 63d805c..0000000
--- a/clients/cli/src/core/events.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Agent 事件 —— 对外流式输出的带类型事件流。
- * 移植自 Java 版 AgentEvent(sealed interface → discriminated union)。
- *
- * 为什么用带类型事件而不是纯字符串流:运维 Agent 输出有多种语义——模型在说话、
- * 要调命令、命令出结果、命令被拦、任务完成。UI 据此区别渲染。语义色是记忆点核心,
- * 与 Web 版 / App 端必须对齐。
- */
-
-/** 模型增量输出的一段文本 token */
-export interface TokenEvent {
- type: 'token'
- text: string
-}
-
-/** 模型思考过程增量(GLM reasoning_content) */
-export interface ReasoningEvent {
- type: 'reasoning'
- text: string
-}
-
-/** 模型决定调用某个工具 */
-export interface ToolCallEvent {
- type: 'tool_call'
- name: string
- args: string
-}
-
-/** 工具执行结果(executed=false 表示被拦截/拒绝,未真正执行) */
-export interface ToolResultEvent {
- type: 'tool_result'
- name: string
- summary: string
- executed: boolean
-}
-
-/** 命令被安全门禁拦截 / 用户拒绝 */
-export interface BlockedEvent {
- type: 'blocked'
- command: string
- reason: string
-}
-
-/** 任务完成,带最终结论文本 */
-export interface DoneEvent {
- type: 'done'
- finalText: string
-}
-
-/** 出错 */
-export interface ErrorEvent {
- type: 'error'
- message: string
-}
-
-export type AgentEvent =
- | TokenEvent
- | ReasoningEvent
- | ToolCallEvent
- | ToolResultEvent
- | BlockedEvent
- | DoneEvent
- | ErrorEvent
-
-/** ASK 态命令的人工确认入口:返回 true 放行,false 拒绝 */
-export type Confirmer = (command: string, reason: string) => Promise
diff --git a/clients/cli/src/core/glm.ts b/clients/cli/src/core/glm.ts
deleted file mode 100644
index 53f2fbe..0000000
--- a/clients/cli/src/core/glm.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-/**
- * GLM 接入 —— OpenAI 兼容协议封装,支持 function calling + streaming + reasoning_content。
- * 替代 Java 版 Spring AI 的 OpenAiChatModel + ToolCallingManager。
- *
- * 模型无关设计:换模型只改 baseURL / model(GLM / 通义 / DeepSeek 都走 OpenAI 兼容协议)。
- * GLM 端点是 .../paas/v4/chat/completions,openai sdk 的 baseURL 指到 .../paas/v4 即可。
- */
-import OpenAI from 'openai'
-import type { LlmConfig } from './config.js'
-
-/** 对话消息(OpenAI chat 格式子集,够 agent loop 用) */
-export type ChatMessage =
- | { role: 'system'; content: string }
- | { role: 'user'; content: string }
- | { role: 'assistant'; content: string | null; tool_calls?: ToolCall[] }
- | { role: 'tool'; tool_call_id: string; content: string }
-
-/** 一次工具调用 */
-export interface ToolCall {
- id: string
- type: 'function'
- function: { name: string; arguments: string }
-}
-
-/** 工具定义(function schema) */
-export interface ToolDef {
- type: 'function'
- function: {
- name: string
- description: string
- parameters: Record
- }
-}
-
-/** 一次模型响应聚合结果 */
-export interface ChatResult {
- /** 模型正文文本 */
- text: string
- /** 本轮发起的工具调用(无则空数组) */
- toolCalls: ToolCall[]
- /** token 用量(用于缓存命中测量) */
- usage?: {
- promptTokens?: number
- completionTokens?: number
- totalTokens?: number
- cachedTokens?: number
- }
-}
-
-/** 流式回调:边收边推 */
-export interface StreamHandlers {
- onToken?: (text: string) => void
- onReasoning?: (text: string) => void
-}
-
-export class GlmClient {
- private client: OpenAI
- private model: string
-
- constructor(cfg: LlmConfig) {
- this.client = new OpenAI({ baseURL: cfg.baseURL, apiKey: cfg.apiKey })
- this.model = cfg.model
- }
-
- /**
- * 一次流式调用:透传 token / reasoning 增量,聚合成完整结果返回。
- * 关闭框架自动工具执行——工具调用聚合后交回上层 loop 过门禁再执行。
- */
- async stream(
- messages: ChatMessage[],
- tools: ToolDef[],
- handlers: StreamHandlers,
- ): Promise {
- const stream = await this.client.chat.completions.create({
- model: this.model,
- messages: messages as OpenAI.Chat.ChatCompletionMessageParam[],
- tools: tools.length > 0 ? (tools as OpenAI.Chat.ChatCompletionTool[]) : undefined,
- stream: true,
- stream_options: { include_usage: true },
- })
-
- let text = ''
- // tool_calls 在流式下分片到达,按 index 累积
- const toolAcc = new Map()
- let usage: ChatResult['usage']
-
- for await (const chunk of stream) {
- const choice = chunk.choices[0]
- if (choice) {
- const delta = choice.delta as {
- content?: string | null
- reasoning_content?: string | null
- tool_calls?: Array<{
- index: number
- id?: string
- function?: { name?: string; arguments?: string }
- }>
- }
-
- // GLM 思考阶段:reasoning_content 增量,单独推
- if (delta.reasoning_content) {
- handlers.onReasoning?.(delta.reasoning_content)
- }
- if (delta.content) {
- text += delta.content
- handlers.onToken?.(delta.content)
- }
- if (delta.tool_calls) {
- for (const tc of delta.tool_calls) {
- const cur = toolAcc.get(tc.index) ?? { id: '', name: '', args: '' }
- if (tc.id) cur.id = tc.id
- if (tc.function?.name) cur.name = tc.function.name
- if (tc.function?.arguments) cur.args += tc.function.arguments
- toolAcc.set(tc.index, cur)
- }
- }
- }
- // usage 通常在最后一个 chunk(include_usage)
- if (chunk.usage) {
- const u = chunk.usage as OpenAI.Completions.CompletionUsage & {
- prompt_tokens_details?: { cached_tokens?: number }
- }
- usage = {
- promptTokens: u.prompt_tokens,
- completionTokens: u.completion_tokens,
- totalTokens: u.total_tokens,
- cachedTokens: u.prompt_tokens_details?.cached_tokens ?? 0,
- }
- }
- }
-
- const toolCalls: ToolCall[] = [...toolAcc.entries()]
- .sort((a, b) => a[0] - b[0])
- .map(([, v]) => ({
- id: v.id,
- type: 'function' as const,
- function: { name: v.name, arguments: v.args },
- }))
-
- return { text, toolCalls, usage }
- }
-
- /** 非流式调用:用于上下文压缩的摘要请求(纯文本进出,不带工具) */
- async complete(messages: ChatMessage[]): Promise {
- const resp = await this.client.chat.completions.create({
- model: this.model,
- messages: messages as OpenAI.Chat.ChatCompletionMessageParam[],
- stream: false,
- })
- return resp.choices[0]?.message?.content ?? ''
- }
-}
diff --git a/clients/cli/src/core/guard.test.ts b/clients/cli/src/core/guard.test.ts
deleted file mode 100644
index 8d32951..0000000
--- a/clients/cli/src/core/guard.test.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { describe, it, expect } from 'vitest'
-import { evaluate } from './guard.js'
-
-describe('CommandGuard 门禁三态', () => {
- describe('DENY —— 毁灭性操作直接拒', () => {
- it.each([
- 'rm -rf /',
- 'rm -fr /var/data',
- 'rm -r -f /tmp',
- 'mkfs.ext4 /dev/sdb',
- 'dd if=/dev/zero of=/dev/sda',
- 'shutdown -h now',
- 'reboot',
- 'halt',
- 'echo x > /dev/sda',
- ':(){ :|:& };:',
- 'mv /important /dev/null',
- 'find /tmp -name "*.log" -delete',
- 'find / -name core -exec rm {} \\;',
- ])('拒绝: %s', (cmd) => {
- expect(evaluate(cmd).decision).toBe('DENY')
- })
- })
-
- describe('ASK —— 有副作用需确认', () => {
- it.each([
- 'rm /tmp/old.log',
- 'kill 1234',
- 'systemctl stop nginx',
- 'systemctl restart docker',
- 'service mysql stop',
- 'chmod 777 /etc/passwd',
- 'chown root:root /opt',
- 'apt-get install vim',
- 'yum remove httpd',
- 'truncate -s 0 app.log',
- 'echo data > /etc/config',
- ])('询问: %s', (cmd) => {
- expect(evaluate(cmd).decision).toBe('ASK')
- })
- })
-
- describe('ALLOW —— 只读/安全命令放行', () => {
- it.each([
- 'df -h',
- 'free -m',
- 'ps aux | grep java',
- 'cat /etc/nginx/nginx.conf',
- 'tail -n 100 /var/log/syslog',
- 'ls -la',
- 'add-apt-repository ppa:x', // \b 不应误伤 add
- ])('放行: %s', (cmd) => {
- expect(evaluate(cmd).decision).toBe('ALLOW')
- })
- })
-
- describe('复合命令取最严', () => {
- it('ls && rm -rf / —— 整条 DENY', () => {
- expect(evaluate('ls && rm -rf /').decision).toBe('DENY')
- })
- it('df -h ; kill 1 —— 整条 ASK', () => {
- expect(evaluate('df -h ; kill 1').decision).toBe('ASK')
- })
- it('cat a | grep b —— 全只读 ALLOW', () => {
- expect(evaluate('cat a | grep b').decision).toBe('ALLOW')
- })
- })
-
- describe('边界', () => {
- it('空命令 ALLOW', () => {
- expect(evaluate('').decision).toBe('ALLOW')
- expect(evaluate(' ').decision).toBe('ALLOW')
- })
- })
-})
diff --git a/clients/cli/src/core/guard.ts b/clients/cli/src/core/guard.ts
deleted file mode 100644
index b6c5004..0000000
--- a/clients/cli/src/core/guard.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * 命令门禁 —— deny / ask / allow 三态判定。Agent 安全的硬边界。
- * 1:1 移植自 Java 版 CommandGuard,规则与语义保持一致(两端必须对齐)。
- *
- * 设计原则:
- * 1. 安全检查是独立代码路径,不写进工具方法、不靠模型自觉。模型越狱也绕不过。
- * 2. 三态顺序固定:先查 deny(命中即拒)→ 再看是否需 ask → 默认 allow。
- * 3. 只看实际要执行的命令,不看模型话术。
- * 4. 复合命令(&& | ; 串起来)拆段逐查,取最严结果。
- */
-
-export type Decision = 'DENY' | 'ASK' | 'ALLOW'
-
-export interface Verdict {
- decision: Decision
- reason: string
-}
-
-/**
- * deny 名单:不可逆的毁灭性操作,直接拒绝。
- * 用 \b 保证匹配独立命令词而非子串(dd 不误伤 add)。
- */
-const DENY: RegExp[] = [
- /\brm\s+(-\w*\s+)*-\w*[rf]/, // rm -rf / rm -fr 等带 r/f 组合
- /\bmkfs\b/, // 格式化文件系统
- /\bdd\b/, // 块设备读写,易毁盘
- /\bshutdown\b/, // 关机
- /\breboot\b/, // 重启
- /\bhalt\b/, // 停机
- />\s*\/dev\/sd/, // 直接写裸盘
- /:\(\)\s*\{.*\}/, // fork 炸弹 :(){ :|:& };:
- /\bmv\s+.*\s+\/dev\/null/, // mv 到 /dev/null 销毁数据
- /\bfind\b.*-delete/, // find ... -delete 批量删除(rm -rf 等价绕过)
- /\bfind\b.*-exec\s+rm/, // find ... -exec rm 批量删除
-]
-
-/**
- * ask 名单:有副作用但未必致命,执行前问一句。
- */
-const ASK: RegExp[] = [
- /\brm\b/, // 普通 rm(非 -rf)
- /\bkill\b/, // 杀进程
- /\bsystemctl\s+(stop|restart|disable)/, // 停/重启/禁用服务
- /\bservice\s+\S+\s+(stop|restart)/,
- /\b(chmod|chown)\b/, // 改权限/属主
- /\b(apt|apt-get|yum|dnf)\s+(install|remove|purge)/, // 装/卸软件
- /\btruncate\b/, // 清空文件
- />\s*\//, // 重定向覆盖写到绝对路径文件
-]
-
-/** 三态严重程度排序,越小越严:DENY < ASK < ALLOW */
-const ORDINAL: Record = { DENY: 0, ASK: 1, ALLOW: 2 }
-
-/**
- * 判定一条命令。复合命令会被拆段,取最严结果(任一段 deny 则整条 deny)。
- */
-export function evaluate(command: string): Verdict {
- if (!command || command.trim() === '') {
- return { decision: 'ALLOW', reason: '空命令' }
- }
-
- // 先对完整命令整体过一遍 DENY:fork 炸弹 :(){ :|:& };: 本身含 | 和 ;,
- // 拆段会把它切碎导致漏判。DENY 命中即拒,整体多查一次只会更安全。
- // (相对 Java 版的增强,Java 版 splitSegments 同样会漏 fork 炸弹,待同步修复。)
- for (const p of DENY) {
- const m = command.match(p)
- if (m) {
- return { decision: 'DENY', reason: `命中危险命令拦截规则: '${m[0]}'` }
- }
- }
-
- let worst: Decision = 'ALLOW'
- let worstReason = ''
-
- for (const seg of splitSegments(command)) {
- const s = seg.trim()
- if (s === '') continue
-
- const v = evaluateSingle(s)
- if (ORDINAL[v.decision] < ORDINAL[worst]) {
- worst = v.decision
- worstReason = v.reason
- }
- if (worst === 'DENY') break // 已最严,提前结束
- }
-
- if (worst === 'ALLOW') {
- return { decision: 'ALLOW', reason: '只读/安全命令' }
- }
- return { decision: worst, reason: worstReason }
-}
-
-/** 单段命令判定:先 deny 再 ask 后 allow */
-function evaluateSingle(seg: string): Verdict {
- for (const p of DENY) {
- const m = seg.match(p)
- if (m) {
- return { decision: 'DENY', reason: `命中危险命令拦截规则: '${m[0]}'` }
- }
- }
- for (const p of ASK) {
- const m = seg.match(p)
- if (m) {
- return { decision: 'ASK', reason: `涉及有副作用的操作: '${m[0]}'` }
- }
- }
- return { decision: 'ALLOW', reason: '' }
-}
-
-/** 按命令分隔符拆段:&& || | ; 换行,分隔符本身丢弃 */
-function splitSegments(command: string): string[] {
- return command.split(/&&|\|\||[|;\n]/)
-}
diff --git a/clients/cli/src/core/ssh.ts b/clients/cli/src/core/ssh.ts
deleted file mode 100644
index 65adb23..0000000
--- a/clients/cli/src/core/ssh.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-/**
- * SSH 客户端 —— 一个实例持有一个长连接,多条命令复用同一会话。
- * 移植自 Java 版 SshClient(JSch → ssh2)。
- *
- * 为什么长连接复用:agentic loop 里连续执行多条命令,每次重连既慢又丢上下文。
- * 非线程安全:一个实例对应一台机器一个会话,由上层串行使用。
- */
-import { Client, type ConnectConfig, type SFTPWrapper } from 'ssh2'
-
-/** 命令执行结果三件套 */
-export interface ExecResult {
- stdout: string
- stderr: string
- exitCode: number
-}
-
-/** 远程文件项 */
-export interface RemoteFile {
- name: string
- path: string
- size: number
- isDir: boolean
- perms: string
-}
-
-export class SshClient {
- private conn: Client | null = null
- private connected = false
-
- /** 建立连接(密码认证)。10s 超时。 */
- connect(host: string, port: number, username: string, password: string): Promise {
- return new Promise((resolve, reject) => {
- const conn = new Client()
- const cfg: ConnectConfig = {
- host,
- port: port || 22,
- username,
- password,
- readyTimeout: 10_000,
- // demo 方便跳过 host key 校验;生产应校验 known_hosts,否则有中间人风险
- }
- conn
- .on('ready', () => {
- this.conn = conn
- this.connected = true
- resolve()
- })
- .on('error', (err) => {
- this.connected = false
- reject(err)
- })
- .on('close', () => {
- this.connected = false
- })
- .connect(cfg)
- })
- }
-
- isConnected(): boolean {
- return this.connected && this.conn !== null
- }
-
- /**
- * 执行一条命令,收集 stdout、stderr、exitCode。
- * ssh2 的 exec 回调给一个 stream,stdout 走 data 事件,stderr 走 stream.stderr,
- * exitCode 在 close 事件回调里拿。
- */
- exec(command: string): Promise {
- return new Promise((resolve, reject) => {
- if (!this.conn || !this.connected) {
- reject(new Error('SSH 未连接,先调用 connect()'))
- return
- }
- this.conn.exec(command, (err, stream) => {
- if (err) {
- reject(err)
- return
- }
- let stdout = ''
- let stderr = ''
- stream
- .on('close', (code: number | null) => {
- resolve({ stdout, stderr, exitCode: code ?? 0 })
- })
- .on('data', (data: Buffer) => {
- stdout += data.toString('utf8')
- })
- stream.stderr.on('data', (data: Buffer) => {
- stderr += data.toString('utf8')
- })
- })
- })
- }
-
- /** 懒开 SFTP 通道 */
- private sftp(): Promise {
- return new Promise((resolve, reject) => {
- if (!this.conn || !this.connected) {
- reject(new Error('SSH 未连接'))
- return
- }
- this.conn.sftp((err, sftp) => {
- if (err) reject(err)
- else resolve(sftp)
- })
- })
- }
-
- /** 列目录。过滤 . 和 ..,目录在前、名称升序。 */
- async listDir(path: string): Promise {
- const sftp = await this.sftp()
- const base = path.endsWith('/') ? path : path + '/'
- const entries = await new Promise((resolve, reject) => {
- sftp.readdir(path, (err, list) => {
- if (err) {
- reject(err)
- return
- }
- const files: RemoteFile[] = list
- .filter((e) => e.filename !== '.' && e.filename !== '..')
- .map((e) => ({
- name: e.filename,
- path: base + e.filename,
- size: e.attrs.size,
- isDir: e.attrs.isDirectory(),
- perms: e.longname.split(/\s+/)[0] ?? '', // longname 首列形如 drwxr-xr-x
- }))
- resolve(files)
- })
- })
- entries.sort((a, b) => {
- if (a.isDir !== b.isDir) return a.isDir ? -1 : 1
- return a.name.toLowerCase().localeCompare(b.name.toLowerCase())
- })
- return entries
- }
-
- /** 删除文件 */
- async deleteFile(path: string): Promise {
- const sftp = await this.sftp()
- await new Promise((resolve, reject) => {
- sftp.unlink(path, (err) => (err ? reject(err) : resolve()))
- })
- }
-
- /** 新建目录 */
- async mkdir(path: string): Promise {
- const sftp = await this.sftp()
- await new Promise((resolve, reject) => {
- sftp.mkdir(path, (err) => (err ? reject(err) : resolve()))
- })
- }
-
- /** 重命名/移动 */
- async rename(from: string, to: string): Promise {
- const sftp = await this.sftp()
- await new Promise((resolve, reject) => {
- sftp.rename(from, to, (err) => (err ? reject(err) : resolve()))
- })
- }
-
- /** 关闭连接 */
- close(): void {
- if (this.conn) {
- this.conn.end()
- this.conn = null
- }
- this.connected = false
- }
-}
diff --git a/clients/cli/src/ui/AddHost.tsx b/clients/cli/src/ui/AddHost.tsx
deleted file mode 100644
index 316ab0e..0000000
--- a/clients/cli/src/ui/AddHost.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-/**
- * 交互式添加主机表单(`lowenssh add-host`)。
- *
- * 逐字段填写,回车进下一项:alias → host → port → user → password。
- * - host 必填,空则停留当前字段
- * - port 默认 22,user 默认 root(直接回车用默认值)
- * - password 用 mask 隐藏输入;存库时由 config.addHost 走 AES-GCM 加密,绝不存明文
- * 全部填完保存到 ~/.lowenssh/config.json 并退出。
- */
-import { useState } from 'react'
-import { Box, Text, useApp } from 'ink'
-import TextInput from 'ink-text-input'
-import { addHost } from '../core/config.js'
-
-/** 表单字段定义:按 steps 顺序逐个填写 */
-interface FieldDef {
- key: 'alias' | 'host' | 'port' | 'user' | 'password'
- label: string
- placeholder: string
- mask?: boolean
- required?: boolean
-}
-
-const FIELDS: FieldDef[] = [
- { key: 'alias', label: '别名(可选)', placeholder: '如 生产-web01,可留空' },
- { key: 'host', label: '主机地址', placeholder: 'IP 或域名', required: true },
- { key: 'port', label: '端口', placeholder: '22' },
- { key: 'user', label: '用户名', placeholder: 'root' },
- { key: 'password', label: '密码(可选)', placeholder: '留空则连接时不带密码', mask: true },
-]
-
-export function AddHost() {
- const { exit } = useApp()
- const [step, setStep] = useState(0)
- const [value, setValue] = useState('')
- const [draft, setDraft] = useState>({})
- const [saved, setSaved] = useState(null)
-
- const onSubmit = () => {
- const field = FIELDS[step]!
- const v = value.trim()
- // 必填字段空则不放行
- if (field.required && v === '') return
-
- const nextDraft = { ...draft, [field.key]: v }
- setDraft(nextDraft)
- setValue('')
-
- if (step < FIELDS.length - 1) {
- setStep(step + 1)
- return
- }
-
- // 最后一项:保存
- const host = addHost({
- alias: nextDraft.alias || undefined,
- host: nextDraft.host!,
- port: nextDraft.port ? Number(nextDraft.port) : 22,
- user: nextDraft.user || 'root',
- password: nextDraft.password || undefined,
- })
- const label = host.alias ? `${host.alias} (${host.user}@${host.host}:${host.port})` : `${host.user}@${host.host}:${host.port}`
- setSaved(label)
- // 渲染成功提示后退出
- setTimeout(() => exit(), 50)
- }
-
- if (saved) {
- return (
-
- ✓ 已添加主机:{saved}
- 密码已加密存入 ~/.lowenssh/config.json,直接运行 lowenssh 即可连接。
-
- )
- }
-
- const field = FIELDS[step]!
- return (
-
- ▰ 添加主机(回车下一项,Ctrl+C 取消)
- {/* 已填字段回显 */}
-
- {FIELDS.slice(0, step).map((f) => (
-
- {f.label}:{f.mask ? maskValue(draft[f.key]) : draft[f.key] || '(默认)'}
-
- ))}
-
- {/* 当前字段输入 */}
- 0 ? 1 : 0}>
- {field.label}:
-
-
-
- )
-}
-
-/** 密码回显成等长星号,空值显示"(无)" */
-function maskValue(v?: string): string {
- if (!v) return '(无)'
- return '*'.repeat(v.length)
-}
diff --git a/clients/cli/src/ui/App.tsx b/clients/cli/src/ui/App.tsx
deleted file mode 100644
index f9b6024..0000000
--- a/clients/cli/src/ui/App.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * 顶层应用状态机。
- *
- * 三阶段:
- * select —— 选主机(或提示先去加主机)
- * connect —— 正在 SSH 连接
- * chat —— 对话流,跑 agent loop
- *
- * Confirmer 桥:agent loop 在 ASK 态需要用户 y/n,但 loop 是异步 generator,
- * 不能直接读键盘。这里用一个 pending Promise 把 loop 的"等确认"和 UI 的按键解耦——
- * loop 调 confirmer() 拿到 Promise 并挂起,UI 渲染确认框,用户按键 resolve 这个 Promise。
- */
-import { useState, useRef, useCallback } from 'react'
-import { Box, Text } from 'ink'
-import type { Host, AppConfig } from '../core/config.js'
-import { getHostPassword } from '../core/config.js'
-import { GlmClient } from '../core/glm.js'
-import { SshClient } from '../core/ssh.js'
-import type { Confirmer } from '../core/events.js'
-import { HostSelect } from './HostSelect.js'
-import { Chat } from './Chat.js'
-import { ConfirmPrompt, type PendingConfirm } from './ConfirmPrompt.js'
-
-type Stage = 'select' | 'connect' | 'chat' | 'fatal'
-
-export interface AppProps {
- config: AppConfig
-}
-
-export function App({ config }: AppProps) {
- const [stage, setStage] = useState('select')
- const [error, setError] = useState(null)
- const [host, setHost] = useState(null)
-
- // 连接产物:连上后才有 ssh / llm 实例
- const sshRef = useRef(null)
- const llmRef = useRef(null)
-
- // 当前挂起的确认请求(ASK 态);null 表示没有待确认
- const [pendingConfirm, setPendingConfirm] = useState(null)
-
- /** Confirmer:被 agent loop 调用,返回一个 Promise,UI 按键后 resolve */
- const confirmer = useCallback((command, reason) => {
- return new Promise((resolve) => {
- setPendingConfirm({ command, reason, resolve })
- })
- }, [])
-
- /** 用户在确认框按了 y/n */
- const onConfirm = useCallback((approved: boolean) => {
- setPendingConfirm((cur) => {
- cur?.resolve(approved)
- return null
- })
- }, [])
-
- /** 选定主机后建立连接 */
- const onPickHost = useCallback(
- async (picked: Host) => {
- setHost(picked)
- setStage('connect')
- try {
- const password = getHostPassword(picked)
- if (!password) {
- setError(`主机 ${picked.host} 未保存密码,请先在配置里补充。`)
- setStage('fatal')
- return
- }
- const ssh = new SshClient()
- await ssh.connect(picked.host, picked.port, picked.user, password)
- sshRef.current = ssh
- llmRef.current = new GlmClient(config.llm)
- setStage('chat')
- } catch (e) {
- setError(`连接失败: ${(e as Error).message}`)
- setStage('fatal')
- }
- },
- [config.llm],
- )
-
- if (stage === 'fatal') {
- return (
-
- ✗ {error}
- 按 Ctrl+C 退出。
-
- )
- }
-
- if (stage === 'select') {
- return
- }
-
- if (stage === 'connect') {
- return (
-
- ◇ 正在连接 {host?.host} …
-
- )
- }
-
- // chat
- return (
-
-
- {pendingConfirm && }
-
- )
-}
diff --git a/clients/cli/src/ui/Chat.tsx b/clients/cli/src/ui/Chat.tsx
deleted file mode 100644
index ea73cec..0000000
--- a/clients/cli/src/ui/Chat.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-/**
- * 对话主界面。输入任务 → 跑 agent loop → 流式渲染 6 类事件。
- *
- * 事件语义色(记忆点核心,与后端 SSE / App 端保持一致):
- * token 默认色,模型正文,逐字累积
- * reasoning 灰显,模型思考过程
- * tool_call 青色,要执行的工具(命令折叠成一行)
- * tool_result 暗灰,工具结果摘要
- * blocked 红色高亮,被门禁/用户拦截
- * done 正文收尾
- * error 红色,异常
- */
-import { useState, useCallback } from 'react'
-import { Box, Text } from 'ink'
-import TextInput from 'ink-text-input'
-import type { Host } from '../core/config.js'
-import type { SshClient } from '../core/ssh.js'
-import type { GlmClient, ChatMessage } from '../core/glm.js'
-import type { Confirmer } from '../core/events.js'
-import { runAgent } from '../core/agent.js'
-
-/** 屏幕上的一条消息块(按语义渲染) */
-interface Line {
- kind: 'user' | 'token' | 'reasoning' | 'tool_call' | 'tool_result' | 'blocked' | 'error'
- text: string
-}
-
-export interface ChatProps {
- host: Host
- ssh: SshClient
- llm: GlmClient
- confirmer: Confirmer
- /** ASK 确认框弹出时锁住输入,避免按键串进 TextInput */
- inputLocked: boolean
-}
-
-export function Chat({ host, ssh, llm, confirmer, inputLocked }: ChatProps) {
- const [lines, setLines] = useState([])
- const [input, setInput] = useState('')
- const [busy, setBusy] = useState(false)
- // 多轮续聊历史:loop 之间累积(system 由 agent 内部加,这里只存 user/assistant/tool)
- const [history, setHistory] = useState([])
-
- const push = useCallback((line: Line) => {
- setLines((prev) => [...prev, line])
- }, [])
-
- /** 把流式 token 累积到最后一条 token 行,避免每个字一行 */
- const appendToken = useCallback((kind: 'token' | 'reasoning', text: string) => {
- setLines((prev) => {
- const last = prev[prev.length - 1]
- if (last && last.kind === kind) {
- const copy = prev.slice(0, -1)
- copy.push({ kind, text: last.text + text })
- return copy
- }
- return [...prev, { kind, text }]
- })
- }, [])
-
- const onSubmit = useCallback(async () => {
- const task = input.trim()
- if (!task || busy) return
- setInput('')
- setBusy(true)
- push({ kind: 'user', text: task })
-
- try {
- for await (const ev of runAgent(task, { llm, ssh, confirmer, history })) {
- switch (ev.type) {
- case 'token':
- appendToken('token', ev.text)
- break
- case 'reasoning':
- appendToken('reasoning', ev.text)
- break
- case 'tool_call':
- push({ kind: 'tool_call', text: `${ev.name}(${shorten(ev.args)})` })
- break
- case 'tool_result':
- push({ kind: 'tool_result', text: oneLine(ev.summary) })
- break
- case 'blocked':
- push({ kind: 'blocked', text: `⛔ 已拦截: ${ev.command} —— ${ev.reason}` })
- break
- case 'done':
- // 最终结论作为一条 token 行收尾(若正文已流式输出过,done 文本可能与其重复,仍补一条确保完整)
- push({ kind: 'token', text: '\n' + ev.finalText })
- break
- case 'error':
- push({ kind: 'error', text: `✗ ${ev.message}` })
- break
- }
- }
- // 续聊:把本轮 user 追进历史(assistant/tool 由下一轮 agent 内部 messages 重建,
- // 这里保留 user 提问让模型有上下文)
- setHistory((h) => [...h, { role: 'user', content: task }])
- } catch (e) {
- push({ kind: 'error', text: `✗ ${(e as Error).message}` })
- } finally {
- setBusy(false)
- }
- }, [input, busy, llm, ssh, confirmer, history, push, appendToken])
-
- return (
-
- ▰ {host.alias ?? host.host}
-
- {lines.map((l, i) => (
-
- ))}
-
-
- {busy ? (
- ◇ 处理中…
- ) : (
- <>
- ❯
-
- >
- )}
-
-
- )
-}
-
-/** 单行渲染:按语义上色 */
-function LineView({ line }: { line: Line }) {
- switch (line.kind) {
- case 'user':
- return ❯ {line.text}
- case 'token':
- return {line.text}
- case 'reasoning':
- return {line.text}
- case 'tool_call':
- return ⚙ {line.text}
- case 'tool_result':
- return ↳ {line.text}
- case 'blocked':
- return {line.text}
- case 'error':
- return {line.text}
- }
-}
-
-/** 工具参数 JSON 折叠成短文本 */
-function shorten(argsJson: string): string {
- const s = argsJson.replace(/\s+/g, ' ')
- return s.length > 80 ? s.slice(0, 80) + '…' : s
-}
-
-/** 多行摘要压成一行展示 */
-function oneLine(text: string): string {
- const s = text.replace(/\n+/g, ' ┊ ')
- return s.length > 120 ? s.slice(0, 120) + '…' : s
-}
diff --git a/clients/cli/src/ui/ConfirmPrompt.tsx b/clients/cli/src/ui/ConfirmPrompt.tsx
deleted file mode 100644
index 5eb1808..0000000
--- a/clients/cli/src/ui/ConfirmPrompt.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * ASK 态确认框:危险但可控的命令(rm / kill / chmod 等)执行前弹出,等用户 y/n。
- * 命令文本完整展示,让用户看清要批准什么再决定。
- */
-import { Box, Text, useInput } from 'ink'
-
-/** 一个挂起的确认请求:loop 提供 command/reason 和待 resolve 的回调 */
-export interface PendingConfirm {
- command: string
- reason: string
- resolve: (approved: boolean) => void
-}
-
-export interface ConfirmPromptProps {
- pending: PendingConfirm
- onAnswer: (approved: boolean) => void
-}
-
-export function ConfirmPrompt({ pending, onAnswer }: ConfirmPromptProps) {
- useInput((input, key) => {
- if (input === 'y' || input === 'Y') {
- onAnswer(true)
- } else if (input === 'n' || input === 'N' || key.escape) {
- onAnswer(false)
- }
- })
-
- return (
-
- ⚠ 需要确认 —— {pending.reason}
-
- $
- {pending.command}
-
- 执行? [y] 批准 / [n] 拒绝
-
- )
-}
diff --git a/clients/cli/src/ui/HostSelect.tsx b/clients/cli/src/ui/HostSelect.tsx
deleted file mode 100644
index 01f25cf..0000000
--- a/clients/cli/src/ui/HostSelect.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * 主机选择界面。上下键移动光标,回车连接。
- * 没有主机时提示去配置文件加主机(内置版主机簿在 ~/.lowenssh/config.json)。
- */
-import { useState } from 'react'
-import { Box, Text, useInput } from 'ink'
-import type { Host } from '../core/config.js'
-import { CONFIG_FILE } from '../core/config.js'
-
-export interface HostSelectProps {
- hosts: Host[]
- onPick: (host: Host) => void
-}
-
-export function HostSelect({ hosts, onPick }: HostSelectProps) {
- const [cursor, setCursor] = useState(0)
-
- useInput((input, key) => {
- if (hosts.length === 0) return
- if (key.upArrow || input === 'k') {
- setCursor((c) => (c - 1 + hosts.length) % hosts.length)
- } else if (key.downArrow || input === 'j') {
- setCursor((c) => (c + 1) % hosts.length)
- } else if (key.return) {
- onPick(hosts[cursor]!)
- }
- })
-
- return (
-
- ▰ LowenSSH
- 选择要连接的主机(↑↓ 移动,回车连接,Ctrl+C 退出)
-
- {hosts.length === 0 ? (
-
- 还没有主机。
- 请编辑配置文件添加:{CONFIG_FILE}
-
- ) : (
- hosts.map((h, i) => {
- const active = i === cursor
- const label = h.alias ? `${h.alias} (${h.user}@${h.host}:${h.port})` : `${h.user}@${h.host}:${h.port}`
- return (
-
- {active ? '❯ ' : ' '}
- {label}
-
- )
- })
- )}
-
-
- )
-}
diff --git a/clients/cli/tsconfig.json b/clients/cli/tsconfig.json
deleted file mode 100644
index 5d122de..0000000
--- a/clients/cli/tsconfig.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2022",
- "module": "ESNext",
- "moduleResolution": "Bundler",
- "lib": ["ES2023"],
- "jsx": "react-jsx",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "resolveJsonModule": true,
- "declaration": false,
- "outDir": "dist",
- "rootDir": "src"
- },
- "include": ["src"]
-}
diff --git a/clients/cli/tsup.config.ts b/clients/cli/tsup.config.ts
deleted file mode 100644
index 1ad522c..0000000
--- a/clients/cli/tsup.config.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { defineConfig } from 'tsup'
-
-// 打包配置:把 TUI 入口和 bin 入口打成 ESM。
-// ssh2 含原生依赖,标记 external 不打进 bundle,由 node_modules 提供。
-export default defineConfig({
- entry: {
- cli: 'src/cli.tsx',
- },
- format: ['esm'],
- target: 'node20',
- platform: 'node',
- banner: { js: '#!/usr/bin/env node' },
- clean: true,
- external: ['ssh2', 'react', 'ink', 'openai'],
-})
diff --git a/clients/cli/vitest.config.ts b/clients/cli/vitest.config.ts
deleted file mode 100644
index d2d9690..0000000
--- a/clients/cli/vitest.config.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { defineConfig } from 'vitest/config'
-
-export default defineConfig({
- test: {
- include: ['src/**/*.test.ts'],
- environment: 'node',
- },
-})
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 0831a1d..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-# LowenSSH 本地一键启动
-# 用法:先 export MYSQL_PASSWORD 和 GLM_API_KEY,再 docker compose up
-services:
- mysql:
- image: mysql:8.4
- environment:
- # root 密码 + 自动建库 lowenssh
- MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
- MYSQL_DATABASE: lowenssh
- ports:
- - "3306:3306"
- volumes:
- # 容器首次启动时在 lowenssh 库执行建表脚本
- - ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro
- - mysql-data:/var/lib/mysql
- healthcheck:
- test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_PASSWORD}"]
- interval: 5s
- timeout: 3s
- retries: 10
-
- app:
- build: .
- ports:
- - "8081:8081"
- environment:
- # 指向 mysql 容器(覆盖默认 localhost)
- DB_HOST: mysql
- # 密钥从宿主环境透传,compose 文件里不含明文
- MYSQL_PASSWORD: ${MYSQL_PASSWORD}
- GLM_API_KEY: ${GLM_API_KEY}
- depends_on:
- mysql:
- # 等 MySQL 就绪再启动,避免连接失败
- condition: service_healthy
-
-volumes:
- mysql-data:
diff --git a/mvnw b/mvnw
deleted file mode 100755
index 5272759..0000000
--- a/mvnw
+++ /dev/null
@@ -1,332 +0,0 @@
-#!/bin/sh
-# ----------------------------------------------------------------------------
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-# ----------------------------------------------------------------------------
-
-# ----------------------------------------------------------------------------
-# Apache Maven Wrapper startup batch script, version @@project.version@@
-#
-# Required ENV vars:
-# ------------------
-# JAVA_HOME - location of a JDK home dir
-#
-# Optional ENV vars
-# -----------------
-# MAVEN_OPTS - parameters passed to the Java VM when running Maven
-# e.g. to debug Maven itself, use
-# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
-# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
-# ----------------------------------------------------------------------------
-
-if [ -z "$MAVEN_SKIP_RC" ]; then
-
- if [ -f /usr/local/etc/mavenrc ]; then
- . /usr/local/etc/mavenrc
- fi
-
- if [ -f /etc/mavenrc ]; then
- . /etc/mavenrc
- fi
-
- if [ -f "$HOME/.mavenrc" ]; then
- . "$HOME/.mavenrc"
- fi
-
-fi
-
-# OS specific support. $var _must_ be set to either true or false.
-cygwin=false
-darwin=false
-mingw=false
-case "$(uname)" in
-CYGWIN*) cygwin=true ;;
-MINGW*) mingw=true ;;
-Darwin*)
- darwin=true
- # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
- # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
- if [ -z "$JAVA_HOME" ]; then
- if [ -x "/usr/libexec/java_home" ]; then
- JAVA_HOME="$(/usr/libexec/java_home)"
- export JAVA_HOME
- else
- JAVA_HOME="/Library/Java/Home"
- export JAVA_HOME
- fi
- fi
- ;;
-esac
-
-if [ -z "$JAVA_HOME" ]; then
- if [ -r /etc/gentoo-release ]; then
- JAVA_HOME=$(java-config --jre-home)
- fi
-fi
-
-# For Cygwin, ensure paths are in UNIX format before anything is touched
-if $cygwin; then
- [ -n "$JAVA_HOME" ] \
- && JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
- [ -n "$CLASSPATH" ] \
- && CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
-fi
-
-# For Mingw, ensure paths are in UNIX format before anything is touched
-if $mingw; then
- [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \
- && JAVA_HOME="$(
- cd "$JAVA_HOME" || (
- echo "cannot cd into $JAVA_HOME." >&2
- exit 1
- )
- pwd
- )"
-fi
-
-if [ -z "$JAVA_HOME" ]; then
- javaExecutable="$(which javac)"
- if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then
- # readlink(1) is not available as standard on Solaris 10.
- readLink=$(which readlink)
- if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
- if $darwin; then
- javaHome="$(dirname "$javaExecutable")"
- javaExecutable="$(cd "$javaHome" && pwd -P)/javac"
- else
- javaExecutable="$(readlink -f "$javaExecutable")"
- fi
- javaHome="$(dirname "$javaExecutable")"
- javaHome=$(expr "$javaHome" : '\(.*\)/bin')
- JAVA_HOME="$javaHome"
- export JAVA_HOME
- fi
- fi
-fi
-
-if [ -z "$JAVACMD" ]; then
- if [ -n "$JAVA_HOME" ]; then
- if [ -x "$JAVA_HOME/jre/sh/java" ]; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- fi
- else
- JAVACMD="$(
- \unset -f command 2>/dev/null
- \command -v java
- )"
- fi
-fi
-
-if [ ! -x "$JAVACMD" ]; then
- echo "Error: JAVA_HOME is not defined correctly." >&2
- echo " We cannot execute $JAVACMD" >&2
- exit 1
-fi
-
-if [ -z "$JAVA_HOME" ]; then
- echo "Warning: JAVA_HOME environment variable is not set." >&2
-fi
-
-# traverses directory structure from process work directory to filesystem root
-# first directory with .mvn subdirectory is considered project base directory
-find_maven_basedir() {
- if [ -z "$1" ]; then
- echo "Path not specified to find_maven_basedir" >&2
- return 1
- fi
-
- basedir="$1"
- wdir="$1"
- while [ "$wdir" != '/' ]; do
- if [ -d "$wdir"/.mvn ]; then
- basedir=$wdir
- break
- fi
- # workaround for JBEAP-8937 (on Solaris 10/Sparc)
- if [ -d "${wdir}" ]; then
- wdir=$(
- cd "$wdir/.." || exit 1
- pwd
- )
- fi
- # end of workaround
- done
- printf '%s' "$(
- cd "$basedir" || exit 1
- pwd
- )"
-}
-
-# concatenates all lines of a file
-concat_lines() {
- if [ -f "$1" ]; then
- # Remove \r in case we run on Windows within Git Bash
- # and check out the repository with auto CRLF management
- # enabled. Otherwise, we may read lines that are delimited with
- # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
- # splitting rules.
- tr -s '\r\n' ' ' <"$1"
- fi
-}
-
-log() {
- if [ "$MVNW_VERBOSE" = true ]; then
- printf '%s\n' "$1"
- fi
-}
-
-BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
-if [ -z "$BASE_DIR" ]; then
- exit 1
-fi
-
-MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
-export MAVEN_PROJECTBASEDIR
-log "$MAVEN_PROJECTBASEDIR"
-
-##########################################################################################
-# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
-# This allows using the maven wrapper in projects that prohibit checking in binary data.
-##########################################################################################
-wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
-if [ -r "$wrapperJarPath" ]; then
- log "Found $wrapperJarPath"
-else
- log "Couldn't find $wrapperJarPath, downloading it ..."
-
- if [ -n "$MVNW_REPOURL" ]; then
- wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/@@project.version@@/maven-wrapper-@@project.version@@.jar"
- else
- wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/@@project.version@@/maven-wrapper-@@project.version@@.jar"
- fi
- while IFS="=" read -r key value; do
- # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
- safeValue=$(echo "$value" | tr -d '\r')
- case "$key" in wrapperUrl)
- wrapperUrl="$safeValue"
- break
- ;;
- esac
- done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
- log "Downloading from: $wrapperUrl"
-
- if $cygwin; then
- wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
- fi
-
- if command -v wget >/dev/null; then
- log "Found wget ... using wget"
- [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
- if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
- wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
- else
- wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
- fi
- elif command -v curl >/dev/null; then
- log "Found curl ... using curl"
- [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
- if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
- curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
- else
- curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
- fi
- else
- log "Falling back to using Java to download"
- javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
- javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
- # For Cygwin, switch paths to Windows format before running javac
- if $cygwin; then
- javaSource=$(cygpath --path --windows "$javaSource")
- javaClass=$(cygpath --path --windows "$javaClass")
- fi
- if [ -e "$javaSource" ]; then
- if [ ! -e "$javaClass" ]; then
- log " - Compiling MavenWrapperDownloader.java ..."
- ("$JAVA_HOME/bin/javac" "$javaSource")
- fi
- if [ -e "$javaClass" ]; then
- log " - Running MavenWrapperDownloader.java ..."
- ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
- fi
- fi
- fi
-fi
-##########################################################################################
-# End of extension
-##########################################################################################
-
-# If specified, validate the SHA-256 sum of the Maven wrapper jar file
-wrapperSha256Sum=""
-while IFS="=" read -r key value; do
- case "$key" in wrapperSha256Sum)
- wrapperSha256Sum=$value
- break
- ;;
- esac
-done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
-if [ -n "$wrapperSha256Sum" ]; then
- wrapperSha256Result=false
- if command -v sha256sum >/dev/null; then
- if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then
- wrapperSha256Result=true
- fi
- elif command -v shasum >/dev/null; then
- if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then
- wrapperSha256Result=true
- fi
- else
- echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
- echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2
- exit 1
- fi
- if [ $wrapperSha256Result = false ]; then
- echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
- echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
- echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
- exit 1
- fi
-fi
-
-MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
-
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin; then
- [ -n "$JAVA_HOME" ] \
- && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
- [ -n "$CLASSPATH" ] \
- && CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
- [ -n "$MAVEN_PROJECTBASEDIR" ] \
- && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
-fi
-
-# Provide a "standardized" way to retrieve the CLI args that will
-# work with both Windows and non-Windows executions.
-MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
-export MAVEN_CMD_LINE_ARGS
-
-WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
-
-# shellcheck disable=SC2086 # safe args
-exec "$JAVACMD" \
- $MAVEN_OPTS \
- $MAVEN_DEBUG_OPTS \
- -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
- "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
- ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
deleted file mode 100644
index 708460f..0000000
--- a/mvnw.cmd
+++ /dev/null
@@ -1,206 +0,0 @@
-@REM ----------------------------------------------------------------------------
-@REM Licensed to the Apache Software Foundation (ASF) under one
-@REM or more contributor license agreements. See the NOTICE file
-@REM distributed with this work for additional information
-@REM regarding copyright ownership. The ASF licenses this file
-@REM to you under the Apache License, Version 2.0 (the
-@REM "License"); you may not use this file except in compliance
-@REM with the License. You may obtain a copy of the License at
-@REM
-@REM http://www.apache.org/licenses/LICENSE-2.0
-@REM
-@REM Unless required by applicable law or agreed to in writing,
-@REM software distributed under the License is distributed on an
-@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-@REM KIND, either express or implied. See the License for the
-@REM specific language governing permissions and limitations
-@REM under the License.
-@REM ----------------------------------------------------------------------------
-
-@REM ----------------------------------------------------------------------------
-@REM Apache Maven Wrapper startup batch script, version @@project.version@@
-@REM
-@REM Required ENV vars:
-@REM JAVA_HOME - location of a JDK home dir
-@REM
-@REM Optional ENV vars
-@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
-@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
-@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
-@REM e.g. to debug Maven itself, use
-@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
-@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
-@REM ----------------------------------------------------------------------------
-
-@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
-@echo off
-@REM set title of command window
-title %0
-@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
-@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
-
-@REM set %HOME% to equivalent of $HOME
-if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
-
-@REM Execute a user defined script before this one
-if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
-@REM check for pre script, once with legacy .bat ending and once with .cmd ending
-if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
-if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
-:skipRcPre
-
-@setlocal
-
-set ERROR_CODE=0
-
-@REM To isolate internal variables from possible post scripts, we use another setlocal
-@setlocal
-
-@REM ==== START VALIDATION ====
-if not "%JAVA_HOME%" == "" goto OkJHome
-
-echo. >&2
-echo Error: JAVA_HOME not found in your environment. >&2
-echo Please set the JAVA_HOME variable in your environment to match the >&2
-echo location of your Java installation. >&2
-echo. >&2
-goto error
-
-:OkJHome
-if exist "%JAVA_HOME%\bin\java.exe" goto init
-
-echo. >&2
-echo Error: JAVA_HOME is set to an invalid directory. >&2
-echo JAVA_HOME = "%JAVA_HOME%" >&2
-echo Please set the JAVA_HOME variable in your environment to match the >&2
-echo location of your Java installation. >&2
-echo. >&2
-goto error
-
-@REM ==== END VALIDATION ====
-
-:init
-
-@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
-@REM Fallback to current working directory if not found.
-
-set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
-IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
-
-set EXEC_DIR=%CD%
-set WDIR=%EXEC_DIR%
-:findBaseDir
-IF EXIST "%WDIR%"\.mvn goto baseDirFound
-cd ..
-IF "%WDIR%"=="%CD%" goto baseDirNotFound
-set WDIR=%CD%
-goto findBaseDir
-
-:baseDirFound
-set MAVEN_PROJECTBASEDIR=%WDIR%
-cd "%EXEC_DIR%"
-goto endDetectBaseDir
-
-:baseDirNotFound
-set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
-cd "%EXEC_DIR%"
-
-:endDetectBaseDir
-
-IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
-
-@setlocal EnableExtensions EnableDelayedExpansion
-for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
-@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
-
-:endReadAdditionalConfig
-
-SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
-set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
-set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
-
-set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/@@project.version@@/maven-wrapper-@@project.version@@.jar"
-
-FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
- IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
-)
-
-@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
-@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
-if exist %WRAPPER_JAR% (
- if "%MVNW_VERBOSE%" == "true" (
- echo Found %WRAPPER_JAR%
- )
-) else (
- if not "%MVNW_REPOURL%" == "" (
- SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/@@project.version@@/maven-wrapper-@@project.version@@.jar"
- )
- if "%MVNW_VERBOSE%" == "true" (
- echo Couldn't find %WRAPPER_JAR%, downloading it ...
- echo Downloading from: %WRAPPER_URL%
- )
-
- powershell -Command "&{"^
- "$webclient = new-object System.Net.WebClient;"^
- "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
- "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
- "}"^
- "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
- "}"
- if "%MVNW_VERBOSE%" == "true" (
- echo Finished downloading %WRAPPER_JAR%
- )
-)
-@REM End of extension
-
-@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
-SET WRAPPER_SHA_256_SUM=""
-FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
- IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
-)
-IF NOT %WRAPPER_SHA_256_SUM%=="" (
- powershell -Command "&{"^
- "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^
- "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
- "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
- " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
- " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
- " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
- " exit 1;"^
- "}"^
- "}"
- if ERRORLEVEL 1 goto error
-)
-
-@REM Provide a "standardized" way to retrieve the CLI args that will
-@REM work with both Windows and non-Windows executions.
-set MAVEN_CMD_LINE_ARGS=%*
-
-%MAVEN_JAVA_EXE% ^
- %JVM_CONFIG_MAVEN_PROPS% ^
- %MAVEN_OPTS% ^
- %MAVEN_DEBUG_OPTS% ^
- -classpath %WRAPPER_JAR% ^
- "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
- %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
-if ERRORLEVEL 1 goto error
-goto end
-
-:error
-set ERROR_CODE=1
-
-:end
-@endlocal & set ERROR_CODE=%ERROR_CODE%
-
-if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
-@REM check for post script, once with legacy .bat ending and once with .cmd ending
-if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
-if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
-:skipRcPost
-
-@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
-if "%MAVEN_BATCH_PAUSE%"=="on" pause
-
-if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
-
-cmd /C exit /B %ERROR_CODE%
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index b6852b9..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
- 4.0.0
-
-
- org.springframework.boot
- spring-boot-starter-parent
- 3.4.3
-
-
-
- com.lowenssh
- lowenssh
- 0.0.1-SNAPSHOT
- lowenssh
- AI SSH 智能运维 Agent
-
-
- 17
-
- 1.1.5
-
-
-
-
-
- org.springframework.ai
- spring-ai-bom
- ${spring-ai.version}
- pom
- import
-
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
-
-
- org.springframework.boot
- spring-boot-starter-actuator
-
-
-
-
- org.springframework.ai
- spring-ai-starter-model-openai
-
-
-
-
- com.github.mwiede
- jsch
- 0.2.21
-
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
-
-
-
- com.baomidou
- mybatis-plus-spring-boot3-starter
- 3.5.9
-
-
- com.mysql
- mysql-connector-j
- runtime
-
-
-
-
- org.projectlombok
- lombok
- true
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
-
-
diff --git a/src/main/java/com/lowenssh/LowenSshApplication.java b/src/main/java/com/lowenssh/LowenSshApplication.java
deleted file mode 100644
index f2e4c50..0000000
--- a/src/main/java/com/lowenssh/LowenSshApplication.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.lowenssh;
-
-import org.mybatis.spring.annotation.MapperScan;
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.scheduling.annotation.EnableScheduling;
-
-/**
- * LowenSSH 启动类 —— AI SSH 智能运维 Agent
- */
-@SpringBootApplication
-@EnableScheduling // 开启定时任务:SessionManager 定时回收超时的常驻 SSH 连接
-@MapperScan("com.lowenssh.persistence.mapper") // 扫描 MyBatis Mapper 接口
-public class LowenSshApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(LowenSshApplication.class, args);
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/AgentController.java b/src/main/java/com/lowenssh/agent/AgentController.java
deleted file mode 100644
index b5564ca..0000000
--- a/src/main/java/com/lowenssh/agent/AgentController.java
+++ /dev/null
@@ -1,187 +0,0 @@
-package com.lowenssh.agent;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.lowenssh.agent.guard.AutoConfirmationHandler;
-import com.lowenssh.agent.guard.CommandGuard;
-import com.lowenssh.persistence.AuditService;
-import com.lowenssh.persistence.MessageService;
-import com.lowenssh.persistence.entity.SessionEntity;
-import com.lowenssh.persistence.mapper.SessionMapper;
-import com.lowenssh.ssh.SshClient;
-import org.springframework.http.MediaType;
-import org.springframework.http.codec.ServerSentEvent;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RestController;
-import reactor.core.publisher.Flux;
-
-import java.time.format.DateTimeFormatter;
-import java.util.List;
-
-/**
- * Agent 接口。
- *
- * 流式端点支持多轮对话:
- * - 首轮:sessionId 为空,带 host/port/user/password,SessionManager 建会话 + 连一次 SSH,
- * 把连接常驻;先回 session_ready 事件把 sessionId 给前端。
- * - 续聊:sessionId 非空,复用该会话的常驻连接(保留 cd 等上下文),历史从库里回灌给模型。
- *
- * 连接生命周期归 SessionManager 管(超时回收 / 显式关闭),不再随单次请求开关。
- */
-@RestController
-public class AgentController {
-
- private final AgentService agentService;
- private final SessionManager sessionManager;
- private final SessionMapper sessionMapper;
- private final AuditService auditService;
- private final MessageService messageService;
- private final CommandGuard guard;
-
- public AgentController(AgentService agentService, SessionManager sessionManager,
- SessionMapper sessionMapper, AuditService auditService,
- MessageService messageService, CommandGuard guard) {
- this.agentService = agentService;
- this.sessionManager = sessionManager;
- this.sessionMapper = sessionMapper;
- this.auditService = auditService;
- this.messageService = messageService;
- this.guard = guard;
- }
-
- private static final DateTimeFormatter TS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
-
- /**
- * 列出会话,按更新时间倒序(左侧历史栏用)。
- * 带 hostId 则只列该主机的会话(历史按主机隔离);不带则列全部(兼容旧调用)。
- * 只读查库,不碰 SSH。
- */
- @GetMapping("/api/agent/sessions")
- public List sessions(
- @org.springframework.web.bind.annotation.RequestParam(value = "hostId", required = false) Long hostId) {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
- .eq(hostId != null, SessionEntity::getHostId, hostId)
- .orderByDesc(SessionEntity::getUpdatedAt)
- .orderByDesc(SessionEntity::getId); // updatedAt 为空时退而按 id 排
- return sessionMapper.selectList(wrapper).stream()
- .map(s -> new SessionDto.SessionItem(
- s.getId(), s.getTitle(), s.getSshHost(), s.getSshUser(), s.getSshPort(),
- s.getUpdatedAt() == null ? null : s.getUpdatedAt().format(TS_FMT)))
- .toList();
- }
-
- /**
- * 拉某会话的历史消息 + 连接信息 + 常驻连接是否存活(左栏点开旧会话回看用)。
- * live=true 表示常驻连接还在,可直接续聊;false 则前端提示需重连。
- * 只读查库 + 查内存连接状态,不碰 SSH 执行。
- */
- @GetMapping("/api/agent/sessions/{id}/messages")
- public SessionDto.SessionDetail sessionMessages(@PathVariable("id") Long id) {
- SessionEntity s = sessionMapper.selectById(id);
- if (s == null) {
- return new SessionDto.SessionDetail(id, null, null, null, false, List.of());
- }
- boolean live = sessionManager.get(id) != null;
- return new SessionDto.SessionDetail(
- id, s.getSshHost(), s.getSshPort(), s.getSshUser(), live,
- messageService.loadHistoryForView(id));
- }
-
- @PostMapping("/api/agent/run")
- public String run(@RequestBody RunRequest req) {
- int port = req.port() == 0 ? 22 : req.port();
-
- // 先建会话拿 id:审计要 session_id 关联
- SessionEntity session = new SessionEntity();
- session.setTitle(SessionManager.toTitle(req.task()));
- session.setSshHost(req.host());
- session.setSshPort(port);
- session.setSshUser(req.user());
- sessionMapper.insert(session);
- Long sessionId = session.getId();
-
- // try-with-resources:loop 跑完自动关连接(同步接口是一次性测试用,不参与多轮常驻)
- try (SshClient ssh = new SshClient()) {
- ssh.connect(req.host(), port, req.user(), req.password());
- SshTools tools = new SshTools(ssh, sessionId, auditService, guard);
- // REST 场景用自动确认:deny 已被门禁拦死,ask 态自动放行以便自动化测试
- return agentService.run(sessionId, req.task(), tools, new AutoConfirmationHandler());
- } catch (Exception e) {
- return "任务执行失败: " + e.getMessage();
- }
- }
-
- /**
- * 流式 + 多轮:用 SSE 把 agent 执行过程逐事件吐出来。
- *
- * 首轮(curl 示例,sessionId 不传):
- * curl -N -X POST http://localhost:8081/api/agent/stream \
- * -H 'Content-Type: application/json' \
- * -d '{"host":"1.2.3.4","user":"root","password":"xxx","task":"看下根分区还剩多少空间"}'
- * 续聊:带上首轮拿到的 sessionId,连接信息可省:
- * -d '{"sessionId":1,"task":"那内存呢"}'
- *
- * 连接由 SessionManager 常驻,这里不再 doFinally 关连接。
- */
- @PostMapping(value = "/api/agent/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
- public Flux> stream(@RequestBody RunRequest req) {
- SessionManager.LiveSession live;
- boolean firstTurn = req.sessionId() == null;
-
- if (firstTurn) {
- // 首轮:复用进主机时建好的预连接;没有(curl 直连)则现连一次。再落库建会话行(title=任务摘要)。
- int port = req.port() == 0 ? 22 : req.port();
- try {
- live = sessionManager.getByHost(req.hostId());
- if (live == null) {
- live = sessionManager.connectHost(req.hostId(), req.host(), port, req.user(), req.password());
- }
- } catch (Exception e) {
- return Flux.just(sse(new AgentEvent.Error("SSH 连接失败: " + e.getMessage())));
- }
- // 落库与连接分开归类:落库失败不能误报成连接失败,否则排查方向全错
- try {
- sessionManager.attachSession(live, req.task());
- } catch (Exception e) {
- return Flux.just(sse(new AgentEvent.Error("创建会话失败: " + e.getMessage())));
- }
- } else {
- // 续聊:取常驻连接;不存在/已过期则提示前端重连
- live = sessionManager.get(req.sessionId());
- if (live == null) {
- return Flux.just(sse(new AgentEvent.SessionExpired(req.sessionId(),
- "常驻连接已断开(空闲超时回收),请在右侧开新会话重连")));
- }
- }
-
- Long sessionId = live.sessionId();
- // 每轮新建 SshTools,但底层复用 manager 里同一个常驻 SshClient(保留 cd 等上下文)。
- // 传 live.lock() 让 SFTP 工具与人工面板/监控串行化,避免抢同一条 Session。
- SshTools tools = new SshTools(live.ssh(), sessionId, auditService, guard, live.lock());
-
- Flux> events = agentService
- .runStream(sessionId, req.task(), tools, new AutoConfirmationHandler())
- .map(this::sse);
-
- // 首轮在事件流最前面插一个 session_ready,把 sessionId 交给前端用于后续续聊
- if (firstTurn) {
- events = Flux.concat(Flux.just(sse(new AgentEvent.SessionReady(sessionId))), events);
- }
- return events;
- }
-
- /** 把领域事件包成 SSE:event 名取事件 type,方便前端按类型分发 */
- private ServerSentEvent sse(AgentEvent event) {
- return ServerSentEvent.builder()
- .event(event.type())
- .data(event)
- .build();
- }
-
- /** 请求体。sessionId 为空=首轮(带 hostId/host/port/user/password 新建会话+连 SSH);
- * 非空=续聊(复用该会话的常驻连接,连接信息可不传) */
- public record RunRequest(Long sessionId, Long hostId, String host, int port, String user, String password, String task) {
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/AgentEvent.java b/src/main/java/com/lowenssh/agent/AgentEvent.java
deleted file mode 100644
index dc6c9d4..0000000
--- a/src/main/java/com/lowenssh/agent/AgentEvent.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.lowenssh.agent;
-
-/**
- * Agent 事件 —— 对外流式输出的带类型事件流。
- *
- * 为什么用带类型事件而不是纯 Flux:运维 Agent 的输出有多种语义——
- * 模型在说话、模型要调命令、命令出结果、命令被拦、任务完成。前端要据此区别渲染
- * (token 追加、工具调用展开、拦截高亮、完成收尾)。纯字符串流分不出这些。
- *
- * sealed + record(Java 17):限定子类集合,消费方 switch 表达式时编译器保证穷尽,
- * 加事件类型不会漏处理。
- */
-public sealed interface AgentEvent {
-
- /** 会话就绪:首轮建会话后回传 sessionId,前端存下来用于后续多轮续聊 */
- record SessionReady(Long sessionId) implements AgentEvent {}
-
- /** 模型增量输出的一段文本 token */
- record Token(String text) implements AgentEvent {}
-
- /** 模型的思考过程增量(GLM reasoning_content),前端实时展示"在想什么" */
- record Reasoning(String text) implements AgentEvent {}
-
- /** 模型决定调用某个工具 */
- record ToolCall(String name, String args) implements AgentEvent {}
-
- /** 工具执行结果(executed=false 表示被拦截/拒绝,未真正执行) */
- record ToolResult(String name, String summary, boolean executed) implements AgentEvent {}
-
- /** 命令被安全门禁拦截 / 用户拒绝 */
- record Blocked(String command, String reason) implements AgentEvent {}
-
- /** 任务完成,带最终结论文本 */
- record Done(String finalText) implements AgentEvent {}
-
- /** 出错 */
- record Error(String message) implements AgentEvent {}
-
- /** 续聊时常驻连接已被回收/不存在:前端据此锁输入并切到断线态,提示开新会话重连 */
- record SessionExpired(Long sessionId, String message) implements AgentEvent {}
-
- /** 事件类型名,用作 SSE 的 event 字段,方便前端按类型监听 */
- default String type() {
- if (this instanceof SessionReady) return "session_ready";
- if (this instanceof Token) return "token";
- if (this instanceof Reasoning) return "reasoning";
- if (this instanceof ToolCall) return "tool_call";
- if (this instanceof ToolResult) return "tool_result";
- if (this instanceof Blocked) return "blocked";
- if (this instanceof Done) return "done";
- if (this instanceof Error) return "error";
- if (this instanceof SessionExpired) return "session_expired";
- return "unknown";
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/AgentService.java b/src/main/java/com/lowenssh/agent/AgentService.java
deleted file mode 100644
index 302dc9b..0000000
--- a/src/main/java/com/lowenssh/agent/AgentService.java
+++ /dev/null
@@ -1,520 +0,0 @@
-package com.lowenssh.agent;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.lowenssh.agent.guard.CommandGuard;
-import com.lowenssh.agent.guard.ConfirmationHandler;
-import com.lowenssh.persistence.AuditService;
-import com.lowenssh.persistence.MessageService;
-import org.springframework.ai.chat.messages.AssistantMessage;
-import org.springframework.ai.chat.messages.Message;
-import org.springframework.ai.chat.messages.SystemMessage;
-import org.springframework.ai.chat.messages.ToolResponseMessage;
-import org.springframework.ai.chat.messages.UserMessage;
-import org.springframework.ai.chat.model.ChatResponse;
-import org.springframework.ai.chat.model.MessageAggregator;
-import org.springframework.ai.chat.prompt.Prompt;
-import org.springframework.ai.model.tool.ToolCallingManager;
-import org.springframework.ai.model.tool.ToolExecutionResult;
-import org.springframework.ai.openai.OpenAiChatModel;
-import org.springframework.ai.openai.OpenAiChatOptions;
-import org.springframework.ai.support.ToolCallbacks;
-import org.springframework.ai.tool.ToolCallback;
-import org.springframework.stereotype.Service;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Sinks;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-
-/**
- * Agent 核心 —— 手写的 agentic loop + 安全门禁。
- *
- * 为什么手写而不用 Spring AI 的自动循环:自动循环把 tool_call 在框架内部执行掉,
- * 我们插不进"执行前人工确认 / 危险命令拦截"这一刀。关键开关:
- * options.internalToolExecutionEnabled(false) —— 关掉自动执行,让 tool_call
- * 回到我们手里,先过门禁、再由 ToolCallingManager.executeToolCalls 显式执行。
- *
- * 循环结构(Claude Code 同款状态机思路):
- * 请求 → 模型返回 → 有 tool_call?
- * 有 → 门禁预检 → 全放行才执行工具 → 结果回灌 → 带新历史再请求
- * 任一被拒 → 不执行,手动回灌"拒绝"作为工具结果 → loop 继续让模型换方案
- * 无 → 模型给出最终结论,循环结束
- * 加最大轮数上限防死循环。
- *
- * 安全是独立代码路径:门禁不写进工具方法、不靠模型自觉,越狱也绕不过。
- */
-@Service
-public class AgentService {
-
- private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AgentService.class);
-
- /** 最大循环轮数,防止模型反复调工具停不下来。可配置:xwssh.agent.max-rounds */
- private final int maxRounds;
-
- /** 只对这个工具的命令做门禁;其余(读文件/看日志)天然只读,放行 */
- private static final String EXEC_TOOL = "execCommand";
-
- private static final String SYSTEM_PROMPT = """
- ## 身份
- 你是 LowenSSH,一个面向 Linux 服务器的 SSH/SFTP 智能体。
- 你帮用户远程排查问题、执行命令、读取文件、查看日志,并在用户授权下完成文件传输等运维操作。
- 你的能力随工具集扩展——当前可用的工具见工具列表,只调用列表里实际存在的工具,不要臆造工具。
-
- ## 环境与安全
- 你执行的每条命令都会经过一道独立的安全门禁,危险命令会被拦截。
- 被拦时换一个更安全的方式达成目标,不要重复同一条被拒命令,也不要改用等价的危险命令绕过拦截。
-
- ## 工作方式
- - 先理解任务目标再决定查什么,每步拿到结果后判断下一步,不要一次堆一堆命令。
- - 优先用只读命令探查(df / free / ps / cat / tail),看清现状再动有副作用的操作。
- - 命令输出可能被截断(节省 token),抓关键信息即可,需要时再精确查询。
-
- ## 输出格式
- - 用中文,给出结论,不要只罗列原始命令输出。
- - 简单结果用自然语言简短回答,多维度信息才用列表或表格。
- - 关键数字(磁盘占用 %、内存、负载等)直接点出来,别让用户自己从输出里找。
- """;
-
- private final OpenAiChatModel chatModel;
- private final ToolCallingManager toolCallingManager;
- private final CommandGuard guard;
- private final AuditService auditService;
- private final MessageService messageService;
- private final ContextManager contextManager;
- private final ObjectMapper objectMapper = new ObjectMapper();
-
- // OpenAiChatModel 和 ToolCallingManager 都由 starter 自动配置好,直接注入
- public AgentService(OpenAiChatModel chatModel, ToolCallingManager toolCallingManager,
- CommandGuard guard, AuditService auditService, MessageService messageService,
- ContextManager contextManager,
- @org.springframework.beans.factory.annotation.Value("${xwssh.agent.max-rounds:40}") int maxRounds) {
- this.chatModel = chatModel;
- this.toolCallingManager = toolCallingManager;
- this.guard = guard;
- this.auditService = auditService;
- this.messageService = messageService;
- this.contextManager = contextManager;
- this.maxRounds = maxRounds;
- }
-
- /**
- * 跑一轮 agent 任务。
- *
- * @param sessionId 本次会话 id(审计落库用)
- * @param task 用户的运维任务
- * @param tools 会话级工具集(已绑定连好的 SSH 会话)
- * @param confirmer ask 态命令的人工确认入口(控制台真人 / REST 自动放行)
- * @return 模型的最终结论文本
- */
- public String run(Long sessionId, String task, SshTools tools, ConfirmationHandler confirmer) {
- ToolCallback[] callbacks = ToolCallbacks.from(tools);
-
- // 关键:internalToolExecutionEnabled(false) 关掉框架自动执行工具。
- // options 在循环外只构建一次:工具 schema 是 GLM 上下文缓存前缀的一部分,
- // 若每轮重建导致 schema 序列化抖动,会让缓存前缀失配、整段历史按全价重算。
- OpenAiChatOptions options = OpenAiChatOptions.builder()
- .toolCallbacks(callbacks)
- .internalToolExecutionEnabled(false)
- .build();
-
- List messages = new ArrayList<>();
- messages.add(new SystemMessage(SYSTEM_PROMPT));
- messages.addAll(messageService.loadHistory(sessionId)); // 还原历史,支持多轮续聊
- messages.add(new UserMessage(task));
- messageService.saveUser(sessionId, task); // 落用户任务
-
- for (int round = 1; round <= maxRounds; round++) {
- // 进模型前整理上下文:Layer 0 截断大工具结果 + Layer 4 历史超阈值则压缩
- messages = contextManager.truncateToolResponses(messages);
- messages = contextManager.compressIfNeeded(messages);
-
- Prompt prompt = new Prompt(messages, options);
- ChatResponse response = chatModel.call(prompt);
- logUsage(response); // 测缓存命中
-
- // 没有 tool_call 了,模型给出最终结论,结束
- if (!response.hasToolCalls()) {
- String text = response.getResult().getOutput().getText();
- // 兜底:模型可能给出空结论(尤其命令被反复拦截后),别返回空串误导调用方
- if (text == null || text.isBlank()) {
- text = "模型暂时没有返回内容,请重试。";
- messageService.saveAssistant(sessionId, text, null);
- return text;
- }
- messageService.saveAssistant(sessionId, text, null); // 落最终结论
- return text;
- }
-
- AssistantMessage assistant = response.getResult().getOutput();
- persistAssistant(sessionId, assistant); // 落 assistant(文字 + tool_calls)
-
- // —— 门禁预检:逐个 tool_call 判定,收集被拒的 ——
- List rejected = screen(sessionId, assistant, confirmer, null);
-
- if (!rejected.isEmpty()) {
- // 有被拒的:不调框架执行(executeToolCalls 是整批执行,没法只跑一部分)。
- // 手动把 assistant 的 tool_call 消息 + 拒绝结果回灌,loop 继续让模型换方案。
- messages.add(assistant);
- messages.add(ToolResponseMessage.builder().responses(rejected).build());
- persistToolResponses(sessionId, rejected); // 落拒绝结果(还原"想跑啥被拦了")
- continue;
- }
-
- // 全放行:交给框架执行,拿回灌后的完整历史
- ToolExecutionResult execResult = toolCallingManager.executeToolCalls(prompt, response);
- persistLastToolResponses(sessionId, execResult); // 落本轮工具执行结果
- messages = new ArrayList<>(execResult.conversationHistory());
- }
-
- return "已达到最大循环轮数(" + maxRounds + "),任务可能未完成。请拆分任务后重试。";
- }
-
- /**
- * 流式版本:把同步 run() 的执行过程拆成带类型事件流对外推送(SSE 用)。
- *
- * 架构(混合线程):
- * - 对外用 Sinks.Many 造一条 Flux,方法立刻返回,不阻塞。
- * - 真正的 agentic loop 是命令式 while,放到独立线程里跑(loop 内部有 block 调用,
- * 不能跑在 reactor 调度线程上)。
- * - 每轮 chatModel.stream 拿到 token 流,用 MessageAggregator 边推 Token 事件、
- * 边把碎片聚合成完整 ChatResponse(含 tool_call),聚合完再走门禁/执行。
- *
- * 注意:SSH 连接的关闭由调用方在 Flux.doFinally 里做——这里是异步的,方法返回时 loop 还没跑完,
- * 不能用 try-with-resources。
- */
- public Flux runStream(Long sessionId, String task, SshTools tools, ConfirmationHandler confirmer) {
- Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer();
-
- Thread worker = new Thread(() -> {
- try {
- ToolCallback[] callbacks = ToolCallbacks.from(tools);
- // 同步版同款:循环外构建一次,保住缓存前缀稳定
- OpenAiChatOptions options = OpenAiChatOptions.builder()
- .toolCallbacks(callbacks)
- .internalToolExecutionEnabled(false)
- .build();
-
- List messages = new ArrayList<>();
- messages.add(new SystemMessage(SYSTEM_PROMPT));
- messages.addAll(messageService.loadHistory(sessionId)); // 还原历史,支持多轮续聊
- messages.add(new UserMessage(task));
- messageService.saveUser(sessionId, task); // 落用户任务
-
- for (int round = 1; round <= maxRounds; round++) {
- // 进模型前整理上下文:Layer 0 截断大工具结果 + Layer 4 历史超阈值则压缩
- messages = contextManager.truncateToolResponses(messages);
- messages = contextManager.compressIfNeeded(messages);
-
- Prompt prompt = new Prompt(messages, options);
-
- // 一次流式模型调用:边推 token 边聚合,返回完整 ChatResponse
- ChatResponse response = streamOnce(prompt, sink);
-
- // 没有 tool_call:模型给出最终结论,结束
- if (response == null || !response.hasToolCalls()) {
- String text = response == null ? null : response.getResult().getOutput().getText();
- // 空响应可能是模型偶发抖动(返回空 choice),重试一次再判定
- if (text == null || text.isBlank()) {
- log.warn("模型返回空结论,重试一次 sessionId={} round={}", sessionId, round);
- response = streamOnce(prompt, sink);
- text = response == null ? null : response.getResult().getOutput().getText();
- // 重试后又冒出 tool_call,回主循环正常处理
- if (response != null && response.hasToolCalls()) {
- AssistantMessage retried = response.getResult().getOutput();
- persistAssistant(sessionId, retried);
- for (AssistantMessage.ToolCall call : retried.getToolCalls()) {
- sink.tryEmitNext(new AgentEvent.ToolCall(call.name(), call.arguments()));
- }
- List rj =
- screen(sessionId, retried, confirmer, sink::tryEmitNext);
- if (!rj.isEmpty()) {
- messages.add(retried);
- messages.add(ToolResponseMessage.builder().responses(rj).build());
- persistToolResponses(sessionId, rj);
- continue;
- }
- Prompt retryPrompt = new Prompt(messages, options);
- ToolExecutionResult er = toolCallingManager.executeToolCalls(retryPrompt, response);
- emitToolResults(er, sink);
- persistLastToolResponses(sessionId, er);
- messages = new ArrayList<>(er.conversationHistory());
- continue;
- }
- }
- if (text == null || text.isBlank()) {
- // 重试仍空:中性文案,不再误导为"被安全策略阻止"
- text = "模型暂时没有返回内容,请重试。";
- }
- messageService.saveAssistant(sessionId, text, null); // 落最终结论
- sink.tryEmitNext(new AgentEvent.Done(text));
- sink.tryEmitComplete();
- return;
- }
-
- AssistantMessage assistant = response.getResult().getOutput();
- persistAssistant(sessionId, assistant); // 落 assistant(文字 + tool_calls)
- // 先把本轮要调的工具吐出去,让前端看到"准备执行什么"
- for (AssistantMessage.ToolCall call : assistant.getToolCalls()) {
- sink.tryEmitNext(new AgentEvent.ToolCall(call.name(), call.arguments()));
- }
-
- // 门禁预检:DENY/用户拒绝会通过 onBlocked 推 Blocked 事件
- List rejected =
- screen(sessionId, assistant, confirmer, sink::tryEmitNext);
-
- if (!rejected.isEmpty()) {
- // 有被拒:整批不执行,把 assistant + 拒绝结果回灌,让模型换方案
- messages.add(assistant);
- messages.add(ToolResponseMessage.builder().responses(rejected).build());
- persistToolResponses(sessionId, rejected); // 落拒绝结果
- continue;
- }
-
- // 全放行:交框架执行,并把每个工具结果摘要吐给前端
- ToolExecutionResult execResult = toolCallingManager.executeToolCalls(prompt, response);
- emitToolResults(execResult, sink);
- persistLastToolResponses(sessionId, execResult); // 落本轮工具执行结果
- messages = new ArrayList<>(execResult.conversationHistory());
- }
-
- sink.tryEmitNext(new AgentEvent.Done(
- "已达到最大循环轮数(" + maxRounds + "),任务可能未完成。请拆分任务后重试。"));
- sink.tryEmitComplete();
- } catch (Exception e) {
- log.warn("流式 agent 执行异常 sessionId={}", sessionId, e);
- sink.tryEmitNext(new AgentEvent.Error(e.getMessage() == null ? e.toString() : e.getMessage()));
- sink.tryEmitComplete();
- }
- }, "agent-stream-" + sessionId);
- worker.setDaemon(true);
- worker.start();
-
- return sink.asFlux();
- }
-
- /**
- * 一次流式模型调用:透传 chunk 推 Token 事件,聚合成完整 ChatResponse 返回。
- * 抽出来供主循环和空响应重试复用。
- */
- private ChatResponse streamOnce(Prompt prompt, Sinks.Many sink) {
- AtomicReference aggregatedRef = new AtomicReference<>();
- new MessageAggregator()
- .aggregate(chatModel.stream(prompt), aggregatedRef::set)
- .doOnNext(chunk -> {
- if (chunk.getResult() == null) {
- return;
- }
- var output = chunk.getResult().getOutput();
- // GLM 思考阶段 text 为 null,思考增量在 output.metadata.reasoningContent,
- // 单独推 Reasoning 事件,前端实时展示"在想什么"。
- Object reasoning = output.getMetadata() == null ? null
- : output.getMetadata().get("reasoningContent");
- if (reasoning instanceof String rc && !rc.isEmpty()) {
- sink.tryEmitNext(new AgentEvent.Reasoning(rc));
- }
- String t = output.getText();
- if (t != null && !t.isEmpty()) {
- sink.tryEmitNext(new AgentEvent.Token(t));
- }
- })
- .blockLast();
- ChatResponse resp = aggregatedRef.get();
- logUsage(resp);
- return resp;
- }
-
- /**
- * 打印本轮 token 用量和缓存命中率(省 token 的测量基础)。
- *
- * GLM 隐式上下文缓存:命中的 token 按更低价计费,命中数在
- * usage.prompt_tokens_details.cached_tokens(OpenAI 兼容字段,Spring AI 收进 nativeUsage)。
- * Spring AI 的标准 Usage 只有 prompt/completion/total,cached 要从 nativeUsage 里挖,
- * 字段不一定有,全程防御式读取,取不到只打基础值,绝不影响主流程。
- */
- private void logUsage(ChatResponse resp) {
- try {
- if (resp == null || resp.getMetadata() == null || resp.getMetadata().getUsage() == null) {
- return;
- }
- var usage = resp.getMetadata().getUsage();
- Integer prompt = usage.getPromptTokens();
- Integer completion = usage.getCompletionTokens();
- Integer total = usage.getTotalTokens();
- long cached = extractCachedTokens(usage.getNativeUsage());
- String hitRate = (prompt != null && prompt > 0)
- ? String.format("%.0f%%", cached * 100.0 / prompt) : "n/a";
- log.info("token 用量 prompt={} completion={} total={} cached={} 命中率={}",
- prompt, completion, total, cached, hitRate);
- } catch (Exception e) {
- // 测量失败绝不能拖累主流程
- log.debug("读取 token 用量失败: {}", e.getMessage());
- }
- }
-
- /** 从 nativeUsage(GLM 返回的原始 usage 对象)里挖 prompt_tokens_details.cached_tokens;挖不到返回 0 */
- private long extractCachedTokens(Object nativeUsage) {
- if (nativeUsage == null) {
- return 0;
- }
- try {
- // nativeUsage 一般是 OpenAI SDK 的 Usage 对象,序列化成树后按字段名取,避免硬依赖具体类型
- var node = objectMapper.valueToTree(nativeUsage);
- var details = node.get("promptTokensDetails");
- if (details == null) {
- details = node.get("prompt_tokens_details");
- }
- if (details == null) {
- return 0;
- }
- var cached = details.get("cachedTokens");
- if (cached == null) {
- cached = details.get("cached_tokens");
- }
- return cached == null ? 0 : cached.asLong();
- } catch (Exception e) {
- return 0;
- }
- }
-
- /** 从框架执行后的会话历史里抽取本轮工具结果,逐个推 ToolResult 事件 */
- private void emitToolResults(ToolExecutionResult execResult, Sinks.Many sink) {
- List history = execResult.conversationHistory();
- if (history.isEmpty()) {
- return;
- }
- Message last = history.get(history.size() - 1);
- if (last instanceof ToolResponseMessage trm) {
- for (ToolResponseMessage.ToolResponse resp : trm.getResponses()) {
- String data = unwrapToolData(resp.responseData());
- String summary = data.length() > 500 ? data.substring(0, 500) + "…" : data;
- sink.tryEmitNext(new AgentEvent.ToolResult(resp.name(), summary, true));
- }
- }
- }
-
- /** 把 assistant 本轮发起的 tool_call 列表序列化成 JSON 落库;无工具调用返回 null */
- private String toolCallsToJson(AssistantMessage assistant) {
- if (assistant.getToolCalls() == null || assistant.getToolCalls().isEmpty()) {
- return null;
- }
- try {
- return objectMapper.writeValueAsString(assistant.getToolCalls());
- } catch (Exception e) {
- // 序列化失败不影响主流程,落个占位串即可
- return "[\"tool_calls 序列化失败\"]";
- }
- }
-
- /** 落一条 assistant 消息:文字 + 本轮 tool_calls(两者可同时为空/有值) */
- private void persistAssistant(Long sessionId, AssistantMessage assistant) {
- messageService.saveAssistant(sessionId, assistant.getText(), toolCallsToJson(assistant));
- }
-
- /**
- * 框架的 resp.responseData() 是 JSON 序列化后的字符串(带外层引号、\n 被转义成 \\n)。
- * 推给前端、落库前先反序列化成干净文本,避免截断切掉结尾引号导致前端解析失败。
- */
- private String unwrapToolData(String data) {
- if (data == null) {
- return "";
- }
- if (data.startsWith("\"")) {
- try {
- return objectMapper.readValue(data, String.class);
- } catch (Exception e) {
- // 解析失败就用原文,至少不丢内容
- return data;
- }
- }
- return data;
- }
-
- /** 把一批工具结果(执行结果 / 拒绝结果)逐条落 t_message */
- private void persistToolResponses(Long sessionId, List responses) {
- for (ToolResponseMessage.ToolResponse resp : responses) {
- messageService.saveToolResult(sessionId, resp.id(), unwrapToolData(resp.responseData()));
- }
- }
-
- /** 从框架执行后的会话历史末尾抽取本轮工具结果落库(数据源同 emitToolResults) */
- private void persistLastToolResponses(Long sessionId, ToolExecutionResult execResult) {
- List history = execResult.conversationHistory();
- if (history.isEmpty()) {
- return;
- }
- Message last = history.get(history.size() - 1);
- if (last instanceof ToolResponseMessage trm) {
- persistToolResponses(sessionId, trm.getResponses());
- }
- }
-
- /**
- * 对本轮所有 tool_call 做门禁预检。
- * 返回被拒绝的 tool_call 对应的"拒绝"工具结果;空列表表示全部放行。
- *
- * 注意:只要有一个被拒,本轮就整批不执行(受框架整批执行限制)。所以这里把
- * 被拒的攒成拒绝结果,放行的不在这里执行——交给外层 executeToolCalls 统一跑。
- */
- private List screen(Long sessionId, AssistantMessage assistant,
- ConfirmationHandler confirmer,
- Consumer onBlocked) {
- List rejected = new ArrayList<>();
-
- for (AssistantMessage.ToolCall call : assistant.getToolCalls()) {
- // 非 execCommand 的工具(读文件/看日志)只读,直接放行
- if (!EXEC_TOOL.equals(call.name())) {
- continue;
- }
-
- String command = extractCommand(call.arguments());
- CommandGuard.Verdict verdict = guard.evaluate(command);
-
- switch (verdict.decision()) {
- case DENY -> {
- // 拦截点审计:模型试图跑危险命令、被门禁拦下——最有价值的审计记录
- auditService.logBlocked(sessionId, command, true,
- "DENY: " + verdict.reason());
- if (onBlocked != null) {
- onBlocked.accept(new AgentEvent.Blocked(command, verdict.reason()));
- }
- rejected.add(reject(call,
- "命令被安全门禁拒绝执行(" + verdict.reason() + ")。请改用更安全的方式。"));
- }
- case ASK -> {
- boolean ok = confirmer.confirm(command, verdict.reason());
- if (!ok) {
- // 用户拒绝也记一笔(dangerous=true 因为是 ask 态命中副作用规则)
- auditService.logBlocked(sessionId, command, true,
- "用户拒绝: " + verdict.reason());
- if (onBlocked != null) {
- onBlocked.accept(new AgentEvent.Blocked(command, "用户拒绝: " + verdict.reason()));
- }
- rejected.add(reject(call, "用户拒绝执行该命令。请换一种方式或询问用户。"));
- }
- // 批准则不加入 rejected,留给外层执行(执行点会在 SshTools 落审计)
- }
- case ALLOW -> { /* 放行 */ }
- }
- }
- return rejected;
- }
-
- /** 从 tool_call 的 JSON 参数里取出 command 字段 */
- private String extractCommand(String argumentsJson) {
- try {
- var node = objectMapper.readTree(argumentsJson);
- var cmd = node.get("command");
- return cmd == null ? "" : cmd.asText();
- } catch (Exception e) {
- // 解析失败当空命令处理(门禁会放行,但实际执行会报错,模型自己会看到)
- return "";
- }
- }
-
- /** 构造一条"拒绝"工具结果,id/name 必须和原 tool_call 对上,模型才知道是哪一步被拒 */
- private ToolResponseMessage.ToolResponse reject(AssistantMessage.ToolCall call, String reason) {
- return new ToolResponseMessage.ToolResponse(call.id(), call.name(), reason);
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/ContextManager.java b/src/main/java/com/lowenssh/agent/ContextManager.java
deleted file mode 100644
index 14a386c..0000000
--- a/src/main/java/com/lowenssh/agent/ContextManager.java
+++ /dev/null
@@ -1,290 +0,0 @@
-package com.lowenssh.agent;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.ai.chat.messages.AssistantMessage;
-import org.springframework.ai.chat.messages.Message;
-import org.springframework.ai.chat.messages.SystemMessage;
-import org.springframework.ai.chat.messages.ToolResponseMessage;
-import org.springframework.ai.chat.messages.UserMessage;
-import org.springframework.ai.chat.model.ChatResponse;
-import org.springframework.ai.chat.prompt.Prompt;
-import org.springframework.ai.openai.OpenAiChatModel;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * 上下文管理 —— 防止 agentic loop 多轮滚下来把模型上下文撑爆。
- *
- * 抄 Claude Code 的思路,只做两层(其余 cache_edits / session memory 是为 prompt cache
- * 做的精细活,DeepSeek/GLM 场景 ROI 低,不做):
- *
- * Layer 0 —— 大工具结果截断:单条工具结果(cat 大文件、tail 海量日志)超阈值就截掉中段,
- * 只留头尾 + 一行截断提示。分级:最近 K 条用大阈值保细节,更早的用小阈值大力收紧,
- * 让 token 不随轮数线性膨胀。注意:完整内容我们本来就落了 t_message,
- * 截断只作用于"回灌给模型的副本",落库的仍是完整内容,可还原。
- *
- * Layer 4 —— 历史压缩:整段 messages 估算 token 超阈值时,把较早的对话丢给 LLM 摘要成一条,
- * 保留 system + 最近 K 条原文。硬约束:assistant 的 tool_call 和它的
- * tool_result 必须成对,切割点不能落在中间,否则 GLM 直接报错。
- * 摘要 LLM 连续失败到熔断阈值就停止压缩、裸跑兜底,避免摘要本身挂了拖死主流程。
- *
- * 阈值全部可配置(application 配置项 xwssh.context.*),方便联调时调小快速触发验证。
- * token 用字符数粗估:中英混合约 2.5 字符 / token,不引 tokenizer 依赖。
- */
-@Component
-public class ContextManager {
-
- private static final Logger log = LoggerFactory.getLogger(ContextManager.class);
-
- /** 字符数到 token 的粗估系数:中英混合约 2.5 字符 = 1 token */
- private static final double CHARS_PER_TOKEN = 2.5;
-
- private final OpenAiChatModel chatModel;
-
- /** Layer 0:最近 K 条内的工具结果保留的最大字符数,超出截断中段 */
- private final int toolResultMaxChars;
- /** Layer 0:更早(保留区之外)的工具结果用更小的阈值,旧命令输出大力收紧——token 不随轮数膨胀 */
- private final int oldToolResultMaxChars;
- /** Layer 4:整段上下文估算 token 超过此值触发压缩 */
- private final int maxContextTokens;
- /** Layer 4:压缩时保留最近多少条消息原文(不进摘要) */
- private final int keepRecentMessages;
- /** Layer 4:摘要 LLM 连续失败达到此次数后熔断,不再压缩 */
- private final int circuitLimit;
-
- /** 摘要 LLM 连续失败计数,成功清零;达到 circuitLimit 触发熔断 */
- private final AtomicInteger consecutiveFailures = new AtomicInteger(0);
-
- public ContextManager(
- OpenAiChatModel chatModel,
- @Value("${xwssh.context.tool-result-max-chars:8000}") int toolResultMaxChars,
- @Value("${xwssh.context.old-tool-result-max-chars:800}") int oldToolResultMaxChars,
- @Value("${xwssh.context.max-context-tokens:32000}") int maxContextTokens,
- @Value("${xwssh.context.keep-recent-messages:6}") int keepRecentMessages,
- @Value("${xwssh.context.circuit-limit:3}") int circuitLimit) {
- this.chatModel = chatModel;
- this.toolResultMaxChars = toolResultMaxChars;
- this.oldToolResultMaxChars = oldToolResultMaxChars;
- this.maxContextTokens = maxContextTokens;
- this.keepRecentMessages = keepRecentMessages;
- this.circuitLimit = circuitLimit;
- }
-
- // ============================ Layer 0:工具结果截断 ============================
-
- /**
- * 对历史里所有工具结果做截断(幂等:已截短的再跑也不变)。
- * 返回新列表,不改原列表。只重建超长的 ToolResponseMessage,其余消息原样保留。
- */
- /**
- * 对历史里所有工具结果做截断(幂等:已截短的再跑也不变)。
- * 返回新列表,不改原列表。只重建超长的 ToolResponseMessage,其余消息原样保留。
- *
- * 分级截断(省 token 核心 + 缓存友好):
- * - 最近 keepRecentMessages 条内的工具结果:用大阈值 toolResultMaxChars,保住当前推理需要的细节;
- * - 更早的工具结果:用小阈值 oldToolResultMaxChars 大力收紧——旧命令输出模型已读过、结论已在历史里,
- * 没必要每轮全量重发。这样 token 不随轮数线性膨胀。
- * 判定按"距末尾的距离"而非绝对下标:一旦某条进入"旧区",后续轮次它只会更旧,
- * 截断结果跨轮稳定 → 不破坏 GLM 上下文缓存前缀。
- */
- public List truncateToolResponses(List messages) {
- List result = new ArrayList<>(messages.size());
- int size = messages.size();
- for (int i = 0; i < size; i++) {
- Message msg = messages.get(i);
- if (msg instanceof ToolResponseMessage trm) {
- // 距末尾 keepRecentMessages 条以内算"近",用大阈值;否则算"旧",用小阈值
- boolean recent = (size - i) <= keepRecentMessages;
- int limit = recent ? toolResultMaxChars : oldToolResultMaxChars;
- result.add(truncateOne(trm, limit));
- } else {
- result.add(msg);
- }
- }
- return result;
- }
-
- /** 重建一条 ToolResponseMessage:把每个超长的 responseData 截掉中段 */
- private ToolResponseMessage truncateOne(ToolResponseMessage trm, int limit) {
- List truncated = new ArrayList<>();
- for (ToolResponseMessage.ToolResponse resp : trm.getResponses()) {
- String data = resp.responseData();
- truncated.add(new ToolResponseMessage.ToolResponse(
- resp.id(), resp.name(), truncateText(data, limit)));
- }
- return ToolResponseMessage.builder().responses(truncated).build();
- }
-
- /** 截断提示里的哨兵串:已含此串说明截过了,幂等跳过,避免二次截断 */
- private static final String TRUNCATE_MARKER = "完整结果见 t_message";
-
- /** 截掉中段,保留头 60% / 尾 40%,中间塞一行提示(指向 t_message 取完整内容) */
- private String truncateText(String text, int limit) {
- if (text == null || text.length() <= limit) {
- return text;
- }
- // 幂等:已经截过的(含哨兵)不再处理,否则头尾+提示可能仍超阈值被反复截。
- // 注意:旧区阈值比近区小,一条"近区截断版"挪到旧区后因含哨兵会保持原样,
- // 不会再按小阈值二次收紧——可接受:避免反复改写历史、保住缓存比再省几百字符更划算。
- if (text.contains(TRUNCATE_MARKER)) {
- return text;
- }
- int headLen = (int) (limit * 0.6);
- int tailLen = limit - headLen;
- int cut = text.length() - headLen - tailLen;
- String head = text.substring(0, headLen);
- String tail = text.substring(text.length() - tailLen);
- return head
- + "\n...[已截断 " + cut + " 字符," + TRUNCATE_MARKER + "]...\n"
- + tail;
- }
-
- // ============================ Layer 4:历史压缩 ============================
-
- /**
- * 估算上下文超阈值时压缩历史,否则原样返回。
- *
- * 切割策略:messages[0] 是 system,固定保留;尾部保留 keepRecentMessages 条原文;
- * 中间较早的对话渲染成纯文本丢给 LLM 摘要,压成一条 UserMessage 插在 system 之后。
- *
- * 成对约束:保留区开头不能是 ToolResponseMessage(否则它的 tool_call 落在摘要区被切走,
- * 模型见到孤儿 tool_result 会报错)。切割点往前移到对应 assistant,让两者一起进保留区。
- */
- public List compressIfNeeded(List messages) {
- // 熔断:摘要 LLM 连续挂了就别再试,裸跑兜底
- if (consecutiveFailures.get() >= circuitLimit) {
- return messages;
- }
- if (estimateTokens(messages) <= maxContextTokens) {
- return messages;
- }
- // 太短没什么可压(至少要有 system + 摘要区 + 保留区)
- if (messages.size() <= keepRecentMessages + 1) {
- return messages;
- }
-
- int cutIndex = messages.size() - keepRecentMessages;
- // 把切割点往前挪,避免保留区以孤儿 tool_result 开头
- while (cutIndex > 1 && messages.get(cutIndex) instanceof ToolResponseMessage) {
- cutIndex--;
- }
- // 挪到头了说明摘要区为空,没东西可压
- if (cutIndex <= 1) {
- return messages;
- }
-
- List summaryRegion = messages.subList(1, cutIndex);
- String summary = summarize(summaryRegion);
- if (summary == null) {
- // 摘要失败:计数 +1,本轮放弃压缩,原样返回
- int fails = consecutiveFailures.incrementAndGet();
- log.warn("历史摘要失败(连续 {} 次),本轮跳过压缩", fails);
- return messages;
- }
- consecutiveFailures.set(0); // 成功清零
-
- List compressed = new ArrayList<>();
- compressed.add(messages.get(0)); // system
- compressed.add(new UserMessage("以下是早先对话的摘要,供你继续任务时参考:\n" + summary));
- compressed.addAll(messages.subList(cutIndex, messages.size())); // 最近 K 条原文
-
- log.info("上下文压缩:{} 条 -> {} 条(摘要了 {} 条)",
- messages.size(), compressed.size(), summaryRegion.size());
- return compressed;
- }
-
- /** 调摘要 LLM 把一段历史压成结论文本;失败返回 null(由调用方走熔断逻辑) */
- private String summarize(List region) {
- try {
- String rendered = renderRegion(region);
- // 摘要请求不带任何工具,纯文本进纯文本出,避免又触发 tool_call
- List prompt = List.of(
- new SystemMessage(SUMMARY_PROMPT),
- new UserMessage(rendered));
- ChatResponse resp = chatModel.call(new Prompt(prompt));
- if (resp == null || resp.getResult() == null) {
- return null;
- }
- String text = resp.getResult().getOutput().getText();
- return (text == null || text.isBlank()) ? null : text;
- } catch (Exception e) {
- log.warn("摘要 LLM 调用异常: {}", e.getMessage());
- return null;
- }
- }
-
- private static final String SUMMARY_PROMPT = """
- 你是上下文压缩器。下面是一段 AI 运维助手与目标服务器之间的历史对话(含用户任务、助手发起的命令调用、命令执行结果)。
- 请把它压缩成简洁的中文摘要,必须保留以下信息,丢弃冗长的原始命令输出(只留结论):
- 1. 用户的原始运维目标;
- 2. 已执行过的关键命令及其结果结论(例如磁盘占用多少、进程是否存活、配置是否正确);
- 3. 已发现的问题或系统状态;
- 4. 被安全门禁拦截的危险操作(如果有)。
- 只输出摘要正文,不要解释你在做什么。
- """;
-
- /** 把一段消息渲染成纯文本喂给摘要 LLM(按角色标注,工具调用/结果也转成可读文本) */
- private String renderRegion(List region) {
- StringBuilder sb = new StringBuilder();
- for (Message msg : region) {
- if (msg instanceof UserMessage um) {
- sb.append("用户: ").append(um.getText()).append("\n");
- } else if (msg instanceof AssistantMessage am) {
- if (am.getText() != null && !am.getText().isBlank()) {
- sb.append("助手: ").append(am.getText()).append("\n");
- }
- if (am.getToolCalls() != null) {
- for (AssistantMessage.ToolCall call : am.getToolCalls()) {
- sb.append("助手调用工具 ").append(call.name())
- .append(": ").append(call.arguments()).append("\n");
- }
- }
- } else if (msg instanceof ToolResponseMessage trm) {
- for (ToolResponseMessage.ToolResponse resp : trm.getResponses()) {
- sb.append("工具[").append(resp.name()).append("]结果: ")
- .append(resp.responseData()).append("\n");
- }
- }
- }
- return sb.toString();
- }
-
- // ============================ 工具方法 ============================
-
- /** 估算整段消息的 token 数(字符数粗估,不引 tokenizer) */
- public int estimateTokens(List messages) {
- long chars = 0;
- for (Message msg : messages) {
- chars += messageChars(msg);
- }
- return (int) (chars / CHARS_PER_TOKEN);
- }
-
- /** 单条消息的字符量:取其文本 + 工具调用参数 + 工具结果内容 */
- private long messageChars(Message msg) {
- if (msg instanceof ToolResponseMessage trm) {
- long n = 0;
- for (ToolResponseMessage.ToolResponse resp : trm.getResponses()) {
- String d = resp.responseData();
- n += d == null ? 0 : d.length();
- }
- return n;
- }
- if (msg instanceof AssistantMessage am) {
- long n = am.getText() == null ? 0 : am.getText().length();
- if (am.getToolCalls() != null) {
- for (AssistantMessage.ToolCall call : am.getToolCalls()) {
- n += call.arguments() == null ? 0 : call.arguments().length();
- }
- }
- return n;
- }
- String t = msg.getText();
- return t == null ? 0 : t.length();
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/HostController.java b/src/main/java/com/lowenssh/agent/HostController.java
deleted file mode 100644
index b1331da..0000000
--- a/src/main/java/com/lowenssh/agent/HostController.java
+++ /dev/null
@@ -1,110 +0,0 @@
-package com.lowenssh.agent;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.lowenssh.persistence.entity.HostEntity;
-import com.lowenssh.persistence.mapper.HostMapper;
-import com.lowenssh.ssh.SshClient;
-import com.lowenssh.util.CryptoUtil;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.List;
-
-/**
- * 主机簿接口 —— 管理常用服务器 + 进入主机时建立连接。
- *
- * - GET /api/hosts 列出主机(不回传密码)
- * - POST /api/hosts 新增主机(密码 AES 加密落库)
- * - DELETE /api/hosts/{id} 删除主机
- * - POST /api/hosts/{id}/connect 进入主机:解密密码 → 建/复用常驻连接 → 回 sessionId
- *
- * 密码只在 connect 时解密用一次去连 SSH,不回传前端、不落明文。
- */
-@RestController
-public class HostController {
-
- private final HostMapper hostMapper;
- private final SessionManager sessionManager;
- private final CryptoUtil crypto;
-
- public HostController(HostMapper hostMapper, SessionManager sessionManager, CryptoUtil crypto) {
- this.hostMapper = hostMapper;
- this.sessionManager = sessionManager;
- this.crypto = crypto;
- }
-
- /** 列主机,按更新时间倒序。hasPassword 标记是否存了密码(迁移出的老主机没有,前端提示补填)。 */
- @GetMapping("/api/hosts")
- public List list() {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
- .orderByDesc(HostEntity::getUpdatedAt)
- .orderByDesc(HostEntity::getId);
- return hostMapper.selectList(wrapper).stream()
- .map(h -> new HostDto.HostItem(
- h.getId(), h.getAlias(), h.getSshHost(), h.getSshPort(), h.getSshUser(),
- h.getPasswordEnc() != null && !h.getPasswordEnc().isBlank()))
- .toList();
- }
-
- /** 新增主机:密码加密存。返回新主机 id。 */
- @PostMapping("/api/hosts")
- public HostDto.HostItem create(@RequestBody HostDto.CreateRequest req) {
- HostEntity h = new HostEntity();
- h.setAlias(req.alias());
- h.setSshHost(req.host());
- h.setSshPort(req.port() == null || req.port() == 0 ? 22 : req.port());
- h.setSshUser(req.user());
- h.setPasswordEnc(crypto.encrypt(req.password())); // 明文不落库
- hostMapper.insert(h);
- return new HostDto.HostItem(h.getId(), h.getAlias(), h.getSshHost(), h.getSshPort(),
- h.getSshUser(), h.getPasswordEnc() != null);
- }
-
- /** 删除主机(历史会话仍在库里,只是从主机簿移除入口) */
- @DeleteMapping("/api/hosts/{id}")
- public ResponseEntity delete(@PathVariable("id") Long id) {
- hostMapper.deleteById(id);
- return ResponseEntity.noContent().build();
- }
-
- /**
- * 进入主机:解密密码连一次 SSH(或复用该主机已活预连接),只建连不落库。
- * 会话行延迟到首条任务才建(lazy create),所以这里返回的 sessionId 恒为 null。
- * 没存密码(迁移出的老主机)则要求前端带 password 进来补连。
- */
- @PostMapping("/api/hosts/{id}/connect")
- public ResponseEntity> connect(@PathVariable("id") Long id,
- @RequestBody(required = false) HostDto.ConnectRequest req) {
- HostEntity h = hostMapper.selectById(id);
- if (h == null) {
- return ResponseEntity.status(404).body(new HostDto.ConnectResult(null, "主机不存在"));
- }
- // 优先用库里存的密码;没存则用前端补填的
- String password;
- try {
- String stored = crypto.decrypt(h.getPasswordEnc());
- password = (stored != null && !stored.isBlank())
- ? stored
- : (req == null ? null : req.password());
- } catch (Exception e) {
- return ResponseEntity.status(500).body(new HostDto.ConnectResult(null, "密码解密失败,请重新保存主机密码"));
- }
- if (password == null || password.isBlank()) {
- return ResponseEntity.status(400).body(new HostDto.ConnectResult(null, "该主机未保存密码,请补填密码后连接"));
- }
-
- try {
- sessionManager.connectHost(
- id, h.getSshHost(), h.getSshPort() == null ? 22 : h.getSshPort(), h.getSshUser(), password);
- // 只建连不落库,sessionId 留给首条任务时 attach;这里回 null 表示「连上了,等首条任务」
- return ResponseEntity.ok(new HostDto.ConnectResult(null, null));
- } catch (Exception e) {
- return ResponseEntity.status(502).body(new HostDto.ConnectResult(null, "SSH 连接失败: " + e.getMessage()));
- }
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/HostDto.java b/src/main/java/com/lowenssh/agent/HostDto.java
deleted file mode 100644
index ec05995..0000000
--- a/src/main/java/com/lowenssh/agent/HostDto.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.lowenssh.agent;
-
-/**
- * 主机簿相关 DTO。密码绝不出现在响应里(只在 connect 入参里临时补填)。
- */
-public final class HostDto {
-
- private HostDto() {
- }
-
- /** 主机列表项 / 新增响应。hasPassword 表示库里是否已存密码(false 则连接时需补填)。 */
- public record HostItem(Long id, String alias, String host, Integer port, String user, boolean hasPassword) {
- }
-
- /** 新增主机请求 */
- public record CreateRequest(String alias, String host, Integer port, String user, String password) {
- }
-
- /** 进入主机连接请求:库里没存密码时带上明文补连,否则可不传 */
- public record ConnectRequest(String password) {
- }
-
- /** 连接结果:成功带 sessionId,失败带 error */
- public record ConnectResult(Long sessionId, String error) {
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/HostMetrics.java b/src/main/java/com/lowenssh/agent/HostMetrics.java
deleted file mode 100644
index a7c2ef7..0000000
--- a/src/main/java/com/lowenssh/agent/HostMetrics.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.lowenssh.agent;
-
-/**
- * 远程主机监控指标快照(一次采集的结果)。
- * 字段都是采集瞬间的值,CPU 使用率由两次 /proc/stat 采样差值算得。
- */
-public record HostMetrics(
- double cpuPercent, // CPU 使用率 %(0~100)
- int cpuCores, // 核数
- double load1, // 1 分钟负载
- double load5, // 5 分钟负载
- double load15, // 15 分钟负载
- long memTotalKb, // 内存总量 KB
- long memUsedKb, // 已用内存 KB(total - available)
- double memPercent, // 内存使用率 %
- long diskTotalKb, // 根分区总量 KB
- long diskUsedKb, // 根分区已用 KB
- double diskPercent, // 根分区使用率 %
- long uptimeSec // 开机时长(秒)
-) {}
diff --git a/src/main/java/com/lowenssh/agent/MetricsCollector.java b/src/main/java/com/lowenssh/agent/MetricsCollector.java
deleted file mode 100644
index acd7d28..0000000
--- a/src/main/java/com/lowenssh/agent/MetricsCollector.java
+++ /dev/null
@@ -1,170 +0,0 @@
-package com.lowenssh.agent;
-
-import com.lowenssh.ssh.ExecResult;
-import com.lowenssh.ssh.SshClient;
-
-/**
- * 主机指标采集器:用一条复合 shell 命令把 CPU/内存/磁盘/负载/uptime 的原始数据一次取回,
- * 在 Java 端解析,避免多次 SSH 往返。
- *
- * CPU 使用率需要两次 /proc/stat 采样求差:命令里 sleep 0.3 取前后两行,
- * 解析时算 (busyΔ / totalΔ) * 100。
- *
- * 输出用标记行分隔,逐段解析,避免被 locale / 多余空白干扰。
- */
-public final class MetricsCollector {
-
- private MetricsCollector() {}
-
- // 一条命令取全部原始指标。各段用 ===TAG=== 包起来,Java 端按标记切分。
- private static final String CMD = String.join(" ; ",
- "echo ===CPU1===", "cat /proc/stat | grep '^cpu '",
- "sleep 0.3",
- "echo ===CPU2===", "cat /proc/stat | grep '^cpu '",
- "echo ===CORES===", "nproc",
- "echo ===LOAD===", "cat /proc/loadavg",
- "echo ===MEM===", "cat /proc/meminfo | grep -E '^(MemTotal|MemAvailable):'",
- "echo ===DISK===", "df -k / | tail -1",
- "echo ===UPTIME===", "cat /proc/uptime"
- );
-
- /** 在已加锁的 SshClient 上采集一次。调用方负责 lock。 */
- public static HostMetrics collect(SshClient ssh) throws Exception {
- ExecResult r = ssh.exec(CMD);
- if (!r.isSuccess()) {
- throw new IllegalStateException("采集失败 exit=" + r.exitCode() + " " + r.stderr());
- }
- return parse(r.stdout());
- }
-
- // —— 解析 ——
- static HostMetrics parse(String out) {
- String[] lines = out.split("\n");
- // 先把各标记段的内容收集起来
- String cpu1 = null, cpu2 = null, cores = null, load = null, disk = null, uptime = null;
- String memTotal = null, memAvail = null;
- String tag = "";
- for (String raw : lines) {
- String line = raw.trim();
- if (line.startsWith("===") && line.endsWith("===")) {
- tag = line;
- continue;
- }
- if (line.isEmpty()) continue;
- switch (tag) {
- case "===CPU1===" -> cpu1 = line;
- case "===CPU2===" -> cpu2 = line;
- case "===CORES===" -> cores = line;
- case "===LOAD===" -> load = line;
- case "===MEM===" -> {
- if (line.startsWith("MemTotal")) memTotal = line;
- else if (line.startsWith("MemAvailable")) memAvail = line;
- }
- case "===DISK===" -> disk = line;
- case "===UPTIME===" -> uptime = line;
- default -> { /* 忽略 */ }
- }
- }
-
- double cpuPercent = parseCpu(cpu1, cpu2);
- int cpuCores = parseInt(cores, 1);
-
- double[] loads = parseLoad(load);
-
- long memTotalKb = parseMemKb(memTotal);
- long memAvailKb = parseMemKb(memAvail);
- long memUsedKb = Math.max(0, memTotalKb - memAvailKb);
- double memPercent = memTotalKb > 0 ? memUsedKb * 100.0 / memTotalKb : 0;
-
- long[] diskKb = parseDisk(disk); // [total, used]
- double diskPercent = diskKb[0] > 0 ? diskKb[1] * 100.0 / diskKb[0] : 0;
-
- long uptimeSec = parseUptime(uptime);
-
- return new HostMetrics(
- round1(cpuPercent), cpuCores,
- loads[0], loads[1], loads[2],
- memTotalKb, memUsedKb, round1(memPercent),
- diskKb[0], diskKb[1], round1(diskPercent),
- uptimeSec
- );
- }
-
- // /proc/stat 行:cpu user nice system idle iowait irq softirq steal guest guest_nice
- // 使用率 = (totalΔ - idleΔ) / totalΔ * 100,idle = idle + iowait
- private static double parseCpu(String l1, String l2) {
- if (l1 == null || l2 == null) return 0;
- long[] a = cpuFields(l1);
- long[] b = cpuFields(l2);
- if (a == null || b == null) return 0;
- long idleA = a[3] + (a.length > 4 ? a[4] : 0);
- long idleB = b[3] + (b.length > 4 ? b[4] : 0);
- long totalA = sum(a), totalB = sum(b);
- long totalD = totalB - totalA, idleD = idleB - idleA;
- if (totalD <= 0) return 0;
- double pct = (totalD - idleD) * 100.0 / totalD;
- return clamp(pct);
- }
-
- private static long[] cpuFields(String line) {
- // 去掉开头的 "cpu" 标签
- String[] p = line.split("\\s+");
- if (p.length < 5) return null;
- long[] v = new long[p.length - 1];
- for (int i = 1; i < p.length; i++) {
- v[i - 1] = parseLong(p[i], 0);
- }
- return v;
- }
-
- // /proc/loadavg: "0.00 0.01 0.05 1/123 4567"
- private static double[] parseLoad(String line) {
- double[] d = {0, 0, 0};
- if (line == null) return d;
- String[] p = line.split("\\s+");
- for (int i = 0; i < 3 && i < p.length; i++) d[i] = parseDouble(p[i], 0);
- return d;
- }
-
- // "MemTotal: 16331756 kB"
- private static long parseMemKb(String line) {
- if (line == null) return 0;
- String[] p = line.split("\\s+");
- if (p.length < 2) return 0;
- return parseLong(p[1], 0);
- }
-
- // df -k / 末行: "/dev/vda1 41152736 8765432 30293560 23% /"
- private static long[] parseDisk(String line) {
- long[] r = {0, 0};
- if (line == null) return r;
- String[] p = line.split("\\s+");
- if (p.length >= 4) {
- r[0] = parseLong(p[1], 0); // total
- r[1] = parseLong(p[2], 0); // used
- }
- return r;
- }
-
- // /proc/uptime: "350735.47 234388.90",取第一个
- private static long parseUptime(String line) {
- if (line == null) return 0;
- String[] p = line.split("\\s+");
- return (long) parseDouble(p[0], 0);
- }
-
- // —— 小工具 ——
- private static long sum(long[] a) { long s = 0; for (long x : a) s += x; return s; }
- private static double clamp(double v) { return v < 0 ? 0 : (v > 100 ? 100 : v); }
- private static double round1(double v) { return Math.round(v * 10) / 10.0; }
-
- private static int parseInt(String s, int def) {
- try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; }
- }
- private static long parseLong(String s, long def) {
- try { return Long.parseLong(s.trim()); } catch (Exception e) { return def; }
- }
- private static double parseDouble(String s, double def) {
- try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; }
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/MonitorController.java b/src/main/java/com/lowenssh/agent/MonitorController.java
deleted file mode 100644
index bd436f8..0000000
--- a/src/main/java/com/lowenssh/agent/MonitorController.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.lowenssh.agent;
-
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.Map;
-
-/**
- * 远程主机监控接口(给人用):前端轮询拉一次快照,自己在内存里攒历史画趋势。
- *
- * - GET /api/monitor/{hostId}/metrics 采集一次 CPU/内存/磁盘/负载/uptime
- *
- * 复用主机常驻连接,lock 串行化,避免和 SFTP/Agent 抢同一 Session。
- */
-@RestController
-public class MonitorController {
-
- private final SessionManager sessionManager;
-
- public MonitorController(SessionManager sessionManager) {
- this.sessionManager = sessionManager;
- }
-
- @GetMapping("/api/monitor/{hostId}/metrics")
- public ResponseEntity> metrics(@PathVariable("hostId") Long hostId) {
- SessionManager.LiveSession ls = sessionManager.getByHost(hostId);
- if (ls == null) {
- return ResponseEntity.status(409).body(Map.of("error", "该主机未连接,请先从主机簿进入"));
- }
- ls.lock().lock();
- try {
- HostMetrics m = MetricsCollector.collect(ls.ssh());
- return ResponseEntity.ok(m);
- } catch (Exception e) {
- return ResponseEntity.status(500).body(Map.of("error", "采集失败: " + e.getMessage()));
- } finally {
- ls.lock().unlock();
- }
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/SessionDto.java b/src/main/java/com/lowenssh/agent/SessionDto.java
deleted file mode 100644
index 4bd7934..0000000
--- a/src/main/java/com/lowenssh/agent/SessionDto.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.lowenssh.agent;
-
-import java.util.List;
-
-/**
- * 左侧历史栏用的只读 DTO 集合。
- * 这些接口只查库 + 查常驻连接状态,不碰 SSH 执行,给前端会话列表/历史回看用。
- */
-public final class SessionDto {
-
- private SessionDto() {
- }
-
- /** 会话列表项:左栏每一行 */
- public record SessionItem(Long id, String title, String host, String user, Integer port, String updatedAt) {
- }
-
- /** 单条历史消息:转成前端能直接渲染的格式 */
- public record HistoryMessage(String type, String text, String name, String summary) {
- }
-
- /**
- * 点开某会话的完整回看数据:连接信息 + 历史消息 + 常驻连接是否还活着。
- * live=true 表示能直接续聊(复用常驻连接);false 则前端提示需重连。
- */
- public record SessionDetail(Long id, String host, Integer port, String user,
- boolean live, List messages) {
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/SessionManager.java b/src/main/java/com/lowenssh/agent/SessionManager.java
deleted file mode 100644
index 4d9b343..0000000
--- a/src/main/java/com/lowenssh/agent/SessionManager.java
+++ /dev/null
@@ -1,236 +0,0 @@
-package com.lowenssh.agent;
-
-import com.lowenssh.persistence.entity.SessionEntity;
-import com.lowenssh.persistence.mapper.SessionMapper;
-import com.lowenssh.ssh.SshClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * 会话管理器 —— 支撑多轮对话的「连接常驻」:
- * 进主机时连一次 SSH,把连接挂成「预连接」常驻,首条任务到来才落库建会话行(lazy create),
- * 后续轮复用同一连接(保留 cd 等上下文),会话结束或超时无活动才关闭。
- *
- * 为什么 lazy create:进主机就建会话行会堆出一堆没消息、没标题的空会话。改成
- * 「进主机只连不落库,首条任务才落库(title=任务)」,标题和消息天然落在同一个 sessionId 上。
- *
- * 两张表:
- * byHost —— 进主机建的预连接(hostId→连接),还没发首条任务,sessionId 仍为 null。
- * bySession —— 首条任务 attach 后的正式会话(sessionId→连接),续聊按它查。
- * 一条预连接首条任务后从 byHost 移出、登记进 bySession,不会同时在两张表里。
- *
- * 并发:SshClient 非线程安全。每个 LiveSession 自带一把锁,同一会话的多个请求串行执行。
- * 连接泄漏防护:@Scheduled 定时扫两张表,关掉超时无活动的连接。
- */
-@Component
-public class SessionManager {
-
- private static final Logger log = LoggerFactory.getLogger(SessionManager.class);
-
- private final SessionMapper sessionMapper;
-
- /** 会话空闲超时(分钟):超过这么久没活动的连接会被定时任务回收 */
- private final long idleTimeoutMinutes;
-
- /** hostId -> 进主机时建的预连接(已连 SSH、未发首条任务)。首条任务 attach 后移出。 */
- private final Map byHost = new ConcurrentHashMap<>();
-
- /** sessionId -> 已绑定会话的活连接,续聊按它查。 */
- private final Map bySession = new ConcurrentHashMap<>();
-
- public SessionManager(SessionMapper sessionMapper,
- @Value("${xwssh.agent.session-idle-timeout-minutes:30}") long idleTimeoutMinutes) {
- this.sessionMapper = sessionMapper;
- this.idleTimeoutMinutes = idleTimeoutMinutes;
- }
-
- /**
- * 一个活跃连接:SSH 连接 + 锁 + 最后活跃时间 + 建连时的连接信息(attach 落库要用)。
- * sessionId 未绑定会话前为 null(进主机已连,但还没发首条任务)。
- * lock 保证同一会话的请求串行(SshClient 非线程安全)。
- */
- public static class LiveSession {
- volatile Long sessionId; // 首条任务 attach 后回填
- final Long hostId; // 所属主机,进主机按它复用预连接
- final String host;
- final int port;
- final String user;
- final SshClient ssh;
- final ReentrantLock lock = new ReentrantLock();
- volatile Instant lastActiveAt = Instant.now();
-
- LiveSession(Long hostId, String host, int port, String user, SshClient ssh) {
- this.hostId = hostId;
- this.host = host;
- this.port = port;
- this.user = user;
- this.ssh = ssh;
- }
-
- public Long sessionId() {
- return sessionId;
- }
-
- public Long hostId() {
- return hostId;
- }
-
- public SshClient ssh() {
- return ssh;
- }
-
- public ReentrantLock lock() {
- return lock;
- }
-
- void touch() {
- this.lastActiveAt = Instant.now();
- }
- }
-
- /**
- * 进主机:复用该主机的预连接(若仍连通),否则连一次 SSH 建预连接。不落库。
- * 真正的会话行延迟到首条任务 attachSession 时才插,避免堆空会话。
- * hostId 为 null(curl 直连调试)时不进 byHost,连完直接返回。
- * 连接失败抛异常,调用方转成 error 事件。
- */
- public LiveSession connectHost(Long hostId, String host, int port, String user, String password) throws Exception {
- if (hostId != null) {
- LiveSession existing = byHost.get(hostId);
- if (existing != null && existing.ssh.isConnected()) {
- existing.touch();
- return existing; // 复用该主机现有预连接
- }
- }
- SshClient ssh = new SshClient();
- try {
- ssh.connect(host, port, user, password);
- } catch (Exception e) {
- ssh.close();
- throw e;
- }
- LiveSession live = new LiveSession(hostId, host, port, user, ssh);
- if (hostId != null) {
- byHost.put(hostId, live);
- }
- log.info("预连接已建立 hostId={} host={}", hostId, host);
- return live;
- }
-
- /** 取该主机进主机时建的预连接(尚未发首条任务);无或已断返回 null。 */
- public LiveSession getByHost(Long hostId) {
- if (hostId == null) {
- return null;
- }
- LiveSession live = byHost.get(hostId);
- if (live != null && live.ssh.isConnected()) {
- live.touch();
- return live;
- }
- return null;
- }
-
- /**
- * 首条任务:把预连接升级为正式会话 —— 落库拿 sessionId(title=首条任务),
- * 回填到 live、移出 byHost、登记进 bySession。返回 sessionId。
- */
- public Long attachSession(LiveSession live, String task) {
- SessionEntity session = new SessionEntity();
- session.setHostId(live.hostId);
- session.setTitle(toTitle(task)); // 标题是会话名摘要,截断防超列长(title VARCHAR(255))
- session.setSshHost(live.host);
- session.setSshPort(live.port);
- session.setSshUser(live.user);
- sessionMapper.insert(session);
- Long sessionId = session.getId();
-
- live.sessionId = sessionId;
- if (live.hostId != null) {
- byHost.remove(live.hostId, live); // 出预连接槽(仅当仍是当前预连接)
- }
- bySession.put(sessionId, live);
- log.info("会话已绑定 sessionId={} hostId={} 当前活跃会话数={}", sessionId, live.hostId, bySession.size());
- return sessionId;
- }
-
- /** 任务文本压成会话标题:取首行、超 40 字截断加省略号,远小于 title 列上限避免落库截断报错 */
- public static String toTitle(String task) {
- if (task == null) return "新会话";
- String t = task.strip();
- int nl = t.indexOf('\n');
- if (nl >= 0) t = t.substring(0, nl).strip();
- if (t.isEmpty()) return "新会话";
- return t.length() > 40 ? t.substring(0, 40) + "…" : t;
- }
-
- /**
- * 续聊:取已绑定会话的常驻连接。
- * 返回 null 表示会话不存在或已过期(前端据此提示重新连接)。
- */
- public LiveSession get(Long sessionId) {
- LiveSession live = bySession.get(sessionId);
- if (live == null) {
- return null;
- }
- // 连接可能已被对端断开,校验一下
- if (!live.ssh.isConnected()) {
- log.warn("会话连接已断开 sessionId={},移除", sessionId);
- close(sessionId);
- return null;
- }
- live.touch();
- return live;
- }
-
- /** 关闭并移除一个会话(显式结束 / 连接失效时调用) */
- public void close(Long sessionId) {
- LiveSession live = bySession.remove(sessionId);
- if (live != null) {
- live.ssh.close();
- if (live.hostId != null) {
- byHost.remove(live.hostId, live);
- }
- log.info("会话已关闭 sessionId={} 剩余活跃会话数={}", sessionId, bySession.size());
- }
- }
-
- /** 定时回收超时无活动的连接(含从未发任务的预连接),防连接泄漏。每 5 分钟扫一次。 */
- @Scheduled(fixedDelay = 5 * 60 * 1000)
- public void reapIdleSessions() {
- Instant deadline = Instant.now().minus(Duration.ofMinutes(idleTimeoutMinutes));
- int reaped = reap(byHost, deadline) + reap(bySession, deadline);
- if (reaped > 0) {
- log.info("回收超时连接 {} 个,剩余活跃会话 {} 个", reaped, bySession.size());
- }
- }
-
- /** 扫一张表,关掉超时或已断开的连接 */
- private int reap(Map map, Instant deadline) {
- Iterator> it = map.entrySet().iterator();
- int reaped = 0;
- while (it.hasNext()) {
- LiveSession live = it.next().getValue();
- if (live.lastActiveAt.isBefore(deadline) || !live.ssh.isConnected()) {
- live.ssh.close();
- it.remove();
- reaped++;
- }
- }
- return reaped;
- }
-
- /** 当前活跃会话数(监控/测试用) */
- public int activeCount() {
- return bySession.size();
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/SftpController.java b/src/main/java/com/lowenssh/agent/SftpController.java
deleted file mode 100644
index 0e8b9a0..0000000
--- a/src/main/java/com/lowenssh/agent/SftpController.java
+++ /dev/null
@@ -1,153 +0,0 @@
-package com.lowenssh.agent;
-
-import com.lowenssh.ssh.RemoteFile;
-import org.springframework.core.io.InputStreamResource;
-import org.springframework.http.ContentDisposition;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.multipart.MultipartFile;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.Map;
-
-/**
- * SFTP 文件管理接口(给人用)—— 复用主机的常驻 SSH 连接开 sftp 通道,不重连。
- *
- * - GET /api/sftp/{hostId}/list?path=/xxx 列目录
- * - POST /api/sftp/{hostId}/upload 上传(multipart:file + path)
- * - GET /api/sftp/{hostId}/download?path=/xxx 下载(流式)
- * - DELETE /api/sftp/{hostId}/file?path=/xxx 删除文件
- * - POST /api/sftp/{hostId}/mkdir 建目录(body: {path})
- *
- * 关键:SFTP / Agent 命令 / 监控共用同一条 JSch Session,必须用 LiveSession.lock()
- * 串行化,否则 channel 会串数据。每个接口都在 lock 内操作。
- */
-@RestController
-public class SftpController {
-
- private final SessionManager sessionManager;
-
- public SftpController(SessionManager sessionManager) {
- this.sessionManager = sessionManager;
- }
-
- /** 取该主机的常驻连接,没连上返回 null */
- private SessionManager.LiveSession live(Long hostId) {
- return sessionManager.getByHost(hostId);
- }
-
- /** 列目录 */
- @GetMapping("/api/sftp/{hostId}/list")
- public ResponseEntity> list(@PathVariable("hostId") Long hostId,
- @RequestParam(value = "path", defaultValue = "/") String path) {
- SessionManager.LiveSession ls = live(hostId);
- if (ls == null) return notConnected();
- ls.lock().lock();
- try {
- List files = ls.ssh().listDir(path);
- return ResponseEntity.ok(Map.of("path", path, "files", files));
- } catch (Exception e) {
- return ResponseEntity.status(500).body(Map.of("error", "列目录失败: " + e.getMessage()));
- } finally {
- ls.lock().unlock();
- }
- }
-
- /** 上传文件到指定目录 */
- @PostMapping("/api/sftp/{hostId}/upload")
- public ResponseEntity> upload(@PathVariable("hostId") Long hostId,
- @RequestParam("file") MultipartFile file,
- @RequestParam("path") String dir) {
- SessionManager.LiveSession ls = live(hostId);
- if (ls == null) return notConnected();
- String remote = (dir.endsWith("/") ? dir : dir + "/") + file.getOriginalFilename();
- ls.lock().lock();
- try {
- ls.ssh().upload(file.getInputStream(), remote);
- return ResponseEntity.ok(Map.of("path", remote));
- } catch (Exception e) {
- return ResponseEntity.status(500).body(Map.of("error", "上传失败: " + e.getMessage()));
- } finally {
- ls.lock().unlock();
- }
- }
-
- /** 下载文件。先在 lock 内读进内存再返回,避免流式期间长期占着 lock。 */
- @GetMapping("/api/sftp/{hostId}/download")
- public ResponseEntity> download(@PathVariable("hostId") Long hostId,
- @RequestParam("path") String path) {
- SessionManager.LiveSession ls = live(hostId);
- if (ls == null) return notConnected();
- ByteArrayOutputStream buf = new ByteArrayOutputStream();
- ls.lock().lock();
- try {
- ls.ssh().download(path, buf);
- } catch (Exception e) {
- return ResponseEntity.status(500).body(Map.of("error", "下载失败: " + e.getMessage()));
- } finally {
- ls.lock().unlock();
- }
- String name = path.substring(path.lastIndexOf('/') + 1);
- byte[] data = buf.toByteArray();
- HttpHeaders headers = new HttpHeaders();
- headers.setContentDisposition(ContentDisposition.attachment().filename(name).build());
- return ResponseEntity.ok()
- .headers(headers)
- .contentType(MediaType.APPLICATION_OCTET_STREAM)
- .contentLength(data.length)
- .body(new InputStreamResource(new ByteArrayInputStream(data)));
- }
-
- /** 删除文件 */
- @DeleteMapping("/api/sftp/{hostId}/file")
- public ResponseEntity> delete(@PathVariable("hostId") Long hostId,
- @RequestParam("path") String path) {
- SessionManager.LiveSession ls = live(hostId);
- if (ls == null) return notConnected();
- ls.lock().lock();
- try {
- ls.ssh().deleteFile(path);
- return ResponseEntity.noContent().build();
- } catch (Exception e) {
- return ResponseEntity.status(500).body(Map.of("error", "删除失败: " + e.getMessage()));
- } finally {
- ls.lock().unlock();
- }
- }
-
- /** 新建目录 */
- @PostMapping("/api/sftp/{hostId}/mkdir")
- public ResponseEntity> mkdir(@PathVariable("hostId") Long hostId,
- @RequestBody Map body) {
- String path = body.get("path");
- if (path == null || path.isBlank()) {
- return ResponseEntity.status(400).body(Map.of("error", "path 不能为空"));
- }
- SessionManager.LiveSession ls = live(hostId);
- if (ls == null) return notConnected();
- ls.lock().lock();
- try {
- ls.ssh().mkdir(path);
- return ResponseEntity.ok(Map.of("path", path));
- } catch (Exception e) {
- return ResponseEntity.status(500).body(Map.of("error", "建目录失败: " + e.getMessage()));
- } finally {
- ls.lock().unlock();
- }
- }
-
- private ResponseEntity> notConnected() {
- return ResponseEntity.status(409).body(Map.of("error", "该主机未连接,请先从主机簿进入"));
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/SshTools.java b/src/main/java/com/lowenssh/agent/SshTools.java
deleted file mode 100644
index 1987ff8..0000000
--- a/src/main/java/com/lowenssh/agent/SshTools.java
+++ /dev/null
@@ -1,189 +0,0 @@
-package com.lowenssh.agent;
-
-import com.lowenssh.agent.guard.CommandGuard;
-import com.lowenssh.ssh.ExecResult;
-import com.lowenssh.ssh.RemoteFile;
-import com.lowenssh.ssh.SshClient;
-import com.lowenssh.persistence.AuditService;
-import org.springframework.ai.tool.annotation.Tool;
-import org.springframework.ai.tool.annotation.ToolParam;
-
-import java.util.List;
-import java.util.concurrent.locks.Lock;
-
-/**
- * Agent 的工具集 —— 会话级实例:一个 SshTools 绑定一台已连接的目标机。
- *
- * 为什么不做成 @Service 单例:连接信息是会话级的("这次会话操作哪台机"),
- * 单例工具没法持有"当前会话的连接"。所以每次发起一轮 agent 任务时 new 一个,
- * 把连好的 SshClient + 本次会话 id + 审计/门禁注进来,loop 结束随会话释放。
- *
- * 审计:execCommand 是真正下发命令的点,在这里记一笔 t_audit(执行点审计)。
- * 能到达这里的命令必经 AgentService.screen 放行,所以危险命令视为已确认。
- *
- * SFTP 工具(listFiles/deleteFile/makeDir/moveFile)走 ChannelSftp。写操作映射成
- * 等价 shell 命令(rm/mkdir/mv)过同一个 CommandGuard:DENY 直接拒,复用现有规则,
- * 审计可读。SFTP 与人工面板共用一条 Session,操作在 lock 内串行化(lock 可为 null,
- * 同步测试场景独占连接无需锁)。
- */
-public class SshTools {
-
- private final SshClient ssh;
- private final Long sessionId;
- private final AuditService auditService;
- private final CommandGuard guard;
- private final Lock lock; // 与人工 SFTP/监控串行化,可为 null(独占连接时)
-
- public SshTools(SshClient ssh, Long sessionId, AuditService auditService, CommandGuard guard) {
- this(ssh, sessionId, auditService, guard, null);
- }
-
- public SshTools(SshClient ssh, Long sessionId, AuditService auditService, CommandGuard guard, Lock lock) {
- this.ssh = ssh;
- this.sessionId = sessionId;
- this.auditService = auditService;
- this.guard = guard;
- this.lock = lock;
- }
-
- @Tool(description = "在目标服务器上执行一条 shell 命令,返回标准输出、错误输出和退出码。用于查看系统状态、进程、磁盘等运维操作。")
- public String execCommand(
- @ToolParam(description = "要执行的 shell 命令,例如 'df -h' 或 'ps aux | grep java'") String command) {
- // execCommand 是可写工具:审计要标注危险性。能执行到这说明已过门禁放行,
- // 危险命令(非 ALLOW)视为已确认(confirmed=true)。
- boolean dangerous = guard.evaluate(command).decision() != CommandGuard.Decision.ALLOW;
- return runAndAudit(command, dangerous, dangerous);
- }
-
- @Tool(description = "读取目标服务器上指定路径的文本文件的完整内容。")
- public String readRemoteFile(
- @ToolParam(description = "远程文件的绝对路径,例如 '/etc/nginx/nginx.conf'") String path) {
- // 只读工具:固定非危险、无需确认。单引号包裹防路径里的空格/特殊字符
- return runAndAudit("cat '" + path + "'", false, false);
- }
-
- @Tool(description = "读取目标服务器上日志文件的末尾若干行,用于快速查看最新日志。")
- public String tailLog(
- @ToolParam(description = "日志文件的绝对路径,例如 '/var/log/nginx/error.log'") String path,
- @ToolParam(description = "读取末尾的行数,例如 100") int lines) {
- return runAndAudit("tail -n " + lines + " '" + path + "'", false, false);
- }
-
- // —— SFTP 文件操作工具 ——
-
- @Tool(description = "列出目标服务器上指定目录的文件和子目录,返回每项的名称、是否目录、大小(字节)和权限。")
- public String listFiles(
- @ToolParam(description = "要列出的目录绝对路径,例如 '/var/log'") String path) {
- return withLock(() -> {
- try {
- List files = ssh.listDir(path);
- if (files.isEmpty()) return "(空目录)" + path;
- StringBuilder sb = new StringBuilder("目录 ").append(path).append(" 共 ")
- .append(files.size()).append(" 项:\n");
- for (RemoteFile f : files) {
- sb.append(f.isDir() ? "[d] " : "[f] ").append(f.name())
- .append(" ").append(f.isDir() ? "-" : f.size() + "B")
- .append(" ").append(f.perms()).append("\n");
- }
- return sb.toString();
- } catch (Exception e) {
- return "列目录失败: " + e.getMessage();
- }
- });
- }
-
- @Tool(description = "删除目标服务器上的一个文件(不能删目录)。危险操作,会经过安全门禁审查。")
- public String deleteFile(
- @ToolParam(description = "要删除的文件绝对路径,例如 '/tmp/old.log'") String path) {
- // 映射成等价 rm 命令过门禁,复用现有删除规则
- return sftpWrite("rm '" + path + "'", () -> {
- ssh.deleteFile(path);
- return "已删除文件: " + path;
- });
- }
-
- @Tool(description = "在目标服务器上创建一个目录。会经过安全门禁审查。")
- public String makeDir(
- @ToolParam(description = "要创建的目录绝对路径,例如 '/opt/app/data'") String path) {
- return sftpWrite("mkdir '" + path + "'", () -> {
- ssh.mkdir(path);
- return "已创建目录: " + path;
- });
- }
-
- @Tool(description = "重命名或移动目标服务器上的文件/目录。会经过安全门禁审查。")
- public String moveFile(
- @ToolParam(description = "源路径绝对路径") String from,
- @ToolParam(description = "目标路径绝对路径") String to) {
- return sftpWrite("mv '" + from + "' '" + to + "'", () -> {
- ssh.rename(from, to);
- return "已移动: " + from + " -> " + to;
- });
- }
-
- /**
- * SFTP 写操作统一入口:先把等价 shell 命令过 CommandGuard,DENY 直接拒绝(复刻线上
- * AutoConfirmationHandler 语义:ASK 自动放行)。放行后在 lock 内执行 SFTP 动作并落审计。
- */
- private String sftpWrite(String equivCommand, SftpAction action) {
- CommandGuard.Verdict verdict = guard.evaluate(equivCommand);
- if (verdict.decision() == CommandGuard.Decision.DENY) {
- auditService.logBlocked(sessionId, equivCommand, true, "DENY: " + verdict.reason());
- return "操作被安全门禁拒绝(" + verdict.reason() + ")。请改用更安全的方式或询问用户。";
- }
- boolean dangerous = verdict.decision() != CommandGuard.Decision.ALLOW;
- return withLock(() -> {
- try {
- String msg = action.run();
- // SFTP 无 shell exitCode,成功即 0、失败走 catch
- auditService.logExecuted(sessionId, equivCommand,
- new ExecResult(msg, "", 0), dangerous, dangerous);
- return msg;
- } catch (Exception e) {
- auditService.logExecuted(sessionId, equivCommand,
- new ExecResult("", e.getMessage(), 1), dangerous, dangerous);
- return "操作失败: " + e.getMessage();
- }
- });
- }
-
- /** 在 lock 内执行(lock 为 null 时直接执行),与人工 SFTP/监控串行化共用一条 Session */
- private String withLock(java.util.function.Supplier body) {
- if (lock == null) return body.get();
- lock.lock();
- try {
- return body.get();
- } finally {
- lock.unlock();
- }
- }
-
- /** SFTP 动作:可抛异常,返回成功描述 */
- @FunctionalInterface
- private interface SftpAction {
- String run() throws Exception;
- }
-
- /**
- * 执行命令 → 落审计 → 把三件套格式化成文本喂回模型。
- * 模型靠这段文本判断命令成败、决定下一步,所以 exitCode 和 stderr 都要明确带上。
- */
- private String runAndAudit(String command, boolean dangerous, boolean confirmed) {
- try {
- ExecResult r = ssh.exec(command);
- auditService.logExecuted(sessionId, command, r, dangerous, confirmed);
- StringBuilder sb = new StringBuilder();
- sb.append("exitCode=").append(r.exitCode()).append("\n");
- if (!r.stdout().isEmpty()) {
- sb.append("stdout:\n").append(r.stdout());
- }
- if (!r.stderr().isEmpty()) {
- sb.append("stderr:\n").append(r.stderr());
- }
- return sb.toString();
- } catch (Exception e) {
- // 工具内部异常不能抛给 loop,要作为"工具结果"回灌,让模型知道这步失败了
- return "命令执行异常: " + e.getMessage();
- }
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/guard/AutoConfirmationHandler.java b/src/main/java/com/lowenssh/agent/guard/AutoConfirmationHandler.java
deleted file mode 100644
index e3c2c8f..0000000
--- a/src/main/java/com/lowenssh/agent/guard/AutoConfirmationHandler.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.lowenssh.agent.guard;
-
-/**
- * 自动确认实现 —— REST/自动化测试场景用:ask 态默认放行。
- *
- * 注意:这不削弱安全。deny 态命令在门禁那层就被拦死了,根本到不了这里;
- * 这里只处理 ask 态("可疑但不致命"),自动场景下选择放行以便自动化跑通。
- * 真要人盯着的高危场景用 ConsoleConfirmationHandler 走真人 y/n。
- */
-public class AutoConfirmationHandler implements ConfirmationHandler {
-
- @Override
- public boolean confirm(String command, String reason) {
- // 自动放行 ask 态命令(deny 已在门禁拦截)
- return true;
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/guard/CommandGuard.java b/src/main/java/com/lowenssh/agent/guard/CommandGuard.java
deleted file mode 100644
index 6a194cf..0000000
--- a/src/main/java/com/lowenssh/agent/guard/CommandGuard.java
+++ /dev/null
@@ -1,123 +0,0 @@
-package com.lowenssh.agent.guard;
-
-import org.springframework.stereotype.Component;
-
-import java.util.List;
-import java.util.regex.Pattern;
-
-/**
- * 命令门禁 —— deny / ask / allow 三态判定。Agent 安全的硬边界。
- *
- * 设计原则(抄 Claude Code 并落地):
- * 1. 安全检查是独立代码路径,不写进工具方法、不靠模型自觉。模型越狱也绕不过这层。
- * 2. 三态评估顺序固定:先查 deny(命中即拒,deny 永远赢)→ 再看是否需 ask → 默认 allow。
- * 3. 只看"实际要执行的命令",不看模型的话术,防花言巧语骗过门禁。
- * 4. 复合命令(&& | ; 串起来的)拆开逐段查,防"ls && rm -rf /"整条被当成一段漏过。
- *
- * 判定结果是纯函数,无副作用,方便单测。
- */
-@Component
-public class CommandGuard {
-
- /** 三态 */
- public enum Decision { DENY, ASK, ALLOW }
-
- /** 判定结果:状态 + 原因(原因用于回灌给模型 / 展示给用户) */
- public record Verdict(Decision decision, String reason) {
- }
-
- /**
- * deny 名单:不可逆的毁灭性操作,直接拒绝,不给确认机会。
- * 用正则匹配,\b 保证匹配的是独立命令词而非子串(如 dd 不误伤 add)。
- */
- private static final List DENY = List.of(
- Pattern.compile("\\brm\\s+(-\\w*\\s+)*-\\w*[rf]"), // rm -rf / rm -fr 等带 r/f 组合
- Pattern.compile("\\bmkfs\\b"), // 格式化文件系统
- Pattern.compile("\\bdd\\b"), // 块设备读写,易毁盘
- Pattern.compile("\\bshutdown\\b"), // 关机
- Pattern.compile("\\breboot\\b"), // 重启
- Pattern.compile("\\bhalt\\b"), // 停机
- Pattern.compile(">\\s*/dev/sd"), // 直接写裸盘
- Pattern.compile(":\\(\\)\\s*\\{.*\\}"), // fork 炸弹 :(){ :|:& };:
- Pattern.compile("\\bmv\\s+.*\\s+/dev/null"), // mv 到 /dev/null 销毁数据
- // 真机联调发现:find 是 rm -rf 的等价绕过——模型被拦 rm -rf 后改用 find 删
- Pattern.compile("\\bfind\\b.*-delete"), // find ... -delete 批量删除
- Pattern.compile("\\bfind\\b.*-exec\\s+rm") // find ... -exec rm 批量删除
- );
-
- /**
- * ask 名单:有副作用但未必致命,执行前问一句。
- */
- private static final List ASK = List.of(
- Pattern.compile("\\brm\\b"), // 普通 rm(非 -rf,已被 deny 漏下来的)
- Pattern.compile("\\bkill\\b"), // 杀进程
- Pattern.compile("\\bsystemctl\\s+(stop|restart|disable)"), // 停/重启/禁用服务
- Pattern.compile("\\bservice\\s+\\S+\\s+(stop|restart)"),
- Pattern.compile("\\b(chmod|chown)\\b"), // 改权限/属主
- Pattern.compile("\\b(apt|apt-get|yum|dnf)\\s+(install|remove|purge)"), // 装/卸软件
- Pattern.compile("\\btruncate\\b"), // 清空文件
- Pattern.compile(">\\s*/") // 重定向覆盖写到绝对路径文件
- );
-
- /**
- * 判定一条命令。复合命令会被拆段,取最严结果(任一段 deny 则整条 deny)。
- */
- public Verdict evaluate(String command) {
- if (command == null || command.isBlank()) {
- return new Verdict(Decision.ALLOW, "空命令");
- }
-
- Decision worst = Decision.ALLOW;
- String worstReason = "";
-
- // 复合命令拆段:&& || | ; 都是命令分隔符
- for (String seg : splitSegments(command)) {
- String s = seg.trim();
- if (s.isEmpty()) continue;
-
- Verdict v = evaluateSingle(s);
- // 取最严:DENY > ASK > ALLOW(enum ordinal 越小越严)
- if (v.decision().ordinal() < worst.ordinal()) {
- worst = v.decision();
- worstReason = v.reason();
- }
- // 已经最严了,提前结束
- if (worst == Decision.DENY) break;
- }
-
- if (worst == Decision.ALLOW) {
- return new Verdict(Decision.ALLOW, "只读/安全命令");
- }
- return new Verdict(worst, worstReason);
- }
-
- /** 单段命令判定:先 deny 再 ask 后 allow */
- private Verdict evaluateSingle(String seg) {
- for (Pattern p : DENY) {
- if (p.matcher(seg).find()) {
- return new Verdict(Decision.DENY, "命中危险命令拦截规则: " + describe(p, seg));
- }
- }
- for (Pattern p : ASK) {
- if (p.matcher(seg).find()) {
- return new Verdict(Decision.ASK, "涉及有副作用的操作: " + describe(p, seg));
- }
- }
- return new Verdict(Decision.ALLOW, "");
- }
-
- /** 按命令分隔符拆段,分隔符本身丢弃 */
- private List splitSegments(String command) {
- // 用正则一次切掉 && || | ; 以及换行
- return List.of(command.split("&&|\\|\\||[|;\\n]"));
- }
-
- /** 给出命中片段,便于用户/模型理解为什么被拦 */
- private String describe(Pattern p, String seg) {
- var m = p.matcher(seg);
- if (m.find()) {
- return "'" + m.group() + "'";
- }
- return p.pattern();
- }
-}
diff --git a/src/main/java/com/lowenssh/agent/guard/ConfirmationHandler.java b/src/main/java/com/lowenssh/agent/guard/ConfirmationHandler.java
deleted file mode 100644
index 74cf688..0000000
--- a/src/main/java/com/lowenssh/agent/guard/ConfirmationHandler.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.lowenssh.agent.guard;
-
-/**
- * 人工确认抽象 —— ask 态命令在执行前问一句"干不干"。
- *
- * 为什么抽象成接口:不同入口的确认方式不一样。控制台走 System.in 真敲 y/n;
- * 将来 WebSocket 走前端弹窗。loop 只依赖这个接口,换入口不动核心逻辑。
- */
-public interface ConfirmationHandler {
-
- /**
- * 请求用户确认是否执行某条命令。
- *
- * @param command 待执行的完整命令
- * @param reason 为什么需要确认(门禁给出的原因,例如"涉及写操作 rm")
- * @return true=批准执行,false=拒绝
- */
- boolean confirm(String command, String reason);
-}
diff --git a/src/main/java/com/lowenssh/agent/guard/ConsoleConfirmationHandler.java b/src/main/java/com/lowenssh/agent/guard/ConsoleConfirmationHandler.java
deleted file mode 100644
index 95600a5..0000000
--- a/src/main/java/com/lowenssh/agent/guard/ConsoleConfirmationHandler.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.lowenssh.agent.guard;
-
-import java.util.Scanner;
-
-/**
- * 控制台确认实现 —— 前台运行时走 System.in,真人敲 y/n。
- *
- * 用于 CLI 交互入口(CommandLineRunner 模式)演示"执行前人工确认"这一刀。
- * Web 后台进程没有终端,别用这个,用 AutoConfirmationHandler。
- */
-public class ConsoleConfirmationHandler implements ConfirmationHandler {
-
- // System.in 全局只有一个,复用同一个 Scanner,别每次 new(会吃掉缓冲)
- private final Scanner scanner = new Scanner(System.in);
-
- @Override
- public boolean confirm(String command, String reason) {
- System.out.println("\n⚠️ 需要确认:" + reason);
- System.out.println(" 命令: " + command);
- System.out.print(" 执行吗?(y/n): ");
- String line = scanner.nextLine().trim().toLowerCase();
- return line.equals("y") || line.equals("yes");
- }
-}
diff --git a/src/main/java/com/lowenssh/persistence/AuditService.java b/src/main/java/com/lowenssh/persistence/AuditService.java
deleted file mode 100644
index 6709660..0000000
--- a/src/main/java/com/lowenssh/persistence/AuditService.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.lowenssh.persistence;
-
-import com.lowenssh.persistence.entity.AuditEntity;
-import com.lowenssh.persistence.mapper.AuditMapper;
-import com.lowenssh.ssh.ExecResult;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.stereotype.Service;
-
-/**
- * 审计服务 —— 每条命令落一笔 t_audit,可追溯。Agent 安全卖点的证据链。
- *
- * 记两类:
- * - 已执行:命令真落到服务器、带 stdout/stderr/exitCode({@link #logExecuted})
- * - 被拦截:deny 拒掉 / ask 被拒,没执行、无结果({@link #logBlocked})
- *
- * 铁律:审计失败绝不能拖垮主任务。所有写库 try/catch 兜住,只记日志不抛。
- */
-@Service
-public class AuditService {
-
- private static final Logger log = LoggerFactory.getLogger(AuditService.class);
-
- private final AuditMapper auditMapper;
-
- public AuditService(AuditMapper auditMapper) {
- this.auditMapper = auditMapper;
- }
-
- /** 记录一条已执行的命令(带执行结果) */
- public void logExecuted(Long sessionId, String command, ExecResult result,
- boolean dangerous, boolean confirmed) {
- AuditEntity e = new AuditEntity();
- e.setSessionId(sessionId);
- e.setCommand(command);
- e.setStdout(result.stdout());
- e.setStderr(result.stderr());
- e.setExitCode(result.exitCode());
- e.setDangerous(dangerous);
- e.setConfirmed(confirmed);
- save(e);
- }
-
- /** 记录一条被门禁拦截 / 用户拒绝的命令(未执行,exitCode 留空,原因记进 stderr) */
- public void logBlocked(Long sessionId, String command, boolean dangerous, String reason) {
- AuditEntity e = new AuditEntity();
- e.setSessionId(sessionId);
- e.setCommand(command);
- e.setStderr(reason); // 拦截原因借 stderr 字段存,便于审计查阅
- e.setDangerous(dangerous);
- e.setConfirmed(false); // 被拦截 = 未经确认放行
- save(e);
- }
-
- private void save(AuditEntity e) {
- try {
- auditMapper.insert(e);
- } catch (Exception ex) {
- // 审计写库失败不影响主流程,只告警
- log.warn("审计写库失败 command={}: {}", e.getCommand(), ex.getMessage());
- }
- }
-}
diff --git a/src/main/java/com/lowenssh/persistence/MessageService.java b/src/main/java/com/lowenssh/persistence/MessageService.java
deleted file mode 100644
index f8d7c56..0000000
--- a/src/main/java/com/lowenssh/persistence/MessageService.java
+++ /dev/null
@@ -1,252 +0,0 @@
-package com.lowenssh.persistence;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.lowenssh.persistence.entity.MessageEntity;
-import com.lowenssh.persistence.mapper.MessageMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.ai.chat.messages.AssistantMessage;
-import org.springframework.ai.chat.messages.Message;
-import org.springframework.ai.chat.messages.ToolResponseMessage;
-import org.springframework.ai.chat.messages.UserMessage;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * 对话消息服务 —— 把 agentic loop 的每轮消息落 t_message,按 session_id 可还原完整对话。
- *
- * 不落 system prompt:它每次固定,捞历史时代码里加回去即可,存了是冗余。
- *
- * 铁律同 AuditService:落库失败绝不能拖垮主任务,写库 try/catch 兜住,只告警不抛。
- */
-@Service
-public class MessageService {
-
- private static final Logger log = LoggerFactory.getLogger(MessageService.class);
-
- private final MessageMapper messageMapper;
- private final ObjectMapper objectMapper = new ObjectMapper();
-
- public MessageService(MessageMapper messageMapper) {
- this.messageMapper = messageMapper;
- }
-
- /** 落一条用户消息(loop 开始时的 task) */
- public void saveUser(Long sessionId, String content) {
- MessageEntity e = new MessageEntity();
- e.setSessionId(sessionId);
- e.setRole("user");
- e.setContent(content);
- save(e);
- }
-
- /**
- * 落一条 assistant 消息。
- * @param content 模型的文字回复(可能为空,纯工具调用时)
- * @param toolCalls 本轮发起的工具调用 JSON(无则传 null)
- */
- public void saveAssistant(Long sessionId, String content, String toolCalls) {
- MessageEntity e = new MessageEntity();
- e.setSessionId(sessionId);
- e.setRole("assistant");
- e.setContent(content);
- e.setToolCalls(toolCalls);
- save(e);
- }
-
- /**
- * 落一条工具结果消息(含被门禁拒绝的"拒绝结果",这样历史能还原"模型想跑啥被拦了")。
- * @param toolCallId 对应的工具调用 id
- * @param content 工具返回内容 / 拒绝原因
- */
- public void saveToolResult(Long sessionId, String toolCallId, String content) {
- MessageEntity e = new MessageEntity();
- e.setSessionId(sessionId);
- e.setRole("tool");
- e.setToolCallId(toolCallId);
- e.setContent(content);
- save(e);
- }
-
- private void save(MessageEntity e) {
- try {
- messageMapper.insert(e);
- } catch (Exception ex) {
- // 落库失败不影响主流程,只告警
- log.warn("消息写库失败 sessionId={} role={}: {}", e.getSessionId(), e.getRole(), ex.getMessage());
- }
- }
-
- /**
- * 按 sessionId 还原历史对话为 Spring AI 的 Message 列表(供多轮续聊回灌给模型)。
- *
- * 不含 system prompt(捞回去由 AgentService 自己加)。还原规则:
- * - user -> UserMessage
- * - assistant -> AssistantMessage(带 tool_calls 反序列化,id/name 必须和后续 tool 结果配对)
- * - tool -> ToolResponseMessage;同一轮可能有多条 tool 行,连续的 tool 合并进一个
- * ToolResponseMessage,符合 OpenAI 协议「一个 assistant.tool_calls 对应一组 tool 结果」
- *
- * 任一条解析失败只跳过该条,不拖垮续聊。
- */
- public List loadHistory(Long sessionId) {
- List messages = new ArrayList<>();
- if (sessionId == null) {
- return messages;
- }
-
- List rows;
- try {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
- .eq(MessageEntity::getSessionId, sessionId)
- .orderByAsc(MessageEntity::getId); // 按落库顺序还原
- rows = messageMapper.selectList(wrapper);
- } catch (Exception ex) {
- log.warn("加载历史失败 sessionId={}: {}", sessionId, ex.getMessage());
- return messages;
- }
-
- // 连续的 tool 行要合并成一个 ToolResponseMessage,先攒着,遇到非 tool 行再 flush
- List pendingTool = new ArrayList<>();
-
- for (MessageEntity row : rows) {
- String role = row.getRole();
- if ("tool".equals(role)) {
- // 工具结果先攒进 pending,name 历史没单独存,回灌不影响模型理解,用占位即可
- pendingTool.add(new ToolResponseMessage.ToolResponse(
- row.getToolCallId(), "", nullToEmpty(row.getContent())));
- continue;
- }
-
- // 遇到非 tool 行,先把攒着的工具结果 flush 成一条 ToolResponseMessage
- flushTool(messages, pendingTool);
-
- switch (role) {
- case "user" -> messages.add(new UserMessage(nullToEmpty(row.getContent())));
- case "assistant" -> messages.add(toAssistant(row));
- default -> { /* system 等不还原 */ }
- }
- }
- // 收尾 flush(历史以工具结果结尾的情况)
- flushTool(messages, pendingTool);
-
- return messages;
- }
-
- /**
- * 按 sessionId 把历史转成前端可直接渲染的消息列表(左栏点开旧会话回看用)。
- *
- * 与 loadHistory 不同:这里不是回灌给模型,而是给人看,所以:
- * - user -> {type:user, text}
- * - assistant -> 文字非空时产出 {type:assistant, text};带 tool_calls 时每个调用
- * 额外产出一条 {type:tool_call, name, summary=命令}(从 arguments 提取 command)
- * - tool -> {type:tool_result, summary=结果内容}
- *
- * 任一条解析失败只跳过该条,不影响整体回看。
- */
- public List loadHistoryForView(Long sessionId) {
- List out = new ArrayList<>();
- if (sessionId == null) {
- return out;
- }
-
- List rows;
- try {
- LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
- .eq(MessageEntity::getSessionId, sessionId)
- .orderByAsc(MessageEntity::getId);
- rows = messageMapper.selectList(wrapper);
- } catch (Exception ex) {
- log.warn("回看历史失败 sessionId={}: {}", sessionId, ex.getMessage());
- return out;
- }
-
- for (MessageEntity row : rows) {
- String role = row.getRole();
- switch (role == null ? "" : role) {
- case "user" -> out.add(new com.lowenssh.agent.SessionDto.HistoryMessage(
- "user", nullToEmpty(row.getContent()), null, null));
- case "assistant" -> {
- // 文字部分(纯工具调用时为空,不产出空气泡)
- String text = nullToEmpty(row.getContent());
- if (!text.isBlank()) {
- out.add(new com.lowenssh.agent.SessionDto.HistoryMessage(
- "assistant", text, null, null));
- }
- // 工具调用部分:每个 call 转一条 tool_call,展示命令
- appendToolCalls(out, row.getToolCalls());
- }
- case "tool" -> out.add(new com.lowenssh.agent.SessionDto.HistoryMessage(
- "tool_result", null, null, nullToEmpty(row.getContent())));
- default -> { /* system 等不回看 */ }
- }
- }
- return out;
- }
-
- /** 解析 assistant 的 tool_calls JSON,每个调用产出一条 tool_call 历史项(命令从 arguments 提取) */
- private void appendToolCalls(List out, String toolCallsJson) {
- if (toolCallsJson == null || toolCallsJson.isBlank()) {
- return;
- }
- try {
- List calls = objectMapper.readValue(
- toolCallsJson, new TypeReference>() {});
- for (AssistantMessage.ToolCall call : calls) {
- out.add(new com.lowenssh.agent.SessionDto.HistoryMessage(
- "tool_call", null, call.name(), extractCommand(call.arguments())));
- }
- } catch (Exception ex) {
- log.warn("回看解析 tool_calls 失败: {}", ex.getMessage());
- }
- }
-
- /** 从工具参数 JSON 里取 command 字段;取不到就原样返回 */
- private String extractCommand(String arguments) {
- if (arguments == null || arguments.isBlank()) {
- return "";
- }
- try {
- var node = objectMapper.readTree(arguments);
- if (node.has("command")) {
- return node.get("command").asText();
- }
- } catch (Exception ignored) {
- // 解析失败原样返回
- }
- return arguments;
- }
-
- /** 把攒着的工具结果合并成一条 ToolResponseMessage 加入历史,并清空缓冲 */
- private void flushTool(List messages, List pending) {
- if (!pending.isEmpty()) {
- messages.add(ToolResponseMessage.builder().responses(new ArrayList<>(pending)).build());
- pending.clear();
- }
- }
-
- /** 还原一条 assistant 消息:文字 + tool_calls(JSON 反序列化回 ToolCall 列表) */
- private AssistantMessage toAssistant(MessageEntity row) {
- String content = nullToEmpty(row.getContent());
- String toolCallsJson = row.getToolCalls();
- if (toolCallsJson == null || toolCallsJson.isBlank()) {
- return new AssistantMessage(content);
- }
- try {
- List calls = objectMapper.readValue(
- toolCallsJson, new TypeReference>() {});
- return AssistantMessage.builder().content(content).toolCalls(calls).build();
- } catch (Exception ex) {
- // 反序列化失败:退化成纯文字 assistant,至少保住对话连续性
- log.warn("还原 tool_calls 失败,退化为纯文字 sessionId={}: {}", row.getSessionId(), ex.getMessage());
- return new AssistantMessage(content);
- }
- }
-
- private static String nullToEmpty(String s) {
- return s == null ? "" : s;
- }
-}
diff --git a/src/main/java/com/lowenssh/persistence/SchemaInitializer.java b/src/main/java/com/lowenssh/persistence/SchemaInitializer.java
deleted file mode 100644
index 59a3837..0000000
--- a/src/main/java/com/lowenssh/persistence/SchemaInitializer.java
+++ /dev/null
@@ -1,143 +0,0 @@
-package com.lowenssh.persistence;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.stereotype.Component;
-
-import javax.sql.DataSource;
-import java.util.List;
-
-/**
- * 启动时自动建表 + 轻量迁移 —— 让首次部署 / 升级无需手动跑 schema.sql。
- *
- * 做两件事(都幂等,重复启动安全):
- * 1. CREATE TABLE IF NOT EXISTS:建齐 t_host / t_session / t_message / t_audit。
- * 2. 给 t_session 补 host_id 列(老库没有),并把历史会话按 host/port/user 去重,
- * 自动生成对应主机、回填 host_id —— 老对话不丢,能归到主机簿对应主机下。
- *
- * 为什么不用 Flyway:项目刻意保持轻量(无额外依赖),迁移逻辑简单,手写幂等 SQL 够用。
- * 用 JdbcTemplate 直接执行 DDL;列是否存在查 information_schema,避开 MySQL
- * 不支持「ADD COLUMN IF NOT EXISTS」的问题。
- */
-@Component
-public class SchemaInitializer {
-
- private static final Logger log = LoggerFactory.getLogger(SchemaInitializer.class);
- private final JdbcTemplate jdbc;
-
- public SchemaInitializer(DataSource dataSource) {
- this.jdbc = new JdbcTemplate(dataSource);
- }
-
- @jakarta.annotation.PostConstruct
- public void init() {
- createTables();
- migrateSessionHostId();
- }
-
- /** 建齐所有表(幂等) */
- private void createTables() {
- jdbc.execute("""
- CREATE TABLE IF NOT EXISTS t_host (
- id BIGINT NOT NULL AUTO_INCREMENT,
- alias VARCHAR(128) DEFAULT NULL,
- ssh_host VARCHAR(128) NOT NULL,
- ssh_port INT DEFAULT 22,
- ssh_user VARCHAR(64) NOT NULL,
- password_enc VARCHAR(512) DEFAULT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (id)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主机簿'
- """);
- jdbc.execute("""
- CREATE TABLE IF NOT EXISTS t_session (
- id BIGINT NOT NULL AUTO_INCREMENT,
- host_id BIGINT DEFAULT NULL,
- title VARCHAR(255) DEFAULT NULL,
- ssh_host VARCHAR(128) DEFAULT NULL,
- ssh_port INT DEFAULT 22,
- ssh_user VARCHAR(64) DEFAULT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (id),
- KEY idx_host (host_id)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话'
- """);
- jdbc.execute("""
- CREATE TABLE IF NOT EXISTS t_message (
- id BIGINT NOT NULL AUTO_INCREMENT,
- session_id BIGINT NOT NULL,
- role VARCHAR(16) NOT NULL,
- content MEDIUMTEXT DEFAULT NULL,
- tool_calls MEDIUMTEXT DEFAULT NULL,
- tool_call_id VARCHAR(64) DEFAULT NULL,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (id),
- KEY idx_session (session_id)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话消息'
- """);
- jdbc.execute("""
- CREATE TABLE IF NOT EXISTS t_audit (
- id BIGINT NOT NULL AUTO_INCREMENT,
- session_id BIGINT NOT NULL,
- command TEXT NOT NULL,
- stdout MEDIUMTEXT DEFAULT NULL,
- stderr MEDIUMTEXT DEFAULT NULL,
- exit_code INT DEFAULT NULL,
- dangerous TINYINT NOT NULL DEFAULT 0,
- confirmed TINYINT NOT NULL DEFAULT 0,
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (id),
- KEY idx_session (session_id)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='命令执行审计'
- """);
- }
-
- /**
- * 给 t_session 补 host_id 列并迁移老数据。
- * 仅当列不存在时执行加列 + 迁移,已迁移过的库再启动是空操作。
- */
- private void migrateSessionHostId() {
- if (columnExists("t_session", "host_id")) {
- return; // 新库建表时已带 host_id,或已迁移过,跳过
- }
- log.info("检测到老版 t_session 无 host_id 列,开始迁移历史会话到主机簿…");
- jdbc.execute("ALTER TABLE t_session ADD COLUMN host_id BIGINT DEFAULT NULL AFTER id");
- jdbc.execute("ALTER TABLE t_session ADD KEY idx_host (host_id)");
-
- // 把历史会话里出现过的 (host,port,user) 去重,每组生成一台主机
- List> groups = jdbc.queryForList("""
- SELECT ssh_host, ssh_port, ssh_user
- FROM t_session
- WHERE ssh_host IS NOT NULL
- GROUP BY ssh_host, ssh_port, ssh_user
- """);
- int migrated = 0;
- for (var g : groups) {
- String host = (String) g.get("ssh_host");
- Integer port = g.get("ssh_port") == null ? 22 : ((Number) g.get("ssh_port")).intValue();
- String user = (String) g.get("ssh_user");
- // 老会话没存密码,迁移出的主机 password_enc 留空,首次连接时让用户补填
- jdbc.update("INSERT INTO t_host (alias, ssh_host, ssh_port, ssh_user) VALUES (?,?,?,?)",
- null, host, port, user);
- Long hostId = jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
- jdbc.update("""
- UPDATE t_session SET host_id = ?
- WHERE ssh_host = ? AND ssh_port = ? AND ssh_user = ?
- """, hostId, host, port, user);
- migrated++;
- }
- log.info("历史会话迁移完成,自动生成主机 {} 台", migrated);
- }
-
- /** 查 information_schema 判断列是否存在 */
- private boolean columnExists(String table, String column) {
- Integer cnt = jdbc.queryForObject("""
- SELECT COUNT(*) FROM information_schema.COLUMNS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?
- """, Integer.class, table, column);
- return cnt != null && cnt > 0;
- }
-}
diff --git a/src/main/java/com/lowenssh/persistence/entity/AuditEntity.java b/src/main/java/com/lowenssh/persistence/entity/AuditEntity.java
deleted file mode 100644
index 1e390c3..0000000
--- a/src/main/java/com/lowenssh/persistence/entity/AuditEntity.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.lowenssh.persistence.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-import java.time.LocalDateTime;
-
-/**
- * 命令执行审计实体 —— 对应 t_audit
- * 每条实际下发到服务器的命令都落一笔,可追溯(危险命令、是否人工确认)
- */
-@Data
-@TableName("t_audit")
-public class AuditEntity {
-
- @TableId(type = IdType.AUTO)
- private Long id;
-
- private Long sessionId;
- private String command;
- private String stdout;
- private String stderr;
- private Integer exitCode;
- private Boolean dangerous; // TINYINT 0/1 自动映射 Boolean
- private Boolean confirmed;
- private LocalDateTime createdAt;
-}
diff --git a/src/main/java/com/lowenssh/persistence/entity/HostEntity.java b/src/main/java/com/lowenssh/persistence/entity/HostEntity.java
deleted file mode 100644
index d1857f8..0000000
--- a/src/main/java/com/lowenssh/persistence/entity/HostEntity.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.lowenssh.persistence.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-import java.time.LocalDateTime;
-
-/**
- * 主机实体 —— 对应 t_host,主机簿里的一台常用服务器。
- *
- * 与 t_session 的关系:一台主机下可有多个历史会话(session.host_id 外键关联),
- * 进入某主机后只看该主机的会话历史(按主机隔离)。
- *
- * passwordEnc 存的是 AES-GCM 密文(CryptoUtil 加密),绝不存明文;
- * 对外 DTO 也不回传密码,只在 connect 时解密用一次。
- */
-@Data
-@TableName("t_host")
-public class HostEntity {
-
- @TableId(type = IdType.AUTO)
- private Long id;
-
- private String alias; // 用户起的别名,如「京东云」,可空
- private String sshHost; // 驼峰自动映射下划线 ssh_host
- private Integer sshPort;
- private String sshUser;
- private String passwordEnc; // AES-GCM 密文,对应 password_enc
- private LocalDateTime createdAt;
- private LocalDateTime updatedAt;
-}
diff --git a/src/main/java/com/lowenssh/persistence/entity/MessageEntity.java b/src/main/java/com/lowenssh/persistence/entity/MessageEntity.java
deleted file mode 100644
index 06df6e3..0000000
--- a/src/main/java/com/lowenssh/persistence/entity/MessageEntity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.lowenssh.persistence.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-import java.time.LocalDateTime;
-
-/**
- * 对话消息实体 —— 对应 t_message
- * agentic loop 的上下文就是按 session_id 捞出这张表的历史
- */
-@Data
-@TableName("t_message")
-public class MessageEntity {
-
- @TableId(type = IdType.AUTO)
- private Long id;
-
- private Long sessionId;
- private String role; // user / assistant / tool / system
- private String content;
- private String toolCalls; // assistant 发起工具调用时的 JSON
- private String toolCallId; // role=tool 时对应的调用 id
- private LocalDateTime createdAt;
-}
diff --git a/src/main/java/com/lowenssh/persistence/entity/SessionEntity.java b/src/main/java/com/lowenssh/persistence/entity/SessionEntity.java
deleted file mode 100644
index 03eadc4..0000000
--- a/src/main/java/com/lowenssh/persistence/entity/SessionEntity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.lowenssh.persistence.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-import java.time.LocalDateTime;
-
-/**
- * 会话实体 —— 对应 t_session
- */
-@Data
-@TableName("t_session")
-public class SessionEntity {
-
- @TableId(type = IdType.AUTO)
- private Long id;
-
- private Long hostId; // 所属主机(t_host.id),历史按主机隔离的关键
- private String title;
- private String sshHost; // 驼峰自动映射下划线 ssh_host(MP 默认开启)
- private Integer sshPort;
- private String sshUser;
- private LocalDateTime createdAt;
- private LocalDateTime updatedAt;
-}
diff --git a/src/main/java/com/lowenssh/persistence/mapper/AuditMapper.java b/src/main/java/com/lowenssh/persistence/mapper/AuditMapper.java
deleted file mode 100644
index 73e8857..0000000
--- a/src/main/java/com/lowenssh/persistence/mapper/AuditMapper.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.lowenssh.persistence.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.lowenssh.persistence.entity.AuditEntity;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * 审计 Mapper
- */
-@Mapper
-public interface AuditMapper extends BaseMapper {
-}
diff --git a/src/main/java/com/lowenssh/persistence/mapper/HostMapper.java b/src/main/java/com/lowenssh/persistence/mapper/HostMapper.java
deleted file mode 100644
index 2b21863..0000000
--- a/src/main/java/com/lowenssh/persistence/mapper/HostMapper.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.lowenssh.persistence.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.lowenssh.persistence.entity.HostEntity;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * 主机 Mapper —— 继承 BaseMapper 即得基础 CRUD,无需写 XML
- */
-@Mapper
-public interface HostMapper extends BaseMapper {
-}
diff --git a/src/main/java/com/lowenssh/persistence/mapper/MessageMapper.java b/src/main/java/com/lowenssh/persistence/mapper/MessageMapper.java
deleted file mode 100644
index 4931c7d..0000000
--- a/src/main/java/com/lowenssh/persistence/mapper/MessageMapper.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.lowenssh.persistence.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.lowenssh.persistence.entity.MessageEntity;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * 消息 Mapper
- */
-@Mapper
-public interface MessageMapper extends BaseMapper {
-}
diff --git a/src/main/java/com/lowenssh/persistence/mapper/SessionMapper.java b/src/main/java/com/lowenssh/persistence/mapper/SessionMapper.java
deleted file mode 100644
index b258ee6..0000000
--- a/src/main/java/com/lowenssh/persistence/mapper/SessionMapper.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.lowenssh.persistence.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.lowenssh.persistence.entity.SessionEntity;
-import org.apache.ibatis.annotations.Mapper;
-
-/**
- * 会话 Mapper —— 继承 BaseMapper 即得基础 CRUD,无需写 XML
- */
-@Mapper
-public interface SessionMapper extends BaseMapper {
-}
diff --git a/src/main/java/com/lowenssh/ssh/ExecResult.java b/src/main/java/com/lowenssh/ssh/ExecResult.java
deleted file mode 100644
index fc4fb85..0000000
--- a/src/main/java/com/lowenssh/ssh/ExecResult.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.lowenssh.ssh;
-
-/**
- * 命令执行结果 —— stdout / stderr / exitCode 三件套
- * 用 record(Java 17):不可变、自带 equals/toString,正好装这种纯数据
- */
-public record ExecResult(String stdout, String stderr, int exitCode) {
-
- /** exitCode 为 0 视为成功 */
- public boolean isSuccess() {
- return exitCode == 0;
- }
-}
diff --git a/src/main/java/com/lowenssh/ssh/RemoteFile.java b/src/main/java/com/lowenssh/ssh/RemoteFile.java
deleted file mode 100644
index 04b17f0..0000000
--- a/src/main/java/com/lowenssh/ssh/RemoteFile.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.lowenssh.ssh;
-
-/**
- * 远程文件/目录的元信息,SFTP 列目录用。
- *
- * @param name 文件名(不含路径)
- * @param path 绝对路径
- * @param size 字节大小(目录为 0)
- * @param isDir 是否目录
- * @param perms 权限字符串,如 "rwxr-xr-x"
- * @param mtime 修改时间(Unix 秒)
- */
-public record RemoteFile(
- String name,
- String path,
- long size,
- boolean isDir,
- String perms,
- long mtime
-) {}
diff --git a/src/main/java/com/lowenssh/ssh/SshClient.java b/src/main/java/com/lowenssh/ssh/SshClient.java
deleted file mode 100644
index d8fe945..0000000
--- a/src/main/java/com/lowenssh/ssh/SshClient.java
+++ /dev/null
@@ -1,188 +0,0 @@
-package com.lowenssh.ssh;
-
-import com.jcraft.jsch.ChannelExec;
-import com.jcraft.jsch.ChannelSftp;
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.Session;
-import com.jcraft.jsch.SftpException;
-
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Properties;
-import java.util.Vector;
-
-/**
- * SSH 客户端 —— 简化版方案 B:一个实例持有一个长连接,多条命令复用同一会话。
- *
- * 为什么是长连接复用:这是个 agentic 运维 agent,loop 里会连续执行多条命令,
- * 每次重连既慢、又丢上下文。MVP 阶段先不上连接池,够用。
- *
- * 注意:非线程安全,一个 SshClient 实例对应一台机器的一个会话,由上层串行使用。
- */
-public class SshClient implements AutoCloseable {
-
- private final JSch jsch = new JSch();
- private Session session;
- // SFTP 通道:懒开 + 保持复用(同一 Session 上长期有效),随 close 一并释放。
- // 上层用 LiveSession.lock() 串行化,这里不另加锁。
- private ChannelSftp sftp;
-
- /**
- * 建立连接。密码认证(MVP 够用,后续可加密钥)。
- */
- public void connect(String host, int port, String username, String password) throws Exception {
- session = jsch.getSession(username, host, port);
- session.setPassword(password);
-
- // demo 方便:跳过 host key 校验。生产环境要换成 known_hosts 校验,否则有中间人风险
- Properties config = new Properties();
- config.put("StrictHostKeyChecking", "no");
- session.setConfig(config);
-
- // 连接超时 10s
- session.connect(10_000);
- }
-
- /**
- * 执行一条命令,同时收集 stdout、stderr、exitCode。
- *
- * JSch 的坑:stdout 走 channel 的 InputStream,stderr 要单独用 setErrStream 接,
- * exitCode 必须等 channel 真正关闭后才能拿到,所以这里要轮询 isClosed。
- */
- public ExecResult exec(String command) throws Exception {
- if (session == null || !session.isConnected()) {
- throw new IllegalStateException("SSH 未连接,先调用 connect()");
- }
-
- ChannelExec channel = (ChannelExec) session.openChannel("exec");
- channel.setCommand(command);
-
- ByteArrayOutputStream stdout = new ByteArrayOutputStream();
- ByteArrayOutputStream stderr = new ByteArrayOutputStream();
- channel.setErrStream(stderr); // stderr 直接重定向到内存流
- InputStream in = channel.getInputStream(); // stdout 手动读
-
- channel.connect();
-
- // 边读 stdout 边等命令结束
- byte[] buf = new byte[4096];
- while (true) {
- while (in.available() > 0) {
- int n = in.read(buf, 0, buf.length);
- if (n < 0) break;
- stdout.write(buf, 0, n);
- }
- // channel 关闭代表命令执行完毕
- if (channel.isClosed()) {
- if (in.available() > 0) continue; // 还有残留数据,再读一轮
- break;
- }
- Thread.sleep(50); // 没数据也没关闭,稍等避免空转
- }
-
- int exitCode = channel.getExitStatus();
- channel.disconnect();
-
- return new ExecResult(
- stdout.toString(java.nio.charset.StandardCharsets.UTF_8),
- stderr.toString(java.nio.charset.StandardCharsets.UTF_8),
- exitCode
- );
- }
-
- /** 当前是否连接中 */
- public boolean isConnected() {
- return session != null && session.isConnected();
- }
-
- // ===================== SFTP 文件操作 =====================
- // 复用同一条 SSH Session 开 sftp 通道,不重连。非线程安全,由上层串行调用。
-
- /** 懒开并复用 sftp 通道;断了就重开 */
- private ChannelSftp sftp() throws Exception {
- if (session == null || !session.isConnected()) {
- throw new IllegalStateException("SSH 未连接,先调用 connect()");
- }
- if (sftp == null || !sftp.isConnected()) {
- sftp = (ChannelSftp) session.openChannel("sftp");
- sftp.connect(10_000);
- }
- return sftp;
- }
-
- /** 列目录。过滤掉 . 和 ..,按「目录在前、名称升序」排列 */
- @SuppressWarnings("unchecked")
- public List listDir(String path) throws Exception {
- Vector entries = sftp().ls(path);
- String base = path.endsWith("/") ? path : path + "/";
- List result = new ArrayList<>();
- for (ChannelSftp.LsEntry e : entries) {
- String name = e.getFilename();
- if (name.equals(".") || name.equals("..")) continue;
- var attrs = e.getAttrs();
- result.add(new RemoteFile(
- name,
- base + name,
- attrs.getSize(),
- attrs.isDir(),
- attrs.getPermissionsString(), // 形如 "drwxr-xr-x"
- attrs.getMTime()
- ));
- }
- result.sort((a, b) -> {
- if (a.isDir() != b.isDir()) return a.isDir() ? -1 : 1;
- return a.name().compareToIgnoreCase(b.name());
- });
- return result;
- }
-
- /** 上传:从输入流写到远端路径(覆盖) */
- public void upload(InputStream in, String remotePath) throws Exception {
- sftp().put(in, remotePath, ChannelSftp.OVERWRITE);
- }
-
- /** 下载:把远端文件写到输出流 */
- public void download(String remotePath, OutputStream out) throws Exception {
- sftp().get(remotePath, out);
- }
-
- /** 删除文件 */
- public void deleteFile(String path) throws Exception {
- sftp().rm(path);
- }
-
- /** 新建目录 */
- public void mkdir(String path) throws Exception {
- sftp().mkdir(path);
- }
-
- /** 重命名/移动 */
- public void rename(String from, String to) throws Exception {
- sftp().rename(from, to);
- }
-
- /** 远端路径是否为目录(不存在也返回 false) */
- public boolean isDir(String path) {
- try {
- return sftp().stat(path).isDir();
- } catch (SftpException e) {
- return false;
- } catch (Exception e) {
- return false;
- }
- }
-
- /** 关闭会话,释放 sftp 通道和连接 */
- @Override
- public void close() {
- if (sftp != null && sftp.isConnected()) {
- sftp.disconnect();
- }
- if (session != null && session.isConnected()) {
- session.disconnect();
- }
- }
-}
diff --git a/src/main/java/com/lowenssh/util/CryptoUtil.java b/src/main/java/com/lowenssh/util/CryptoUtil.java
deleted file mode 100644
index fae7f3b..0000000
--- a/src/main/java/com/lowenssh/util/CryptoUtil.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.lowenssh.util;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-
-import javax.crypto.Cipher;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.SecureRandom;
-import java.util.Base64;
-
-/**
- * 密码加密工具 —— 主机簿密码落库前用 AES-GCM 加密,绝不存明文。
- *
- * 为什么 AES-GCM:对称加密里 GCM 自带完整性校验(认证标签),密文被篡改解密会失败,
- * 比 AES-CBC 安全。密钥从环境变量 XWSSH_CRYPTO_KEY 读,遵循本项目「密钥走环境变量」惯例。
- *
- * 密文格式:Base64( iv[12] + cipherText + tag[16] ),IV 每次随机生成同密文一起存,
- * 解密时切出来用。同一明文每次加密结果不同(IV 随机),符合预期。
- *
- * 注意:这是演示/面试项目的够用方案。生产应上 KMS / Vault 管密钥,不靠单个环境变量。
- */
-@Component
-public class CryptoUtil {
-
- private static final Logger log = LoggerFactory.getLogger(CryptoUtil.class);
- private static final String ALGO = "AES/GCM/NoPadding";
- private static final int IV_LEN = 12; // GCM 推荐 12 字节 IV
- private static final int TAG_BITS = 128; // 认证标签 128 位
- private final SecretKeySpec key;
- private final SecureRandom random = new SecureRandom();
-
- public CryptoUtil(@Value("${XWSSH_CRYPTO_KEY:}") String rawKey) {
- if (rawKey == null || rawKey.isBlank()) {
- // 没配密钥时退到开发默认值,仅保证能跑;生产务必设 XWSSH_CRYPTO_KEY
- rawKey = "xwssh-dev-default-key-change-me";
- log.warn("未设置环境变量 XWSSH_CRYPTO_KEY,主机密码用开发默认密钥加密,生产环境请务必配置!");
- }
- // 任意长度的密钥串经 SHA-256 派生成固定 32 字节,得到 AES-256 密钥
- this.key = new SecretKeySpec(sha256(rawKey), "AES");
- }
-
- /** 加密:明文 → Base64(iv + 密文 + tag)。入参为空返回 null。 */
- public String encrypt(String plain) {
- if (plain == null || plain.isEmpty()) return null;
- try {
- byte[] iv = new byte[IV_LEN];
- random.nextBytes(iv);
- Cipher cipher = Cipher.getInstance(ALGO);
- cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
- byte[] ct = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
- // iv 拼在密文前一起 Base64
- byte[] out = new byte[iv.length + ct.length];
- System.arraycopy(iv, 0, out, 0, iv.length);
- System.arraycopy(ct, 0, out, iv.length, ct.length);
- return Base64.getEncoder().encodeToString(out);
- } catch (Exception e) {
- throw new IllegalStateException("密码加密失败", e);
- }
- }
-
- /** 解密:Base64(iv + 密文 + tag) → 明文。入参为空返回 null。 */
- public String decrypt(String enc) {
- if (enc == null || enc.isEmpty()) return null;
- try {
- byte[] all = Base64.getDecoder().decode(enc);
- byte[] iv = new byte[IV_LEN];
- System.arraycopy(all, 0, iv, 0, IV_LEN);
- Cipher cipher = Cipher.getInstance(ALGO);
- cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
- byte[] plain = cipher.doFinal(all, IV_LEN, all.length - IV_LEN);
- return new String(plain, StandardCharsets.UTF_8);
- } catch (Exception e) {
- throw new IllegalStateException("密码解密失败(密钥变更或密文损坏)", e);
- }
- }
-
- private static byte[] sha256(String s) {
- try {
- return MessageDigest.getInstance("SHA-256").digest(s.getBytes(StandardCharsets.UTF_8));
- } catch (Exception e) {
- throw new IllegalStateException(e);
- }
- }
-}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
deleted file mode 100644
index 131e119..0000000
--- a/src/main/resources/application.yml
+++ /dev/null
@@ -1,89 +0,0 @@
-server:
- # 8080 被本机常驻的 Tomcat 占用,换 8081
- port: 8081
-
-spring:
- application:
- name: lowenssh
-
- # SFTP 上传:默认 1MB 太小,运维传包/日志放宽到 100MB
- servlet:
- multipart:
- max-file-size: 100MB
- max-request-size: 100MB
-
- # 本机 MySQL,库 lowenssh 已建,密码走环境变量 MYSQL_PASSWORD
- datasource:
- # allowPublicKeyRetrieval:MySQL9 默认 caching_sha2 认证,非SSL连接需开此项取公钥加密密码(本地连接安全)
- # DB_HOST:本地直跑默认 localhost;docker-compose 下设为 mysql 指向容器
- url: jdbc:mysql://${DB_HOST:localhost}:3306/lowenssh?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
- username: root
- password: ${MYSQL_PASSWORD}
- driver-class-name: com.mysql.cj.jdbc.Driver
-
- ai:
- openai:
- # 模型无关设计:统一走 OpenAI 兼容协议,换模型只改下面三处即可,零代码改动
- # 智谱GLM(当前): base-url=https://open.bigmodel.cn/api/paas/v4 model=glm-4.6
- # 通义千问: base-url=https://dashscope.aliyuncs.com/compatible-mode/v1 model=qwen-plus
- # DeepSeek: base-url=https://api.deepseek.com model=deepseek-chat
- # 选 GLM:agent 命门是 tool_call 稳定性,GLM 原生 function calling 口碑最稳;
- # 真测下来若漂,切上面任一个,零代码改动。
- base-url: https://open.bigmodel.cn/api/paas/v4
- # API key 从环境变量读,不要写死在配置里
- api-key: ${GLM_API_KEY}
- chat:
- # GLM 端点是 .../paas/v4/chat/completions,没有 /v1;
- # 覆盖 Spring AI 默认的 /v1/chat/completions,否则会 404
- completions-path: /chat/completions
- options:
- model: glm-4.7
- temperature: 0.7
-
-# 日志:方便看 Spring AI 发出的请求
-logging:
- level:
- org.springframework.ai: INFO
-
-# 应用自监控:暴露 health + 关键 metrics 给前端"监控"页读取(仅本机/内网,不鉴权)
-management:
- endpoints:
- web:
- exposure:
- include: health,info,metrics
- endpoint:
- health:
- show-details: always
-
-# 上下文管理(M3):防止多轮 loop 把模型上下文撑爆
-# 联调验证压缩逻辑时把阈值调小即可快速触发:
-# tool-result-max-chars 调到 2000、max-context-tokens 调到 2000
-xwssh:
- agent:
- # agentic loop 最大循环轮数,防模型反复调工具停不下来。
- # 注意:轮数直接乘 token——第 N 轮要把前 N-1 轮历史全发一遍,是 O(N²) 累积。
- # 25 对复杂运维够用,又能挡住失控的烧钱长循环。
- max-rounds: 25
- # 会话空闲超时(分钟):超过这么久没活动的常驻 SSH 连接会被定时回收,防连接泄漏。
- # 设 120(2h)让演示/续聊期间基本不会中途断;断了也能点"新会话"用同样信息秒重连。
- session-idle-timeout-minutes: 120
- context:
- # Layer 0:最近几条工具结果回灌给模型的最大字符数,超出截掉中段(完整内容仍存 t_message)。
- # 这条最关键:工具结果每轮都重发,留得越大、轮数越多,token 烧得越凶。
- # 3000 字符≈1200token,运维命令输出大多看头尾就够判断,调小立竿见影省钱。
- tool-result-max-chars: 3000
- # Layer 0:更早(保留区之外)的工具结果用更小阈值大力收紧——旧命令模型已读过、结论已在历史里,
- # 没必要每轮全量重发。800 字符≈320token,让上下文不随轮数线性膨胀。
- old-tool-result-max-chars: 800
- # Layer 4:整段上下文估算 token 超此值触发 LLM 压缩历史。
- # 12000 比原来 32000 早压缩,但压缩本身也要调一次模型,不宜过小,否则频繁压缩反而费钱。
- max-context-tokens: 12000
- # Layer 4:压缩时保留最近多少条消息原文(不进摘要)
- keep-recent-messages: 4
- # Layer 4:摘要 LLM 连续失败达此次数后熔断,停止压缩裸跑兜底
- circuit-limit: 3
-
-# MyBatis-Plus:开发期打印 SQL 方便调试
-mybatis-plus:
- configuration:
- log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
deleted file mode 100644
index fb51b46..0000000
--- a/src/main/resources/schema.sql
+++ /dev/null
@@ -1,58 +0,0 @@
--- LowenSSH 建表 SQL(手动执行:mysql -u root -p lowenssh < schema.sql)
--- 库已建:CREATE DATABASE lowenssh DEFAULT CHARACTER SET utf8mb4;
--- 注:应用启动时 SchemaInitializer 会自动跑这些建表/加列,平时无需手动执行此文件。
-
--- 主机表:主机簿里的一台常用服务器,password_enc 存 AES-GCM 密文
-CREATE TABLE IF NOT EXISTS t_host (
- id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
- alias VARCHAR(128) DEFAULT NULL COMMENT '主机别名',
- ssh_host VARCHAR(128) NOT NULL COMMENT '目标服务器 host',
- ssh_port INT DEFAULT 22 COMMENT '端口',
- ssh_user VARCHAR(64) NOT NULL COMMENT 'SSH 用户名',
- password_enc VARCHAR(512) DEFAULT NULL COMMENT 'SSH 密码密文(AES-GCM)',
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
- PRIMARY KEY (id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主机簿';
-
--- 会话表:一次对话 = 一个 session,绑定一台目标服务器(host_id 关联 t_host)
-CREATE TABLE IF NOT EXISTS t_session (
- id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
- host_id BIGINT DEFAULT NULL COMMENT '所属主机 t_host.id',
- title VARCHAR(255) DEFAULT NULL COMMENT '会话标题',
- ssh_host VARCHAR(128) DEFAULT NULL COMMENT '目标服务器 host',
- ssh_port INT DEFAULT 22 COMMENT '目标服务器端口',
- ssh_user VARCHAR(64) DEFAULT NULL COMMENT 'SSH 用户名',
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
- PRIMARY KEY (id),
- KEY idx_host (host_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话';
-
--- 消息表:对话历史,agentic loop 的上下文来源
-CREATE TABLE IF NOT EXISTS t_message (
- id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
- session_id BIGINT NOT NULL COMMENT '所属会话',
- role VARCHAR(16) NOT NULL COMMENT '角色: user/assistant/tool/system',
- content MEDIUMTEXT DEFAULT NULL COMMENT '消息内容',
- tool_calls MEDIUMTEXT DEFAULT NULL COMMENT '工具调用 JSON(assistant 发起时)',
- tool_call_id VARCHAR(64) DEFAULT NULL COMMENT '工具结果对应的调用 id(role=tool 时)',
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- PRIMARY KEY (id),
- KEY idx_session (session_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话消息';
-
--- 审计表:每条实际执行的命令都记一笔,可追溯
-CREATE TABLE IF NOT EXISTS t_audit (
- id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
- session_id BIGINT NOT NULL COMMENT '所属会话',
- command TEXT NOT NULL COMMENT '执行的命令',
- stdout MEDIUMTEXT DEFAULT NULL COMMENT '标准输出',
- stderr MEDIUMTEXT DEFAULT NULL COMMENT '错误输出',
- exit_code INT DEFAULT NULL COMMENT '退出码',
- dangerous TINYINT NOT NULL DEFAULT 0 COMMENT '是否危险命令: 0否 1是',
- confirmed TINYINT NOT NULL DEFAULT 0 COMMENT '是否经人工确认: 0否 1是',
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '执行时间',
- PRIMARY KEY (id),
- KEY idx_session (session_id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='命令执行审计';
diff --git a/src/test/java/com/lowenssh/agent/AgentServiceTest.java b/src/test/java/com/lowenssh/agent/AgentServiceTest.java
deleted file mode 100644
index 94f0939..0000000
--- a/src/test/java/com/lowenssh/agent/AgentServiceTest.java
+++ /dev/null
@@ -1,175 +0,0 @@
-package com.lowenssh.agent;
-
-import com.lowenssh.agent.guard.CommandGuard;
-import com.lowenssh.agent.guard.ConfirmationHandler;
-import com.lowenssh.persistence.AuditService;
-import com.lowenssh.persistence.MessageService;
-import com.lowenssh.ssh.SshClient;
-import org.junit.jupiter.api.Test;
-import org.springframework.ai.chat.messages.AssistantMessage;
-import org.springframework.ai.chat.messages.Message;
-import org.springframework.ai.chat.messages.ToolResponseMessage;
-import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
-import org.springframework.ai.chat.model.ChatResponse;
-import org.springframework.ai.chat.model.Generation;
-import org.springframework.ai.chat.prompt.Prompt;
-import org.springframework.ai.model.tool.ToolCallingManager;
-import org.springframework.ai.model.tool.ToolExecutionResult;
-import org.springframework.ai.openai.OpenAiChatModel;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * Agent loop 核心单测 —— 项目卖点是"手写 loop + 安全门禁",loop 的决策分支必须覆盖:
- * 1. 模型不再调工具 → 给出结论结束
- * 2. DENY 命令 → 不执行、回灌拒绝、loop 继续让模型换方案
- * 3. ASK 命令用户拒绝 → 同 DENY(不执行)
- * 4. ASK 命令用户批准 → 交框架执行
- * 5. MAX_ROUNDS 上限 → 防死循环兜底
- *
- * 全程不连真模型/真 SSH:chatModel、toolCallingManager 用 mock;门禁用真 CommandGuard
- * (它本身已有独立单测,这里用真实判定让用例更接近线上)。
- */
-class AgentServiceTest {
-
- private static final Long SID = 1L;
-
- // —— 依赖:模型和工具执行器 mock,其余给真实/哑实现 ——
- private final OpenAiChatModel chatModel = mock(OpenAiChatModel.class);
- private final ToolCallingManager toolCallingManager = mock(ToolCallingManager.class);
- private final CommandGuard guard = new CommandGuard();
- private final AuditService auditService = mock(AuditService.class);
- private final MessageService messageService = mock(MessageService.class);
- // ContextManager 用真实对象但阈值设到永不压缩,截断也不影响这里的短消息
- private final ContextManager contextManager = new ContextManager(null, 8000, 800, 999999, 6, 3);
-
- private final AgentService service = new AgentService(
- chatModel, toolCallingManager, guard, auditService, messageService, contextManager, 15);
-
- // ToolCallbacks.from(tools) 只反射读 @Tool 注解,不真连 SSH,deps 给 null 即可
- private SshTools tools() {
- return new SshTools(new SshClient(), SID, auditService, guard);
- }
-
- // —— 造 ChatResponse 的辅助方法 ——
-
- /** 造一个"模型给出纯文字结论、无 tool_call"的响应 */
- private ChatResponse textResponse(String text) {
- return new ChatResponse(List.of(
- new Generation(new AssistantMessage(text), ChatGenerationMetadata.NULL)));
- }
-
- /** 造一个"模型要调 execCommand 跑某条命令"的响应。
- * 用 AssistantMessage.builder() 公开 API(1.1.5 带 toolCall 的构造器是 protected)。 */
- private ChatResponse execCallResponse(String callId, String command) {
- AssistantMessage.ToolCall call = new AssistantMessage.ToolCall(
- callId, "function", "execCommand",
- "{\"command\":\"" + command + "\"}");
- AssistantMessage assistant = AssistantMessage.builder()
- .content("")
- .toolCalls(List.of(call))
- .build();
- return new ChatResponse(List.of(
- new Generation(assistant, ChatGenerationMetadata.NULL)));
- }
-
- // ============================ 1. 正常结束 ============================
-
- @Test
- void 模型不调工具时直接给出结论结束() {
- when(chatModel.call(any(Prompt.class))).thenReturn(textResponse("磁盘还剩 58%,一切正常。"));
-
- String result = service.run(SID, "看下磁盘", tools(), (cmd, reason) -> true);
-
- assertEquals("磁盘还剩 58%,一切正常。", result);
- // 没有工具调用,执行器一次都不该被碰
- verify(toolCallingManager, never()).executeToolCalls(any(), any());
- }
-
- // ============================ 2. DENY 命令被拦 ============================
-
- @Test
- void DENY命令不执行且回灌拒绝后继续loop() {
- // 第 1 轮:模型想跑 rm -rf /(必被 DENY);第 2 轮:模型改口给结论
- when(chatModel.call(any(Prompt.class)))
- .thenReturn(execCallResponse("c1", "rm -rf /"))
- .thenReturn(textResponse("好的,我不执行删除操作。"));
-
- String result = service.run(SID, "清理磁盘", tools(), (cmd, reason) -> true);
-
- assertEquals("好的,我不执行删除操作。", result);
- // 危险命令绝不能进框架执行
- verify(toolCallingManager, never()).executeToolCalls(any(), any());
- // 拦截点必须落审计
- verify(auditService).logBlocked(eq(SID), eq("rm -rf /"), eq(true), anyString());
- }
-
- // ============================ 3. ASK 用户拒绝 ============================
-
- @Test
- void ASK命令用户拒绝则不执行() {
- // systemctl restart 命中 ASK;确认器返回 false(拒绝)
- when(chatModel.call(any(Prompt.class)))
- .thenReturn(execCallResponse("c1", "systemctl restart nginx"))
- .thenReturn(textResponse("已取消重启。"));
-
- ConfirmationHandler denyAll = (cmd, reason) -> false;
- String result = service.run(SID, "重启nginx", tools(), denyAll);
-
- assertEquals("已取消重启。", result);
- verify(toolCallingManager, never()).executeToolCalls(any(), any());
- }
-
- // ============================ 4. ASK 用户批准 → 执行 ============================
-
- @Test
- void ASK命令用户批准则交框架执行() {
- when(chatModel.call(any(Prompt.class)))
- .thenReturn(execCallResponse("c1", "systemctl restart nginx"))
- .thenReturn(textResponse("nginx 已重启完成。"));
-
- // 批准后框架执行,返回一段只含工具结果、无新 tool_call 的历史,让下一轮收尾
- ToolExecutionResult execResult = mock(ToolExecutionResult.class);
- when(execResult.conversationHistory()).thenReturn(List.of(
- ToolResponseMessage.builder()
- .responses(List.of(new ToolResponseMessage.ToolResponse(
- "c1", "execCommand", "Job for nginx.service done.")))
- .build()));
- when(toolCallingManager.executeToolCalls(any(), any())).thenReturn(execResult);
-
- ConfirmationHandler approveAll = (cmd, reason) -> true;
- String result = service.run(SID, "重启nginx", tools(), approveAll);
-
- assertEquals("nginx 已重启完成。", result);
- // 批准的命令确实交给框架执行了一次
- verify(toolCallingManager).executeToolCalls(any(), any());
- }
-
- // ============================ 5. 死循环兜底 ============================
-
- @Test
- void 模型反复调安全命令达上限则兜底返回() {
- // 每轮都返回一个 ALLOW 命令,永不收尾 → 必然撞 MAX_ROUNDS
- when(chatModel.call(any(Prompt.class)))
- .thenReturn(execCallResponse("c", "ls -al"));
-
- // ALLOW 命令会进框架执行,给个空历史让 loop 继续转
- ToolExecutionResult execResult = mock(ToolExecutionResult.class);
- when(execResult.conversationHistory()).thenReturn(List.of());
- when(toolCallingManager.executeToolCalls(any(), any())).thenReturn(execResult);
-
- String result = service.run(SID, "一直查", tools(), (cmd, reason) -> true);
-
- assertTrue(result.contains("最大循环轮数"), "撞上限应返回兜底文案,实际:" + result);
- }
-}
diff --git a/src/test/java/com/lowenssh/agent/ContextManagerTest.java b/src/test/java/com/lowenssh/agent/ContextManagerTest.java
deleted file mode 100644
index f5d5fc7..0000000
--- a/src/test/java/com/lowenssh/agent/ContextManagerTest.java
+++ /dev/null
@@ -1,227 +0,0 @@
-package com.lowenssh.agent;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.ai.chat.messages.AssistantMessage;
-import org.springframework.ai.chat.messages.Message;
-import org.springframework.ai.chat.messages.SystemMessage;
-import org.springframework.ai.chat.messages.ToolResponseMessage;
-import org.springframework.ai.chat.messages.UserMessage;
-import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
-import org.springframework.ai.chat.model.ChatResponse;
-import org.springframework.ai.chat.model.Generation;
-import org.springframework.ai.openai.OpenAiChatModel;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * 上下文管理单测 —— 截断/压缩/配对/熔断是 M3 的硬逻辑,必须覆盖。
- * Layer 0 截断不调模型,chatModel 传 null;Layer 4 压缩用 mock 模拟摘要返回,纯逻辑不连真模型。
- */
-class ContextManagerTest {
-
- // ============================ Layer 0:截断 ============================
-
- /** 截断不调模型,chatModel 给 null 也能跑 */
- private ContextManager truncator(int maxChars) {
- // old 阈值给同值:旧用例只放单条工具结果、位置都在保留区内,走 recent 分支,old 不生效
- return new ContextManager(null, maxChars, maxChars, 999999, 6, 3);
- }
-
- /** 造一条工具结果消息 */
- private ToolResponseMessage toolMsg(String id, String name, String data) {
- return ToolResponseMessage.builder()
- .responses(List.of(new ToolResponseMessage.ToolResponse(id, name, data)))
- .build();
- }
-
- private String toolData(Message msg) {
- return ((ToolResponseMessage) msg).getResponses().get(0).responseData();
- }
-
- @Test
- void 短工具结果不截断() {
- ContextManager cm = truncator(8000);
- List in = List.of(toolMsg("1", "execCommand", "磁盘占用 42%"));
- List out = cm.truncateToolResponses(in);
- assertEquals("磁盘占用 42%", toolData(out.get(0)));
- }
-
- @Test
- void 超长工具结果截断中段保留头尾() {
- ContextManager cm = truncator(100);
- String big = "H".repeat(80) + "M".repeat(500) + "T".repeat(80);
- List out = cm.truncateToolResponses(List.of(toolMsg("1", "tailLog", big)));
- String data = toolData(out.get(0));
- // 截断后总长远小于原始;含截断提示;保留了开头的 H 和结尾的 T
- assertTrue(data.length() < big.length(), "应被截短");
- assertTrue(data.contains("已截断"), "应含截断提示");
- assertTrue(data.contains("t_message"), "提示应指向 t_message");
- assertTrue(data.startsWith("H"), "应保留头部");
- assertTrue(data.endsWith("T"), "应保留尾部");
- }
-
- @Test
- void 截断只动工具结果不动普通消息() {
- ContextManager cm = truncator(50);
- List in = List.of(
- new SystemMessage("系统提示"),
- new UserMessage("查一下磁盘"),
- toolMsg("1", "execCommand", "X".repeat(500)));
- List out = cm.truncateToolResponses(in);
- assertEquals("系统提示", out.get(0).getText());
- assertEquals("查一下磁盘", out.get(1).getText());
- assertTrue(toolData(out.get(2)).contains("已截断"));
- }
-
- @Test
- void 截断幂等再跑结果不变() {
- ContextManager cm = truncator(100);
- List once = cm.truncateToolResponses(List.of(toolMsg("1", "x", "Z".repeat(800))));
- List twice = cm.truncateToolResponses(once);
- assertEquals(toolData(once.get(0)), toolData(twice.get(0)));
- }
-
- @Test
- void 旧工具结果用更小阈值收紧() {
- // 近区大阈值 1000、旧区小阈值 100,keep-recent=2
- ContextManager cm = new ContextManager(null, 1000, 100, 999999, 2, 3);
- String big = "X".repeat(900);
- // 列表:旧工具结果(距末尾4) + 两条占位 + 近工具结果(距末尾1)
- List in = List.of(
- toolMsg("old", "tailLog", big),
- new UserMessage("中间一"),
- new UserMessage("中间二"),
- toolMsg("new", "tailLog", big));
- List out = cm.truncateToolResponses(in);
- String oldData = toolData(out.get(0));
- String newData = toolData(out.get(3));
- // 旧的被小阈值截断(含提示且远小于 900);新的在阈值内不截
- assertTrue(oldData.contains("已截断"), "旧工具结果应被截断");
- assertTrue(oldData.length() < 200, "旧工具结果应收紧到小阈值附近");
- assertEquals(big, newData, "近区工具结果在大阈值内不应被截");
- }
-
- // ============================ Layer 4:压缩 ============================
-
- /** 造一个会返回固定摘要文本的 mock 模型 */
- private OpenAiChatModel mockModel(String summary) {
- OpenAiChatModel m = mock(OpenAiChatModel.class);
- ChatResponse resp = new ChatResponse(List.of(
- new Generation(new AssistantMessage(summary), ChatGenerationMetadata.NULL)));
- when(m.call(any(org.springframework.ai.chat.prompt.Prompt.class))).thenReturn(resp);
- return m;
- }
-
- /** 造一段够长的对话(system + 多轮 user/assistant),确保 token 估算超阈值 */
- private List longHistory(int rounds) {
- java.util.List msgs = new java.util.ArrayList<>();
- msgs.add(new SystemMessage("你是运维助手"));
- for (int i = 0; i < rounds; i++) {
- msgs.add(new UserMessage("第" + i + "步:" + "内".repeat(100)));
- msgs.add(new AssistantMessage("回复" + i + ":" + "容".repeat(100)));
- }
- return msgs;
- }
-
- @Test
- void 未超阈值不压缩() {
- // 阈值设很大,短历史不该触发压缩
- ContextManager cm = new ContextManager(mockModel("摘要"), 8000, 800, 999999, 6, 3);
- List in = longHistory(2);
- List out = cm.compressIfNeeded(in);
- assertEquals(in.size(), out.size(), "未超阈值应原样返回");
- }
-
- @Test
- void 超阈值触发压缩且保留system和最近K条() {
- // 阈值调到 100 token,长历史必然超
- ContextManager cm = new ContextManager(mockModel("【这是早先对话的摘要】"), 8000, 800, 100, 6, 3);
- List in = longHistory(10); // 1 system + 20 条
- List out = cm.compressIfNeeded(in);
- assertTrue(out.size() < in.size(), "应被压缩变短");
- // 第一条仍是 system
- assertTrue(out.get(0) instanceof SystemMessage, "首条应保留 system");
- // 第二条是摘要(UserMessage 含摘要文本)
- assertTrue(out.get(1).getText().contains("早先对话的摘要"), "次条应是摘要");
- // 保留了最近 6 条原文
- assertEquals(6, out.size() - 2, "应保留最近 6 条原文 + system + 摘要");
- }
-
- @Test
- void 保留区开头是孤儿工具结果时切割点前移保配对() {
- // 构造:system + assistant(tool_call) + tool_result,且 tool_result 恰好落在保留区开头
- AssistantMessage.ToolCall call = new AssistantMessage.ToolCall("c1", "function", "execCommand", "{\"command\":\"df -h\"}");
- java.util.List msgs = new java.util.ArrayList<>();
- msgs.add(new SystemMessage("系统"));
- // 前面填一堆把 token 撑上去
- for (int i = 0; i < 8; i++) {
- msgs.add(new UserMessage("填充" + "占".repeat(80)));
- msgs.add(new AssistantMessage("回复" + "位".repeat(80)));
- }
- // 末尾一对:assistant 带 tool_call + 对应 tool_result
- msgs.add(AssistantMessage.builder().content("").toolCalls(List.of(call)).build());
- msgs.add(toolMsg("c1", "execCommand", "结果"));
-
- // keep-recent=1 会让切割点正好落在 tool_result 上 -> 须前移把 assistant 一起留下
- ContextManager cm = new ContextManager(mockModel("摘要"), 8000, 800, 50, 1, 3);
- List out = cm.compressIfNeeded(msgs);
-
- // 保留区不能以孤儿 ToolResponseMessage 开头:找到摘要后的第一条原文
- // out = [system, 摘要, ...保留区],保留区首条不应是 ToolResponseMessage
- Message firstKept = out.get(2);
- assertFalse(firstKept instanceof ToolResponseMessage,
- "保留区开头不能是孤儿 tool_result,切割点应前移到对应 assistant");
- }
-
- @Test
- void 摘要连续失败达阈值后熔断不再调模型() {
- // mock 模型抛异常模拟摘要失败
- OpenAiChatModel failModel = mock(OpenAiChatModel.class);
- when(failModel.call(any(org.springframework.ai.chat.prompt.Prompt.class)))
- .thenThrow(new RuntimeException("模型挂了"));
- // circuit-limit=3
- ContextManager cm = new ContextManager(failModel, 8000, 800, 100, 6, 3);
-
- // 前 3 次都会尝试调用并失败
- for (int i = 0; i < 3; i++) {
- cm.compressIfNeeded(longHistory(10));
- }
- // 第 4、5 次应已熔断,不再调模型
- cm.compressIfNeeded(longHistory(10));
- cm.compressIfNeeded(longHistory(10));
-
- // 总调用次数应恰好 3(熔断后不再调)
- verify(failModel, times(3)).call(any(org.springframework.ai.chat.prompt.Prompt.class));
- }
-
- @Test
- void 摘要失败时原样返回不丢历史() {
- OpenAiChatModel failModel = mock(OpenAiChatModel.class);
- when(failModel.call(any(org.springframework.ai.chat.prompt.Prompt.class)))
- .thenThrow(new RuntimeException("挂了"));
- ContextManager cm = new ContextManager(failModel, 8000, 800, 100, 6, 3);
- List in = longHistory(10);
- List out = cm.compressIfNeeded(in);
- // 摘要失败不能丢历史,必须原样返回
- assertEquals(in.size(), out.size(), "摘要失败应原样返回不丢历史");
- }
-
- // ============================ token 估算 ============================
-
- @Test
- void token估算随内容增长() {
- ContextManager cm = truncator(8000);
- int small = cm.estimateTokens(List.of(new UserMessage("短")));
- int big = cm.estimateTokens(List.of(new UserMessage("长".repeat(1000))));
- assertTrue(big > small, "内容越多估算 token 越大");
- }
-}
diff --git a/src/test/java/com/lowenssh/agent/SessionManagerTest.java b/src/test/java/com/lowenssh/agent/SessionManagerTest.java
deleted file mode 100644
index ee6cb6b..0000000
--- a/src/test/java/com/lowenssh/agent/SessionManagerTest.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.lowenssh.agent;
-
-import com.lowenssh.persistence.mapper.SessionMapper;
-import com.lowenssh.ssh.SshClient;
-import org.junit.jupiter.api.Test;
-
-import java.lang.reflect.Field;
-import java.time.Instant;
-import java.util.Map;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * SessionManager 单测 —— open() 要连真 SSH,这里不测;聚焦不依赖真连接的逻辑:
- * get(命中/不存在/连接已断)、close(关连接 + 幂等)、reap(回收超时/断开会话)。
- *
- * 用反射把 mock 的 LiveSession 塞进内部 map,绕开 open() 的真实 SSH 连接。
- */
-class SessionManagerTest {
-
- private final SessionMapper sessionMapper = mock(SessionMapper.class);
-
- /** 反射取出内部 bySession map(已绑定会话的活连接) */
- @SuppressWarnings("unchecked")
- private Map sessionsOf(SessionManager mgr) throws Exception {
- Field f = SessionManager.class.getDeclaredField("bySession");
- f.setAccessible(true);
- return (Map) f.get(mgr);
- }
-
- /** 造一个挂了 mock SshClient 的 LiveSession(回填 sessionId)并塞进 manager */
- private SshClient injectSession(SessionManager mgr, Long id, boolean connected) throws Exception {
- SshClient ssh = mock(SshClient.class);
- when(ssh.isConnected()).thenReturn(connected);
- SessionManager.LiveSession live = new SessionManager.LiveSession(1L, "h", 22, "root", ssh);
- live.sessionId = id;
- sessionsOf(mgr).put(id, live);
- return ssh;
- }
-
- @Test
- void get命中活跃会话() throws Exception {
- SessionManager mgr = new SessionManager(sessionMapper, 30);
- injectSession(mgr, 1L, true);
-
- SessionManager.LiveSession live = mgr.get(1L);
-
- assertThat(live).isNotNull();
- assertThat(live.sessionId()).isEqualTo(1L);
- }
-
- @Test
- void get不存在的会话返回null() {
- SessionManager mgr = new SessionManager(sessionMapper, 30);
- assertThat(mgr.get(999L)).isNull();
- }
-
- @Test
- void get发现连接已断则移除并返回null() throws Exception {
- SessionManager mgr = new SessionManager(sessionMapper, 30);
- SshClient ssh = injectSession(mgr, 2L, false); // 连接已断
-
- assertThat(mgr.get(2L)).isNull();
- assertThat(mgr.activeCount()).isZero();
- verify(ssh).close(); // 顺手关掉
- }
-
- @Test
- void close关连接并移除且幂等() throws Exception {
- SessionManager mgr = new SessionManager(sessionMapper, 30);
- SshClient ssh = injectSession(mgr, 3L, true);
-
- mgr.close(3L);
- assertThat(mgr.activeCount()).isZero();
- verify(ssh, times(1)).close();
-
- // 再关一次不报错、不重复关
- mgr.close(3L);
- verify(ssh, times(1)).close();
- }
-
- @Test
- void reap回收超时会话保留活跃会话() throws Exception {
- SessionManager mgr = new SessionManager(sessionMapper, 30);
- // 活跃会话:刚活动过
- SshClient fresh = injectSession(mgr, 10L, true);
- // 超时会话:lastActiveAt 拨到 31 分钟前
- SshClient stale = injectSession(mgr, 11L, true);
- SessionManager.LiveSession staleLive = sessionsOf(mgr).get(11L);
- Field lastActive = SessionManager.LiveSession.class.getDeclaredField("lastActiveAt");
- lastActive.setAccessible(true);
- lastActive.set(staleLive, Instant.now().minusSeconds(31 * 60));
-
- mgr.reapIdleSessions();
-
- assertThat(mgr.activeCount()).isEqualTo(1);
- verify(stale).close(); // 超时的被回收
- verify(fresh, never()).close(); // 活跃的保留
- }
-}
diff --git a/src/test/java/com/lowenssh/agent/guard/CommandGuardTest.java b/src/test/java/com/lowenssh/agent/guard/CommandGuardTest.java
deleted file mode 100644
index 48c4929..0000000
--- a/src/test/java/com/lowenssh/agent/guard/CommandGuardTest.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package com.lowenssh.agent.guard;
-
-import com.lowenssh.agent.guard.CommandGuard.Decision;
-import com.lowenssh.agent.guard.CommandGuard.Verdict;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * 门禁单测 —— 三态判定是 Agent 安全的硬边界,必须覆盖到位。
- * 不依赖 SSH/模型,纯逻辑,跑得快。
- */
-class CommandGuardTest {
-
- private final CommandGuard guard = new CommandGuard();
-
- private Decision decide(String cmd) {
- return guard.evaluate(cmd).decision();
- }
-
- // —— allow:只读命令 ——
- @Test
- void 只读命令放行() {
- assertEquals(Decision.ALLOW, decide("df -h"));
- assertEquals(Decision.ALLOW, decide("ps aux | grep java"));
- assertEquals(Decision.ALLOW, decide("cat /etc/hostname"));
- assertEquals(Decision.ALLOW, decide("free -m"));
- }
-
- // —— deny:毁灭性命令直接拒 ——
- @Test
- void 危险命令拒绝() {
- assertEquals(Decision.DENY, decide("rm -rf /"));
- assertEquals(Decision.DENY, decide("rm -rf /var/data"));
- assertEquals(Decision.DENY, decide("rm -fr /tmp/x"));
- assertEquals(Decision.DENY, decide("mkfs.ext4 /dev/sdb"));
- assertEquals(Decision.DENY, decide("dd if=/dev/zero of=/dev/sda"));
- assertEquals(Decision.DENY, decide("shutdown -h now"));
- assertEquals(Decision.DENY, decide("reboot"));
- }
-
- // —— ask:有副作用,要确认 ——
- @Test
- void 副作用命令要确认() {
- assertEquals(Decision.ASK, decide("rm /tmp/a.log")); // 普通 rm(非 -rf)
- assertEquals(Decision.ASK, decide("kill 1234"));
- assertEquals(Decision.ASK, decide("systemctl restart nginx"));
- assertEquals(Decision.ASK, decide("chmod 777 /etc/passwd"));
- assertEquals(Decision.ASK, decide("apt-get install vim"));
- }
-
- // —— 复合命令拆段:任一段最严即整条最严 ——
- @Test
- void 复合命令取最严() {
- // 前段安全、后段毁灭 → DENY(防整条被当一段漏过)
- assertEquals(Decision.DENY, decide("ls && rm -rf /data"));
- // 管道里藏 dd → DENY
- assertEquals(Decision.DENY, decide("cat x | dd of=/dev/sda"));
- // 分号串联,含 ask 段 → ASK
- assertEquals(Decision.ASK, decide("df -h; systemctl stop nginx"));
- // 全段安全 → ALLOW
- assertEquals(Decision.ALLOW, decide("cd /var && ls -al"));
- }
-
- // —— deny 优先于 ask:评估顺序保证 deny 永远赢 ——
- @Test
- void deny优先于ask() {
- // rm -rf 同时命中 ask(rm) 和 deny(rm -rf),必须 DENY
- assertEquals(Decision.DENY, decide("rm -rf /opt/app"));
- }
-
- // —— 边界 ——
- @Test
- void 空命令放行() {
- assertEquals(Decision.ALLOW, decide(""));
- assertEquals(Decision.ALLOW, decide(" "));
- assertEquals(Decision.ALLOW, decide(null));
- }
-
- // —— 防误伤:dd 不该误伤 add,rm 不该误伤 chmod 之外的词 ——
- @Test
- void 不误伤子串() {
- assertEquals(Decision.ALLOW, decide("git add .")); // add 含 dd 不该命中
- assertEquals(Decision.ALLOW, decide("echo warm")); // warm 含 rm 不该命中
- }
-
- @Test
- void 拒绝原因可读() {
- Verdict v = guard.evaluate("rm -rf /");
- assertEquals(Decision.DENY, v.decision());
- // 原因里应带命中片段,便于回灌给模型/展示用户
- org.junit.jupiter.api.Assertions.assertTrue(v.reason().contains("rm"));
- }
-
- // —— find 等价绕过:真机联调发现模型被拦 rm -rf 后改用 find 删除 ——
- @Test
- void find删除变体也拒绝() {
- assertEquals(Decision.DENY, decide("find /tmp -mindepth 1 -delete"));
- assertEquals(Decision.DENY, decide("find /var/log -name '*.log' -delete"));
- assertEquals(Decision.DENY, decide("find /data -type f -exec rm -f {} \\;"));
- // 普通 find 查找不该误伤
- assertEquals(Decision.ALLOW, decide("find /etc -name nginx.conf"));
- }
-}
diff --git a/src/test/java/com/lowenssh/persistence/MessageServiceTest.java b/src/test/java/com/lowenssh/persistence/MessageServiceTest.java
deleted file mode 100644
index 2ca8ea2..0000000
--- a/src/test/java/com/lowenssh/persistence/MessageServiceTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-package com.lowenssh.persistence;
-
-import com.lowenssh.persistence.entity.MessageEntity;
-import com.lowenssh.persistence.mapper.MessageMapper;
-import org.junit.jupiter.api.Test;
-import org.springframework.ai.chat.messages.AssistantMessage;
-import org.springframework.ai.chat.messages.Message;
-import org.springframework.ai.chat.messages.MessageType;
-import org.springframework.ai.chat.messages.ToolResponseMessage;
-import org.springframework.ai.chat.messages.UserMessage;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-/**
- * MessageService.loadHistory 单测 —— 多轮对话的核心:把 t_message 还原成
- * Spring AI 的 Message 列表,再回灌给模型。这里重点验证还原规则正确。
- */
-class MessageServiceTest {
-
- /** 造一条 MessageEntity 行 */
- private MessageEntity row(String role, String content, String toolCalls, String toolCallId) {
- MessageEntity e = new MessageEntity();
- e.setRole(role);
- e.setContent(content);
- e.setToolCalls(toolCalls);
- e.setToolCallId(toolCallId);
- return e;
- }
-
- @SuppressWarnings("unchecked")
- private MessageService serviceReturning(List rows) {
- MessageMapper mapper = mock(MessageMapper.class);
- when(mapper.selectList(any())).thenReturn(rows);
- return new MessageService(mapper);
- }
-
- @Test
- void 空sessionId返回空列表() {
- MessageService svc = serviceReturning(new ArrayList<>());
- assertThat(svc.loadHistory(null)).isEmpty();
- }
-
- @Test
- void 还原user和assistant纯文字消息() {
- List rows = List.of(
- row("user", "看下磁盘", null, null),
- row("assistant", "根分区还剩 30G", null, null));
-
- List history = serviceReturning(rows).loadHistory(1L);
-
- assertThat(history).hasSize(2);
- assertThat(history.get(0)).isInstanceOf(UserMessage.class);
- assertThat(history.get(0).getText()).isEqualTo("看下磁盘");
- assertThat(history.get(1)).isInstanceOf(AssistantMessage.class);
- assertThat(history.get(1).getText()).isEqualTo("根分区还剩 30G");
- }
-
- @Test
- void 还原带工具调用的完整一轮() {
- // assistant 发起一个 execCommand 调用,随后一条 tool 结果
- String toolCallsJson = "[{\"id\":\"call_1\",\"type\":\"function\","
- + "\"name\":\"execCommand\",\"arguments\":\"{\\\"command\\\":\\\"df -h\\\"}\"}]";
- List rows = List.of(
- row("user", "看下磁盘", null, null),
- row("assistant", "", toolCallsJson, null),
- row("tool", "Filesystem ... 30G", null, "call_1"),
- row("assistant", "根分区还剩 30G", null, null));
-
- List history = serviceReturning(rows).loadHistory(1L);
-
- assertThat(history).hasSize(4);
- // 第二条 assistant 带 tool_calls
- AssistantMessage assistant = (AssistantMessage) history.get(1);
- assertThat(assistant.getToolCalls()).hasSize(1);
- assertThat(assistant.getToolCalls().get(0).id()).isEqualTo("call_1");
- assertThat(assistant.getToolCalls().get(0).name()).isEqualTo("execCommand");
- // 第三条是工具结果,id 与调用配对
- assertThat(history.get(2)).isInstanceOf(ToolResponseMessage.class);
- ToolResponseMessage trm = (ToolResponseMessage) history.get(2);
- assertThat(trm.getResponses()).hasSize(1);
- assertThat(trm.getResponses().get(0).id()).isEqualTo("call_1");
- }
-
- @Test
- void 同一轮多条tool结果合并为一个ToolResponseMessage() {
- List rows = List.of(
- row("assistant", "", "[{\"id\":\"c1\",\"type\":\"function\",\"name\":\"execCommand\",\"arguments\":\"{}\"}]", null),
- row("tool", "结果1", null, "c1"),
- row("tool", "结果2", null, "c2"));
-
- List history = serviceReturning(rows).loadHistory(1L);
-
- // 1 条 assistant + 1 条合并后的 ToolResponseMessage(含 2 个 response)
- assertThat(history).hasSize(2);
- ToolResponseMessage trm = (ToolResponseMessage) history.get(1);
- assertThat(trm.getResponses()).hasSize(2);
- }
-
- @Test
- void 坏的toolCallsJson退化为纯文字assistant() {
- List rows = List.of(
- row("assistant", "兜底文字", "{不是合法json", null));
-
- List history = serviceReturning(rows).loadHistory(1L);
-
- assertThat(history).hasSize(1);
- AssistantMessage assistant = (AssistantMessage) history.get(0);
- assertThat(assistant.getMessageType()).isEqualTo(MessageType.ASSISTANT);
- assertThat(assistant.getText()).isEqualTo("兜底文字");
- // 反序列化失败:退化成无 tool_calls
- assertThat(assistant.getToolCalls()).isEmpty();
- }
-}