chore: 恢复旧布局基线(迁移改动丢失后重建,apps/packages 布局暂存)
This commit is contained in:
1 parent
a9510235bb
commit
456fa6954b
203 files changed
+18787
-48
No files matched your search
+592
@@ -0,0 +1,592 @@
|
||||
import type { SavedConnection, QueryResult, TreeNode, ColumnInfo } from "./store";
|
||||
import { decodeSecret } from "./secrets";
|
||||
|
||||
/** C2:桌面 sidecar 注入的运行时配置(缺省时行为与现状一致) */
|
||||
declare global {
|
||||
interface Window {
|
||||
__OPSXDB_API__?: string;
|
||||
__OPSXDB_TOKEN__?: string;
|
||||
}
|
||||
}
|
||||
|
||||
function injectedApi(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
const v = window.__OPSXDB_API__;
|
||||
return v ? v.replace(/\/$/, "") : undefined;
|
||||
}
|
||||
|
||||
function injectedToken(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
return window.__OPSXDB_TOKEN__ ?? "";
|
||||
}
|
||||
|
||||
/** 开发态走 Vite 代理(空串);桌面生产包优先 sidecar 注入地址,回落默认 8788。 */
|
||||
function resolveApiBase(): string {
|
||||
const injected = injectedApi();
|
||||
if (injected) return injected;
|
||||
const fromEnv = (import.meta as { env?: Record<string, string> }).env?.VITE_OPSXDB_API;
|
||||
if (fromEnv) return fromEnv.replace(/\/$/, "");
|
||||
if (typeof window === "undefined") return "";
|
||||
const { protocol, hostname, port } = window.location;
|
||||
if (protocol === "tauri:" || hostname === "tauri.localhost" || hostname === "localhost" && port === "") {
|
||||
return "http://127.0.0.1:8788";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const API = resolveApiBase();
|
||||
|
||||
/** C2:token 存在时统一带 Authorization 头;token 为空(本机 dev)行为不变 */
|
||||
function authHeaders(): Record<string, string> {
|
||||
const token = injectedToken();
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
async function post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {
|
||||
const res = await fetch(`${API}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...authHeaders() },
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
throw new Error((data as { error?: string }).error || res.statusText || "request failed");
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
|
||||
async function get<T>(path: string, signal?: AbortSignal): Promise<T> {
|
||||
const res = await fetch(`${API}${path}`, { headers: { ...authHeaders() }, signal });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
throw new Error((data as { error?: string }).error || res.statusText || "request failed");
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
|
||||
export type ConnBody = {
|
||||
driver?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
user?: string;
|
||||
password?: string;
|
||||
database?: string;
|
||||
service?: string;
|
||||
ssl?: boolean;
|
||||
sslMode?: string;
|
||||
sslCA?: string;
|
||||
sslCert?: string;
|
||||
sslKey?: string;
|
||||
sessionId?: string;
|
||||
sql?: string;
|
||||
table?: string;
|
||||
limit?: number;
|
||||
/** E1:执行请求标识,配合 POST /api/db/cancel 取消在途语句 */
|
||||
requestId?: string;
|
||||
/** E2:单语句执行超时(毫秒),0/缺省 = 服务端默认不额外限制 */
|
||||
timeoutMs?: number;
|
||||
/** E6:对象搜索关键字 */
|
||||
keyword?: string;
|
||||
/** E7:服务端导出格式 */
|
||||
format?: string;
|
||||
/** C5:结果分页,跳过前 offset 行 */
|
||||
offset?: number;
|
||||
/** C1:前端已完成危险语句确认后重发时置 true */
|
||||
confirmDanger?: boolean;
|
||||
key?: string;
|
||||
pattern?: string;
|
||||
cursor?: number;
|
||||
redisMode?: string;
|
||||
redisNodes?: string;
|
||||
masterName?: string;
|
||||
sentinelPassword?: string;
|
||||
sshEnabled?: boolean;
|
||||
sshHost?: string;
|
||||
sshPort?: number;
|
||||
sshUser?: string;
|
||||
sshAuth?: string;
|
||||
sshPassword?: string;
|
||||
sshPrivateKey?: string;
|
||||
sshPassphrase?: string;
|
||||
};
|
||||
|
||||
export function fullConnPayload(c: SavedConnection): ConnBody {
|
||||
const sslOn = !!(c.ssl || (c.sslMode && c.sslMode !== "disable"));
|
||||
return {
|
||||
driver: c.driver,
|
||||
host: c.host,
|
||||
port: c.port,
|
||||
user: c.user,
|
||||
password: decodeSecret(c.password),
|
||||
database: c.database,
|
||||
service: c.service,
|
||||
ssl: sslOn,
|
||||
sslMode: sslOn ? (c.sslMode && c.sslMode !== "disable" ? c.sslMode : "require") : "disable",
|
||||
sslCA: c.sslCA || "",
|
||||
sslCert: c.sslCert || "",
|
||||
sslKey: c.sslKey || "",
|
||||
redisMode: c.redisMode || "standalone",
|
||||
redisNodes: c.redisNodes || "",
|
||||
masterName: c.masterName || "",
|
||||
sentinelPassword: c.sentinelPassword ? decodeSecret(c.sentinelPassword) : "",
|
||||
sshEnabled: !!c.sshEnabled,
|
||||
sshHost: c.sshHost,
|
||||
sshPort: c.sshPort,
|
||||
sshUser: c.sshUser,
|
||||
sshAuth: c.sshAuth,
|
||||
sshPassword: c.sshPassword ? decodeSecret(c.sshPassword) : "",
|
||||
sshPrivateKey: c.sshPrivateKey,
|
||||
sshPassphrase: c.sshPassphrase ? decodeSecret(c.sshPassphrase) : "",
|
||||
};
|
||||
}
|
||||
|
||||
export function sessionPayload(c: SavedConnection, sessionId: string, extra?: Partial<ConnBody>): ConnBody {
|
||||
return {
|
||||
sessionId,
|
||||
database: extra?.database ?? c.database,
|
||||
sql: extra?.sql,
|
||||
table: extra?.table,
|
||||
limit: extra?.limit,
|
||||
offset: extra?.offset,
|
||||
confirmDanger: extra?.confirmDanger,
|
||||
key: extra?.key,
|
||||
pattern: extra?.pattern,
|
||||
service: extra?.service ?? c.service,
|
||||
requestId: extra?.requestId,
|
||||
timeoutMs: extra?.timeoutMs,
|
||||
keyword: extra?.keyword,
|
||||
format: extra?.format,
|
||||
};
|
||||
}
|
||||
|
||||
export async function testConnection(c: SavedConnection) {
|
||||
return post<{ ok: boolean; message?: string }>("/api/db/test-connection", fullConnPayload(c));
|
||||
}
|
||||
|
||||
export async function openSession(c: SavedConnection) {
|
||||
return post<{ ok: boolean; sessionId: string; message?: string }>("/api/db/open", fullConnPayload(c));
|
||||
}
|
||||
|
||||
export async function closeSession(sessionId: string) {
|
||||
return post<{ ok: boolean }>("/api/db/close", { sessionId });
|
||||
}
|
||||
|
||||
function body(c: SavedConnection, sessionId: string | undefined, extra?: Partial<ConnBody>): ConnBody {
|
||||
if (sessionId) return sessionPayload(c, sessionId, extra);
|
||||
return { ...fullConnPayload(c), ...extra };
|
||||
}
|
||||
|
||||
export async function listDatabases(c: SavedConnection, sessionId?: string) {
|
||||
return post<{ databases: string[] }>("/api/db/databases", body(c, sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* C8:schema 缓存 —— connId → database → table → columns。
|
||||
* listTables/listColumns 成功时写入,供 SqlEditor 补全与 AiPanel 上下文注入共用。
|
||||
*/
|
||||
const schemaCache = new Map<string, Map<string, Map<string, ColumnInfo[]>>>();
|
||||
|
||||
function cacheTables(connId: string, db: string, tables: string[]) {
|
||||
let dbMap = schemaCache.get(connId);
|
||||
if (!dbMap) {
|
||||
dbMap = new Map();
|
||||
schemaCache.set(connId, dbMap);
|
||||
}
|
||||
let tblMap = dbMap.get(db);
|
||||
if (!tblMap) {
|
||||
tblMap = new Map();
|
||||
dbMap.set(db, tblMap);
|
||||
}
|
||||
for (const t of tables) {
|
||||
if (!tblMap.has(t)) tblMap.set(t, []);
|
||||
}
|
||||
}
|
||||
|
||||
function cacheColumns(connId: string, db: string, table: string, columns: ColumnInfo[]) {
|
||||
let dbMap = schemaCache.get(connId);
|
||||
if (!dbMap) {
|
||||
dbMap = new Map();
|
||||
schemaCache.set(connId, dbMap);
|
||||
}
|
||||
let tblMap = dbMap.get(db);
|
||||
if (!tblMap) {
|
||||
tblMap = new Map();
|
||||
dbMap.set(db, tblMap);
|
||||
}
|
||||
tblMap.set(table, columns);
|
||||
}
|
||||
|
||||
/** 连接关闭/删除后丢弃其缓存,避免跨连接串数据 */
|
||||
export function dropSchemaCache(connId: string) {
|
||||
schemaCache.delete(connId);
|
||||
}
|
||||
|
||||
/** SqlEditor 补全数据源:已加载的表与列 */
|
||||
export function getCachedSchema(
|
||||
connId: string | null | undefined,
|
||||
): Map<string, Map<string, ColumnInfo[]>> | null {
|
||||
if (!connId) return null;
|
||||
return schemaCache.get(connId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* AiPanel 上下文注入:把已加载的库表列拼成文本,按 connId 隔离,截断 4000 字符。
|
||||
* 优先输出当前库,其余库只列库.表名。
|
||||
*/
|
||||
export function schemaContextFor(connId: string | null | undefined, currentDb?: string): string {
|
||||
const dbMap = connId ? schemaCache.get(connId) : undefined;
|
||||
if (!dbMap || dbMap.size === 0) return "";
|
||||
const MAX = 4000;
|
||||
const lines: string[] = [];
|
||||
const dbs = [...dbMap.keys()].sort((a, b) => {
|
||||
if (a === currentDb) return -1;
|
||||
if (b === currentDb) return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
for (const db of dbs) {
|
||||
const tblMap = dbMap.get(db)!;
|
||||
for (const [table, cols] of tblMap) {
|
||||
const colText = cols.length
|
||||
? `(${cols.map((c) => `${c.name} ${c.type}`.trim()).join(", ")})`
|
||||
: "";
|
||||
lines.push(`${db}.${table}${colText}`);
|
||||
if (lines.join("\n").length > MAX) {
|
||||
return lines.join("\n").slice(0, MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.join("\n").slice(0, MAX);
|
||||
}
|
||||
|
||||
export async function listTables(c: SavedConnection, database: string, sessionId?: string) {
|
||||
const res = await post<{ tables: string[] }>("/api/db/tables", body(c, sessionId, { database }));
|
||||
if (Array.isArray(res.tables)) cacheTables(c.id, database, res.tables);
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function listColumns(c: SavedConnection, database: string, table: string, sessionId?: string) {
|
||||
const res = await post<{ columns: ColumnInfo[] }>("/api/db/columns", body(c, sessionId, { database, table }));
|
||||
if (Array.isArray(res.columns)) cacheColumns(c.id, database, table, res.columns);
|
||||
return res;
|
||||
}
|
||||
|
||||
/** E1:最近一次 execSql 的 requestId,供 cancelExec() 取消在途语句 */
|
||||
let lastExecRequestId: string | undefined;
|
||||
|
||||
export async function execSql(
|
||||
c: SavedConnection,
|
||||
sql: string,
|
||||
opts?: {
|
||||
database?: string;
|
||||
sessionId?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
confirmDanger?: boolean;
|
||||
/** E2:单语句执行超时(毫秒),0/缺省 = 不额外限制 */
|
||||
timeoutMs?: number;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
) {
|
||||
// E1:每次执行生成独立 requestId(模块级记录最后一次),取消端点按 id 精准取消
|
||||
const requestId = crypto.randomUUID();
|
||||
lastExecRequestId = requestId;
|
||||
return post<QueryResult>(
|
||||
"/api/db/exec",
|
||||
body(c, opts?.sessionId, {
|
||||
sql,
|
||||
database: opts?.database || c.database,
|
||||
limit: opts?.limit,
|
||||
offset: opts?.offset,
|
||||
confirmDanger: opts?.confirmDanger,
|
||||
requestId,
|
||||
timeoutMs: opts?.timeoutMs,
|
||||
}),
|
||||
opts?.signal,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* E1:取消最近一次执行 —— fire-and-forget 调 POST /api/db/cancel。
|
||||
* 与前端 abort 互补:服务端收到取消后释放连接/杀查询,错误一律吞掉。
|
||||
*/
|
||||
export function cancelExec(): void {
|
||||
const rid = lastExecRequestId;
|
||||
if (!rid) return;
|
||||
void fetch(`${API}/api/db/cancel`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...authHeaders() },
|
||||
body: JSON.stringify({ requestId: rid }),
|
||||
}).catch(() => {
|
||||
/* 取消失败不影响本地中止流程 */
|
||||
});
|
||||
}
|
||||
|
||||
/** E5:查看表/视图 DDL(方言差异由服务端处理) */
|
||||
export async function getDdl(
|
||||
c: SavedConnection,
|
||||
table: string,
|
||||
opts?: { database?: string; sessionId?: string },
|
||||
) {
|
||||
return post<{ ddl?: string; error?: string }>(
|
||||
"/api/db/ddl",
|
||||
body(c, opts?.sessionId, { table, database: opts?.database || c.database }),
|
||||
);
|
||||
}
|
||||
|
||||
export type SearchObjectHit = { schema: string; name: string; type: "table" | "view" };
|
||||
|
||||
/** E6:跨库对象搜索(服务端 LIKE,大小写策略由驱动决定) */
|
||||
export async function searchObjects(
|
||||
c: SavedConnection,
|
||||
keyword: string,
|
||||
opts?: { database?: string; sessionId?: string },
|
||||
) {
|
||||
return post<{ results: SearchObjectHit[]; error?: string }>(
|
||||
"/api/db/search",
|
||||
body(c, opts?.sessionId, { keyword, database: opts?.database || c.database }),
|
||||
);
|
||||
}
|
||||
|
||||
export type ExportRequest = {
|
||||
format: "csv" | "json" | "insert";
|
||||
/** 自定义查询导出(单条 SELECT);命中危险/非 SELECT 时服务端返回 needConfirm */
|
||||
sql?: string;
|
||||
/** 整表导出 */
|
||||
table?: string;
|
||||
limit?: number;
|
||||
database?: string;
|
||||
sessionId?: string;
|
||||
/** C1:危险确认二段流重发时置 true */
|
||||
confirmDanger?: boolean;
|
||||
};
|
||||
|
||||
/** E7 服务端导出失败:Error 上挂 needConfirm,由调用侧弹二段确认后重发 */
|
||||
export type ExportError = Error & { needConfirm?: boolean };
|
||||
|
||||
/**
|
||||
* E7:服务端导出 —— 文件流下载(Content-Disposition attachment)。
|
||||
* 返回 blob 与解析出的文件名;失败时抛 ExportError(needConfirm=true 表示需二段确认)。
|
||||
*/
|
||||
export async function exportData(c: SavedConnection, opts: ExportRequest): Promise<{ blob: Blob; filename?: string }> {
|
||||
const res = await fetch(`${API}/api/db/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...authHeaders() },
|
||||
body: JSON.stringify(
|
||||
body(c, opts.sessionId, {
|
||||
format: opts.format,
|
||||
sql: opts.sql,
|
||||
table: opts.table,
|
||||
limit: opts.limit,
|
||||
database: opts.database || c.database,
|
||||
confirmDanger: opts.confirmDanger,
|
||||
}),
|
||||
),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
const err = new Error((data as { error?: string }).error || res.statusText || "导出失败") as ExportError;
|
||||
if ((data as { needConfirm?: boolean }).needConfirm) err.needConfirm = true;
|
||||
throw err;
|
||||
}
|
||||
const cd = res.headers.get("Content-Disposition") || "";
|
||||
const m = /filename\*?=(?:UTF-8''|\")?([^";]+)/i.exec(cd);
|
||||
const filename = m ? decodeURIComponent(m[1].replace(/"$/, "").trim()) : undefined;
|
||||
const blob = await res.blob();
|
||||
return { blob, filename };
|
||||
}
|
||||
|
||||
/** C3:服务端专用执行计划端点(方言包装在服务端完成,前端不再拼 EXPLAIN) */
|
||||
export type ExplainResult = {
|
||||
columns?: string[];
|
||||
rows?: Record<string, unknown>[];
|
||||
/** MSSQL SHOWPLAN_XML / Oracle 文本计划 */
|
||||
text?: string;
|
||||
timeMs: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export async function explainSql(
|
||||
c: SavedConnection,
|
||||
sql: string,
|
||||
opts?: { database?: string; sessionId?: string; signal?: AbortSignal },
|
||||
) {
|
||||
return post<ExplainResult>(
|
||||
"/api/db/explain",
|
||||
body(c, opts?.sessionId, { sql, database: opts?.database || c.database }),
|
||||
opts?.signal,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export type RedisKeyInfo = { name: string; type: string; ttl: number };
|
||||
|
||||
export type RedisKeysResult = {
|
||||
keys: RedisKeyInfo[];
|
||||
truncated?: boolean;
|
||||
timeMs?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type RedisKeyValue = {
|
||||
name: string;
|
||||
type: string;
|
||||
ttl: number;
|
||||
columns: string[];
|
||||
rows: Record<string, unknown>[];
|
||||
rowCount: number;
|
||||
timeMs: number;
|
||||
truncated?: boolean;
|
||||
limit?: number;
|
||||
/** >0 表示还有更多;续载时作为 cursor 回传 */
|
||||
nextCursor?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export async function listRedisKeys(
|
||||
c: SavedConnection,
|
||||
database: string,
|
||||
opts?: { sessionId?: string; pattern?: string; limit?: number },
|
||||
) {
|
||||
return post<RedisKeysResult>(
|
||||
"/api/db/redis/keys",
|
||||
body(c, opts?.sessionId, { database, pattern: opts?.pattern, limit: opts?.limit, driver: c.driver }),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getRedisKey(
|
||||
c: SavedConnection,
|
||||
database: string,
|
||||
key: string,
|
||||
opts?: { sessionId?: string; limit?: number; cursor?: number },
|
||||
) {
|
||||
return post<RedisKeyValue>(
|
||||
"/api/db/redis/key",
|
||||
body(c, opts?.sessionId, { database, key, table: key, limit: opts?.limit, cursor: opts?.cursor, driver: c.driver }),
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteRedisKey(
|
||||
c: SavedConnection,
|
||||
database: string,
|
||||
key: string,
|
||||
opts?: { sessionId?: string },
|
||||
) {
|
||||
return post<QueryResult>(
|
||||
"/api/db/redis/key/delete",
|
||||
body(c, opts?.sessionId, { database, key, table: key, driver: c.driver }),
|
||||
);
|
||||
}
|
||||
|
||||
/** 目录(前缀)批量删除:pattern 如 `agent:*`;后端 SCAN+UNLINK,拒全库与超上限 */
|
||||
export async function deleteRedisPattern(
|
||||
c: SavedConnection,
|
||||
database: string,
|
||||
pattern: string,
|
||||
opts?: { sessionId?: string },
|
||||
) {
|
||||
return post<{ deleted: number; scanned: number; truncated: boolean; error?: string; timeMs?: number }>(
|
||||
"/api/db/redis/del-pattern",
|
||||
body(c, opts?.sessionId, { database, pattern, driver: c.driver }),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export async function loadTree(c: SavedConnection, sessionId?: string): Promise<TreeNode[]> {
|
||||
const { databases } = await listDatabases(c, sessionId);
|
||||
return databases.map((name) => ({ name, kind: "database" as const }));
|
||||
}
|
||||
|
||||
export type AiAction =
|
||||
| "ask"
|
||||
| "generate"
|
||||
| "explain"
|
||||
| "optimize"
|
||||
| "fix"
|
||||
| "dialect"
|
||||
| "sample";
|
||||
|
||||
export async function callAi(payload: {
|
||||
action: AiAction;
|
||||
prompt?: string;
|
||||
sql?: string;
|
||||
driver?: string;
|
||||
database?: string;
|
||||
error?: string;
|
||||
/** C4/C8:前端注入的库表列上下文(截断 4000 字符) */
|
||||
schema?: string;
|
||||
model?: string;
|
||||
}) {
|
||||
return post<{ content: string; sql?: string }>("/api/ai/chat", payload);
|
||||
}
|
||||
|
||||
/** C4:AI 配置(key 脱敏,只回掩码与是否已配置) */
|
||||
export type AiConfig = {
|
||||
provider: string;
|
||||
baseURL: string;
|
||||
model: string;
|
||||
hasKey: boolean;
|
||||
apiKey?: string;
|
||||
};
|
||||
|
||||
export async function getAiConfig(signal?: AbortSignal) {
|
||||
return get<AiConfig>("/api/ai/config", signal);
|
||||
}
|
||||
|
||||
export async function saveAiConfig(cfg: { provider: string; baseURL: string; apiKey?: string; model: string }) {
|
||||
return post<{ ok: boolean }>("/api/ai/config", cfg);
|
||||
}
|
||||
|
||||
export async function health() {
|
||||
const res = await fetch(`${API}/api/health`, { headers: { ...authHeaders() } });
|
||||
if (!res.ok) throw new Error(`health ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** fetch AbortController / DOMException AbortError */
|
||||
export function isAbortError(e: unknown): boolean {
|
||||
if (!e || typeof e !== "object") return false;
|
||||
const name = (e as { name?: string }).name;
|
||||
if (name === "AbortError") return true;
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return /aborted|abort/i.test(msg) && !/session/i.test(msg);
|
||||
}
|
||||
|
||||
// ---------- 驱动管理(agent 驱动) ----------
|
||||
|
||||
export type DriverJarStatus = { file: string; size: number; installed: boolean };
|
||||
|
||||
export type DriverStatusItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
runtime: string;
|
||||
license: string;
|
||||
autoFetch: boolean;
|
||||
jars: DriverJarStatus[];
|
||||
allInstalled: boolean;
|
||||
jdkRequired: boolean;
|
||||
jdkOk: boolean;
|
||||
javaVersion: string;
|
||||
agentInstalled: boolean;
|
||||
offlinePack: boolean;
|
||||
};
|
||||
|
||||
export async function driverStatus() {
|
||||
const res = await fetch(`${API}/api/drivers/status`);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error((data as { error?: string }).error || res.statusText);
|
||||
return (data as { drivers: DriverStatusItem[] }).drivers;
|
||||
}
|
||||
|
||||
export async function driverInstall(driver: string) {
|
||||
return post<{ ok: boolean; error?: string; drivers?: DriverStatusItem[] }>("/api/drivers/install", { driver });
|
||||
}
|
||||
|
||||
export async function driverImportOffline(file: File) {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
const res = await fetch(`${API}/api/drivers/import`, { method: "POST", body: form });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error((data as { error?: string }).error || res.statusText);
|
||||
return data as { ok: boolean; error?: string; imported?: number; drivers?: DriverStatusItem[] };
|
||||
}
|
||||
Reference in new issue
Block a user