Wave 3.1:老板十项返工(kimi 换代 kimi-code/授权检测/可更新判定/模型列表+测试连通/系统环境诊断/外链/命令复制/官方图标主色)
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use agentdock_adapter::{AdapterAction, DryRunPlan};
|
||||
use agentdock_core::{ActionEvent, ActionOpts, AuthFlowEvent, AuthStatus, ConfigFormState, ConfigVerifyResult, DetectResult, Engine, WriteResult};
|
||||
use agentdock_core::{ActionEvent, ActionOpts, AuthFlowEvent, AuthStatus, ConfigFormState, ConfigVerifyResult, ConnectionTestResult, DetectResult, Engine, ModelListResult, SystemEnvReport, UpdateCheckResult, WriteResult};
|
||||
use agentdock_diag::DiagnosticReport;
|
||||
use serde_json::json;
|
||||
use tauri::Emitter;
|
||||
@@ -166,3 +166,63 @@ pub fn diagnose_all(state: tauri::State<'_, Engine>) -> Result<Vec<DiagnosticRep
|
||||
let env = agentdock_platform::detect::detect_env();
|
||||
state.diagnose_all(&env).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 系统环境诊断(Wave 3.1 Req 7):检查缺失/过低的系统依赖,标注影响哪些 CLI。
|
||||
#[tauri::command(rename = "diagnoseSystem")]
|
||||
pub fn diagnose_system(state: tauri::State<'_, Engine>) -> Result<SystemEnvReport, String> {
|
||||
let env = agentdock_platform::detect::detect_env();
|
||||
state.diagnose_system(&env).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 「可更新」判定(Wave 3.1 Req 4):官方源最新版本 vs 本地已装版本对比。
|
||||
#[tauri::command(rename = "checkUpdate")]
|
||||
pub fn check_update(id: String, state: tauri::State<'_, Engine>) -> Result<UpdateCheckResult, String> {
|
||||
state.check_update(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 模型列表(Wave 3.1 Req 5)。
|
||||
#[tauri::command(rename = "listModels")]
|
||||
pub fn list_models(id: String, state: tauri::State<'_, Engine>) -> Result<ModelListResult, String> {
|
||||
state.list_models(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 测试连通(Wave 3.1 Req 5):密钥从密钥库读取直连官方接口测一次。
|
||||
#[tauri::command(rename = "testConnection")]
|
||||
pub fn test_connection(id: String, state: tauri::State<'_, Engine>) -> Result<ConnectionTestResult, String> {
|
||||
state.test_connection(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 用系统默认浏览器打开外链(Wave 3.1 Req 8),目标主机须在白名单内。
|
||||
#[tauri::command(rename = "openExternal")]
|
||||
pub fn open_external(url: String, state: tauri::State<'_, Engine>) -> Result<(), String> {
|
||||
let allowed: Vec<String> = state
|
||||
.adapters()
|
||||
.map(|ads| {
|
||||
let mut hosts: Vec<String> = ads
|
||||
.iter()
|
||||
.flat_map(|a| a.official.as_ref().map(|o| o.allowed_hosts.clone()).unwrap_or_default())
|
||||
.collect();
|
||||
hosts.extend(well_known_docs_hosts().iter().cloned());
|
||||
hosts
|
||||
})
|
||||
.unwrap_or_else(|_| well_known_docs_hosts());
|
||||
if !agentdock_core::is_url_host_allowed(&url, &allowed) {
|
||||
return Err("目标链接不在白名单内".to_string());
|
||||
}
|
||||
agentdock_core::open_with_shell(&url).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 官方文档/主页常见主机(与适配器 allowed_hosts 合并,覆盖 docs.* 站点)。
|
||||
fn well_known_docs_hosts() -> Vec<String> {
|
||||
[
|
||||
"opencode.ai", "moonshotai.github.io", "code.kimi.com", "github.com",
|
||||
"code.claude.com", "geminicli.com", "qwen.ai", "codebuddy.ai", "docs.cline.bot",
|
||||
"aider.chat", "cursor.com", "docs.warp.dev", "warp.dev", "block.github.io",
|
||||
"charm.sh", "docs.github.com", "learn.chatgpt.com", "claude.ai",
|
||||
"nodejs.org", "python.org", "git-scm.com", "docs.astral.sh", "learn.microsoft.com",
|
||||
"aka.ms", "pypi.org", "registry.npmjs.org", "api.github.com", "chatgpt.com",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -60,6 +60,11 @@ pub fn run() {
|
||||
commands::cli::cancel_authorize,
|
||||
commands::cli::diagnose,
|
||||
commands::cli::diagnose_all,
|
||||
commands::cli::diagnose_system,
|
||||
commands::cli::check_update,
|
||||
commands::cli::list_models,
|
||||
commands::cli::test_connection,
|
||||
commands::cli::open_external,
|
||||
commands::runtime::preview_runtime_install,
|
||||
commands::runtime::open_runtime_page,
|
||||
commands::runtime::install_runtime,
|
||||
|
||||
@@ -30,7 +30,29 @@ function fallbackAbbr(name: string): string {
|
||||
return name.replace(/\s+/g, "").slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
/** CLI 单色字母头像(视觉规范 §7:双字母单色,--ad-bg-2 底 + 1px --ad-border) */
|
||||
/**
|
||||
* 各工具官方图标主色(Wave 3.1 Req 10:按官方主色微调头像角标,不改动六色语义)。
|
||||
* 官方图标二进制资源待打包波由美术师统一下载本地化;此处以主色作为识别配色过渡。
|
||||
*/
|
||||
const CLI_BRAND_COLORS: Record<string, string> = {
|
||||
codex: "#10A37F",
|
||||
"claude-code": "#D97757",
|
||||
gemini: "#4285F4",
|
||||
copilot: "#8957E5",
|
||||
kimi: "#5B5BEE",
|
||||
qwen: "#615CED",
|
||||
codebuddy: "#0052D9",
|
||||
opencode: "#7C5CFF",
|
||||
crush: "#F4B860",
|
||||
goose: "#E84393",
|
||||
aider: "#FF6B6B",
|
||||
cursor: "#4A4A4A",
|
||||
cline: "#6B6BFF",
|
||||
warp: "#00E0FF",
|
||||
};
|
||||
|
||||
/** CLI 单色字母头像(视觉规范 §7:双字母单色,--ad-bg-2 底 + 1px --ad-border)
|
||||
* Wave 3.1:官方图标主色作为字母/角标识别配色(官方图标二进制待打包波本地化) */
|
||||
export function CliMonogram({
|
||||
id,
|
||||
name,
|
||||
@@ -41,10 +63,16 @@ export function CliMonogram({
|
||||
size?: number;
|
||||
}) {
|
||||
const chars = CLI_MONOGRAMS[id] ?? fallbackAbbr(name);
|
||||
const accent = CLI_BRAND_COLORS[id];
|
||||
return (
|
||||
<span
|
||||
className="monogram"
|
||||
style={{ width: size, height: size, fontSize: Math.round(size * 0.42) }}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
fontSize: Math.round(size * 0.42),
|
||||
...(accent ? { color: accent, borderColor: `${accent}66` } : {}),
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{chars}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Check, ChevronDown, CircleAlert, ExternalLink, Eye, EyeOff, Lock, ShieldCheck } from "lucide-react";
|
||||
import { readConfig, verifyConfig, writeConfig } from "../ipc";
|
||||
import { Check, ChevronDown, CircleAlert, ExternalLink, Eye, EyeOff, Lock, PlugZap, ShieldCheck } from "lucide-react";
|
||||
import { listModels, readConfig, testConnection, verifyConfig, writeConfig, openExternal } from "../ipc";
|
||||
import type {
|
||||
ConfigFieldState,
|
||||
ConfigFormState,
|
||||
ConfigVerifyResult,
|
||||
ConnectionTestResult,
|
||||
ModelListResult,
|
||||
WriteResult,
|
||||
} from "../ipc/types";
|
||||
import { AuthPanel } from "./AuthPanel";
|
||||
@@ -12,8 +14,17 @@ import { MonoChip } from "./MonoChip";
|
||||
|
||||
/** 配置表单(视觉规范 §3.4:单列 ≤640px、敏感字段密码框 + 密钥库说明、吸底保存条)
|
||||
* Wave 2.1:字段按官方配置方法渲染 + 保存后「生效检查」
|
||||
* Wave 2.2:授权/登录层改为可操作授权流程(AuthPanel) */
|
||||
export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?: () => void }) {
|
||||
* Wave 2.2:授权/登录层改为可操作授权流程(AuthPanel)
|
||||
* Wave 3.1:默认模型 → 模型列表 + 测试连通 */
|
||||
export function ConfigForm({
|
||||
id,
|
||||
onAuthChanged,
|
||||
canTestConnection = false,
|
||||
}: {
|
||||
id: string;
|
||||
onAuthChanged?: () => void;
|
||||
canTestConnection?: boolean;
|
||||
}) {
|
||||
const [state, setState] = useState<ConfigFormState | null>(null);
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
const [revealed, setRevealed] = useState<Record<string, boolean>>({});
|
||||
@@ -24,6 +35,9 @@ export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?:
|
||||
const [verify, setVerify] = useState<ConfigVerifyResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const [models, setModels] = useState<ModelListResult | null>(null);
|
||||
const [connTest, setConnTest] = useState<ConnectionTestResult | null>(null);
|
||||
const [connBusy, setConnBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -44,11 +58,29 @@ export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?:
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
listModels(id)
|
||||
.then((m) => {
|
||||
if (!cancelled) setModels(m);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
async function runTestConnection() {
|
||||
setConnBusy(true);
|
||||
setConnTest(null);
|
||||
try {
|
||||
const r = await testConnection(id);
|
||||
setConnTest(r);
|
||||
} catch (e) {
|
||||
setConnTest({ cli_id: id, ok: false, message_zh: String(e), http_status: null, detail: null });
|
||||
} finally {
|
||||
setConnBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const sensitiveSaved = useMemo(() => {
|
||||
const m: Record<string, boolean> = {};
|
||||
state?.fields.forEach((f) => {
|
||||
@@ -67,6 +99,8 @@ export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?:
|
||||
const advancedFields = state.fields.filter((f) => f.group === "advanced");
|
||||
|
||||
function renderField(field: ConfigFieldState) {
|
||||
const isModelField = field.field_type === "string" && field.id.toLowerCase().includes("model");
|
||||
const suggestions = isModelField ? (models?.models.map((m) => m.id) ?? []) : [];
|
||||
return (
|
||||
<Field
|
||||
key={field.id}
|
||||
@@ -74,6 +108,7 @@ export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?:
|
||||
value={values[field.id] ?? ""}
|
||||
saved={sensitiveSaved[field.id] ?? false}
|
||||
revealed={!!revealed[field.id]}
|
||||
suggestions={suggestions}
|
||||
onToggleReveal={() => setRevealed((r) => ({ ...r, [field.id]: !r[field.id] }))}
|
||||
onChange={(v) => setValues((x) => ({ ...x, [field.id]: v }))}
|
||||
/>
|
||||
@@ -135,6 +170,46 @@ export function ConfigForm({ id, onAuthChanged }: { id: string; onAuthChanged?:
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 第二层:模型列表 + 测试连通(Wave 3.1 Req 5) */}
|
||||
{(models && models.models.length > 0) || canTestConnection ? (
|
||||
<section className="config-section">
|
||||
<h4 className="config-section-title">模型</h4>
|
||||
{models && models.models.length > 0 ? (
|
||||
<div className="model-list">
|
||||
<div className="model-list-rows">
|
||||
{models.models.map((m) => (
|
||||
<div key={m.id} className="model-row">
|
||||
<MonoChip>{m.id}</MonoChip>
|
||||
{m.label_zh && <span className="model-row-label">{m.label_zh}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="model-list-hint">{models.detail_zh}</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="model-list-hint">该工具未提供模型清单,可在默认模型字段手动填写。</p>
|
||||
)}
|
||||
{canTestConnection && (
|
||||
<div className="model-connect">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={runTestConnection}
|
||||
disabled={connBusy}
|
||||
>
|
||||
<PlugZap size={13} strokeWidth={1.5} aria-hidden="true" />
|
||||
{connBusy ? "测试中…" : "测试连通"}
|
||||
</button>
|
||||
{connTest && (
|
||||
<span className={`connect-test-result ${connTest.ok ? "ok" : "fail"}`}>
|
||||
{connTest.message_zh}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{/* 第二层:常用配置 */}
|
||||
{commonFields.length > 0 && (
|
||||
<section className="config-section">
|
||||
@@ -222,6 +297,7 @@ function Field({
|
||||
value,
|
||||
saved,
|
||||
revealed,
|
||||
suggestions,
|
||||
onChange,
|
||||
onToggleReveal,
|
||||
}: {
|
||||
@@ -229,12 +305,14 @@ function Field({
|
||||
value: string;
|
||||
saved: boolean;
|
||||
revealed: boolean;
|
||||
suggestions?: string[];
|
||||
onChange: (v: string) => void;
|
||||
onToggleReveal: () => void;
|
||||
}) {
|
||||
const isPassword = field.sensitive;
|
||||
const isEnum = field.field_type === "enum";
|
||||
const inputType = isPassword ? (revealed ? "text" : "password") : "text";
|
||||
const datalistId = suggestions && suggestions.length > 0 ? `dl-${field.id}` : undefined;
|
||||
return (
|
||||
<div className="field">
|
||||
<label className="field-label" htmlFor={`field-${field.id}`}>
|
||||
@@ -266,12 +344,20 @@ function Field({
|
||||
id={`field-${field.id}`}
|
||||
type={inputType}
|
||||
value={value}
|
||||
list={datalistId}
|
||||
placeholder={isPassword ? (saved ? "已保存 · 留空则不变" : "输入 API Key") : ""}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{datalistId && (
|
||||
<datalist id={datalistId}>
|
||||
{suggestions!.map((m) => (
|
||||
<option key={m} value={m} />
|
||||
))}
|
||||
</datalist>
|
||||
)}
|
||||
{isPassword && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -294,7 +380,11 @@ function Field({
|
||||
</span>
|
||||
)}
|
||||
{field.docs_url && (
|
||||
<a className="field-docs-link" href={field.docs_url} target="_blank" rel="noreferrer">
|
||||
<a
|
||||
className="field-docs-link"
|
||||
href={field.docs_url}
|
||||
onClick={(e) => { e.preventDefault(); void openExternal(field.docs_url!); }}
|
||||
>
|
||||
官方文档 <ExternalLink size={11} strokeWidth={1.5} aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -10,12 +10,16 @@ import type {
|
||||
CliActionEvent,
|
||||
ConfigFormState,
|
||||
ConfigVerifyResult,
|
||||
ConnectionTestResult,
|
||||
DetectResult,
|
||||
DiagnosticReport,
|
||||
DryRunPlan,
|
||||
ModelListResult,
|
||||
PlatformEnv,
|
||||
RuntimeInstallEvent,
|
||||
RuntimeSource,
|
||||
SystemEnvReport,
|
||||
UpdateCheckResult,
|
||||
WriteResult,
|
||||
} from "./types";
|
||||
|
||||
@@ -106,6 +110,39 @@ export async function diagnoseAll(): Promise<DiagnosticReport[]> {
|
||||
return invoke<DiagnosticReport[]>("diagnoseAll");
|
||||
}
|
||||
|
||||
/** 系统环境诊断(Wave 3.1 Req 7):检查缺失/过低的系统依赖,标注影响哪些 CLI。 */
|
||||
export async function diagnoseSystem(): Promise<SystemEnvReport> {
|
||||
if (!isTauri()) return { findings: [] };
|
||||
return invoke<SystemEnvReport>("diagnoseSystem");
|
||||
}
|
||||
|
||||
/** 「可更新」判定(Wave 3.1 Req 4):官方源最新版本 vs 本地已装版本。 */
|
||||
export async function checkUpdate(id: string): Promise<UpdateCheckResult> {
|
||||
if (!isTauri()) return { cli_id: id, current: null, latest: null, update_available: false, source_zh: "官方源", source_url: "", error: null };
|
||||
return invoke<UpdateCheckResult>("checkUpdate", { id });
|
||||
}
|
||||
|
||||
/** 模型列表(Wave 3.1 Req 5)。 */
|
||||
export async function listModels(id: string): Promise<ModelListResult> {
|
||||
if (!isTauri()) return { cli_id: id, models: [], source: "empty", detail_zh: "" };
|
||||
return invoke<ModelListResult>("listModels", { id });
|
||||
}
|
||||
|
||||
/** 测试连通(Wave 3.1 Req 5):密钥从密钥库读取直连官方接口。 */
|
||||
export async function testConnection(id: string): Promise<ConnectionTestResult> {
|
||||
if (!isTauri()) return { cli_id: id, ok: false, message_zh: "浏览器预览模式不支持测试连通", http_status: null, detail: null };
|
||||
return invoke<ConnectionTestResult>("testConnection", { id });
|
||||
}
|
||||
|
||||
/** 用系统默认浏览器打开外链(Wave 3.1 Req 8)。 */
|
||||
export async function openExternal(url: string): Promise<void> {
|
||||
if (!isTauri()) {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
return invoke<void>("openExternal", { url });
|
||||
}
|
||||
|
||||
/** 软件内授权:启动指定模式的授权流程(事件经 cli-auth-event 流式回传)。 */
|
||||
export async function authorize(id: string, mode: string): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
|
||||
@@ -234,6 +234,51 @@ export interface RuntimeInstallEvent {
|
||||
fallback?: boolean;
|
||||
}
|
||||
|
||||
// ---- Wave 3.1:可更新判定 / 模型列表 / 测试连通 / 系统环境诊断 ----
|
||||
|
||||
export interface UpdateCheckResult {
|
||||
cli_id: string;
|
||||
current: string | null;
|
||||
latest: string | null;
|
||||
update_available: boolean;
|
||||
source_zh: string;
|
||||
source_url: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
label_zh: string | null;
|
||||
}
|
||||
|
||||
export interface ModelListResult {
|
||||
cli_id: string;
|
||||
models: ModelInfo[];
|
||||
source: string;
|
||||
detail_zh: string;
|
||||
}
|
||||
|
||||
export interface ConnectionTestResult {
|
||||
cli_id: string;
|
||||
ok: boolean;
|
||||
message_zh: string;
|
||||
http_status: number | null;
|
||||
detail: string | null;
|
||||
}
|
||||
|
||||
export interface SystemEnvFinding {
|
||||
runtime_id: string;
|
||||
label_zh: string;
|
||||
status: string;
|
||||
version: string | null;
|
||||
affected_cli: string[];
|
||||
message_zh: string;
|
||||
}
|
||||
|
||||
export interface SystemEnvReport {
|
||||
findings: SystemEnvFinding[];
|
||||
}
|
||||
|
||||
// ---- 适配器完整定义(getAdapter 返回,detail 页展示用) ----
|
||||
|
||||
export interface AdapterChannel {
|
||||
@@ -259,7 +304,7 @@ export interface Adapter {
|
||||
windows?: { architectures?: string[]; notes?: string | null } | null;
|
||||
linux?: { distributions?: string[]; architectures?: string[]; min_ubuntu?: string | null } | null;
|
||||
} | null;
|
||||
official?: { homepage?: string | null; docs?: string | null; allowed_hosts?: string[] } | null;
|
||||
official?: { homepage?: string | null; docs?: string | null; icon?: string | null; allowed_hosts?: string[] } | null;
|
||||
runtime_deps?: { id: string; semver_range?: string | null; required_for?: string[] }[];
|
||||
install?: { preferred?: string | null; channels: AdapterChannel[] } | null;
|
||||
detect?: {
|
||||
@@ -269,7 +314,7 @@ export interface Adapter {
|
||||
version_unconfirmed?: boolean | null;
|
||||
path_hints?: string[];
|
||||
} | null;
|
||||
update?: { method?: string | null; command?: string[] } | null;
|
||||
update?: { method?: string | null; command?: string[]; source?: { kind: string; package?: string | null; repo?: string | null } | null } | null;
|
||||
uninstall?: { method?: string | null; command?: string[]; keep_config_default?: boolean } | null;
|
||||
authorization?: {
|
||||
modes: {
|
||||
@@ -279,6 +324,12 @@ export interface Adapter {
|
||||
status_command?: string[];
|
||||
notes_zh?: string | null;
|
||||
}[];
|
||||
credential_files?: string[];
|
||||
test?: { url: string; key_header?: string | null; bearer?: boolean; extra_headers?: string[] } | null;
|
||||
} | null;
|
||||
models?: {
|
||||
command?: string[];
|
||||
list?: { id: string; label_zh?: string | null }[];
|
||||
} | null;
|
||||
configuration?: {
|
||||
files: { path: string; format: string; scope?: string | null }[];
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { useCatalog } from "../hooks/useCatalog";
|
||||
import { useDetectAll } from "../hooks/useDetectAll";
|
||||
import { checkUpdate } from "../ipc";
|
||||
import type { UpdateCheckResult } from "../ipc/types";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { CliMonogram } from "../components/CliMonogram";
|
||||
import { MonoChip } from "../components/MonoChip";
|
||||
@@ -22,13 +24,46 @@ const PLATFORM_FILTERS: { key: PlatformFilter; label: string }[] = [
|
||||
{ key: "linux", label: "Linux" },
|
||||
];
|
||||
|
||||
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml + 真机 detect) */
|
||||
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml + 真机 detect + 官方源可更新判定) */
|
||||
export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => void }) {
|
||||
const { entries, loading, error } = useCatalog();
|
||||
const { detectMap, loading: detecting } = useDetectAll();
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState<StatusFilter>("all");
|
||||
const [platform, setPlatform] = useState<PlatformFilter>("all");
|
||||
const [updates, setUpdates] = useState<Record<string, UpdateCheckResult>>({});
|
||||
const [checkingUpdates, setCheckingUpdates] = useState(false);
|
||||
|
||||
// 「可更新」筛选:仅当用户点选时才对已装工具查询官方源最新版本(Wave 3.1 Req 4)
|
||||
useEffect(() => {
|
||||
if (status !== "update" || checkingUpdates) return;
|
||||
let cancelled = false;
|
||||
setCheckingUpdates(true);
|
||||
const installedIds = entries.filter((e) => detectMap[e.id]?.status === "installed").map((e) => e.id);
|
||||
Promise.all(
|
||||
installedIds.map(async (id) => {
|
||||
try {
|
||||
return await checkUpdate(id);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then((results) => {
|
||||
if (cancelled) return;
|
||||
const m: Record<string, UpdateCheckResult> = {};
|
||||
results.forEach((r, i) => {
|
||||
if (r) m[installedIds[i]] = r;
|
||||
});
|
||||
setUpdates(m);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setCheckingUpdates(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [status, entries, detectMap, checkingUpdates]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
@@ -41,13 +76,13 @@ export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => vo
|
||||
// 状态筛选:接真机 detect 结果
|
||||
if (status === "installed" && !installed) return false;
|
||||
if (status === "uninstalled" && installed) return false;
|
||||
// 可更新:Wave 2.1 不检测可更新,恒为空(诚实)
|
||||
if (status === "update") return false;
|
||||
// 可更新:官方源最新版本 vs 本地已装版本(真实数据,Wave 3.1 Req 4)
|
||||
if (status === "update") return updates[e.id]?.update_available === true;
|
||||
// 平台:全部 14 个均支持 Windows + Ubuntu(调研底稿结论),筛选不改变集合
|
||||
if (platform === "windows" || platform === "linux") return true;
|
||||
return true;
|
||||
});
|
||||
}, [entries, query, status, platform, detectMap]);
|
||||
}, [entries, query, status, platform, detectMap, updates]);
|
||||
|
||||
return (
|
||||
<div className="catalog">
|
||||
@@ -128,11 +163,18 @@ export function CatalogPage({ onOpenDetail }: { onOpenDetail: (id: string) => vo
|
||||
)}
|
||||
<span className="catalog-platforms">Windows · Linux</span>
|
||||
</div>
|
||||
{updates[entry.id]?.update_available && (
|
||||
<div className="catalog-update">
|
||||
可更新 {updates[entry.id].latest}(依据 {updates[entry.id].source_zh})
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
{!loading && filtered.length === 0 && (
|
||||
<p className="panel-empty catalog-empty">没有符合条件的 CLI</p>
|
||||
<p className="panel-empty catalog-empty">
|
||||
{status === "update" && checkingUpdates ? "正在查询官方源最新版本…" : "没有符合条件的 CLI"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
ChevronDown,
|
||||
CircleHelp,
|
||||
ClipboardList,
|
||||
Copy,
|
||||
Loader2,
|
||||
ShieldAlert,
|
||||
Terminal,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useCliDetail } from "../hooks/useCliDetail";
|
||||
import { diagnose, onCliAction, previewAction, runAction } from "../ipc";
|
||||
import { diagnose, onCliAction, openExternal, previewAction, runAction } from "../ipc";
|
||||
import type {
|
||||
ActionEvent,
|
||||
Adapter,
|
||||
@@ -249,7 +250,7 @@ export function CliDetailPage({
|
||||
|
||||
<section className="cli-detail-body">
|
||||
{tab === "overview" && <OverviewTab adapter={adapter} detect={detect} auth={auth} />}
|
||||
{tab === "config" && <ConfigForm id={id} onAuthChanged={refresh} />}
|
||||
{tab === "config" && <ConfigForm id={id} onAuthChanged={refresh} canTestConnection={adapter.authorization?.test != null} />}
|
||||
{tab === "docs" && <DocsTab adapter={adapter} />}
|
||||
{tab === "diag" && <DiagTab id={id} />}
|
||||
</section>
|
||||
@@ -552,12 +553,20 @@ function OverviewTab({
|
||||
<h3 className="detail-block-title">官方链接</h3>
|
||||
<div className="detail-links">
|
||||
{adapter.official?.homepage && (
|
||||
<a className="detail-link" href={adapter.official.homepage} target="_blank" rel="noreferrer">
|
||||
<a
|
||||
className="detail-link"
|
||||
href={adapter.official.homepage}
|
||||
onClick={(e) => { e.preventDefault(); void openExternal(adapter.official!.homepage!); }}
|
||||
>
|
||||
官方主页
|
||||
</a>
|
||||
)}
|
||||
{adapter.official?.docs && (
|
||||
<a className="detail-link" href={adapter.official.docs} target="_blank" rel="noreferrer">
|
||||
<a
|
||||
className="detail-link"
|
||||
href={adapter.official.docs}
|
||||
onClick={(e) => { e.preventDefault(); void openExternal(adapter.official!.docs!); }}
|
||||
>
|
||||
官方文档
|
||||
</a>
|
||||
)}
|
||||
@@ -589,6 +598,37 @@ function authModeLabel(mode: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** 可复制命令块(Wave 3.1 Req 9):hover 高亮 + 点击复制 + 复制成功轻提示 */
|
||||
function CopyableCommand({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1500);
|
||||
}
|
||||
return (
|
||||
<button type="button" className="command-copy" onClick={copy} title="点击复制命令" aria-label={`复制命令 ${text}`}>
|
||||
<MonoChip>{text}</MonoChip>
|
||||
{copied ? (
|
||||
<span className="command-copied">
|
||||
<Check size={12} strokeWidth={1.5} aria-hidden="true" /> 已复制
|
||||
</span>
|
||||
) : (
|
||||
<Copy size={12} strokeWidth={1.5} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function DocsTab({ adapter }: { adapter: Adapter }) {
|
||||
const doc = adapter.documentation;
|
||||
const docUrl = adapter.official?.docs;
|
||||
@@ -622,7 +662,7 @@ function DocsTab({ adapter }: { adapter: Adapter }) {
|
||||
<ul className="detail-commands">
|
||||
{doc!.commands!.map((c) => (
|
||||
<li key={c.cmd} className="detail-command">
|
||||
<MonoChip>{c.cmd}</MonoChip>
|
||||
<CopyableCommand text={c.cmd} />
|
||||
{c.desc_zh && <span className="detail-command-desc">{c.desc_zh}</span>}
|
||||
</li>
|
||||
))}
|
||||
@@ -635,7 +675,7 @@ function DocsTab({ adapter }: { adapter: Adapter }) {
|
||||
<ul className="detail-commands">
|
||||
{doc!.params!.map((p) => (
|
||||
<li key={p.param} className="detail-command">
|
||||
<MonoChip>{p.param}</MonoChip>
|
||||
<CopyableCommand text={p.param} />
|
||||
{p.desc_zh && <span className="detail-command-desc">{p.desc_zh}</span>}
|
||||
</li>
|
||||
))}
|
||||
@@ -654,7 +694,11 @@ function DocsTab({ adapter }: { adapter: Adapter }) {
|
||||
)}
|
||||
<div className="detail-block detail-doc-footer">
|
||||
{docUrl && (
|
||||
<a className="detail-link" href={docUrl} target="_blank" rel="noreferrer">
|
||||
<a
|
||||
className="detail-link"
|
||||
href={docUrl}
|
||||
onClick={(e) => { e.preventDefault(); void openExternal(docUrl); }}
|
||||
>
|
||||
官方文档原文
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Activity, Archive, Download, Loader2, Lock, Plus, ScanSearch } from "lucide-react";
|
||||
import { Activity, Archive, Cpu, Download, Loader2, Lock, Plus, ScanSearch } from "lucide-react";
|
||||
import { useEnv } from "../hooks/useEnv";
|
||||
import { useCatalog } from "../hooks/useCatalog";
|
||||
import { useDetectAll } from "../hooks/useDetectAll";
|
||||
import { diagnoseAll } from "../ipc";
|
||||
import { diagnoseAll, diagnoseSystem } from "../ipc";
|
||||
import type {
|
||||
CatalogEntry,
|
||||
DetectResult,
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
PlatformEnv,
|
||||
RuntimeInfo,
|
||||
RuntimeStatus,
|
||||
SystemEnvReport,
|
||||
} from "../ipc/types";
|
||||
import { KpiCard } from "../components/KpiCard";
|
||||
import { MonoChip } from "../components/MonoChip";
|
||||
@@ -82,8 +83,8 @@ function RuntimeItem({
|
||||
<span className="runtime-missing">{meta.missingText}</span>
|
||||
)}
|
||||
{meta.needsInstall && (
|
||||
<button type="button" className="link-btn runtime-action" onClick={() => onInstall(name)}>
|
||||
<Download size={12} strokeWidth={1.5} aria-hidden="true" /> 安装
|
||||
<button type="button" className="btn btn-secondary btn-sm runtime-action" onClick={() => onInstall(name)}>
|
||||
<Download size={13} strokeWidth={1.5} aria-hidden="true" /> 安装
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -92,8 +93,10 @@ function RuntimeItem({
|
||||
|
||||
function EnvPanel({
|
||||
onInstallRuntime,
|
||||
onSystemDiagnose,
|
||||
}: {
|
||||
onInstallRuntime: (runtime: string) => void;
|
||||
onSystemDiagnose: () => void;
|
||||
}) {
|
||||
const { env, loading } = useEnv();
|
||||
const [showAllPath, setShowAllPath] = useState(false);
|
||||
@@ -132,6 +135,9 @@ function EnvPanel({
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">本机环境</h2>
|
||||
{loading && <span className="panel-hint">刷新中…</span>}
|
||||
<button type="button" className="btn btn-secondary btn-sm env-diag-btn" onClick={onSystemDiagnose}>
|
||||
<Cpu size={13} strokeWidth={1.5} aria-hidden="true" /> 系统环境诊断
|
||||
</button>
|
||||
</div>
|
||||
<div className="env-grid">
|
||||
<div className="env-basic">
|
||||
@@ -408,6 +414,9 @@ export function OverviewPage({ onNavigate, onOpenDetail, pendingRuntime, onRunti
|
||||
const [diagOpen, setDiagOpen] = useState(false);
|
||||
const [diagReports, setDiagReports] = useState<DiagnosticReport[] | null>(null);
|
||||
const [diagBusy, setDiagBusy] = useState(false);
|
||||
const [sysDiagOpen, setSysDiagOpen] = useState(false);
|
||||
const [sysDiag, setSysDiag] = useState<SystemEnvReport | null>(null);
|
||||
const [sysDiagBusy, setSysDiagBusy] = useState(false);
|
||||
|
||||
// 外部跳转(详情页错误内联按钮「去安装 Node.js」)
|
||||
useEffect(() => {
|
||||
@@ -431,6 +440,21 @@ export function OverviewPage({ onNavigate, onOpenDetail, pendingRuntime, onRunti
|
||||
}
|
||||
}
|
||||
|
||||
async function runSystemDiagnose() {
|
||||
setSysDiagOpen(true);
|
||||
setSysDiagBusy(true);
|
||||
setSysDiag(null);
|
||||
try {
|
||||
const report = await diagnoseSystem();
|
||||
setSysDiag(report);
|
||||
} catch (e) {
|
||||
setSysDiag({ findings: [] });
|
||||
void e;
|
||||
} finally {
|
||||
setSysDiagBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const installedCount = entries.filter((e) => detectMap[e.id]?.status === "installed").length;
|
||||
// 检测未完成(首次、无缓存)时不得判空态
|
||||
const ready = !detecting && !catalogLoading;
|
||||
@@ -495,7 +519,7 @@ export function OverviewPage({ onNavigate, onOpenDetail, pendingRuntime, onRunti
|
||||
)}
|
||||
|
||||
{/* ④ 本机环境(真实检测结果,架构 §5;超宽断点上提为第三栏) */}
|
||||
<EnvPanel onInstallRuntime={setRuntimeInstall} />
|
||||
<EnvPanel onInstallRuntime={setRuntimeInstall} onSystemDiagnose={runSystemDiagnose} />
|
||||
|
||||
{/* 运行时一键安装弹窗 */}
|
||||
{runtimeInstall && (
|
||||
@@ -508,6 +532,13 @@ export function OverviewPage({ onNavigate, onOpenDetail, pendingRuntime, onRunti
|
||||
<DiagSummary busy={diagBusy} reports={diagReports} />
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* 系统环境诊断弹窗(Wave 3.1 Req 7) */}
|
||||
{sysDiagOpen && (
|
||||
<Modal title="系统环境诊断" onClose={() => setSysDiagOpen(false)}>
|
||||
<SystemDiagSummary busy={sysDiagBusy} report={sysDiag} onInstall={setRuntimeInstall} />
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -559,3 +590,72 @@ function DiagSummary({ busy, reports }: { busy: boolean; reports: DiagnosticRepo
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 系统环境诊断(Wave 3.1 Req 7):每项标注影响哪些 CLI,缺失项带一键安装 */
|
||||
function SystemDiagSummary({
|
||||
busy,
|
||||
report,
|
||||
onInstall,
|
||||
}: {
|
||||
busy: boolean;
|
||||
report: SystemEnvReport | null;
|
||||
onInstall: (runtime: string) => void;
|
||||
}) {
|
||||
if (busy) {
|
||||
return (
|
||||
<p className="panel-empty">
|
||||
<Loader2 size={14} strokeWidth={1.5} className="spin" aria-hidden="true" /> 正在检查系统环境依赖…
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (!report) return null;
|
||||
const problems = report.findings.filter((f) => f.status !== "installed");
|
||||
const allOk = problems.length === 0;
|
||||
return (
|
||||
<div className="sys-diag">
|
||||
<div className="diag-summary">
|
||||
{allOk ? (
|
||||
<>
|
||||
<span className="status-dot ok" aria-hidden="true" />
|
||||
<span>系统环境依赖完整,无缺失</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="status-dot warn" aria-hidden="true" />
|
||||
<span>发现 {problems.length} 项环境依赖缺失或版本过低</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{report.findings.map((f) => {
|
||||
const missing = f.status !== "installed";
|
||||
return (
|
||||
<div key={f.runtime_id} className="sys-diag-item">
|
||||
<span className={`status-dot ${missing ? "warn" : "ok"}`} aria-hidden="true" />
|
||||
<div className="sys-diag-main">
|
||||
<div className="sys-diag-head">
|
||||
<span className="sys-diag-name">{f.label_zh}</span>
|
||||
{f.version && <MonoChip>{f.version}</MonoChip>}
|
||||
<span className={`sys-diag-status ${missing ? "warn" : ""}`}>
|
||||
{missing ? "缺失 / 需更新" : "正常"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="sys-diag-msg">{f.message_zh}</p>
|
||||
{f.affected_cli.length > 0 && (
|
||||
<p className="sys-diag-affected">影响:{f.affected_cli.join(" · ")}</p>
|
||||
)}
|
||||
</div>
|
||||
{missing && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => onInstall(f.runtime_id)}
|
||||
>
|
||||
<Download size={13} strokeWidth={1.5} aria-hidden="true" /> 安装
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -288,6 +288,14 @@ button {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 紧凑尺寸(本机环境内联「安装」等) */
|
||||
.btn-sm {
|
||||
height: 26px;
|
||||
min-width: 0;
|
||||
padding: 0 var(--ad-space-3);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
/* 主按钮(§2.6 v1.2:内渐变 + 内高光 + 常驻弱辉光,hover 双层扩散辉光) */
|
||||
.btn-primary {
|
||||
background: var(--ad-btn-primary-bg);
|
||||
@@ -1412,6 +1420,7 @@ button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.detail-command-desc {
|
||||
@@ -1419,6 +1428,38 @@ button {
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
/* 可复制命令块(Wave 3.1 Req 9):hover 高亮 + 点击复制 */
|
||||
.command-copy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--ad-text-2);
|
||||
border-radius: var(--ad-radius-s);
|
||||
transition: color 120ms ease, background 120ms ease;
|
||||
}
|
||||
|
||||
.command-copy:hover {
|
||||
color: var(--ad-primary);
|
||||
background: var(--ad-bg-1);
|
||||
}
|
||||
|
||||
.command-copy:hover .mono-chip {
|
||||
border-color: var(--ad-primary);
|
||||
color: var(--ad-primary);
|
||||
}
|
||||
|
||||
.command-copied {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: var(--ad-text-xs-size);
|
||||
color: var(--ad-success);
|
||||
}
|
||||
|
||||
/* ---------- 诊断结果(§3.5) ---------- */
|
||||
.diag-summary {
|
||||
display: flex;
|
||||
@@ -2208,11 +2249,11 @@ button {
|
||||
|
||||
|
||||
/* ============================================================
|
||||
* Wave 2.2£ºÊµÊ±Êä³ö / Èí¼þÄÚÊÚȨ / ÒÀÀµÒ»¼ü×° / ¼ÓÔØ¹Ç¼Ü
|
||||
* È«²¿ÑÕÉ«À´×Ô tokens/
|
||||
* Wave 2.2��ʵʱ��� / ��������Ȩ / ����һ��װ / ���عǼ�
|
||||
* ȫ����ɫ���� tokens/
|
||||
* ============================================================ */
|
||||
|
||||
/* ---------- ʧ°Ü̬ÈË»°»¯£¨°²×°/Ð¶ÔØµ¯´°£© ---------- */
|
||||
/* ---------- ʧ��̬�˻�������װ/ж�ص����� ---------- */
|
||||
.run-failure {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -2245,7 +2286,7 @@ button {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ---------- Èí¼þÄÚÊÚȨ£¨AuthPanel£© ---------- */
|
||||
/* ---------- ��������Ȩ��AuthPanel�� ---------- */
|
||||
.auth-mode-action {
|
||||
height: 28px;
|
||||
min-width: 0;
|
||||
@@ -2355,7 +2396,7 @@ button {
|
||||
color: var(--ad-attention);
|
||||
}
|
||||
|
||||
/* ---------- ±¾»ú»·¾³Ò»¼ü°²×°£¨RuntimeInstallModal£© ---------- */
|
||||
/* ---------- ��������һ����װ��RuntimeInstallModal�� ---------- */
|
||||
.runtime-install {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2403,7 +2444,7 @@ button {
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
/* ---------- ×ÜÀÀ¼ÓÔØ¹Ç¼ÜÆÁ£¨Wave 2.2 Req 4£© ---------- */
|
||||
/* ---------- �������عǼ�����Wave 2.2 Req 4�� ---------- */
|
||||
.overview-skeleton {
|
||||
padding: var(--ad-space-5);
|
||||
background: var(--ad-bg-1);
|
||||
@@ -2468,7 +2509,7 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- È«Á¿Õï¶Ï»ã×Ü ---------- */
|
||||
/* ---------- ȫ����ϻ��� ---------- */
|
||||
.diag-summary-all {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2504,7 +2545,7 @@ button {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* È«Á¿Õï¶Ï»ã×ÜÀïµÄ¼¶±ðÎÄ×Ö±êÇ©£¨½öÎÄ×ÖÉ«£¬²»Óà diag-sev-tag µÄÌî³äÉ«£¬±ÜÃâÎÄ×Ö±»±³¾°Í̵ô£© */
|
||||
/* ȫ����ϻ�����ļ������ֱ�ǩ��������ɫ������ diag-sev-tag �����ɫ���������ֱ������̵��� */
|
||||
.diag-sev-label {
|
||||
flex-shrink: 0;
|
||||
font-size: var(--ad-text-xs-size);
|
||||
@@ -2521,3 +2562,125 @@ button {
|
||||
.diag-sev-label.info {
|
||||
color: var(--ad-attention);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Wave 3.1:外链 / 命令复制 / 依赖内联安装 / 系统环境诊断 / 模型列表
|
||||
* 全部取色取字走 tokens/
|
||||
* ============================================================ */
|
||||
|
||||
/* ---------- 本机环境头部:系统环境诊断入口 ---------- */
|
||||
.env-diag-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ---------- 系统环境诊断(Req 7) ---------- */
|
||||
.sys-diag {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-3);
|
||||
}
|
||||
|
||||
.sys-diag-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-3);
|
||||
padding: var(--ad-space-4);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-m);
|
||||
background: var(--ad-bg-1);
|
||||
}
|
||||
|
||||
.sys-diag-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sys-diag-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.sys-diag-name {
|
||||
color: var(--ad-text-1);
|
||||
font-size: var(--ad-text-m-size);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sys-diag-status {
|
||||
color: var(--ad-success);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.sys-diag-status.warn {
|
||||
color: var(--ad-warning);
|
||||
}
|
||||
|
||||
.sys-diag-msg {
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-s-size);
|
||||
line-height: 1.5;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.sys-diag-affected {
|
||||
color: var(--ad-text-3);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ---------- 模型列表 + 测试连通(Req 5) ---------- */
|
||||
.model-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.model-list-hint {
|
||||
color: var(--ad-text-3);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.model-list-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.model-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ad-space-2);
|
||||
}
|
||||
|
||||
.model-row-label {
|
||||
color: var(--ad-text-2);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
.connect-test-result {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ad-space-2);
|
||||
padding: var(--ad-space-3);
|
||||
border: 1px solid var(--ad-border);
|
||||
border-radius: var(--ad-radius-m);
|
||||
font-size: var(--ad-text-s-size);
|
||||
}
|
||||
|
||||
.connect-test-result.ok {
|
||||
color: var(--ad-success);
|
||||
}
|
||||
|
||||
.connect-test-result.fail {
|
||||
color: var(--ad-warning);
|
||||
}
|
||||
|
||||
/* ---------- 目录卡片「可更新」角标(Req 4) ---------- */
|
||||
.catalog-update {
|
||||
margin-top: var(--ad-space-2);
|
||||
padding-top: var(--ad-space-2);
|
||||
border-top: 1px solid var(--ad-border);
|
||||
color: var(--ad-accent);
|
||||
font-size: var(--ad-text-xs-size);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user