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
50 changes: 35 additions & 15 deletions packages/plugin/console/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,24 +119,34 @@ window.__ModuleLoader__.load({
lineHeight: "18px",
color: "var(--dsw-alias-state-success-primary)"
};
/** 版本行:v当前 · latest(可更新时高亮);本地/非 registry 包无 latest。 */
function versionText(plugin, latest, checked) {
/** 版本行:v当前 · latest(可更新时高亮);本地/非 registry 包无 latest;
* 检查失败(error 非空)显示失败原因,与「本地包」区分开。 */
function versionText(plugin, latest, checked, error) {
const current = plugin.version === void 0 ? "?" : `v${plugin.version}`;
if (!checked) return {
text: `${current} · 待检查`,
canUpdate: false
canUpdate: false,
failed: false
};
if (error !== null && error !== void 0) return {
text: `${current} · 检查失败(${error})`,
canUpdate: false,
failed: true
};
if (latest === null) return {
text: `${current} · 本地`,
canUpdate: false
text: `${current} · 本地/非 registry`,
canUpdate: false,
failed: false
};
if (latest === plugin.version) return {
text: `${current} · 已最新`,
canUpdate: false
canUpdate: false,
failed: false
};
return {
text: `${current} → v${latest}`,
canUpdate: true
canUpdate: true,
failed: false
};
}
/**
Expand Down Expand Up @@ -165,6 +175,7 @@ window.__ModuleLoader__.load({
const [installMsg, setInstallMsg] = (0, react.useState)(void 0);
const [versions, setVersions] = (0, react.useState)({});
const [versionChecked, setVersionChecked] = (0, react.useState)({});
const [versionErrors, setVersionErrors] = (0, react.useState)({});
const refresh = (0, react.useCallback)(async () => {
try {
const [installedRes, versionsRes] = await Promise.all([fetch("/api/plugin-console/installed", { headers: { accept: "application/json" } }), fetch("/api/plugin-console/versions", { headers: { accept: "application/json" } })]);
Expand All @@ -173,12 +184,15 @@ window.__ModuleLoader__.load({
setInstalled(installedBody.plugins ?? []);
const map = {};
const checkedMap = {};
const errorsMap = {};
for (const row of versionsBody.versions ?? []) {
map[row.name] = row.latest;
checkedMap[row.name] = row.checked === true;
errorsMap[row.name] = row.error ?? null;
}
setVersions(map);
setVersionChecked(checkedMap);
setVersionErrors(errorsMap);
} catch (caught) {
setError(caught instanceof Error ? caught.message : String(caught));
} finally {
Expand Down Expand Up @@ -214,12 +228,15 @@ window.__ModuleLoader__.load({
})).json();
const map = {};
const checkedMap = {};
const errorsMap = {};
for (const row of versionBody.versions ?? []) {
map[row.name] = row.latest;
checkedMap[row.name] = row.checked === true;
errorsMap[row.name] = row.error ?? null;
}
setVersions(map);
setVersionChecked(checkedMap);
setVersionErrors(errorsMap);
} catch (caught) {
setError(caught instanceof Error ? caught.message : String(caught));
} finally {
Expand Down Expand Up @@ -401,7 +418,7 @@ window.__ModuleLoader__.load({
})), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
style: rowsStyle,
children: [shown.map((plugin) => {
const version = versionText(plugin, versions[plugin.name] ?? null, versionChecked[plugin.name] === true);
const version = versionText(plugin, versions[plugin.name] ?? null, versionChecked[plugin.name] === true, versionErrors[plugin.name] ?? null);
const isUserRow = !isOfficial(plugin) && !isSelf(plugin);
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
style: rowCardStyle,
Expand Down Expand Up @@ -449,7 +466,10 @@ window.__ModuleLoader__.load({
]
})]
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
style: versionLineStyle,
style: version.failed ? {
...versionLineStyle,
color: "var(--dsw-alias-state-error-primary)"
} : versionLineStyle,
children: version.text
})]
}, showAll ? `a${plugin.id}` : `u${plugin.id}`);
Expand All @@ -469,7 +489,7 @@ window.__ModuleLoader__.load({
//#region src/client/index.ts
/** Cordis 插件名。 */
const name = "plugin-console-client";
/** 需要 slots(settings.section 插槽)。 */
/** 需要 slots(settings.plugins.tab 子 tab 插槽)。 */
const inject = ["slots"];
/**
* 插头图标(plugin-line,参考 Clarity 图标库,dsh 风格:fill
Expand All @@ -493,18 +513,18 @@ window.__ModuleLoader__.load({
host.dataset.dshConsoleIcon = "1";
}
}
/** 注册设置页「插件」面板 + 替换 tab 图标(设置页随时打开/关闭,全程监听)。 */
/** 注册官方「插件」页内的「插件控制台」子 tab + 替换顶层 tab 图标(设置页随时打开/关闭,全程监听)。 */
function apply(ctx) {
new MutationObserver(patchPluginTabIcon).observe(document.body, {
childList: true,
subtree: true
});
patchPluginTabIcon();
ctx.slots.inject("settings.section", () => ctx.slots.register({
name: "settings.section",
ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
name: "settings.plugins.tab",
id: "plugin-console",
order: 60,
label: () => "插件",
order: 10,
label: () => "插件控制台",
inject: () => ({})
}, ConsolePanel));
}
Expand Down
117 changes: 69 additions & 48 deletions packages/plugin/console/lib/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,45 @@ function createPluginTools(deps) {
];
}
//#endregion
//#region src/versions.ts
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
/** npm registry 根(npm_config_registry 环境变量优先,兼容镜像源)。 */
function registryRoot() {
const configured = process.env.npm_config_registry;
return (configured !== void 0 && configured.trim() !== "" ? configured : DEFAULT_REGISTRY).replace(/\/+$/, "");
}
/** scoped 包名(@scope/name)在 registry URL 路径中需把 / 编码为 %2f。 */
function registryPackagePath(name) {
return name.startsWith("@") ? name.replace("/", "%2f") : name;
}
/**
* 查询某包在 registry 上的最新版本(GET <registry>/<name>/latest)。
* 永不抛出:失败折叠为 { latest: null, error },由调用方记录/展示。
*/
async function npmViewLatest(name, fetchFn = fetch) {
const url = `${registryRoot()}/${registryPackagePath(name)}/latest`;
try {
const res = await fetchFn(url, { signal: AbortSignal.timeout(15e3) });
if (res.status === 404) return {
latest: null,
error: null
};
if (!res.ok) return {
latest: null,
error: `registry ${res.status}`
};
return {
latest: (await res.json()).version ?? null,
error: null
};
} catch (caught) {
return {
latest: null,
error: caught instanceof Error ? caught.message : String(caught)
};
}
}
//#endregion
//#region src/index.ts
/**
* 薄控制台 Node half(0811 适配):读写 web profile 的安装态——
Expand Down Expand Up @@ -978,49 +1017,45 @@ async function collectLoaderEntries(ctx) {
for (const row of byId.values()) if (row.disabled && presetIds.has(row.id)) row.presetMounted = true;
return [...byId.values()];
}
/** 版本检查缓存:name -> { latest, checkedAt }(进程内存)。 */
/** 版本检查缓存:name -> { latest, error, checkedAt }(进程内存)。 */
const versionCache = /* @__PURE__ */ new Map();
const VERSION_REFRESH_MIN_MS = 3e4;
let lastVersionRefreshAt = 0;
/** npm view <name> version(registry 最新版);失败/非 registry 包返回 null。 */
function npmViewLatest(name) {
let latest = null;
try {
const result = spawnSync("npm", [
"view",
name,
"version"
], {
encoding: "utf8",
timeout: 15e3,
stdio: [
"ignore",
"pipe",
"pipe"
]
/** 批量强制刷新版本缓存(可选 force):registry 查询走原生 fetch,
* 不 spawn 子进程(受限宿主环境管道捕获会被拦);404 = 非 registry 包。 */
async function refreshVersions(ctx, force) {
const now = Date.now();
if (!force && now - lastVersionRefreshAt < VERSION_REFRESH_MIN_MS) return false;
lastVersionRefreshAt = now;
const names = await userPluginNames(ctx);
await Promise.all(names.map(async (name) => {
const result = await npmViewLatest(name);
if (result.error !== null) ctx.logger.warn(`[plugin-console] version check failed for ${name}: ${result.error}`);
versionCache.set(name, {
latest: result.latest,
error: result.error,
checkedAt: Date.now()
});
const text = (result.stdout ?? "").trim();
if (result.status === 0 && /^\d+(\.\d+)+/.test(text)) latest = text.split("\n")[0].trim();
} catch {}
versionCache.set(name, {
latest,
checkedAt: Date.now()
}));
return true;
}
/** 版本行(缓存内容;error 区分「本地包」与「检查失败」)。 */
function versionRows(names) {
return names.map((name) => {
const cached = versionCache.get(name);
return {
name,
latest: cached?.latest ?? null,
checked: cached !== void 0,
error: cached?.error ?? null
};
});
return latest;
}
/** 用户插件名列表(排除官方命名空间)。 */
async function userPluginNames(ctx) {
const entries = await collectLoaderEntries(ctx);
return [...new Set(entries.map((row) => row.name).filter((name) => !name.startsWith("@deepseek-ai/") && !name.startsWith("@cordisjs/") && !name.startsWith("cordis:")))];
}
/** 批量强制刷新版本缓存(可选 force)。 */
async function refreshVersions(ctx, force) {
const now = Date.now();
if (!force && now - lastVersionRefreshAt < VERSION_REFRESH_MIN_MS) return false;
lastVersionRefreshAt = now;
for (const name of await userPluginNames(ctx)) npmViewLatest(name);
return true;
}
/** Cordis 插件名。 */
const name = "plugin-console";
/** 需要宿主 web server(web 组合)+ loader(读/改 loader 树条目)+ tools(注册 plugin_* 管理工具)+ agentPresets(预设挂载标注)。 */
Expand Down Expand Up @@ -1136,29 +1171,15 @@ function apply(ctx) {
if (method === "GET" && (path === "/api/plugin-console/versions" || path === "/api/plugin-console/versions/")) {
json(200, {
ok: true,
versions: (await userPluginNames(ctx)).map((name) => {
const cached = versionCache.get(name);
return {
name,
latest: cached?.latest ?? null,
checked: cached !== void 0
};
})
versions: versionRows(await userPluginNames(ctx))
});
return;
}
if (method === "POST" && (path === "/api/plugin-console/versions/refresh" || path === "/api/plugin-console/versions/refresh/")) {
json(200, {
ok: true,
refreshed: await refreshVersions(ctx, false),
versions: (await userPluginNames(ctx)).map((name) => {
const cached = versionCache.get(name);
return {
name,
latest: cached?.latest ?? null,
checked: cached !== void 0
};
})
versions: versionRows(await userPluginNames(ctx))
});
return;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"main": "lib/index.mjs",
"scripts": {
"build": "tsdown --config tsdown.config.ts",
"test": "TSX_TSCONFIG_PATH=tests/tsconfig.json node --import tsx --test tests/discovery/*.spec.ts"
"test": "cd tests && node --import tsx --test \"**/*.spec.ts\""
},
"exports": {
".": {
Expand Down
Loading