feat: discover codex app-server capabilities

This commit is contained in:
AI Bot
2026-05-31 03:44:02 +08:00
parent 4800352e22
commit f333676c36
16 changed files with 662 additions and 5 deletions

View File

@@ -50,6 +50,42 @@ test("device detail exposes gui cli capability state and preferred execution mod
assert.equal(cards.capabilities.items.preferredExecutionMode, "默认执行模式CLI");
});
test("device detail exposes Codex App Server discovered model and extension summary", async () => {
await setup();
const state = await readState();
const device = state.devices.find((item) => item.id === "mac-studio");
assert.ok(device);
device!.capabilities = {
...(device!.capabilities ?? {}),
codexAppServer: {
connected: true,
lastSeenAt: "2026-05-31T10:00:00.000Z",
lastActiveProjectId: "",
metadata: {
models: [
{ id: "gpt-5.4", displayName: "GPT-5.4" },
{ id: "gpt-5.4-mini", displayName: "GPT-5.4 mini" },
],
defaultModelId: "gpt-5.4",
fastModelId: "gpt-5.4-mini",
deepModelId: "gpt-5.4",
skills: [{ name: "image2-ui-prototype" }],
plugins: [{ id: "github" }],
apps: [{ id: "canva" }],
},
},
};
await writeState(state);
const workspace = getDeviceWorkspaceView(await readState(), "mac-studio");
const cards = buildDeviceWorkspaceDetailCards(workspace);
assert.equal(cards.capabilities.items.codexAppServer, "Codex App Server已连接");
assert.equal(cards.capabilities.items.codexModels, "模型2 个 · 默认 gpt-5.4 · 快速 gpt-5.4-mini · 深度 gpt-5.4");
assert.equal(cards.capabilities.items.codexExtensions, "扩展Skill 1 个 · Plugin 1 个 · App 1 个");
});
test("device detail exposes folder and project conflict skeleton from workspace policy", async () => {
await setup();

View File

@@ -0,0 +1,74 @@
import test from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import { mkdtemp, rm } from "node:fs/promises";
import { NextRequest } from "next/server";
let runtimeRoot = "";
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let deviceHeartbeatRoute: (typeof import("../src/app/api/device-heartbeat/route"))["POST"];
async function setup() {
if (runtimeRoot) return;
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-device-capability-metadata-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [data, heartbeatModule] = await Promise.all([
import("../src/lib/boss-data.ts"),
import("../src/app/api/device-heartbeat/route.ts"),
]);
readState = data.readState;
deviceHeartbeatRoute = heartbeatModule.POST;
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test("device heartbeat preserves Codex App Server capability metadata", async () => {
await setup();
const state = await readState();
const device = state.devices.find((item) => item.id === "mac-studio");
assert.ok(device, "expected seeded mac-studio device");
const response = await deviceHeartbeatRoute(
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
deviceId: device!.id,
token: device!.token,
name: device!.name,
avatar: device!.avatar,
account: device!.account,
status: "online",
quota5h: device!.quota5h,
quota7d: device!.quota7d,
projects: device!.projects,
endpoint: device!.endpoint,
capabilities: {
codexAppServer: {
connected: true,
lastSeenAt: "2026-05-31T10:00:00.000Z",
metadata: {
models: [{ id: "gpt-5.4", displayName: "GPT-5.4" }],
defaultModelId: "gpt-5.4",
fastModelId: "gpt-5.4-mini",
providerCapabilities: { webSearch: true },
},
},
},
}),
}),
);
assert.equal(response.status, 200);
const nextState = await readState();
const updatedDevice = nextState.devices.find((item) => item.id === device!.id);
assert.equal(updatedDevice?.capabilities?.codexAppServer.metadata?.models?.[0]?.id, "gpt-5.4");
assert.equal(updatedDevice?.capabilities?.codexAppServer.metadata?.providerCapabilities?.webSearch, true);
});

View File

@@ -31,6 +31,131 @@ rl.on("line", (line) => {
return;
}
if (message.method === "model/list") {
send({
id: message.id,
result: {
data: [
{
id: "gpt-5.4",
model: "gpt-5.4",
displayName: "GPT-5.4",
description: "Deep reasoning model",
hidden: false,
supportedReasoningEfforts: ["low", "medium", "high"],
defaultReasoningEffort: "medium",
inputModalities: ["text", "image"],
supportsPersonality: true,
serviceTiers: [{ id: "default", displayName: "Default" }],
defaultServiceTier: "default",
isDefault: true,
},
{
id: "gpt-5.4-mini",
model: "gpt-5.4-mini",
displayName: "GPT-5.4 mini",
description: "Fast response model",
hidden: false,
supportedReasoningEfforts: ["none", "low"],
defaultReasoningEffort: "none",
inputModalities: ["text"],
supportsPersonality: true,
serviceTiers: [],
defaultServiceTier: null,
isDefault: false,
},
],
nextCursor: null,
},
});
return;
}
if (message.method === "modelProvider/capabilities/read") {
send({
id: message.id,
result: {
namespaceTools: true,
imageGeneration: true,
webSearch: true,
},
});
return;
}
if (message.method === "skills/list") {
send({
id: message.id,
result: {
data: [
{
cwd: "/Users/kris/code/boss",
skills: [
{
name: "image2-ui-prototype",
description: "Generate high fidelity UI prototypes",
path: "/Users/kris/.codex/skills/image2-ui-prototype/SKILL.md",
scope: "user",
enabled: true,
},
],
errors: [],
},
],
},
});
return;
}
if (message.method === "plugin/list") {
send({
id: message.id,
result: {
marketplaces: [
{
name: "local",
path: "/Users/kris/.codex/plugins/marketplace.json",
interface: null,
plugins: [
{
id: "github",
remotePluginId: null,
localVersion: "1.0.0",
name: "GitHub",
installed: true,
enabled: true,
keywords: ["repo"],
},
],
},
],
marketplaceLoadErrors: [],
featuredPluginIds: ["github"],
},
});
return;
}
if (message.method === "app/list") {
send({
id: message.id,
result: {
data: [
{
id: "canva",
name: "Canva",
description: "Design app",
isAccessible: true,
isEnabled: true,
pluginDisplayNames: ["Canva"],
},
],
nextCursor: null,
},
});
return;
}
if (message.method === "thread/resume") {
send({
id: message.id,

View File

@@ -189,3 +189,85 @@ test("local-agent heartbeat reports Codex App Server capability only when enable
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test("local-agent heartbeat reports Codex App Server discovered models, skills, plugins, and apps", async () => {
const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-local-agent-app-server-discovery-"));
const skillsDir = path.join(runtimeRoot, "skills");
await mkdir(skillsDir, { recursive: true });
const mockControlPlane = await startMockControlPlane();
const exampleConfig = JSON.parse(
await readFile(path.join(repoRoot, "local-agent", "config.example.json"), "utf8"),
);
const configPath = path.join(runtimeRoot, "config.json");
await writeFile(
configPath,
JSON.stringify(
{
...exampleConfig,
bindHost: "127.0.0.1",
port: 0,
controlPlaneUrl: `http://127.0.0.1:${mockControlPlane.port}`,
heartbeatIntervalMs: 60_000,
masterAgentPollIntervalMs: 60_000,
masterAgentEnabled: false,
codexSessionDiscoveryEnabled: false,
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
codexAppServerDiscoveryTtlMs: 60_000,
projects: [],
projectCandidates: [],
skillsDir,
},
null,
2,
),
"utf8",
);
const child = spawn(process.execPath, ["local-agent/server.mjs", configPath], {
cwd: repoRoot,
stdio: ["ignore", "pipe", "pipe"],
});
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr += String(chunk);
});
try {
const heartbeatRequest = await Promise.race([
mockControlPlane.heartbeatReceived,
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`timed out waiting for heartbeat\n${stderr}`));
}, 8000);
}),
]);
const payload = JSON.parse(heartbeatRequest.bodyText);
const metadata = payload.capabilities.codexAppServer.metadata;
assert.equal(payload.capabilities.codexAppServer.connected, true);
assert.equal(metadata.models[0].id, "gpt-5.4");
assert.equal(metadata.models[0].displayName, "GPT-5.4");
assert.equal(metadata.defaultModelId, "gpt-5.4");
assert.equal(metadata.fastModelId, "gpt-5.4-mini");
assert.equal(metadata.providerCapabilities.webSearch, true);
assert.equal(metadata.skills[0].name, "image2-ui-prototype");
assert.equal(metadata.plugins[0].id, "github");
assert.equal(metadata.apps[0].id, "canva");
} finally {
child.kill("SIGTERM");
await new Promise((resolve) => {
child.once("close", resolve);
}).catch(() => null);
await new Promise((resolve) => {
mockControlPlane.server.close(resolve);
});
await rm(runtimeRoot, { recursive: true, force: true });
}
});