fix: expose AgentRun skill assembly in v02 web

This commit is contained in:
Codex
2026-06-03 07:02:09 +08:00
parent b57ceff959
commit e2d4989468
9 changed files with 126 additions and 0 deletions
@@ -36,6 +36,8 @@ Code Agent 第一版 skill 来源分为两类:预装 skill 读取镜像内只
- HWLAB 自有发现路径:`internal/cloud/codex-stdio-session-helpers.ts``discoverSkillsForStdio()` 读取 `HWLAB_CODE_AGENT_SKILLS_DIRS`,扫描每个目录下直接或一级子目录中的 `SKILL.md`,并把 `Current skills discovery facts` 注入 Codex prompt。
- Codex 原生发现路径:Codex 会从工作区 `.agents/skills` 等标准位置加载 skill;因此 `hwlab-cloud-api` 必须准备 `/workspace/hwlab/.agents/skills` symlink 聚合目录,让 Codex 原生机制也能看到 `/app/skills``/data/user-skills` 中的 skill。
- 两条发现路径都必须保留来源信息;同名 skill 不能因为 HWLAB 侧去重而消失。
- AgentRun 装配发现路径:HWLAB v0.2 通过 AgentRun `ResourceBundleRef.skillRefs` 把 repo 内 `skills/device-pod-cli/SKILL.md``skills/hwlab-agent-runtime/SKILL.md` 装配到 runner 工作区 `.agents/skills``GET /v1/skills` 必须额外返回 `agentRunAssembly` 摘要,包含 `skillRefs``promptRefs``toolAliases`、资源 bundle repo/branch/commit 和 `.agents/skills` 运行时装配目录;Cloud Web skills 面板必须展示该摘要,不能只展示 `/app/skills``/data/user-skills` 的静态列表。
- MiniMax-M3 runner 引导必须说明工具调用 JSON、BusyBox/GNU 工具差异和 GitHub CLI 读 issue/PR 的低噪声用法,降低无效 tool-call arguments 与命令兼容性摩擦。
## API 接口说明
+38
View File
@@ -18,6 +18,44 @@ export const DEFAULT_HWLAB_AGENTRUN_SKILL_REFS = Object.freeze([
{ name: "hwlab-agent-runtime", path: "skills/hwlab-agent-runtime/SKILL.md", required: true, aggregateAs: "hwlab-agent-runtime" }
]);
export function createHwlabAgentRunAssemblySummary(options = {}) {
const env = options.env ?? process.env;
const toolAliases = normalizeResourceToolAliases(options.toolAliases ?? DEFAULT_HWLAB_AGENTRUN_TOOL_ALIASES);
const promptRefs = normalizeResourcePromptRefs(options.promptRefs ?? DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS);
const skillRefs = normalizeResourceSkillRefs(options.skillRefs ?? DEFAULT_HWLAB_AGENTRUN_SKILL_REFS);
const repoUrl = optionalString(options.repoUrl ?? env.HWLAB_CODE_AGENT_AGENTRUN_REPO_URL ?? env.AGENTRUN_REPO_URL) ?? DEFAULT_HWLAB_AGENTRUN_REPO_URL;
const branch = optionalString(options.branch ?? env.HWLAB_BOOT_REF ?? env.HWLAB_CODE_AGENT_AGENTRUN_BRANCH ?? env.AGENTRUN_BRANCH) ?? DEFAULT_HWLAB_AGENTRUN_BRANCH;
const commitId = optionalString(options.commitId ?? env.HWLAB_COMMIT_ID ?? env.HWLAB_GIT_SHA ?? env.HWLAB_REVISION);
const namespace = optionalString(options.namespace ?? env.AGENTRUN_RUNTIME_NAMESPACE ?? env.HWLAB_CODE_AGENT_AGENTRUN_NAMESPACE) ?? DEFAULT_AGENTRUN_NAMESPACE;
const managerUrl = optionalString(options.managerUrl ?? env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL ?? env.AGENTRUN_MANAGER_URL) ?? DEFAULT_AGENTRUN_MANAGER_URL;
return {
status: skillRefs.length > 0 ? "ready" : "degraded",
provider: "agentrun-v01",
namespace,
managerUrl,
resourceBundle: {
kind: "git",
repoUrl,
branch,
commitId,
skillsDir: ".agents/skills"
},
counts: {
toolAliases: toolAliases.length,
promptRefs: promptRefs.length,
skillRefs: skillRefs.length
},
toolAliases: toolAliases.map((item) => ({ ...item })),
promptRefs: promptRefs.map((item) => ({ ...item })),
skillRefs: skillRefs.map((item) => ({ ...item })),
runtimeEnv: {
assembledRuntime: "HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME",
skillsDirs: "HWLAB_CODE_AGENT_SKILLS_DIRS"
},
valuesPrinted: false
};
}
export const AGENTRUN_REUSABLE_CREDENTIAL_ENV_NAMES = Object.freeze([
"AUTH_PASSWORD",
"CODEX_API_KEY",
@@ -18,4 +18,11 @@ Do not use fallback execution paths:
When the user asks what skills are visible, mention the HWLAB bundle skills by name and manifest path before any generic model/system skill list.
MiniMax-M3 tool-call guidance:
- Keep every command tool call argument as valid JSON. Put shell metacharacters, pipes, quotes, and newlines inside one command string instead of emitting partial JSON fragments.
- Prefer `rg` for repository search. If `rg` is unavailable, use `find ... -name` plus plain `grep`; BusyBox `grep` may not support GNU flags such as `--include`.
- For GitHub issue/PR reads, prefer `gh issue view <number> --repo owner/name --json title,body,state,comments` or `gh pr view ... --json ...`; avoid plain `gh issue view` output that can be polluted by Projects Classic GraphQL warnings.
- If a command fails because the command arguments were malformed, report the malformed command and retry once with a shorter single-purpose command.
When the user asks to compile or operate `D601-F103-V2`, start from the Device Pod path with `hwpod bootsharp --pod-id D601-F103-V2`, then use `hwpod` job/status/output commands as needed. If the Device Pod path returns a named blocker, report that blocker and do not switch to fallback paths.
+5
View File
@@ -57,6 +57,11 @@ test("cloud api skills upload/list/tree/preview keeps preinstalled and uploaded
assert.equal(list.status, 200);
assert.equal(list.body.roots.preinstalled, preinstalledDir);
assert.equal(list.body.roots.uploaded, userSkillsDir);
assert.equal(list.body.agentRunAssembly.status, "ready");
assert.equal(list.body.agentRunAssembly.resourceBundle.skillsDir, ".agents/skills");
assert.deepEqual(list.body.agentRunAssembly.skillRefs.map((item) => item.name), ["device-pod-cli", "hwlab-agent-runtime"]);
assert.deepEqual(list.body.agentRunAssembly.promptRefs.map((item) => item.name), ["hwlab-v02-runtime"]);
assert.deepEqual(list.body.agentRunAssembly.toolAliases.map((item) => item.name), ["hwpod", "unidesk-ssh"]);
const sameName = list.body.skills.filter((skill) => skill.name === "same-name");
assert.equal(sameName.length, 2);
assert.deepEqual(sameName.map((skill) => skill.source).sort(), ["preinstalled", "uploaded"]);
+2
View File
@@ -18,6 +18,7 @@ import {
firstMarkdownSummary,
parseFrontmatter
} from "./codex-stdio-session-helpers.ts";
import { createHwlabAgentRunAssemblySummary } from "../agent/agentrun-dispatch.mjs";
export const DEFAULT_PREINSTALLED_SKILLS_DIR = "/app/skills";
export const DEFAULT_USER_SKILLS_DIR = "/data/user-skills";
@@ -79,6 +80,7 @@ export async function listSkills(options = {}) {
uploaded: config.userSkillsDir,
codexAggregation: config.codexSkillsDir
},
agentRunAssembly: createHwlabAgentRunAssemblySummary({ env }),
count: skills.length,
skills
};
+45
View File
@@ -32,11 +32,13 @@ async function loadSkillsSurface(options = {}) {
state.skills.error = response.error || "skills 列表不可用";
state.skills.items = [];
state.skills.roots = null;
state.skills.agentRunAssembly = null;
renderSkillsSurface();
return;
}
state.skills.items = Array.isArray(response.data?.skills) ? response.data.skills : [];
state.skills.roots = response.data?.roots ?? null;
state.skills.agentRunAssembly = response.data?.agentRunAssembly ?? null;
if (!state.skills.selectedId && state.skills.items.length > 0) {
state.skills.selectedId = state.skills.items[0].id;
} else if (state.skills.selectedId && !state.skills.items.some((skill) => skill.id === state.skills.selectedId)) {
@@ -164,10 +166,53 @@ function renderSkillsSurface() {
el.skillStatus.className = `state-tag tone-${toneClass(state.skills.error ? "blocked" : state.skills.loading || state.skills.uploadPending ? "pending" : "source")}`;
el.skillUploadButton.disabled = state.skills.uploadPending;
el.skillRefresh.disabled = state.skills.loading;
renderAgentRunAssembly();
renderSkillList();
renderSkillPreview();
}
function renderAgentRunAssembly() {
const assembly = state.skills.agentRunAssembly;
if (!assembly) {
replaceChildren(el.skillAssembly, skillNotice(state.skills.loading ? "正在读取 AgentRun skill 装配摘要。" : "AgentRun skill 装配摘要未返回。"));
return;
}
const skillRefs = safeAssemblyItems(assembly.skillRefs);
const toolAliases = safeAssemblyItems(assembly.toolAliases);
const promptRefs = safeAssemblyItems(assembly.promptRefs);
const wrap = document.createElement("div");
wrap.className = `skill-assembly-body tone-border-${toneClass(assembly.status || "source")}`;
const head = document.createElement("div");
head.className = "skill-assembly-head";
head.append(textSpan("AgentRun 装配", "skill-name"), badge(assembly.status || "unknown", assembly.status || "source"));
const meta = document.createElement("div");
meta.className = "skill-meta";
meta.append(...[
textSpan(`skills=${Number(assembly.counts?.skillRefs) || skillRefs.length}`),
textSpan(`prompts=${Number(assembly.counts?.promptRefs) || promptRefs.length}`),
textSpan(`tools=${Number(assembly.counts?.toolAliases) || toolAliases.length}`),
assembly.resourceBundle?.skillsDir ? textSpan(`runtime=${assembly.resourceBundle.skillsDir}`, "mono wrap") : null,
assembly.resourceBundle?.commitId ? textSpan(`commit=${String(assembly.resourceBundle.commitId).slice(0, 12)}`, "mono wrap") : null
].filter(Boolean));
const skills = document.createElement("div");
skills.className = "skill-assembly-list";
skills.append(...skillRefs.map((item) => badge(item.name, "source")));
const detail = document.createElement("div");
detail.className = "skill-meta";
detail.append(
textSpan(`promptRefs=${promptRefs.map((item) => item.name).join(",") || "none"}`, "mono wrap"),
textSpan(`toolAliases=${toolAliases.map((item) => item.name).join(",") || "none"}`, "mono wrap")
);
wrap.append(head, meta, skills, detail);
replaceChildren(el.skillAssembly, wrap);
}
function safeAssemblyItems(items) {
return Array.isArray(items)
? items.filter((item) => item && typeof item === "object" && item.name).map((item) => ({ ...item, name: String(item.name) }))
: [];
}
function renderSkillList() {
if (state.skills.loading && state.skills.items.length === 0) {
replaceChildren(el.skillList, skillNotice("正在读取 /v1/skills。"));
+2
View File
@@ -142,6 +142,7 @@ const el = {
skillUploadForm: byId("skill-upload-form"),
skillUploadInput: byId("skill-upload-input"),
skillUploadButton: byId("skill-upload-button"),
skillAssembly: byId("skill-assembly"),
skillList: byId("skill-list"),
skillDetailTitle: byId("skill-detail-title"),
skillTree: byId("skill-tree"),
@@ -222,6 +223,7 @@ const state = {
previewLoading: false,
items: [],
roots: null,
agentRunAssembly: null,
selectedId: null,
detail: null,
preview: null,
+1
View File
@@ -122,6 +122,7 @@
</label>
<button class="command-button" id="skill-upload-button" type="submit">上传</button>
</form>
<div id="skill-assembly" class="skill-assembly" aria-live="polite"></div>
<div id="skill-list" class="skill-list" aria-label="Skill 列表" aria-live="polite"></div>
</section>
<section class="skill-detail-panel" aria-label="Skill 文件">
+24
View File
@@ -531,6 +531,7 @@ h3 {
color: var(--text);
}
.skill-assembly,
.skill-list,
.skill-tree,
.skill-preview {
@@ -540,6 +541,7 @@ h3 {
overscroll-behavior: contain;
}
.skill-assembly,
.skill-list,
.skill-tree {
display: grid;
@@ -547,6 +549,28 @@ h3 {
gap: 7px;
}
.skill-assembly-body {
min-width: 0;
display: grid;
gap: 7px;
padding: 9px;
border: 1px solid var(--line);
background: var(--surface-2);
}
.skill-assembly-head,
.skill-assembly-list {
min-width: 0;
display: flex;
align-items: center;
gap: 7px;
flex-wrap: wrap;
}
.skill-assembly-head {
justify-content: space-between;
}
.skill-row,
.skill-notice,
.skill-tree-entry {