Merge pull request #730 from pikasTech/fix/issue729-skills-assembly
fix: expose AgentRun skill assembly in v02 web
This commit is contained in:
@@ -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 接口说明
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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。"));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 文件">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user