{status.urls.length > 0 && (
@@ -92,13 +109,7 @@ export function LocalApiSettings(): JSX.Element {
)}
{t.localapi.port}
- setPort(e.target.value.replace(/\D/g, ''))}
- onBlur={commitPort}
- onKeyDown={(e) => { if (e.key === 'Enter') commitPort() }}
- className="w-20 h-6 px-2 rounded bg-raised border border-line text-caption text-ink tabular-nums focus:outline-none focus:border-accent/50"
- />
+ {portInput}
{t.localapi.passwordLabel}
diff --git a/src/renderer/src/lib/webLinks.ts b/src/renderer/src/lib/webLinks.ts
index 3c50e7f..2a4dbdb 100644
--- a/src/renderer/src/lib/webLinks.ts
+++ b/src/renderer/src/lib/webLinks.ts
@@ -2,18 +2,16 @@ import { useEffect, useState } from 'react'
import { IS_WEB } from './platform'
/**
- * 网页链接(http://…/open?…,见 shared/deeplink.ts)的基址:
- * - 网页版:当前站点(NAS 地址 / 桌面"本地接口"地址),浏览器里打开就是这个
- * - 桌面版:"本地接口"在跑时取它的地址 —— 开了局域网访问优先给局域网地址(手机、别的电脑能点开),
- * 否则 127.0.0.1;没开本地接口返回 null,界面不出"拷贝网页链接"
+ * "拷贝链接"的基址(链接形如 http://…/open?…,见 shared/deeplink.ts):
+ * - 网页版:当前站点(NAS 地址 / 桌面"本地接口"地址)
+ * - 桌面版:常驻链接服务的地址 —— 开了局域网访问给局域网地址(手机、别的电脑能点开),否则 localhost;
+ * 服务没起来(端口被占)返回 null,拷贝项置灰并说明原因
+ * 一律给 http 链接而不是 vixel://:笔记、聊天、文档软件都能识别成链接(高亮、可点)
*/
export async function webLinkBase(): Promise {
if (IS_WEB) return location.origin
try {
- const s = await window.api.localApi.status()
- if (!s.running || s.urls.length === 0) return null
- const lan = s.lan ? s.urls.find((u) => !/\/\/(127\.0\.0\.1|localhost|\[::1\])/.test(u)) : undefined
- return lan ?? s.urls[0]
+ return (await window.api.localApi.status()).linkBase
} catch {
return null
}
diff --git a/src/server/app.test.ts b/src/server/app.test.ts
index f725954..1fad70c 100644
--- a/src/server/app.test.ts
+++ b/src/server/app.test.ts
@@ -1,5 +1,8 @@
import { describe, it, expect } from 'vitest'
-import { loopbackHost } from './app'
+import { networkInterfaces } from 'node:os'
+import type { IncomingMessage } from 'node:http'
+import { isLocalRequest, loopbackHost } from './app'
+import { renderOpenedPage } from './webapp'
describe('loopbackHost (DNS rebinding guard for the password-less local API)', () => {
it('accepts loopback hosts with or without a port', () => {
@@ -13,3 +16,27 @@ describe('loopbackHost (DNS rebinding guard for the password-less local API)', (
}
})
})
+
+describe('isLocalRequest (who gets "open in Vixel" instead of the web view)', () => {
+ const from = (remoteAddress: string) => ({ socket: { remoteAddress } }) as Pick
+ it('treats loopback (incl. IPv4-mapped) and this machine\'s own interface addresses as local', () => {
+ expect(isLocalRequest(from('127.0.0.1'))).toBe(true)
+ expect(isLocalRequest(from('::ffff:127.0.0.1'))).toBe(true)
+ expect(isLocalRequest(from('::1'))).toBe(true)
+ const own = Object.values(networkInterfaces()).flat().find((a) => a && !a.internal)
+ if (own) expect(isLocalRequest(from(own.address))).toBe(true)
+ })
+ it('treats other devices as remote', () => {
+ expect(isLocalRequest(from('203.0.113.7'))).toBe(false)
+ expect(isLocalRequest(from(''))).toBe(false)
+ })
+})
+
+describe('renderOpenedPage', () => {
+ it('is a script-free page in the requested language', () => {
+ const zh = renderOpenedPage('zh', true)
+ expect(zh).toContain('已在 Vixel 中打开')
+ expect(zh).not.toMatch(/