使用者在 Ubuntu 上 scan 不到 Kneron 裝置。根因:Linux 預設 USB 裝置 權限是 root only,非 root 使用者的 kp.core.scan_devices 因 permission denied 而 silently 回傳 0 裝置。需要安裝 udev rule。 修法三層: 1. Server:GET/POST /api/devices 在 Linux + 0 裝置 + udev rule 不存在 時帶 udevHint: true 2. 新增 POST /api/system/install-udev:用 pkexec 提權安裝 99-kneron.rules + reload udev(彈 Linux 圖形化密碼對話框) 3. 前端 devices page:udevHint=true 時顯示 amber 色 banner 提示 + 一鍵安裝按鈕,成功後自動 rescan Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
133 lines
4.2 KiB
TypeScript
133 lines
4.2 KiB
TypeScript
import { create } from 'zustand';
|
||
import { api } from '@/lib/api';
|
||
import type { Device, DeviceEvent } from '@/types/device';
|
||
import { showSuccess, showError, showApiError } from '@/lib/toast';
|
||
import { getTranslation } from '@/lib/i18n';
|
||
import { useActivityStore } from './activity-store';
|
||
|
||
interface DeviceState {
|
||
devices: Device[];
|
||
selectedDevice: Device | null;
|
||
loading: boolean;
|
||
scanning: boolean;
|
||
connectingId: string | null;
|
||
disconnectingId: string | null;
|
||
udevHint: boolean;
|
||
fetchDevices: () => Promise<void>;
|
||
scanDevices: () => Promise<void>;
|
||
fetchDevice: (id: string) => Promise<void>;
|
||
connectDevice: (id: string) => Promise<void>;
|
||
disconnectDevice: (id: string) => Promise<void>;
|
||
handleEvent: (event: DeviceEvent) => void;
|
||
}
|
||
|
||
export const useDeviceStore = create<DeviceState>((set, get) => ({
|
||
devices: [],
|
||
selectedDevice: null,
|
||
loading: false,
|
||
scanning: false,
|
||
connectingId: null,
|
||
disconnectingId: null,
|
||
udevHint: false,
|
||
|
||
scanDevices: async () => {
|
||
set({ scanning: true });
|
||
try {
|
||
const res = await api.post<{ devices: Device[]; udevHint?: boolean }>('/devices/scan');
|
||
if (res.success && res.data) {
|
||
set({ devices: res.data.devices || [], scanning: false, udevHint: !!res.data.udevHint });
|
||
} else {
|
||
set({ scanning: false });
|
||
showApiError(res.error);
|
||
}
|
||
} catch {
|
||
set({ scanning: false });
|
||
showError(getTranslation().t('errors.failedToLoadDevices'));
|
||
}
|
||
},
|
||
|
||
fetchDevices: async () => {
|
||
set({ loading: true });
|
||
try {
|
||
const res = await api.get<{ devices: Device[]; udevHint?: boolean }>('/devices');
|
||
if (res.success && res.data) {
|
||
set({ devices: res.data.devices || [], loading: false, udevHint: !!res.data.udevHint });
|
||
} else {
|
||
set({ loading: false });
|
||
showApiError(res.error);
|
||
}
|
||
} catch {
|
||
set({ loading: false });
|
||
showError(getTranslation().t('errors.failedToLoadDevices'));
|
||
}
|
||
},
|
||
|
||
fetchDevice: async (id) => {
|
||
const res = await api.get<Device>(`/devices/${id}`);
|
||
if (res.success && res.data) {
|
||
set({ selectedDevice: res.data });
|
||
} else {
|
||
showApiError(res.error);
|
||
}
|
||
},
|
||
|
||
connectDevice: async (id) => {
|
||
set({ connectingId: id });
|
||
try {
|
||
const res = await api.post(`/devices/${id}/connect`);
|
||
if (res.success) {
|
||
showSuccess(getTranslation().t('errors.deviceConnected'));
|
||
const name = get().devices.find((d) => d.id === id)?.name || id;
|
||
useActivityStore.getState().addActivity('device_connect', `Device connected: ${name}`);
|
||
} else {
|
||
const msg = res.error?.message || '';
|
||
if (/winusb|error.{0,5}code.{0,5}28|KP_ERROR_CONNECT_FAILED/i.test(msg)) {
|
||
showApiError({
|
||
code: 'DRIVER_NOT_INSTALLED',
|
||
message:
|
||
'連線失敗:Kneron WinUSB driver 尚未安裝。\n' +
|
||
'請點擊右上角「安裝 USB Driver」按鈕(需要系統管理員權限),安裝完成後重新點選連線。',
|
||
});
|
||
} else {
|
||
showApiError(res.error);
|
||
}
|
||
}
|
||
} finally {
|
||
set({ connectingId: null });
|
||
get().fetchDevices();
|
||
}
|
||
},
|
||
|
||
disconnectDevice: async (id) => {
|
||
set({ disconnectingId: id });
|
||
try {
|
||
const res = await api.post(`/devices/${id}/disconnect`);
|
||
if (res.success) {
|
||
showSuccess(getTranslation().t('errors.deviceDisconnected'));
|
||
const name = get().devices.find((d) => d.id === id)?.name || id;
|
||
useActivityStore.getState().addActivity('device_disconnect', `Device disconnected: ${name}`);
|
||
} else {
|
||
showApiError(res.error);
|
||
}
|
||
} finally {
|
||
set({ disconnectingId: null });
|
||
get().fetchDevices();
|
||
}
|
||
},
|
||
|
||
handleEvent: (event) => {
|
||
set((state) => {
|
||
const devices = [...state.devices];
|
||
const idx = devices.findIndex((d) => d.id === event.device.id);
|
||
if (event.event === 'discovered') {
|
||
if (idx === -1) devices.push(event.device);
|
||
} else if (event.event === 'removed') {
|
||
if (idx !== -1) devices.splice(idx, 1);
|
||
} else if (event.event === 'updated') {
|
||
if (idx !== -1) devices[idx] = event.device;
|
||
}
|
||
return { devices };
|
||
});
|
||
},
|
||
}));
|