|
@@ -10,14 +10,14 @@ const bundleAvailable=ref(false),fixedDesktops=ref(false)
|
|
|
const runners = ref<Runner[]>([]), sources = ref<Array<{id:string;name:string}>>([])
|
|
const runners = ref<Runner[]>([]), sources = ref<Array<{id:string;name:string}>>([])
|
|
|
const busy = ref(false), error = ref(''), notice = ref('')
|
|
const busy = ref(false), error = ref(''), notice = ref('')
|
|
|
const name = ref(''), source = ref('local'), profiles = ref('default')
|
|
const name = ref(''), source = ref('local'), profiles = ref('default')
|
|
|
-const serverURL = ref(window.location.origin), hermesURL = ref('http://127.0.0.1:9119'), satellite = ref(false), serverHermes = ref('')
|
|
|
|
|
|
|
+const serverURL = ref(window.location.origin), hermesURL = ref('http://127.0.0.1:9119')
|
|
|
const computerNetwork=ref<'none'|'public-proxy'>('public-proxy')
|
|
const computerNetwork=ref<'none'|'public-proxy'>('public-proxy')
|
|
|
const computers=ref(!!props.computerSetup),computerRuntime=ref<'docker'|'podman'>('docker'),computerImage=ref(''),computerPython=ref(''),hermesSource=ref(''),hermesHome=ref('')
|
|
const computers=ref(!!props.computerSetup),computerRuntime=ref<'docker'|'podman'>('docker'),computerImage=ref(''),computerPython=ref(''),hermesSource=ref(''),hermesHome=ref('')
|
|
|
const artifactRoots = ref(''), insecure = ref(false), configuration = ref<RunnerConfiguration>()
|
|
const artifactRoots = ref(''), insecure = ref(false), configuration = ref<RunnerConfiguration>()
|
|
|
|
|
|
|
|
async function refresh() {
|
|
async function refresh() {
|
|
|
const data = await apiRequest<{runners:Runner[];sources:Array<{id:string;name:string}>;bundleAvailable?:boolean;fixedDesktops?:boolean;hermesUpstream?:string}>('/api/app/admin/runners')
|
|
const data = await apiRequest<{runners:Runner[];sources:Array<{id:string;name:string}>;bundleAvailable?:boolean;fixedDesktops?:boolean;hermesUpstream?:string}>('/api/app/admin/runners')
|
|
|
- runners.value = data.runners; sources.value = data.sources;bundleAvailable.value=!!data.bundleAvailable;fixedDesktops.value=!!data.fixedDesktops;serverHermes.value=data.hermesUpstream||''
|
|
|
|
|
|
|
+ runners.value = data.runners; sources.value = data.sources;bundleAvailable.value=!!data.bundleAvailable;fixedDesktops.value=!!data.fixedDesktops
|
|
|
}
|
|
}
|
|
|
async function action(work:()=>Promise<void>) {
|
|
async function action(work:()=>Promise<void>) {
|
|
|
if(busy.value) return
|
|
if(busy.value) return
|
|
@@ -27,21 +27,15 @@ async function action(work:()=>Promise<void>) {
|
|
|
}
|
|
}
|
|
|
async function register() {
|
|
async function register() {
|
|
|
await action(async()=>{
|
|
await action(async()=>{
|
|
|
- const hermesTarget = satellite.value ? serverHermes.value : hermesURL.value
|
|
|
|
|
- if(satellite.value && !hermesTarget)throw new Error('服务器尚未配置 Hermes 地址')
|
|
|
|
|
- const web = new URL(serverURL.value), local = new URL(hermesTarget)
|
|
|
|
|
|
|
+ const web = new URL(serverURL.value), local = new URL(hermesURL.value)
|
|
|
if(!['http:','https:'].includes(web.protocol)||web.username||web.password||web.search||web.hash||web.pathname!=='/')throw new Error('请填写不带路径的夭夭服务地址')
|
|
if(!['http:','https:'].includes(web.protocol)||web.username||web.password||web.search||web.hash||web.pathname!=='/')throw new Error('请填写不带路径的夭夭服务地址')
|
|
|
if(web.protocol==='http:'&&!['localhost','127.0.0.1','[::1]'].includes(web.hostname)&&!insecure.value)throw new Error('远程节点请使用 HTTPS,或勾选可信局域网 HTTP')
|
|
if(web.protocol==='http:'&&!['localhost','127.0.0.1','[::1]'].includes(web.hostname)&&!insecure.value)throw new Error('远程节点请使用 HTTPS,或勾选可信局域网 HTTP')
|
|
|
- if(satellite.value){
|
|
|
|
|
- if(['127.0.0.1','localhost','[::1]'].includes(local.hostname))throw new Error('服务器 Hermes 仍是本机回环地址,卫星连不上。请把上游配成卫星能访问的地址。')
|
|
|
|
|
- if(!['http:','https:'].includes(local.protocol)||local.username||local.password)throw new Error('服务器 Hermes 地址无效')
|
|
|
|
|
- }else if(!['127.0.0.1','[::1]'].includes(local.hostname)||!['http:','https:'].includes(local.protocol)||local.username||local.password)throw new Error('Hermes 地址必须指向 Runner 所在电脑的回环地址')
|
|
|
|
|
|
|
+ if(!['127.0.0.1','[::1]'].includes(local.hostname)||!['http:','https:'].includes(local.protocol)||local.username||local.password)throw new Error('Hermes 地址必须指向 Runner 所在电脑的回环地址')
|
|
|
const allowedProfiles = [...new Set(profiles.value.split(/[\s,,]+/).filter(Boolean))]
|
|
const allowedProfiles = [...new Set(profiles.value.split(/[\s,,]+/).filter(Boolean))]
|
|
|
if(computers.value&&((!!computerImage.value&&!/^sha256:[a-f0-9]{64}$/.test(computerImage.value))||[computerPython.value,hermesSource.value,hermesHome.value].some(path=>path&&!path.startsWith('/'))))throw new Error('自定义路径必须为执行电脑上的绝对路径;默认安装可留空。镜像 ID 可留空或填写完整 sha256 ID。')
|
|
if(computers.value&&((!!computerImage.value&&!/^sha256:[a-f0-9]{64}$/.test(computerImage.value))||[computerPython.value,hermesSource.value,hermesHome.value].some(path=>path&&!path.startsWith('/'))))throw new Error('自定义路径必须为执行电脑上的绝对路径;默认安装可留空。镜像 ID 可留空或填写完整 sha256 ID。')
|
|
|
const result = await apiRequest<{runner:Runner;token:string}>('/api/app/admin/runners', {method:'POST',body:{name:name.value.trim(),sourceNodeId:source.value,allowedProfiles}})
|
|
const result = await apiRequest<{runner:Runner;token:string}>('/api/app/admin/runners', {method:'POST',body:{name:name.value.trim(),sourceNodeId:source.value,allowedProfiles}})
|
|
|
- const lanHermes = satellite.value && local.protocol==='http:'
|
|
|
|
|
- configuration.value = {protocol:1,serverURL:web.origin,runnerId:result.runner.id,token:result.token,hermesURL:local.toString(),...(satellite.value?{satellite:true}:{}),allowedProfiles,artifactRoots:artifactRoots.value.split('\n').map(p=>p.trim()).filter(Boolean),...(insecure.value||lanHermes?{allowInsecureLan:true}:{}),...(computers.value?{computers:{...(fixedDesktops.value?{managedBy:'compose' as const}:{}),network:fixedDesktops.value?'none':computerNetwork.value,runtime:computerRuntime.value,imageId:computerImage.value||UNCONFIGURED_COMPUTER_IMAGE,python:computerPython.value,hermesSource:hermesSource.value,hermesHome:hermesHome.value}}:{})}
|
|
|
|
|
- notice.value = satellite.value ? '节点已注册。请在卫星电脑上启动 Runner。会话连接服务器 Hermes,不要在卫星上启动本机 Hermes。' : '节点已注册,请下载配置并在 Hermes 所在电脑启动 Runner。'
|
|
|
|
|
|
|
+ configuration.value = {protocol:1,serverURL:web.origin,runnerId:result.runner.id,token:result.token,hermesURL:local.toString(),allowedProfiles,artifactRoots:artifactRoots.value.split('\n').map(p=>p.trim()).filter(Boolean),...(insecure.value?{allowInsecureLan:true}:{}),...(computers.value?{computers:{...(fixedDesktops.value?{managedBy:'compose' as const}:{}),network:fixedDesktops.value?'none':computerNetwork.value,runtime:computerRuntime.value,imageId:computerImage.value||UNCONFIGURED_COMPUTER_IMAGE,python:computerPython.value,hermesSource:hermesSource.value,hermesHome:hermesHome.value}}:{})}
|
|
|
|
|
+ notice.value = '节点已注册,请下载配置并在 Hermes 所在电脑启动 Runner。虚拟环境才会用到它。'
|
|
|
await refresh()
|
|
await refresh()
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
@@ -63,7 +57,7 @@ onMounted(()=>{void action(refresh)})
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
|
<section class="runner-panel" aria-label="执行节点设置">
|
|
<section class="runner-panel" aria-label="执行节点设置">
|
|
|
- <p>在运行 Hermes 的电脑上启动 Runner,它会主动连接夭夭,接收 Bot 任务并提供本轮团队工具。注册后,该来源的 Bot 任务会交给 Runner;节点离线时无法启动新任务,恢复连接后可重试。</p>
|
|
|
|
|
|
|
+ <p>执行节点只给虚拟环境用。聊天和 Mac 桌面不经过这里。节点要和 Hermes 装在同一台电脑上。</p>
|
|
|
<p v-if="bundleAvailable"><a href="/api/app/admin/runners/bundle" download>下载配套执行节点程序</a>(包含桌面资源,需要 Node.js 24 或更高版本)。</p>
|
|
<p v-if="bundleAvailable"><a href="/api/app/admin/runners/bundle" download>下载配套执行节点程序</a>(包含桌面资源,需要 Node.js 24 或更高版本)。</p>
|
|
|
<div class="runner-toolbar"><strong>已注册节点</strong><button type="button" :disabled="busy" @click="action(refresh)">刷新状态</button></div>
|
|
<div class="runner-toolbar"><strong>已注册节点</strong><button type="button" :disabled="busy" @click="action(refresh)">刷新状态</button></div>
|
|
|
<ul v-if="runners.length" class="runner-list">
|
|
<ul v-if="runners.length" class="runner-list">
|
|
@@ -77,10 +71,8 @@ onMounted(()=>{void action(refresh)})
|
|
|
<label>节点名称<input v-model="name" required maxlength="100" autocomplete="off" placeholder="我的 Mac" :disabled="busy"></label>
|
|
<label>节点名称<input v-model="name" required maxlength="100" autocomplete="off" placeholder="我的 Mac" :disabled="busy"></label>
|
|
|
<label>接管的 Bot 来源<select v-model="source" :disabled="busy"><option v-for="item in sources" :key="item.id" :value="item.id">{{ item.name }}</option></select></label>
|
|
<label>接管的 Bot 来源<select v-model="source" :disabled="busy"><option v-for="item in sources" :key="item.id" :value="item.id">{{ item.name }}</option></select></label>
|
|
|
<label>允许的 Profile<input v-model="profiles" required placeholder="default, writer" :disabled="busy"><small>填写 Hermes 的 Profile 名称,以逗号分隔;账号权限仍然生效。</small></label>
|
|
<label>允许的 Profile<input v-model="profiles" required placeholder="default, writer" :disabled="busy"><small>填写 Hermes 的 Profile 名称,以逗号分隔;账号权限仍然生效。</small></label>
|
|
|
- <label class="runner-check"><input v-model="satellite" type="checkbox" :disabled="busy">卫星电脑(会话使用服务器 Hermes,不连接本机 9119)</label>
|
|
|
|
|
<label>夭夭服务地址<input v-model="serverURL" type="url" required :disabled="busy"><small>必须能从 Runner 所在电脑访问。</small></label>
|
|
<label>夭夭服务地址<input v-model="serverURL" type="url" required :disabled="busy"><small>必须能从 Runner 所在电脑访问。</small></label>
|
|
|
- <label v-if="!satellite">Runner 本机 Hermes 地址<input v-model="hermesURL" type="url" required :disabled="busy"></label>
|
|
|
|
|
- <p v-else>会话连接服务器 Hermes:{{ serverHermes || '尚未配置' }}</p>
|
|
|
|
|
|
|
+ <label>Runner 本机 Hermes 地址<input v-model="hermesURL" type="url" required :disabled="busy"></label>
|
|
|
<label>允许导出的产物目录<textarea v-model="artifactRoots" rows="2" :disabled="busy" placeholder="每行一个绝对路径,留空时禁止导出文件"></textarea></label>
|
|
<label>允许导出的产物目录<textarea v-model="artifactRoots" rows="2" :disabled="busy" placeholder="每行一个绝对路径,留空时禁止导出文件"></textarea></label>
|
|
|
<label class="runner-check"><input v-model="insecure" type="checkbox" :disabled="busy">使用可信局域网 HTTP</label>
|
|
<label class="runner-check"><input v-model="insecure" type="checkbox" :disabled="busy">使用可信局域网 HTTP</label>
|
|
|
<label class="runner-check"><input v-model="computers" type="checkbox" :disabled="busy">启用隔离电脑 Worker</label>
|
|
<label class="runner-check"><input v-model="computers" type="checkbox" :disabled="busy">启用隔离电脑 Worker</label>
|
|
@@ -100,7 +92,7 @@ onMounted(()=>{void action(refresh)})
|
|
|
</form>
|
|
</form>
|
|
|
<div v-else class="runner-setup">
|
|
<div v-else class="runner-setup">
|
|
|
<button class="solid-button" type="button" @click="download">下载 Runner 配置</button>
|
|
<button class="solid-button" type="button" @click="download">下载 Runner 配置</button>
|
|
|
- <p>{{ configuration.satellite ? '配置包含连接凭据,仅本次可下载。将文件放到卫星电脑,设置为仅自己可读。不要在卫星上启动 Hermes。' : '配置包含本节点的连接凭据,仅本次可下载。将文件放在 Hermes 所在电脑,设置为仅自己可读:' }}</p>
|
|
|
|
|
|
|
+ <p>配置包含本节点的连接凭据,仅本次可下载。将文件放在 Hermes 所在电脑,设置为仅自己可读:</p>
|
|
|
<code v-if="bundleAvailable">mkdir -p yaoyao-runner<br>tar -xzf yaoyao-runner.tar.gz -C yaoyao-runner<br>chmod 600 runner.json<br>node yaoyao-runner/runner.mjs --config /完整路径/runner.json</code>
|
|
<code v-if="bundleAvailable">mkdir -p yaoyao-runner<br>tar -xzf yaoyao-runner.tar.gz -C yaoyao-runner<br>chmod 600 runner.json<br>node yaoyao-runner/runner.mjs --config /完整路径/runner.json</code>
|
|
|
<code v-else>chmod 600 runner.json<br>npm run runner -- --config /完整路径/runner.json</code>
|
|
<code v-else>chmod 600 runner.json<br>npm run runner -- --config /完整路径/runner.json</code>
|
|
|
<small>{{bundleAvailable?'在运行 Hermes 和 Docker/Podman 的电脑上执行,节点连接后返回本地虚拟机设置准备桌面。':'在 yaoyao 项目中先运行 npm run build。'}}配置丢失后可停用节点并重新注册。</small>
|
|
<small>{{bundleAvailable?'在运行 Hermes 和 Docker/Podman 的电脑上执行,节点连接后返回本地虚拟机设置准备桌面。':'在 yaoyao 项目中先运行 npm run build。'}}配置丢失后可停用节点并重新注册。</small>
|