描述
{description ? (
-
- {description}
-
+
+
+ {description}
+
+
) : (
暂无描述
)}
@@ -175,42 +198,65 @@ export function SkillsPage() {
okText="确认"
cancelText="取消"
confirmLoading={submitLoading}
- okButtonProps={{ "data-testid": "skill-form-submit" }}
+ okButtonProps={{
+ "data-testid": "skill-form-submit",
+ disabled: previewLoading || !skillPreview?.slug
+ }}
cancelButtonProps={{ "data-testid": "skill-form-cancel" }}
- onCancel={() => setOpen(false)}
+ onCancel={() => {
+ setOpen(false);
+ setSkillPreview(null);
+ form.resetFields();
+ }}
onOk={async () => {
await handleSubmit();
}}
>
-
-
-
-
-
event?.fileList || []}
rules={[{ required: true, message: "必须上传技能 zip" }]}
- extra="zip 第一层必须包含符合 AgentSkills 规范的 SKILL.md,frontmatter 至少包含 name 和 description。"
+ extra="选择后会读取 SKILL.md 中的 name 和 description;若 name 不是 slug,会自动转换为 slug 后安装。"
>
- false} maxCount={1}>
+ {
+ void inspectSkillFile(file);
+ return false;
+ }}
+ onRemove={() => {
+ setSkillPreview(null);
+ }}
+ maxCount={1}
+ >
+
+ {skillPreview ? (
+
+ {skillPreview.name_changed ? (
+
+ ) : null}
+ {skillPreview.name || "-"} },
+ { key: "slug", label: "安装 ID", children: {skillPreview.slug || "-"} },
+ { key: "description", label: "描述", children: {skillPreview.description || "-"} }
+ ]}
+ />
+
+ ) : null}
+
diff --git a/apps/management-website/test/integration/common.js b/apps/management-website/test/integration/common.js
index fa7569a..de4b348 100644
--- a/apps/management-website/test/integration/common.js
+++ b/apps/management-website/test/integration/common.js
@@ -82,16 +82,22 @@ export class HttpSession {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.token = "";
+ this.serviceToken = "";
}
setToken(token) {
this.token = token;
}
+ setServiceToken(token) {
+ this.serviceToken = token;
+ }
+
async request(method, route, options = {}) {
const headers = new Headers(options.headers || {});
- if (this.token) {
- headers.set("Authorization", `Bearer ${this.token}`);
+ const token = options.token === "service" ? this.serviceToken : this.token;
+ if (token) {
+ headers.set("Authorization", `Bearer ${token}`);
}
let body = undefined;
@@ -143,6 +149,10 @@ export class HttpSession {
return this.request("GET", route);
}
+ getExternal(route) {
+ return this.request("GET", route, { token: "service" });
+ }
+
post(route, json) {
return this.request("POST", route, { json });
}
diff --git a/apps/management-website/test/integration/full.js b/apps/management-website/test/integration/full.js
index cd6a4bc..550f4a0 100644
--- a/apps/management-website/test/integration/full.js
+++ b/apps/management-website/test/integration/full.js
@@ -27,6 +27,10 @@ function hasExternalTopics(value) {
return nonEmptyText(value?.inboundTopic) && nonEmptyText(value?.outboundTopic);
}
+function findExternalAgent(items, agentSlug) {
+ return Array.isArray(items) ? findBy(items, "agent_id", agentSlug) : null;
+}
+
async function main() {
const settings = loadSettings();
const report = newReport("full", {
@@ -57,6 +61,13 @@ async function main() {
const login = await loginAsAdmin(session, report);
browserSession = await createBrowserSession(settings, login.token);
+ const accessTokenCreate = await session.post("/api/settings/access-tokens", {});
+ addAction(report, "settings.access-tokens.create-for-external-api", accessTokenCreate.ok, accessTokenCreate.status, {
+ tokenCreated: Boolean(accessTokenCreate.body?.token)
+ });
+ assertOk(accessTokenCreate, "settings.access-tokens.create-for-external-api");
+ session.setServiceToken(accessTokenCreate.body.token);
+
for (const route of ["/api/auth/me", "/api/bootstrap", "/api/dashboard", "/api/settings"]) {
const response = await session.get(route);
addAction(report, route, response.ok, response.status, response.body);
@@ -187,10 +198,8 @@ async function main() {
throw new Error(`settings.catalog-sync.before-external-api did not sync agents: ${JSON.stringify(catalogSync.body)}`);
}
- const externalAgents = await session.get(`/api/external/agents?userName=${encodeURIComponent("zhangsan")}`);
- const externalAgent = Array.isArray(externalAgents.body?.items)
- ? findBy(externalAgents.body.items, "agent_id", agentSlug)
- : null;
+ const externalAgents = await session.getExternal(`/api/external/agents?userName=${encodeURIComponent("zhangsan")}`);
+ const externalAgent = findExternalAgent(externalAgents.body?.items, agentSlug);
addAction(
report,
"external.agents.topics",
@@ -203,7 +212,66 @@ async function main() {
throw new Error(`external.agents.topics returned empty topics: ${JSON.stringify(externalAgents.body)}`);
}
- const externalSession = await session.get(`/api/external/session?userName=${encodeURIComponent("zhangsan")}&agentId=${encodeURIComponent(agentSlug)}`);
+ const renamedAgentName = "Integration Agent Synced Rename";
+ const agentRename = await session.put(`/api/agents/${agentRow.id}`, {
+ agent_name: renamedAgentName,
+ permission_usernames: ["zhangsan"],
+ restart: false
+ });
+ addAction(
+ report,
+ "agents.rename.write-through",
+ agentRename.ok && agentRename.body?.agent_name === renamedAgentName,
+ agentRename.status,
+ agentRename.body
+ );
+ assertOk(agentRename, "agents.rename.write-through");
+ if (agentRename.body?.agent_name !== renamedAgentName) {
+ throw new Error(`agents.rename.write-through returned wrong name: ${JSON.stringify(agentRename.body)}`);
+ }
+
+ const catalogSyncAfterRename = await session.request("POST", "/api/settings/catalog-sync");
+ addAction(
+ report,
+ "settings.catalog-sync.after-agent-rename",
+ catalogSyncAfterRename.ok && catalogSyncAfterRename.body?.agent_sync?.status === "success",
+ catalogSyncAfterRename.status,
+ catalogSyncAfterRename.body
+ );
+ assertOk(catalogSyncAfterRename, "settings.catalog-sync.after-agent-rename");
+ if (catalogSyncAfterRename.body?.agent_sync?.status !== "success") {
+ throw new Error(`settings.catalog-sync.after-agent-rename did not sync agents: ${JSON.stringify(catalogSyncAfterRename.body)}`);
+ }
+
+ const agentsAfterRenameSync = await session.get("/api/agents");
+ const managementRenamedAgent = findBy(agentsAfterRenameSync.body, "slug", agentSlug);
+ addAction(
+ report,
+ "agents.rename.catalog-sync.management-name",
+ agentsAfterRenameSync.ok && managementRenamedAgent?.agent_name === renamedAgentName,
+ agentsAfterRenameSync.status,
+ { agent: managementRenamedAgent }
+ );
+ assertOk(agentsAfterRenameSync, "agents.rename.catalog-sync.management-name");
+ if (managementRenamedAgent?.agent_name !== renamedAgentName) {
+ throw new Error(`management agent name regressed after catalog sync: ${JSON.stringify(managementRenamedAgent)}`);
+ }
+
+ const externalAgentsAfterRenameSync = await session.getExternal(`/api/external/agents?userName=${encodeURIComponent("zhangsan")}`);
+ const externalRenamedAgent = findExternalAgent(externalAgentsAfterRenameSync.body?.items, agentSlug);
+ addAction(
+ report,
+ "agents.rename.catalog-sync.external-name",
+ externalAgentsAfterRenameSync.ok && externalRenamedAgent?.agent_name === renamedAgentName,
+ externalAgentsAfterRenameSync.status,
+ { agent: externalRenamedAgent }
+ );
+ assertOk(externalAgentsAfterRenameSync, "agents.rename.catalog-sync.external-name");
+ if (externalRenamedAgent?.agent_name !== renamedAgentName) {
+ throw new Error(`external agent name regressed after catalog sync: ${JSON.stringify(externalAgentsAfterRenameSync.body)}`);
+ }
+
+ const externalSession = await session.getExternal(`/api/external/session?userName=${encodeURIComponent("zhangsan")}&agentId=${encodeURIComponent(agentSlug)}`);
addAction(
report,
"external.session.topics",
@@ -216,7 +284,7 @@ async function main() {
throw new Error(`external.session.topics returned empty topics: ${JSON.stringify(externalSession.body)}`);
}
- const externalContext = await session.get(`/api/external/context?sessionId=${encodeURIComponent(externalSession.body.sessionId)}`);
+ const externalContext = await session.getExternal(`/api/external/context?sessionId=${encodeURIComponent(externalSession.body.sessionId)}`);
addAction(
report,
"external.context.topics",
diff --git a/apps/management-website/test/integration/website-full.js b/apps/management-website/test/integration/website-full.js
index e2a6f2e..b76b1c1 100644
--- a/apps/management-website/test/integration/website-full.js
+++ b/apps/management-website/test/integration/website-full.js
@@ -287,9 +287,16 @@ async function createSkillOnPage(page, skillSlug, skillDescription, skillZip) {
await navigate(page, "skills", "skill-create-button");
await page.getByTestId("skill-create-button").click();
await page.getByTestId("skill-form").waitFor({ state: "visible" });
- await page.getByTestId("skill-form-slug").fill(skillSlug);
- await page.getByTestId("skill-form-description").fill(skillDescription);
await uploadFile(page, "skill-form", skillZip);
+ await page.getByTestId("skill-preview-slug").waitFor({ state: "visible" });
+ const previewSlug = await page.getByTestId("skill-preview-slug").innerText();
+ if (previewSlug !== skillSlug) {
+ throw new Error(`skill preview slug mismatch: ${previewSlug}`);
+ }
+ const previewDescription = await page.getByTestId("skill-preview-description").innerText();
+ if (!previewDescription.includes(skillDescription)) {
+ throw new Error(`skill preview description mismatch: ${previewDescription}`);
+ }
await page.getByTestId("skill-form-submit").click();
await page.getByTestId(`skill-card-${skillSlug}`).waitFor({ state: "visible" });
const descriptionText = await page.getByTestId(`skill-card-description-${skillSlug}`).innerText();
diff --git a/docker-images/apps/Dockerfile b/docker-images/apps/Dockerfile
index 9d0693e..8927531 100644
--- a/docker-images/apps/Dockerfile
+++ b/docker-images/apps/Dockerfile
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.7
-ARG AIOS_MANAGEMENT_WEB_NPM_VERSION=0.3.4
+ARG AIOS_MANAGEMENT_WEB_NPM_VERSION=0.3.5
ARG AIOS_PROXY_NPM_VERSION=0.1.3
ARG AIOS_BUILD_NPM_REGISTRY=https://registry.npmjs.org
diff --git a/docker-images/kernal/Dockerfile b/docker-images/kernal/Dockerfile
index ce23eca..8a1649a 100644
--- a/docker-images/kernal/Dockerfile
+++ b/docker-images/kernal/Dockerfile
@@ -166,7 +166,7 @@ ARG CLAWHUB_VERSION=0.18.0
ARG QMD_VERSION=2.5.3
ARG AGENT_BROWSER_VERSION=0.27.1
ARG MCPORTER_VERSION=0.11.3
-ARG AIOS_MANAGEMENT_SERIVCE_VERSION=0.3.9
+ARG AIOS_MANAGEMENT_SERIVCE_VERSION=0.4.0
ARG AIOS_APPS_INVOKE_CLI_VERSION=0.0.1
ARG AIOS_BUILD_NPM_REGISTRY=https://registry.npmjs.org
diff --git a/joint-test/dmz-mode/README.md b/joint-test/dmz-mode/README.md
index 9bfb5e9..d65e19b 100644
--- a/joint-test/dmz-mode/README.md
+++ b/joint-test/dmz-mode/README.md
@@ -97,6 +97,17 @@ AIOS_OPENAI_API_KEY=sk-... bash joint-test/dmz-mode/run-all-in-one-dev2-flow.win
AIOS_TEST_SKIP_BUILD=1 AIOS_OPENAI_API_KEY=sk-... bash joint-test/dmz-mode/run-all-in-one-dev2-flow.macos.sh
```
+使用历史问题镜像复现/验证“管理控制台改名后 catalog 同步不应回退名称”的场景:
+
+```bash
+AIOS_TEST_SKIP_BUILD=1 \
+AIOS_ALL_IN_ONE_IMAGE=sha256:b2ad0841e6c17229fc7eb189e1e69d235ee2b227242924839c1e141bebc52154 \
+AIOS_OPENAI_API_KEY=sk-... \
+bash joint-test/dmz-mode/run-all-in-one-dev2-flow.macos.sh
+```
+
+Playwright 流程会在创建并授权 `public-demo` 后执行 `5a.verify-agent-rename-survives-catalog-sync`:先通过管理 API 修改数字员工名称,再触发 `/api/settings/catalog-sync`,最后分别断言 `/api/agents` 和 `/api/external/agents` 返回的新名称没有被内核旧快照覆盖。
+
all-in-one 联调会使用内置 MinIO 和内置 MQTT。测试脚本只把管理端口 `3030` 和 MQTT 端口 `1883` 映射到宿主机,其中 MQTT 映射用于 Playwright 脚本实际发送消息验证 agent 可用。
旧入口 `run-all-in-one-dev2-flow.sh` 仍保留为兼容分发脚本,会根据当前 shell 自动转到 macOS 或 Windows 入口。
diff --git a/joint-test/dmz-mode/playwright-dev2-flow.js b/joint-test/dmz-mode/playwright-dev2-flow.js
index 0a3a18f..ad2a4c6 100644
--- a/joint-test/dmz-mode/playwright-dev2-flow.js
+++ b/joint-test/dmz-mode/playwright-dev2-flow.js
@@ -337,6 +337,17 @@ async function nodeApi(baseUrl, token, method, route, body, timeoutMs) {
return result.body;
}
+async function createServiceAccessToken(baseUrl, adminToken) {
+ const created = await nodeApi(baseUrl, adminToken, "POST", "/api/settings/access-tokens", {}, 60000);
+ assert.ok(created?.token, `access token creation should return token: ${JSON.stringify(created)}`);
+ return created.token;
+}
+
+async function externalApi(baseUrl, serviceToken, method, route, body, timeoutMs) {
+ assert.ok(serviceToken, "service access token is required for external API checks");
+ return await nodeApi(baseUrl, serviceToken, method, route, body, timeoutMs);
+}
+
async function nodeMultipart(baseUrl, token, method, route, fields, fileField, timeoutMs = Number(process.env.AIOS_TEST_API_TIMEOUT_MS || "180000")) {
const form = new FormData();
for (const [key, value] of Object.entries(fields)) {
@@ -538,6 +549,49 @@ async function assignAdministrator(baseUrl, token, agentId) {
return updated;
}
+async function verifyAgentRenameSurvivesCatalogSync(baseUrl, token, serviceToken, agentId) {
+ const renamedAgentName = "Public Demo Synced Rename";
+ const renamed = await nodeApi(baseUrl, token, "PUT", `/api/agents/${agentId}`, {
+ agent_name: renamedAgentName,
+ permission_usernames: [ASSIGNEE_USERNAME],
+ restart: false
+ }, 120000);
+ assert.equal(renamed.agent_name, renamedAgentName, "agent rename should update management record");
+
+ const syncResult = await nodeApi(baseUrl, token, "POST", "/api/settings/catalog-sync", {}, 300000);
+ assert.equal(syncResult?.agent_sync?.status, "success", `catalog sync after rename should succeed: ${JSON.stringify(syncResult)}`);
+
+ const agents = await nodeApi(baseUrl, token, "GET", "/api/agents", undefined, 60000);
+ const managementAgent = findBy(agents, "slug", AGENT_ID);
+ assert.equal(
+ managementAgent?.agent_name,
+ renamedAgentName,
+ `management agent name should not regress after catalog sync: ${JSON.stringify(managementAgent)}`
+ );
+
+ const externalAgents = await externalApi(
+ baseUrl,
+ serviceToken,
+ "GET",
+ `/api/external/agents?userName=${encodeURIComponent(ASSIGNEE_USERNAME)}`,
+ undefined,
+ 60000
+ );
+ const externalAgent = findBy(externalAgents?.items, "agent_id", AGENT_ID);
+ assert.equal(
+ externalAgent?.agent_name,
+ renamedAgentName,
+ `external API agent name should not regress after catalog sync: ${JSON.stringify(externalAgents)}`
+ );
+
+ return {
+ renamed,
+ syncSummary: syncResult?.agent_sync?.summary || {},
+ managementAgent,
+ externalAgent
+ };
+}
+
async function uploadGlobalSkillWithPlaywright(page, baseUrl, skillArtifact) {
await page.goto(baseUrl, { waitUntil: "domcontentloaded" });
await page.getByTestId("nav-skills").waitFor({ state: "visible", timeout: 60000 });
@@ -548,9 +602,11 @@ async function uploadGlobalSkillWithPlaywright(page, baseUrl, skillArtifact) {
const skillForm = page.getByTestId("skill-form");
await skillForm.waitFor({ state: "visible", timeout: 60000 });
- await page.getByTestId("skill-form-slug").fill(skillArtifact.slug);
- await page.getByTestId("skill-form-description").fill(skillArtifact.description);
await skillForm.locator('input[type="file"]').setInputFiles(skillArtifact.skillZip);
+ await page.getByTestId("skill-preview-slug").waitFor({ state: "visible", timeout: 60000 });
+ assert.equal(await page.getByTestId("skill-preview-slug").innerText(), skillArtifact.slug);
+ const previewDescription = await page.getByTestId("skill-preview-description").innerText();
+ assert.ok(previewDescription.includes(skillArtifact.description), "skill preview should show package description");
const startedAt = Date.now();
const [response] = await Promise.all([
@@ -1238,7 +1294,8 @@ async function main() {
const baseUrl = process.env.AIOS_WEB_BASE_URL || DEFAULT_BASE_URL;
const envJsonPath = path.resolve(process.env.AIOS_TEST_ENV_JSON || ".env.json");
const reportDir = path.resolve(process.env.AIOS_TEST_REPORT_DIR || path.join("test", ".reports"));
- const apiKey = readModelApiKey();
+ const focus = String(process.env.AIOS_TEST_FOCUS || "").trim();
+ const apiKey = focus === "agent-rename-sync" ? "sk-agent-rename-sync-placeholder" : readModelApiKey();
const artifactRoot = fs.mkdtempSync(path.join(os.tmpdir(), "aios-pw-flow-"));
const artifacts = createDefaultWorkspaceTemplateZip(artifactRoot);
const globalSkillArtifact = createGlobalSkillZip(artifactRoot);
@@ -1255,6 +1312,8 @@ async function main() {
agentId: AGENT_ID,
assigneeUsername: ASSIGNEE_USERNAME,
globalSkillSlug: GLOBAL_SKILL_SLUG,
+ allInOneImage: process.env.AIOS_TEST_ALL_IN_ONE_IMAGE || process.env.AIOS_ALL_IN_ONE_IMAGE || null,
+ focus: focus || null,
packageInstalls: PACKAGE_INSTALLS
},
artifacts: {
@@ -1282,6 +1341,7 @@ async function main() {
let browser;
let context;
let token = "";
+ let serviceToken = "";
try {
browser = await chromium.launch({
@@ -1297,6 +1357,11 @@ async function main() {
assert.ok(token, "login did not store auth token");
addStep(report, "login", true);
+ serviceToken = await createServiceAccessToken(baseUrl, token);
+ addStep(report, "0.create-service-access-token", true, {
+ tokenCreated: Boolean(serviceToken)
+ });
+
report.managementRpcReady = await waitForManagementRpcReady(
baseUrl,
token,
@@ -1304,21 +1369,29 @@ async function main() {
);
addStep(report, "0.wait-management-rpc-ready", true);
- report.startupCatalogSync = await waitForStartupCatalogSyncReady(
- baseUrl,
- token,
- Number(process.env.AIOS_TEST_STARTUP_SYNC_READY_TIMEOUT_MS || "120000")
- );
- addStep(report, "0.wait-startup-catalog-sync-ready", true, {
- states: Object.fromEntries(Object.entries(report.startupCatalogSync).map(([key, state]) => [
- key,
- {
- status: state.status,
- lastSuccessAt: state.last_success_at,
- summary: state.summary || {}
- }
- ]))
- });
+ if (focus === "agent-rename-sync") {
+ report.startupCatalogSync = null;
+ addStep(report, "0.skip-startup-catalog-sync-ready-for-focus", true, {
+ focus,
+ reason: "focused rename regression triggers catalog sync explicitly after rename"
+ });
+ } else {
+ report.startupCatalogSync = await waitForStartupCatalogSyncReady(
+ baseUrl,
+ token,
+ Number(process.env.AIOS_TEST_STARTUP_SYNC_READY_TIMEOUT_MS || "120000")
+ );
+ addStep(report, "0.wait-startup-catalog-sync-ready", true, {
+ states: Object.fromEntries(Object.entries(report.startupCatalogSync).map(([key, state]) => [
+ key,
+ {
+ status: state.status,
+ lastSuccessAt: state.last_success_at,
+ summary: state.summary || {}
+ }
+ ]))
+ });
+ }
report.cleanup = await cleanupCustomResources(baseUrl, token);
addStep(report, "0.cleanup-custom-resources", report.cleanup.every((item) => item.ok), {
@@ -1343,6 +1416,24 @@ async function main() {
report.agent = await assignAdministrator(baseUrl, token, report.agent.id);
addStep(report, "5.assign-administrator", true, { username: ASSIGNEE_USERNAME });
+ report.agentRenameSync = await verifyAgentRenameSurvivesCatalogSync(baseUrl, token, serviceToken, report.agent.id);
+ report.agent = report.agentRenameSync.managementAgent || report.agent;
+ addStep(report, "5a.verify-agent-rename-survives-catalog-sync", true, {
+ agentId: AGENT_ID,
+ agentName: report.agentRenameSync.managementAgent?.agent_name || null,
+ externalAgentName: report.agentRenameSync.externalAgent?.agent_name || null,
+ syncSummary: report.agentRenameSync.syncSummary
+ });
+
+ if (focus === "agent-rename-sync") {
+ report.ok = true;
+ addStep(report, "5b.stop-after-agent-rename-sync-focus", true, {
+ focus,
+ reason: "focused regression does not require model API calls or MQTT agent dialogue"
+ });
+ return;
+ }
+
report.globalSkillUpload = await uploadGlobalSkillWithPlaywright(page, baseUrl, globalSkillArtifact);
addStep(report, "6.upload-global-skill-with-playwright", true, {
slug: globalSkillArtifact.slug,
diff --git a/joint-test/dmz-mode/run-all-in-one-dev2-flow.core.sh b/joint-test/dmz-mode/run-all-in-one-dev2-flow.core.sh
index 5f3521f..a64b95d 100644
--- a/joint-test/dmz-mode/run-all-in-one-dev2-flow.core.sh
+++ b/joint-test/dmz-mode/run-all-in-one-dev2-flow.core.sh
@@ -152,8 +152,9 @@ wait_for_url() {
}
run_flow() {
- if [[ -z "${AIOS_OPENAI_API_KEY:-}" && -z "${AIOS_TEST_MODEL_API_KEY:-}" ]]; then
+ if [[ "${AIOS_TEST_FOCUS:-}" != "agent-rename-sync" && -z "${AIOS_OPENAI_API_KEY:-}" && -z "${AIOS_TEST_MODEL_API_KEY:-}" ]]; then
printf 'AIOS_OPENAI_API_KEY or AIOS_TEST_MODEL_API_KEY is required for the public-demo MQTT echo step.\n' >&2
+ printf 'Set AIOS_TEST_FOCUS=agent-rename-sync to run only the catalog-sync rename regression without a model API key.\n' >&2
return 1
fi
@@ -197,6 +198,7 @@ run_flow() {
AIOS_TEST_REPORT_DIR="${REPORT_DIR}" \
AIOS_TEST_STARTUP_TIME_MS="${startup_time_ms}" \
AIOS_TEST_RESTART_TARGETS="${CONTAINER}" \
+ AIOS_TEST_ALL_IN_ONE_IMAGE="${IMAGE}" \
node "${SCRIPT_DIR}/playwright-dev2-flow.js"
}
diff --git a/kernal/aios-management-serivce/package.json b/kernal/aios-management-serivce/package.json
index 98b3174..d34e352 100644
--- a/kernal/aios-management-serivce/package.json
+++ b/kernal/aios-management-serivce/package.json
@@ -1,6 +1,6 @@
{
"name": "aios-management-serivce",
- "version": "0.3.9",
+ "version": "0.4.0",
"description": "AIOS / OpenClaw 的 MQTT 管理控制台服务。",
"type": "module",
"private": false,
diff --git a/kernal/aios-management-serivce/src/capabilities/agent/lifecycle-service.ts b/kernal/aios-management-serivce/src/capabilities/agent/lifecycle-service.ts
index a18cf5e..f7eddb6 100644
--- a/kernal/aios-management-serivce/src/capabilities/agent/lifecycle-service.ts
+++ b/kernal/aios-management-serivce/src/capabilities/agent/lifecycle-service.ts
@@ -3,6 +3,7 @@ import path from "node:path";
import { CliRunner } from "../../tools/cli-runner.js";
import type {
+ AgentNameSetParams,
AgentModelSetParams,
EnvironmentConfig,
ManagedAgentRecord
@@ -358,6 +359,39 @@ export class AgentLifecycleService {
};
}
+ async setAgentName(params: AgentNameSetParams): Promise
{
+ const name = firstText(params.name);
+ if (!name) {
+ throw new Error("Agent name is required");
+ }
+
+ if (this.env.gatewayRestartMode === "container") {
+ const config = await this.readOpenClawConfig();
+ const agents = Array.isArray(config.agents?.list) ? config.agents.list : [];
+ const agent = agents.find((entry: { id?: string }) => entry?.id === params.agentId);
+ if (!agent) {
+ throw new Error(`Agent config not found: ${params.agentId}`);
+ }
+
+ agent.name = name;
+ await this.writeOpenClawConfig(config);
+ } else {
+ const index = await this.requireAgentConfigIndex(params.agentId);
+ await this.cli.run([
+ "config",
+ "set",
+ `agents.list[${index}].name`,
+ JSON.stringify(name),
+ "--strict-json"
+ ]);
+ }
+
+ return {
+ agentId: params.agentId,
+ name
+ };
+ }
+
async queryAllUsage(agentIds?: string[]): Promise {
const normalizedAgentIds = Array.isArray(agentIds)
? [...new Set(agentIds.map((item) => String(item || "").trim()).filter(Boolean))]
diff --git a/kernal/aios-management-serivce/src/capabilities/agent/name-set.ts b/kernal/aios-management-serivce/src/capabilities/agent/name-set.ts
new file mode 100644
index 0000000..adccb82
--- /dev/null
+++ b/kernal/aios-management-serivce/src/capabilities/agent/name-set.ts
@@ -0,0 +1,6 @@
+import type { AgentNameSetParams } from "../../types.js";
+import { readCapabilityParams, type CapabilityHandler } from "../context.js";
+
+export const agentNameSet: CapabilityHandler = async (request, context) => {
+ return await context.agentManager.setAgentName(readCapabilityParams(request));
+};
diff --git a/kernal/aios-management-serivce/src/capabilities/agent/service.ts b/kernal/aios-management-serivce/src/capabilities/agent/service.ts
index 61d40fb..38b10ee 100644
--- a/kernal/aios-management-serivce/src/capabilities/agent/service.ts
+++ b/kernal/aios-management-serivce/src/capabilities/agent/service.ts
@@ -5,6 +5,7 @@ import type {
AgentCreateParams,
AgentDocsUpdateParams,
AgentGetParams,
+ AgentNameSetParams,
AgentModelSetParams,
AgentMutationParams,
AgentWorkspaceExportParams,
@@ -171,6 +172,12 @@ export class OpenClawManager {
return result;
}
+ async setAgentName(params: AgentNameSetParams): Promise {
+ const result = await this.lifecycleService.setAgentName(params);
+ await this.operationsService.maybeRefreshGatewayAfterManagedTopologyChange(params.restart, `agent.name.set:${params.agentId}`);
+ return result;
+ }
+
async listLlmProviders(): Promise {
return await this.llmModelConfigService.listProviders();
}
diff --git a/kernal/aios-management-serivce/src/capabilities/context.ts b/kernal/aios-management-serivce/src/capabilities/context.ts
index d3a79fb..4c7bbd7 100644
--- a/kernal/aios-management-serivce/src/capabilities/context.ts
+++ b/kernal/aios-management-serivce/src/capabilities/context.ts
@@ -2,6 +2,7 @@ import type {
AgentCreateParams,
AgentDocsUpdateParams,
AgentGetParams,
+ AgentNameSetParams,
AgentModelSetParams,
AgentMutationParams,
AgentWorkspaceExportParams,
@@ -40,6 +41,7 @@ export interface AgentCapacityService {
deleteAgent(params: AgentMutationParams): Promise<{ agentId: string; accountId: string }>;
queryAllUsage(params?: { agentIds?: string[] }): Promise;
setAgentModel(params: AgentModelSetParams): Promise;
+ setAgentName(params: AgentNameSetParams): Promise;
updateAgentDocs(params: AgentDocsUpdateParams): Promise<{ path: string }>;
exportAgentWorkspace(params: AgentWorkspaceExportParams): Promise;
listLlmProviders(): Promise;
diff --git a/kernal/aios-management-serivce/src/capabilities/registry.ts b/kernal/aios-management-serivce/src/capabilities/registry.ts
index 7440432..882ec46 100644
--- a/kernal/aios-management-serivce/src/capabilities/registry.ts
+++ b/kernal/aios-management-serivce/src/capabilities/registry.ts
@@ -5,6 +5,7 @@ import { agentDocsUpdate } from "./agent/docs-update.js";
import { agentEnable } from "./agent/enable.js";
import { agentGet } from "./agent/get.js";
import { agentModelSet } from "./agent/model-set.js";
+import { agentNameSet } from "./agent/name-set.js";
import { agentUsageList } from "./agent/usage-list.js";
import { agentWorkspaceExport } from "./agent/workspace-export.js";
import { agentTemplateCreate } from "./agent-template/create.js";
@@ -41,6 +42,7 @@ export const capabilityHandlers: Record = {
"agent.delete": agentDelete,
"agent.usage.list": agentUsageList,
"agent.model.set": agentModelSet,
+ "agent.name.set": agentNameSet,
"agent.docs.update": agentDocsUpdate,
"agent.workspace.export": agentWorkspaceExport,
"agent.template.create": agentTemplateCreate,
diff --git a/kernal/aios-management-serivce/src/types.ts b/kernal/aios-management-serivce/src/types.ts
index d0aa47c..8362ad9 100644
--- a/kernal/aios-management-serivce/src/types.ts
+++ b/kernal/aios-management-serivce/src/types.ts
@@ -134,6 +134,12 @@ export interface AgentModelSetParams {
restart?: boolean;
}
+export interface AgentNameSetParams {
+ agentId: string;
+ name: string;
+ restart?: boolean;
+}
+
export interface LlmProviderCreateParams {
providerId: string;
baseUrl: string;
diff --git a/kernal/aios-management-serivce/test/capabilities-registry.test.ts b/kernal/aios-management-serivce/test/capabilities-registry.test.ts
index 228424a..85a87c8 100644
--- a/kernal/aios-management-serivce/test/capabilities-registry.test.ts
+++ b/kernal/aios-management-serivce/test/capabilities-registry.test.ts
@@ -14,6 +14,7 @@ const expectedActions = [
"agent.delete",
"agent.usage.list",
"agent.model.set",
+ "agent.name.set",
"agent.docs.update",
"agent.workspace.export",
"agent.template.create",
@@ -64,6 +65,7 @@ function createContext(calls: Array<{ method: string; args: unknown[] }>): Capab
deleteAgent: respond("agentManager.deleteAgent") as never,
queryAllUsage: respond("agentManager.queryAllUsage"),
setAgentModel: respond("agentManager.setAgentModel"),
+ setAgentName: respond("agentManager.setAgentName"),
updateAgentDocs: respond("agentManager.updateAgentDocs") as never,
exportAgentWorkspace: respond("agentManager.exportAgentWorkspace") as never,
listLlmProviders: respond("agentManager.listLlmProviders"),
@@ -193,6 +195,12 @@ test("capability handlers delegate to the expected service methods", async () =>
expectedMethod: "agentManager.setAgentModel",
expectedArgs: [{ agentId: "lowcode", model: { primary: "openai/gpt-5.5" } }]
},
+ {
+ action: "agent.name.set",
+ params: { agentId: "lowcode", name: "Lowcode Agent Updated" },
+ expectedMethod: "agentManager.setAgentName",
+ expectedArgs: [{ agentId: "lowcode", name: "Lowcode Agent Updated" }]
+ },
{
action: "agent.docs.update",
params: { agentId: "lowcode", content: "# AGENTS" },
diff --git a/kernal/aios-management-serivce/test/openclaw-manager.test.ts b/kernal/aios-management-serivce/test/openclaw-manager.test.ts
index 87fe04e..4b17f10 100644
--- a/kernal/aios-management-serivce/test/openclaw-manager.test.ts
+++ b/kernal/aios-management-serivce/test/openclaw-manager.test.ts
@@ -982,6 +982,17 @@ test("container-mode agent mutations edit openclaw.json without shelling out", a
},
restart: false
});
+ await manager.setAgentName({
+ agentId: "lowcode",
+ name: "Lowcode Agent Updated",
+ restart: false
+ });
+
+ const renamedConfig = JSON.parse(await readFile(configPath, "utf8")) as {
+ agents?: { list?: Array<{ id?: string; name?: string }> };
+ };
+ assert.equal(renamedConfig.agents?.list?.find((agent) => agent.id === "lowcode")?.name, "Lowcode Agent Updated");
+
await manager.disableAgent({ agentId: "lowcode", restart: false });
await manager.deleteAgent({ agentId: "lowcode", restart: false });
@@ -998,7 +1009,7 @@ test("container-mode agent mutations edit openclaw.json without shelling out", a
assert.deepEqual(config.bindings, []);
});
-test("getAgent, setAgentModel, and updateAgentDocs follow the capability contract", async () => {
+test("getAgent, setAgentName, setAgentModel, and updateAgentDocs follow the capability contract", async () => {
const root = await mkdtemp(path.join(tmpdir(), "oc-mgmt-agent-contract-"));
const workspace = path.join(root, "workspaces", "lowcode");
const stateStore = new ManagedStateStore(path.join(root, "managed-state.json"));
@@ -1019,6 +1030,11 @@ test("getAgent, setAgentModel, and updateAgentDocs follow the capability contrac
name: "Lowcode Agent",
workspace: "/ignored/from/config"
}],
+ [{
+ id: "lowcode",
+ name: "Lowcode Agent",
+ workspace: "/ignored/from/config"
+ }],
[{
id: "lowcode",
name: "Lowcode Agent",
@@ -1040,6 +1056,11 @@ test("getAgent, setAgentModel, and updateAgentDocs follow the capability contrac
);
const agent = await manager.getAgent({ agentId: "lowcode" }) as Record;
+ const renamed = await manager.setAgentName({
+ agentId: "lowcode",
+ name: "Lowcode Agent Updated",
+ restart: false
+ });
const model = await manager.setAgentModel({
agentId: "lowcode",
model: "openai/gpt-5.5",
@@ -1051,6 +1072,10 @@ test("getAgent, setAgentModel, and updateAgentDocs follow the capability contrac
});
assert.equal(agent.agentId, "lowcode");
+ assert.deepEqual(renamed, {
+ agentId: "lowcode",
+ name: "Lowcode Agent Updated"
+ });
assert.deepEqual(model, {
agentId: "lowcode",
model: {
@@ -1063,10 +1088,12 @@ test("getAgent, setAgentModel, and updateAgentDocs follow the capability contrac
assert.equal(docsStat.mode & 0o777, 0o640);
}
assert.deepEqual(cli.runJsonCalls, [
+ ["agents", "list", "--json", "--bindings"],
["agents", "list", "--json", "--bindings"],
["agents", "list", "--json", "--bindings"]
]);
assert.deepEqual(cli.runCalls.map((call) => call.args), [
+ ["config", "set", "agents.list[0].name", "\"Lowcode Agent Updated\"", "--strict-json"],
["config", "set", "agents.list[0].model", "{\"primary\":\"openai/gpt-5.5\"}", "--strict-json"]
]);
});
--
Gitee
From 4d59c9023cb536c32e17adcc61e907a1565d3055 Mon Sep 17 00:00:00 2001
From: NingWei
Date: Wed, 17 Jun 2026 11:31:37 +0800
Subject: [PATCH 02/10] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=89=88=E6=9C=AC?=
=?UTF-8?q?=E6=98=BE=E7=A4=BA=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=8A=80=E8=83=BD?=
=?UTF-8?q?=E5=92=8C=E7=BB=84=E4=BB=B6=EF=BC=8C=E5=8D=87=E7=BA=A7openclaw?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/management-website/README.md | 13 +-
apps/management-website/docs/spec.md | 2 +-
apps/management-website/package.json | 2 +-
.../server/src/api/routes/index.js | 17 ++
apps/management-website/server/src/app.js | 1 +
.../server/src/background/index.js | 6 +
.../management-website/server/src/db/index.js | 50 +++-
.../services/large-language-model-service.js | 37 ++-
.../server/src/services/portal-service.js | 161 +++++++++++
.../server/test/background-services.test.js | 7 +
.../server/test/db-reset-migration.test.js | 16 +-
.../test/large-language-model-route.test.js | 60 ++++
.../test/large-language-model-service.test.js | 13 +-
.../test/portal-service-alignment.test.js | 100 +++++++
apps/management-website/src/app/App.jsx | 4 +-
.../src/pages/LargeLanguageModelsPage.jsx | 111 +++++---
.../src/pages/SettingsPage.jsx | 36 +++
apps/management-website/src/styles.css | 20 ++
.../test/integration/common.js | 23 +-
.../test/integration/fast.js | 18 +-
.../test/integration/full.js | 18 +-
.../test/integration/website-full.js | 10 +-
docker-images/apps/Dockerfile | 2 +-
docker-images/kernal/Dockerfile | 22 +-
docker-images/kernal/README.md | 4 +
docker-images/kernal/docs/build.md | 6 +-
docker-images/kernal/docs/design.md | 4 +
.../test/kernal-tests/test/fast-smoke.js | 8 +
.../test/kernal-tests/test/full-smoke.js | 2 +-
joint-test/dmz-mode/run-dev2-flow.core.sh | 1 +
kernal/aios-management-serivce/README.md | 9 +-
.../docs/compatibility-list.md | 51 ++++
kernal/aios-management-serivce/docs/design.md | 24 +-
kernal/aios-management-serivce/package.json | 2 +-
.../src/capabilities/agent/service.ts | 5 +
.../capabilities/agent/workspace-service.ts | 1 +
.../src/capabilities/context.ts | 2 +
.../src/capabilities/llm/model-test.ts | 6 +
.../src/capabilities/llm/service.ts | 61 +++-
.../src/capabilities/registry.ts | 4 +
.../src/capabilities/version/list.ts | 78 ++++++
kernal/aios-management-serivce/src/service.ts | 1 +
kernal/aios-management-serivce/src/types.ts | 9 +
.../test/capabilities-registry.test.ts | 24 ++
.../test/llm-model-config-service.test.ts | 30 +-
.../test/openclaw-manager.test.ts | 45 +++
.../test/service-queue.test.ts | 1 +
.../aios-self-improving-agent/SKILL.md | 261 ++++++++++++++++++
.../agents/openai.yaml | 7 +
49 files changed, 1259 insertions(+), 136 deletions(-)
create mode 100644 kernal/aios-management-serivce/src/capabilities/llm/model-test.ts
create mode 100644 kernal/aios-management-serivce/src/capabilities/version/list.ts
create mode 100644 kernal/clawhub-skills/aios-self-improving-agent/SKILL.md
create mode 100644 kernal/clawhub-skills/aios-self-improving-agent/agents/openai.yaml
diff --git a/apps/management-website/README.md b/apps/management-website/README.md
index 5d2ec0e..3f9904b 100644
--- a/apps/management-website/README.md
+++ b/apps/management-website/README.md
@@ -14,7 +14,7 @@ AIOS 管理控制台是面向运维和管理员的 Web 应用,技术栈为 `Re
- 本地数据库缓存环境目录和 Token 用量
- 启动同步、定时同步和手动同步共用同一套同步计划
-管理控制台的列表、状态和统计统一从 SQLite 读取。远端数据只通过 `catalog.snapshot` 和 `agent.usage.list` 两类同步任务刷新。
+管理控制台的列表、状态和统计统一从 SQLite 读取。远端数据只通过 `catalog.snapshot` 和 `agent.usage.list` 两类同步任务刷新;版本信息在启动时通过一次 `version.list` 拉取并缓存到 `component_versions`,后续只读本地数据库。
## 目录结构
@@ -78,6 +78,7 @@ npm run test:full
- `SOUL.md`
- `TOOLS.md`
- `HEARTBEAT.md`
+- `.learnings/`
- `skills/`
- `knowledge/`
@@ -109,10 +110,11 @@ npm run test:full
启动后后台流程为:
1. 等待 `aios-management-serivce` 的 `service.ping` 可用
-2. 执行一次 `catalog.snapshot`
-3. 如果目录快照同步成功,60 秒后执行第一次 `agent.usage.list`
-4. 每次 `catalog.snapshot` 完成后 120 秒再次执行
-5. 每次 `agent.usage.list` 完成后 120 秒再次执行
+2. 执行一次 `version.list` 并写入 `component_versions`
+3. 执行一次 `catalog.snapshot`
+4. 如果目录快照同步成功,60 秒后执行第一次 `agent.usage.list`
+5. 每次 `catalog.snapshot` 完成后 120 秒再次执行
+6. 每次 `agent.usage.list` 完成后 120 秒再次执行
设置页“同步数据”按钮会先执行 `catalog.snapshot`,再执行 `agent.usage.list`。同步过程会显示蒙版,完成后用 toast 提示结果。
@@ -124,6 +126,7 @@ npm run test:full
- `template_sync_state`
- `system_sync_state`
- `usage_refresh_state`
+- `component_versions`
## 对外 API
diff --git a/apps/management-website/docs/spec.md b/apps/management-website/docs/spec.md
index b96550d..ca441c8 100644
--- a/apps/management-website/docs/spec.md
+++ b/apps/management-website/docs/spec.md
@@ -40,7 +40,7 @@ Token 用量由 `agent.usage.list` 同步到 `agents.usage_snapshot_json`。超
### 模板
-模板由模板名和 workspace zip 组成,zip 顶层必须包含 `AGENTS.md`。模板可配置默认 Provider / Model,创建数字员工时作为表单默认值。
+模板由模板名和 workspace zip 组成,zip 顶层必须包含 `AGENTS.md`,并应保留可复用目录(例如 `.learnings/`、`skills/`、`knowledge/`)。模板可配置默认 Provider / Model,创建数字员工时作为表单默认值。
### Skill
diff --git a/apps/management-website/package.json b/apps/management-website/package.json
index 822fe07..a3808f9 100644
--- a/apps/management-website/package.json
+++ b/apps/management-website/package.json
@@ -1,6 +1,6 @@
{
"name": "aios-management-web",
- "version": "0.3.5",
+ "version": "0.3.6",
"type": "module",
"files": [
"dist",
diff --git a/apps/management-website/server/src/api/routes/index.js b/apps/management-website/server/src/api/routes/index.js
index 3c39a5d..de27211 100644
--- a/apps/management-website/server/src/api/routes/index.js
+++ b/apps/management-website/server/src/api/routes/index.js
@@ -293,6 +293,7 @@ export function createRoutes(services) {
current_user: req.currentUser,
settings: services.portalService.getSettings(),
access_tokens: services.portalService.listAccessTokens(),
+ version_info: services.portalService.getVersionInfo(),
...getCatalogSyncStatuses(services),
env_config: {
admin_inbound_topic: services.env?.mqtt?.adminInboundTopic || ""
@@ -468,6 +469,22 @@ export function createRoutes(services) {
res.json(result);
}));
+ router.post("/llm/providers/:providerId/models/:modelId/test", asyncRoute(async (req, res) => {
+ services.authService.assertAdmin(req.currentUser);
+ const result = await services.largeLanguageModelService.testModel(
+ req.params.providerId,
+ req.params.modelId || "",
+ req.body || {}
+ );
+ writeAuditLog(
+ services,
+ req,
+ "测试大语言模型 Model 连接",
+ `测试大语言模型 Model:${result.model_ref || `${req.params.providerId}/${req.params.modelId}`},结果:${result.ok ? "成功" : "失败"}`
+ );
+ res.json(result);
+ }));
+
router.delete("/llm/providers/:providerId/models/:modelId", asyncRoute(async (req, res) => {
services.authService.assertAdmin(req.currentUser);
const result = await services.largeLanguageModelService.deleteModel(
diff --git a/apps/management-website/server/src/app.js b/apps/management-website/server/src/app.js
index ca22ab7..6c9cb01 100644
--- a/apps/management-website/server/src/app.js
+++ b/apps/management-website/server/src/app.js
@@ -29,6 +29,7 @@ export function createServerApp() {
const auditLogService = new AuditLogService({ db });
const catalogSyncService = new CatalogSyncService({ db, rpcClient, auditLogService, env });
const portalService = new PortalService({ db, objectStorage, rpcClient, authService });
+ portalService.cacheLocalVersionInfo();
const largeLanguageModelService = new LargeLanguageModelService({ db, rpcClient });
const topicPingService = new TopicPingService({ db, env, rpcClient });
const agentService = new AgentService({ db, rpcClient, objectStorage, env });
diff --git a/apps/management-website/server/src/background/index.js b/apps/management-website/server/src/background/index.js
index c935420..fcd211e 100644
--- a/apps/management-website/server/src/background/index.js
+++ b/apps/management-website/server/src/background/index.js
@@ -121,6 +121,12 @@ export function startBackgroundServices({ env, services }) {
return;
}
+ try {
+ await services.portalService.refreshVersionInfoCache?.({ trigger: "startup" });
+ } catch (error) {
+ console.error("Startup version sync failed", error);
+ }
+
try {
const result = await services.catalogSyncService.syncStartupCatalogTask({ trigger: "startup" });
if (result?.status === "success") {
diff --git a/apps/management-website/server/src/db/index.js b/apps/management-website/server/src/db/index.js
index 3a79bc4..1d2cbb4 100644
--- a/apps/management-website/server/src/db/index.js
+++ b/apps/management-website/server/src/db/index.js
@@ -7,7 +7,7 @@ import { loadEnv } from "../config/env.js";
import { hashPassword } from "../utils/security.js";
const env = loadEnv();
-const SCHEMA_VERSION = 6;
+const SCHEMA_VERSION = 7;
const DEFAULT_ACCESS_TOKEN = "sk-1234567890qwertyuiop";
const DEFAULT_PORTAL_NAME = "AIOS 管理控制台";
const DEFAULT_BRAND_SUBTITLE = "Your Company Name";
@@ -288,6 +288,26 @@ function migrateV5ToV6() {
`);
}
+function createComponentVersionsTable() {
+ return `
+ CREATE TABLE IF NOT EXISTS component_versions (
+ component_key TEXT PRIMARY KEY,
+ group_name TEXT NOT NULL,
+ name TEXT NOT NULL,
+ package_name TEXT NOT NULL DEFAULT '',
+ version TEXT NOT NULL,
+ source TEXT NOT NULL,
+ display_order INTEGER NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+ );
+ `;
+}
+
+function migrateV6ToV7() {
+ db.exec(createComponentVersionsTable());
+}
+
function addAgentCreatorColumn() {
if (!hasTable("agents")) {
return;
@@ -333,6 +353,8 @@ function createSchema() {
updated_at TEXT NOT NULL
);
+ ${createComponentVersionsTable()}
+
${createStateTable("usage_refresh_state")}
${createStateTable("agent_sync_state")}
${createStateTable("llm_sync_state")}
@@ -676,6 +698,9 @@ function migrate() {
if (SCHEMA_VERSION >= 6) {
migrateV5ToV6();
}
+ if (SCHEMA_VERSION >= 7) {
+ migrateV6ToV7();
+ }
setSchemaVersion(SCHEMA_VERSION);
db.exec("COMMIT");
} catch (error) {
@@ -695,6 +720,9 @@ function migrate() {
if (SCHEMA_VERSION >= 6) {
migrateV5ToV6();
}
+ if (SCHEMA_VERSION >= 7) {
+ migrateV6ToV7();
+ }
setSchemaVersion(SCHEMA_VERSION);
db.exec("COMMIT");
} catch (error) {
@@ -711,6 +739,9 @@ function migrate() {
if (SCHEMA_VERSION >= 6) {
migrateV5ToV6();
}
+ if (SCHEMA_VERSION >= 7) {
+ migrateV6ToV7();
+ }
setSchemaVersion(SCHEMA_VERSION);
db.exec("COMMIT");
} catch (error) {
@@ -724,6 +755,9 @@ function migrate() {
if (SCHEMA_VERSION >= 6) {
migrateV5ToV6();
}
+ if (SCHEMA_VERSION >= 7) {
+ migrateV6ToV7();
+ }
setSchemaVersion(SCHEMA_VERSION);
db.exec("COMMIT");
} catch (error) {
@@ -734,6 +768,19 @@ function migrate() {
db.exec("BEGIN");
try {
migrateV5ToV6();
+ if (SCHEMA_VERSION >= 7) {
+ migrateV6ToV7();
+ }
+ setSchemaVersion(SCHEMA_VERSION);
+ db.exec("COMMIT");
+ } catch (error) {
+ db.exec("ROLLBACK");
+ throw error;
+ }
+ } else if (version === 6 && SCHEMA_VERSION >= 7) {
+ db.exec("BEGIN");
+ try {
+ migrateV6ToV7();
setSchemaVersion(SCHEMA_VERSION);
db.exec("COMMIT");
} catch (error) {
@@ -758,6 +805,7 @@ function migrate() {
}
addAgentCreatorColumn();
+ migrateV6ToV7();
ensureCoreRows();
}
diff --git a/apps/management-website/server/src/services/large-language-model-service.js b/apps/management-website/server/src/services/large-language-model-service.js
index 6313038..aaeecbc 100644
--- a/apps/management-website/server/src/services/large-language-model-service.js
+++ b/apps/management-website/server/src/services/large-language-model-service.js
@@ -333,24 +333,43 @@ export class LargeLanguageModelService {
async testProvider(providerId, payload = {}) {
const normalizedProviderId = normalizeProviderId(providerId);
- const provider = this.getProvider(normalizedProviderId);
const model = this.listModels(normalizedProviderId)[0] || null;
+ return await this.testModel(
+ normalizedProviderId,
+ payload.model_id || payload.modelId || model?.model_id,
+ payload
+ );
+ }
+
+ async testModel(providerId, modelId, payload = {}) {
+ const normalizedProviderId = normalizeProviderId(providerId);
+ const normalizedModelId = normalizeModelId(modelId);
+ const provider = this.getProvider(normalizedProviderId);
+ const model = this.getModel(normalizedProviderId, normalizedModelId);
const testedAt = new Date().toISOString();
const startedAt = Date.now();
+ const timeoutMs = payload.timeout_ms || payload.timeoutMs || 30000;
try {
- const result = await this.rpcClient.call("llm.provider.test", {
+ const result = await this.rpcClient.call("llm.model.test", {
providerId: normalizedProviderId,
- modelId: payload.model_id || payload.modelId || model?.model_id,
- timeoutMs: payload.timeout_ms || payload.timeoutMs || 30000
+ modelId: normalizedModelId,
+ modelRef: model.model_ref,
+ contextTokens: model.context_tokens,
+ maxTokens: model.max_tokens,
+ timeoutMs
}, payload.timeout_ms || payload.timeoutMs || 45000);
return {
ok: Boolean(result?.ok),
provider_id: normalizedProviderId,
provider,
- model_ref: result?.modelRef || model?.model_ref || "",
+ model_id: normalizedModelId,
+ model,
+ model_ref: result?.modelRef || model.model_ref,
base_url: result?.baseUrl || provider.base_url,
+ context_tokens: result?.contextTokens ?? model.context_tokens,
+ max_tokens: result?.outputTokens ?? model.max_tokens,
tested_at: result?.testedAt || testedAt,
response_time_ms: result?.responseTimeMs ?? (Date.now() - startedAt),
result
@@ -360,11 +379,15 @@ export class LargeLanguageModelService {
ok: false,
provider_id: normalizedProviderId,
provider,
- model_ref: model?.model_ref || "",
+ model_id: normalizedModelId,
+ model,
+ model_ref: model.model_ref,
base_url: provider.base_url,
+ context_tokens: model.context_tokens,
+ max_tokens: model.max_tokens,
tested_at: testedAt,
response_time_ms: Date.now() - startedAt,
- error_message: error.message || "Provider 测试失败",
+ error_message: error.message || "Model 测试失败",
error_details: error.details || null
};
}
diff --git a/apps/management-website/server/src/services/portal-service.js b/apps/management-website/server/src/services/portal-service.js
index 2511af9..d46dedc 100644
--- a/apps/management-website/server/src/services/portal-service.js
+++ b/apps/management-website/server/src/services/portal-service.js
@@ -1,4 +1,7 @@
import { createHash } from "node:crypto";
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
import AdmZip from "adm-zip";
@@ -15,6 +18,69 @@ const LOGO_MIME_TYPES = new Map([
["image/bmp", "bmp"],
["image/x-ms-bmp", "bmp"]
]);
+const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "..");
+const LOCAL_VERSION_COMPONENTS = [
+ {
+ group: "apps",
+ packagePath: "apps/management-website/package.json"
+ },
+ {
+ group: "apps",
+ packagePath: "apps/aios-app-invoke-proxy-service/package.json"
+ }
+];
+
+function readPackageMetadata(relativePath) {
+ const packagePath = path.join(REPO_ROOT, relativePath);
+ try {
+ const metadata = JSON.parse(fs.readFileSync(packagePath, "utf8"));
+ return {
+ name: firstText(metadata?.name, path.basename(path.dirname(packagePath))),
+ version: firstText(metadata?.version, "unknown")
+ };
+ } catch {
+ return {
+ name: path.basename(path.dirname(packagePath)),
+ version: "unknown"
+ };
+ }
+}
+
+function normalizeVersionItem(item, fallbackGroup = "kernal") {
+ const packageName = firstText(item?.package_name, item?.packageName, item?.name);
+ const name = packageName === "openclaw"
+ ? "core"
+ : firstText(item?.name, item?.displayName, packageName);
+
+ return {
+ group: firstText(item?.group, fallbackGroup),
+ name: name || "-",
+ package_name: packageName || name || "-",
+ version: firstText(item?.version, "unknown")
+ };
+}
+
+function versionComponentKey(item) {
+ return `${item.group || "unknown"}:${item.package_name || item.name || "unknown"}`;
+}
+
+function toVersionCacheRow(row) {
+ return {
+ group: row.group_name,
+ name: row.name,
+ package_name: row.package_name,
+ version: row.version,
+ source: row.source,
+ updated_at: row.updated_at
+ };
+}
+
+function getErrorMessage(error, fallback = "Unknown error") {
+ if (error instanceof Error && error.message) {
+ return error.message;
+ }
+ return error ? String(error) : fallback;
+}
function ensureZipContains(buffer, requiredName) {
const zip = new AdmZip(buffer);
@@ -259,6 +325,101 @@ export class PortalService {
};
}
+ getVersionInfo() {
+ const rows = this.db.prepare(`
+ SELECT group_name, name, package_name, version, source, updated_at
+ FROM component_versions
+ ORDER BY display_order ASC, name ASC
+ `).all();
+ const generatedAt = rows.reduce((latest, row) => (
+ row.updated_at && row.updated_at > latest ? row.updated_at : latest
+ ), "");
+
+ return {
+ generated_at: generatedAt || null,
+ items: rows.map(toVersionCacheRow)
+ };
+ }
+
+ localVersionItems() {
+ return LOCAL_VERSION_COMPONENTS.map((component) => {
+ const metadata = readPackageMetadata(component.packagePath);
+ return normalizeVersionItem({
+ group: component.group,
+ name: component.displayName || metadata.name,
+ package_name: metadata.name,
+ version: metadata.version
+ }, component.group);
+ });
+ }
+
+ writeVersionInfoCache(items, source) {
+ const now = new Date().toISOString();
+ const normalizedItems = Array.isArray(items)
+ ? items.map((item) => normalizeVersionItem(item)).filter((item) => item.name && item.version)
+ : [];
+ const removeSource = this.db.prepare("DELETE FROM component_versions WHERE source = ?");
+ const upsert = this.db.prepare(`
+ INSERT INTO component_versions (
+ component_key, group_name, name, package_name, version, source,
+ display_order, created_at, updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(component_key) DO UPDATE SET
+ group_name = excluded.group_name,
+ name = excluded.name,
+ package_name = excluded.package_name,
+ version = excluded.version,
+ source = excluded.source,
+ display_order = excluded.display_order,
+ updated_at = excluded.updated_at
+ `);
+
+ removeSource.run(source);
+ normalizedItems.forEach((item, index) => {
+ upsert.run(
+ versionComponentKey(item),
+ item.group,
+ item.name,
+ item.package_name,
+ item.version,
+ source,
+ source === "local" ? index : index + LOCAL_VERSION_COMPONENTS.length,
+ now,
+ now
+ );
+ });
+ }
+
+ cacheLocalVersionInfo() {
+ this.writeVersionInfoCache(this.localVersionItems(), "local");
+ return this.getVersionInfo();
+ }
+
+ async refreshVersionInfoCache({ timeoutMs = 15000 } = {}) {
+ this.cacheLocalVersionInfo();
+
+ if (
+ typeof this.rpcClient?.call !== "function"
+ || (typeof this.rpcClient?.isConfigured === "function" && !this.rpcClient.isConfigured())
+ ) {
+ return this.getVersionInfo();
+ }
+
+ try {
+ const remote = await this.rpcClient.call("version.list", {}, timeoutMs);
+ const remoteItems = Array.isArray(remote?.items)
+ ? remote.items.map((item) => normalizeVersionItem(item, "kernal"))
+ : [];
+ this.writeVersionInfoCache(remoteItems, "management-service");
+ return this.getVersionInfo();
+ } catch (error) {
+ return {
+ ...this.getVersionInfo(),
+ error_message: getErrorMessage(error, "version.list failed")
+ };
+ }
+ }
+
listAccessTokens() {
return this.db.prepare(`
SELECT token, created_at, updated_at
diff --git a/apps/management-website/server/test/background-services.test.js b/apps/management-website/server/test/background-services.test.js
index d3b7643..4f02b32 100644
--- a/apps/management-website/server/test/background-services.test.js
+++ b/apps/management-website/server/test/background-services.test.js
@@ -61,6 +61,9 @@ function createServices({
calls.push("rpc.stop");
}
},
+ portalService: {
+ refreshVersionInfoCache: taskFactory("sync-version")
+ },
catalogSyncService: {
syncStartupCatalogTask: taskFactory("sync-startup-catalog"),
syncCatalogTask: taskFactory("sync-catalog"),
@@ -97,6 +100,7 @@ it("schedules catalog and token sync from completion times after startup catalog
expect(calls).toEqual([
"rpc.start",
+ "sync-version:startup",
"sync-startup-catalog:startup"
]);
expect(timers.activeTimeouts().map((timer) => timer.delay)).toEqual([
@@ -154,6 +158,7 @@ it("does not schedule token sync when startup catalog sync fails", async () => {
await flushMicrotasks();
expect(calls).toEqual([
"rpc.start",
+ "sync-version:startup",
"sync-startup-catalog:startup"
]);
expect(timers.activeTimeouts().map((timer) => timer.delay)).toEqual([
@@ -202,6 +207,7 @@ it("waits for a management RPC ping before startup sync", async () => {
await flushMicrotasks();
expect(calls.filter((entry) => entry.endsWith(":startup"))).toEqual([
+ "sync-version:startup",
"sync-startup-catalog:startup"
]);
} finally {
@@ -261,6 +267,7 @@ it("retries management RPC readiness before startup sync failure is marked", asy
"rpc.start",
"rpc.call:service.ping:1",
"rpc.call:service.ping:2",
+ "sync-version:startup",
"sync-startup-catalog:startup"
]);
expect(startupFailures).toEqual([]);
diff --git a/apps/management-website/server/test/db-reset-migration.test.js b/apps/management-website/server/test/db-reset-migration.test.js
index 688c97e..c18b470 100644
--- a/apps/management-website/server/test/db-reset-migration.test.js
+++ b/apps/management-website/server/test/db-reset-migration.test.js
@@ -105,6 +105,11 @@ it("resets drifted legacy schemas to the latest database structure", async () =>
FROM sqlite_master
WHERE type = 'table' AND name = 'llm_providers'
`).get();
+ const componentVersionsTable = db.prepare(`
+ SELECT name
+ FROM sqlite_master
+ WHERE type = 'table' AND name = 'component_versions'
+ `).get();
const llmModelTable = db.prepare(`
SELECT name
FROM sqlite_master
@@ -146,10 +151,11 @@ it("resets drifted legacy schemas to the latest database structure", async () =>
const settingsColumns = db.prepare("PRAGMA table_info(settings)").all().map((row) => row.name);
- expect(schemaVersion?.value).toBe("6");
+ expect(schemaVersion?.value).toBe("7");
expect(settingsColumns).toContain("logo_mime_type");
expect(settingsColumns).toContain("logo_data");
expect(aiosUsersTable?.name).toBe("aios_users");
+ expect(componentVersionsTable?.name).toBe("component_versions");
expect(agentColumns).toContain("template_name");
expect(agentColumns).toContain("llm_provider_id");
expect(agentColumns).toContain("llm_model_id");
@@ -301,7 +307,7 @@ it("migrates v1 agents to template relationships without dropping data", async (
ORDER BY slug
`).all();
- expect(schemaVersion?.value).toBe("6");
+ expect(schemaVersion?.value).toBe("7");
expect(agentColumns).toContain("template_name");
expect(agentColumns).toContain("llm_model_ref");
expect(agentColumns).toContain("created_by");
@@ -422,7 +428,7 @@ it("migrates v3 templates to default llm columns and removes template descriptio
const templateColumns = db.prepare("PRAGMA table_info(agent_templates)").all().map((row) => row.name);
const template = db.prepare("SELECT template_name, default_llm_model_ref FROM agent_templates WHERE template_name = ?").get("default");
- expect(schemaVersion?.value).toBe("6");
+ expect(schemaVersion?.value).toBe("7");
expect(templateColumns).toContain("default_llm_provider_id");
expect(templateColumns).toContain("default_llm_model_id");
expect(templateColumns).toContain("default_llm_model_ref");
@@ -506,7 +512,7 @@ it("migrates v4 settings through uploaded logo columns without dropping data", a
const schemaVersion = db.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").get();
const settings = db.prepare("SELECT portal_name, brand_subtitle, theme_color, logo_mime_type, logo_data FROM settings WHERE id = 1").get();
- expect(schemaVersion?.value).toBe("6");
+ expect(schemaVersion?.value).toBe("7");
expect(settings).toEqual({
portal_name: "AIOS",
brand_subtitle: "Test",
@@ -647,7 +653,7 @@ it("migrates v5 templates by dropping template description without dropping data
WHERE template_name = ?
`).get("default");
- expect(schemaVersion?.value).toBe("6");
+ expect(schemaVersion?.value).toBe("7");
expect(templateColumns).not.toContain("description");
expect(template).toEqual({
id: 1,
diff --git a/apps/management-website/server/test/large-language-model-route.test.js b/apps/management-website/server/test/large-language-model-route.test.js
index d203e09..1b39172 100644
--- a/apps/management-website/server/test/large-language-model-route.test.js
+++ b/apps/management-website/server/test/large-language-model-route.test.js
@@ -109,3 +109,63 @@ it("passes encoded model id through a single decode step", async () => {
await close(server);
}
});
+
+it("routes model test requests with encoded model id", async () => {
+ const calls = [];
+ const app = express();
+ app.use(express.json());
+ app.use("/api", createRoutes({
+ authService: {
+ getLocalApiUser() {
+ return {
+ id: 1,
+ username: "aios",
+ role: "aios-admin"
+ };
+ },
+ assertAdmin() {}
+ },
+ auditLogService: {
+ write() {}
+ },
+ largeLanguageModelService: {
+ async testModel(providerId, modelId, payload) {
+ calls.push({ providerId, modelId, payload });
+ return {
+ ok: true,
+ provider_id: providerId,
+ model_id: modelId,
+ model_ref: `${providerId}/${modelId}`
+ };
+ }
+ },
+ portalService: {},
+ catalogSyncService: {},
+ env: {}
+ }));
+
+ const server = await listen(app);
+ try {
+ const address = server.address();
+ const modelId = "gpt%mini";
+ const response = await requestJson(
+ `http://127.0.0.1:${address.port}/api/llm/providers/corp-openai/models/${encodeURIComponent(modelId)}/test`,
+ {
+ method: "POST",
+ body: { timeout_ms: 1234 }
+ }
+ );
+
+ expect(response.status).toBe(200);
+ expect(response.body.model_id).toBe(modelId);
+ expect(calls).toEqual([{
+ providerId: "corp-openai",
+ modelId,
+ payload: {
+ timeout_ms: 1234
+ }
+ }]);
+ } finally {
+ await close(server);
+ }
+});
diff --git a/apps/management-website/server/test/large-language-model-service.test.js b/apps/management-website/server/test/large-language-model-service.test.js
index 48d92d0..cfdfa04 100644
--- a/apps/management-website/server/test/large-language-model-service.test.js
+++ b/apps/management-website/server/test/large-language-model-service.test.js
@@ -300,7 +300,7 @@ it("keeps existing OpenAI compatible v1 provider URL unchanged", async () => {
}
});
-it("tests provider through management RPC with the first local model", async () => {
+it("tests model through management RPC with local context and output limits", async () => {
const db = createDb();
const { service, calls } = createService(db, (_action, payload) => ({
ok: true,
@@ -308,21 +308,28 @@ it("tests provider through management RPC with the first local model", async ()
modelId: payload.modelId,
modelRef: `${payload.providerId}/${payload.modelId}`,
baseUrl: "https://api.example.com/v1",
+ contextTokens: payload.contextTokens,
+ outputTokens: payload.maxTokens,
responseTimeMs: 25,
testedAt: TEST_NOW
}));
try {
- const result = await service.testProvider("corp-openai");
+ const result = await service.testModel("corp-openai", "gpt-4.1-mini");
expect(result.ok).toBe(true);
expect(result.model_ref).toBe("corp-openai/gpt-4.1-mini");
+ expect(result.context_tokens).toBe(32768);
+ expect(result.max_tokens).toBe(4096);
expect(result.response_time_ms).toBe(25);
expect(calls).toEqual([{
- action: "llm.provider.test",
+ action: "llm.model.test",
payload: {
providerId: "corp-openai",
modelId: "gpt-4.1-mini",
+ modelRef: "corp-openai/gpt-4.1-mini",
+ contextTokens: 32768,
+ maxTokens: 4096,
timeoutMs: 30000
}
}]);
diff --git a/apps/management-website/server/test/portal-service-alignment.test.js b/apps/management-website/server/test/portal-service-alignment.test.js
index 1451cbe..8c4c60a 100644
--- a/apps/management-website/server/test/portal-service-alignment.test.js
+++ b/apps/management-website/server/test/portal-service-alignment.test.js
@@ -25,6 +25,24 @@ function createDb() {
};
}
+function createVersionDb() {
+ const db = new DatabaseSync(":memory:");
+ db.exec(`
+ CREATE TABLE component_versions (
+ component_key TEXT PRIMARY KEY,
+ group_name TEXT NOT NULL,
+ name TEXT NOT NULL,
+ package_name TEXT NOT NULL DEFAULT '',
+ version TEXT NOT NULL,
+ source TEXT NOT NULL,
+ display_order INTEGER NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+ );
+ `);
+ return db;
+}
+
function createSkillZip({ name = "skill-a", description = "Skill A description" } = {}) {
const zip = new AdmZip();
zip.addFile("SKILL.md", Buffer.from([
@@ -133,6 +151,88 @@ it("returns dashboard metrics after agent and usage startup sync complete", () =
}]);
});
+it("returns component version info from the local cache", () => {
+ const db = createVersionDb();
+ db.prepare(`
+ INSERT INTO component_versions (
+ component_key, group_name, name, package_name, version, source,
+ display_order, created_at, updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `).run(
+ "kernal:openclaw",
+ "kernal",
+ "core",
+ "openclaw",
+ "2026.5.28",
+ "management-service",
+ 1,
+ "2026-06-17T00:00:00.000Z",
+ "2026-06-17T00:00:00.000Z"
+ );
+ const service = new PortalService({
+ db,
+ objectStorage: {},
+ rpcClient: {},
+ authService: {}
+ });
+
+ const versionInfo = service.getVersionInfo();
+
+ expect(versionInfo.generated_at).toBe("2026-06-17T00:00:00.000Z");
+ expect(versionInfo.items).toEqual([{
+ group: "kernal",
+ name: "core",
+ package_name: "openclaw",
+ version: "2026.5.28",
+ source: "management-service",
+ updated_at: "2026-06-17T00:00:00.000Z"
+ }]);
+});
+
+it("refreshes component version cache from local packages and version.list", async () => {
+ const db = createVersionDb();
+ const calls = [];
+ const service = new PortalService({
+ db,
+ objectStorage: {},
+ rpcClient: {
+ isConfigured() {
+ return true;
+ },
+ async call(action, params, timeoutMs) {
+ calls.push({ action, params, timeoutMs });
+ return {
+ version: 1,
+ items: [
+ { group: "kernal", name: "aios-apps-invoke-cli", packageName: "aios-apps-invoke-cli", version: "0.0.1" },
+ { group: "kernal", name: "aios-management-serivce", packageName: "aios-management-serivce", version: "0.4.0" },
+ { group: "kernal", name: "aios-mqtt-channel", packageName: "aios-mqtt-channel", version: "0.1.0" },
+ { group: "kernal", name: "core", packageName: "openclaw", version: "2026.5.28" }
+ ]
+ };
+ }
+ },
+ authService: {}
+ });
+
+ const versionInfo = await service.refreshVersionInfoCache({ timeoutMs: 1234 });
+
+ expect(calls).toEqual([{
+ action: "version.list",
+ params: {},
+ timeoutMs: 1234
+ }]);
+ expect(versionInfo.items.map((item) => item.name)).toEqual([
+ "aios-management-web",
+ "aios-app-invoke-proxy-service",
+ "aios-apps-invoke-cli",
+ "aios-management-serivce",
+ "aios-mqtt-channel",
+ "core"
+ ]);
+ expect(versionInfo.items.find((item) => item.name === "core")?.package_name).toBe("openclaw");
+});
+
it("updates portal settings without touching uploaded logo data", () => {
const calls = [];
let settings = {
diff --git a/apps/management-website/src/app/App.jsx b/apps/management-website/src/app/App.jsx
index c124be9..3793c99 100644
--- a/apps/management-website/src/app/App.jsx
+++ b/apps/management-website/src/app/App.jsx
@@ -165,6 +165,7 @@ export default function App() {
const templateSync = boot?.template_sync;
const systemSync = boot?.system_sync;
const usageRefresh = boot?.usage_refresh;
+ const versionInfo = boot?.version_info;
useEffect(() => {
document.title = getPortalTitle(activeSettings);
@@ -226,6 +227,7 @@ export default function App() {
templateSyncStatus={templateSync}
systemSyncStatus={systemSync}
usageRefreshStatus={usageRefresh}
+ versionInfo={versionInfo}
notify={messageApi}
onSave={async (values) => {
const next = await api.put("/api/settings", {
@@ -303,7 +305,7 @@ export default function App() {
default:
return null;
}
- }, [accessTokens, agentSync, currentUser, dashboardRefreshKey, llmSync, messageApi, selectedKey, settings, skillSync, systemSync, templateSync, usageRefresh]);
+ }, [accessTokens, agentSync, currentUser, dashboardRefreshKey, llmSync, messageApi, selectedKey, settings, skillSync, systemSync, templateSync, usageRefresh, versionInfo]);
return (
{
- setProviderTestingId(row.provider_id);
+ const handleModelTest = async (row) => {
+ setModelTestingRef(row.model_ref);
try {
- const result = await api.post(`/api/llm/providers/${encodePath(row.provider_id)}/test`, {});
- setProviderTestResult({
+ const result = await api.post(
+ `/api/llm/providers/${encodePath(row.provider_id)}/models/${encodePath(row.model_id)}/test`,
+ {}
+ );
+ setModelTestResult({
providerId: row.provider_id,
+ modelRef: row.model_ref,
ok: Boolean(result?.ok),
data: result,
errorMessage: result?.error_message || ""
});
- setProviderTestOpen(true);
+ setModelTestOpen(true);
} catch (error) {
- setProviderTestResult({
+ setModelTestResult({
providerId: row.provider_id,
+ modelRef: row.model_ref,
ok: false,
data: null,
- errorMessage: error.message || "Provider 连接测试失败"
+ errorMessage: error.message || "Model 连接测试失败"
});
- setProviderTestOpen(true);
+ setModelTestOpen(true);
} finally {
- setProviderTestingId("");
+ setModelTestingRef("");
}
};
@@ -344,17 +349,6 @@ export function LargeLanguageModelsPage() {
} onClick={() => openEditProvider(row)}>
编辑
- }
- data-testid={`provider-test-${row.provider_id}`}
- loading={providerTestingId === row.provider_id}
- onClick={() => {
- void handleProviderTest(row);
- }}
- >
- 测试连接
-
0 || Number(row.agent_count || 0) > 0}
title={`确认删除服务商 ${row.provider_id} 吗?`}
@@ -413,6 +407,17 @@ export function LargeLanguageModelsPage() {
} onClick={() => openEditModel(row)}>
编辑
+ }
+ data-testid={`model-test-${row.model_ref}`}
+ loading={modelTestingRef === row.model_ref}
+ onClick={() => {
+ void handleModelTest(row);
+ }}
+ >
+ 测试连接
+
0}
title={`确认删除模型 ${row.model_ref} 吗?`}
@@ -433,27 +438,27 @@ export function LargeLanguageModelsPage() {
/>
{
- setProviderTestOpen(false);
- setProviderTestResult(null);
+ setModelTestOpen(false);
+ setModelTestResult(null);
}}
>
确定
)}
onCancel={() => {
- setProviderTestOpen(false);
- setProviderTestResult(null);
+ setModelTestOpen(false);
+ setModelTestResult(null);
}}
>
-
+
- {providerTestResult?.ok ? "成功" : "失败"}
+
+ {modelTestResult?.ok ? "成功" : "失败"}
)
},
{
key: "model",
label: "测试模型",
- children: providerTestResult?.data?.model_ref || providerTestResult?.data?.result?.modelRef || "-"
+ children: modelTestResult?.data?.model_ref || modelTestResult?.data?.result?.modelRef || modelTestResult?.modelRef || "-"
},
{
key: "base_url",
label: "基础地址",
- children: providerTestResult?.data?.base_url || providerTestResult?.data?.result?.baseUrl || "-"
+ children: modelTestResult?.data?.base_url || modelTestResult?.data?.result?.baseUrl || "-"
+ },
+ {
+ key: "context",
+ label: "Context",
+ children: renderTokens(
+ modelTestResult?.data?.context_tokens
+ ?? modelTestResult?.data?.result?.contextTokens
+ ?? modelTestResult?.data?.result?.openClawCall?.context?.contextTokens
+ )
+ },
+ {
+ key: "output",
+ label: "Output",
+ children: renderTokens(
+ modelTestResult?.data?.max_tokens
+ ?? modelTestResult?.data?.result?.outputTokens
+ ?? modelTestResult?.data?.result?.openClawCall?.output?.maxTokens
+ )
},
{
key: "status",
label: "HTTP 状态",
- children: providerTestResult?.data?.result?.status ?? "-"
+ children: modelTestResult?.data?.result?.status ?? "-"
},
{
key: "response_time",
label: "耗时",
- children: providerTestResult?.data?.response_time_ms
- ? `${providerTestResult.data.response_time_ms} ms`
+ children: modelTestResult?.data?.response_time_ms
+ ? `${modelTestResult.data.response_time_ms} ms`
: "-"
},
{
key: "tested_at",
label: "测试时间",
- children: providerTestResult?.data?.tested_at
- ? formatDateTime(providerTestResult.data.tested_at)
+ children: modelTestResult?.data?.tested_at
+ ? formatDateTime(modelTestResult.data.tested_at)
: "-"
},
{
key: "error",
label: "错误信息",
- children: providerTestResult?.errorMessage || providerTestResult?.data?.result?.errorMessage || "-"
+ children: modelTestResult?.errorMessage || modelTestResult?.data?.result?.errorMessage || "-"
}
]}
/>
- {formatJson(providerTestResult?.data || providerTestResult?.errorMessage || "-")}
+ {formatJson(modelTestResult?.data || modelTestResult?.errorMessage || "-")}
diff --git a/apps/management-website/src/pages/SettingsPage.jsx b/apps/management-website/src/pages/SettingsPage.jsx
index 73473fa..f3956b7 100644
--- a/apps/management-website/src/pages/SettingsPage.jsx
+++ b/apps/management-website/src/pages/SettingsPage.jsx
@@ -367,6 +367,16 @@ function buildUsageSyncRows(usageRefreshStatus) {
});
}
+function buildVersionRows(versionInfo) {
+ return Array.isArray(versionInfo?.items)
+ ? versionInfo.items.map((item, index) => ({
+ key: `${item.name || "unknown"}-${item.version || index}-${index}`,
+ name: String(item.name || "-").replace(/^aios-/, ""),
+ version: item.version || "unknown"
+ }))
+ : [];
+}
+
export function SettingsPage({
settings,
logoSrc,
@@ -377,6 +387,7 @@ export function SettingsPage({
templateSyncStatus,
systemSyncStatus,
usageRefreshStatus,
+ versionInfo,
notify,
onSave,
onUploadLogo,
@@ -622,6 +633,7 @@ export function SettingsPage({
}
];
const usageSyncRows = buildUsageSyncRows(usageRefreshStatus);
+ const versionRows = buildVersionRows(versionInfo);
return (
<>
@@ -847,6 +859,30 @@ export function SettingsPage({
+