界面文案改成中性措辞;延迟测试报出真实原因
原来那句「X 测不通」把两种要查不同地方的情况混在了一起。内核的语义是精确的 (hub/route/proxies.go:122-145):504 表示在给定超时内没有完成,503 表示连接建立失败 或测得延迟为 0。现在分开报,超时会写明是几秒,503 且内核没给具体错误时说明可能是 线路失效或本机到该服务器网络不通。 其余文案统一过一遍:「本机之外也能连」「这个名字已经被占用了」「还没做」这类口语改成 「允许来自其它主机的连接」「名称已存在」「尚未实现」。
This commit is contained in:
+4
-4
@@ -139,16 +139,16 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<p v-if="!booted && refreshing" class="note">读取中</p>
|
<p v-if="!booted && refreshing" class="note">载入中</p>
|
||||||
|
|
||||||
<div v-else-if="loadError" class="alert">
|
<div v-else-if="loadError" class="alert">
|
||||||
<h2>读不到配置</h2>
|
<h2>配置读取失败</h2>
|
||||||
<p>{{ loadError }}</p>
|
<p>{{ loadError }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else-if="snapshot">
|
<template v-else-if="snapshot">
|
||||||
<div v-if="problems.length" class="alert">
|
<div v-if="problems.length" class="alert">
|
||||||
<h2>{{ problems.length }} 处问题</h2>
|
<h2>配置存在 {{ problems.length }} 处错误</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(p, i) in problems" :key="i">
|
<li v-for="(p, i) in problems" :key="i">
|
||||||
<code>{{ p.path.join('.') }}</code> {{ p.message }}
|
<code>{{ p.path.join('.') }}</code> {{ p.message }}
|
||||||
@@ -169,7 +169,7 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
|
|
||||||
<div v-else class="note">
|
<div v-else class="note">
|
||||||
<component :is="current?.icon" class="note-icon" />
|
<component :is="current?.icon" class="note-icon" />
|
||||||
<p>{{ current?.label }}还没做</p>
|
<p>{{ current?.label }} 尚未实现</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ export class ApiError extends Error {
|
|||||||
readonly status: number,
|
readonly status: number,
|
||||||
readonly payload: Record<string, unknown>,
|
readonly payload: Record<string, unknown>,
|
||||||
) {
|
) {
|
||||||
super(typeof payload.message === 'string' ? payload.message : `请求失败(${String(status)})`)
|
super(typeof payload.message === 'string' ? payload.message : `请求失败(HTTP ${String(status)})`)
|
||||||
this.name = 'ApiError'
|
this.name = 'ApiError'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function delayClass(ms: number | null): string {
|
|||||||
type="button"
|
type="button"
|
||||||
:class="['delay', delayClass(delay)]"
|
:class="['delay', delayClass(delay)]"
|
||||||
:disabled="testing"
|
:disabled="testing"
|
||||||
:title="delay === null ? '测这条线路的延迟' : '重新测'"
|
:title="delay === null ? '测试延迟' : '重新测试'"
|
||||||
@click="emit('test')"
|
@click="emit('test')"
|
||||||
>
|
>
|
||||||
<IconLoad v-if="testing" class="spin" />
|
<IconLoad v-if="testing" class="spin" />
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ function isCurrentOutbound(mode: OutboundMode): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BUILTINS: Array<{ mode: OutboundMode; label: string; sub: string }> = [
|
const BUILTINS: Array<{ mode: OutboundMode; label: string; sub: string }> = [
|
||||||
{ mode: { kind: 'rules' }, label: 'RULE', sub: '走分流规则' },
|
{ mode: { kind: 'rules' }, label: 'RULE', sub: '由规则决定' },
|
||||||
{ mode: { kind: 'direct' }, label: 'DIRECT', sub: '不经过代理' },
|
{ mode: { kind: 'direct' }, label: 'DIRECT', sub: '直接连接' },
|
||||||
{ mode: { kind: 'global' }, label: 'GLOBAL', sub: '跟随全局选择' },
|
{ mode: { kind: 'global' }, label: 'GLOBAL', sub: '跟随 GLOBAL 组' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const LINE_LOCKED = '订阅线路不能被入站直接引用,选中它需要自动建一个筛选组——这部分还没做'
|
const LINE_LOCKED = '订阅线路无法被入站直接引用,需要通过筛选组间接引入。该功能尚未实现。'
|
||||||
const NO_PROXY_FIELD = '这路入站没有 proxy 字段'
|
const NO_PROXY_FIELD = '该入站没有 proxy 字段'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -126,7 +126,7 @@ const NO_PROXY_FIELD = '这路入站没有 proxy 字段'
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<template v-if="switchable">
|
<template v-if="switchable">
|
||||||
<h4>{{ switchable.group }} 当前选中</h4>
|
<h4>{{ switchable.group }} 的当前选中</h4>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<OutboundCard
|
<OutboundCard
|
||||||
v-for="m in switchable.members"
|
v-for="m in switchable.members"
|
||||||
@@ -148,7 +148,7 @@ const NO_PROXY_FIELD = '这路入站没有 proxy 字段'
|
|||||||
<section class="block">
|
<section class="block">
|
||||||
<h3>出站</h3>
|
<h3>出站</h3>
|
||||||
<p v-if="locked" class="hint">
|
<p v-if="locked" class="hint">
|
||||||
由 <code>{{ inbound.legacyKey }}</code> 声明,这种写法没有 proxy 字段,固定走 RULE
|
由 <code>{{ inbound.legacyKey }}</code> 声明。该写法没有 proxy 字段,出站固定为 RULE。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="!locked" class="grid">
|
<div v-if="!locked" class="grid">
|
||||||
@@ -207,9 +207,9 @@ const NO_PROXY_FIELD = '这路入站没有 proxy 字段'
|
|||||||
<h4 :class="locked && 'is-dim'">订阅线路</h4>
|
<h4 :class="locked && 'is-dim'">订阅线路</h4>
|
||||||
<p v-if="!kernelOnline" class="hint">
|
<p v-if="!kernelOnline" class="hint">
|
||||||
<IconOffline />
|
<IconOffline />
|
||||||
内核未连接,看不到订阅里有哪些线路
|
内核未连接,无法获取订阅线路
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="providerLines.length === 0" class="hint">订阅里没有线路</p>
|
<p v-else-if="providerLines.length === 0" class="hint">订阅中没有线路</p>
|
||||||
<div v-else class="grid">
|
<div v-else class="grid">
|
||||||
<OutboundCard
|
<OutboundCard
|
||||||
v-for="line in providerLines"
|
v-for="line in providerLines"
|
||||||
|
|||||||
@@ -129,10 +129,10 @@ function submit() {
|
|||||||
|
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<label class="row">
|
<label class="row">
|
||||||
<span class="k">名字</span>
|
<span class="k">名称</span>
|
||||||
<span class="v">
|
<span class="v">
|
||||||
<input v-model="name" type="text" placeholder="容器A" autocomplete="off" />
|
<input v-model="name" type="text" placeholder="容器A" autocomplete="off" />
|
||||||
<span v-if="nameTaken" class="bad">这个名字已经被占用了</span>
|
<span v-if="nameTaken" class="bad">名称已存在</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ function submit() {
|
|||||||
<span class="k">端口</span>
|
<span class="k">端口</span>
|
||||||
<span class="v">
|
<span class="v">
|
||||||
<input v-model="port" class="mono" type="text" placeholder="7778" autocomplete="off" />
|
<input v-model="port" class="mono" type="text" placeholder="7778" autocomplete="off" />
|
||||||
<span v-if="portInvalid" class="bad">支持 7778、200,302、401-429 这几种写法</span>
|
<span v-if="portInvalid" class="bad">支持单个端口、逗号分隔的多个端口、连字符表示的区间</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ function submit() {
|
|||||||
<span class="k">监听</span>
|
<span class="k">监听</span>
|
||||||
<span class="v">
|
<span class="v">
|
||||||
<input v-model="listen" class="mono" type="text" placeholder="0.0.0.0" autocomplete="off" />
|
<input v-model="listen" class="mono" type="text" placeholder="0.0.0.0" autocomplete="off" />
|
||||||
<span class="note">留空等于 0.0.0.0,本机之外也能连</span>
|
<span class="note">留空为 0.0.0.0,允许来自其它主机的连接</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ function submit() {
|
|||||||
<span class="k">{{ f.key }}</span>
|
<span class="k">{{ f.key }}</span>
|
||||||
<span class="v">
|
<span class="v">
|
||||||
<select v-if="f.enum" v-model="extras[f.key]">
|
<select v-if="f.enum" v-model="extras[f.key]">
|
||||||
<option value="" disabled>选一个</option>
|
<option value="" disabled>请选择</option>
|
||||||
<option v-for="opt in f.enum" :key="String(opt)" :value="String(opt)">{{ opt }}</option>
|
<option v-for="opt in f.enum" :key="String(opt)" :value="String(opt)">{{ opt }}</option>
|
||||||
</select>
|
</select>
|
||||||
<input v-else v-model="extras[f.key]" class="mono" type="text" autocomplete="off" />
|
<input v-else v-model="extras[f.key]" class="mono" type="text" autocomplete="off" />
|
||||||
|
|||||||
+37
-12
@@ -115,7 +115,7 @@ async function pickOutbound(mode: OutboundMode) {
|
|||||||
else entry.proxy = proxy
|
else entry.proxy = proxy
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await commit(next, `${target.name} 的出站改成了 ${outboundLabel(mode).text}`)
|
await commit(next, `${target.name} 的出站已设为 ${outboundLabel(mode).text}`)
|
||||||
} finally {
|
} finally {
|
||||||
pendingOutbound.value = null
|
pendingOutbound.value = null
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ async function submitForm(entry: ConfigObject) {
|
|||||||
next.listeners = listeners
|
next.listeners = listeners
|
||||||
|
|
||||||
formOpen.value = false
|
formOpen.value = false
|
||||||
await commit(next, editingIndex === null ? `新增了 ${String(entry.name)}` : '改动已保存')
|
await commit(next, editingIndex === null ? `已新增 ${String(entry.name)}` : '已保存')
|
||||||
if (editingIndex === null) selectedId.value = `listener:${String(listeners.length - 1)}`
|
if (editingIndex === null) selectedId.value = `listener:${String(listeners.length - 1)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,22 +159,47 @@ async function selectMember(group: string, member: string) {
|
|||||||
emit('changed')
|
emit('changed')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof ApiError ? String(error.payload.error ?? error.message) : String(error)
|
const message = error instanceof ApiError ? String(error.payload.error ?? error.message) : String(error)
|
||||||
failure.value = { message, hint: '这一步只改内核的运行状态,配置文件没有被动过。' }
|
failure.value = { message, hint: '该操作仅修改内核运行状态,配置文件未被修改。' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 测一条线路的延迟。纯读操作,不影响任何配置,所以谁都不用禁用。 */
|
/** 延迟测试用的超时,毫秒。也用在失败提示里,让「超时」这个词有具体含义。 */
|
||||||
|
const TEST_TIMEOUT_MS = 5000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把内核的延迟测试结果翻译成一句说明原因的话。
|
||||||
|
*
|
||||||
|
* 内核的语义是精确的(hub/route/proxies.go:122-145):
|
||||||
|
* 504 在给定超时内没有完成
|
||||||
|
* 503 连接建立失败,或者测得的延迟为 0
|
||||||
|
* 笼统说一句「测不通」会把这两种情况混在一起,而它们要查的地方不一样。
|
||||||
|
*/
|
||||||
|
function describeTestFailure(name: string, error: unknown): string {
|
||||||
|
if (!(error instanceof ApiError)) return `${name} 延迟测试失败:${String(error)}`
|
||||||
|
|
||||||
|
const seconds = String(TEST_TIMEOUT_MS / 1000)
|
||||||
|
if (error.status === 504) return `${name} 延迟测试超时,${seconds} 秒内没有取到测试地址`
|
||||||
|
|
||||||
|
const detail = String(error.payload.message ?? '')
|
||||||
|
const generic = detail === '' || detail === 'An error occurred in the delay test'
|
||||||
|
if (error.status === 503) {
|
||||||
|
return generic
|
||||||
|
? `${name} 无法建立连接,线路可能已失效,或本机到该服务器的网络不通`
|
||||||
|
: `${name} 无法建立连接:${detail}`
|
||||||
|
}
|
||||||
|
return `${name} 延迟测试失败(HTTP ${String(error.status)})${generic ? '' : `:${detail}`}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 测一条线路的延迟。纯读操作,不影响任何配置,所以不禁用任何东西。 */
|
||||||
async function testLine(name: string) {
|
async function testLine(name: string) {
|
||||||
if (testing.value.includes(name)) return
|
if (testing.value.includes(name)) return
|
||||||
testing.value = [...testing.value, name]
|
testing.value = [...testing.value, name]
|
||||||
try {
|
try {
|
||||||
await api.testLine(name)
|
await api.testLine(name, undefined, TEST_TIMEOUT_MS)
|
||||||
emit('changed')
|
emit('changed')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 测不通是关于线路的结论,不是 Patchbay 出了问题,不弹错误框
|
// 测不通是关于线路的结论,不是 Patchbay 出了故障,所以不用错误框
|
||||||
const raw =
|
done.value = describeTestFailure(name, error)
|
||||||
error instanceof ApiError ? String(error.payload.message ?? error.message) : String(error)
|
|
||||||
done.value = raw.includes('timeout') ? `${name} 超时` : `${name} 测不通`
|
|
||||||
} finally {
|
} finally {
|
||||||
testing.value = testing.value.filter((n) => n !== name)
|
testing.value = testing.value.filter((n) => n !== name)
|
||||||
}
|
}
|
||||||
@@ -183,7 +208,7 @@ async function testLine(name: string) {
|
|||||||
async function removeSelected() {
|
async function removeSelected() {
|
||||||
const target = selected.value
|
const target = selected.value
|
||||||
if (!target || target.listenerIndex === null || saving.value) return
|
if (!target || target.listenerIndex === null || saving.value) return
|
||||||
if (!confirm(`删除入站「${target.name}」?端口 ${target.port} 会立刻停止监听。`)) return
|
if (!confirm(`删除入站「${target.name}」?端口 ${target.port} 将停止监听。`)) return
|
||||||
|
|
||||||
const next = clone()
|
const next = clone()
|
||||||
const listeners = (next.listeners as ConfigObject[]).filter((_, i) => i !== target.listenerIndex)
|
const listeners = (next.listeners as ConfigObject[]).filter((_, i) => i !== target.listenerIndex)
|
||||||
@@ -192,7 +217,7 @@ async function removeSelected() {
|
|||||||
else next.listeners = listeners
|
else next.listeners = listeners
|
||||||
|
|
||||||
selectedId.value = null
|
selectedId.value = null
|
||||||
await commit(next, `删除了 ${target.name}`)
|
await commit(next, `已删除 ${target.name}`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -266,7 +291,7 @@ async function removeSelected() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="inbounds.length === 0" class="empty">没有入站。mihomo 不会监听任何端口。</p>
|
<p v-if="inbounds.length === 0" class="empty">没有入站,内核不会监听任何端口。</p>
|
||||||
|
|
||||||
<InboundForm
|
<InboundForm
|
||||||
:open="formOpen"
|
:open="formOpen"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const value = ref('')
|
|||||||
<div class="gate">
|
<div class="gate">
|
||||||
<form class="box" @submit.prevent="emit('submit', value)">
|
<form class="box" @submit.prevent="emit('submit', value)">
|
||||||
<h1>Patchbay</h1>
|
<h1>Patchbay</h1>
|
||||||
<p class="lead">输入访问令牌。</p>
|
<p class="lead">需要访问令牌。</p>
|
||||||
<input
|
<input
|
||||||
v-model="value"
|
v-model="value"
|
||||||
class="field mono"
|
class="field mono"
|
||||||
@@ -21,7 +21,7 @@ const value = ref('')
|
|||||||
/>
|
/>
|
||||||
<button class="go" type="submit" :disabled="!value.trim()">进入</button>
|
<button class="go" type="submit" :disabled="!value.trim()">进入</button>
|
||||||
<p class="where">
|
<p class="where">
|
||||||
令牌在服务端配置文件旁边的 <code>.patchbay-token</code> 里,启动时也会打印在控制台上。
|
令牌位于服务端配置文件的同级目录 <code>.patchbay-token</code>,启动时也会输出到控制台。
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user