diff --git a/internal/agent/agentrun-dispatch.mjs b/internal/agent/agentrun-dispatch.mjs index 2f3cabf9..eb59e07a 100644 --- a/internal/agent/agentrun-dispatch.mjs +++ b/internal/agent/agentrun-dispatch.mjs @@ -23,6 +23,7 @@ export function createHwlabAgentRunAssemblySummary(options = {}) { 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 promptAssembly = createPromptAssemblySummary(promptRefs); 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); @@ -47,6 +48,7 @@ export function createHwlabAgentRunAssemblySummary(options = {}) { }, toolAliases: toolAliases.map((item) => ({ ...item })), promptRefs: promptRefs.map((item) => ({ ...item })), + promptAssembly, skillRefs: skillRefs.map((item) => ({ ...item })), runtimeEnv: { assembledRuntime: "HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME", @@ -56,6 +58,30 @@ export function createHwlabAgentRunAssemblySummary(options = {}) { }; } +function createPromptAssemblySummary(promptRefs) { + const refs = promptRefs.map((item) => ({ + name: item.name, + path: item.path, + inject: item.inject, + stage: promptInjectStageLabel(item.inject), + required: item.required !== false, + source: "ResourceBundleRef.promptRefs" + })); + return { + status: refs.length > 0 ? "ready" : "missing", + count: refs.length, + requiredCount: refs.filter((item) => item.required).length, + injectStages: [...new Set(refs.map((item) => item.inject))], + refs, + valuesPrinted: false + }; +} + +function promptInjectStageLabel(inject) { + if (inject === "thread-start") return "thread-start initial prompt"; + return inject; +} + export const AGENTRUN_REUSABLE_CREDENTIAL_ENV_NAMES = Object.freeze([ "AUTH_PASSWORD", "CODEX_API_KEY", diff --git a/internal/cloud/server-skills.test.ts b/internal/cloud/server-skills.test.ts index ae7d435d..ea969106 100644 --- a/internal/cloud/server-skills.test.ts +++ b/internal/cloud/server-skills.test.ts @@ -61,6 +61,18 @@ test("cloud api skills upload/list/tree/preview keeps preinstalled and uploaded 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.equal(list.body.agentRunAssembly.promptAssembly.status, "ready"); + assert.equal(list.body.agentRunAssembly.promptAssembly.valuesPrinted, false); + assert.equal(list.body.agentRunAssembly.promptAssembly.requiredCount, 1); + assert.deepEqual(list.body.agentRunAssembly.promptAssembly.injectStages, ["thread-start"]); + assert.deepEqual(list.body.agentRunAssembly.promptAssembly.refs, [{ + name: "hwlab-v02-runtime", + path: "internal/agent/prompts/hwlab-v02-runtime.md", + inject: "thread-start", + stage: "thread-start initial prompt", + required: true, + source: "ResourceBundleRef.promptRefs" + }]); 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); diff --git a/web/hwlab-cloud-web/app-skills.ts b/web/hwlab-cloud-web/app-skills.ts index 2698cee9..e96934fd 100644 --- a/web/hwlab-cloud-web/app-skills.ts +++ b/web/hwlab-cloud-web/app-skills.ts @@ -180,6 +180,7 @@ function renderAgentRunAssembly() { const skillRefs = safeAssemblyItems(assembly.skillRefs); const toolAliases = safeAssemblyItems(assembly.toolAliases); const promptRefs = safeAssemblyItems(assembly.promptRefs); + const promptAssembly = safePromptAssembly(assembly.promptAssembly, promptRefs); const wrap = document.createElement("div"); wrap.className = `skill-assembly-body tone-border-${toneClass(assembly.status || "source")}`; const head = document.createElement("div"); @@ -200,13 +201,91 @@ function renderAgentRunAssembly() { 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); + wrap.append(head, meta, skills, renderPromptAssembly(promptAssembly), detail); replaceChildren(el.skillAssembly, wrap); } +function renderPromptAssembly(promptAssembly) { + const refs = promptAssembly.refs; + const section = document.createElement("div"); + section.className = "skill-prompt-assembly"; + const head = document.createElement("div"); + head.className = "skill-prompt-head"; + head.append(textSpan("引导 prompt 装配", "skill-prompt-title"), badge(promptAssembly.status || "unknown", promptAssembly.status || "source")); + const meta = document.createElement("div"); + meta.className = "skill-prompt-meta"; + meta.append( + textSpan(`refs=${Number(promptAssembly.count) || refs.length}`), + textSpan(`required=${Number(promptAssembly.requiredCount) || refs.filter((item) => item.required).length}`), + textSpan(`inject=${promptAssembly.injectStages.join(",") || "none"}`, "mono wrap") + ); + const list = document.createElement("div"); + list.className = "skill-prompt-list"; + if (refs.length === 0) { + list.append(textSpan("未声明引导 prompt refs。", "skill-prompt-empty")); + } else { + list.append(...refs.map(renderPromptAssemblyRef)); + } + section.append(head, meta, list); + return section; +} + +function renderPromptAssemblyRef(item) { + const row = document.createElement("div"); + row.className = "skill-prompt-row"; + const head = document.createElement("div"); + head.className = "skill-prompt-row-head"; + head.append(textSpan(item.name, "skill-prompt-name mono wrap"), badge(item.required ? "required" : "optional", item.required ? "source" : "pending")); + const meta = document.createElement("div"); + meta.className = "skill-prompt-meta"; + meta.append( + textSpan(`path=${item.path}`, "mono wrap"), + textSpan(`inject=${item.inject}`, "mono wrap"), + textSpan(`stage=${item.stage}`, "mono wrap"), + textSpan(`source=${item.source}`, "mono wrap") + ); + row.append(head, meta); + return row; +} + +function safePromptAssembly(promptAssembly, fallbackPromptRefs) { + const fallbackRefs = fallbackPromptRefs.map((item) => ({ + ...item, + path: String(item.path || "unknown"), + inject: String(item.inject || "thread-start"), + stage: String(item.stage || "thread-start initial prompt"), + required: item.required !== false, + source: String(item.source || "ResourceBundleRef.promptRefs") + })); + if (!promptAssembly || typeof promptAssembly !== "object") { + return createSafePromptAssembly(fallbackRefs); + } + const refs = safeAssemblyItems(promptAssembly.refs).map((item) => ({ + ...item, + path: String(item.path || "unknown"), + inject: String(item.inject || "thread-start"), + stage: String(item.stage || "thread-start initial prompt"), + required: item.required !== false, + source: String(item.source || "ResourceBundleRef.promptRefs") + })); + return createSafePromptAssembly(refs, promptAssembly); +} + +function createSafePromptAssembly(refs, promptAssembly = {}) { + const injectStages = Array.isArray(promptAssembly.injectStages) + ? promptAssembly.injectStages.map((item) => String(item)).filter(Boolean) + : [...new Set(refs.map((item) => item.inject).filter(Boolean))]; + return { + status: String(promptAssembly.status || (refs.length > 0 ? "ready" : "missing")), + count: Number(promptAssembly.count) || refs.length, + requiredCount: Number(promptAssembly.requiredCount) || refs.filter((item) => item.required).length, + injectStages, + refs + }; +} + function safeAssemblyItems(items) { return Array.isArray(items) ? items.filter((item) => item && typeof item === "object" && item.name).map((item) => ({ ...item, name: String(item.name) })) diff --git a/web/hwlab-cloud-web/scripts/check.ts b/web/hwlab-cloud-web/scripts/check.ts index 76575ff7..39818715 100644 --- a/web/hwlab-cloud-web/scripts/check.ts +++ b/web/hwlab-cloud-web/scripts/check.ts @@ -73,6 +73,11 @@ assert.match(app, /function\s+initSkillsPanel\s*\(/u, "missing initSkillsPanel") assert.match(app, /function\s+loadSkillsSurface\s*\(/u, "missing loadSkillsSurface"); assert.match(app, /function\s+uploadSelectedSkillFiles\s*\(/u, "missing uploadSelectedSkillFiles"); assert.match(app, /function\s+renderSkillPreview\s*\(/u, "missing renderSkillPreview"); +assertIncludes(app, "promptAssembly", "skills frontend must render AgentRun prompt assembly summary"); +assertIncludes(app, "引导 prompt 装配", "skills frontend must label the prompt assembly panel"); +assertIncludes(app, "ResourceBundleRef.promptRefs", "skills frontend must expose prompt assembly source metadata"); +assertIncludes(styles, ".skill-prompt-assembly", "skills frontend must style prompt assembly rows"); +assertIncludes(styles, ".skill-prompt-row", "skills frontend must style prompt assembly refs"); console.log("hwlab-cloud-web check: skills contracts verified"); for (const term of [ diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index a20653cd..f797cb1f 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -571,6 +571,72 @@ h3 { justify-content: space-between; } +.skill-prompt-assembly { + min-width: 0; + display: grid; + gap: 6px; + padding-top: 7px; + border-top: 1px solid var(--line); +} + +.skill-prompt-head, +.skill-prompt-row-head, +.skill-prompt-meta { + min-width: 0; + display: flex; + align-items: center; + gap: 7px; + flex-wrap: wrap; +} + +.skill-prompt-head, +.skill-prompt-row-head { + justify-content: space-between; +} + +.skill-prompt-list { + min-width: 0; + display: grid; + gap: 0; +} + +.skill-prompt-row { + min-width: 0; + display: grid; + gap: 5px; + padding: 7px 0; + border-top: 1px solid var(--line); +} + +.skill-prompt-row:first-child { + border-top: 0; + padding-top: 0; +} + +.skill-prompt-title, +.skill-prompt-name, +.skill-prompt-empty, +.skill-prompt-meta { + min-width: 0; + overflow-wrap: anywhere; +} + +.skill-prompt-title, +.skill-prompt-name { + color: var(--text); + font-weight: 800; +} + +.skill-prompt-empty, +.skill-prompt-meta { + color: var(--muted); + font-size: 11px; +} + +.skill-prompt-meta { + font-family: var(--mono); +} + .skill-row, .skill-notice, .skill-tree-entry {