diff --git a/docs/reference/spec-v02-hwlab-agent-skills.md b/docs/reference/spec-v02-hwlab-agent-skills.md index 85d9a926..a6e7b87b 100644 --- a/docs/reference/spec-v02-hwlab-agent-skills.md +++ b/docs/reference/spec-v02-hwlab-agent-skills.md @@ -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 接口说明 diff --git a/internal/agent/agentrun-dispatch.mjs b/internal/agent/agentrun-dispatch.mjs index fc8deddc..2f3cabf9 100644 --- a/internal/agent/agentrun-dispatch.mjs +++ b/internal/agent/agentrun-dispatch.mjs @@ -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", diff --git a/internal/agent/prompts/hwlab-v02-runtime.md b/internal/agent/prompts/hwlab-v02-runtime.md index 4a3d91f0..8a17f3c4 100644 --- a/internal/agent/prompts/hwlab-v02-runtime.md +++ b/internal/agent/prompts/hwlab-v02-runtime.md @@ -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 --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. diff --git a/internal/cloud/server-skills.test.ts b/internal/cloud/server-skills.test.ts index ebe1e330..ae7d435d 100644 --- a/internal/cloud/server-skills.test.ts +++ b/internal/cloud/server-skills.test.ts @@ -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"]); diff --git a/internal/cloud/skills-store.ts b/internal/cloud/skills-store.ts index 71b6a5b7..f8a86be8 100644 --- a/internal/cloud/skills-store.ts +++ b/internal/cloud/skills-store.ts @@ -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 }; diff --git a/web/hwlab-cloud-web/app-skills.ts b/web/hwlab-cloud-web/app-skills.ts index 6742f25d..2698cee9 100644 --- a/web/hwlab-cloud-web/app-skills.ts +++ b/web/hwlab-cloud-web/app-skills.ts @@ -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。")); diff --git a/web/hwlab-cloud-web/app.ts b/web/hwlab-cloud-web/app.ts index a019f1db..872ac768 100644 --- a/web/hwlab-cloud-web/app.ts +++ b/web/hwlab-cloud-web/app.ts @@ -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, diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 1d244974..82b7d468 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -122,6 +122,7 @@ +
diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index e4c371b6..a20653cd 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -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 {