Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/fingerprint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ on completion. In a standalone copy of this package, `bun install
--frozen-lockfile --registry https://registry.npmjs.org` and `bun run check`
produce the same build.

The plugin page service listens on `3488` by default and uses Magpie's gateway
The plugin page service listens on `4388` by default and uses Magpie's gateway
at `http://127.0.0.1:3425`. Opening or refreshing the page never downloads
reference data. **Download reference data** explicitly fetches a fresh snapshot;
starting a run downloads one only when no usable snapshot is cached. There is
Expand Down
2 changes: 1 addition & 1 deletion packages/fingerprint/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ bun start

`rebuild` 在隔离副本中使用 `--frozen-lockfile --ignore-scripts` 安装,完成类型检查、测试和构建,再复制 `dist/` 回来并清理临时目录,避免 Bun 使用父级 monorepo 的 workspace 配置。

页面服务默认监听 `3488`,通过 `http://127.0.0.1:3425` 使用 Magpie 网关。打开、刷新页面不会下载参考库,也没有后台刷新。点击「下载参考数据」会下载新快照;启动检测时仅在没有可用缓存时下载。主源为 `https://lm.ikale.io/data/`,备用源为 `https://cdn.jsdmirror.com/npm/lmfpd-data@latest/`。这两个域名会看到客户端 IP;下载请求不携带 provider 凭证、模型标识、提示词或回答。仅主动开始任务才会通过网关发送采样提示。网关认证、`magpie web`、环境变量和离线数据源见 [integration.md](integration.md)。
页面服务默认监听 `4388`,通过 `http://127.0.0.1:3425` 使用 Magpie 网关。打开、刷新页面不会下载参考库,也没有后台刷新。点击「下载参考数据」会下载新快照;启动检测时仅在没有可用缓存时下载。主源为 `https://lm.ikale.io/data/`,备用源为 `https://cdn.jsdmirror.com/npm/lmfpd-data@latest/`。这两个域名会看到客户端 IP;下载请求不携带 provider 凭证、模型标识、提示词或回答。仅主动开始任务才会通过网关发送采样提示。网关认证、`magpie web`、环境变量和离线数据源见 [integration.md](integration.md)。

## 范围与限制

Expand Down
18 changes: 9 additions & 9 deletions packages/fingerprint/dist/cli.js

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions packages/fingerprint/dist/plugin.js

Large diffs are not rendered by default.

12 changes: 0 additions & 12 deletions packages/fingerprint/dist/ui/assets/index-1yjBrEfd.js

This file was deleted.

1 change: 1 addition & 0 deletions packages/fingerprint/dist/ui/assets/index-BNQ_IIC1.css

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions packages/fingerprint/dist/ui/assets/index-BeAnCYkZ.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion packages/fingerprint/dist/ui/assets/index-DeacCGXr.css

This file was deleted.

4 changes: 2 additions & 2 deletions packages/fingerprint/dist/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#f7f7f5" />
<title>Magpie · 模型指纹</title>
<script type="module" crossorigin src="./assets/index-1yjBrEfd.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DeacCGXr.css">
<script type="module" crossorigin src="./assets/index-BeAnCYkZ.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BNQ_IIC1.css">
</head>
<body>
<div id="root"></div>
Expand Down
18 changes: 11 additions & 7 deletions packages/fingerprint/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Plugin contract

The package is a Magpie page plugin. Its manifest declares `page.file` as
`page/index.html`, a local service on port `3488` (configurable through the
`page/index.html`, a local service on port `4388` (configurable through the
`port` option), and one provider-menu action named `fingerprint`. Magpie sends
the action as `{ id, provider: { id, name } }` to the page frame. The page boot
document captures this event before navigating to the service, so the selected
Expand All @@ -21,13 +21,17 @@ WebKitGTK, magpie's Linux window, matches nothing to it there, so a policy of
`'self'` alone loads no script and the page stays blank. The launcher origin changes
only the document policy; the API bearer token and Host/Origin checks still apply.

On Windows, port 3488 can be in an excluded TCP range. Check
On Windows, a TCP port can be in an excluded range. Check
`netsh interface ipv4 show excludedportrange protocol=tcp` and the Magpie
plugin host log if the service cannot bind; configure the plugin's `port`
option to an available port, such as 4388. If the launcher remains on
"Connecting" after its handshake, it shows the local service address for
opening in a browser. Magpie's "page didn't answer" message instead means the
page did not complete the initial host handshake; inspect the host log and
plugin host log if the service cannot bind. `EADDRINUSE` means another process
already owns the port; `EACCES` or `EPERM` can indicate that Windows reserved
or excluded it. Configure the plugin's `port` option to an available port.
If the launcher remains on "Connecting" after its handshake, the page shows
the local service address and a copy action after four seconds. Magpie's
plugin iframe currently has no popup or top-level navigation permission, so
opening the system browser requires an upstream native bridge. Magpie's "page
didn't answer" message instead means the page did not complete the initial
host handshake; inspect the host log and
page loading errors to identify its cause.

## UI integration
Expand Down
8 changes: 5 additions & 3 deletions packages/fingerprint/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magpie-community/page-fingerprint",
"version": "0.2.3",
"version": "0.2.4",
"description": "Check for suspected model substitution with numeric response fingerprints in Magpie, powered by lm-detector.",
"type": "module",
"license": "MIT",
Expand Down Expand Up @@ -28,6 +28,7 @@
"rebuild": "bun scripts/rebuild.ts"
},
"magpie": {
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMTY4NzdkIiBzdHJva2Utd2lkdGg9IjEuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KICA8cGF0aCBkPSJNNC4yIDEyYTcuOCA3LjggMCAwIDEgMTUuNiAwYzAgMy4xLS40IDUuNS0xLjIgNy43Ii8+CiAgPHBhdGggZD0iTTIuMiAxMmE5LjggOS44IDAgMCAxIDIuMi02LjJNNy4xIDMuNUE5LjggOS44IDAgMCAxIDIxLjggMTIiLz4KICA8cGF0aCBkPSJNNy4zIDEyYTQuNyA0LjcgMCAwIDEgOS40IDBjMCA0LjEtLjcgNy4xLTEuOSA5LjMiLz4KICA8cGF0aCBkPSJNMTAuNCAxMmExLjYgMS42IDAgMCAxIDMuMiAwYzAgNC0uOCA3LjUtMi4zIDkuOE03LjMgMTUuMWMtLjIgMi4yLS44IDQuMi0xLjcgNS45TTQuMiAxNS40Yy0uMiAxLjItLjUgMi4zLTEgMy4zIi8+Cjwvc3ZnPgo=",
"page": {
"title": "Fingerprint",
"titles": {
Expand All @@ -37,8 +38,9 @@
"de": "Modell-Fingerabdruck"
},
"file": "page/index.html",
"icon": "page/icon.svg",
"service": {
"port": 3488,
"port": 4388,
"option": "port"
}
},
Expand All @@ -56,7 +58,7 @@
}
],
"options": {
"port": 3488
"port": 4388
},
"permissions": []
},
Expand Down
32 changes: 30 additions & 2 deletions packages/fingerprint/page/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
invalidPort: 'Invalid Fingerprint service port.',
failed: 'Could not connect',
fallback: 'If the page does not open, copy this address into a browser on this computer:',
copyAddress: 'Copy address',
copied: 'Address copied.',
copyFallback: 'Address selected. Copy it and open it in a browser on this computer.',
},
'zh-Hans': {
title: '模型指纹',
Expand All @@ -16,6 +19,9 @@
invalidPort: '模型指纹服务端口无效。',
failed: '连接失败',
fallback: '如果页面未打开,请将此地址复制到本机浏览器:',
copyAddress: '复制地址',
copied: '地址已复制。',
copyFallback: '已选中地址,请复制后在本机浏览器中打开。',
},
'zh-Hant': {
title: '模型指紋',
Expand All @@ -24,6 +30,9 @@
invalidPort: '模型指紋服務連接埠無效。',
failed: '連線失敗',
fallback: '如果頁面未開啟,請將此網址複製到本機瀏覽器:',
copyAddress: '複製網址',
copied: '網址已複製。',
copyFallback: '已選取網址,請複製後在本機瀏覽器中開啟。',
},
ja: {
title: 'モデル指紋',
Expand All @@ -34,6 +43,9 @@
failed: '接続できませんでした',
fallback:
'ページが開かない場合は、このアドレスをこのコンピューターのブラウザーにコピーしてください:',
copyAddress: 'アドレスをコピー',
copied: 'アドレスをコピーしました。',
copyFallback: 'アドレスを選択しました。このコンピューターのブラウザーに貼り付けてください。',
},
de: {
title: 'Modell-Fingerabdruck',
Expand All @@ -44,6 +56,9 @@
failed: 'Verbindung fehlgeschlagen',
fallback:
'Falls sich die Seite nicht öffnet, diese Adresse im Browser auf diesem Computer öffnen:',
copyAddress: 'Adresse kopieren',
copied: 'Adresse kopiert.',
copyFallback: 'Adresse ausgewählt. Kopieren und im Browser auf diesem Computer öffnen.',
},
};
let text = messages.en;
Expand Down Expand Up @@ -71,7 +86,7 @@
followLanguage(theme);
window.magpie?.onTheme?.(followLanguage);
const config = (await window.magpie?.options?.get?.()) || {};
const port = Number(config.port ?? 3488);
const port = Number(config.port ?? 4388);
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(text.invalidPort);
let service = window.magpie?.service;
let localFallback = '';
Expand Down Expand Up @@ -114,7 +129,20 @@
status.textContent = text.fallback;
const field = document.getElementById('service-url');
field.value = localFallback;
field.onclick = () => field.select();
const copy = document.getElementById('copy-service-url');
copy.textContent = text.copyAddress;
copy.onclick = async () => {
try {
const clipboard = window.navigator?.clipboard;
if (!clipboard?.writeText) throw new Error('Clipboard unavailable');
await clipboard.writeText(localFallback);
status.textContent = text.copied;
} catch {
field.focus();
field.select();
status.textContent = text.copyFallback;
}
};
document.getElementById('fallback').hidden = false;
}, 4000);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/fingerprint/page/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion packages/fingerprint/page/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,19 @@
width: 100%;
padding: 6px;
}
button {
margin-top: 8px;
padding: 6px 10px;
}
</style>
</head>
<body>
<main>
<strong id="title">Fingerprint</strong>
<p id="status">Connecting to Fingerprint…</p>
<p id="status" aria-live="polite">Connecting to Fingerprint…</p>
<div id="fallback" hidden>
<input id="service-url" readonly aria-label="Fingerprint service URL" />
<button id="copy-service-url" type="button">Copy address</button>
</div>
</main>
<script src="_magpie/bridge.js"></script>
Expand Down
2 changes: 1 addition & 1 deletion packages/fingerprint/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { startServer } from './server';
const argv = process.argv.slice(2);
if (argv.includes('--help')) {
console.log(
'Magpie Fingerprint\n bun start [--demo] [--port 3488]\nEnvironment: MAGPIE_FINGERPRINT_GATEWAY, MAGPIE_FINGERPRINT_API_KEY, MAGPIE_FINGERPRINT_DATA_DIR, MAGPIE_FINGERPRINT_BANK_SOURCE',
'Magpie Fingerprint\n bun start [--demo] [--port 4388]\nEnvironment: MAGPIE_FINGERPRINT_GATEWAY, MAGPIE_FINGERPRINT_API_KEY, MAGPIE_FINGERPRINT_DATA_DIR, MAGPIE_FINGERPRINT_BANK_SOURCE',
);
process.exit(0);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/fingerprint/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function options(
throw new Error(
'Gateway must be an HTTP(S) origin without credentials, path, query or fragment.',
);
const port = input.port ?? Number(process.env.MAGPIE_FINGERPRINT_PORT || 3488);
const port = input.port ?? Number(process.env.MAGPIE_FINGERPRINT_PORT || 4388);
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Invalid service port.');
return {
port,
Expand Down
20 changes: 19 additions & 1 deletion packages/fingerprint/src/managed-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import { boundedText } from './http';
import { startServer } from './server';
import { serviceIdentity } from './service-identity';

export function serviceBindError(error: unknown, port: number) {
const cause = error as NodeJS.ErrnoException;
if (cause.code === 'EADDRINUSE')
return new Error(
`Cannot bind Fingerprint service to port ${port}: the port is already in use. Configure a different page service port.`,
);
if (cause.code === 'EACCES' || cause.code === 'EPERM')
return new Error(
`Cannot bind Fingerprint service to port ${port}: the operating system denied access. On Windows, this port may be reserved or excluded; choose another available page service port and check \`netsh interface ipv4 show excludedportrange protocol=tcp\`.`,
);
return error instanceof Error ? error : new Error(String(error));
}

/** Hosts with identical settings share one listener. An attached host takes over
* when its owner exits; no detached daemon survives the Magpie hosts. */
export async function startManagedService(input: Options = {}, pollMs = 1000) {
Expand Down Expand Up @@ -45,7 +58,12 @@ export async function startManagedService(input: Options = {}, pollMs = 1000) {
owned = await startServer(opt);
} catch (error) {
// Another host may have won between discovery and bind.
if ((error as NodeJS.ErrnoException).code === 'EADDRINUSE' && (await probe())) return;
if ((error as NodeJS.ErrnoException).code === 'EADDRINUSE') {
if (await probe()) return;
throw serviceBindError(error, opt.port);
}
if (['EACCES', 'EPERM'].includes((error as NodeJS.ErrnoException).code || ''))
throw serviceBindError(error, opt.port);
throw error;
}
if (closed) {
Expand Down
3 changes: 2 additions & 1 deletion packages/fingerprint/src/service-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import {
writeSync,
} from 'node:fs';
import { join, resolve } from 'node:path';
import { version } from '../package.json';
import { options, type Options } from './config';

export const VERSION = '0.2.2';
export const VERSION = version;

// The health endpoint is public, so the configuration digest must not be a
// plain hash of a low-entropy gateway key. Keep the salt beside the install's
Expand Down
32 changes: 25 additions & 7 deletions packages/fingerprint/tests/boot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ import { readFile } from 'node:fs/promises';
import { runInNewContext } from 'node:vm';

const source = await readFile(new URL('../page/boot.js', import.meta.url), 'utf8');
async function boot(lang?: string, remote = false, port = 3488, desktopHost = '') {
async function boot(
lang?: string,
remote = false,
port = 4388,
desktopHost = '',
clipboard?: { writeText: (value: string) => Promise<void> },
) {
const elements = {
status: { textContent: 'Connecting to Fingerprint…' },
title: { textContent: 'Fingerprint' },
fallback: { hidden: true },
'service-url': { value: '', onclick: () => {}, select: () => {} },
'service-url': { value: '', focus: () => {}, select: () => {} },
'copy-service-url': { textContent: '', onclick: async () => {} },
};
const document = {
title: 'Fingerprint',
Expand All @@ -18,14 +25,15 @@ async function boot(lang?: string, remote = false, port = 3488, desktopHost = ''
let destination: URL | undefined;
let fallback: (() => void) | undefined;
// magpie hands no service to its desktop window, only to magpie web
const service = remote || desktopHost ? '' : 'http://127.0.0.1:3488/';
const service = remote || desktopHost ? '' : `http://127.0.0.1:${port}/`;
await runInNewContext(source, {
URL,
setTimeout: (callback: () => void) => {
fallback = callback;
},
document,
window: {
navigator: clipboard ? { clipboard } : undefined,
magpie: {
ready: Promise.resolve({ dark: true, ...(lang ? { lang } : {}) }),
service,
Expand Down Expand Up @@ -61,7 +69,7 @@ test('boot defaults to English and follows host languages before navigation', as
expect(result.document.documentElement.lang).toBe(htmlLang!);
expect(result.elements.status.textContent).toContain(status!);
expect(result.destination?.searchParams.get('provider')).toBe('vendor');
expect(result.destination?.searchParams.get('uiOrigin')).toBe('http://127.0.0.1:3488');
expect(result.destination?.searchParams.get('uiOrigin')).toBe('http://127.0.0.1:4388');
expect(result.destination?.searchParams.get('lang')).toBe(lang || null);
}
});
Expand All @@ -73,6 +81,16 @@ test('a stalled desktop navigation offers the local service address', async () =
expect(result.elements.fallback.hidden).toBe(false);
expect(result.elements['service-url'].value).toBe('http://127.0.0.1:4388/');
expect(result.elements.status.textContent).toContain('本机浏览器');
const copied: string[] = [];
const withClipboard = await boot('en', false, 4388, 'wails.localhost', {
writeText: async (value) => void copied.push(value),
});
withClipboard.fallback?.();
await withClipboard.elements['copy-service-url'].onclick();
expect(copied).toEqual(['http://127.0.0.1:4388/']);
expect(withClipboard.elements.status.textContent).toBe('Address copied.');
await result.elements['copy-service-url'].onclick();
expect(result.elements.status.textContent).toContain('已选中地址');
const remote = await boot('en');
expect(remote.fallback).toBeUndefined();
});
Expand All @@ -90,9 +108,9 @@ test("boot goes to the local service from magpie's desktop window on every syste
// Wails v3: wails://localhost on macOS and Linux, http://wails.localhost
// on Windows (WebView2), where the page said "upgrade Magpie" instead
for (const host of ['localhost', 'wails.localhost', '[::1]', '127.0.0.1']) {
const result = await boot('zh-CN', false, 3490, host);
expect(result.destination?.origin).toBe('http://127.0.0.1:3490');
expect(result.destination?.searchParams.get('uiOrigin')).toBe('http://127.0.0.1:3490');
const result = await boot('zh-CN', false, 4390, host);
expect(result.destination?.origin).toBe('http://127.0.0.1:4390');
expect(result.destination?.searchParams.get('uiOrigin')).toBe('http://127.0.0.1:4390');
expect(result.elements.status.textContent).not.toContain('升级');
}
});
12 changes: 11 additions & 1 deletion packages/fingerprint/tests/managed-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createHash } from 'node:crypto';
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { startManagedService } from '../src/managed-service';
import { serviceBindError, startManagedService } from '../src/managed-service';

const waitFor = async (check: () => boolean) => {
for (let i = 0; i < 100; i++) {
Expand All @@ -13,6 +13,16 @@ const waitFor = async (check: () => boolean) => {
throw new Error('Timed out waiting for host takeover');
};

test('bind diagnostics distinguish port conflicts from Windows port exclusions', () => {
const inUse = Object.assign(new Error('bind failed'), { code: 'EADDRINUSE' });
const denied = Object.assign(new Error('bind failed'), { code: 'EACCES' });
expect(serviceBindError(inUse, 3488).message).toContain('port 3488: the port is already in use');
expect(serviceBindError(denied, 3488).message).toContain('reserved or excluded');
expect(serviceBindError(denied, 3488).message).toContain(
'netsh interface ipv4 show excludedportrange protocol=tcp',
);
});

test('two hosts share one compatible service and the survivor takes over', async () => {
const dir = await mkdtemp(join(tmpdir(), 'fingerprint-hosts-'));
const opts = { demo: true, dataDir: dir, uiDir: join(dir, 'ui') };
Expand Down
Loading
Loading