Compare commits
11 Commits
c027df0c6f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a7c6f47c12 | |||
| f10cd4cd9a | |||
| 7aa483f003 | |||
| 1acfb6486b | |||
| 00ea452310 | |||
| 7440da9d23 | |||
| 0835732aba | |||
| c27a12f56c | |||
| 90929d0bfb | |||
| ed905e419d | |||
| 6972717193 |
@@ -1,101 +0,0 @@
|
||||
name: Bug Report
|
||||
description: File a bug report to help us improve
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Hermes Web UI Version
|
||||
description: What version of Hermes Web UI are you using?
|
||||
placeholder: e.g., v0.5.8
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: hermes_version
|
||||
attributes:
|
||||
label: Hermes Agent Version
|
||||
description: What version of Hermes Agent are you using?
|
||||
placeholder: e.g., v0.12.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: A clear and concise description of what the bug is
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What you expected to happen
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: What actually happened
|
||||
placeholder: |
|
||||
If applicable, add screenshots to help explain your problem
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs / Error Messages
|
||||
description: Paste any relevant logs or error messages
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Where are you running Hermes Web UI?
|
||||
options:
|
||||
- Docker
|
||||
- macOS
|
||||
- Linux
|
||||
- Windows
|
||||
- WSL
|
||||
multiple: true
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: node_version
|
||||
attributes:
|
||||
label: Node Version
|
||||
description: What version of Node.js are you using?
|
||||
placeholder: e.g., v24.14.1
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Any other context about the problem
|
||||
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://github.com/EKKOLearnAI/hermes-web-ui#readme
|
||||
about: Please check the documentation first
|
||||
- name: GitHub Discussions
|
||||
url: https://github.com/EKKOLearnAI/hermes-web-ui/discussions
|
||||
about: Use GitHub Discussions for questions that don't fit as issues
|
||||
@@ -1,76 +0,0 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for suggesting a new feature! Please fill out the form below.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem Statement
|
||||
description: What problem does this feature solve? What pain point does it address?
|
||||
placeholder: |
|
||||
I'm always frustrated when...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: What would you like to see implemented?
|
||||
placeholder: |
|
||||
I think adding X would be great because...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: Have you considered any alternative solutions or workarounds?
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: Priority
|
||||
description: How important is this feature to you?
|
||||
options:
|
||||
- Critical - blocking my usage
|
||||
- High - really need this
|
||||
- Medium - nice to have
|
||||
- Low - would be convenient
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: use_cases
|
||||
attributes:
|
||||
label: Use Cases
|
||||
description: Describe specific use cases where this feature would be helpful
|
||||
placeholder: |
|
||||
1. When I do X...
|
||||
2. When I need to Y...
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution
|
||||
attributes:
|
||||
label: Willing to Contribute?
|
||||
description: Would you be willing to help implement this feature?
|
||||
options:
|
||||
- label: Yes, I'd like to submit a PR
|
||||
required: false
|
||||
- label: Yes, but I need guidance
|
||||
required: false
|
||||
- label: No, I don't have time
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Any other context, mockups, or examples about the feature request
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: General Issue
|
||||
about: Use this for issues that don't fit into bug reports or feature requests
|
||||
title: '[Question]: '
|
||||
labels: ['question']
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Please describe your issue
|
||||
|
||||
<!-- Provide a clear description of what you'd like to ask or discuss -->
|
||||
|
||||
## Context
|
||||
|
||||
<!-- Add any other context or screenshots about the issue -->
|
||||
|
||||
## Environment (if applicable)
|
||||
|
||||
- Hermes Web UI Version:
|
||||
- Hermes Agent Version:
|
||||
- Operating System:
|
||||
- Node Version:
|
||||
@@ -1,113 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- base
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci --ignore-scripts
|
||||
npm rebuild node-pty
|
||||
|
||||
- name: Check repository harness
|
||||
run: npm run harness:check
|
||||
|
||||
- name: Test with coverage
|
||||
run: npm run test:coverage
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
# Smoke test only: verify desktop packaging still works on pull requests.
|
||||
# Full multi-platform release artifacts are built by desktop-release.yml on release.
|
||||
desktop:
|
||||
name: Desktop smoke test (${{ matrix.label }})
|
||||
needs: build
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- label: Linux x64
|
||||
runner: ubuntu-22.04
|
||||
target_os: linux
|
||||
target_arch: x64
|
||||
electron_target: "--linux AppImage deb --x64"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
packages/desktop/package-lock.json
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Install web UI dependencies
|
||||
run: |
|
||||
npm ci --ignore-scripts
|
||||
npm rebuild node-pty
|
||||
|
||||
- name: Build web UI
|
||||
run: npm run build
|
||||
|
||||
- name: Keep production web UI dependencies only
|
||||
run: npm prune --omit=dev --no-audit --no-fund
|
||||
|
||||
- name: Install desktop dependencies
|
||||
run: npm ci --prefix packages/desktop --no-audit --no-fund
|
||||
|
||||
- name: Prepare bundled Python
|
||||
env:
|
||||
TARGET_OS: ${{ matrix.target_os }}
|
||||
TARGET_ARCH: ${{ matrix.target_arch }}
|
||||
run: npm --prefix packages/desktop run prepare:python
|
||||
|
||||
- name: Build desktop artifact
|
||||
run: npm --prefix packages/desktop run dist -- ${{ matrix.electron_target }} --publish never
|
||||
|
||||
- name: Upload desktop artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: desktop-${{ matrix.target_os }}-${{ matrix.target_arch }}
|
||||
path: |
|
||||
packages/desktop/release/*.AppImage
|
||||
packages/desktop/release/*.deb
|
||||
packages/desktop/release/latest*.yml
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
@@ -1,207 +0,0 @@
|
||||
name: Manual Desktop Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_os:
|
||||
description: "Desktop target OS"
|
||||
required: true
|
||||
type: choice
|
||||
default: win32
|
||||
options:
|
||||
- win32
|
||||
- darwin
|
||||
- linux
|
||||
target_arch:
|
||||
description: "Desktop target architecture"
|
||||
required: true
|
||||
type: choice
|
||||
default: x64
|
||||
options:
|
||||
- x64
|
||||
- arm64
|
||||
release_tag:
|
||||
description: "Optional release tag to attach artifacts to"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: desktop-manual-${{ github.event.inputs.target_os }}-${{ github.event.inputs.target_arch }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
label: ${{ steps.target.outputs.label }}
|
||||
runner: ${{ steps.target.outputs.runner }}
|
||||
target_os: ${{ steps.target.outputs.target_os }}
|
||||
target_arch: ${{ steps.target.outputs.target_arch }}
|
||||
electron_target: ${{ steps.target.outputs.electron_target }}
|
||||
artifact_name: ${{ steps.target.outputs.artifact_name }}
|
||||
artifact_files: ${{ steps.target.outputs.artifact_files }}
|
||||
steps:
|
||||
- name: Select requested target
|
||||
id: target
|
||||
shell: bash
|
||||
run: |
|
||||
write_common_outputs() {
|
||||
{
|
||||
echo "label=$1"
|
||||
echo "runner=$2"
|
||||
echo "target_os=${{ github.event.inputs.target_os }}"
|
||||
echo "target_arch=${{ github.event.inputs.target_arch }}"
|
||||
echo "electron_target=$3"
|
||||
echo "artifact_name=$4"
|
||||
echo "artifact_files<<EOF"
|
||||
shift 4
|
||||
printf '%s\n' "$@"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
}
|
||||
|
||||
case "${{ github.event.inputs.target_os }}-${{ github.event.inputs.target_arch }}" in
|
||||
win32-x64)
|
||||
write_common_outputs "Windows x64" "windows-latest" "--win nsis --x64" "desktop-win32-x64" \
|
||||
"packages/desktop/release/*.exe" \
|
||||
"packages/desktop/release/*.exe.blockmap" \
|
||||
"packages/desktop/release/latest*.yml"
|
||||
;;
|
||||
darwin-arm64)
|
||||
write_common_outputs "macOS arm64" "macos-14" "--mac dmg zip --arm64" "desktop-darwin-arm64" \
|
||||
"packages/desktop/release/*.dmg" \
|
||||
"packages/desktop/release/*.dmg.blockmap" \
|
||||
"packages/desktop/release/*.zip" \
|
||||
"packages/desktop/release/*.zip.blockmap" \
|
||||
"packages/desktop/release/latest*.yml"
|
||||
;;
|
||||
darwin-x64)
|
||||
write_common_outputs "macOS x64" "macos-15-intel" "--mac dmg zip --x64" "desktop-darwin-x64" \
|
||||
"packages/desktop/release/*.dmg" \
|
||||
"packages/desktop/release/*.dmg.blockmap" \
|
||||
"packages/desktop/release/*.zip" \
|
||||
"packages/desktop/release/*.zip.blockmap" \
|
||||
"packages/desktop/release/latest*.yml"
|
||||
;;
|
||||
linux-x64)
|
||||
write_common_outputs "Linux x64" "ubuntu-22.04" "--linux AppImage deb --x64" "desktop-linux-x64" \
|
||||
"packages/desktop/release/*.AppImage" \
|
||||
"packages/desktop/release/*.deb" \
|
||||
"packages/desktop/release/latest*.yml"
|
||||
;;
|
||||
linux-arm64)
|
||||
write_common_outputs "Linux arm64" "ubuntu-22.04-arm" "--linux AppImage --arm64" "desktop-linux-arm64" \
|
||||
"packages/desktop/release/*.AppImage" \
|
||||
"packages/desktop/release/latest*.yml"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported desktop target: ${{ github.event.inputs.target_os }} ${{ github.event.inputs.target_arch }}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
desktop:
|
||||
name: Desktop (${{ needs.validate.outputs.label }})
|
||||
needs: validate
|
||||
runs-on: ${{ needs.validate.outputs.runner }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
packages/desktop/package-lock.json
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Install web UI dependencies
|
||||
run: |
|
||||
npm ci --ignore-scripts
|
||||
npm rebuild node-pty
|
||||
|
||||
- name: Build web UI
|
||||
run: npm run build
|
||||
|
||||
- name: Keep production web UI dependencies only
|
||||
run: npm prune --omit=dev --no-audit --no-fund
|
||||
|
||||
- name: Install desktop dependencies
|
||||
run: npm ci --prefix packages/desktop --no-audit --no-fund
|
||||
|
||||
- name: Prepare bundled Python
|
||||
env:
|
||||
TARGET_OS: ${{ needs.validate.outputs.target_os }}
|
||||
TARGET_ARCH: ${{ needs.validate.outputs.target_arch }}
|
||||
run: npm --prefix packages/desktop run prepare:python
|
||||
|
||||
- name: Configure macOS signing
|
||||
if: needs.validate.outputs.target_os == 'darwin'
|
||||
shell: bash
|
||||
env:
|
||||
MAC_CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
|
||||
MAC_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
|
||||
MAC_APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
MAC_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
MAC_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
write_env() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
if [ -n "$value" ]; then
|
||||
{
|
||||
echo "$name<<EOF"
|
||||
echo "$value"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_ENV"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -z "${MAC_CSC_LINK:-}" ]; then
|
||||
echo "CSC_IDENTITY_AUTO_DISCOVERY=false" >> "$GITHUB_ENV"
|
||||
echo "MAC_BUILD_EXTRA_ARGS=--config.mac.notarize=false" >> "$GITHUB_ENV"
|
||||
echo "No macOS signing certificate configured; building unsigned and skipping notarization."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
write_env "CSC_LINK" "$MAC_CSC_LINK"
|
||||
write_env "CSC_KEY_PASSWORD" "$MAC_CSC_KEY_PASSWORD"
|
||||
|
||||
if [ -n "${MAC_APPLE_ID:-}" ] && [ -n "${MAC_APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${MAC_APPLE_TEAM_ID:-}" ]; then
|
||||
write_env "APPLE_ID" "$MAC_APPLE_ID"
|
||||
write_env "APPLE_APP_SPECIFIC_PASSWORD" "$MAC_APPLE_APP_SPECIFIC_PASSWORD"
|
||||
write_env "APPLE_TEAM_ID" "$MAC_APPLE_TEAM_ID"
|
||||
echo "macOS signing and notarization are configured."
|
||||
else
|
||||
echo "MAC_BUILD_EXTRA_ARGS=--config.mac.notarize=false" >> "$GITHUB_ENV"
|
||||
echo "macOS signing certificate configured; Apple notarization credentials incomplete, skipping notarization."
|
||||
fi
|
||||
|
||||
- name: Build desktop artifact
|
||||
shell: bash
|
||||
run: npm --prefix packages/desktop run dist -- ${{ needs.validate.outputs.electron_target }} ${MAC_BUILD_EXTRA_ARGS:-} --publish never
|
||||
|
||||
- name: Upload workflow artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ needs.validate.outputs.artifact_name }}
|
||||
path: ${{ needs.validate.outputs.artifact_files }}
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload artifacts to release
|
||||
if: github.event.inputs.release_tag != ''
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.release_tag }}
|
||||
fail_on_unmatched_files: true
|
||||
files: ${{ needs.validate.outputs.artifact_files }}
|
||||
@@ -1,204 +0,0 @@
|
||||
name: Publish Desktop Artifacts to Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Existing release tag to attach artifacts to (e.g. v0.6.5)"
|
||||
required: true
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: desktop-release-${{ github.event.release.tag_name || github.event.inputs.tag || github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
desktop:
|
||||
name: Desktop (${{ matrix.label }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- label: macOS arm64
|
||||
runner: macos-14
|
||||
target_os: darwin
|
||||
target_arch: arm64
|
||||
electron_target: "--mac dmg zip --arm64"
|
||||
artifact_files: |
|
||||
packages/desktop/release/*.dmg
|
||||
packages/desktop/release/*.dmg.blockmap
|
||||
packages/desktop/release/*.zip
|
||||
packages/desktop/release/*.zip.blockmap
|
||||
- label: macOS x64
|
||||
runner: macos-15-intel
|
||||
target_os: darwin
|
||||
target_arch: x64
|
||||
electron_target: "--mac dmg zip --x64"
|
||||
artifact_files: |
|
||||
packages/desktop/release/*.dmg
|
||||
packages/desktop/release/*.dmg.blockmap
|
||||
packages/desktop/release/*.zip
|
||||
packages/desktop/release/*.zip.blockmap
|
||||
- label: Windows x64
|
||||
runner: windows-latest
|
||||
target_os: win32
|
||||
target_arch: x64
|
||||
electron_target: "--win nsis --x64"
|
||||
artifact_files: |
|
||||
packages/desktop/release/*.exe
|
||||
packages/desktop/release/*.exe.blockmap
|
||||
packages/desktop/release/latest*.yml
|
||||
- label: Linux x64
|
||||
runner: ubuntu-22.04
|
||||
target_os: linux
|
||||
target_arch: x64
|
||||
electron_target: "--linux AppImage deb --x64"
|
||||
artifact_files: |
|
||||
packages/desktop/release/*.AppImage
|
||||
packages/desktop/release/*.deb
|
||||
packages/desktop/release/latest*.yml
|
||||
- label: Linux arm64
|
||||
runner: ubuntu-22.04-arm
|
||||
target_os: linux
|
||||
target_arch: arm64
|
||||
electron_target: "--linux AppImage --arm64"
|
||||
artifact_files: |
|
||||
packages/desktop/release/*.AppImage
|
||||
packages/desktop/release/latest*.yml
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name || github.event.inputs.tag }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
packages/desktop/package-lock.json
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Install web UI dependencies
|
||||
run: |
|
||||
npm ci --ignore-scripts
|
||||
npm rebuild node-pty
|
||||
|
||||
- name: Build web UI
|
||||
run: npm run build
|
||||
|
||||
- name: Keep production web UI dependencies only
|
||||
run: npm prune --omit=dev --no-audit --no-fund
|
||||
|
||||
- name: Install desktop dependencies
|
||||
run: npm ci --prefix packages/desktop --no-audit --no-fund
|
||||
|
||||
- name: Prepare bundled Python
|
||||
env:
|
||||
TARGET_OS: ${{ matrix.target_os }}
|
||||
TARGET_ARCH: ${{ matrix.target_arch }}
|
||||
run: npm --prefix packages/desktop run prepare:python
|
||||
|
||||
- name: Configure macOS signing
|
||||
if: matrix.target_os == 'darwin'
|
||||
shell: bash
|
||||
env:
|
||||
MAC_CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
|
||||
MAC_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
|
||||
MAC_APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
MAC_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
MAC_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
write_env() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
if [ -n "$value" ]; then
|
||||
{
|
||||
echo "$name<<EOF"
|
||||
echo "$value"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_ENV"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -z "${MAC_CSC_LINK:-}" ]; then
|
||||
echo "CSC_IDENTITY_AUTO_DISCOVERY=false" >> "$GITHUB_ENV"
|
||||
echo "MAC_BUILD_EXTRA_ARGS=--config.mac.notarize=false" >> "$GITHUB_ENV"
|
||||
echo "No macOS signing certificate configured; building unsigned and skipping notarization."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
write_env "CSC_LINK" "$MAC_CSC_LINK"
|
||||
write_env "CSC_KEY_PASSWORD" "$MAC_CSC_KEY_PASSWORD"
|
||||
|
||||
if [ -n "${MAC_APPLE_ID:-}" ] && [ -n "${MAC_APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${MAC_APPLE_TEAM_ID:-}" ]; then
|
||||
write_env "APPLE_ID" "$MAC_APPLE_ID"
|
||||
write_env "APPLE_APP_SPECIFIC_PASSWORD" "$MAC_APPLE_APP_SPECIFIC_PASSWORD"
|
||||
write_env "APPLE_TEAM_ID" "$MAC_APPLE_TEAM_ID"
|
||||
echo "macOS signing and notarization are configured."
|
||||
else
|
||||
echo "MAC_BUILD_EXTRA_ARGS=--config.mac.notarize=false" >> "$GITHUB_ENV"
|
||||
echo "macOS signing certificate configured; Apple notarization credentials incomplete, skipping notarization."
|
||||
fi
|
||||
|
||||
- name: Build desktop artifact
|
||||
shell: bash
|
||||
run: npm --prefix packages/desktop run dist -- ${{ matrix.electron_target }} ${MAC_BUILD_EXTRA_ARGS:-} --publish never
|
||||
|
||||
- name: Upload macOS update manifest artifact
|
||||
if: matrix.target_os == 'darwin'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: latest-mac-${{ matrix.target_arch }}
|
||||
path: packages/desktop/release/latest-mac.yml
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload artifacts to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.release.tag_name || github.event.inputs.tag }}
|
||||
fail_on_unmatched_files: true
|
||||
files: ${{ matrix.artifact_files }}
|
||||
|
||||
mac-update-manifest:
|
||||
name: Merge macOS updater manifest
|
||||
needs: desktop
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name || github.event.inputs.tag }}
|
||||
|
||||
- name: Download macOS update manifests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: latest-mac-*
|
||||
path: /tmp/hermes-mac-manifests
|
||||
merge-multiple: false
|
||||
|
||||
- name: Merge macOS update manifests
|
||||
shell: bash
|
||||
run: |
|
||||
node packages/desktop/scripts/merge-mac-latest-yml.mjs \
|
||||
/tmp/hermes-mac-manifests/latest-mac-arm64/latest-mac.yml \
|
||||
/tmp/hermes-mac-manifests/latest-mac-x64/latest-mac.yml \
|
||||
> /tmp/latest-mac.yml
|
||||
|
||||
- name: Upload merged macOS updater manifest to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.release.tag_name || github.event.inputs.tag }}
|
||||
fail_on_unmatched_files: true
|
||||
files: /tmp/latest-mac.yml
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Build and Push Docker Image to Docker Hub
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docker-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/hermes-web-ui:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/hermes-web-ui:${{ github.sha }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/hermes-web-ui:${{ github.event.release.tag_name || github.ref_name }}
|
||||
@@ -1,45 +0,0 @@
|
||||
name: NPM Lockfile Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- package.json
|
||||
- package-lock.json
|
||||
- .github/workflows/npm-lockfile-check.yml
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- base
|
||||
paths:
|
||||
- package.json
|
||||
- package-lock.json
|
||||
- .github/workflows/npm-lockfile-check.yml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: npm-lockfile-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: npm ci --ignore-scripts
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Verify package-lock.json is in sync
|
||||
run: npm ci --ignore-scripts
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Playwright
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: playwright-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci --ignore-scripts
|
||||
npm rebuild node-pty
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run test:e2e
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: |
|
||||
playwright-report/
|
||||
test-results/
|
||||
retention-days: 7
|
||||
@@ -1,89 +0,0 @@
|
||||
name: Website
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- base
|
||||
paths:
|
||||
- packages/website/**
|
||||
- packages/client/src/styles/variables.scss
|
||||
- package.json
|
||||
- package-lock.json
|
||||
- tsconfig.website.json
|
||||
- vite.config.website.ts
|
||||
- .github/workflows/website-deploy.yml
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Publish Desktop Artifacts to Release
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: website-${{ github.event.pull_request.number || github.event.workflow_run.id || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build website
|
||||
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
if: github.event_name != 'workflow_run'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout desktop release ref
|
||||
if: github.event_name == 'workflow_run'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_branch || github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Type-check website
|
||||
run: npx vue-tsc -p tsconfig.website.json --noEmit
|
||||
|
||||
- name: Build website
|
||||
run: npm run build:website
|
||||
|
||||
- name: Prepare SSH
|
||||
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
WEBSITE_SSH_KEY: ${{ secrets.WEBSITE_SSH_KEY }}
|
||||
WEBSITE_SSH_KNOWN_HOSTS: ${{ secrets.WEBSITE_SSH_KNOWN_HOSTS }}
|
||||
run: |
|
||||
test -n "$WEBSITE_SSH_KEY"
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
printf '%s\n' "$WEBSITE_SSH_KEY" > ~/.ssh/website_deploy_key
|
||||
chmod 600 ~/.ssh/website_deploy_key
|
||||
if [ -n "$WEBSITE_SSH_KNOWN_HOSTS" ]; then
|
||||
printf '%s\n' "$WEBSITE_SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||
fi
|
||||
|
||||
- name: Deploy website
|
||||
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
WEBSITE_SSH_USER: ${{ secrets.WEBSITE_SSH_USER }}
|
||||
WEBSITE_SSH_PORT: ${{ secrets.WEBSITE_SSH_PORT }}
|
||||
run: |
|
||||
SSH_USER="${WEBSITE_SSH_USER:-root}"
|
||||
SSH_PORT="${WEBSITE_SSH_PORT:-22}"
|
||||
DEPLOY_DIR="/var/www/ekkolearnai.com/current"
|
||||
SSH_CMD="ssh -i ~/.ssh/website_deploy_key -p ${SSH_PORT} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
|
||||
$SSH_CMD "$SSH_USER@154.3.33.232" "mkdir -p '$DEPLOY_DIR' && find '$DEPLOY_DIR' -mindepth 1 -maxdepth 1 -exec rm -rf {} +"
|
||||
tar -C dist/website -czf - . | $SSH_CMD "$SSH_USER@154.3.33.232" "tar -xzf - -C '$DEPLOY_DIR'"
|
||||
@@ -0,0 +1,19 @@
|
||||
=== 改造日志 ===
|
||||
时间: 2026-06-02 03:44:23
|
||||
源: https://github.com/EKKOLearnAI/hermes-web-ui
|
||||
目标: http://192.168.6.101:3001/root/Hermes-ui
|
||||
|
||||
## 执行操作
|
||||
- 替换 github.com → www.xinmi.cloud
|
||||
- 替换 ekkolearnai.com → www.xinmi.cloud
|
||||
- 替换 EKKOLearnAI → root
|
||||
- 替换 GitHub/GitLab → 新觅源码库
|
||||
- 删除 .github 目录
|
||||
- 更新 package.json repository/homepage
|
||||
- 更新侧边栏链接
|
||||
- 保留 LICENSE 原文
|
||||
- 跳过 AI 页面删除(本项目为 AI 应用)
|
||||
|
||||
## 保留内容
|
||||
- LICENSE: 保留原始版权声明
|
||||
- 所有 AI 对话功能: 本项目核心功能完整保留
|
||||
+3
-2
@@ -70,8 +70,9 @@ Frontend rules:
|
||||
|
||||
Desktop packaging is intentionally split:
|
||||
|
||||
- Pull requests run a Linux desktop smoke test in `.github/workflows/build.yml`.
|
||||
- Published releases and manual dispatches run `.github/workflows/desktop-release.yml`.
|
||||
- Pull requests run the web UI build and tests in `.github/workflows/build.yml`.
|
||||
- Published releases and manual dispatches run desktop artifact packaging in `.github/workflows/desktop-release.yml`
|
||||
and `.github/workflows/desktop-manual-build.yml`.
|
||||
- Each release matrix target uploads only the artifact globs for its own platform.
|
||||
|
||||
Do not make a Windows job require macOS `.dmg` files or a Linux job require
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
ARG BASE_IMAGE=nousresearch/hermes-agent:latest
|
||||
ARG BASE_IMAGE=xinmi/hermes-agent:latest
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG NODE_VERSION=24.15.0
|
||||
|
||||
@@ -4,29 +4,29 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
A full-featured desktop app and web dashboard for <a href="https://github.com/NousResearch/hermes-agent">Hermes Agent</a>.<br/>
|
||||
A full-featured desktop app and web dashboard for <a href="https://www.xinmi.cloud/NousResearch/hermes-agent">Hermes Agent</a>.<br/>
|
||||
Manage AI chat sessions, monitor usage & costs, configure platform channels,<br/>
|
||||
schedule cron jobs, browse skills — all from a clean, responsive web interface.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest">Download Hermes Studio Desktop</a>
|
||||
<a href="https://www.xinmi.cloud/root/Hermes-ui/releases/latest">Download Hermes Studio Desktop</a>
|
||||
·
|
||||
<code>npm install -g hermes-web-ui && hermes-web-ui start</code>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/packages/client/src/assets/image1.png" alt="Hermes Web UI Demo" width="680"/>
|
||||
<img src="https://www.xinmi.cloud/root/Hermes-ui/blob/main/packages/client/src/assets/image1.png" alt="Hermes Web UI Demo" width="680"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/packages/client/src/assets/image2.png" alt="Hermes Web UI Demo" width="680"/>
|
||||
<img src="https://www.xinmi.cloud/root/Hermes-ui/blob/main/packages/client/src/assets/image2.png" alt="Hermes Web UI Demo" width="680"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/hermes-web-ui"><img src="https://img.shields.io/npm/v/hermes-web-ui?style=flat-square&color=blue" alt="npm version"/></a>
|
||||
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hermes-web-ui?style=flat-square" alt="license"/></a>
|
||||
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/stargazers"><img src="https://img.shields.io/github/stars/EKKOLearnAI/hermes-web-ui?style=flat-square" alt="stars"/></a>
|
||||
<a href="https://www.xinmi.cloud/root/Hermes-ui/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hermes-web-ui?style=flat-square" alt="license"/></a>
|
||||
<a href="https://www.xinmi.cloud/root/Hermes-ui/stargazers"><img src="https://img.shields.io/github/stars/EKKOLearnAI/hermes-web-ui?style=flat-square" alt="stars"/></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -176,7 +176,7 @@ hermes-web-ui reset-default-login
|
||||
### Desktop App (Recommended)
|
||||
|
||||
Download the latest **Hermes Studio** desktop installer from
|
||||
[GitHub Releases](https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest).
|
||||
[新觅源码库 Releases](https://www.xinmi.cloud/root/Hermes-ui/releases/latest).
|
||||
|
||||
Desktop builds are published for macOS, Windows, and Linux, with separate
|
||||
architecture assets where applicable. The desktop app bundles the Web UI
|
||||
@@ -268,13 +268,13 @@ These variables configure Hermes Web UI, its local Hermes runtime integration, a
|
||||
| `HERMES_BRIDGE_TOOLSETS` | profile/default | Toolset override for bridge runs. |
|
||||
| `HERMES_BRIDGE_MAX_TURNS` | profile/default | Maximum turn override for bridge runs. |
|
||||
| `HERMES_BRIDGE_SUPPRESS_PLATFORM_HINT` | `cli` | Controls bridge platform hint suppression passed to Hermes Agent. |
|
||||
| `HERMES_OPENROUTER_APP_REFERER` | `https://ekkolearnai.com` | OpenRouter attribution referer sent by bridge runs. |
|
||||
| `HERMES_OPENROUTER_APP_REFERER` | `https://www.xinmi.cloud` | OpenRouter attribution referer sent by bridge runs. |
|
||||
| `HERMES_OPENROUTER_APP_TITLE` | `Hermes Web UI` | OpenRouter attribution title sent by bridge runs. |
|
||||
| `HERMES_OPENROUTER_APP_CATEGORIES` | `cli-agent,personal-agent` | OpenRouter attribution categories sent by bridge runs. |
|
||||
| `HERMES_WEB_UI_MANAGED_GATEWAY` | platform/runtime dependent | Force managed legacy gateway process handling. Set `1`, `true`, `yes`, or `on` to enable. |
|
||||
| `HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN` | enabled in production | Controls whether Web UI shutdown also stops managed gateway processes. Set `0` or `false` to detach them. |
|
||||
| `GATEWAY_HOST` | `127.0.0.1` | Default gateway host written into profile config for legacy gateway compatibility. |
|
||||
| `HERMES_WEB_UI_PREVIEW_REPO` | package repository | GitHub repository used by Version Preview. |
|
||||
| `HERMES_WEB_UI_PREVIEW_REPO` | package repository | 新觅源码库 repository used by Version Preview. |
|
||||
| `HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_TRANSPORT` | platform default | Version Preview broker transport. Set `tcp` to use loopback TCP for Preview on macOS/Linux; when unset, Preview follows `HERMES_AGENT_BRIDGE_WORKER_TRANSPORT=tcp`. |
|
||||
| `HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_ENDPOINT` | isolated preview endpoint | Directly overrides the Version Preview broker endpoint. |
|
||||
| `HERMES_WEB_UI_BACKEND_PORT` | `8648` | Backend port used by the Vite dev proxy. |
|
||||
@@ -309,7 +309,7 @@ On startup the BFF server automatically:
|
||||
## Development
|
||||
|
||||
```bash
|
||||
git clone https://github.com/EKKOLearnAI/hermes-web-ui.git
|
||||
git clone https://www.xinmi.cloud/root/Hermes-ui.git
|
||||
cd hermes-web-ui
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
+11
-11
@@ -4,23 +4,23 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/NousResearch/hermes-agent">Hermes Agent</a> 的全功能桌面应用和 Web 管理面板。<br/>
|
||||
<a href="https://www.xinmi.cloud/NousResearch/hermes-agent">Hermes Agent</a> 的全功能桌面应用和 Web 管理面板。<br/>
|
||||
管理 AI 聊天会话、监控用量与成本、配置平台渠道、<br/>
|
||||
管理定时任务、浏览技能 —— 全部在一个简洁响应式的 Web 界面中完成。
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest">下载 Hermes Studio 桌面版</a>
|
||||
<a href="https://www.xinmi.cloud/root/Hermes-ui/releases/latest">下载 Hermes Studio 桌面版</a>
|
||||
·
|
||||
<code>npm install -g hermes-web-ui && hermes-web-ui start</code>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/packages/client/src/assets/image1.png" alt="Hermes Web UI 演示" width="680"/>
|
||||
<img src="https://www.xinmi.cloud/root/Hermes-ui/blob/main/packages/client/src/assets/image1.png" alt="Hermes Web UI 演示" width="680"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/packages/client/src/assets/image2.png" alt="Hermes Web UI 演示" width="680"/>
|
||||
<img src="https://www.xinmi.cloud/root/Hermes-ui/blob/main/packages/client/src/assets/image2.png" alt="Hermes Web UI 演示" width="680"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -28,13 +28,13 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<video src="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/packages/client/src/assets/video.mp4?raw=true" width="360" controls></video>
|
||||
<video src="https://www.xinmi.cloud/root/Hermes-ui/blob/main/packages/client/src/assets/video.mp4?raw=true" width="360" controls></video>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/hermes-web-ui"><img src="https://img.shields.io/npm/v/hermes-web-ui?style=flat-square&color=blue" alt="npm 版本"/></a>
|
||||
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hermes-web-ui?style=flat-square" alt="许可证"/></a>
|
||||
<a href="https://github.com/EKKOLearnAI/hermes-web-ui/stargazers"><img src="https://img.shields.io/github/stars/EKKOLearnAI/hermes-web-ui?style=flat-square" alt="Star"/></a>
|
||||
<a href="https://www.xinmi.cloud/root/Hermes-ui/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/hermes-web-ui?style=flat-square" alt="许可证"/></a>
|
||||
<a href="https://www.xinmi.cloud/root/Hermes-ui/stargazers"><img src="https://img.shields.io/github/stars/EKKOLearnAI/hermes-web-ui?style=flat-square" alt="Star"/></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -183,7 +183,7 @@ hermes-web-ui reset-default-login
|
||||
|
||||
### 桌面应用(推荐)
|
||||
|
||||
从 [GitHub Releases](https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest)
|
||||
从 [新觅源码库 Releases](https://www.xinmi.cloud/root/Hermes-ui/releases/latest)
|
||||
下载最新的 **Hermes Studio** 桌面安装包。
|
||||
|
||||
桌面版会发布 macOS、Windows 和 Linux 构建;适用时会区分不同 CPU 架构。
|
||||
@@ -274,13 +274,13 @@ Web UI 启动后端聊天能力时,会优先使用包含 `run_agent.py` 的源
|
||||
| `HERMES_BRIDGE_TOOLSETS` | profile/默认值 | bridge 运行时的 toolset 覆盖。 |
|
||||
| `HERMES_BRIDGE_MAX_TURNS` | profile/默认值 | bridge 运行时的最大轮数覆盖。 |
|
||||
| `HERMES_BRIDGE_SUPPRESS_PLATFORM_HINT` | `cli` | 控制传给 Hermes Agent 的 bridge platform hint suppression。 |
|
||||
| `HERMES_OPENROUTER_APP_REFERER` | `https://ekkolearnai.com` | bridge 运行发送给 OpenRouter 的 attribution referer。 |
|
||||
| `HERMES_OPENROUTER_APP_REFERER` | `https://www.xinmi.cloud` | bridge 运行发送给 OpenRouter 的 attribution referer。 |
|
||||
| `HERMES_OPENROUTER_APP_TITLE` | `Hermes Web UI` | bridge 运行发送给 OpenRouter 的 attribution title。 |
|
||||
| `HERMES_OPENROUTER_APP_CATEGORIES` | `cli-agent,personal-agent` | bridge 运行发送给 OpenRouter 的 attribution categories。 |
|
||||
| `HERMES_WEB_UI_MANAGED_GATEWAY` | 由平台/运行环境决定 | 强制启用旧 gateway 进程托管;设为 `1`、`true`、`yes` 或 `on` 开启。 |
|
||||
| `HERMES_WEB_UI_STOP_GATEWAYS_ON_SHUTDOWN` | 生产环境默认开启 | Web UI 关闭时是否同时停止托管的 gateway 进程;设为 `0` 或 `false` 可让 gateway 分离运行。 |
|
||||
| `GATEWAY_HOST` | `127.0.0.1` | 旧 gateway 兼容配置中写入 profile 的默认 gateway host。 |
|
||||
| `HERMES_WEB_UI_PREVIEW_REPO` | package repository | Version Preview 使用的 GitHub 仓库。 |
|
||||
| `HERMES_WEB_UI_PREVIEW_REPO` | package repository | Version Preview 使用的 新觅源码库 仓库。 |
|
||||
| `HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_TRANSPORT` | 平台默认值 | Version Preview broker transport。设为 `tcp` 可让预览环境在 macOS/Linux 上也使用 loopback TCP;未设置时会跟随 `HERMES_AGENT_BRIDGE_WORKER_TRANSPORT=tcp`。 |
|
||||
| `HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_ENDPOINT` | 隔离的预览 endpoint | 直接覆盖 Version Preview 的 broker endpoint。 |
|
||||
| `HERMES_WEB_UI_BACKEND_PORT` | `8648` | Vite dev proxy 使用的后端端口。 |
|
||||
@@ -315,7 +315,7 @@ Web UI 启动后端聊天能力时,会优先使用包含 `run_agent.py` 的源
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
git clone https://github.com/EKKOLearnAI/hermes-web-ui.git
|
||||
git clone https://www.xinmi.cloud/root/Hermes-ui.git
|
||||
cd hermes-web-ui
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
services:
|
||||
hermes-webui:
|
||||
xinmi-hermes-ui:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: ${WEBUI_IMAGE:-hermes-web-ui-local:latest}
|
||||
container_name: ${WEBUI_CONTAINER_NAME:-hermes-webui}
|
||||
image: ${WEBUI_IMAGE:-xinmi-hermes-ui:latest}
|
||||
container_name: ${WEBUI_CONTAINER_NAME:-xinmi-hermes-ui}
|
||||
ports:
|
||||
- "${PORT:-6060}:${PORT:-6060}"
|
||||
- "${PREVIEW_FRONTEND_PORT:-8651}:8651"
|
||||
|
||||
@@ -28,17 +28,17 @@ npm run build
|
||||
| Auth, profile, or credential behavior | focused server tests plus relevant e2e auth tests |
|
||||
| Chat, Socket.IO, group chat | focused server tests plus relevant e2e chat tests |
|
||||
| Desktop packaging | `npm run harness:check`, `npm run build`, and a platform-specific desktop build when practical |
|
||||
| GitHub workflow | `npm run harness:check` and `actionlint` when available |
|
||||
| 新觅源码库 workflow | `npm run harness:check` and `actionlint` when available |
|
||||
| Package manifests | `npm ci --ignore-scripts` and lockfile workflow expectations |
|
||||
|
||||
## CI Mapping
|
||||
|
||||
- Build workflow: installs dependencies, runs coverage, builds production assets,
|
||||
then runs a Linux desktop smoke test on pull requests.
|
||||
- Build workflow: installs dependencies, runs coverage, and builds production
|
||||
assets on pushes and pull requests.
|
||||
- Playwright workflow: runs browser e2e tests.
|
||||
- NPM lockfile workflow: verifies `package-lock.json` is synchronized.
|
||||
- Desktop release workflow: builds and uploads platform-specific desktop artifacts
|
||||
for release tags.
|
||||
- Desktop release and manual desktop build workflows build and upload
|
||||
platform-specific desktop artifacts.
|
||||
- Docker workflow: builds and publishes release images.
|
||||
|
||||
## Release Workflow Guardrail
|
||||
|
||||
Generated
+103
-103
@@ -116,7 +116,7 @@
|
||||
"tinyexec": "^1.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
"url": "https://www.xinmi.cloud/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
@@ -280,7 +280,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
"url": "https://www.xinmi.cloud/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -300,7 +300,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
"url": "https://www.xinmi.cloud/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -324,7 +324,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
"url": "https://www.xinmi.cloud/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -352,7 +352,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
"url": "https://www.xinmi.cloud/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -375,7 +375,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
"url": "https://www.xinmi.cloud/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -400,7 +400,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
"url": "https://www.xinmi.cloud/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1299,7 +1299,7 @@
|
||||
"node": ">= 22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
"url": "https://www.xinmi.cloud/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/devtools-types": {
|
||||
@@ -1316,7 +1316,7 @@
|
||||
"node": ">= 22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
"url": "https://www.xinmi.cloud/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
@@ -1333,7 +1333,7 @@
|
||||
"node": ">= 22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
"url": "https://www.xinmi.cloud/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
@@ -1346,7 +1346,7 @@
|
||||
"node": ">= 22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
"url": "https://www.xinmi.cloud/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
@@ -1507,7 +1507,7 @@
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
"url": "https://www.xinmi.cloud/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
@@ -1528,7 +1528,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
"url": "https://www.xinmi.cloud/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
@@ -1852,7 +1852,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinia/testing": {
|
||||
@@ -1862,7 +1862,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
"url": "https://www.xinmi.cloud/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pinia": ">=3.0.4"
|
||||
@@ -3474,7 +3474,7 @@
|
||||
"integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
"https://github.com/sponsors/katex"
|
||||
"https://www.xinmi.cloud/sponsors/katex"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3600,7 +3600,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
@@ -3801,7 +3801,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
@@ -3816,7 +3816,7 @@
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
@@ -3937,7 +3937,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
"url": "https://www.xinmi.cloud/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
@@ -3979,7 +3979,7 @@
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
@@ -3989,7 +3989,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
"url": "https://www.xinmi.cloud/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
@@ -4139,7 +4139,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
@@ -4183,7 +4183,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
@@ -4303,7 +4303,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
@@ -4411,7 +4411,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/config-chain": {
|
||||
@@ -4491,7 +4491,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/cors": {
|
||||
@@ -5199,7 +5199,7 @@
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
"url": "https://www.xinmi.cloud/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns-tz": {
|
||||
@@ -5545,7 +5545,7 @@
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
@@ -5904,7 +5904,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
"url": "https://www.xinmi.cloud/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
@@ -5931,7 +5931,7 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
@@ -6015,7 +6015,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generator-function": {
|
||||
@@ -6059,7 +6059,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
@@ -6095,7 +6095,7 @@
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
@@ -6121,7 +6121,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
@@ -6180,7 +6180,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
@@ -6196,7 +6196,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
@@ -6401,7 +6401,7 @@
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
"url": "https://www.xinmi.cloud/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/inflation": {
|
||||
@@ -6486,7 +6486,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
@@ -6525,7 +6525,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
@@ -6574,7 +6574,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
@@ -6587,7 +6587,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
@@ -6674,7 +6674,7 @@
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
@@ -6834,7 +6834,7 @@
|
||||
"integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==",
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
"https://github.com/sponsors/katex"
|
||||
"https://www.xinmi.cloud/sponsors/katex"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7317,11 +7317,11 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
"url": "https://www.xinmi.cloud/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/markdown-it"
|
||||
"url": "https://www.xinmi.cloud/sponsors/markdown-it"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
@@ -7415,7 +7415,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
@@ -7433,11 +7433,11 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
"url": "https://www.xinmi.cloud/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/markdown-it"
|
||||
"url": "https://www.xinmi.cloud/sponsors/markdown-it"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
@@ -7463,7 +7463,7 @@
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
@@ -7526,7 +7526,7 @@
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-anything/node_modules/is-what": {
|
||||
@@ -7539,7 +7539,7 @@
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
@@ -7549,7 +7549,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid": {
|
||||
@@ -7599,7 +7599,7 @@
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
"https://github.com/sponsors/katex"
|
||||
"https://www.xinmi.cloud/sponsors/katex"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7666,7 +7666,7 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
@@ -7676,7 +7676,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
@@ -7797,7 +7797,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
@@ -7951,7 +7951,7 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/supports-color": {
|
||||
@@ -8012,7 +8012,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-exit-leak-free": {
|
||||
@@ -8067,7 +8067,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
@@ -8117,7 +8117,7 @@
|
||||
"entities": "^8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5/node_modules/entities": {
|
||||
@@ -8130,7 +8130,7 @@
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
@@ -8208,7 +8208,7 @@
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||
@@ -8270,7 +8270,7 @@
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
@@ -8283,7 +8283,7 @@
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
"url": "https://www.xinmi.cloud/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.5.0",
|
||||
@@ -8451,7 +8451,7 @@
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
@@ -8472,7 +8472,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
"url": "https://www.xinmi.cloud/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -8697,7 +8697,7 @@
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-format-unescaped": {
|
||||
@@ -8821,7 +8821,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-path": {
|
||||
@@ -8939,7 +8939,7 @@
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/minimatch": {
|
||||
@@ -9086,7 +9086,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
"url": "https://www.xinmi.cloud/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
@@ -9114,7 +9114,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
@@ -9206,7 +9206,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
"url": "https://www.xinmi.cloud/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -9360,7 +9360,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
@@ -9380,7 +9380,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
@@ -9397,7 +9397,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
@@ -9416,7 +9416,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
@@ -9436,7 +9436,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
@@ -9456,7 +9456,7 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
@@ -9649,7 +9649,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
@@ -9711,7 +9711,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
@@ -9758,7 +9758,7 @@
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal": {
|
||||
@@ -9771,7 +9771,7 @@
|
||||
"js-tokens": "^9.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
"url": "https://www.xinmi.cloud/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal/node_modules/js-tokens": {
|
||||
@@ -9814,7 +9814,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
@@ -9827,7 +9827,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
@@ -9888,7 +9888,7 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
@@ -9942,7 +9942,7 @@
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
"url": "https://www.xinmi.cloud/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/fdir": {
|
||||
@@ -9973,7 +9973,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
@@ -10357,8 +10357,8 @@
|
||||
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
"https://www.xinmi.cloud/sponsors/broofa",
|
||||
"https://www.xinmi.cloud/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -10424,7 +10424,7 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
@@ -10533,7 +10533,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node/node_modules/vite": {
|
||||
@@ -10557,7 +10557,7 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
@@ -10621,7 +10621,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
@@ -10762,7 +10762,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
"url": "https://www.xinmi.cloud/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/tinyexec": {
|
||||
@@ -10793,7 +10793,7 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
@@ -10912,7 +10912,7 @@
|
||||
"node": ">= 22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
"url": "https://www.xinmi.cloud/sponsors/kazupon"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
@@ -10935,7 +10935,7 @@
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
"url": "https://www.xinmi.cloud/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
@@ -11103,7 +11103,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
@@ -11122,7 +11122,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
@@ -11180,7 +11180,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
@@ -11268,7 +11268,7 @@
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
"url": "https://www.xinmi.cloud/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
@@ -11360,4 +11360,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-9
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hermes-web-ui",
|
||||
"name": "xinmi-hermes-ui",
|
||||
"version": "0.6.8",
|
||||
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/EKKOLearnAI/hermes-web-ui.git"
|
||||
"url": "https://www.xinmi.cloud/root/Hermes-ui.git"
|
||||
},
|
||||
"homepage": "https://ekkolearnai.com",
|
||||
"homepage": "https://www.xinmi.cloud",
|
||||
"license": "BSL-1.1",
|
||||
"engines": {
|
||||
"node": ">=23.0.0"
|
||||
@@ -30,7 +30,7 @@
|
||||
"typescript"
|
||||
],
|
||||
"bin": {
|
||||
"hermes-web-ui": "./bin/hermes-web-ui.mjs"
|
||||
"xinmi-hermes-ui": "./bin/hermes-web-ui.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite --host --port 8648",
|
||||
@@ -50,11 +50,12 @@
|
||||
"build:website": "vite build --config vite.config.website.ts",
|
||||
"preview:website": "vite preview --config vite.config.website.ts",
|
||||
"desktop:install": "npm ci --prefix packages/desktop --no-audit --no-fund",
|
||||
"desktop:prepare-runtime": "npm --prefix packages/desktop run prepare:runtime",
|
||||
"desktop:prepare-python": "npm --prefix packages/desktop run prepare:python",
|
||||
"build:desktop": "npm run build && npm run desktop:install && npm run desktop:prepare-python && npm --prefix packages/desktop run dist -- --publish never",
|
||||
"build:desktop:mac": "npm run build && npm run desktop:install && npm run desktop:prepare-python && npm --prefix packages/desktop run dist -- --mac --publish never",
|
||||
"build:desktop:win": "npm run build && npm run desktop:install && npm run desktop:prepare-python && npm --prefix packages/desktop run dist -- --win --publish never",
|
||||
"build:desktop:linux": "npm run build && npm run desktop:install && npm run desktop:prepare-python && npm --prefix packages/desktop run dist -- --linux --publish never",
|
||||
"build:desktop": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --publish never",
|
||||
"build:desktop:mac": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --mac --publish never",
|
||||
"build:desktop:win": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --win --publish never",
|
||||
"build:desktop:linux": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --linux --publish never",
|
||||
"openapi:generate": "node scripts/generate-openapi.mjs",
|
||||
"claude": "claude --dangerously-skip-permissions"
|
||||
},
|
||||
@@ -131,4 +132,4 @@
|
||||
"vue-virtual-scroller": "^3.0.4",
|
||||
"ws": "^8.20.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ This page collects useful community skill repositories that can extend Hermes, C
|
||||
|
||||
Community skills are third-party code and instructions. Review them before installing, especially when a skill can read API keys, cookies, browser sessions, local files, repositories, shell scripts, package managers, or social media accounts.
|
||||
|
||||
Useful skill recommendations are welcome. If you find a high-quality skill that should be listed here, please submit a pull request on GitHub with the repository link, usage scenario, and any security notes.
|
||||
Useful skill recommendations are welcome. If you find a high-quality skill that should be listed here, please submit a pull request on 新觅源码库 with the repository link, usage scenario, and any security notes.
|
||||
|
||||
## Maintenance Guidelines
|
||||
|
||||
@@ -28,7 +28,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Anthropic Official Skills
|
||||
|
||||
- Repository: [anthropics/skills](https://github.com/anthropics/skills/tree/main/skills)
|
||||
- Repository: [anthropics/skills](https://www.xinmi.cloud/anthropics/skills/tree/main/skills)
|
||||
- Focus: official reference skills for Claude-style agents.
|
||||
- Good for: learning the expected skill structure, adapting stable examples, and bootstrapping common workflows.
|
||||
- Representative skills: `docx`, `pdf`, `pptx`, `xlsx`, `frontend-design`, `webapp-testing`, `skill-creator`, `mcp-builder`, `theme-factory`, `web-artifacts-builder`.
|
||||
@@ -36,7 +36,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Matt Pocock Skills
|
||||
|
||||
- Repository: [mattpocock/skills](https://github.com/mattpocock/skills)
|
||||
- Repository: [mattpocock/skills](https://www.xinmi.cloud/mattpocock/skills)
|
||||
- Focus: engineering and productivity skills from a real development workflow.
|
||||
- Good for: TypeScript engineering, test-driven work, triage, diagnosis, reviews, prototyping, and product handoff workflows.
|
||||
- Representative skills: `tdd`, `triage`, `diagnose`, `prototype`, `review`, `to-prd`, `to-issues`, `handoff`, `write-a-skill`.
|
||||
@@ -46,56 +46,56 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Frontend Slides
|
||||
|
||||
- Repository: [zarazhangrui/frontend-slides](https://github.com/zarazhangrui/frontend-slides)
|
||||
- Repository: [zarazhangrui/frontend-slides](https://www.xinmi.cloud/zarazhangrui/frontend-slides)
|
||||
- Focus: creating web-native slide decks with frontend techniques.
|
||||
- Good for: HTML/CSS slide decks, visual storytelling, and browser-rendered presentations.
|
||||
- Notes: useful when a deck should be designed as a rich web artifact rather than a traditional office file.
|
||||
|
||||
### Huashu Design
|
||||
|
||||
- Repository: [alchaincyf/huashu-design](https://github.com/alchaincyf/huashu-design)
|
||||
- Repository: [alchaincyf/huashu-design](https://www.xinmi.cloud/alchaincyf/huashu-design)
|
||||
- Focus: HTML-native design work for Claude Code and agent workflows.
|
||||
- Good for: high-fidelity prototypes, slides, animation concepts, visual review, and export-oriented design flows.
|
||||
- Notes: includes design philosophy, review heuristics, and presentation-oriented workflows.
|
||||
|
||||
### Guizang PPT Skill
|
||||
|
||||
- Repository: [op7418/guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill)
|
||||
- Repository: [op7418/guizang-ppt-skill](https://www.xinmi.cloud/op7418/guizang-ppt-skill)
|
||||
- Focus: polished HTML slide decks with editorial, magazine, and Swiss-style layouts.
|
||||
- Good for: presentation decks, social covers, image prompts, and visual narrative work.
|
||||
- Notes: includes a presentation runtime and style-oriented slide generation patterns.
|
||||
|
||||
### HTML PPT Skill
|
||||
|
||||
- Repository: [lewislulu/html-ppt-skill](https://github.com/lewislulu/html-ppt-skill)
|
||||
- Repository: [lewislulu/html-ppt-skill](https://www.xinmi.cloud/lewislulu/html-ppt-skill)
|
||||
- Focus: HTML PPT Studio for professional HTML presentations.
|
||||
- Good for: themed slide decks, layout-rich presentations, and animated browser presentations.
|
||||
- Representative capabilities: multiple themes, layout templates, animation patterns, and HTML presentation scaffolding.
|
||||
|
||||
### PPT Image First
|
||||
|
||||
- Repository: [NyxTides/ppt-image-first](https://github.com/NyxTides/ppt-image-first)
|
||||
- Repository: [NyxTides/ppt-image-first](https://www.xinmi.cloud/NyxTides/ppt-image-first)
|
||||
- Focus: image-first presentation generation.
|
||||
- Good for: decks where the visual direction should lead the content structure.
|
||||
- Notes: designed for Codex, Claude Code, and OpenCode-style CLI agents.
|
||||
|
||||
### GPT Image To PPT
|
||||
|
||||
- Repository: [JuneYaooo/gpt-image2-ppt-skills](https://github.com/JuneYaooo/gpt-image2-ppt-skills)
|
||||
- Repository: [JuneYaooo/gpt-image2-ppt-skills](https://www.xinmi.cloud/JuneYaooo/gpt-image2-ppt-skills)
|
||||
- Focus: cloning or adapting PowerPoint visual layouts using image generation.
|
||||
- Good for: recreating a deck style from an existing `.pptx` template while replacing the actual content.
|
||||
- Notes: useful for template-driven presentations, but review external image generation/API behavior before use.
|
||||
|
||||
### Fireworks Tech Graph
|
||||
|
||||
- Repository: [yizhiyanhua-ai/fireworks-tech-graph](https://github.com/yizhiyanhua-ai/fireworks-tech-graph)
|
||||
- Repository: [yizhiyanhua-ai/fireworks-tech-graph](https://www.xinmi.cloud/yizhiyanhua-ai/fireworks-tech-graph)
|
||||
- Focus: technical diagram generation.
|
||||
- Good for: architecture diagrams, workflow charts, UML-style visuals, AI agent workflow diagrams, and production-ready SVG/PNG outputs.
|
||||
- Notes: a practical choice when you need diagrams rather than full slide decks.
|
||||
|
||||
### Diagram Skill
|
||||
|
||||
- Repository: [312362115/claude diagram skill](https://github.com/312362115/claude/blob/main/skills/diagram/SKILL.md)
|
||||
- Repository: [312362115/claude diagram skill](https://www.xinmi.cloud/312362115/claude/blob/main/skills/diagram/SKILL.md)
|
||||
- Focus: diagram generation inside a broader Claude skill collection.
|
||||
- Good for: generating structured diagrams, templates, and visual explanations.
|
||||
- Notes: this is a direct skill file link, so review the surrounding `references`, `scripts`, and `templates` folders before installing.
|
||||
@@ -104,7 +104,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Huashu Markdown To HTML
|
||||
|
||||
- Repository: [alchaincyf/huashu-md-html](https://github.com/alchaincyf/huashu-md-html)
|
||||
- Repository: [alchaincyf/huashu-md-html](https://www.xinmi.cloud/alchaincyf/huashu-md-html)
|
||||
- Focus: Markdown and HTML conversion pipelines.
|
||||
- Good for: converting files or URLs to Markdown, turning Markdown into polished HTML, and converting HTML back to Markdown.
|
||||
- Representative tools: MarkItDown, Pandoc, html-to-markdown, and trafilatura-based workflows.
|
||||
@@ -112,14 +112,14 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Chinese Web Novel Skill
|
||||
|
||||
- Repository: [Tomsawyerhu/Chinese-WebNovel-Skill](https://github.com/Tomsawyerhu/Chinese-WebNovel-Skill)
|
||||
- Repository: [Tomsawyerhu/Chinese-WebNovel-Skill](https://www.xinmi.cloud/Tomsawyerhu/Chinese-WebNovel-Skill)
|
||||
- Focus: Chinese web novel writing workflows.
|
||||
- Good for: long-form fiction planning, chapter writing, style continuity, and web-novel oriented drafting.
|
||||
- Representative skill: `webnovel-writing`.
|
||||
|
||||
### Software Copyright Skill
|
||||
|
||||
- Repository: [Fokkyp/SoftwareCopyright-Skill](https://github.com/Fokkyp/SoftwareCopyright-Skill)
|
||||
- Repository: [Fokkyp/SoftwareCopyright-Skill](https://www.xinmi.cloud/Fokkyp/SoftwareCopyright-Skill)
|
||||
- Focus: preparing Chinese software copyright application materials.
|
||||
- Good for: generating `.docx` application documents from a local software project.
|
||||
- Representative skills: `software-copyright-materials`, `docx-toolkit`.
|
||||
@@ -127,7 +127,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Patent Disclosure Skill
|
||||
|
||||
- Repository: [handsomestWei/patent-disclosure-skill](https://github.com/handsomestWei/patent-disclosure-skill)
|
||||
- Repository: [handsomestWei/patent-disclosure-skill](https://www.xinmi.cloud/handsomestWei/patent-disclosure-skill)
|
||||
- Focus: patent disclosure drafting.
|
||||
- Good for: extracting patentable points from project documents, novelty checks, desensitized drafting, and self-review loops.
|
||||
- Notes: may involve web research and sensitive technical documents. Review data handling carefully.
|
||||
@@ -136,7 +136,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Baoyu Skills
|
||||
|
||||
- Repository: [JimLiu/baoyu-skills](https://github.com/JimLiu/baoyu-skills)
|
||||
- Repository: [JimLiu/baoyu-skills](https://www.xinmi.cloud/JimLiu/baoyu-skills)
|
||||
- Focus: image generation, content transformation, publishing, and media workflows.
|
||||
- Good for: image cards, article illustrations, slide decks, URL-to-Markdown conversion, YouTube transcripts, Markdown-to-HTML, and social posting workflows.
|
||||
- Representative skills: `baoyu-image-gen`, `baoyu-imagine`, `baoyu-slide-deck`, `baoyu-markdown-to-html`, `baoyu-post-to-x`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-url-to-markdown`, `baoyu-youtube-transcript`, `baoyu-translate`, `baoyu-diagram`, `baoyu-comic`.
|
||||
@@ -144,7 +144,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Virtual Couple Travel Vlog
|
||||
|
||||
- Repository: [vibeshotclub/vsc-skills / virtual-couple-travel-vlog](https://github.com/vibeshotclub/vsc-skills/tree/main/virtual-couple-travel-vlog)
|
||||
- Repository: [vibeshotclub/vsc-skills / virtual-couple-travel-vlog](https://www.xinmi.cloud/vibeshotclub/vsc-skills/tree/main/virtual-couple-travel-vlog)
|
||||
- Focus: travel-vlog style media generation.
|
||||
- Good for: short-form visual storytelling, character-based travel content, and repeatable media production prompts.
|
||||
- Notes: this is a subdirectory skill inside a larger skill collection.
|
||||
@@ -153,14 +153,14 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Web Access
|
||||
|
||||
- Repository: [eze-is/web-access](https://github.com/eze-is/web-access)
|
||||
- Repository: [eze-is/web-access](https://www.xinmi.cloud/eze-is/web-access)
|
||||
- Focus: giving an agent structured web access through layered routing and browser/CDP workflows.
|
||||
- Good for: web research, browser-assisted tasks, parallel information gathering, and pages that require interaction.
|
||||
- Security note: browser access can expose logged-in sessions and local browser state. Audit before enabling.
|
||||
|
||||
### OpenCLI
|
||||
|
||||
- Repository: [jackwener/opencli](https://github.com/jackwener/opencli)
|
||||
- Repository: [jackwener/opencli](https://www.xinmi.cloud/jackwener/opencli)
|
||||
- Focus: converting websites, browser sessions, Electron apps, and local tools into CLI-accessible automation surfaces for humans and AI agents.
|
||||
- Good for: letting agents operate logged-in Chrome pages, building reusable website adapters, wrapping local binaries, and turning browser workflows into deterministic commands.
|
||||
- Representative skills: `opencli-browser`, `opencli-adapter-author`, `opencli-autofix`, `opencli-usage`.
|
||||
@@ -168,7 +168,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Follow Builders
|
||||
|
||||
- Repository: [zarazhangrui/follow-builders](https://github.com/zarazhangrui/follow-builders)
|
||||
- Repository: [zarazhangrui/follow-builders](https://www.xinmi.cloud/zarazhangrui/follow-builders)
|
||||
- Focus: monitoring AI builders across X, blogs, and YouTube podcasts.
|
||||
- Good for: tracking builders rather than influencers, summarizing feeds, and creating digest-style updates.
|
||||
- Representative data/config files: X feeds, blog feeds, podcast feeds, prompts, and state files.
|
||||
@@ -176,7 +176,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### SlowMist Agent Security
|
||||
|
||||
- Repository: [slowmist/slowmist-agent-security](https://github.com/slowmist/slowmist-agent-security)
|
||||
- Repository: [slowmist/slowmist-agent-security](https://www.xinmi.cloud/slowmist/slowmist-agent-security)
|
||||
- Focus: security review for AI agents operating with untrusted inputs.
|
||||
- Good for: checking skills, MCP servers, repositories, URLs, prompts, and crypto/on-chain addresses for security risks.
|
||||
- Core idea: external input should be considered untrusted until verified.
|
||||
@@ -186,7 +186,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Huashu Nuwa Skill
|
||||
|
||||
- Repository: [alchaincyf/nuwa-skill](https://github.com/alchaincyf/nuwa-skill)
|
||||
- Repository: [alchaincyf/nuwa-skill](https://www.xinmi.cloud/alchaincyf/nuwa-skill)
|
||||
- Focus: distilling a person or viewpoint into a reusable agent skill.
|
||||
- Good for: advisory-board style thinking, mental models, decision heuristics, and writing in a specific perspective.
|
||||
- Representative perspectives: Huashu Nuwa, Feynman, Steve Jobs, Elon Musk, Naval Ravikant, Paul Graham, Nassim Taleb.
|
||||
@@ -194,7 +194,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### PUA / Anti-PUA Skills
|
||||
|
||||
- Repository: [tanweai/pua](https://github.com/tanweai/pua)
|
||||
- Repository: [tanweai/pua](https://www.xinmi.cloud/tanweai/pua)
|
||||
- Focus: high-agency, confrontational, coaching, or anti-PUA style agent behavior.
|
||||
- Good for: motivation, critique, resistance to manipulation, and intentionally sharp agent feedback.
|
||||
- Representative skills: `pua`, `pua-en`, `pua-ja`, `pua-loop`, `mama`, `p7`, `p9`, `p10`, `pro`, `shot`, `yes`.
|
||||
@@ -202,7 +202,7 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
### Ex Skill
|
||||
|
||||
- Repository: [therealXiaomanChu/ex-skill](https://github.com/therealXiaomanChu/ex-skill)
|
||||
- Repository: [therealXiaomanChu/ex-skill](https://www.xinmi.cloud/therealXiaomanChu/ex-skill)
|
||||
- Focus: distilling an ex-partner/persona into an AI skill that speaks in that style.
|
||||
- Good for: persona experiments, emotional roleplay, and style simulation.
|
||||
- Representative skill: `create-ex`.
|
||||
@@ -212,17 +212,17 @@ Useful skill recommendations are welcome. If you find a high-quality skill that
|
||||
|
||||
If you only want a practical starter set:
|
||||
|
||||
- [Anthropic Official Skills](https://github.com/anthropics/skills/tree/main/skills) for reference implementations.
|
||||
- [Matt Pocock Skills](https://github.com/mattpocock/skills) for engineering workflows.
|
||||
- [Baoyu Skills](https://github.com/JimLiu/baoyu-skills) for image, media, and publishing workflows.
|
||||
- [Huashu Design](https://github.com/alchaincyf/huashu-design) for high-fidelity HTML-native design.
|
||||
- [Guizang PPT Skill](https://github.com/op7418/guizang-ppt-skill) or [HTML PPT Skill](https://github.com/lewislulu/html-ppt-skill) for browser-based presentations.
|
||||
- [Huashu Markdown To HTML](https://github.com/alchaincyf/huashu-md-html) for Markdown/HTML document conversion.
|
||||
- [Web Access](https://github.com/eze-is/web-access) for web research workflows.
|
||||
- [OpenCLI](https://github.com/jackwener/opencli) for logged-in browser automation and reusable website CLI adapters.
|
||||
- [Fireworks Tech Graph](https://github.com/yizhiyanhua-ai/fireworks-tech-graph) for technical diagrams.
|
||||
- [SlowMist Agent Security](https://github.com/slowmist/slowmist-agent-security) for reviewing risky community skills.
|
||||
- [Anthropic Official Skills](https://www.xinmi.cloud/anthropics/skills/tree/main/skills) for reference implementations.
|
||||
- [Matt Pocock Skills](https://www.xinmi.cloud/mattpocock/skills) for engineering workflows.
|
||||
- [Baoyu Skills](https://www.xinmi.cloud/JimLiu/baoyu-skills) for image, media, and publishing workflows.
|
||||
- [Huashu Design](https://www.xinmi.cloud/alchaincyf/huashu-design) for high-fidelity HTML-native design.
|
||||
- [Guizang PPT Skill](https://www.xinmi.cloud/op7418/guizang-ppt-skill) or [HTML PPT Skill](https://www.xinmi.cloud/lewislulu/html-ppt-skill) for browser-based presentations.
|
||||
- [Huashu Markdown To HTML](https://www.xinmi.cloud/alchaincyf/huashu-md-html) for Markdown/HTML document conversion.
|
||||
- [Web Access](https://www.xinmi.cloud/eze-is/web-access) for web research workflows.
|
||||
- [OpenCLI](https://www.xinmi.cloud/jackwener/opencli) for logged-in browser automation and reusable website CLI adapters.
|
||||
- [Fireworks Tech Graph](https://www.xinmi.cloud/yizhiyanhua-ai/fireworks-tech-graph) for technical diagrams.
|
||||
- [SlowMist Agent Security](https://www.xinmi.cloud/slowmist/slowmist-agent-security) for reviewing risky community skills.
|
||||
|
||||
## Original Source List
|
||||
|
||||
This document was compiled from a curated Hermes / Claude skill sharing list and expanded with public GitHub repository metadata.
|
||||
This document was compiled from a curated Hermes / Claude skill sharing list and expanded with public 新觅源码库 repository metadata.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
社区 Skill 本质上是第三方指令和代码。安装前请先审计,尤其是会读取 API Key、Cookie、浏览器登录态、本地文件、仓库内容,或者会执行 shell、安装依赖、自动发帖、访问外部 API 的 Skill。
|
||||
|
||||
欢迎大家推荐各种好用的 Skill。如果你发现值得收录的高质量 Skill,可以到 GitHub 提交 PR,并附上仓库链接、适用场景和必要的安全说明。
|
||||
欢迎大家推荐各种好用的 Skill。如果你发现值得收录的高质量 Skill,可以到 新觅源码库 提交 PR,并附上仓库链接、适用场景和必要的安全说明。
|
||||
|
||||
## 维护规范
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
### Anthropic 官方 Skills
|
||||
|
||||
- 仓库:[anthropics/skills](https://github.com/anthropics/skills/tree/main/skills)
|
||||
- 仓库:[anthropics/skills](https://www.xinmi.cloud/anthropics/skills/tree/main/skills)
|
||||
- 方向:Claude 官方参考 Skill。
|
||||
- 适合:学习标准 Skill 结构、参考稳定实现、搭建通用工作流。
|
||||
- 代表 Skills:`docx`、`pdf`、`pptx`、`xlsx`、`frontend-design`、`webapp-testing`、`skill-creator`、`mcp-builder`、`theme-factory`、`web-artifacts-builder`。
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
### Matt Pocock Skills
|
||||
|
||||
- 仓库:[mattpocock/skills](https://github.com/mattpocock/skills)
|
||||
- 仓库:[mattpocock/skills](https://www.xinmi.cloud/mattpocock/skills)
|
||||
- 方向:工程与生产力工作流。
|
||||
- 适合:TypeScript 工程、TDD、问题诊断、代码评审、原型开发、PRD/Issue/Handoff 等开发流程。
|
||||
- 代表 Skills:`tdd`、`triage`、`diagnose`、`prototype`、`review`、`to-prd`、`to-issues`、`handoff`、`write-a-skill`。
|
||||
@@ -46,56 +46,56 @@
|
||||
|
||||
### Frontend Slides
|
||||
|
||||
- 仓库:[zarazhangrui/frontend-slides](https://github.com/zarazhangrui/frontend-slides)
|
||||
- 仓库:[zarazhangrui/frontend-slides](https://www.xinmi.cloud/zarazhangrui/frontend-slides)
|
||||
- 方向:用前端技术生成网页幻灯片。
|
||||
- 适合:HTML/CSS 幻灯片、视觉叙事、浏览器渲染的演示稿。
|
||||
- 备注:适合把演示稿当成 Web Artifact 来做,而不是传统 Office 文件。
|
||||
|
||||
### 华叔 Design
|
||||
|
||||
- 仓库:[alchaincyf/huashu-design](https://github.com/alchaincyf/huashu-design)
|
||||
- 仓库:[alchaincyf/huashu-design](https://www.xinmi.cloud/alchaincyf/huashu-design)
|
||||
- 方向:Claude Code 中的 HTML 原生设计 Skill。
|
||||
- 适合:高保真原型、幻灯片、动画概念、视觉评审和导出型设计流程。
|
||||
- 备注:包含设计哲学、评审维度和演示型工作流。
|
||||
|
||||
### 归藏 PPT Skill
|
||||
|
||||
- 仓库:[op7418/guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill)
|
||||
- 仓库:[op7418/guizang-ppt-skill](https://www.xinmi.cloud/op7418/guizang-ppt-skill)
|
||||
- 方向:生成高质量 HTML 幻灯片。
|
||||
- 适合:杂志风、编辑风、瑞士风等视觉风格的演示稿、社交封面、图片提示词和叙事型页面。
|
||||
- 备注:包含演示运行时和风格化生成模式。
|
||||
|
||||
### HTML PPT Skill
|
||||
|
||||
- 仓库:[lewislulu/html-ppt-skill](https://github.com/lewislulu/html-ppt-skill)
|
||||
- 仓库:[lewislulu/html-ppt-skill](https://www.xinmi.cloud/lewislulu/html-ppt-skill)
|
||||
- 方向:HTML PPT Studio。
|
||||
- 适合:主题化幻灯片、复杂布局演示稿和带动画的浏览器演示。
|
||||
- 代表能力:多主题、多布局、动画模式和 HTML 演示脚手架。
|
||||
|
||||
### PPT Image First
|
||||
|
||||
- 仓库:[NyxTides/ppt-image-first](https://github.com/NyxTides/ppt-image-first)
|
||||
- 仓库:[NyxTides/ppt-image-first](https://www.xinmi.cloud/NyxTides/ppt-image-first)
|
||||
- 方向:图片优先的 PPT 生成。
|
||||
- 适合:视觉方向先行的演示稿创作。
|
||||
- 备注:面向 Codex、Claude Code、OpenCode CLI 等 Agent 工作流。
|
||||
|
||||
### GPT Image To PPT
|
||||
|
||||
- 仓库:[JuneYaooo/gpt-image2-ppt-skills](https://github.com/JuneYaooo/gpt-image2-ppt-skills)
|
||||
- 仓库:[JuneYaooo/gpt-image2-ppt-skills](https://www.xinmi.cloud/JuneYaooo/gpt-image2-ppt-skills)
|
||||
- 方向:用图像生成能力复刻或改造 PPT 视觉版式。
|
||||
- 适合:从已有 `.pptx` 模板中学习版式,再替换成自己的内容。
|
||||
- 备注:涉及图像生成和外部 API 时请先检查配置与数据发送逻辑。
|
||||
|
||||
### Fireworks Tech Graph
|
||||
|
||||
- 仓库:[yizhiyanhua-ai/fireworks-tech-graph](https://github.com/yizhiyanhua-ai/fireworks-tech-graph)
|
||||
- 仓库:[yizhiyanhua-ai/fireworks-tech-graph](https://www.xinmi.cloud/yizhiyanhua-ai/fireworks-tech-graph)
|
||||
- 方向:技术图表生成。
|
||||
- 适合:架构图、流程图、UML 风格图、AI Agent 工作流图,以及 SVG/PNG 输出。
|
||||
- 备注:需要图表而不是整套演示稿时很实用。
|
||||
|
||||
### Diagram Skill
|
||||
|
||||
- 仓库:[312362115/claude diagram skill](https://github.com/312362115/claude/blob/main/skills/diagram/SKILL.md)
|
||||
- 仓库:[312362115/claude diagram skill](https://www.xinmi.cloud/312362115/claude/blob/main/skills/diagram/SKILL.md)
|
||||
- 方向:结构化图表生成。
|
||||
- 适合:生成图表、模板化视觉解释和技术说明。
|
||||
- 备注:这是一个直接指向 `SKILL.md` 的链接,安装前也要检查同目录下的 `references`、`scripts` 和 `templates`。
|
||||
@@ -104,7 +104,7 @@
|
||||
|
||||
### 华叔 Markdown To HTML
|
||||
|
||||
- 仓库:[alchaincyf/huashu-md-html](https://github.com/alchaincyf/huashu-md-html)
|
||||
- 仓库:[alchaincyf/huashu-md-html](https://www.xinmi.cloud/alchaincyf/huashu-md-html)
|
||||
- 方向:Markdown 与 HTML 双向转换流水线。
|
||||
- 适合:把文件或网页转 Markdown,把 Markdown 转精美 HTML,把 HTML 再转回 Markdown。
|
||||
- 代表工具:MarkItDown、Pandoc、html-to-markdown、trafilatura。
|
||||
@@ -112,14 +112,14 @@
|
||||
|
||||
### 中文网文写作 Skill
|
||||
|
||||
- 仓库:[Tomsawyerhu/Chinese-WebNovel-Skill](https://github.com/Tomsawyerhu/Chinese-WebNovel-Skill)
|
||||
- 仓库:[Tomsawyerhu/Chinese-WebNovel-Skill](https://www.xinmi.cloud/Tomsawyerhu/Chinese-WebNovel-Skill)
|
||||
- 方向:中文网文小说写作。
|
||||
- 适合:长篇小说规划、章节创作、风格延续和网文式叙事。
|
||||
- 代表 Skill:`webnovel-writing`。
|
||||
|
||||
### 软件著作权材料 Skill
|
||||
|
||||
- 仓库:[Fokkyp/SoftwareCopyright-Skill](https://github.com/Fokkyp/SoftwareCopyright-Skill)
|
||||
- 仓库:[Fokkyp/SoftwareCopyright-Skill](https://www.xinmi.cloud/Fokkyp/SoftwareCopyright-Skill)
|
||||
- 方向:中国软件著作权申请材料生成。
|
||||
- 适合:根据本地项目生成 `.docx` 软著申请材料。
|
||||
- 代表 Skills:`software-copyright-materials`、`docx-toolkit`。
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
### 专利交底书 Skill
|
||||
|
||||
- 仓库:[handsomestWei/patent-disclosure-skill](https://github.com/handsomestWei/patent-disclosure-skill)
|
||||
- 仓库:[handsomestWei/patent-disclosure-skill](https://www.xinmi.cloud/handsomestWei/patent-disclosure-skill)
|
||||
- 方向:专利技术交底书生成。
|
||||
- 适合:从项目文档挖掘专利点、联网查新、脱敏成文和自检。
|
||||
- 备注:可能涉及敏感技术资料和联网检索,使用前请关注数据处理方式。
|
||||
@@ -136,7 +136,7 @@
|
||||
|
||||
### 宝玉 Skills
|
||||
|
||||
- 仓库:[JimLiu/baoyu-skills](https://github.com/JimLiu/baoyu-skills)
|
||||
- 仓库:[JimLiu/baoyu-skills](https://www.xinmi.cloud/JimLiu/baoyu-skills)
|
||||
- 方向:图片生成、内容转换、发布和媒体工作流。
|
||||
- 适合:图片卡片、文章配图、幻灯片、URL 转 Markdown、YouTube 字幕、Markdown 转 HTML、社交平台发布。
|
||||
- 代表 Skills:`baoyu-image-gen`、`baoyu-imagine`、`baoyu-slide-deck`、`baoyu-markdown-to-html`、`baoyu-post-to-x`、`baoyu-post-to-wechat`、`baoyu-post-to-weibo`、`baoyu-url-to-markdown`、`baoyu-youtube-transcript`、`baoyu-translate`、`baoyu-diagram`、`baoyu-comic`。
|
||||
@@ -144,7 +144,7 @@
|
||||
|
||||
### Virtual Couple Travel Vlog
|
||||
|
||||
- 仓库:[vibeshotclub/vsc-skills / virtual-couple-travel-vlog](https://github.com/vibeshotclub/vsc-skills/tree/main/virtual-couple-travel-vlog)
|
||||
- 仓库:[vibeshotclub/vsc-skills / virtual-couple-travel-vlog](https://www.xinmi.cloud/vibeshotclub/vsc-skills/tree/main/virtual-couple-travel-vlog)
|
||||
- 方向:旅行 vlog 风格媒体生成。
|
||||
- 适合:短视频视觉叙事、角色化旅行内容和可复用媒体提示词。
|
||||
- 备注:这是一个大仓库里的子目录 Skill。
|
||||
@@ -153,14 +153,14 @@
|
||||
|
||||
### Web Access
|
||||
|
||||
- 仓库:[eze-is/web-access](https://github.com/eze-is/web-access)
|
||||
- 仓库:[eze-is/web-access](https://www.xinmi.cloud/eze-is/web-access)
|
||||
- 方向:为 Agent 提供结构化联网能力。
|
||||
- 适合:网页研究、浏览器辅助任务、并行信息收集和需要交互的网站。
|
||||
- 安全提示:浏览器访问可能暴露已登录状态和本地浏览器数据,启用前要审计。
|
||||
|
||||
### OpenCLI
|
||||
|
||||
- 仓库:[jackwener/opencli](https://github.com/jackwener/opencli)
|
||||
- 仓库:[jackwener/opencli](https://www.xinmi.cloud/jackwener/opencli)
|
||||
- 方向:把网站、浏览器会话、Electron 应用和本地工具转换成 CLI 可调用的自动化入口。
|
||||
- 适合:让 Agent 操作已登录的 Chrome 页面、编写可复用网站适配器、封装本地命令,以及把浏览器流程变成稳定命令。
|
||||
- 代表 Skills:`opencli-browser`、`opencli-adapter-author`、`opencli-autofix`、`opencli-usage`。
|
||||
@@ -168,7 +168,7 @@
|
||||
|
||||
### Follow Builders
|
||||
|
||||
- 仓库:[zarazhangrui/follow-builders](https://github.com/zarazhangrui/follow-builders)
|
||||
- 仓库:[zarazhangrui/follow-builders](https://www.xinmi.cloud/zarazhangrui/follow-builders)
|
||||
- 方向:跟踪 AI builders 的 X、博客和 YouTube 播客内容。
|
||||
- 适合:关注 builder 而不是 influencer,生成摘要和内容 digest。
|
||||
- 代表内容:X feed、blog feed、podcast feed、prompts 和状态文件。
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
### SlowMist Agent Security
|
||||
|
||||
- 仓库:[slowmist/slowmist-agent-security](https://github.com/slowmist/slowmist-agent-security)
|
||||
- 仓库:[slowmist/slowmist-agent-security](https://www.xinmi.cloud/slowmist/slowmist-agent-security)
|
||||
- 方向:AI Agent 安全审计框架。
|
||||
- 适合:检查 Skill、MCP、仓库、URL、Prompt 和链上地址的安全风险。
|
||||
- 核心原则:所有外部输入在验证前都不可信。
|
||||
@@ -186,7 +186,7 @@
|
||||
|
||||
### 华叔 Nuwa Skill
|
||||
|
||||
- 仓库:[alchaincyf/nuwa-skill](https://github.com/alchaincyf/nuwa-skill)
|
||||
- 仓库:[alchaincyf/nuwa-skill](https://www.xinmi.cloud/alchaincyf/nuwa-skill)
|
||||
- 方向:把某个人或视角蒸馏成可复用 Skill。
|
||||
- 适合:顾问团式思考、心智模型、决策启发式和特定视角写作。
|
||||
- 代表视角:华叔 Nuwa、Feynman、Jobs、Musk、Naval、Paul Graham、Taleb。
|
||||
@@ -194,7 +194,7 @@
|
||||
|
||||
### PUA / 反 PUA 类 Skills
|
||||
|
||||
- 仓库:[tanweai/pua](https://github.com/tanweai/pua)
|
||||
- 仓库:[tanweai/pua](https://www.xinmi.cloud/tanweai/pua)
|
||||
- 方向:高能动性、强反馈、反操控或尖锐教练风格的 Agent 行为。
|
||||
- 适合:动机强化、批判反馈、反操控和刻意强风格交互。
|
||||
- 代表 Skills:`pua`、`pua-en`、`pua-ja`、`pua-loop`、`mama`、`p7`、`p9`、`p10`、`pro`、`shot`、`yes`。
|
||||
@@ -202,7 +202,7 @@
|
||||
|
||||
### Ex Skill
|
||||
|
||||
- 仓库:[therealXiaomanChu/ex-skill](https://github.com/therealXiaomanChu/ex-skill)
|
||||
- 仓库:[therealXiaomanChu/ex-skill](https://www.xinmi.cloud/therealXiaomanChu/ex-skill)
|
||||
- 方向:把某个前任/人格风格蒸馏成 AI Skill。
|
||||
- 适合:Persona 实验、情绪化角色扮演和特定语气模拟。
|
||||
- 代表 Skill:`create-ex`。
|
||||
@@ -212,17 +212,17 @@
|
||||
|
||||
如果你只想先装一批实用的,可以从这些开始:
|
||||
|
||||
- [Anthropic 官方 Skills](https://github.com/anthropics/skills/tree/main/skills):参考实现和通用能力。
|
||||
- [Matt Pocock Skills](https://github.com/mattpocock/skills):工程流程。
|
||||
- [宝玉 Skills](https://github.com/JimLiu/baoyu-skills):图片、媒体和发布。
|
||||
- [华叔 Design](https://github.com/alchaincyf/huashu-design):高保真 HTML 设计。
|
||||
- [归藏 PPT Skill](https://github.com/op7418/guizang-ppt-skill) 或 [HTML PPT Skill](https://github.com/lewislulu/html-ppt-skill):浏览器演示稿。
|
||||
- [华叔 Markdown To HTML](https://github.com/alchaincyf/huashu-md-html):Markdown/HTML 文档转换。
|
||||
- [Web Access](https://github.com/eze-is/web-access):网页研究。
|
||||
- [OpenCLI](https://github.com/jackwener/opencli):已登录浏览器自动化和可复用网站 CLI 适配器。
|
||||
- [Fireworks Tech Graph](https://github.com/yizhiyanhua-ai/fireworks-tech-graph):技术图表。
|
||||
- [SlowMist Agent Security](https://github.com/slowmist/slowmist-agent-security):社区 Skill 安全审计。
|
||||
- [Anthropic 官方 Skills](https://www.xinmi.cloud/anthropics/skills/tree/main/skills):参考实现和通用能力。
|
||||
- [Matt Pocock Skills](https://www.xinmi.cloud/mattpocock/skills):工程流程。
|
||||
- [宝玉 Skills](https://www.xinmi.cloud/JimLiu/baoyu-skills):图片、媒体和发布。
|
||||
- [华叔 Design](https://www.xinmi.cloud/alchaincyf/huashu-design):高保真 HTML 设计。
|
||||
- [归藏 PPT Skill](https://www.xinmi.cloud/op7418/guizang-ppt-skill) 或 [HTML PPT Skill](https://www.xinmi.cloud/lewislulu/html-ppt-skill):浏览器演示稿。
|
||||
- [华叔 Markdown To HTML](https://www.xinmi.cloud/alchaincyf/huashu-md-html):Markdown/HTML 文档转换。
|
||||
- [Web Access](https://www.xinmi.cloud/eze-is/web-access):网页研究。
|
||||
- [OpenCLI](https://www.xinmi.cloud/jackwener/opencli):已登录浏览器自动化和可复用网站 CLI 适配器。
|
||||
- [Fireworks Tech Graph](https://www.xinmi.cloud/yizhiyanhua-ai/fireworks-tech-graph):技术图表。
|
||||
- [SlowMist Agent Security](https://www.xinmi.cloud/slowmist/slowmist-agent-security):社区 Skill 安全审计。
|
||||
|
||||
## 来源说明
|
||||
|
||||
本文档基于一份 Hermes / Claude Skills 分享清单整理,并补充了公开 GitHub 仓库描述与目录信息。
|
||||
本文档基于一份 Hermes / Claude Skills 分享清单整理,并补充了公开 新觅源码库 仓库描述与目录信息。
|
||||
|
||||
@@ -355,10 +355,10 @@ function openChangelog() {
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<div class="version-links">
|
||||
<a class="github-link" href="https://github.com/EKKOLearnAI/hermes-web-ui" target="_blank" rel="noopener noreferrer" title="GitHub">
|
||||
<a class="github-link" href="http://192.168.6.101:3001/root/Hermes-ui" target="_blank" rel="noopener noreferrer" title="新觅源码库">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
||||
</a>
|
||||
<a class="website-link" href="https://ekkolearnai.com/" target="_blank" rel="noopener noreferrer" title="Website">
|
||||
<a class="website-link" href="https://www.xinmi.cloud/" target="_blank" rel="noopener noreferrer" title="Website">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1261,7 +1261,7 @@ jobTriggered: 'Job ausgelost',
|
||||
// Anderungsprotokoll
|
||||
changelog: {
|
||||
new_0_6_7_1: 'Die Desktop-App nutzt jetzt standardmäßig Port 8748, unterstützt Zugriff im lokalen Netzwerk und kann direkt im lokalen Browser geöffnet werden',
|
||||
new_0_6_7_9: 'Desktop-Download-Links sind jetzt auf der offiziellen Website https://ekkolearnai.com/ verfügbar, aktuelle Installer bleiben außerdem über GitHub Releases verfügbar',
|
||||
new_0_6_7_9: 'Desktop-Download-Links sind jetzt auf der offiziellen Website https://www.xinmi.cloud/ verfügbar, aktuelle Installer bleiben außerdem über GitHub Releases verfügbar',
|
||||
new_0_6_7_2: 'MCP-Tools sind vollständiger: Bridge Tool Discovery, MCP-Management-Lifecycle und Tool-Sichtbarkeit pro Modell im Manager wurden verbessert',
|
||||
new_0_6_7_3: 'Nachrichtenlisten zentrieren leere Zustände korrekt, reduzieren Scroll-Jitter, zeigen beim Laden von History keine Live-Chat-Nachrichten mehr, behalten Scrollpositionen pro Session und blenden beim Session-Wechsel 1,5 Sekunden ein',
|
||||
new_0_6_7_4: 'Bridge und Runtime sind stabiler durch erhaltene Text/tool-call-Reihenfolge, korrektes Profile runtime status loading, bessere Node/npm-Erkennung und übersprungene Produktionsdatenverzeichnis-Erstellung',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
// Login
|
||||
login: {
|
||||
title: 'Hermes Web UI',
|
||||
title: '新觅 Web UI',
|
||||
description: 'Enter your username and password to continue.',
|
||||
placeholder: 'Access token',
|
||||
submit: 'Login',
|
||||
@@ -1483,7 +1483,7 @@ export default {
|
||||
// Changelog
|
||||
changelog: {
|
||||
new_0_6_7_1: 'The desktop app now defaults to port 8748, supports LAN access, and can be opened directly from a local browser',
|
||||
new_0_6_7_9: 'Desktop download links are now available on the official website at https://ekkolearnai.com/, and the latest installers remain available from GitHub Releases',
|
||||
new_0_6_7_9: 'Desktop download links are now available on the official website at https://www.xinmi.cloud/, and the latest installers remain available from GitHub Releases',
|
||||
new_0_6_7_2: 'MCP tooling is more complete with bridge tool discovery fixes, MCP management lifecycle fixes, and per-model tool visibility controls in the manager',
|
||||
new_0_6_7_3: 'Message lists now center empty states correctly, reduce scroll jitter, avoid leaking live chat messages into History while loading, preserve per-session scroll positions, and fade in over 1.5 seconds on session switches',
|
||||
new_0_6_7_4: 'Bridge and runtime stability improved by preserving text/tool-call ordering, fixing Profile runtime status loading, improving Node/npm detection, and skipping production data directory creation',
|
||||
|
||||
@@ -1261,7 +1261,7 @@ jobTriggered: 'Job ejecutado',
|
||||
// Registro de cambios
|
||||
changelog: {
|
||||
new_0_6_7_1: 'La app de escritorio ahora usa el puerto 8748 por defecto, permite acceso desde la red local y puede abrirse directamente desde un navegador local',
|
||||
new_0_6_7_9: 'Los enlaces de descarga de escritorio ya están disponibles en el sitio oficial https://ekkolearnai.com/, y los instaladores más recientes siguen disponibles en GitHub Releases',
|
||||
new_0_6_7_9: 'Los enlaces de descarga de escritorio ya están disponibles en el sitio oficial https://www.xinmi.cloud/, y los instaladores más recientes siguen disponibles en GitHub Releases',
|
||||
new_0_6_7_2: 'Las herramientas MCP quedan más completas con arreglos de discovery en bridge, ciclo de vida de gestión MCP y controles de visibilidad por modelo en el gestor',
|
||||
new_0_6_7_3: 'Las listas de mensajes centran mejor el estado vacío, reducen saltos de scroll, evitan mostrar mensajes del chat activo mientras carga History, preservan la posición por sesión y hacen fade-in de 1,5 segundos al cambiar de sesión',
|
||||
new_0_6_7_4: 'Bridge y runtime son más estables al preservar el orden texto/tool-call, corregir la carga de estado runtime de Profile, mejorar detección Node/npm y evitar crear directorios de datos en producción',
|
||||
|
||||
@@ -1261,7 +1261,7 @@ jobTriggered: 'Job declenche',
|
||||
// Journal des modifications
|
||||
changelog: {
|
||||
new_0_6_7_1: 'L application desktop utilise maintenant le port 8748 par défaut, prend en charge l accès LAN et peut être ouverte directement depuis un navigateur local',
|
||||
new_0_6_7_9: 'Les liens de téléchargement desktop sont maintenant disponibles sur le site officiel https://ekkolearnai.com/, et les derniers installateurs restent disponibles via GitHub Releases',
|
||||
new_0_6_7_9: 'Les liens de téléchargement desktop sont maintenant disponibles sur le site officiel https://www.xinmi.cloud/, et les derniers installateurs restent disponibles via GitHub Releases',
|
||||
new_0_6_7_2: 'Les outils MCP sont plus complets avec des corrections de découverte bridge, de cycle de vie MCP et des contrôles de visibilité par modèle dans le gestionnaire',
|
||||
new_0_6_7_3: 'Les listes de messages centrent mieux les états vides, réduisent les sauts de scroll, évitent d afficher le chat actif pendant le chargement de History, conservent la position par session et ajoutent un fondu de 1,5 seconde au changement de session',
|
||||
new_0_6_7_4: 'Bridge et runtime sont plus stables avec ordre texte/tool-call préservé, chargement du statut runtime de Profile corrigé, meilleure détection Node/npm et création du dossier de données production évitée',
|
||||
|
||||
@@ -1260,7 +1260,7 @@ export default {
|
||||
// 更新履歴
|
||||
changelog: {
|
||||
new_0_6_7_1: 'Desktop アプリは既定で port 8748 を使用し、LAN アクセスとローカルブラウザからの直接アクセスに対応しました',
|
||||
new_0_6_7_9: 'Desktop のダウンロードリンクを公式サイト https://ekkolearnai.com/ に追加し、最新インストーラーは引き続き GitHub Releases からも取得できます',
|
||||
new_0_6_7_9: 'Desktop のダウンロードリンクを公式サイト https://www.xinmi.cloud/ に追加し、最新インストーラーは引き続き GitHub Releases からも取得できます',
|
||||
new_0_6_7_2: 'MCP ツールは bridge の tool discovery 修正、MCP 管理ライフサイクル修正、管理画面のモデル別 tool visibility によりさらに整備されました',
|
||||
new_0_6_7_3: 'メッセージ一覧は empty state の中央揃え、scroll jitter、History 読み込み中のライブチャット混入を修正し、セッション別スクロール位置保持と 1.5 秒のフェードインに対応しました',
|
||||
new_0_6_7_4: 'Bridge と runtime は text/tool-call の順序保持、Profile runtime status loading 修正、Node/npm 検出改善、本番 data directory 作成スキップで安定しました',
|
||||
|
||||
@@ -1260,7 +1260,7 @@ export default {
|
||||
// 변경 이력
|
||||
changelog: {
|
||||
new_0_6_7_1: 'Desktop 앱은 기본적으로 8748 포트를 사용하며 LAN 접근과 로컬 브라우저 직접 열기를 지원합니다',
|
||||
new_0_6_7_9: 'Desktop 다운로드 링크가 공식 웹사이트 https://ekkolearnai.com/ 에 추가되었으며 최신 설치 파일은 GitHub Releases 에서도 계속 받을 수 있습니다',
|
||||
new_0_6_7_9: 'Desktop 다운로드 링크가 공식 웹사이트 https://www.xinmi.cloud/ 에 추가되었으며 최신 설치 파일은 GitHub Releases 에서도 계속 받을 수 있습니다',
|
||||
new_0_6_7_2: 'MCP 도구는 bridge tool discovery 수정, MCP 관리 라이프사이클 수정, 관리자 화면의 모델별 tool visibility 제어로 더 완성되었습니다',
|
||||
new_0_6_7_3: '메시지 목록은 빈 상태 중앙 정렬, 스크롤 튐, History 로딩 중 라이브 채팅 메시지 노출을 수정하고 세션별 스크롤 위치 보존과 1.5초 페이드인을 지원합니다',
|
||||
new_0_6_7_4: 'Bridge 와 runtime 은 text/tool-call 순서 보존, Profile runtime status loading 수정, Node/npm 감지 개선, 운영 데이터 디렉터리 생성 생략으로 더 안정적입니다',
|
||||
|
||||
@@ -1261,7 +1261,7 @@ jobTriggered: 'Job acionado',
|
||||
// Registro de alteracoes
|
||||
changelog: {
|
||||
new_0_6_7_1: 'O app desktop agora usa a porta 8748 por padrão, permite acesso pela rede local e pode ser aberto diretamente em um navegador local',
|
||||
new_0_6_7_9: 'Links de download do desktop agora estão disponíveis no site oficial https://ekkolearnai.com/, e os instaladores mais recentes continuam disponíveis no GitHub Releases',
|
||||
new_0_6_7_9: 'Links de download do desktop agora estão disponíveis no site oficial https://www.xinmi.cloud/, e os instaladores mais recentes continuam disponíveis no GitHub Releases',
|
||||
new_0_6_7_2: 'As ferramentas MCP ficam mais completas com correções de discovery no bridge, ciclo de vida de gestão MCP e controles de visibilidade por modelo no gestor',
|
||||
new_0_6_7_3: 'Listas de mensagens centralizam melhor estados vazios, reduzem saltos de rolagem, evitam mostrar o chat ativo enquanto History carrega, preservam posição por sessão e fazem fade-in de 1,5 segundo ao trocar sessão',
|
||||
new_0_6_7_4: 'Bridge e runtime ficam mais estáveis preservando a ordem texto/tool-call, corrigindo carregamento de status runtime de Profile, melhorando detecção Node/npm e evitando criação de diretório de dados em produção',
|
||||
|
||||
@@ -1488,7 +1488,7 @@ export default {
|
||||
// 更新日誌
|
||||
changelog: {
|
||||
new_0_6_7_1: '桌面版預設使用 8748 連接埠,支援區域網路內存取,也可以直接用本機瀏覽器開啟 Web UI',
|
||||
new_0_6_7_9: '桌面端下載入口已補充到官網 https://ekkolearnai.com/,也可以繼續從 GitHub Releases 取得最新安裝包',
|
||||
new_0_6_7_9: '桌面端下載入口已補充到官網 https://www.xinmi.cloud/,也可以繼續從 GitHub Releases 取得最新安裝包',
|
||||
new_0_6_7_2: 'MCP 工具鏈繼續完善:修復 bridge 工具發現與 MCP 管理生命週期,並在管理頁支援按模型控制工具可見性',
|
||||
new_0_6_7_3: '訊息列表體驗優化:修復空狀態置中、捲動抖動、歷史會話載入串訊息,並在切換會話時保留捲動位置與 1.5 秒淡入效果',
|
||||
new_0_6_7_4: 'Bridge 與執行狀態更穩定:保持文字和 tool-call 順序、修復 Profile runtime 狀態載入、改進 Node/npm 偵測,並避免正式環境自動建立資料目錄',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
// 登录
|
||||
login: {
|
||||
title: 'Hermes Web UI',
|
||||
title: '新觅 Web UI',
|
||||
description: '输入用户名和密码以继续。',
|
||||
placeholder: '访问令牌',
|
||||
submit: '登录',
|
||||
@@ -1485,7 +1485,7 @@ export default {
|
||||
// 更新日志
|
||||
changelog: {
|
||||
new_0_6_7_1: '桌面版默认使用 8748 端口,支持局域网内访问,也可以直接用本机浏览器打开 Web UI',
|
||||
new_0_6_7_9: '桌面端下载入口已补充到官网 https://ekkolearnai.com/,也可以继续从 GitHub Releases 获取最新安装包',
|
||||
new_0_6_7_9: '桌面端下载入口已补充到官网 https://www.xinmi.cloud/,也可以继续从 GitHub Releases 获取最新安装包',
|
||||
new_0_6_7_2: 'MCP 工具链继续完善:修复 bridge 工具发现与 MCP 管理生命周期,并在管理页支持按模型控制工具可见性',
|
||||
new_0_6_7_3: '消息列表体验优化:修复空状态居中、滚动抖动、历史会话加载串消息,并在切换会话时保留滚动位置与 1.5 秒淡入效果',
|
||||
new_0_6_7_4: 'Bridge 与运行态更稳定:保持文本和 tool-call 顺序、修复 Profile runtime 状态加载、改进 Node/npm 检测,并避免生产环境自动创建数据目录',
|
||||
|
||||
@@ -62,7 +62,7 @@ async function handlePasswordLogin() {
|
||||
<div class="login-view">
|
||||
<div class="login-card">
|
||||
<div class="login-logo">
|
||||
<img src="/logo.png" alt="Hermes" width="80" height="80" />
|
||||
<img src="/logo.png" alt="新觅源码库" width="80" height="80" />
|
||||
</div>
|
||||
<h1 class="login-title">{{ t("login.title") }}</h1>
|
||||
<p class="login-desc">{{ t("login.description") }}</p>
|
||||
@@ -87,9 +87,9 @@ async function handlePasswordLogin() {
|
||||
<div v-if="errorMsg" class="login-error">{{ errorMsg }}</div>
|
||||
<div v-if="showLockResetHint" class="login-lock-hint">
|
||||
<span>{{ t("login.lockResetHint") }}</span>
|
||||
<code>hermes-web-ui clear-login-locks --restart</code>
|
||||
<code>xinmi-hermes-ui clear-login-locks --restart</code>
|
||||
<span>{{ t("login.defaultLoginResetHint") }}</span>
|
||||
<code>hermes-web-ui reset-default-login</code>
|
||||
<code>xinmi-hermes-ui reset-default-login</code>
|
||||
</div>
|
||||
<button type="submit" class="login-btn" :disabled="loading">
|
||||
{{ loading ? "..." : t("login.submit") }}
|
||||
|
||||
@@ -6,7 +6,7 @@ Electron desktop distribution for Hermes Studio.
|
||||
|
||||
Download the latest macOS, Windows, or Linux installer for your CPU
|
||||
architecture from the project
|
||||
[GitHub Releases](https://github.com/EKKOLearnAI/hermes-web-ui/releases/latest).
|
||||
[新觅源码库 Releases](https://www.xinmi.cloud/root/Hermes-ui/releases/latest).
|
||||
|
||||
The desktop app bundles the Web UI runtime and launches it locally from the
|
||||
native shell app.
|
||||
@@ -32,9 +32,9 @@ export ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
export ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||
```
|
||||
|
||||
If GitHub release downloads are slow, `fetch-python.mjs` can also use a compatible
|
||||
If 新觅源码库 release downloads are slow, `fetch-python.mjs` can also use a compatible
|
||||
python-build-standalone release mirror:
|
||||
|
||||
```sh
|
||||
export PBS_BASE_URL=https://github.com/astral-sh/python-build-standalone/releases/download
|
||||
export PBS_BASE_URL=https://www.xinmi.cloud/astral-sh/python-build-standalone/releases/download
|
||||
```
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"tag": "hermes-0.15.2-runtime"
|
||||
}
|
||||
@@ -22,7 +22,8 @@ files:
|
||||
- "!**/node_modules/.bin"
|
||||
- "!**/{.DS_Store,.git,.gitignore,.eslintrc*,.prettierrc*,*.map,tsconfig.json}"
|
||||
|
||||
# Web UI source (built dist) and bundled Python live outside the asar.
|
||||
# Web UI source (built dist) lives outside the asar. Python/Node/Git runtime
|
||||
# assets are downloaded into the user's Web UI home on first launch.
|
||||
# This package lives at packages/desktop, so ../.. is the hermes-web-ui repo root.
|
||||
extraResources:
|
||||
- from: "build"
|
||||
@@ -32,6 +33,7 @@ extraResources:
|
||||
- "icon.ico"
|
||||
- "trayTemplate.png"
|
||||
- "trayWindows.png"
|
||||
- "runtime-release.json"
|
||||
- from: "../.."
|
||||
to: "webui"
|
||||
filter:
|
||||
@@ -44,10 +46,6 @@ extraResources:
|
||||
- "!packages/desktop/**"
|
||||
- "!**/{.git,.github,docs,tests,playwright.config.ts,README*,scripts,*.map}"
|
||||
- "!node_modules/**/*.md"
|
||||
- from: "resources/python/${os}-${arch}"
|
||||
to: "python"
|
||||
filter:
|
||||
- "**/*"
|
||||
|
||||
asarUnpack:
|
||||
- "**/*.node"
|
||||
|
||||
Generated
+58
-58
@@ -7,7 +7,7 @@
|
||||
"": {
|
||||
"name": "hermes-studio",
|
||||
"version": "0.6.8",
|
||||
"license": "MIT",
|
||||
"license": "BSL-1.1",
|
||||
"dependencies": {
|
||||
"electron-updater": "^6.3.9"
|
||||
},
|
||||
@@ -169,7 +169,7 @@
|
||||
"node": ">= 8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/gjtorikian/"
|
||||
"url": "https://www.xinmi.cloud/sponsors/gjtorikian/"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild": {
|
||||
@@ -265,7 +265,7 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@gar/promisify": {
|
||||
@@ -303,7 +303,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
|
||||
@@ -316,7 +316,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
|
||||
@@ -341,7 +341,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
|
||||
@@ -357,7 +357,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
|
||||
@@ -375,7 +375,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@malept/cross-spawn-promise": {
|
||||
@@ -386,7 +386,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/malept"
|
||||
"url": "https://www.xinmi.cloud/sponsors/malept"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
@@ -483,7 +483,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
@@ -692,7 +692,7 @@
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
"url": "https://www.xinmi.cloud/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-keywords": {
|
||||
@@ -728,7 +728,7 @@
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-bin": {
|
||||
@@ -967,7 +967,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
"url": "https://www.xinmi.cloud/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
@@ -1030,7 +1030,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
"url": "https://www.xinmi.cloud/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
@@ -1168,7 +1168,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache/node_modules/lru-cache": {
|
||||
@@ -1251,7 +1251,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
@@ -1279,7 +1279,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
@@ -1320,7 +1320,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate": {
|
||||
@@ -1338,7 +1338,7 @@
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
@@ -1376,7 +1376,7 @@
|
||||
"mimic-response": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
@@ -1513,7 +1513,7 @@
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/config-file-ts/node_modules/minimatch": {
|
||||
@@ -1529,7 +1529,7 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/config-file-ts/node_modules/minipass": {
|
||||
@@ -1641,7 +1641,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||
@@ -1654,7 +1654,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defaults": {
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"clone": "^1.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defer-to-connect": {
|
||||
@@ -2015,7 +2015,7 @@
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/err-code": {
|
||||
@@ -2201,7 +2201,7 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child/node_modules/signal-exit": {
|
||||
@@ -2214,7 +2214,7 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
@@ -2283,7 +2283,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge": {
|
||||
@@ -2339,7 +2339,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
@@ -2369,7 +2369,7 @@
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
@@ -2391,7 +2391,7 @@
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/balanced-match": {
|
||||
@@ -2435,7 +2435,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/got": {
|
||||
@@ -2461,7 +2461,7 @@
|
||||
"node": ">=10.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
@@ -2490,7 +2490,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
@@ -2506,7 +2506,7 @@
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-unicode": {
|
||||
@@ -2640,7 +2640,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
"url": "https://www.xinmi.cloud/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
@@ -2759,7 +2759,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
@@ -2780,7 +2780,7 @@
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/gjtorikian/"
|
||||
"url": "https://www.xinmi.cloud/sponsors/gjtorikian/"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
@@ -2800,7 +2800,7 @@
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
@@ -3015,7 +3015,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
@@ -3200,7 +3200,7 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
@@ -3210,7 +3210,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
"url": "https://www.xinmi.cloud/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
@@ -3443,7 +3443,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/npmlog": {
|
||||
@@ -3486,7 +3486,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ora": {
|
||||
@@ -3510,7 +3510,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-cancelable": {
|
||||
@@ -3536,7 +3536,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-map": {
|
||||
@@ -3552,7 +3552,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
@@ -3596,7 +3596,7 @@
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||
@@ -3628,7 +3628,7 @@
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jet2jet"
|
||||
"url": "https://www.xinmi.cloud/sponsors/jet2jet"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
@@ -3730,7 +3730,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/read-binary-file-arch": {
|
||||
@@ -3830,7 +3830,7 @@
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jet2jet"
|
||||
"url": "https://www.xinmi.cloud/sponsors/jet2jet"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-alpn": {
|
||||
@@ -3850,7 +3850,7 @@
|
||||
"lowercase-keys": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
@@ -3891,7 +3891,7 @@
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://www.xinmi.cloud/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
@@ -3902,7 +3902,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
"url": "https://www.xinmi.cloud/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
@@ -4464,7 +4464,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
@@ -4483,7 +4483,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
"url": "https://www.xinmi.cloud/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
@@ -4570,7 +4570,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"url": "https://www.xinmi.cloud/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream": {
|
||||
|
||||
@@ -2,21 +2,27 @@
|
||||
"name": "hermes-studio",
|
||||
"version": "0.6.8",
|
||||
"description": "Hermes Studio desktop distribution with bundled Python runtime and hermes-agent",
|
||||
"homepage": "https://ekkolearnai.com",
|
||||
"homepage": "https://www.xinmi.cloud",
|
||||
"author": {
|
||||
"name": "Hermes Studio Contributors",
|
||||
"email": "noreply@hermes-studio.local"
|
||||
},
|
||||
"license": "MIT",
|
||||
"license": "BSL-1.1",
|
||||
"private": true,
|
||||
"main": "dist/main/index.js",
|
||||
"scripts": {
|
||||
"build:main": "tsc -p tsconfig.json",
|
||||
"build": "npm run build:main",
|
||||
"fetch:node": "node scripts/fetch-node.mjs",
|
||||
"fetch:git": "node scripts/fetch-git.mjs",
|
||||
"fetch:python": "node scripts/fetch-python.mjs",
|
||||
"install:hermes": "node scripts/install-hermes.mjs",
|
||||
"patch:hermes": "node scripts/apply-hermes-patches.mjs",
|
||||
"prepare:python": "npm run fetch:python && npm run install:hermes && npm run patch:hermes && npm run prune:python",
|
||||
"write:runtime-release": "node scripts/write-runtime-release.mjs",
|
||||
"prepare:runtime": "npm run fetch:node && npm run fetch:git && npm run fetch:python && npm run install:hermes && npm run patch:hermes && npm run prune:python",
|
||||
"prepare:python": "npm run prepare:runtime",
|
||||
"package:runtime": "node scripts/package-runtime.mjs",
|
||||
"runtime:asset-name": "node scripts/runtime-asset-name.mjs",
|
||||
"prune:python": "node scripts/prune-python.mjs",
|
||||
"dev": "npm run build:main && electron .",
|
||||
"dist": "npm run build && electron-builder",
|
||||
@@ -33,4 +39,4 @@
|
||||
"dependencies": {
|
||||
"electron-updater": "^6.3.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ const sitePkgs = process.env.HERMES_AGENT_SITE_PACKAGES ?? (
|
||||
)
|
||||
|
||||
const dtPath = join(sitePkgs, 'gateway', 'platforms', 'dingtalk.py')
|
||||
const browserToolPath = join(sitePkgs, 'tools', 'browser_tool.py')
|
||||
const sitecustomizePath = join(sitePkgs, 'sitecustomize.py')
|
||||
if (!existsSync(dtPath)) {
|
||||
console.error(`dingtalk.py not found at ${dtPath} — is hermes-agent installed?`)
|
||||
@@ -59,6 +60,21 @@ function patch(id, marker, find, replace) {
|
||||
applied++
|
||||
}
|
||||
|
||||
function patchText(text, id, marker, find, replace) {
|
||||
if (text.includes(marker)) {
|
||||
console.log(` · ${id} (already applied)`)
|
||||
skipped++
|
||||
return text
|
||||
}
|
||||
if (!text.includes(find)) {
|
||||
console.log(` ✗ ${id} (anchor not found — upstream changed?)`)
|
||||
return text
|
||||
}
|
||||
applied++
|
||||
console.log(` ✓ ${id}`)
|
||||
return text.replace(find, replace)
|
||||
}
|
||||
|
||||
console.log(`Patching ${dtPath}`)
|
||||
|
||||
// NOTE: the former `dt-pre-start` patch was retired — hermes-agent now ships
|
||||
@@ -179,6 +195,63 @@ if (src !== before) {
|
||||
writeFileSync(dtPath, src)
|
||||
}
|
||||
|
||||
if (existsSync(browserToolPath)) {
|
||||
console.log(`Patching ${browserToolPath}`)
|
||||
let browserSrc = readFileSync(browserToolPath, 'utf-8')
|
||||
const browserBefore = browserSrc
|
||||
|
||||
browserSrc = patchText(
|
||||
browserSrc,
|
||||
'browser-stdout-decode-fallback',
|
||||
'# patch:browser-stdout-decode-fallback',
|
||||
`from hermes_cli.config import cfg_get\n`,
|
||||
`from hermes_cli.config import cfg_get
|
||||
|
||||
# patch:browser-stdout-decode-fallback
|
||||
def _hermes_read_browser_output(path: str) -> str:
|
||||
data = Path(path).read_bytes()
|
||||
for encoding in ("utf-8", "gb18030"):
|
||||
try:
|
||||
return data.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return data.decode("utf-8", errors="replace")
|
||||
`,
|
||||
)
|
||||
|
||||
for (const [id, find, replace] of [
|
||||
[
|
||||
'browser-fallback-stdout-read',
|
||||
` with open(stdout_path, "r", encoding="utf-8") as f:
|
||||
stdout = f.read().strip()`,
|
||||
` # patch:browser-fallback-stdout-read
|
||||
stdout = _hermes_read_browser_output(stdout_path).strip()`,
|
||||
],
|
||||
[
|
||||
'browser-command-stdout-read',
|
||||
` with open(stdout_path, "r", encoding="utf-8") as f:
|
||||
stdout = f.read()
|
||||
with open(stderr_path, "r", encoding="utf-8") as f:
|
||||
stderr = f.read()`,
|
||||
` # patch:browser-command-stdout-read
|
||||
stdout = _hermes_read_browser_output(stdout_path)
|
||||
stderr = _hermes_read_browser_output(stderr_path)`,
|
||||
],
|
||||
]) {
|
||||
browserSrc = patchText(
|
||||
browserSrc,
|
||||
id,
|
||||
`# patch:${id}`,
|
||||
find,
|
||||
replace,
|
||||
)
|
||||
}
|
||||
|
||||
if (browserSrc !== browserBefore) {
|
||||
writeFileSync(browserToolPath, browserSrc)
|
||||
}
|
||||
}
|
||||
|
||||
const brotlicffiCompatMarker = '# patch:brotlicffi-error-compat'
|
||||
const brotlicffiCompat = `
|
||||
${brotlicffiCompatMarker}
|
||||
@@ -194,15 +267,76 @@ except Exception:
|
||||
pass
|
||||
`
|
||||
|
||||
const sitecustomize = existsSync(sitecustomizePath) ? readFileSync(sitecustomizePath, 'utf-8') : ''
|
||||
if (sitecustomize.includes(brotlicffiCompatMarker)) {
|
||||
console.log(' · brotlicffi-error-compat (already applied)')
|
||||
skipped++
|
||||
} else {
|
||||
const nextSitecustomize = `${sitecustomize.replace(/\s*$/, '')}\n${brotlicffiCompat.trim()}\n`
|
||||
const desktopHiddenSubprocessMarker = '# patch:desktop-hidden-subprocess-defaults'
|
||||
const desktopHiddenSubprocessDefaults = `
|
||||
${desktopHiddenSubprocessMarker}
|
||||
try:
|
||||
import os as _hermes_os
|
||||
if _hermes_os.name == "nt" and _hermes_os.environ.get("HERMES_DESKTOP", "").strip().lower() == "true":
|
||||
import asyncio as _hermes_asyncio
|
||||
import subprocess as _hermes_subprocess
|
||||
if not getattr(_hermes_subprocess, "_hermes_desktop_hidden_defaults_installed", False):
|
||||
_hermes_create_no_window = getattr(_hermes_subprocess, "CREATE_NO_WINDOW", 0) or 0x08000000
|
||||
|
||||
def _hermes_apply_hidden_process_options(kwargs):
|
||||
flags = kwargs.get("creationflags", 0) or 0
|
||||
try:
|
||||
kwargs["creationflags"] = int(flags) | _hermes_create_no_window
|
||||
except Exception:
|
||||
kwargs["creationflags"] = _hermes_create_no_window
|
||||
|
||||
startupinfo = kwargs.get("startupinfo")
|
||||
if startupinfo is None:
|
||||
try:
|
||||
startupinfo = _hermes_subprocess.STARTUPINFO()
|
||||
except Exception:
|
||||
return
|
||||
kwargs["startupinfo"] = startupinfo
|
||||
try:
|
||||
startupinfo.dwFlags |= getattr(_hermes_subprocess, "STARTF_USESHOWWINDOW", 1)
|
||||
startupinfo.wShowWindow = getattr(_hermes_subprocess, "SW_HIDE", 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_hermes_original_popen = _hermes_subprocess.Popen
|
||||
_hermes_original_create_subprocess_exec = _hermes_asyncio.create_subprocess_exec
|
||||
_hermes_original_create_subprocess_shell = _hermes_asyncio.create_subprocess_shell
|
||||
|
||||
class _HermesHiddenPopen(_hermes_original_popen):
|
||||
def __init__(self, *args, **kwargs):
|
||||
_hermes_apply_hidden_process_options(kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def _hermes_hidden_create_subprocess_exec(*args, **kwargs):
|
||||
_hermes_apply_hidden_process_options(kwargs)
|
||||
return await _hermes_original_create_subprocess_exec(*args, **kwargs)
|
||||
|
||||
async def _hermes_hidden_create_subprocess_shell(*args, **kwargs):
|
||||
_hermes_apply_hidden_process_options(kwargs)
|
||||
return await _hermes_original_create_subprocess_shell(*args, **kwargs)
|
||||
|
||||
_hermes_subprocess.Popen = _HermesHiddenPopen
|
||||
_hermes_asyncio.create_subprocess_exec = _hermes_hidden_create_subprocess_exec
|
||||
_hermes_asyncio.create_subprocess_shell = _hermes_hidden_create_subprocess_shell
|
||||
_hermes_subprocess._hermes_desktop_hidden_defaults_installed = True
|
||||
except Exception:
|
||||
pass
|
||||
`
|
||||
|
||||
function appendSitecustomizePatch(id, marker, body) {
|
||||
const sitecustomize = existsSync(sitecustomizePath) ? readFileSync(sitecustomizePath, 'utf-8') : ''
|
||||
if (sitecustomize.includes(marker)) {
|
||||
console.log(` · ${id} (already applied)`)
|
||||
skipped++
|
||||
return
|
||||
}
|
||||
const nextSitecustomize = `${sitecustomize.replace(/\s*$/, '')}\n${body.trim()}\n`
|
||||
writeFileSync(sitecustomizePath, nextSitecustomize)
|
||||
console.log(' ✓ brotlicffi-error-compat')
|
||||
console.log(` ✓ ${id}`)
|
||||
applied++
|
||||
}
|
||||
|
||||
appendSitecustomizePatch('brotlicffi-error-compat', brotlicffiCompatMarker, brotlicffiCompat)
|
||||
appendSitecustomizePatch('desktop-hidden-subprocess-defaults', desktopHiddenSubprocessMarker, desktopHiddenSubprocessDefaults)
|
||||
|
||||
console.log(`Done. Applied ${applied}, skipped ${skipped}.`)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
// Download Git for Windows MinGit for Windows builds. Other platforms create
|
||||
// an empty resource directory so electron-builder can use the same resource map.
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { arch as osArch, platform as osPlatform, tmpdir } from 'node:os'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(__dirname, '..')
|
||||
|
||||
const TARGET_OS = process.env.TARGET_OS || osPlatform()
|
||||
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
|
||||
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
|
||||
const OUT_DIR = resolve(ROOT, 'resources', 'git', `${OS_LABEL}-${TARGET_ARCH}`)
|
||||
|
||||
mkdirSync(OUT_DIR, { recursive: true })
|
||||
|
||||
if (TARGET_OS !== 'win32') {
|
||||
writeFileSync(resolve(OUT_DIR, '.placeholder'), 'Git for Windows is only bundled on Windows.\n')
|
||||
console.log(`Git resource placeholder ready at ${OUT_DIR}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (TARGET_ARCH !== 'x64') {
|
||||
console.error(`Unsupported Git for Windows target: ${TARGET_OS}-${TARGET_ARCH}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (existsSync(resolve(OUT_DIR, 'cmd', 'git.exe'))) {
|
||||
console.log(`Git for Windows already present at ${OUT_DIR}, skipping`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
async function latestMinGitUrl() {
|
||||
if (process.env.GIT_FOR_WINDOWS_URL?.trim()) return process.env.GIT_FOR_WINDOWS_URL.trim()
|
||||
|
||||
const headers = { 'User-Agent': 'hermes-studio-desktop-build' }
|
||||
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN
|
||||
if (token?.trim()) headers.Authorization = `Bearer ${token.trim()}`
|
||||
|
||||
const response = await fetch('https://api.github.com/repos/git-for-windows/git/releases/latest', {
|
||||
headers,
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API returned ${response.status}`)
|
||||
}
|
||||
const release = await response.json()
|
||||
const asset = release.assets?.find(candidate =>
|
||||
typeof candidate?.name === 'string'
|
||||
&& /^MinGit-.*-64-bit\.zip$/.test(candidate.name)
|
||||
&& typeof candidate.browser_download_url === 'string',
|
||||
)
|
||||
if (!asset) throw new Error('Could not find MinGit 64-bit zip in latest Git for Windows release')
|
||||
return asset.browser_download_url
|
||||
}
|
||||
|
||||
let url
|
||||
try {
|
||||
url = await latestMinGitUrl()
|
||||
} catch (err) {
|
||||
console.error(`Failed to resolve Git for Windows download URL: ${err instanceof Error ? err.message : String(err)}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const file = url.split('/').pop() || 'mingit.zip'
|
||||
const archivePath = resolve(tmpdir(), file)
|
||||
|
||||
console.log(`Fetching ${url}`)
|
||||
const curl = spawnSync('curl', ['-fL', '--retry', '3', '-o', archivePath, url], { stdio: 'inherit' })
|
||||
if (curl.status !== 0) {
|
||||
console.error('curl failed')
|
||||
process.exit(curl.status ?? 1)
|
||||
}
|
||||
|
||||
console.log(`Extracting into ${OUT_DIR}`)
|
||||
const extract = spawnSync('tar', ['-xf', archivePath, '-C', OUT_DIR], { stdio: 'inherit' })
|
||||
if (extract.status !== 0) {
|
||||
console.error('extract failed')
|
||||
process.exit(extract.status ?? 1)
|
||||
}
|
||||
|
||||
rmSync(archivePath, { force: true })
|
||||
console.log(`Git for Windows ready at ${OUT_DIR}`)
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
// Download a portable Node.js runtime for the current (or target) platform/arch
|
||||
// and extract into resources/node/<os>-<arch>/.
|
||||
import { existsSync, mkdirSync, rmSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { arch as osArch, platform as osPlatform, tmpdir } from 'node:os'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(__dirname, '..')
|
||||
|
||||
const TARGET_OS = process.env.TARGET_OS || osPlatform()
|
||||
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
|
||||
const NODE_VERSION = (process.env.HERMES_DESKTOP_NODE_VERSION || process.env.NODE_VERSION || process.versions.node).replace(/^v/, '')
|
||||
|
||||
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
|
||||
const OUT_DIR = resolve(ROOT, 'resources', 'node', `${OS_LABEL}-${TARGET_ARCH}`)
|
||||
|
||||
const DIST_PLATFORM = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'darwin' : TARGET_OS
|
||||
const DIST_ARCH = TARGET_ARCH === 'x64' ? 'x64' : TARGET_ARCH === 'arm64' ? 'arm64' : ''
|
||||
if (!DIST_ARCH || !['win', 'darwin', 'linux'].includes(DIST_PLATFORM)) {
|
||||
console.error(`Unsupported target: ${TARGET_OS}-${TARGET_ARCH}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const ext = TARGET_OS === 'win32' ? 'zip' : 'tar.gz'
|
||||
const file = `node-v${NODE_VERSION}-${DIST_PLATFORM}-${DIST_ARCH}.${ext}`
|
||||
const baseUrl = (process.env.NODE_DIST_BASE_URL || 'https://nodejs.org/dist').replace(/\/$/, '')
|
||||
const url = `${baseUrl}/v${NODE_VERSION}/${file}`
|
||||
const marker = TARGET_OS === 'win32' ? 'node.exe' : join('bin', 'node')
|
||||
|
||||
if (existsSync(resolve(OUT_DIR, marker))) {
|
||||
console.log(`Node.js already present at ${OUT_DIR}, skipping`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
mkdirSync(OUT_DIR, { recursive: true })
|
||||
const archivePath = resolve(tmpdir(), file)
|
||||
|
||||
console.log(`Fetching ${url}`)
|
||||
const curl = spawnSync('curl', ['-fL', '--retry', '3', '-o', archivePath, url], { stdio: 'inherit' })
|
||||
if (curl.status !== 0) {
|
||||
console.error('curl failed')
|
||||
process.exit(curl.status ?? 1)
|
||||
}
|
||||
|
||||
console.log(`Extracting into ${OUT_DIR}`)
|
||||
let extract
|
||||
if (TARGET_OS === 'win32') {
|
||||
extract = spawnSync('tar', ['-xf', archivePath, '-C', OUT_DIR, '--strip-components=1'], { stdio: 'inherit' })
|
||||
} else {
|
||||
extract = spawnSync('tar', ['-xzf', archivePath, '-C', OUT_DIR, '--strip-components=1'], { stdio: 'inherit' })
|
||||
}
|
||||
if (extract.status !== 0) {
|
||||
console.error('extract failed')
|
||||
process.exit(extract.status ?? 1)
|
||||
}
|
||||
|
||||
rmSync(archivePath, { force: true })
|
||||
console.log(`Node.js ready at ${OUT_DIR}`)
|
||||
@@ -1,22 +1,69 @@
|
||||
#!/usr/bin/env node
|
||||
// Install hermes-agent into the bundled Python at resources/python/<os>-<arch>/.
|
||||
// Prefers `uv` (10-100x faster, more deterministic) and falls back to pip.
|
||||
import { existsSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import {
|
||||
chmodSync,
|
||||
copyFileSync,
|
||||
cpSync,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { basename, resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { platform as osPlatform, arch as osArch } from 'node:os'
|
||||
import { platform as osPlatform, arch as osArch, homedir as osHomedir } from 'node:os'
|
||||
import { hermesVersion } from './runtime-config.mjs'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(__dirname, '..')
|
||||
|
||||
const TARGET_OS = process.env.TARGET_OS || osPlatform()
|
||||
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
|
||||
const HERMES_VERSION = process.env.HERMES_VERSION || '0.15.2'
|
||||
const HERMES_PACKAGE = process.env.HERMES_PACKAGE || `hermes-agent[mcp]==${HERMES_VERSION}`
|
||||
const HERMES_VERSION = hermesVersion()
|
||||
// Match the packaged runtime to the channel list exposed at /hermes/channels.
|
||||
// Telegram, Discord, and Slack are covered by "messaging". We intentionally
|
||||
// install Matrix's plaintext deps below instead of using the "matrix" extra:
|
||||
// that extra pulls mautrix[encryption] -> python-olm, which needs a fragile
|
||||
// native build on desktop packaging machines. WhatsApp, QQBot, and Weixin do
|
||||
// not expose dedicated hermes-agent extras; their deps are covered by base or
|
||||
// the channel extras below.
|
||||
const HERMES_EXTRAS = [
|
||||
'mcp',
|
||||
'messaging',
|
||||
'slack',
|
||||
'wecom',
|
||||
'dingtalk',
|
||||
'feishu',
|
||||
].join(',')
|
||||
const HERMES_PACKAGE = process.env.HERMES_PACKAGE || `hermes-agent[${HERMES_EXTRAS}]==${HERMES_VERSION}`
|
||||
const EXTRA_PYTHON_PACKAGES = splitPackageList(
|
||||
process.env.HERMES_EXTRA_PYTHON_PACKAGES || [
|
||||
'websockets',
|
||||
'mautrix==0.21.0',
|
||||
'Markdown==3.10.2',
|
||||
'aiosqlite==0.22.1',
|
||||
'asyncpg==0.31.0',
|
||||
'aiohttp-socks==0.11.0',
|
||||
].join(' '),
|
||||
)
|
||||
const BROWSER_PACKAGES = splitPackageList(
|
||||
process.env.HERMES_BROWSER_PACKAGES || 'agent-browser@^0.26.0 @askjo/camofox-browser@^1.5.2',
|
||||
)
|
||||
const SKIP_BROWSER_RUNTIME = process.env.HERMES_SKIP_BROWSER_RUNTIME === '1'
|
||||
|| process.env.HERMES_SKIP_BROWSER_RUNTIME?.toLowerCase() === 'true'
|
||||
|
||||
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
|
||||
const PY_DIR = resolve(ROOT, 'resources', 'python', `${OS_LABEL}-${TARGET_ARCH}`)
|
||||
const NODE_DIR = resolve(ROOT, 'resources', 'node', `${OS_LABEL}-${TARGET_ARCH}`)
|
||||
const NODE_PREFIX = resolve(PY_DIR, 'node')
|
||||
const AGENT_BROWSER_HOME = resolve(PY_DIR, 'agent-browser')
|
||||
const PLAYWRIGHT_BROWSERS_PATH = resolve(PY_DIR, 'ms-playwright')
|
||||
|
||||
const pyBin = TARGET_OS === 'win32'
|
||||
? resolve(PY_DIR, 'python.exe')
|
||||
@@ -32,34 +79,309 @@ function hasUv() {
|
||||
return r.status === 0
|
||||
}
|
||||
|
||||
let r
|
||||
if (hasUv()) {
|
||||
console.log(`→ Installing ${HERMES_PACKAGE} via uv`)
|
||||
r = spawnSync('uv', [
|
||||
'pip', 'install',
|
||||
'--python', pyBin,
|
||||
HERMES_PACKAGE,
|
||||
], { stdio: 'inherit' })
|
||||
} else {
|
||||
console.log(`→ Installing ${HERMES_PACKAGE} via pip`)
|
||||
r = spawnSync(pyBin, [
|
||||
'-m', 'pip', 'install',
|
||||
HERMES_PACKAGE,
|
||||
'--no-warn-script-location',
|
||||
'--disable-pip-version-check',
|
||||
], { stdio: 'inherit' })
|
||||
function splitPackageList(value) {
|
||||
return value
|
||||
.split(/[,\s]+/)
|
||||
.map(part => part.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
if (r.status !== 0) process.exit(r.status ?? 1)
|
||||
|
||||
r = spawnSync(pyBin, [
|
||||
'-c',
|
||||
'import mcp; import tools.mcp_tool as t; assert t._MCP_AVAILABLE',
|
||||
], { stdio: 'inherit' })
|
||||
if (r.status !== 0) {
|
||||
console.error('MCP Python SDK sanity check failed')
|
||||
process.exit(r.status ?? 1)
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, { stdio: 'inherit', ...options })
|
||||
if (result.status !== 0) process.exit(result.status ?? 1)
|
||||
return result
|
||||
}
|
||||
|
||||
function optionalRun(command, args, options = {}) {
|
||||
return spawnSync(command, args, { stdio: 'inherit', ...options })
|
||||
}
|
||||
|
||||
function commandInvocation(command) {
|
||||
if (TARGET_OS === 'win32' && command.toLowerCase().endsWith('.cmd')) {
|
||||
const cmdCommand = /[\s&()[\]{}^=;!'+,`~]/.test(command) ? `"${command}"` : command
|
||||
return { command: 'cmd.exe', argsPrefix: ['/d', '/s', '/c', cmdCommand] }
|
||||
}
|
||||
return { command, argsPrefix: [] }
|
||||
}
|
||||
|
||||
function runInvocation(invocation, args, options = {}) {
|
||||
return run(invocation.command, [...invocation.argsPrefix, ...args], options)
|
||||
}
|
||||
|
||||
function optionalRunInvocation(invocation, args, options = {}) {
|
||||
return optionalRun(invocation.command, [...invocation.argsPrefix, ...args], options)
|
||||
}
|
||||
|
||||
function pythonBuildEnv() {
|
||||
if (TARGET_OS !== 'darwin') return process.env
|
||||
|
||||
const env = { ...process.env }
|
||||
if (!env.AR && existsSync('/usr/bin/ar')) env.AR = '/usr/bin/ar'
|
||||
if (!env.RANLIB && existsSync('/usr/bin/ranlib')) env.RANLIB = '/usr/bin/ranlib'
|
||||
return env
|
||||
}
|
||||
|
||||
function installPythonPackages(packages, label) {
|
||||
if (packages.length === 0) return
|
||||
const env = pythonBuildEnv()
|
||||
if (hasUv()) {
|
||||
console.log(`→ Installing ${label} via uv: ${packages.join(' ')}`)
|
||||
run('uv', [
|
||||
'pip', 'install',
|
||||
'--python', pyBin,
|
||||
...packages,
|
||||
], { env })
|
||||
} else {
|
||||
console.log(`→ Installing ${label} via pip: ${packages.join(' ')}`)
|
||||
run(pyBin, [
|
||||
'-m', 'pip', 'install',
|
||||
...packages,
|
||||
'--no-warn-script-location',
|
||||
'--disable-pip-version-check',
|
||||
], { env })
|
||||
}
|
||||
}
|
||||
|
||||
function npmCommand() {
|
||||
const bundled = TARGET_OS === 'win32'
|
||||
? resolve(NODE_DIR, 'npm.cmd')
|
||||
: resolve(NODE_DIR, 'bin', 'npm')
|
||||
const candidates = TARGET_OS === 'win32'
|
||||
? [bundled, 'npm.cmd', 'npm.exe', 'npm']
|
||||
: [bundled, 'npm']
|
||||
for (const candidate of candidates) {
|
||||
if (candidate === bundled && !existsSync(candidate)) continue
|
||||
const invocation = commandInvocation(candidate)
|
||||
const result = optionalRunInvocation(invocation, ['--version'], { stdio: 'ignore', env: browserRuntimeEnv(false) })
|
||||
if (result.status === 0) return invocation
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function agentBrowserCommand() {
|
||||
if (TARGET_OS === 'win32') {
|
||||
return resolve(NODE_PREFIX, 'agent-browser.cmd')
|
||||
}
|
||||
return resolve(NODE_PREFIX, 'bin', 'agent-browser')
|
||||
}
|
||||
|
||||
function browserRuntimeEnv(includeAgentBrowser = true) {
|
||||
const bundledNodeBin = TARGET_OS === 'win32'
|
||||
? NODE_DIR
|
||||
: resolve(NODE_DIR, 'bin')
|
||||
const nodePath = TARGET_OS === 'win32'
|
||||
? NODE_PREFIX
|
||||
: resolve(NODE_PREFIX, 'bin')
|
||||
const inheritedPath = process.env.PATH || process.env.Path || ''
|
||||
const pathKey = TARGET_OS === 'win32' ? 'Path' : 'PATH'
|
||||
const browserExecutable = includeAgentBrowser ? ensureBundledBrowserExecutable() : null
|
||||
const pathEntries = includeAgentBrowser
|
||||
? [nodePath, bundledNodeBin, inheritedPath]
|
||||
: [bundledNodeBin, inheritedPath]
|
||||
const env = {
|
||||
...process.env,
|
||||
[pathKey]: pathEntries.filter(Boolean).join(TARGET_OS === 'win32' ? ';' : ':'),
|
||||
HERMES_AGENT_NODE: TARGET_OS === 'win32' ? resolve(NODE_DIR, 'node.exe') : resolve(NODE_DIR, 'bin', 'node'),
|
||||
HERMES_AGENT_NODE_ROOT: NODE_DIR,
|
||||
AGENT_BROWSER_HOME,
|
||||
PLAYWRIGHT_BROWSERS_PATH,
|
||||
}
|
||||
if (browserExecutable) env.AGENT_BROWSER_EXECUTABLE_PATH = browserExecutable
|
||||
return env
|
||||
}
|
||||
|
||||
function bundledBrowserExecutableNames() {
|
||||
if (TARGET_OS === 'win32') return new Set(['chrome.exe'])
|
||||
if (TARGET_OS === 'darwin') return new Set(['Google Chrome for Testing', 'Google Chrome', 'Chromium', 'chrome'])
|
||||
return new Set(['chrome', 'chromium', 'chromium-browser'])
|
||||
}
|
||||
|
||||
function defaultAgentBrowserHomes() {
|
||||
const candidates = [
|
||||
process.env.USERPROFILE,
|
||||
process.env.UserProfile,
|
||||
process.env.HOME,
|
||||
process.env.HOMEDRIVE && process.env.HOMEPATH
|
||||
? `${process.env.HOMEDRIVE}${process.env.HOMEPATH}`
|
||||
: null,
|
||||
osHomedir(),
|
||||
]
|
||||
return Array.from(new Set(
|
||||
candidates
|
||||
.map(home => home?.trim())
|
||||
.filter(Boolean)
|
||||
.map(home => resolve(home, '.agent-browser')),
|
||||
))
|
||||
}
|
||||
|
||||
function findBrowserInstallInHome(home) {
|
||||
const names = bundledBrowserExecutableNames()
|
||||
const browsersDir = join(home, 'browsers')
|
||||
const bundleDirs = []
|
||||
|
||||
if (existsSync(browsersDir)) {
|
||||
try {
|
||||
for (const entry of readdirSync(browsersDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) bundleDirs.push(join(browsersDir, entry.name))
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
for (const bundleDir of bundleDirs) {
|
||||
const executable = findBrowserExecutableUnder(bundleDir, names)
|
||||
if (executable) return { executable, bundleDir }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function findBrowserExecutableUnder(root, names) {
|
||||
const stack = [root].filter(existsSync)
|
||||
const visited = new Set()
|
||||
|
||||
while (stack.length > 0) {
|
||||
const dir = stack.pop()
|
||||
if (!dir || visited.has(dir)) continue
|
||||
visited.add(dir)
|
||||
|
||||
let entries
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const path = join(dir, entry.name)
|
||||
if (entry.isFile() && names.has(entry.name)) return path
|
||||
if (entry.isDirectory()) stack.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function findBundledBrowserExecutable() {
|
||||
return findBrowserInstallInHome(AGENT_BROWSER_HOME)?.executable ?? null
|
||||
}
|
||||
|
||||
function ensureBundledBrowserExecutable() {
|
||||
const bundled = findBrowserInstallInHome(AGENT_BROWSER_HOME)
|
||||
if (bundled) return bundled.executable
|
||||
|
||||
const searchedHomes = []
|
||||
for (const fallbackHome of defaultAgentBrowserHomes()) {
|
||||
if (fallbackHome === AGENT_BROWSER_HOME) continue
|
||||
searchedHomes.push(fallbackHome)
|
||||
|
||||
const fallback = findBrowserInstallInHome(fallbackHome)
|
||||
if (!fallback) continue
|
||||
|
||||
const targetBrowsersDir = join(AGENT_BROWSER_HOME, 'browsers')
|
||||
const targetBundleDir = join(targetBrowsersDir, basename(fallback.bundleDir))
|
||||
mkdirSync(targetBrowsersDir, { recursive: true })
|
||||
cpSync(fallback.bundleDir, targetBundleDir, { recursive: true, force: true, verbatimSymlinks: true })
|
||||
console.log(`✓ copied Chrome bundle into ${targetBundleDir}`)
|
||||
|
||||
return findBundledBrowserExecutable()
|
||||
}
|
||||
|
||||
if (searchedHomes.length > 0) {
|
||||
console.warn(`! no Chrome bundle found in fallback agent-browser homes: ${searchedHomes.join(', ')}`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function sitePackagesDir() {
|
||||
if (TARGET_OS === 'win32') {
|
||||
return resolve(PY_DIR, 'Lib', 'site-packages')
|
||||
}
|
||||
const libDir = resolve(PY_DIR, 'lib')
|
||||
const py = readdirSync(libDir).find(n => /^python\d+\.\d+$/.test(n))
|
||||
if (!py) throw new Error(`Could not locate pythonX.Y under ${libDir}`)
|
||||
return resolve(libDir, py, 'site-packages')
|
||||
}
|
||||
|
||||
function pythonModuleExists(moduleName) {
|
||||
const result = optionalRun(pyBin, [
|
||||
'-c',
|
||||
`import importlib.util, sys; sys.exit(0 if importlib.util.find_spec(${JSON.stringify(moduleName)}) else 1)`,
|
||||
], { stdio: 'ignore' })
|
||||
return result.status === 0
|
||||
}
|
||||
|
||||
function removeBrokenDashboardAuthPlugin() {
|
||||
if (pythonModuleExists('hermes_cli.dashboard_auth')) return
|
||||
|
||||
const pluginDir = resolve(sitePackagesDir(), 'plugins', 'dashboard_auth', 'nous')
|
||||
if (!existsSync(pluginDir)) return
|
||||
|
||||
rmSync(pluginDir, { recursive: true, force: true })
|
||||
console.warn(
|
||||
'! Removed bundled dashboard_auth/nous plugin because hermes_cli.dashboard_auth is missing from the hermes-agent package',
|
||||
)
|
||||
}
|
||||
|
||||
function installBrowserRuntime() {
|
||||
if (SKIP_BROWSER_RUNTIME) {
|
||||
console.warn('! Skipping bundled browser runtime because HERMES_SKIP_BROWSER_RUNTIME is set')
|
||||
return
|
||||
}
|
||||
if (BROWSER_PACKAGES.length === 0) {
|
||||
console.warn('! Skipping bundled browser runtime because HERMES_BROWSER_PACKAGES is empty')
|
||||
return
|
||||
}
|
||||
|
||||
const npm = npmCommand()
|
||||
if (!npm) {
|
||||
console.error('npm not found; bundled browser runtime requires Node.js/npm')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`→ Installing browser runtime via npm prefix ${NODE_PREFIX}`)
|
||||
runInvocation(npm, [
|
||||
'install',
|
||||
'-g',
|
||||
'--prefix',
|
||||
NODE_PREFIX,
|
||||
'--silent',
|
||||
'--ignore-scripts',
|
||||
...BROWSER_PACKAGES,
|
||||
])
|
||||
|
||||
const ab = agentBrowserCommand()
|
||||
if (!existsSync(ab)) {
|
||||
console.error(`agent-browser binary not found at ${ab} after npm install`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`→ Installing Chromium for bundled agent-browser at ${AGENT_BROWSER_HOME}`)
|
||||
runInvocation(commandInvocation(ab), ['install'], { env: browserRuntimeEnv() })
|
||||
|
||||
const browserExecutable = ensureBundledBrowserExecutable()
|
||||
if (!browserExecutable) {
|
||||
console.error(`Bundled Chrome executable not found under ${AGENT_BROWSER_HOME} after agent-browser install`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`✓ bundled Chrome executable available at ${browserExecutable}`)
|
||||
}
|
||||
|
||||
installPythonPackages([HERMES_PACKAGE], 'hermes-agent')
|
||||
installPythonPackages(EXTRA_PYTHON_PACKAGES, 'extra Python runtime packages')
|
||||
removeBrokenDashboardAuthPlugin()
|
||||
installBrowserRuntime()
|
||||
|
||||
run(pyBin, [
|
||||
'-c',
|
||||
[
|
||||
'import importlib.util',
|
||||
'import mcp',
|
||||
'import tools.mcp_tool as t',
|
||||
'assert t._MCP_AVAILABLE',
|
||||
'assert importlib.util.find_spec("websockets") is not None',
|
||||
].join('; '),
|
||||
])
|
||||
|
||||
const hermesBin = TARGET_OS === 'win32'
|
||||
? resolve(PY_DIR, 'Scripts', 'hermes.exe')
|
||||
: resolve(PY_DIR, 'bin', 'hermes')
|
||||
@@ -76,14 +398,11 @@ if (!existsSync(hermesBin)) {
|
||||
// it at the venv root with a *relative* symlink so the venv stays portable when copied
|
||||
// into the packaged .app/.exe (an absolute symlink would break the moment the bundle
|
||||
// is moved to /Applications/...).
|
||||
const { readdirSync, symlinkSync, copyFileSync, unlinkSync, lstatSync } = await import('node:fs')
|
||||
function siteRunAgentRelative() {
|
||||
if (TARGET_OS === 'win32') {
|
||||
return ['Lib', 'site-packages', 'run_agent.py'].join('\\')
|
||||
}
|
||||
const libDir = resolve(PY_DIR, 'lib')
|
||||
const py = readdirSync(libDir).find(n => /^python\d+\.\d+$/.test(n))
|
||||
return ['lib', py, 'site-packages', 'run_agent.py'].join('/')
|
||||
return `${sitePackagesDir().slice(PY_DIR.length + 1)}/run_agent.py`
|
||||
}
|
||||
{
|
||||
const relSrc = siteRunAgentRelative()
|
||||
@@ -102,7 +421,6 @@ function siteRunAgentRelative() {
|
||||
// Relocate: replace the pip-generated launcher (which embeds an absolute
|
||||
// shebang to the build-time Python path) with a relative wrapper so the
|
||||
// bundled venv works after being moved into the .app/.exe payload.
|
||||
const { writeFileSync, chmodSync } = await import('node:fs')
|
||||
if (TARGET_OS === 'win32') {
|
||||
// Windows: pip generates a .exe launcher that embeds a relative shebang
|
||||
// already. Add a .cmd wrapper that prefers the colocated python.exe.
|
||||
@@ -139,8 +457,23 @@ if (TARGET_OS === 'win32') {
|
||||
|
||||
console.log(`✓ hermes installed at ${hermesBin} (relocatable launcher)`)
|
||||
|
||||
r = spawnSync(hermesCheckCommand, hermesCheckArgs, { stdio: 'inherit' })
|
||||
if (r.status !== 0) {
|
||||
console.error('hermes --version failed')
|
||||
process.exit(r.status ?? 1)
|
||||
run(hermesCheckCommand, hermesCheckArgs)
|
||||
|
||||
if (!SKIP_BROWSER_RUNTIME) {
|
||||
run(pyBin, [
|
||||
'-c',
|
||||
[
|
||||
'import os, shutil',
|
||||
`os.environ["PLAYWRIGHT_BROWSERS_PATH"] = ${JSON.stringify(PLAYWRIGHT_BROWSERS_PATH)}`,
|
||||
'from tools.browser_tool import _chromium_installed',
|
||||
'assert shutil.which("agent-browser") is not None',
|
||||
'assert _chromium_installed()',
|
||||
].join('; '),
|
||||
], { env: browserRuntimeEnv() })
|
||||
}
|
||||
|
||||
if (SKIP_BROWSER_RUNTIME) {
|
||||
console.log('✓ hermes Python, MCP, and websockets checks passed; browser runtime skipped')
|
||||
} else {
|
||||
console.log('✓ hermes Python, MCP, websockets, agent-browser, and Chromium checks passed')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
// Package prepared Python/Node/Git runtime resources into a release asset.
|
||||
import {
|
||||
cpSync,
|
||||
createReadStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { arch as osArch, platform as osPlatform, tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(__dirname, '..')
|
||||
const TARGET_OS = process.env.TARGET_OS || osPlatform()
|
||||
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
|
||||
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
|
||||
const PLATFORM = `${OS_LABEL}-${TARGET_ARCH}`
|
||||
const OUT_DIR = resolve(ROOT, 'release', 'runtime')
|
||||
|
||||
const PY_DIR = resolve(ROOT, 'resources', 'python', PLATFORM)
|
||||
const NODE_DIR = resolve(ROOT, 'resources', 'node', PLATFORM)
|
||||
const GIT_DIR = resolve(ROOT, 'resources', 'git', PLATFORM)
|
||||
const pyBin = TARGET_OS === 'win32'
|
||||
? resolve(PY_DIR, 'python.exe')
|
||||
: resolve(PY_DIR, 'bin', 'python3')
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, { stdio: 'inherit', ...options })
|
||||
if (result.status !== 0) process.exit(result.status ?? 1)
|
||||
return result
|
||||
}
|
||||
|
||||
function output(command, args) {
|
||||
const result = spawnSync(command, args, { encoding: 'utf-8' })
|
||||
if (result.status !== 0) {
|
||||
process.stderr.write(result.stderr || result.stdout || '')
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
return result.stdout.trim()
|
||||
}
|
||||
|
||||
async function sha256File(file) {
|
||||
const hash = createHash('sha256')
|
||||
await new Promise((resolvePromise, rejectPromise) => {
|
||||
const stream = createReadStream(file)
|
||||
stream.on('data', chunk => hash.update(chunk))
|
||||
stream.on('end', resolvePromise)
|
||||
stream.on('error', rejectPromise)
|
||||
})
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
for (const dir of [PY_DIR, NODE_DIR]) {
|
||||
if (!existsSync(dir)) {
|
||||
console.error(`Runtime directory missing: ${dir}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
const hermesAgentVersion = output(pyBin, [
|
||||
'-c',
|
||||
'import importlib.metadata as m; print(m.version("hermes-agent"))',
|
||||
])
|
||||
const assetName = `hermes-runtime-hermes-agent-${hermesAgentVersion}-${PLATFORM}.tar.gz`
|
||||
const manifestName = `hermes-runtime-${PLATFORM}.json`
|
||||
|
||||
mkdirSync(OUT_DIR, { recursive: true })
|
||||
const stage = mkdtempSync(join(tmpdir(), `hermes-runtime-${PLATFORM}-`))
|
||||
|
||||
try {
|
||||
cpSync(PY_DIR, join(stage, 'python'), { recursive: true, force: true, verbatimSymlinks: true })
|
||||
cpSync(NODE_DIR, join(stage, 'node'), { recursive: true, force: true, verbatimSymlinks: true })
|
||||
if (existsSync(GIT_DIR)) {
|
||||
cpSync(GIT_DIR, join(stage, 'git'), { recursive: true, force: true, verbatimSymlinks: true })
|
||||
} else {
|
||||
mkdirSync(join(stage, 'git'), { recursive: true })
|
||||
writeFileSync(join(stage, 'git', '.placeholder'), 'Git for Windows is only bundled on Windows.\n')
|
||||
}
|
||||
|
||||
const runtimeManifest = {
|
||||
schema: 1,
|
||||
platform: PLATFORM,
|
||||
targetOs: TARGET_OS,
|
||||
targetArch: TARGET_ARCH,
|
||||
hermesAgentVersion,
|
||||
asset: {
|
||||
name: assetName,
|
||||
},
|
||||
}
|
||||
writeFileSync(join(stage, 'runtime-manifest.json'), JSON.stringify(runtimeManifest, null, 2) + '\n')
|
||||
|
||||
const assetPath = resolve(OUT_DIR, assetName)
|
||||
rmSync(assetPath, { force: true })
|
||||
run('tar', ['-czf', assetPath, '-C', stage, '.'])
|
||||
|
||||
const sha256 = await sha256File(assetPath)
|
||||
writeFileSync(`${assetPath}.sha256`, `${sha256} ${assetName}\n`)
|
||||
|
||||
const platformManifest = {
|
||||
...runtimeManifest,
|
||||
createdAt: new Date().toISOString(),
|
||||
asset: {
|
||||
name: assetName,
|
||||
sha256,
|
||||
size: statSync(assetPath).size,
|
||||
},
|
||||
}
|
||||
writeFileSync(resolve(OUT_DIR, manifestName), JSON.stringify(platformManifest, null, 2) + '\n')
|
||||
|
||||
console.log(`Runtime asset: ${assetPath}`)
|
||||
console.log(`Runtime manifest: ${resolve(OUT_DIR, manifestName)}`)
|
||||
} finally {
|
||||
rmSync(stage, { recursive: true, force: true })
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
import { arch as osArch, platform as osPlatform } from 'node:os'
|
||||
import { hermesVersion, runtimeReleaseTag } from './runtime-config.mjs'
|
||||
|
||||
const TARGET_OS = process.env.TARGET_OS || osPlatform()
|
||||
const TARGET_ARCH = process.env.TARGET_ARCH || osArch()
|
||||
const HERMES_VERSION = hermesVersion()
|
||||
const RUNTIME_RELEASE_TAG = runtimeReleaseTag()
|
||||
const OS_LABEL = TARGET_OS === 'win32' ? 'win' : TARGET_OS === 'darwin' ? 'mac' : TARGET_OS
|
||||
|
||||
if (!['win', 'mac', 'linux'].includes(OS_LABEL) || !['x64', 'arm64'].includes(TARGET_ARCH)) {
|
||||
console.error(`Unsupported runtime target: ${TARGET_OS}-${TARGET_ARCH}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const platform = `${OS_LABEL}-${TARGET_ARCH}`
|
||||
const asset = `hermes-runtime-hermes-agent-${HERMES_VERSION}-${platform}.tar.gz`
|
||||
const manifest = `hermes-runtime-${platform}.json`
|
||||
|
||||
if (process.argv.includes('--manifest')) {
|
||||
console.log(manifest)
|
||||
} else if (process.argv.includes('--platform')) {
|
||||
console.log(platform)
|
||||
} else if (process.argv.includes('--release-tag')) {
|
||||
console.log(RUNTIME_RELEASE_TAG)
|
||||
} else {
|
||||
console.log(asset)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const DEFAULT_HERMES_VERSION = '0.15.2'
|
||||
|
||||
export function hermesVersion(env = process.env) {
|
||||
return env.HERMES_VERSION || DEFAULT_HERMES_VERSION
|
||||
}
|
||||
|
||||
export function runtimeReleaseTag(env = process.env) {
|
||||
const version = hermesVersion(env)
|
||||
return env.HERMES_DESKTOP_RUNTIME_RELEASE_TAG
|
||||
|| env.RUNTIME_RELEASE_TAG
|
||||
|| `hermes-${version}-runtime`
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { runtimeReleaseTag } from './runtime-config.mjs'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(__dirname, '..')
|
||||
const outFile = resolve(ROOT, 'build', 'runtime-release.json')
|
||||
const tag = runtimeReleaseTag()
|
||||
|
||||
mkdirSync(dirname(outFile), { recursive: true })
|
||||
writeFileSync(outFile, JSON.stringify({ tag }, null, 2) + '\n')
|
||||
console.log(`Runtime release metadata: ${tag}`)
|
||||
@@ -0,0 +1 @@
|
||||
export const HERMES_CLI_ARG = '--hermes-cli'
|
||||
@@ -0,0 +1,232 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import {
|
||||
appendFileSync,
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { delimiter, dirname, join, resolve } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { HERMES_CLI_ARG } from './cli-constants'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
const SHIM_MARKER = 'HERMES_STUDIO_CLI_SHIM'
|
||||
const PATH_MARKER_START = '# >>> Hermes Studio CLI shim >>>'
|
||||
const PATH_MARKER_END = '# <<< Hermes Studio CLI shim <<<'
|
||||
|
||||
type ShimInstallStatus = 'installed' | 'updated' | 'unchanged' | 'skipped'
|
||||
|
||||
export interface CliShimInstallResult {
|
||||
shimPath: string
|
||||
status: ShimInstallStatus
|
||||
pathUpdated: boolean
|
||||
reason?: string
|
||||
}
|
||||
|
||||
interface CliShimInstallOptions {
|
||||
env?: NodeJS.ProcessEnv
|
||||
executablePath?: string
|
||||
homeDir?: string
|
||||
platform?: NodeJS.Platform
|
||||
}
|
||||
|
||||
function platformDelimiter(platform: NodeJS.Platform): string {
|
||||
return platform === 'win32' ? ';' : delimiter
|
||||
}
|
||||
|
||||
function pathKey(value: string, platform: NodeJS.Platform): string {
|
||||
const normalized = resolve(value)
|
||||
return platform === 'win32' ? normalized.toLowerCase() : normalized
|
||||
}
|
||||
|
||||
export function pathContainsDir(pathValue: string | undefined, binDir: string, platform: NodeJS.Platform = process.platform): boolean {
|
||||
if (!pathValue) return false
|
||||
const target = pathKey(binDir, platform)
|
||||
return pathValue
|
||||
.split(platformDelimiter(platform))
|
||||
.map(entry => entry.trim())
|
||||
.filter(Boolean)
|
||||
.some(entry => pathKey(entry, platform) === target)
|
||||
}
|
||||
|
||||
function executableForShim(options: Required<Pick<CliShimInstallOptions, 'env' | 'executablePath' | 'platform'>>): string {
|
||||
const appImage = options.platform === 'linux' ? options.env.APPIMAGE?.trim() : ''
|
||||
return appImage || options.executablePath
|
||||
}
|
||||
|
||||
export function shimPathForPlatform(binDir: string, platform: NodeJS.Platform = process.platform): string {
|
||||
return join(binDir, platform === 'win32' ? 'hermes-studio.cmd' : 'hermes-studio')
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`
|
||||
}
|
||||
|
||||
export function createShimContent(executablePath: string, platform: NodeJS.Platform = process.platform): string {
|
||||
if (platform === 'win32') {
|
||||
return [
|
||||
'@echo off',
|
||||
`rem ${SHIM_MARKER}`,
|
||||
`set "APP=${executablePath}"`,
|
||||
'if not exist "%APP%" (',
|
||||
' echo Hermes Studio executable not found at "%APP%" 1>&2',
|
||||
' exit /b 127',
|
||||
')',
|
||||
'set ELECTRON_RUN_AS_NODE=',
|
||||
`"${'%APP%'}" -- ${HERMES_CLI_ARG} %*`,
|
||||
'exit /b %ERRORLEVEL%',
|
||||
'',
|
||||
].join('\r\n')
|
||||
}
|
||||
|
||||
return [
|
||||
'#!/bin/sh',
|
||||
`# ${SHIM_MARKER}`,
|
||||
`APP=${shellQuote(executablePath)}`,
|
||||
'if [ ! -x "$APP" ]; then',
|
||||
' echo "Hermes Studio executable not found at $APP" >&2',
|
||||
' exit 127',
|
||||
'fi',
|
||||
'unset ELECTRON_RUN_AS_NODE',
|
||||
`exec "$APP" -- ${HERMES_CLI_ARG} "$@"`,
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function isManagedShim(content: string): boolean {
|
||||
return content.includes(SHIM_MARKER) && content.includes(HERMES_CLI_ARG)
|
||||
}
|
||||
|
||||
function writeShim(shimPath: string, content: string, platform: NodeJS.Platform): ShimInstallStatus {
|
||||
if (existsSync(shimPath)) {
|
||||
const existing = readFileSync(shimPath, 'utf-8')
|
||||
if (existing === content) return 'unchanged'
|
||||
if (!isManagedShim(existing)) return 'skipped'
|
||||
writeFileSync(shimPath, content, 'utf-8')
|
||||
if (platform !== 'win32') chmodSync(shimPath, 0o755)
|
||||
return 'updated'
|
||||
}
|
||||
|
||||
writeFileSync(shimPath, content, { encoding: 'utf-8', mode: platform === 'win32' ? 0o644 : 0o755 })
|
||||
if (platform !== 'win32') chmodSync(shimPath, 0o755)
|
||||
return 'installed'
|
||||
}
|
||||
|
||||
function shellProfilePaths(homeDir: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string[] {
|
||||
if (platform === 'win32') return []
|
||||
|
||||
const shell = env.SHELL?.trim() || ''
|
||||
const name = shell.split('/').pop() || ''
|
||||
if (name === 'fish') return [join(homeDir, '.config', 'fish', 'conf.d', 'hermes-studio.fish')]
|
||||
if (name === 'bash') return [join(homeDir, '.bash_profile'), join(homeDir, '.bashrc')]
|
||||
if (name === 'zsh' || platform === 'darwin') return [join(homeDir, '.zprofile'), join(homeDir, '.zshrc')]
|
||||
return [join(homeDir, '.profile')]
|
||||
}
|
||||
|
||||
function profileMentionsUserBin(content: string, homeDir: string): boolean {
|
||||
return content.includes('$HOME/bin')
|
||||
|| content.includes('~/bin')
|
||||
|| content.includes(resolve(homeDir, 'bin'))
|
||||
}
|
||||
|
||||
function shellPathSnippet(platform: NodeJS.Platform, profilePath: string): string {
|
||||
if (platform !== 'win32' && profilePath.endsWith('.fish')) {
|
||||
return [
|
||||
'',
|
||||
PATH_MARKER_START,
|
||||
'fish_add_path -m "$HOME/bin"',
|
||||
PATH_MARKER_END,
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
return [
|
||||
'',
|
||||
PATH_MARKER_START,
|
||||
'case ":$PATH:" in',
|
||||
' *":$HOME/bin:"*) ;;',
|
||||
' *) export PATH="$HOME/bin:$PATH" ;;',
|
||||
'esac',
|
||||
PATH_MARKER_END,
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
async function ensureWindowsUserPath(binDir: string): Promise<boolean> {
|
||||
let currentPath = ''
|
||||
try {
|
||||
const { stdout } = await execFileAsync('reg.exe', ['query', 'HKCU\\Environment', '/v', 'Path'], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 1500,
|
||||
windowsHide: true,
|
||||
})
|
||||
const line = stdout.split(/\r?\n/).find(row => /^\s*Path\s+REG_/.test(row))
|
||||
if (line) currentPath = line.replace(/^\s*Path\s+REG_\w+\s+/, '').trim()
|
||||
} catch {
|
||||
currentPath = process.env.Path || process.env.PATH || ''
|
||||
}
|
||||
|
||||
if (pathContainsDir(currentPath, binDir, 'win32')) return false
|
||||
|
||||
const separator = currentPath ? ';' : ''
|
||||
await execFileAsync('reg.exe', ['add', 'HKCU\\Environment', '/v', 'Path', '/t', 'REG_EXPAND_SZ', '/d', `${binDir}${separator}${currentPath}`, '/f'], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 1500,
|
||||
windowsHide: true,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
function ensureUnixShellPath(homeDir: string, binDir: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv): boolean {
|
||||
if (pathContainsDir(env.PATH, binDir, platform)) return false
|
||||
|
||||
let updated = false
|
||||
for (const profilePath of shellProfilePaths(homeDir, platform, env)) {
|
||||
const existing = existsSync(profilePath) ? readFileSync(profilePath, 'utf-8') : ''
|
||||
if (existing.includes(PATH_MARKER_START) || profileMentionsUserBin(existing, homeDir)) continue
|
||||
|
||||
mkdirSync(dirname(profilePath), { recursive: true })
|
||||
appendFileSync(profilePath, shellPathSnippet(platform, profilePath), 'utf-8')
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
async function ensureUserBinOnPath(homeDir: string, binDir: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv): Promise<boolean> {
|
||||
if (platform === 'win32') {
|
||||
return await ensureWindowsUserPath(binDir)
|
||||
}
|
||||
return ensureUnixShellPath(homeDir, binDir, platform, env)
|
||||
}
|
||||
|
||||
export async function installHermesStudioCliShim(options: CliShimInstallOptions = {}): Promise<CliShimInstallResult> {
|
||||
const platform = options.platform || process.platform
|
||||
const env = options.env || process.env
|
||||
const homeDir = options.homeDir || homedir()
|
||||
const binDir = resolve(homeDir, 'bin')
|
||||
const executablePath = executableForShim({
|
||||
env,
|
||||
executablePath: options.executablePath || process.execPath,
|
||||
platform,
|
||||
})
|
||||
const shimPath = shimPathForPlatform(binDir, platform)
|
||||
|
||||
mkdirSync(binDir, { recursive: true })
|
||||
const status = writeShim(shimPath, createShimContent(executablePath, platform), platform)
|
||||
const pathUpdated = await ensureUserBinOnPath(homeDir, binDir, platform, env).catch((err) => {
|
||||
console.warn(`[cli-shim] failed to update PATH: ${err instanceof Error ? err.message : String(err)}`)
|
||||
return false
|
||||
})
|
||||
|
||||
return {
|
||||
shimPath,
|
||||
status,
|
||||
pathUpdated,
|
||||
reason: status === 'skipped' ? 'existing hermes-studio shim is not managed by Hermes Studio' : undefined,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import { delimiter, dirname, join } from 'node:path'
|
||||
import {
|
||||
bundledBrowserExecutable,
|
||||
bundledGit,
|
||||
bundledNode,
|
||||
bundledPython,
|
||||
gitPathDirs,
|
||||
hermesBin,
|
||||
hermesHome,
|
||||
nodeBinDir,
|
||||
pythonDir,
|
||||
webUiHome,
|
||||
} from './paths'
|
||||
import { HERMES_CLI_ARG } from './cli-constants'
|
||||
import { ensureDesktopRuntime } from './runtime-manager'
|
||||
|
||||
export function parseHermesCliArgs(argv: string[] = process.argv): string[] | null {
|
||||
const index = argv.indexOf(HERMES_CLI_ARG)
|
||||
if (index < 0) return null
|
||||
return argv.slice(index + 1)
|
||||
}
|
||||
|
||||
export async function runBundledHermesCli(args: string[]): Promise<number> {
|
||||
try {
|
||||
await ensureDesktopRuntime()
|
||||
} catch (err) {
|
||||
console.error(`Failed to prepare Hermes runtime: ${err instanceof Error ? err.message : String(err)}`)
|
||||
return 1
|
||||
}
|
||||
|
||||
const command = hermesBin()
|
||||
if (!existsSync(command)) {
|
||||
console.error(`hermes binary missing at ${command}`)
|
||||
console.error('Run: npm run prepare:runtime (to build a local Hermes runtime)')
|
||||
return 127
|
||||
}
|
||||
|
||||
mkdirSync(webUiHome(), { recursive: true })
|
||||
mkdirSync(hermesHome(), { recursive: true })
|
||||
|
||||
const binDir = dirname(command)
|
||||
const bundledNodeBin = nodeBinDir()
|
||||
const bundledAgentBrowserBin = process.platform === 'win32'
|
||||
? join(pythonDir(), 'node')
|
||||
: join(pythonDir(), 'node', 'bin')
|
||||
const inheritedPath = process.env.PATH || process.env.Path || ''
|
||||
const pathValue = [
|
||||
binDir,
|
||||
bundledAgentBrowserBin,
|
||||
bundledNodeBin,
|
||||
gitPathDirs().join(delimiter),
|
||||
inheritedPath,
|
||||
].filter(Boolean).join(delimiter)
|
||||
const gitBin = bundledGit()
|
||||
const browserExecutable = process.env.AGENT_BROWSER_EXECUTABLE_PATH?.trim() || bundledBrowserExecutable()
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
HERMES_DESKTOP: 'true',
|
||||
HERMES_BIN: command,
|
||||
HERMES_AGENT_BRIDGE_PYTHON: bundledPython(),
|
||||
HERMES_AGENT_CLI_PYTHON: bundledPython(),
|
||||
HERMES_AGENT_ROOT: pythonDir(),
|
||||
HERMES_AGENT_NODE: bundledNode(),
|
||||
HERMES_AGENT_NODE_ROOT: process.platform === 'win32' ? bundledNodeBin : dirname(bundledNodeBin),
|
||||
AGENT_BROWSER_HOME: process.env.AGENT_BROWSER_HOME?.trim() || join(hermesHome(), 'agent-browser'),
|
||||
...(browserExecutable ? { AGENT_BROWSER_EXECUTABLE_PATH: browserExecutable } : {}),
|
||||
PLAYWRIGHT_BROWSERS_PATH: process.env.PLAYWRIGHT_BROWSERS_PATH || join(pythonDir(), 'ms-playwright'),
|
||||
...(gitBin ? { HERMES_AGENT_GIT: gitBin } : {}),
|
||||
HERMES_HOME: hermesHome(),
|
||||
HERMES_WEB_UI_HOME: webUiHome(),
|
||||
HERMES_WEBUI_STATE_DIR: webUiHome(),
|
||||
PATH: pathValue,
|
||||
}
|
||||
|
||||
return await new Promise(resolve => {
|
||||
const child = spawn(command, args, {
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
windowsHide: false,
|
||||
})
|
||||
child.once('error', (err) => {
|
||||
console.error(`Failed to run bundled Hermes CLI: ${err.message}`)
|
||||
resolve(1)
|
||||
})
|
||||
child.once('exit', (code, signal) => {
|
||||
if (typeof code === 'number') {
|
||||
resolve(code)
|
||||
return
|
||||
}
|
||||
console.error(`Bundled Hermes CLI exited from signal ${signal || 'unknown'}`)
|
||||
resolve(1)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import { startWebUiServer, stopWebUiServer, getToken } from './webui-server'
|
||||
import { desktopIcon, desktopTrayTemplateIcon, desktopWindowsTrayIcon, hermesBinExists, hermesBin } from './paths'
|
||||
import { checkForDesktopUpdates, initAutoUpdater } from './updater'
|
||||
import { t } from './desktop-i18n'
|
||||
import { installHermesStudioCliShim } from './cli-shim'
|
||||
import { parseHermesCliArgs, runBundledHermesCli } from './hermes-cli'
|
||||
import { ensureDesktopRuntime, type RuntimeProgress } from './runtime-manager'
|
||||
|
||||
const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748
|
||||
const START_HIDDEN = process.argv.includes('--hidden')
|
||||
@@ -13,6 +16,7 @@ let mainWindow: BrowserWindow | null = null
|
||||
let serverUrl: string | null = null
|
||||
let tray: Tray | null = null
|
||||
let isQuitting = false
|
||||
let isBootstrapping = false
|
||||
|
||||
function showMainWindow() {
|
||||
if (!mainWindow) {
|
||||
@@ -166,25 +170,91 @@ function splashHtml(): string {
|
||||
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Hermes Studio</title>
|
||||
<style>
|
||||
html,body{margin:0;height:100%;background:#1a1a1a;color:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;}
|
||||
.wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}
|
||||
.wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:20px}
|
||||
.dot{width:10px;height:10px;border-radius:50%;background:#888;animation:pulse 1.2s ease-in-out infinite}
|
||||
@keyframes pulse{0%,100%{opacity:.3}50%{opacity:1}}
|
||||
.row{display:flex;gap:8px}
|
||||
.row .dot:nth-child(2){animation-delay:.2s}.row .dot:nth-child(3){animation-delay:.4s}
|
||||
.label{font-size:14px;color:#999}
|
||||
.label{font-size:14px;color:#b8b8b8}
|
||||
.detail{min-height:18px;font-size:12px;color:#7f7f7f}
|
||||
.progress{width:320px;height:6px;border-radius:999px;background:#2b2b2b;overflow:hidden}
|
||||
.bar{width:0;height:100%;background:#d8d8d8;transition:width .18s ease}
|
||||
h1{font-weight:500;margin:0;font-size:18px}
|
||||
</style></head><body><div class="wrap">
|
||||
<h1>Hermes Studio</h1>
|
||||
<div class="row"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
|
||||
<div class="label">Starting local services…</div>
|
||||
<div id="label" class="label">Starting local services...</div>
|
||||
<div class="progress"><div id="bar" class="bar"></div></div>
|
||||
<div id="detail" class="detail"></div>
|
||||
</div></body></html>`
|
||||
return 'data:text/html;charset=utf-8,' + encodeURIComponent(html)
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
const units = ['KB', 'MB', 'GB']
|
||||
let value = bytes / 1024
|
||||
let unit = units[0]
|
||||
for (let i = 1; i < units.length && value >= 1024; i += 1) {
|
||||
value /= 1024
|
||||
unit = units[i]
|
||||
}
|
||||
return `${value.toFixed(value >= 100 ? 0 : 1)} ${unit}`
|
||||
}
|
||||
|
||||
function updateSplash(progress: RuntimeProgress) {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return
|
||||
const label = progress.message
|
||||
const percent = typeof progress.percent === 'number' ? Math.round(progress.percent) : null
|
||||
let detail = ''
|
||||
if (progress.receivedBytes && progress.totalBytes) {
|
||||
detail = `${formatBytes(progress.receivedBytes)} / ${formatBytes(progress.totalBytes)}`
|
||||
if (percent !== null) detail += ` (${percent}%)`
|
||||
} else if (percent !== null) {
|
||||
detail = `${percent}%`
|
||||
}
|
||||
|
||||
mainWindow.webContents.executeJavaScript(`
|
||||
{
|
||||
const label = document.getElementById('label');
|
||||
const detail = document.getElementById('detail');
|
||||
const bar = document.getElementById('bar');
|
||||
if (label) label.textContent = ${JSON.stringify(label)};
|
||||
if (detail) detail.textContent = ${JSON.stringify(detail)};
|
||||
if (bar) bar.style.width = ${JSON.stringify(percent === null ? '100%' : `${percent}%`)};
|
||||
}
|
||||
`).catch(() => undefined)
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
if (isBootstrapping) return
|
||||
isBootstrapping = true
|
||||
|
||||
try {
|
||||
await ensureDesktopRuntime(updateSplash)
|
||||
} catch (err) {
|
||||
console.error('Failed to prepare Hermes runtime:', err)
|
||||
if (mainWindow) {
|
||||
const msg = String(err instanceof Error ? err.message : err).replace(/[<>]/g, '')
|
||||
mainWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(
|
||||
`<html><body style="font-family:system-ui;padding:32px;background:#1a1a1a;color:#eee">
|
||||
<h2>Failed to prepare Hermes runtime</h2><pre style="white-space:pre-wrap;color:#f88">${msg}</pre>
|
||||
<button id="retry" style="margin-top:16px;padding:8px 14px;border:1px solid #555;border-radius:6px;background:#2b2b2b;color:#eee;cursor:pointer">Retry</button>
|
||||
<script>
|
||||
document.getElementById('retry')?.addEventListener('click', () => {
|
||||
window.hermesDesktop?.retryBootstrap?.()
|
||||
})
|
||||
</script>
|
||||
</body></html>`,
|
||||
))
|
||||
}
|
||||
isBootstrapping = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!hermesBinExists()) {
|
||||
console.error(`hermes binary missing at ${hermesBin()}`)
|
||||
console.error('Run: npm run prepare:python (to bundle Python + hermes-agent)')
|
||||
console.error('Run: npm run prepare:runtime (to build a local Hermes runtime)')
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -201,15 +271,28 @@ async function bootstrap() {
|
||||
</body></html>`,
|
||||
))
|
||||
}
|
||||
} finally {
|
||||
isBootstrapping = false
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle('hermes-desktop:get-token', () => getToken())
|
||||
ipcMain.handle('hermes-desktop:retry-bootstrap', async () => {
|
||||
if (serverUrl) {
|
||||
await mainWindow?.loadURL(serverUrl)
|
||||
return
|
||||
}
|
||||
await mainWindow?.loadURL(splashHtml())
|
||||
await bootstrap()
|
||||
})
|
||||
|
||||
function runDesktopApp() {
|
||||
const gotLock = app.requestSingleInstanceLock()
|
||||
if (!gotLock) {
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
|
||||
const gotLock = app.requestSingleInstanceLock()
|
||||
if (!gotLock) {
|
||||
app.quit()
|
||||
} else {
|
||||
app.on('second-instance', (_event, argv) => {
|
||||
if (argv.includes('--quit')) {
|
||||
quitApp()
|
||||
@@ -229,6 +312,15 @@ if (!gotLock) {
|
||||
// visual clutter. macOS keeps a menu (system requirement) but Electron's
|
||||
// default is fine there.
|
||||
if (process.platform !== 'darwin') Menu.setApplicationMenu(null)
|
||||
if (app.isPackaged) {
|
||||
installHermesStudioCliShim().then(result => {
|
||||
if (result.status === 'skipped') {
|
||||
console.warn(`[cli-shim] ${result.reason}: ${result.shimPath}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
console.warn(`[cli-shim] failed to install hermes-studio command: ${err instanceof Error ? err.message : String(err)}`)
|
||||
})
|
||||
}
|
||||
createTray()
|
||||
createWindow()
|
||||
bootstrap()
|
||||
@@ -262,3 +354,15 @@ if (!gotLock) {
|
||||
app.exit(0)
|
||||
})
|
||||
}
|
||||
|
||||
const hermesCliArgs = parseHermesCliArgs(process.argv)
|
||||
if (hermesCliArgs) {
|
||||
runBundledHermesCli(hermesCliArgs)
|
||||
.then(code => app.exit(code))
|
||||
.catch(err => {
|
||||
console.error(`Failed to run bundled Hermes CLI: ${err instanceof Error ? err.message : String(err)}`)
|
||||
app.exit(1)
|
||||
})
|
||||
} else {
|
||||
runDesktopApp()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { app } from 'electron'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { existsSync, readdirSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { homedir, platform, arch } from 'node:os'
|
||||
|
||||
@@ -23,17 +23,120 @@ export function webuiServerEntry(): string {
|
||||
return join(webuiDir(), 'dist', 'server', 'index.js')
|
||||
}
|
||||
|
||||
// Bundled Python directory.
|
||||
export function runtimePlatformKey(): string {
|
||||
return `${osLabel}-${archLabel}`
|
||||
}
|
||||
|
||||
export function desktopRuntimeDir(): string {
|
||||
const override = process.env.HERMES_DESKTOP_RUNTIME_DIR?.trim()
|
||||
if (override) return resolve(override)
|
||||
return join(webUiHome(), 'desktop-runtime', runtimePlatformKey())
|
||||
}
|
||||
|
||||
function packagedResourceDir(name: string): string {
|
||||
return resolve(process.resourcesPath, name)
|
||||
}
|
||||
|
||||
// dev: packages/desktop/resources/python/<os>-<arch>
|
||||
// prod: <resources>/python
|
||||
// prod: <resources>/python when present, otherwise downloaded runtime cache.
|
||||
export function pythonDir(): string {
|
||||
if (app.isPackaged) return resolve(process.resourcesPath, 'python')
|
||||
return resolve(app.getAppPath(), 'resources', 'python', `${osLabel}-${archLabel}`)
|
||||
if (app.isPackaged) {
|
||||
const packaged = packagedResourceDir('python')
|
||||
return existsSync(packaged) ? packaged : join(desktopRuntimeDir(), 'python')
|
||||
}
|
||||
return resolve(app.getAppPath(), 'resources', 'python', runtimePlatformKey())
|
||||
}
|
||||
|
||||
export function nodeDir(): string {
|
||||
if (app.isPackaged) {
|
||||
const packaged = packagedResourceDir('node')
|
||||
return existsSync(packaged) ? packaged : join(desktopRuntimeDir(), 'node')
|
||||
}
|
||||
return resolve(app.getAppPath(), 'resources', 'node', runtimePlatformKey())
|
||||
}
|
||||
|
||||
export function nodeBinDir(): string {
|
||||
const dir = nodeDir()
|
||||
return isWin ? dir : join(dir, 'bin')
|
||||
}
|
||||
|
||||
export function bundledNode(): string {
|
||||
return isWin ? join(nodeDir(), 'node.exe') : join(nodeBinDir(), 'node')
|
||||
}
|
||||
|
||||
export function gitDir(): string {
|
||||
if (app.isPackaged) {
|
||||
const packaged = packagedResourceDir('git')
|
||||
return existsSync(packaged) ? packaged : join(desktopRuntimeDir(), 'git')
|
||||
}
|
||||
return resolve(app.getAppPath(), 'resources', 'git', runtimePlatformKey())
|
||||
}
|
||||
|
||||
export function gitPathDirs(): string[] {
|
||||
if (!isWin) return []
|
||||
const dir = gitDir()
|
||||
return [
|
||||
join(dir, 'cmd'),
|
||||
join(dir, 'mingw64', 'bin'),
|
||||
join(dir, 'usr', 'bin'),
|
||||
].filter(existsSync)
|
||||
}
|
||||
|
||||
export function bundledGit(): string | undefined {
|
||||
if (!isWin) return undefined
|
||||
const git = join(gitDir(), 'cmd', 'git.exe')
|
||||
return existsSync(git) ? git : undefined
|
||||
}
|
||||
|
||||
export function bundledAgentBrowserHome(): string {
|
||||
return join(pythonDir(), 'agent-browser')
|
||||
}
|
||||
|
||||
function browserExecutableNames(): Set<string> {
|
||||
if (isWin) return new Set(['chrome.exe'])
|
||||
if (platform() === 'darwin') return new Set(['Google Chrome for Testing', 'Google Chrome', 'Chromium', 'chrome'])
|
||||
return new Set(['chrome', 'chromium', 'chromium-browser'])
|
||||
}
|
||||
|
||||
export function bundledBrowserExecutable(): string | undefined {
|
||||
const names = browserExecutableNames()
|
||||
const stack = [join(bundledAgentBrowserHome(), 'browsers'), bundledAgentBrowserHome()].filter(existsSync)
|
||||
const visited = new Set<string>()
|
||||
|
||||
while (stack.length > 0) {
|
||||
const dir = stack.pop()
|
||||
if (!dir || visited.has(dir)) continue
|
||||
visited.add(dir)
|
||||
|
||||
let entries
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const path = join(dir, entry.name)
|
||||
if (entry.isFile() && names.has(entry.name)) return path
|
||||
if (entry.isDirectory()) stack.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function pythonBinDir(): string {
|
||||
const dir = pythonDir()
|
||||
return isWin ? join(dir, 'Scripts') : join(dir, 'bin')
|
||||
}
|
||||
|
||||
export function bundledPython(): string {
|
||||
const dir = pythonDir()
|
||||
return isWin ? join(dir, 'python.exe') : join(dir, 'bin', 'python3')
|
||||
}
|
||||
|
||||
export function hermesBin(): string {
|
||||
const dir = pythonDir()
|
||||
return isWin ? join(dir, 'Scripts', 'hermes.exe') : join(dir, 'bin', 'hermes')
|
||||
return isWin ? join(pythonBinDir(), 'hermes.exe') : join(pythonBinDir(), 'hermes')
|
||||
}
|
||||
|
||||
export function hermesBinExists(): boolean {
|
||||
@@ -63,12 +166,23 @@ export function hermesHome(): string {
|
||||
const override = process.env.HERMES_HOME?.trim()
|
||||
if (override) return resolve(override)
|
||||
|
||||
const defaultHome = resolve(homedir(), '.hermes')
|
||||
|
||||
if (isWin) {
|
||||
const localAppData = process.env.LOCALAPPDATA?.trim() || process.env.APPDATA?.trim()
|
||||
if (localAppData) return resolve(localAppData, 'hermes')
|
||||
const candidates = [
|
||||
process.env.LOCALAPPDATA,
|
||||
process.env.APPDATA,
|
||||
]
|
||||
.map(value => value?.trim())
|
||||
.filter((value): value is string => !!value)
|
||||
.map(value => resolve(value, 'hermes'))
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return resolve(homedir(), '.hermes')
|
||||
return defaultHome
|
||||
}
|
||||
|
||||
export function tokenFile(): string {
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import {
|
||||
createReadStream,
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { get as httpGet } from 'node:http'
|
||||
import { get as httpsGet } from 'node:https'
|
||||
import { basename, dirname, join, relative } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { app } from 'electron'
|
||||
import {
|
||||
bundledGit,
|
||||
bundledNode,
|
||||
desktopRuntimeDir,
|
||||
hermesBinExists,
|
||||
runtimePlatformKey,
|
||||
} from './paths'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
const DEFAULT_RUNTIME_BASE_URL = 'https://download.www.xinmi.cloud'
|
||||
const RUNTIME_MANIFEST_NAME = 'runtime-manifest.json'
|
||||
const PACKAGED_RUNTIME_RELEASE_NAME = 'runtime-release.json'
|
||||
|
||||
type RuntimeManifest = {
|
||||
schema: number
|
||||
platform: string
|
||||
hermesAgentVersion?: string
|
||||
asset?: {
|
||||
name: string
|
||||
url?: string
|
||||
sha256?: string
|
||||
size?: number
|
||||
}
|
||||
}
|
||||
|
||||
type RuntimeDescriptor = {
|
||||
name: string
|
||||
url: string
|
||||
sha256?: string
|
||||
hermesAgentVersion?: string
|
||||
}
|
||||
|
||||
export type RuntimeProgress = {
|
||||
stage: 'resolve' | 'download' | 'verify' | 'extract' | 'ready'
|
||||
message: string
|
||||
percent?: number
|
||||
receivedBytes?: number
|
||||
totalBytes?: number
|
||||
}
|
||||
|
||||
type RuntimeProgressHandler = (progress: RuntimeProgress) => void
|
||||
|
||||
function requiredRuntimeFiles(root: string): string[] {
|
||||
const pythonBin = process.platform === 'win32'
|
||||
? join(root, 'python', 'python.exe')
|
||||
: join(root, 'python', 'bin', 'python3')
|
||||
const hermesBin = process.platform === 'win32'
|
||||
? join(root, 'python', 'Scripts', 'hermes.exe')
|
||||
: join(root, 'python', 'bin', 'hermes')
|
||||
const nodeBin = process.platform === 'win32'
|
||||
? join(root, 'node', 'node.exe')
|
||||
: join(root, 'node', 'bin', 'node')
|
||||
const files = [pythonBin, hermesBin, nodeBin, join(root, RUNTIME_MANIFEST_NAME)]
|
||||
if (process.platform === 'win32') files.push(join(root, 'git', 'cmd', 'git.exe'))
|
||||
return files
|
||||
}
|
||||
|
||||
function missingRuntimeFiles(root: string): string[] {
|
||||
return requiredRuntimeFiles(root).filter(file => !existsSync(file))
|
||||
}
|
||||
|
||||
function runtimeReady(): boolean {
|
||||
const gitReady = process.platform !== 'win32' || !!bundledGit()
|
||||
return hermesBinExists() && existsSync(bundledNode()) && gitReady
|
||||
}
|
||||
|
||||
function releaseTagCandidates(): string[] {
|
||||
const override = process.env.HERMES_DESKTOP_RUNTIME_RELEASE_TAG?.trim()
|
||||
if (override) return [override]
|
||||
|
||||
const version = app.getVersion()
|
||||
const candidates = [packagedRuntimeReleaseTag(), version, `v${version}`, 'latest']
|
||||
return Array.from(new Set(candidates.filter((tag): tag is string => typeof tag === 'string' && tag.length > 0)))
|
||||
}
|
||||
|
||||
function packagedRuntimeReleaseTag(): string | null {
|
||||
const candidates = app.isPackaged
|
||||
? [join(process.resourcesPath, 'build', PACKAGED_RUNTIME_RELEASE_NAME)]
|
||||
: [join(app.getAppPath(), 'build', PACKAGED_RUNTIME_RELEASE_NAME)]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (!existsSync(candidate)) continue
|
||||
try {
|
||||
const metadata = JSON.parse(readFileSync(candidate, 'utf-8')) as { tag?: unknown }
|
||||
if (typeof metadata.tag === 'string' && metadata.tag.trim()) return metadata.tag.trim()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function runtimeAssetUrl(assetName: string, tag: string): string {
|
||||
const repo = process.env.HERMES_DESKTOP_RUNTIME_REPO?.trim()
|
||||
if (repo) {
|
||||
if (tag === 'latest') {
|
||||
return `https://www.xinmi.cloud/${repo}/releases/latest/download/${encodeURIComponent(assetName)}`
|
||||
}
|
||||
return `https://www.xinmi.cloud/${repo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(assetName)}`
|
||||
}
|
||||
|
||||
const template = process.env.HERMES_DESKTOP_RUNTIME_BASE_URL?.trim() || DEFAULT_RUNTIME_BASE_URL
|
||||
if (template.includes('{asset}') || template.includes('{tag}')) {
|
||||
return template
|
||||
.replace(/\{asset\}/g, encodeURIComponent(assetName))
|
||||
.replace(/\{tag\}/g, encodeURIComponent(tag))
|
||||
}
|
||||
return `${template.replace(/\/$/, '')}/${encodeURIComponent(tag)}/${encodeURIComponent(assetName)}`
|
||||
}
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`GET ${url} returned ${response.status}`)
|
||||
}
|
||||
return await response.json() as T
|
||||
}
|
||||
|
||||
async function resolveRuntimeDescriptor(): Promise<RuntimeDescriptor> {
|
||||
const directUrl = process.env.HERMES_DESKTOP_RUNTIME_URL?.trim()
|
||||
if (directUrl) {
|
||||
return { name: basename(new URL(directUrl).pathname) || 'hermes-runtime.tar.gz', url: directUrl }
|
||||
}
|
||||
|
||||
const platformManifestName = `hermes-runtime-${runtimePlatformKey()}.json`
|
||||
const manifestOverride = process.env.HERMES_DESKTOP_RUNTIME_MANIFEST_URL?.trim()
|
||||
const candidates = manifestOverride
|
||||
? [{ tag: '', url: manifestOverride }]
|
||||
: releaseTagCandidates().map(tag => ({ tag, url: runtimeAssetUrl(platformManifestName, tag) }))
|
||||
|
||||
let lastError: Error | null = null
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const manifest = await fetchJson<RuntimeManifest>(candidate.url)
|
||||
if (!manifest.asset?.name) {
|
||||
throw new Error(`runtime manifest is missing asset.name: ${candidate.url}`)
|
||||
}
|
||||
return {
|
||||
name: manifest.asset.name,
|
||||
url: manifest.asset.url || runtimeAssetUrl(manifest.asset.name, candidate.tag),
|
||||
sha256: manifest.asset.sha256,
|
||||
hermesAgentVersion: manifest.hermesAgentVersion,
|
||||
}
|
||||
} catch (err) {
|
||||
lastError = err instanceof Error ? err : new Error(String(err))
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Unable to resolve Hermes desktop runtime package')
|
||||
}
|
||||
|
||||
function readCachedRuntimeManifest(root: string): RuntimeManifest | null {
|
||||
const file = join(root, RUNTIME_MANIFEST_NAME)
|
||||
if (!existsSync(file)) return null
|
||||
try {
|
||||
return JSON.parse(readFileSync(file, 'utf-8')) as RuntimeManifest
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function cachedRuntimeMatches(root: string, descriptor: RuntimeDescriptor): boolean {
|
||||
if (!runtimeReady()) return false
|
||||
const manifest = readCachedRuntimeManifest(root)
|
||||
if (!manifest?.asset?.name) return true
|
||||
return manifest.asset.name === descriptor.name
|
||||
}
|
||||
|
||||
function downloadFile(
|
||||
url: string,
|
||||
target: string,
|
||||
onProgress?: RuntimeProgressHandler,
|
||||
redirects = 5,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const parsed = new URL(url)
|
||||
const getter = parsed.protocol === 'http:' ? httpGet : httpsGet
|
||||
const req = getter(parsed, response => {
|
||||
const status = response.statusCode || 0
|
||||
const location = response.headers.location
|
||||
if (status >= 300 && status < 400 && location && redirects > 0) {
|
||||
response.resume()
|
||||
downloadFile(new URL(location, url).toString(), target, onProgress, redirects - 1).then(resolve, reject)
|
||||
return
|
||||
}
|
||||
if (status < 200 || status >= 300) {
|
||||
response.resume()
|
||||
reject(new Error(`GET ${url} returned ${status}`))
|
||||
return
|
||||
}
|
||||
|
||||
const totalBytes = Number(response.headers['content-length']) || undefined
|
||||
let receivedBytes = 0
|
||||
response.on('data', chunk => {
|
||||
receivedBytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk)
|
||||
onProgress?.({
|
||||
stage: 'download',
|
||||
message: 'Downloading Hermes runtime...',
|
||||
percent: totalBytes ? Math.min(100, (receivedBytes / totalBytes) * 100) : undefined,
|
||||
receivedBytes,
|
||||
totalBytes,
|
||||
})
|
||||
})
|
||||
|
||||
const file = createWriteStream(target)
|
||||
response.pipe(file)
|
||||
file.on('finish', () => file.close(() => resolve()))
|
||||
file.on('error', reject)
|
||||
})
|
||||
req.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function sha256File(file: string): Promise<string> {
|
||||
const hash = createHash('sha256')
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const stream = createReadStream(file)
|
||||
stream.on('data', chunk => hash.update(chunk))
|
||||
stream.on('end', resolve)
|
||||
stream.on('error', reject)
|
||||
})
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
async function extractRuntimeArchive(archive: string, targetRoot: string): Promise<void> {
|
||||
const parent = dirname(targetRoot)
|
||||
const tempRoot = join(parent, `.runtime-${process.pid}-${Date.now()}`)
|
||||
rmSync(tempRoot, { recursive: true, force: true })
|
||||
mkdirSync(tempRoot, { recursive: true })
|
||||
|
||||
try {
|
||||
await execFileAsync(process.platform === 'win32' ? 'tar.exe' : 'tar', ['-xzf', archive, '-C', tempRoot], {
|
||||
windowsHide: true,
|
||||
})
|
||||
const missing = missingRuntimeFiles(tempRoot)
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Runtime archive is missing required files: ${missing.map(file => relative(tempRoot, file)).join(', ')}`)
|
||||
}
|
||||
rmSync(targetRoot, { recursive: true, force: true })
|
||||
mkdirSync(parent, { recursive: true })
|
||||
renameSync(tempRoot, targetRoot)
|
||||
} catch (err) {
|
||||
rmSync(tempRoot, { recursive: true, force: true })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureDesktopRuntime(onProgress?: RuntimeProgressHandler): Promise<void> {
|
||||
const runtimeRoot = desktopRuntimeDir()
|
||||
mkdirSync(runtimeRoot, { recursive: true })
|
||||
|
||||
let descriptor: RuntimeDescriptor
|
||||
try {
|
||||
onProgress?.({ stage: 'resolve', message: 'Checking Hermes runtime...' })
|
||||
descriptor = await resolveRuntimeDescriptor()
|
||||
} catch (err) {
|
||||
if (runtimeReady() && !process.env.HERMES_DESKTOP_RUNTIME_FORCE_UPDATE) {
|
||||
console.warn(`[runtime] using cached Hermes runtime because update check failed: ${err instanceof Error ? err.message : String(err)}`)
|
||||
return
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
if (cachedRuntimeMatches(runtimeRoot, descriptor) && !process.env.HERMES_DESKTOP_RUNTIME_FORCE_UPDATE) return
|
||||
|
||||
const archive = join(dirname(runtimeRoot), `${descriptor.name}.download`)
|
||||
console.log(`[runtime] downloading Hermes runtime ${descriptor.name}`)
|
||||
onProgress?.({ stage: 'download', message: `Downloading ${descriptor.name}...` })
|
||||
let archiveSize = 0
|
||||
try {
|
||||
await downloadFile(descriptor.url, archive, onProgress)
|
||||
archiveSize = statSync(archive).size
|
||||
if (descriptor.sha256) {
|
||||
onProgress?.({ stage: 'verify', message: 'Verifying Hermes runtime...' })
|
||||
const actual = await sha256File(archive)
|
||||
if (actual !== descriptor.sha256) {
|
||||
throw new Error(`Runtime checksum mismatch for ${descriptor.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.({ stage: 'extract', message: 'Extracting Hermes runtime...' })
|
||||
await extractRuntimeArchive(archive, runtimeRoot)
|
||||
} finally {
|
||||
rmSync(archive, { force: true })
|
||||
}
|
||||
|
||||
const manifestPath = join(runtimeRoot, RUNTIME_MANIFEST_NAME)
|
||||
if (!existsSync(manifestPath)) {
|
||||
writeFileSync(manifestPath, JSON.stringify({
|
||||
schema: 1,
|
||||
platform: runtimePlatformKey(),
|
||||
hermesAgentVersion: descriptor.hermesAgentVersion,
|
||||
asset: {
|
||||
name: descriptor.name,
|
||||
sha256: descriptor.sha256,
|
||||
size: archiveSize,
|
||||
},
|
||||
}, null, 2))
|
||||
}
|
||||
onProgress?.({ stage: 'ready', message: 'Hermes runtime ready.' })
|
||||
console.log(`[runtime] Hermes runtime ready at ${runtimeRoot}`)
|
||||
}
|
||||
@@ -6,8 +6,8 @@ let initialized = false
|
||||
let checking = false
|
||||
let updateDownloaded = false
|
||||
|
||||
const LATEST_RELEASE_URL = 'https://api.github.com/repos/EKKOLearnAI/hermes-web-ui/releases/latest'
|
||||
const CLOUDFLARE_DOWNLOAD_BASE_URL = 'https://download.ekkolearnai.com'
|
||||
const LATEST_RELEASE_URL = 'https://api.www.xinmi.cloud/repos/EKKOLearnAI/hermes-web-ui/releases/latest'
|
||||
const CLOUDFLARE_DOWNLOAD_BASE_URL = 'https://download.www.xinmi.cloud'
|
||||
|
||||
interface GitHubRelease {
|
||||
tag_name?: string
|
||||
|
||||
@@ -6,10 +6,25 @@ import { dirname, delimiter, join } from 'node:path'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { promisify } from 'node:util'
|
||||
import { app } from 'electron'
|
||||
import { webuiServerEntry, webuiDir, hermesBin, webUiHome, hermesHome, tokenFile, pythonDir } from './paths'
|
||||
import {
|
||||
bundledBrowserExecutable,
|
||||
bundledGit,
|
||||
bundledNode,
|
||||
gitPathDirs,
|
||||
webuiServerEntry,
|
||||
webuiDir,
|
||||
hermesBin,
|
||||
webUiHome,
|
||||
hermesHome,
|
||||
nodeBinDir,
|
||||
tokenFile,
|
||||
pythonDir,
|
||||
} from './paths'
|
||||
|
||||
const DEFAULT_PORT = 8748
|
||||
const DEFAULT_READY_TIMEOUT_MS = 30_000
|
||||
const DEFAULT_READY_TIMEOUT_MS = 120_000
|
||||
const AGENT_BRIDGE_STARTED_MARKER = '[bootstrap] agent bridge started'
|
||||
const AGENT_BRIDGE_FAILED_MARKER = '[bootstrap] agent bridge failed to start'
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
let serverProc: ChildProcess | null = null
|
||||
@@ -47,6 +62,60 @@ function readyTimeoutMs(): number {
|
||||
return envPositiveInt('HERMES_DESKTOP_READY_TIMEOUT_MS') || DEFAULT_READY_TIMEOUT_MS
|
||||
}
|
||||
|
||||
function createAgentBridgeStartupTracker(): {
|
||||
observe: (chunk: Buffer) => void
|
||||
wait: (timeoutMs: number) => Promise<void>
|
||||
} {
|
||||
let output = ''
|
||||
let state: 'pending' | 'started' | 'failed' = 'pending'
|
||||
let resolveReady: (() => void) | null = null
|
||||
let rejectReady: ((err: Error) => void) | null = null
|
||||
|
||||
const settle = (nextState: 'started' | 'failed') => {
|
||||
if (state !== 'pending') return
|
||||
state = nextState
|
||||
if (nextState === 'started') {
|
||||
resolveReady?.()
|
||||
} else {
|
||||
rejectReady?.(new Error('Agent bridge failed to start'))
|
||||
}
|
||||
}
|
||||
|
||||
const observe = (chunk: Buffer) => {
|
||||
if (state !== 'pending') return
|
||||
output = (output + chunk.toString('utf-8')).slice(-4096)
|
||||
if (output.includes(AGENT_BRIDGE_STARTED_MARKER)) {
|
||||
settle('started')
|
||||
} else if (output.includes(AGENT_BRIDGE_FAILED_MARKER)) {
|
||||
settle('failed')
|
||||
}
|
||||
}
|
||||
|
||||
const wait = (timeoutMs: number) => {
|
||||
if (state === 'started') return Promise.resolve()
|
||||
if (state === 'failed') return Promise.reject(new Error('Agent bridge failed to start'))
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
if (state !== 'pending') return
|
||||
state = 'failed'
|
||||
reject(new Error(`Agent bridge did not become ready within ${timeoutMs}ms`))
|
||||
}, timeoutMs)
|
||||
|
||||
resolveReady = () => {
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
}
|
||||
rejectReady = (err) => {
|
||||
clearTimeout(timer)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { observe, wait }
|
||||
}
|
||||
|
||||
function ensureToken(): string {
|
||||
if (cachedToken) return cachedToken
|
||||
const file = tokenFile()
|
||||
@@ -231,17 +300,28 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
|
||||
const bundledPython = isWin
|
||||
? join(pythonDir(), 'python.exe')
|
||||
: join(pythonDir(), 'bin', 'python3')
|
||||
const bundledAgentBrowserBin = isWin
|
||||
? join(pythonDir(), 'node')
|
||||
: join(pythonDir(), 'node', 'bin')
|
||||
const bundledNodeBin = nodeBinDir()
|
||||
const bundledGitPath = gitPathDirs().join(delimiter)
|
||||
const bridgePort = await getFreeTcpPort()
|
||||
const workerPortBase = await getFreeTcpPortInRange(20000, 59000)
|
||||
const loginShellPath = await getLoginShellPath()
|
||||
const nvmNodeBinPaths = getNvmNodeBinPaths()
|
||||
const runtimePath = mergePathEntries(
|
||||
dirname(hermesBin()),
|
||||
bundledAgentBrowserBin,
|
||||
bundledNodeBin,
|
||||
bundledGitPath,
|
||||
loginShellPath,
|
||||
nvmNodeBinPaths,
|
||||
process.env.PATH,
|
||||
process.env.Path,
|
||||
COMMON_USER_BIN_DIRS.join(delimiter),
|
||||
)
|
||||
const browserExecutable = process.env.AGENT_BROWSER_EXECUTABLE_PATH?.trim() || bundledBrowserExecutable()
|
||||
const gitBin = bundledGit()
|
||||
|
||||
// Run via Electron's "run as Node" mode — Electron binary doubles as Node.
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
@@ -256,11 +336,21 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
|
||||
HERMES_AGENT_BRIDGE_PYTHON: bundledPython,
|
||||
HERMES_AGENT_CLI_PYTHON: bundledPython,
|
||||
HERMES_AGENT_ROOT: pythonDir(),
|
||||
HERMES_AGENT_NODE: bundledNode(),
|
||||
HERMES_AGENT_NODE_ROOT: isWin ? bundledNodeBin : dirname(bundledNodeBin),
|
||||
AGENT_BROWSER_HOME: process.env.AGENT_BROWSER_HOME?.trim() || join(agentHome, 'agent-browser'),
|
||||
...(browserExecutable ? { AGENT_BROWSER_EXECUTABLE_PATH: browserExecutable } : {}),
|
||||
PLAYWRIGHT_BROWSERS_PATH: process.env.PLAYWRIGHT_BROWSERS_PATH || join(pythonDir(), 'ms-playwright'),
|
||||
...(gitBin ? { HERMES_AGENT_GIT: gitBin } : {}),
|
||||
// Force TCP loopback for the agent bridge. The default `ipc:///tmp/...`
|
||||
// unix socket is rejected on macOS in some EDR/sandbox setups (silent
|
||||
// SIGKILL of the bridge child within ~150ms). TCP on 127.0.0.1 works
|
||||
// identically and avoids the issue cross-platform.
|
||||
HERMES_AGENT_BRIDGE_ENDPOINT: `tcp://127.0.0.1:${bridgePort}`,
|
||||
// Desktop opens the UI as soon as the Web UI HTTP server is ready, while
|
||||
// the Python bridge starts in the background. Let the first chat/context
|
||||
// request wait for broker readiness instead of failing during cold start.
|
||||
HERMES_AGENT_BRIDGE_CONNECT_RETRY_MS: process.env.HERMES_AGENT_BRIDGE_CONNECT_RETRY_MS ?? '120000',
|
||||
// Force TCP for worker endpoints too (upstream #1106). Same EDR/sandbox
|
||||
// reason as above — default ipc:// unix sockets in /tmp get killed.
|
||||
HERMES_AGENT_BRIDGE_WORKER_TRANSPORT: 'tcp',
|
||||
@@ -278,8 +368,9 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
|
||||
// HERMES_HOME/.env or by configuring per-platform allowlists.
|
||||
GATEWAY_ALLOW_ALL_USERS: process.env.GATEWAY_ALLOW_ALL_USERS ?? 'true',
|
||||
// Keep the bundled Hermes Agent, bridge, gateway, and Web UI path helpers
|
||||
// on the same data directory. Native Windows uses %LOCALAPPDATA%\hermes;
|
||||
// macOS/Linux keep the standard ~/.hermes layout.
|
||||
// on the same data directory. Native Windows uses an existing
|
||||
// %LOCALAPPDATA%\hermes or %APPDATA%\hermes; otherwise all platforms keep
|
||||
// the standard ~/.hermes layout.
|
||||
HERMES_HOME: agentHome,
|
||||
HERMES_WEB_UI_HOME: home,
|
||||
HERMES_WEBUI_STATE_DIR: home,
|
||||
@@ -295,10 +386,14 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
|
||||
windowsHide: true,
|
||||
})
|
||||
|
||||
const bridgeStartup = createAgentBridgeStartupTracker()
|
||||
|
||||
serverProc.stdout?.on('data', (chunk: Buffer) => {
|
||||
bridgeStartup.observe(chunk)
|
||||
process.stdout.write(`[webui] ${chunk}`)
|
||||
})
|
||||
serverProc.stderr?.on('data', (chunk: Buffer) => {
|
||||
bridgeStartup.observe(chunk)
|
||||
process.stderr.write(`[webui] ${chunk}`)
|
||||
})
|
||||
serverProc.on('exit', (code, signal) => {
|
||||
@@ -309,7 +404,11 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
|
||||
}
|
||||
})
|
||||
|
||||
await waitForReady(port, readyTimeoutMs())
|
||||
const timeoutMs = readyTimeoutMs()
|
||||
void bridgeStartup.wait(timeoutMs).catch(err => {
|
||||
console.warn(`[webui] agent bridge was not ready during startup: ${err instanceof Error ? err.message : String(err)}`)
|
||||
})
|
||||
await waitForReady(port, timeoutMs)
|
||||
return getServerUrl(port)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
contextBridge.exposeInMainWorld('hermesDesktop', {
|
||||
getToken: (): Promise<string> => ipcRenderer.invoke('hermes-desktop:get-token'),
|
||||
retryBootstrap: (): Promise<void> => ipcRenderer.invoke('hermes-desktop:retry-bootstrap'),
|
||||
platform: process.platform,
|
||||
isDesktop: true,
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ function appendPinoContext(message: string, obj: any): string {
|
||||
parts.push(`profile=${obj.profile}`)
|
||||
}
|
||||
if (obj.request?.action) parts.push(`action=${obj.request.action}`)
|
||||
if (obj.err?.message) parts.push(`error=${obj.err.message}`)
|
||||
if (obj.sessionId) parts.push(`session=${obj.sessionId}`)
|
||||
if (obj.runId) parts.push(`run=${obj.runId}`)
|
||||
if (obj.status) parts.push(`status=${obj.status}`)
|
||||
|
||||
@@ -107,7 +107,7 @@ function normalizeGithubRepoUrl(raw: string): string {
|
||||
return raw
|
||||
.trim()
|
||||
.replace(/^git\+/, '')
|
||||
.replace(/^git@github\.com:/, 'https://github.com/')
|
||||
.replace(/^git@github\.com:/, 'https://www.xinmi.cloud/')
|
||||
.replace(/\.git$/, '')
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ function getPreviewRepoApiUrl(): string {
|
||||
const baseUrl = getPreviewRepoBaseUrl()
|
||||
const match = baseUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/)
|
||||
if (!match) throw new Error(`Preview zip fallback only supports GitHub repositories: ${baseUrl}`)
|
||||
return `https://api.github.com/repos/${match[1]}/${match[2]}`
|
||||
return `https://api.www.xinmi.cloud/repos/${match[1]}/${match[2]}`
|
||||
}
|
||||
|
||||
function getPreviewGithubRepoParts(): { owner: string; repo: string } {
|
||||
@@ -902,7 +902,7 @@ async function downloadGithubZip(ref: string, targetDir: string, type: 'tag' | '
|
||||
const { owner, repo } = getPreviewGithubRepoParts()
|
||||
const refKind = type === 'branch' ? 'heads' : 'tags'
|
||||
const archiveKind = process.platform === 'win32' ? 'zip' : 'tar.gz'
|
||||
const url = `https://codeload.github.com/${owner}/${repo}/${archiveKind}/refs/${refKind}/${encodeURIComponent(ref)}`
|
||||
const url = `https://codeload.www.xinmi.cloud/${owner}/${repo}/${archiveKind}/refs/${refKind}/${encodeURIComponent(ref)}`
|
||||
appendPreviewActionLog(`download archive: ${url}`)
|
||||
const res = await fetch(url, {
|
||||
headers: { 'User-Agent': 'hermes-web-ui-preview' },
|
||||
|
||||
@@ -10,7 +10,7 @@ const DEFAULT_AGENT_BRIDGE_STARTUP_TIMEOUT_MS = 120000
|
||||
const DEFAULT_AGENT_BRIDGE_RESTART_DELAY_MS = 1000
|
||||
const MAX_AGENT_BRIDGE_RESTART_DELAY_MS = 30000
|
||||
const OPENROUTER_WEB_UI_ATTRIBUTION_ENV = {
|
||||
HERMES_OPENROUTER_APP_REFERER: 'https://ekkolearnai.com',
|
||||
HERMES_OPENROUTER_APP_REFERER: 'https://www.xinmi.cloud',
|
||||
HERMES_OPENROUTER_APP_TITLE: 'Hermes Web UI',
|
||||
HERMES_OPENROUTER_APP_CATEGORIES: 'cli-agent,personal-agent',
|
||||
} as const
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
*
|
||||
* Mirrors the upstream hermes-agent implementation
|
||||
* (`hermes_cli/copilot_auth.py:155-275`):
|
||||
* - POST https://github.com/login/device/code → device_code, user_code, verification_uri
|
||||
* - POST https://github.com/login/oauth/access_token → access_token (after user approves)
|
||||
* - POST https://www.xinmi.cloud/login/device/code → device_code, user_code, verification_uri
|
||||
* - POST https://www.xinmi.cloud/login/oauth/access_token → access_token (after user approves)
|
||||
* - Polling rules per RFC 8628: authorization_pending, slow_down, expired_token, access_denied
|
||||
*
|
||||
* Client ID `Ov23li8tweQw6odWQebz` is reused from upstream hermes-agent for now;
|
||||
* a dedicated web-ui OAuth App can be registered later without changing the protocol.
|
||||
*/
|
||||
|
||||
const GITHUB_DEVICE_CODE_URL = 'https://github.com/login/device/code'
|
||||
const GITHUB_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'
|
||||
const GITHUB_DEVICE_CODE_URL = 'https://www.xinmi.cloud/login/device/code'
|
||||
const GITHUB_ACCESS_TOKEN_URL = 'https://www.xinmi.cloud/login/oauth/access_token'
|
||||
export const COPILOT_OAUTH_CLIENT_ID = 'Ov23li8tweQw6odWQebz'
|
||||
export const COPILOT_OAUTH_SCOPE = 'read:user'
|
||||
const FETCH_TIMEOUT_MS = 15_000
|
||||
|
||||
@@ -6,7 +6,7 @@ import { join } from 'path'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
const COPILOT_API_TOKEN_URL = 'https://api.github.com/copilot_internal/v2/token'
|
||||
const COPILOT_API_TOKEN_URL = 'https://api.www.xinmi.cloud/copilot_internal/v2/token'
|
||||
const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models'
|
||||
const EDITOR_VERSION = 'vscode/1.104.1'
|
||||
const PLUGIN_VERSION = 'copilot-chat/0.20.0'
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
* Hermes 路径检测工具 - 跨平台兼容
|
||||
*
|
||||
* Hermes 数据目录在不同平台上的位置:
|
||||
* - Windows 原生安装: %LOCALAPPDATA%\hermes
|
||||
* - Windows 原生安装: %LOCALAPPDATA%\hermes when it exists
|
||||
* - Linux/macOS/WSL2: ~/.hermes
|
||||
* - 用户自定义: HERMES_HOME 环境变量
|
||||
*/
|
||||
|
||||
import { existsSync } from 'fs'
|
||||
import { basename, dirname, isAbsolute, relative, resolve, join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
|
||||
@@ -15,7 +16,7 @@ import { homedir } from 'os'
|
||||
*
|
||||
* 检测优先级:
|
||||
* 1. HERMES_HOME 环境变量(用户自定义)
|
||||
* 2. Windows: %LOCALAPPDATA%\hermes(原生安装)
|
||||
* 2. Windows: existing %LOCALAPPDATA%\hermes or %APPDATA%\hermes
|
||||
* 3. 默认: ~/.hermes(Linux/macOS/WSL2)
|
||||
*
|
||||
* @returns Hermes 数据目录的绝对路径
|
||||
@@ -26,16 +27,25 @@ export function detectHermesHome(): string {
|
||||
return resolve(process.env.HERMES_HOME)
|
||||
}
|
||||
|
||||
// 2. Windows:直接使用 %LOCALAPPDATA%\hermes
|
||||
const defaultHome = resolve(homedir(), '.hermes')
|
||||
|
||||
// 2. Windows:优先使用存在的原生安装数据目录;不存在时回退到 ~/.hermes。
|
||||
if (process.platform === 'win32') {
|
||||
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA
|
||||
if (localAppData) {
|
||||
return join(localAppData, 'hermes')
|
||||
const candidates = [
|
||||
process.env.LOCALAPPDATA,
|
||||
process.env.APPDATA,
|
||||
]
|
||||
.map(value => value?.trim())
|
||||
.filter((value): value is string => !!value)
|
||||
.map(value => resolve(value, 'hermes'))
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) return candidate
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Linux/macOS:~/.hermes
|
||||
return resolve(homedir(), '.hermes')
|
||||
return defaultHome
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,7 +137,7 @@ onMounted(() => {
|
||||
</button>
|
||||
<a
|
||||
class="btn-outline"
|
||||
href="https://github.com/EKKOLearnAI/hermes-web-ui"
|
||||
href="https://www.xinmi.cloud/root/Hermes-ui"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
|
||||
@@ -15,10 +15,10 @@ const activeTab = ref<'desktop' | 'npm' | 'docker' | 'source'>('desktop')
|
||||
|
||||
const releaseVersion = __APP_VERSION__.replace(/^v/, '')
|
||||
const releaseTag = `v${releaseVersion}`
|
||||
const releaseBaseUrl = 'https://github.com/EKKOLearnAI/hermes-web-ui/releases'
|
||||
const releaseBaseUrl = 'https://www.xinmi.cloud/root/Hermes-ui/releases'
|
||||
const releaseUrl = `${releaseBaseUrl}/tag/${releaseTag}`
|
||||
const githubDownloadUrl = `${releaseBaseUrl}/download/${releaseTag}`
|
||||
const cloudflareDownloadUrl = `https://download.ekkolearnai.com/${releaseTag}`
|
||||
const cloudflareDownloadUrl = `https://download.www.xinmi.cloud/${releaseTag}`
|
||||
const desktopDownloads = computed(() =>
|
||||
(tm('install.desktop.downloads') as DesktopDownload[]).map((item) => {
|
||||
const assetName = `Hermes.Studio-${releaseVersion}-${item.assetSuffix}`
|
||||
|
||||
@@ -17,7 +17,7 @@ const chartSrc = computed(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch('https://api.github.com/repos/EKKOLearnAI/hermes-web-ui')
|
||||
const res = await fetch('https://api.www.xinmi.cloud/repos/EKKOLearnAI/hermes-web-ui')
|
||||
const data = await res.json()
|
||||
stars.value = data.stargazers_count
|
||||
} catch {}
|
||||
@@ -32,7 +32,7 @@ onMounted(async () => {
|
||||
<div class="star-badges reveal reveal-delay-1">
|
||||
<a
|
||||
class="star-btn"
|
||||
href="https://github.com/EKKOLearnAI/hermes-web-ui"
|
||||
href="https://www.xinmi.cloud/root/Hermes-ui"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
|
||||
@@ -19,7 +19,7 @@ const { t } = useI18n()
|
||||
<p class="footer-meta">{{ t('footer.license') }}</p>
|
||||
<a
|
||||
class="footer-github"
|
||||
href="https://github.com/EKKOLearnAI/hermes-web-ui"
|
||||
href="https://www.xinmi.cloud/root/Hermes-ui"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
|
||||
@@ -39,7 +39,7 @@ function goHome() {
|
||||
<a class="nav-link" @click.prevent="navigateTo('docs.getting-started')">{{ t('nav.docs') }}</a>
|
||||
<a
|
||||
class="nav-link"
|
||||
href="https://github.com/EKKOLearnAI/hermes-web-ui"
|
||||
href="https://www.xinmi.cloud/root/Hermes-ui"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
@@ -84,7 +84,7 @@ function goHome() {
|
||||
<div class="mobile-menu-inner" @click.stop>
|
||||
<a class="mobile-link" @click.prevent="navigateTo('landing')">{{ t('nav.home') }}</a>
|
||||
<a class="mobile-link" @click.prevent="navigateTo('docs.getting-started')">{{ t('nav.docs') }}</a>
|
||||
<a class="mobile-link" href="https://github.com/EKKOLearnAI/hermes-web-ui" target="_blank" rel="noopener">{{ t('nav.github') }}</a>
|
||||
<a class="mobile-link" href="https://www.xinmi.cloud/root/Hermes-ui" target="_blank" rel="noopener">{{ t('nav.github') }}</a>
|
||||
<div class="mobile-actions">
|
||||
<button class="mobile-action-btn" @click="switchLocale">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="action-icon">
|
||||
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
},
|
||||
source: {
|
||||
title: 'From Source',
|
||||
cmd1: 'git clone https://github.com/EKKOLearnAI/hermes-web-ui.git',
|
||||
cmd1: 'git clone https://www.xinmi.cloud/root/Hermes-ui.git',
|
||||
cmd2: 'cd hermes-web-ui && npm install && npm run dev',
|
||||
},
|
||||
prereq: 'Requires Node.js >= 23',
|
||||
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
},
|
||||
source: {
|
||||
title: '源码安装',
|
||||
cmd1: 'git clone https://github.com/EKKOLearnAI/hermes-web-ui.git',
|
||||
cmd1: 'git clone https://www.xinmi.cloud/root/Hermes-ui.git',
|
||||
cmd2: 'cd hermes-web-ui && npm install && npm run dev',
|
||||
},
|
||||
prereq: '需要 Node.js >= 23',
|
||||
|
||||
@@ -117,7 +117,14 @@ if (!buildWorkflow.includes('npm run harness:check')) {
|
||||
}
|
||||
|
||||
const desktopReleaseWorkflow = await readText('.github/workflows/desktop-release.yml')
|
||||
const desktopRuntimeWorkflow = await readText('.github/workflows/desktop-runtime.yml')
|
||||
const electronBuilderConfig = await readText('packages/desktop/electron-builder.yml')
|
||||
const desktopPackageJson = await readText('packages/desktop/package.json')
|
||||
const desktopInstallHermes = await readText('packages/desktop/scripts/install-hermes.mjs')
|
||||
const desktopWebuiServer = await readText('packages/desktop/src/main/webui-server.ts')
|
||||
const desktopRuntimeManager = await readText('packages/desktop/src/main/runtime-manager.ts')
|
||||
const desktopPaths = await readText('packages/desktop/src/main/paths.ts')
|
||||
const desktopRuntimeAssetName = await readText('packages/desktop/scripts/runtime-asset-name.mjs')
|
||||
if (!desktopReleaseWorkflow.includes('files: ${{ matrix.artifact_files }}')) {
|
||||
fail('desktop-release.yml must upload matrix-specific artifact_files')
|
||||
}
|
||||
@@ -142,6 +149,82 @@ if (!desktopReleaseWorkflow.includes('fail_on_unmatched_files: true')) {
|
||||
fail('desktop-release.yml must keep fail_on_unmatched_files: true')
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'resources/python/${os}-${arch}',
|
||||
'resources/node/${os}-${arch}',
|
||||
'resources/git/${os}-${arch}',
|
||||
]) {
|
||||
if (electronBuilderConfig.includes(phrase)) {
|
||||
fail(`electron-builder.yml must not bundle desktop runtime resource: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'"fetch:node"',
|
||||
'"fetch:git"',
|
||||
'"prepare:runtime"',
|
||||
'"package:runtime"',
|
||||
'"runtime:asset-name"',
|
||||
]) {
|
||||
if (!desktopPackageJson.includes(phrase)) {
|
||||
fail(`packages/desktop/package.json must support runtime package publishing: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'steps.check.outputs.missing',
|
||||
'npm --prefix packages/desktop run prepare:runtime',
|
||||
'npm --prefix packages/desktop run package:runtime',
|
||||
]) {
|
||||
if (!desktopRuntimeWorkflow.includes(phrase)) {
|
||||
fail(`desktop-runtime.yml must build and publish missing runtime package assets: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!desktopRuntimeAssetName.includes('hermes-runtime-hermes-agent-')) {
|
||||
fail('runtime asset naming must include hermes-agent version')
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'websockets',
|
||||
'agent-browser@^0.26.0',
|
||||
'AGENT_BROWSER_HOME',
|
||||
'AGENT_BROWSER_EXECUTABLE_PATH',
|
||||
'PLAYWRIGHT_BROWSERS_PATH',
|
||||
'ms-playwright',
|
||||
'removeBrokenDashboardAuthPlugin',
|
||||
]) {
|
||||
if (!desktopInstallHermes.includes(phrase)) {
|
||||
fail(`install-hermes.mjs must bundle Hermes browser runtime support: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'bundledNodeBin',
|
||||
'HERMES_AGENT_NODE',
|
||||
'HERMES_AGENT_GIT',
|
||||
'PLAYWRIGHT_BROWSERS_PATH',
|
||||
'ms-playwright',
|
||||
]) {
|
||||
if (!desktopWebuiServer.includes(phrase)) {
|
||||
fail(`desktop webui server must expose bundled browser runtime: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const phrase of [
|
||||
'HERMES_DESKTOP_RUNTIME_URL',
|
||||
'HERMES_DESKTOP_RUNTIME_BASE_URL',
|
||||
'runtime-manifest.json',
|
||||
]) {
|
||||
if (!desktopRuntimeManager.includes(phrase)) {
|
||||
fail(`desktop runtime manager must support downloadable runtime packages: ${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!desktopPaths.includes('HERMES_DESKTOP_RUNTIME_DIR')) {
|
||||
fail('desktop paths must allow HERMES_DESKTOP_RUNTIME_DIR override')
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('Harness check failed:')
|
||||
for (const failure of failures) {
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
|
||||
mockApi.startCopilotLogin.mockResolvedValue({
|
||||
session_id: 'sess-1',
|
||||
user_code: 'ABCD-1234',
|
||||
verification_url: 'https://github.com/login/device',
|
||||
verification_url: 'https://www.xinmi.cloud/login/device',
|
||||
expires_in: 900,
|
||||
interval: 5,
|
||||
})
|
||||
@@ -62,7 +62,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
|
||||
mockApi.startCopilotLogin.mockResolvedValue({
|
||||
session_id: 'sess-2',
|
||||
user_code: 'WXYZ-9999',
|
||||
verification_url: 'https://github.com/login/device',
|
||||
verification_url: 'https://www.xinmi.cloud/login/device',
|
||||
expires_in: 900,
|
||||
interval: 5,
|
||||
})
|
||||
@@ -87,7 +87,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
|
||||
mockApi.startCopilotLogin.mockResolvedValue({
|
||||
session_id: 'sess-3',
|
||||
user_code: 'EXPI-RED!',
|
||||
verification_url: 'https://github.com/login/device',
|
||||
verification_url: 'https://www.xinmi.cloud/login/device',
|
||||
expires_in: 900,
|
||||
interval: 5,
|
||||
})
|
||||
@@ -116,7 +116,7 @@ describe('CopilotLoginModal device-flow state machine', () => {
|
||||
mockApi.startCopilotLogin.mockResolvedValue({
|
||||
session_id: 'sess-4',
|
||||
user_code: 'NOPE',
|
||||
verification_url: 'https://github.com/login/device',
|
||||
verification_url: 'https://www.xinmi.cloud/login/device',
|
||||
expires_in: 900,
|
||||
interval: 5,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
createShimContent,
|
||||
installHermesStudioCliShim,
|
||||
pathContainsDir,
|
||||
shimPathForPlatform,
|
||||
} from '../../packages/desktop/src/main/cli-shim'
|
||||
|
||||
let tempDirs: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
tempDirs = []
|
||||
})
|
||||
|
||||
function tempHome(): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'hermes-studio-shim-'))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
describe('Hermes Studio CLI shim', () => {
|
||||
it('quotes Unix app paths and forwards args through --hermes-cli', () => {
|
||||
const content = createShimContent("/Applications/Hermes Studio's.app/Contents/MacOS/Hermes Studio", 'darwin')
|
||||
|
||||
expect(content).toContain("--hermes-cli")
|
||||
expect(content).toContain("APP='/Applications/Hermes Studio'\\''s.app/Contents/MacOS/Hermes Studio'")
|
||||
expect(content).toContain('unset ELECTRON_RUN_AS_NODE')
|
||||
expect(content).toContain('exec "$APP" -- --hermes-cli "$@"')
|
||||
})
|
||||
|
||||
it('clears Electron Node mode in Windows shims before launching the app', () => {
|
||||
const content = createShimContent('C:\\Users\\Example\\AppData\\Local\\Programs\\Hermes Studio\\Hermes Studio.exe', 'win32')
|
||||
|
||||
expect(content).toContain('set ELECTRON_RUN_AS_NODE=')
|
||||
expect(content).toContain('"%APP%" -- --hermes-cli %*')
|
||||
})
|
||||
|
||||
it('detects user bin paths with platform-specific separators', () => {
|
||||
expect(pathContainsDir('/usr/bin:/Users/example/bin', '/Users/example/bin', 'darwin')).toBe(true)
|
||||
expect(pathContainsDir('C:\\Windows;C:\\Users\\Example\\bin', 'C:\\Users\\Example\\bin', 'win32')).toBe(true)
|
||||
})
|
||||
|
||||
it('installs a managed Unix shim and adds ~/bin to a shell profile', async () => {
|
||||
const homeDir = tempHome()
|
||||
const result = await installHermesStudioCliShim({
|
||||
homeDir,
|
||||
platform: 'darwin',
|
||||
executablePath: '/Applications/Hermes Studio.app/Contents/MacOS/Hermes Studio',
|
||||
env: { PATH: '/usr/bin', SHELL: '/bin/zsh' },
|
||||
})
|
||||
|
||||
expect(result.status).toBe('installed')
|
||||
expect(result.pathUpdated).toBe(true)
|
||||
expect(result.shimPath).toBe(shimPathForPlatform(join(homeDir, 'bin'), 'darwin'))
|
||||
expect(readFileSync(result.shimPath, 'utf-8')).toContain('exec "$APP" -- --hermes-cli "$@"')
|
||||
expect(readFileSync(join(homeDir, '.zprofile'), 'utf-8')).toContain('export PATH="$HOME/bin:$PATH"')
|
||||
})
|
||||
})
|
||||
@@ -97,7 +97,7 @@ describe('agent bridge manager command resolution', () => {
|
||||
const { buildAgentBridgeProcessEnv } = await import('../../packages/server/src/services/hermes/agent-bridge/manager')
|
||||
const env = buildAgentBridgeProcessEnv('ipc:///tmp/test.sock', '/tmp/hermes-home', '/tmp/hermes-agent')
|
||||
|
||||
expect(env.HERMES_OPENROUTER_APP_REFERER).toBe('https://ekkolearnai.com')
|
||||
expect(env.HERMES_OPENROUTER_APP_REFERER).toBe('https://www.xinmi.cloud')
|
||||
expect(env.HERMES_OPENROUTER_APP_TITLE).toBe('Hermes Web UI')
|
||||
expect(env.HERMES_OPENROUTER_APP_CATEGORIES).toBe('cli-agent,personal-agent')
|
||||
})
|
||||
@@ -122,6 +122,15 @@ describe('agent bridge manager command resolution', () => {
|
||||
expect(DEFAULT_AGENT_BRIDGE_ENDPOINT).not.toBe('ipc:///tmp/hermes-agent-bridge.sock')
|
||||
})
|
||||
|
||||
it('honors the bridge connect retry environment override', async () => {
|
||||
process.env.HERMES_AGENT_BRIDGE_CONNECT_RETRY_MS = '120000'
|
||||
|
||||
const { AgentBridgeClient } = await import('../../packages/server/src/services/hermes/agent-bridge/client')
|
||||
const client = new AgentBridgeClient({ endpoint: 'tcp://127.0.0.1:1' })
|
||||
|
||||
expect(client.connectRetryMs).toBe(120000)
|
||||
})
|
||||
|
||||
it('waits briefly for a restarting bridge socket before failing', async () => {
|
||||
const endpoint = process.platform === 'win32'
|
||||
? `tcp://127.0.0.1:${32000 + (process.pid % 10000)}`
|
||||
|
||||
@@ -22,19 +22,19 @@ describe('startDeviceFlow', () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({
|
||||
device_code: 'DC-1',
|
||||
user_code: 'USER-1234',
|
||||
verification_uri: 'https://github.com/login/device',
|
||||
verification_uri: 'https://www.xinmi.cloud/login/device',
|
||||
expires_in: 900,
|
||||
interval: 5,
|
||||
}))
|
||||
const data = await startDeviceFlow(fetchSpy as any)
|
||||
expect(data.device_code).toBe('DC-1')
|
||||
expect(data.user_code).toBe('USER-1234')
|
||||
expect(data.verification_uri).toBe('https://github.com/login/device')
|
||||
expect(data.verification_uri).toBe('https://www.xinmi.cloud/login/device')
|
||||
expect(data.expires_in).toBe(900)
|
||||
expect(data.interval).toBe(5)
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0]
|
||||
expect(url).toBe('https://github.com/login/device/code')
|
||||
expect(url).toBe('https://www.xinmi.cloud/login/device/code')
|
||||
expect(init.method).toBe('POST')
|
||||
const body = String(init.body)
|
||||
expect(body).toContain(`client_id=${encodeURIComponent(COPILOT_OAUTH_CLIENT_ID)}`)
|
||||
@@ -57,7 +57,7 @@ describe('startDeviceFlow', () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({
|
||||
device_code: 'DC-2',
|
||||
user_code: 'AAAA',
|
||||
verification_uri: 'https://github.com/login/device',
|
||||
verification_uri: 'https://www.xinmi.cloud/login/device',
|
||||
}))
|
||||
const data = await startDeviceFlow(fetchSpy as any)
|
||||
expect(data.expires_in).toBe(900)
|
||||
@@ -130,7 +130,7 @@ describe('pollDeviceFlow', () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ access_token: 'gho_x' }))
|
||||
await pollDeviceFlow('DEVICE-CODE-XYZ', fetchSpy as any)
|
||||
const [url, init] = fetchSpy.mock.calls[0]
|
||||
expect(url).toBe('https://github.com/login/oauth/access_token')
|
||||
expect(url).toBe('https://www.xinmi.cloud/login/oauth/access_token')
|
||||
const body = String(init.body)
|
||||
expect(body).toContain(`client_id=${encodeURIComponent(COPILOT_OAUTH_CLIENT_ID)}`)
|
||||
expect(body).toContain('device_code=DEVICE-CODE-XYZ')
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('resolveCopilotOAuthToken', () => {
|
||||
mockReadFile.mockImplementation(async (p: string) => {
|
||||
if (p.includes('apps.json')) {
|
||||
return JSON.stringify({
|
||||
'github.com:abc': { oauth_token: 'gho_from_apps_json', user: 'me' },
|
||||
'www.xinmi.cloud:abc': { oauth_token: 'gho_from_apps_json', user: 'me' },
|
||||
})
|
||||
}
|
||||
throw new Error('ENOENT')
|
||||
@@ -93,7 +93,7 @@ describe('resolveCopilotOAuthToken', () => {
|
||||
it('apps.json 中的 ghp_ token 也应跳过', async () => {
|
||||
mockReadFile.mockImplementation(async (p: string) => {
|
||||
if (p.includes('apps.json')) {
|
||||
return JSON.stringify({ 'github.com:a': { oauth_token: 'ghp_pat_in_apps' } })
|
||||
return JSON.stringify({ 'www.xinmi.cloud:a': { oauth_token: 'ghp_pat_in_apps' } })
|
||||
}
|
||||
throw new Error('ENOENT')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync } from 'fs'
|
||||
import { homedir, tmpdir } from 'os'
|
||||
import { join, resolve } from 'path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { detectHermesHome } from '../../packages/server/src/services/hermes/hermes-path'
|
||||
|
||||
describe('Hermes path detection', () => {
|
||||
const originalEnv = { ...process.env }
|
||||
const originalPlatform = process.platform
|
||||
let tempDir = ''
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'hermes-path-'))
|
||||
process.env = { ...originalEnv }
|
||||
delete process.env.HERMES_HOME
|
||||
delete process.env.LOCALAPPDATA
|
||||
delete process.env.APPDATA
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
||||
process.env = { ...originalEnv }
|
||||
if (tempDir) rmSync(tempDir, { recursive: true, force: true })
|
||||
tempDir = ''
|
||||
})
|
||||
|
||||
it('keeps explicit HERMES_HOME even when the path does not exist', () => {
|
||||
process.env.HERMES_HOME = join(tempDir, 'custom-home')
|
||||
|
||||
expect(detectHermesHome()).toBe(resolve(tempDir, 'custom-home'))
|
||||
})
|
||||
|
||||
it('falls back to ~/.hermes on Windows when LOCALAPPDATA hermes is missing', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' })
|
||||
process.env.LOCALAPPDATA = join(tempDir, 'Local')
|
||||
|
||||
expect(detectHermesHome()).toBe(resolve(homedir(), '.hermes'))
|
||||
})
|
||||
|
||||
it('uses existing Windows LOCALAPPDATA hermes before APPDATA', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' })
|
||||
const localHermes = join(tempDir, 'Local', 'hermes')
|
||||
const roamingHermes = join(tempDir, 'Roaming', 'hermes')
|
||||
mkdirSync(localHermes, { recursive: true })
|
||||
mkdirSync(roamingHermes, { recursive: true })
|
||||
process.env.LOCALAPPDATA = join(tempDir, 'Local')
|
||||
process.env.APPDATA = join(tempDir, 'Roaming')
|
||||
|
||||
expect(detectHermesHome()).toBe(resolve(localHermes))
|
||||
})
|
||||
|
||||
it('falls back to existing Windows APPDATA hermes when LOCALAPPDATA hermes is missing', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' })
|
||||
const roamingHermes = join(tempDir, 'Roaming', 'hermes')
|
||||
mkdirSync(roamingHermes, { recursive: true })
|
||||
process.env.LOCALAPPDATA = join(tempDir, 'Local')
|
||||
process.env.APPDATA = join(tempDir, 'Roaming')
|
||||
|
||||
expect(detectHermesHome()).toBe(resolve(roamingHermes))
|
||||
})
|
||||
})
|
||||
@@ -20,7 +20,7 @@ async function loadUpdateController(overrides: Partial<UpdateControllerMocks> =
|
||||
const readFileSync = overrides.readFileSync ?? vi.fn(() => JSON.stringify({
|
||||
name: 'hermes-web-ui',
|
||||
version: '0.0.0',
|
||||
repository: { url: 'https://github.com/EKKOLearnAI/hermes-web-ui.git' },
|
||||
repository: { url: 'https://www.xinmi.cloud/root/Hermes-ui.git' },
|
||||
}))
|
||||
const appendFileSync = overrides.appendFileSync ?? vi.fn()
|
||||
|
||||
@@ -210,7 +210,7 @@ describe('update controller', () => {
|
||||
})
|
||||
|
||||
it('loads preview tags through async git with a short timeout', async () => {
|
||||
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://github.com/EKKOLearnAI/hermes-web-ui'
|
||||
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://www.xinmi.cloud/root/Hermes-ui'
|
||||
const execFile = vi.fn((_command: string, _args: string[], _options: any, callback: any) => {
|
||||
callback(null, [
|
||||
'abc123\trefs/tags/v0.6.6',
|
||||
@@ -233,14 +233,14 @@ describe('update controller', () => {
|
||||
})
|
||||
expect(mocks.execFile).toHaveBeenCalledWith(
|
||||
'git',
|
||||
['ls-remote', '--tags', '--refs', 'https://github.com/EKKOLearnAI/hermes-web-ui.git'],
|
||||
['ls-remote', '--tags', '--refs', 'https://www.xinmi.cloud/root/Hermes-ui.git'],
|
||||
expect.objectContaining({ timeout: 8000 }),
|
||||
expect.any(Function),
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to GitHub API when async git tag loading fails', async () => {
|
||||
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://github.com/EKKOLearnAI/hermes-web-ui'
|
||||
process.env.HERMES_WEB_UI_PREVIEW_REPO = 'https://www.xinmi.cloud/root/Hermes-ui'
|
||||
const execFile = vi.fn((_command: string, _args: string[], _options: any, callback: any) => {
|
||||
callback(new Error('git timeout'), '', '')
|
||||
})
|
||||
@@ -267,7 +267,7 @@ describe('update controller', () => {
|
||||
],
|
||||
})
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://api.github.com/repos/EKKOLearnAI/hermes-web-ui/tags?per_page=100',
|
||||
'https://api.www.xinmi.cloud/repos/EKKOLearnAI/hermes-web-ui/tags?per_page=100',
|
||||
expect.objectContaining({
|
||||
headers: { 'User-Agent': 'hermes-web-ui-preview' },
|
||||
signal: expect.any(AbortSignal),
|
||||
|
||||
Reference in New Issue
Block a user