beeos-claw 0.6.85 → 0.6.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -342,15 +342,39 @@ Fetch tests cover success, missing Host authority, mismatched run/file bindings,
|
|
|
342
342
|
changed object origins, corrupt bytes, HTTP rejection, and an ended attempt.
|
|
343
343
|
They also cover default, relative, and absolute destinations, missing workspace,
|
|
344
344
|
and preserving factory names and per-agent context on discovery/restart.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
345
|
+
When the user does not request another file format, real report acceptance must
|
|
346
|
+
generate one HTML file, fetch the same file ID successfully when the model
|
|
347
|
+
verifies it, and retain one summary and one HTML delivery in live events and
|
|
348
|
+
stored history. An explicitly requested file format takes precedence in the
|
|
349
|
+
system prompt, legacy Canvas note, tool guidance, and finalization retry. The
|
|
350
|
+
agent must create the actual requested file, check its contents, and wait for a
|
|
351
|
+
successful typed upload outcome and the same call's durable file card before
|
|
352
|
+
claiming delivery. HTML, a filename in prose, or a renamed HTML file cannot
|
|
353
|
+
replace a requested Markdown, CSV, PDF, or other supported file. Do not add an
|
|
354
|
+
extra HTML copy unless requested, or fail the turn after confirmed delivery.
|
|
350
355
|
This applies to all supported upload formats, not only Markdown. Failed uploads,
|
|
351
356
|
success text alone, and files from another turn do not pass the delivery guard.
|
|
352
|
-
Markdown acceptance must
|
|
353
|
-
|
|
357
|
+
Markdown acceptance must use the ordinary request "输出一份md格式给我", retain a
|
|
358
|
+
real `.md` file with Markdown content and its file card, one summary, and a
|
|
359
|
+
completed task across live events and history. It must not produce an extra
|
|
360
|
+
HTML attachment. Prompt and guard regression tests do not replace this real
|
|
361
|
+
model acceptance. This requested-format correction passed selected-instance
|
|
362
|
+
staging acceptance on 2026-10-19: published `beeos-claw@0.6.86` from source
|
|
363
|
+
`3b8a487596a675491eedc69cef7a871f150fe90d`, image
|
|
364
|
+
`sha256:01932b7c6c820e09f18abab61cd267adb8bd74ed67fab008feb2b33f4c73b2a8`,
|
|
365
|
+
upgraded only `inst_46a590294607d9d4b9a50adb` with a verified upgrade receipt.
|
|
366
|
+
The original task's Run 4 uploaded `waimao-tools.md` as `text/markdown` (4,290
|
|
367
|
+
bytes), downloaded as actual Markdown, and opened its heading and table in iOS.
|
|
368
|
+
That run did not call `render_ui`. A separate real regression generated and
|
|
369
|
+
downloaded a new Markdown file without HTML, then delivered explicitly requested
|
|
370
|
+
HTML while preserving access to the earlier Markdown. Other instances and
|
|
371
|
+
default images were unchanged. See [selected staging evidence](docs/static-html-native-tool-result.md#selected-staging-acceptance).
|
|
372
|
+
|
|
373
|
+
This correction does not add a plain-text-only delivery mode. Without a
|
|
374
|
+
successful file upload or interactive result, the existing guard still requests
|
|
375
|
+
default HTML and rejects a completed prose-only reply without its file. A user
|
|
376
|
+
request for plain text with no HTML remains a separate platform gap; it is not
|
|
377
|
+
covered by the requested-file-format fix.
|
|
354
378
|
|
|
355
379
|
Artifact generation guidance uses one executable per `exec` call so native
|
|
356
380
|
approval can bind the command and its file operands. Discovery uses `ls` / `read`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{confirmedUploadResourceLinks}from"../upload/tool-artifact-enrichment.js";import{isPlainRecord}from"../utils/text.js";import{isInteractiveUiResult,staticHtmlProjectionOf}from"./static-html-result.js";export class HtmlDeliveryGuard{published;projectedFileId;uploaded=new Map;uploadedReply=!1;revised=!1;interactive=!1;observe(e,i,
|
|
1
|
+
import{confirmedUploadResourceLinks}from"../upload/tool-artifact-enrichment.js";import{isPlainRecord}from"../utils/text.js";import{isInteractiveUiResult,staticHtmlProjectionOf}from"./static-html-result.js";export class HtmlDeliveryGuard{published;projectedFileId;uploaded=new Map;uploadedReply=!1;revised=!1;interactive=!1;observe(e,t,i,s,l){if(s)return;const r=staticHtmlProjectionOf(e,t,i);if(r&&!this.published&&(this.published=r.result),l){const s=confirmedUploadResourceLinks({toolCallId:l,toolName:e,params:isPlainRecord(t)?t:void 0,rawResult:i});s.length&&this.uploaded.set(l,new Set(s.map(e=>e.uri)))}isInteractiveUiResult(e,t,i)&&(this.interactive=!0)}get publishedFileId(){return this.published?.artifact.fileId}recordReply(e){e.artifact.fileId===this.publishedFileId&&(this.projectedFileId=e.artifact.fileId)}recordUploadReply(e,t){const i=this.uploaded.get(e);i&&t.some(e=>i.has(e.uri))&&(this.uploadedReply=!0)}beforeFinalize(e){if(!(this.published||this.uploaded.size>0||this.interactive||this.revised||e.stopHookActive))return this.revised=!0,{action:"revise",reason:"html_delivery_missing",retry:{instruction:"Continue the previous request. The requested deliverable has not been published. Follow the user's explicitly requested file format: create the actual file, check its contents, call beeos_upload_file with its local path, and wait for a successful upload. Do not substitute HTML or add an HTML copy for a Markdown (.md), CSV, PDF, or other requested-format file. Only when no other file format was requested, call render_ui with id, title and self-contained html containing the completed answer, and wait for a successful static_html result; do not upload that HTML again. Claim delivery only after the matching tool succeeds, then give one brief confirmation with no duplicate answer in chat.",idempotencyKey:"beeos-html-delivery",maxAttempts:1}}}assertComplete(){if((!this.interactive&&!this.uploadedReply||this.published)&&(!this.published||this.projectedFileId!==this.published.artifact.fileId))throw new Error("html_delivery_missing: required HTML or confirmed uploaded file was not published and delivered to this reply")}}
|
package/dist/canvas/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{extractBeeosConversationIdFromSessionKey}from"../runtime/openclaw-session-key-resolution.js";const A2UI_TYPE_NAMES=new Set(["Text","Image","Icon","Video","AudioPlayer","Row","Column","List","Card","Tabs","Divider","Modal","Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider","Chart","WebEmbed","FilePreview","MapView"]);function normalizeComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;for(const e of Object.keys(t))if(A2UI_TYPE_NAMES.has(e)&&"object"==typeof t[e])return{id:t.id,component:{[e]:t[e]}};return t})}const A2UI_INTERACTIVE_TYPES=new Set(["Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider"]);export const NON_INTERACTIVE_A2UI_ERROR="Display-only A2UI is not supported. Call render_ui again with id, title, and self-contained static HTML in html. A2UI root/components are reserved for interactive buttons, inputs, or component actions.";export function hasInteractiveComponent(e){return e.some(e=>{const t=e?.component;return!(!t||"object"!=typeof t)&&Object.entries(t).some(([e,t])=>A2UI_INTERACTIVE_TYPES.has(e)||Boolean(t&&"object"==typeof t&&t.action))})}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const CANVAS_COMPONENT_ID_GUIDANCE="Component IDs must differ from non-reference scalar values (usageHint, literalString, and action.name). Use type-qualified IDs such as text_heading, text_body, and column_root; do not use scalar values such as body, h1, or center as IDs. A2UI v0.8 resolves strings matching component IDs as references. Keep root, child, explicitList, and template.componentId references matching their target IDs. Reuse the same component IDs when updating a surface.";export const RENDER_UI_TOOL_DESCRIPTION="Publish a static Canvas result as self-contained HTML using id, title, and html. The result is stored as a BeeOS file and shown in the conversation. Legacy A2UI calls with id, root, and components remain available for interactive experiences.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"Surface id. Reuse to update."},title:{type:"string",description:"Human-readable title for a static HTML Canvas result."},html:{type:"string",description:"Complete self-contained static HTML. Maximum UTF-8 size: 5 MiB."},root:{type:"string",description:"Root component id."},components:{type:"array",description:"Flat ComponentInstance list. Each item has id plus one component type.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:CANVAS_COMPONENT_ID_GUIDANCE},component:{type:"object",description:"One type key: Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart."}}}},data:{type:"object",description:"Optional binding model, path keys like /name."}}};export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};const SESSION_READY_POLL_ATTEMPTS=3,SESSION_READY_POLL_INTERVAL_MS=400,delay=e=>new Promise(t=>setTimeout(t,e)),MAX_STATIC_HTML_BYTES=5242880,FILE_ID=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function renderStaticHtml(e,t,n){const a=e=>({success:!1,presentation:"static_html",error:e}),o="string"==typeof n.id?n.id.trim():"",i="string"==typeof n.title?n.title.trim():"",r=n.html;if(!o||!i||"string"!=typeof r||!r.trim())return a("id, title, and non-empty html are required");if("root"in n||"components"in n)return a("html cannot be combined with root or components");if(i.length>160)return a("Canvas title is too long (max 160 characters)");if(Buffer.byteLength(r,"utf8")>5242880)return a("Canvas HTML exceeds the 5 MiB preview limit");if(!e.publishStaticHtml)return a("Canvas file upload is unavailable");try{const n=await e.publishStaticHtml({id:o,title:i,html:r},t);return FILE_ID.test(n.fileId)&&n.uri===`beeos-file://${n.fileId}`&&"text/html"===n.mimeType?{success:!0,presentation:"static_html",surfaceId:o,artifact:{kind:"canvas",fileId:n.fileId,uri:n.uri,mimeType:"text/html",title:i}}:a("Canvas HTML upload returned an invalid file reference")}catch(e){return a(`Canvas HTML upload failed: ${e instanceof Error?e.message:String(e)}`)}}async function ensureRelayConnected(e,t,n,a){const{stateManager:o,logger:i}=e,r=a??e.getGatewaySessionKey?.(n),s=r??t,l=o.resolveCanvasForSession(s);if(l&&o.isCanvasJoined(l))return l;let c=o.getRelayClient();if(c||(c=await o.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const a=e.getPlatformAgentId?.(n),l=r??t,d=(r?extractBeeosConversationIdFromSessionKey(r):void 0)??(r&&r!==t?t:void 0),u=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(u,s),i.info?.(`[canvas] Joined canvas=${u} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),u}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,getGatewaySessionKey:o}=e;let i=0;return async(o,r)=>{const s=r;if(Object.prototype.hasOwnProperty.call(s,"html"))return renderStaticHtml(e,o,s);const l=s.id,c=s.root,d="panel";let u=s.components;if("string"==typeof u)try{u=JSON.parse(u)}catch{}if(!(l&&c&&Array.isArray(u)&&u.length))return{success:!1,error:"id, root, and components (array) are required"};const p=normalizeComponents(u);if(!hasInteractiveComponent(p))return{success:!1,error:NON_INTERACTIVE_A2UI_ERROR};if(!t.validateComponentCount(p))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(l))return n.warn?.(`[canvas] Rate limited surfaceId=${l}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:l}}),{success:!1,error:"Rate limited — too many updates per second"};const m=s.data;let h=e.getActiveSessionId?.(o);for(let t=0;!h&&t<3;t++)await delay(400),h=e.getActiveSessionId?.(o);if(!h&&e.waitForAgentSync){try{await e.waitForAgentSync()}catch{}h=e.getActiveSessionId?.(o)}if(!h)return n.warn?.(`[canvas] No active session for toolCallId=${o} after 3 polls, render_ui not ready`),{success:!1,retryable:!0,error:"Canvas session not ready yet (agent just connected) — retry render_ui shortly; do NOT fall back to plain text."};const f=e.getGatewaySessionKey?.(o),y=await ensureRelayConnected(e,h,o,f),v=t.has(l,y??void 0),g=Date.now(),T=t.getRelayClient();y&&T&&g-i>6e4&&T.createSnapshot(y,"Pre-agent edit").then(()=>{i=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),v?t.update(l,p,y,c,m):t.create(y??`_fallback_${Date.now()}`,l,d,p,{origin:{kind:"agent"},rootComponentId:c,dataModel:m});const C=p.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:v?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:l,presentation:d,componentCount:p.length,componentTypes:C}}),n.debug?.(`[canvas] ${v?"Updated":"Created"} surface=${l} components=${p.length}`);const b={success:!0,surfaceId:l,presentation:d,isUpdate:v,activeSurfaces:y?t.getAllActive(y).map(e=>e.surfaceId):[l]};return m&&(b.dataModel=m),b}}export function createRenderUiToolExecutor(e){const t=createRenderUiHandler(e);return async(e,n)=>{const a=await t(e,n),o=[{type:"text",text:JSON.stringify(a)}];if(!0===a.success&&"static_html"===a.presentation){const e=a.artifact;o.push({type:"resource_link",uri:e.uri,name:`${e.title}.html`,mimeType:e.mimeType})}return{...a,content:o,details:a,isError:!1===a.success}}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:a}=e;return async(o,i)=>{const r=i.id;if(!r)return{success:!1,error:"id is required"};const s=t.get(r),l=s?Date.now()-s.createdAt:0,c=e.getGatewaySessionKey?.(o),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(r,d),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}export const CANVAS_PRESENTATION_GATE="## BeeOS Canvas presentation gate\n\nCanvas presentation is available in the background for this BeeOS conversation. The Canvas resource tab does not need to be open and no Web subscriber is required before a Canvas tool is called. For static HTML success, say the HTML file is ready and can be clicked to preview; do not claim it was displayed on Live Canvas or that the user is viewing it.\n\nDo not read `beeos-canvas/SKILL.md` or any canvas skill file. Call `render_ui` directly. The A2UI schema is in this prompt and the `render_ui` tool.\n\nBefore the final reply, you MUST call `render_ui` with `id`, `title`, and self-contained `html` to publish the completed answer as an HTML file. This applies to short answers, weather queries and forecasts, prose explanations, reports, tables, charts, and two or more related records. A short user question is not an exception.\n\n1. Call `render_ui` directly and publish exactly one HTML file for the completed result. The tool uploads that file; do not upload it again with `beeos_upload_file` or call `render_ui` again for the same result.\n2. Do not finish with only plain text, Markdown, a Markdown table, JSON, or a prose summary. Put the actual answer in the HTML file, then keep chat to one short confirmation with the file ready to click and preview.\n3. Honor an explicitly requested alternative file format when supported. Upload that file with `beeos_upload_file` and wait for its successful result; it replaces the default HTML delivery, so do not generate an extra HTML copy. Pending questions, approvals, cancellations, and tool failures remain control/status messages; do not fabricate a successful HTML file for them.\n4. For an interaction, form, or dynamic state, use `render_form` or legacy A2UI `render_ui`.\n5. Generate PNG or PDF only when the user explicitly asks for an image, PDF, download, or export. They are not the default presentation format.\n\nDomain skills control how data is obtained and interpreted. This gate controls how the completed result is presented and overrides a domain skill's default text-summary format. When `render_ui` succeeds, keep the chat reply to one short confirmation sentence.";export const CANVAS_SYSTEM_PROMPT=`## Static Canvas HTML\n\nCall \`render_ui({"id":"report-id","title":"Report title","html":"<!doctype html>..."})\` to publish the completed non-interactive answer as one HTML file, including weather answers and prose explanations.\n\nUse a complete UTF-8 HTML document with inline CSS and inline SVG for charts. Keep it self-contained. Scripts, forms, external stylesheets, fonts, images, and network requests do not run in the preview. HTML upload has a 5 MiB limit. The mobile formatted preview accepts up to 1 MiB; larger files show truncated source instead. Static HTML is a BeeOS file artifact and does not require the Live Canvas panel or relay to be open. Do not call \`beeos_upload_file\` for the same result.\n\n## Live Canvas — Google A2UI v0.8\n\nUse legacy A2UI only for interactions, forms, and dynamic state. Use \`render_ui\`, not the built-in \`canvas\` tool.\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n${CANVAS_COMPONENT_ID_GUIDANCE}\n\nParameters:\n- \`id\`: Surface ID (e.g. "sales-report"). Reuse same id to update.\n- \`root\`: The ID of the top-level component (usually a Column).\n- \`components\`: Flat array of objects. **Each object MUST have \`id\` and \`component\`**:\n \`{"id": "myId", "component": {"Text": {"text": {"literalString": "Hello"}, "usageHint": "body"}}}\`\n The \`component\` key wraps exactly ONE type key (Text, Card, Row, etc.) and its properties.\n- \`data\` (optional): Data model object for form two-way binding.\n\n### Rules\n- Keep chat reply to ONE short sentence when using render_ui. Do NOT repeat data shown in canvas.\n- Use this A2UI schema for interactive experiences. Use static HTML files for completed non-interactive answers, including explanations.\n- Canvas renders in a narrow sidebar (~400px).\n- Prefer fewer, larger components. One table is better than many small cards.\n- Use descriptive surface IDs (e.g. "sales-report", not "ui1").\n- Same \`id\` updates existing surface; different \`id\` creates new one.\n- When asked to modify existing content, reuse the original surface id from [Canvas State].\n\n### StringValue\n\nText properties use StringValue — an object with exactly one key:\n- Static text: \`{"literalString": "Hello"}\`\n- Data binding: \`{"path": "/fieldName"}\` (reads from the \`data\` model)\n\nShorthand: \`sv("text")\` means \`{"literalString": "text"}\` in examples below.\n\n### Component Reference\n\n**Text** — Display text with semantic sizing.\n\`{Text: {text: sv("Title"), usageHint: "h2"}}\`\nusageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body"\ntext also renders Markdown (bold, italic, lists, code blocks, links).\n\n**Image** — Display an image.\n\`{Image: {url: sv("https://..."), usageHint: "mediumFeature"}}\`\nImage.url may be \`beeos-file://<fileId>\`; the renderer resolves it. Do not convert it to a signed URL.\nusageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header"\nfit (optional): "contain" | "cover" | "fill" | "none" | "scale-down"\n\n**Icon** — Material icon.\n\`{Icon: {name: sv("check")}}\`\n\n**Video** — Embed a video.\n\`{Video: {url: sv("https://youtube.com/...")}}\`\n\n**AudioPlayer** — Audio playback.\n\`{AudioPlayer: {url: sv("https://..."), description: sv("Episode 1")}}\`\n\n**Row** — Horizontal layout. Children referenced by ID.\n\`{Row: {children: {explicitList: ["child1", "child2"]}, distribution: "spaceBetween"}}\`\ndistribution: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly"\nalignment: "start" | "center" | "end" | "stretch"\n\n**Column** — Vertical layout. Children referenced by ID.\n\`{Column: {children: {explicitList: ["child1", "child2"]}}}\`\nSame distribution/alignment options as Row.\n\n**List** — Vertical or horizontal list.\n\`{List: {children: {explicitList: ["item1", "item2"]}, direction: "vertical"}}\`\n\n**Card** — Container with a single child.\n\`{Card: {child: "contentColumnId"}}\`\n\n**Tabs** — Tabbed container.\n\`{Tabs: {tabItems: [{title: sv("Tab 1"), child: "panel1"}, {title: sv("Tab 2"), child: "panel2"}]}}\`\n\n**Divider** — Horizontal or vertical separator.\n\`{Divider: {axis: "horizontal"}}\`\n\n**Modal** — Dialog with trigger.\n\`{Modal: {entryPointChild: "triggerBtnId", contentChild: "dialogContentId"}}\`\n\n**Button** — Clickable with action.\n\`{Button: {child: "labelTextId", action: {name: "onClick", context: [{key: "id", value: {literalString: "item-1"}}]}}}\`\n\n**TextField** — Text input with data binding.\n\`{TextField: {label: sv("Name"), text: {path: "/name"}, type: "shortText"}}\`\ntype: "shortText" | "number" | "date" | "longText"\n\n**CheckBox** — Boolean toggle.\n\`{CheckBox: {label: sv("Agree"), value: {path: "/agree"}}}\`\n\n**DateTimeInput** — Date/time picker.\n\`{DateTimeInput: {value: {path: "/date"}, enableDate: true, enableTime: false}}\`\n\n**MultipleChoice** — Selection from options.\n\`{MultipleChoice: {selections: {path: "/country"}, options: [{label: sv("US"), value: "US"}, {label: sv("CN"), value: "CN"}]}}\`\n\n**Slider** — Numeric range input.\n\`{Slider: {value: {path: "/volume"}, minValue: 0, maxValue: 100}}\`\n\n**Chart** (BeeOS extension) — Data visualization.\n\`{Chart: {chartType: sv("bar"), title: sv("Sales"), labels: ["Q1","Q2","Q3"], datasets: [{label: "Revenue", data: [100,200,150]}]}}\`\nchartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline\ngauge uses value + max instead of labels/datasets.\ncandlestick data: [{open,high,low,close}] (full field names, NOT o/h/l/c).\n\n### Composition\n\nNest by id: Column/Row \`children.explicitList\`, Card \`child\`. Example:\n\`{"id":"s1","root":"column_root","components":[{"id":"text_body","component":{"Text":{"text":{"literalString":"Confirm"},"usageHint":"body"}}},{"id":"button_confirm","component":{"Button":{"child":"text_body","action":{"name":"confirm"}}}},{"id":"column_root","component":{"Column":{"children":{"explicitList":["button_confirm"]}}}}]}\`\nDisplay-only tables and charts require static HTML. Forms: TextField/CheckBox + Button \`action.context\` paths.\n\n### render_form\nUse \`render_form\` for schema-driven forms when a domain schema exists.\nParameters: \`domain\`, \`operation\`, \`data\`, \`entityId\`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n`;export const CANVAS_TERM_MAPPING=`[System Note] Always use \`render_ui\` (not the canvas tool) for Canvas results. Every completed non-interactive answer, including weather, explanations, reports, tables, and charts, MUST use render_ui({"id":"report-id","title":"Report title","html":"<!doctype html>..."}) with self-contained static HTML (inline CSS and SVG, no scripts). The result is an HTML file card; tell the user it is ready to click and preview. A2UI root/components are only for interactive buttons, inputs, or component actions; display-only A2UI is rejected. Each A2UI component MUST be {"id":"x","component":{"TypeName":{props}}}. ${CANVAS_COMPONENT_ID_GUIDANCE} Forms: TextField/CheckBox/Slider + Button action.context paths, or \`render_form\` for domain schemas. Never combine html with root/components. Keep chat brief. Use \`close_ui\` to dismiss Live Canvas. Reuse the same id to update; use a new id for new content.`;export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id and provide the full updated component tree.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e,t){let n;if(!t)return null;if(n=e.getAllActive(t),0===n.length)return null;const a=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),o=n.length-a.length;let i=`[Canvas State] Active surfaces: ${a.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const a=n.types.slice(0,3).join(", "),o=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${a}${o})`}).join("; ")}.`;return o>0&&(i+=` ...and ${o} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
|
|
1
|
+
import{extractBeeosConversationIdFromSessionKey}from"../runtime/openclaw-session-key-resolution.js";const A2UI_TYPE_NAMES=new Set(["Text","Image","Icon","Video","AudioPlayer","Row","Column","List","Card","Tabs","Divider","Modal","Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider","Chart","WebEmbed","FilePreview","MapView"]);function normalizeComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;for(const e of Object.keys(t))if(A2UI_TYPE_NAMES.has(e)&&"object"==typeof t[e])return{id:t.id,component:{[e]:t[e]}};return t})}const A2UI_INTERACTIVE_TYPES=new Set(["Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider"]);export const NON_INTERACTIVE_A2UI_ERROR="Display-only A2UI is not supported. Call render_ui again with id, title, and self-contained static HTML in html. A2UI root/components are reserved for interactive buttons, inputs, or component actions.";export function hasInteractiveComponent(e){return e.some(e=>{const t=e?.component;return!(!t||"object"!=typeof t)&&Object.entries(t).some(([e,t])=>A2UI_INTERACTIVE_TYPES.has(e)||Boolean(t&&"object"==typeof t&&t.action))})}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const CANVAS_COMPONENT_ID_GUIDANCE="Component IDs must differ from non-reference scalar values (usageHint, literalString, and action.name). Use type-qualified IDs such as text_heading, text_body, and column_root; do not use scalar values such as body, h1, or center as IDs. A2UI v0.8 resolves strings matching component IDs as references. Keep root, child, explicitList, and template.componentId references matching their target IDs. Reuse the same component IDs when updating a surface.";export const RENDER_UI_TOOL_DESCRIPTION="Publish a static Canvas result as self-contained HTML using id, title, and html. The result is stored as a BeeOS file and shown in the conversation. If the user explicitly requests another file format, create and upload that file with beeos_upload_file instead; HTML does not replace it. Legacy A2UI calls with id, root, and components remain available for interactive experiences.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"Surface id. Reuse to update."},title:{type:"string",description:"Human-readable title for a static HTML Canvas result."},html:{type:"string",description:"Complete self-contained static HTML. Maximum UTF-8 size: 5 MiB."},root:{type:"string",description:"Root component id."},components:{type:"array",description:"Flat ComponentInstance list. Each item has id plus one component type.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:CANVAS_COMPONENT_ID_GUIDANCE},component:{type:"object",description:"One type key: Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart."}}}},data:{type:"object",description:"Optional binding model, path keys like /name."}}};export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};const SESSION_READY_POLL_ATTEMPTS=3,SESSION_READY_POLL_INTERVAL_MS=400,delay=e=>new Promise(t=>setTimeout(t,e)),MAX_STATIC_HTML_BYTES=5242880,FILE_ID=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function renderStaticHtml(e,t,n){const a=e=>({success:!1,presentation:"static_html",error:e}),o="string"==typeof n.id?n.id.trim():"",i="string"==typeof n.title?n.title.trim():"",r=n.html;if(!o||!i||"string"!=typeof r||!r.trim())return a("id, title, and non-empty html are required");if("root"in n||"components"in n)return a("html cannot be combined with root or components");if(i.length>160)return a("Canvas title is too long (max 160 characters)");if(Buffer.byteLength(r,"utf8")>5242880)return a("Canvas HTML exceeds the 5 MiB preview limit");if(!e.publishStaticHtml)return a("Canvas file upload is unavailable");try{const n=await e.publishStaticHtml({id:o,title:i,html:r},t);return FILE_ID.test(n.fileId)&&n.uri===`beeos-file://${n.fileId}`&&"text/html"===n.mimeType?{success:!0,presentation:"static_html",surfaceId:o,artifact:{kind:"canvas",fileId:n.fileId,uri:n.uri,mimeType:"text/html",title:i}}:a("Canvas HTML upload returned an invalid file reference")}catch(e){return a(`Canvas HTML upload failed: ${e instanceof Error?e.message:String(e)}`)}}async function ensureRelayConnected(e,t,n,a){const{stateManager:o,logger:i}=e,r=a??e.getGatewaySessionKey?.(n),s=r??t,l=o.resolveCanvasForSession(s);if(l&&o.isCanvasJoined(l))return l;let c=o.getRelayClient();if(c||(c=await o.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const a=e.getPlatformAgentId?.(n),l=r??t,d=(r?extractBeeosConversationIdFromSessionKey(r):void 0)??(r&&r!==t?t:void 0),u=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(u,s),i.info?.(`[canvas] Joined canvas=${u} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),u}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,getGatewaySessionKey:o}=e;let i=0;return async(o,r)=>{const s=r;if(Object.prototype.hasOwnProperty.call(s,"html"))return renderStaticHtml(e,o,s);const l=s.id,c=s.root,d="panel";let u=s.components;if("string"==typeof u)try{u=JSON.parse(u)}catch{}if(!(l&&c&&Array.isArray(u)&&u.length))return{success:!1,error:"id, root, and components (array) are required"};const p=normalizeComponents(u);if(!hasInteractiveComponent(p))return{success:!1,error:NON_INTERACTIVE_A2UI_ERROR};if(!t.validateComponentCount(p))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(l))return n.warn?.(`[canvas] Rate limited surfaceId=${l}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:l}}),{success:!1,error:"Rate limited — too many updates per second"};const m=s.data;let h=e.getActiveSessionId?.(o);for(let t=0;!h&&t<3;t++)await delay(400),h=e.getActiveSessionId?.(o);if(!h&&e.waitForAgentSync){try{await e.waitForAgentSync()}catch{}h=e.getActiveSessionId?.(o)}if(!h)return n.warn?.(`[canvas] No active session for toolCallId=${o} after 3 polls, render_ui not ready`),{success:!1,retryable:!0,error:"Canvas session not ready yet (agent just connected) — retry render_ui shortly; do NOT fall back to plain text."};const f=e.getGatewaySessionKey?.(o),y=await ensureRelayConnected(e,h,o,f),v=t.has(l,y??void 0),g=Date.now(),T=t.getRelayClient();y&&T&&g-i>6e4&&T.createSnapshot(y,"Pre-agent edit").then(()=>{i=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),v?t.update(l,p,y,c,m):t.create(y??`_fallback_${Date.now()}`,l,d,p,{origin:{kind:"agent"},rootComponentId:c,dataModel:m});const b=p.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:v?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:l,presentation:d,componentCount:p.length,componentTypes:b}}),n.debug?.(`[canvas] ${v?"Updated":"Created"} surface=${l} components=${p.length}`);const C={success:!0,surfaceId:l,presentation:d,isUpdate:v,activeSurfaces:y?t.getAllActive(y).map(e=>e.surfaceId):[l]};return m&&(C.dataModel=m),C}}export function createRenderUiToolExecutor(e){const t=createRenderUiHandler(e);return async(e,n)=>{const a=await t(e,n),o=[{type:"text",text:JSON.stringify(a)}];if(!0===a.success&&"static_html"===a.presentation){const e=a.artifact;o.push({type:"resource_link",uri:e.uri,name:`${e.title}.html`,mimeType:e.mimeType})}return{...a,content:o,details:a,isError:!1===a.success}}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:a}=e;return async(o,i)=>{const r=i.id;if(!r)return{success:!1,error:"id is required"};const s=t.get(r),l=s?Date.now()-s.createdAt:0,c=e.getGatewaySessionKey?.(o),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(r,d),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}export const CANVAS_PRESENTATION_GATE="## BeeOS Canvas presentation gate\n\nCanvas presentation is available in the background for this BeeOS conversation. The Canvas resource tab does not need to be open and no Web subscriber is required before a Canvas tool is called. For static HTML success, say the HTML file is ready and can be clicked to preview; do not claim it was displayed on Live Canvas or that the user is viewing it.\n\nDo not read `beeos-canvas/SKILL.md` or any canvas skill file. Call `render_ui` directly. The A2UI schema is in this prompt and the `render_ui` tool.\n\nThe user's explicitly requested file format takes precedence over the default HTML presentation. For Markdown (.md), CSV, PDF, or another supported file format, create the actual file in that format, check its contents, and call `beeos_upload_file` with its local path. Wait for a successful upload before saying the file is delivered. An HTML file is not a Markdown or other requested-format file, regardless of its title. Do not generate an extra HTML copy unless the user asks for it.\n\nWhen the user has not requested another file format, before the final reply you MUST call `render_ui` with `id`, `title`, and self-contained `html` to publish the completed answer as an HTML file. This applies to short answers, weather queries and forecasts, prose explanations, reports, tables, charts, and two or more related records. A short user question is not an exception.\n\n1. For default HTML delivery, call `render_ui` directly and publish exactly one HTML file for the completed result. The tool uploads that file; do not upload it again with `beeos_upload_file` or call `render_ui` again for the same result.\n2. For default HTML delivery, do not finish with only plain text, Markdown, a Markdown table, JSON, or a prose summary. Put the actual answer in the HTML file, then keep chat to one short confirmation with the file ready to click and preview.\n3. Honor an explicitly requested alternative file format when supported. Upload that file with `beeos_upload_file` and wait for its successful result; it replaces the default HTML delivery, so do not generate an extra HTML copy. Pending questions, approvals, cancellations, and tool failures remain control/status messages; do not fabricate a successful HTML file for them.\n4. For an interaction, form, or dynamic state, use `render_form` or legacy A2UI `render_ui`.\n5. Generate PNG or PDF only when the user explicitly asks for an image, PDF, download, or export. They are not the default presentation format.\n\nDomain skills control how data is obtained and interpreted. This gate controls how the completed result is presented and overrides a domain skill's default text-summary format, while preserving the user's explicitly requested file format. When `render_ui` succeeds, keep the chat reply to one short confirmation sentence.";export const CANVAS_SYSTEM_PROMPT=`## Static Canvas HTML\n\nWhen the user has not requested another file format, call \`render_ui({"id":"report-id","title":"Report title","html":"<!doctype html>..."})\` to publish the completed non-interactive answer as one HTML file, including weather answers and prose explanations. An explicitly requested file format takes precedence: create the actual file, check its contents, and successfully upload it with \`beeos_upload_file\` instead. HTML does not satisfy a request for Markdown (.md), CSV, PDF, or another file format. Do not produce an extra HTML copy unless requested, and do not claim delivery before the upload succeeds.\n\nUse a complete UTF-8 HTML document with inline CSS and inline SVG for charts. Keep it self-contained. Scripts, forms, external stylesheets, fonts, images, and network requests do not run in the preview. HTML upload has a 5 MiB limit. The mobile formatted preview accepts up to 1 MiB; larger files show truncated source instead. Static HTML is a BeeOS file artifact and does not require the Live Canvas panel or relay to be open. Do not call \`beeos_upload_file\` for the same result.\n\n## Live Canvas — Google A2UI v0.8\n\nUse legacy A2UI only for interactions, forms, and dynamic state. Use \`render_ui\`, not the built-in \`canvas\` tool.\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n${CANVAS_COMPONENT_ID_GUIDANCE}\n\nParameters:\n- \`id\`: Surface ID (e.g. "sales-report"). Reuse same id to update.\n- \`root\`: The ID of the top-level component (usually a Column).\n- \`components\`: Flat array of objects. **Each object MUST have \`id\` and \`component\`**:\n \`{"id": "myId", "component": {"Text": {"text": {"literalString": "Hello"}, "usageHint": "body"}}}\`\n The \`component\` key wraps exactly ONE type key (Text, Card, Row, etc.) and its properties.\n- \`data\` (optional): Data model object for form two-way binding.\n\n### Rules\n- Keep chat reply to ONE short sentence when using render_ui. Do NOT repeat data shown in canvas.\n- Use this A2UI schema for interactive experiences. Use static HTML files for completed non-interactive answers, including explanations.\n- Canvas renders in a narrow sidebar (~400px).\n- Prefer fewer, larger components. One table is better than many small cards.\n- Use descriptive surface IDs (e.g. "sales-report", not "ui1").\n- Same \`id\` updates existing surface; different \`id\` creates new one.\n- When asked to modify existing content, reuse the original surface id from [Canvas State].\n\n### StringValue\n\nText properties use StringValue — an object with exactly one key:\n- Static text: \`{"literalString": "Hello"}\`\n- Data binding: \`{"path": "/fieldName"}\` (reads from the \`data\` model)\n\nShorthand: \`sv("text")\` means \`{"literalString": "text"}\` in examples below.\n\n### Component Reference\n\n**Text** — Display text with semantic sizing.\n\`{Text: {text: sv("Title"), usageHint: "h2"}}\`\nusageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body"\ntext also renders Markdown (bold, italic, lists, code blocks, links).\n\n**Image** — Display an image.\n\`{Image: {url: sv("https://..."), usageHint: "mediumFeature"}}\`\nImage.url may be \`beeos-file://<fileId>\`; the renderer resolves it. Do not convert it to a signed URL.\nusageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header"\nfit (optional): "contain" | "cover" | "fill" | "none" | "scale-down"\n\n**Icon** — Material icon.\n\`{Icon: {name: sv("check")}}\`\n\n**Video** — Embed a video.\n\`{Video: {url: sv("https://youtube.com/...")}}\`\n\n**AudioPlayer** — Audio playback.\n\`{AudioPlayer: {url: sv("https://..."), description: sv("Episode 1")}}\`\n\n**Row** — Horizontal layout. Children referenced by ID.\n\`{Row: {children: {explicitList: ["child1", "child2"]}, distribution: "spaceBetween"}}\`\ndistribution: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly"\nalignment: "start" | "center" | "end" | "stretch"\n\n**Column** — Vertical layout. Children referenced by ID.\n\`{Column: {children: {explicitList: ["child1", "child2"]}}}\`\nSame distribution/alignment options as Row.\n\n**List** — Vertical or horizontal list.\n\`{List: {children: {explicitList: ["item1", "item2"]}, direction: "vertical"}}\`\n\n**Card** — Container with a single child.\n\`{Card: {child: "contentColumnId"}}\`\n\n**Tabs** — Tabbed container.\n\`{Tabs: {tabItems: [{title: sv("Tab 1"), child: "panel1"}, {title: sv("Tab 2"), child: "panel2"}]}}\`\n\n**Divider** — Horizontal or vertical separator.\n\`{Divider: {axis: "horizontal"}}\`\n\n**Modal** — Dialog with trigger.\n\`{Modal: {entryPointChild: "triggerBtnId", contentChild: "dialogContentId"}}\`\n\n**Button** — Clickable with action.\n\`{Button: {child: "labelTextId", action: {name: "onClick", context: [{key: "id", value: {literalString: "item-1"}}]}}}\`\n\n**TextField** — Text input with data binding.\n\`{TextField: {label: sv("Name"), text: {path: "/name"}, type: "shortText"}}\`\ntype: "shortText" | "number" | "date" | "longText"\n\n**CheckBox** — Boolean toggle.\n\`{CheckBox: {label: sv("Agree"), value: {path: "/agree"}}}\`\n\n**DateTimeInput** — Date/time picker.\n\`{DateTimeInput: {value: {path: "/date"}, enableDate: true, enableTime: false}}\`\n\n**MultipleChoice** — Selection from options.\n\`{MultipleChoice: {selections: {path: "/country"}, options: [{label: sv("US"), value: "US"}, {label: sv("CN"), value: "CN"}]}}\`\n\n**Slider** — Numeric range input.\n\`{Slider: {value: {path: "/volume"}, minValue: 0, maxValue: 100}}\`\n\n**Chart** (BeeOS extension) — Data visualization.\n\`{Chart: {chartType: sv("bar"), title: sv("Sales"), labels: ["Q1","Q2","Q3"], datasets: [{label: "Revenue", data: [100,200,150]}]}}\`\nchartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline\ngauge uses value + max instead of labels/datasets.\ncandlestick data: [{open,high,low,close}] (full field names, NOT o/h/l/c).\n\n### Composition\n\nNest by id: Column/Row \`children.explicitList\`, Card \`child\`. Example:\n\`{"id":"s1","root":"column_root","components":[{"id":"text_body","component":{"Text":{"text":{"literalString":"Confirm"},"usageHint":"body"}}},{"id":"button_confirm","component":{"Button":{"child":"text_body","action":{"name":"confirm"}}}},{"id":"column_root","component":{"Column":{"children":{"explicitList":["button_confirm"]}}}}]}\`\nDisplay-only tables and charts require static HTML. Forms: TextField/CheckBox + Button \`action.context\` paths.\n\n### render_form\nUse \`render_form\` for schema-driven forms when a domain schema exists.\nParameters: \`domain\`, \`operation\`, \`data\`, \`entityId\`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n`;export const CANVAS_TERM_MAPPING=`[System Note] Always use \`render_ui\` (not the canvas tool) for Canvas results. The user's explicitly requested file format takes precedence: create and check the actual file, then successfully upload it with beeos_upload_file before claiming delivery; do not substitute HTML or add an extra HTML copy unless requested. When no other file format is requested, every completed non-interactive answer, including weather, explanations, reports, tables, and charts, MUST use render_ui({"id":"report-id","title":"Report title","html":"<!doctype html>..."}) with self-contained static HTML (inline CSS and SVG, no scripts). The result is an HTML file card; tell the user it is ready to click and preview. A2UI root/components are only for interactive buttons, inputs, or component actions; display-only A2UI is rejected. Each A2UI component MUST be {"id":"x","component":{"TypeName":{props}}}. ${CANVAS_COMPONENT_ID_GUIDANCE} Forms: TextField/CheckBox/Slider + Button action.context paths, or \`render_form\` for domain schemas. Never combine html with root/components. Keep chat brief. Use \`close_ui\` to dismiss Live Canvas. Reuse the same id to update; use a new id for new content.`;export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id and provide the full updated component tree.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e,t){let n;if(!t)return null;if(n=e.getAllActive(t),0===n.length)return null;const a=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),o=n.length-a.length;let i=`[Canvas State] Active surfaces: ${a.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const a=n.types.slice(0,3).join(", "),o=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${a}${o})`}).join("; ")}.`;return o>0&&(i+=` ...and ${o} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{access,mkdir,rename,rm,writeFile}from"node:fs/promises";import{homedir}from"node:os";import{basename,dirname,join,resolve,sep}from"node:path";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";export class RuntimeSkillsService{deps;constructor(e){this.deps=e}async execute(e,t,i){const a=e,
|
|
1
|
+
import{access,mkdir,rename,rm,writeFile}from"node:fs/promises";import{homedir}from"node:os";import{basename,dirname,isAbsolute,join,relative,resolve,sep}from"node:path";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";export class RuntimeSkillsService{deps;constructor(e){this.deps=e}async execute(e,t,i){const a=e,s=this.requireGateway(),r=a.context.agent.runtimeLocalKey;if("skills.list"===a.name){const e=await s.request("skills.status",{agentId:r}),t=await this.skillsRoot(s,r);return{skills:normalizeSkills(e?.skills,t)}}if("skills.setEnabled"===a.name){const e=a,t=await this.skillsRoot(s,r),i=await s.request("skills.status",{agentId:r}),n=i?.skills?.find(i=>isRecord(i)&&installedDirectoryKey(i,t)===e.params.skillKey),o=isRecord(n)?nativeSkillKey(n):e.params.skillKey;return await s.request("skills.update",{skillKey:o||e.params.skillKey,enabled:e.params.enabled}),{skillKey:e.params.skillKey,enabled:e.params.enabled}}const n=await this.skillsRoot(s,r);if("skills.uninstall"===a.name){const e=validateSkillKey(a.params.skillKey);return await rm(contained(n,e),{recursive:!0,force:!0}),{removed:!0}}const o=a,l=await i.resources.readVerifiedArtifact(o.params.artifact),c=l.manifest?.slug??"";if(!c)throw new RuntimeCommandError("INVALID_PARAMS","skill package declares no manifest slug (manifest.json with schema_version 1 and slug is required)");const m=contained(n,validateSkillKey(c)),d=dirname(m),u=contained(d,`.${basename(m)}.${o.context.operationId}.tmp`),f=contained(d,`.${basename(m)}.${o.context.operationId}.backup`);await mkdir(d,{recursive:!0}),!await exists(m)&&await exists(f)&&await rename(f,m),await rm(u,{recursive:!0,force:!0}),await mkdir(u,{recursive:!0});try{for(const[e,t]of l.workspaceFiles){const i=contained(u,normalizeArtifactPath(e));await mkdir(dirname(i),{recursive:!0}),await writeFile(i,t,{mode:384})}await rm(f,{recursive:!0,force:!0}),await exists(m)&&await rename(m,f),await rename(u,m),await rm(f,{recursive:!0,force:!0})}catch(e){throw await rm(u,{recursive:!0,force:!0}).catch(()=>{}),!await exists(m)&&await exists(f)&&await rename(f,m).catch(()=>{}),new RuntimeCommandError("EXECUTION_FAILED",`skill install write failed (files=${l.workspaceFiles.size}): ${e instanceof Error?e.message:String(e)}`,void 0,{cause:e})}return{installed:!0}}requireGateway(){const e=this.deps.getGateway();if(!e?.isConnected)throw new RuntimeCommandError("GATEWAY_NOT_CONNECTED","Gateway not connected");return e}async skillsRoot(e,t){const i=this.deps.getOpenclawHome?.()??process.env.OPENCLAW_HOME??join(homedir(),".openclaw");if("main"===t)return join(i,"skills");const a=await e.request("agents.list",{}),s=a?.agents?.find(e=>e.id===t)?.workspace;return join(s||join(i,`workspace-${t}`),"skills")}}async function exists(e){try{return await access(e),!0}catch{return!1}}function validateSkillKey(e){if(!/^(?:@?[A-Za-z0-9][A-Za-z0-9._-]{0,63}\/)?[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(e))throw new RuntimeCommandError("INVALID_PARAMS","invalid skill key");return e}function normalizeArtifactPath(e){const t=e.replaceAll("\\","/").replace(/^\.\//,"");if(!t||t.startsWith("/")||t.split("/").includes(".."))throw new RuntimeCommandError("INVALID_PARAMS",`invalid skill artifact path: ${e}`);return t}function contained(e,t){const i=resolve(e),a=resolve(i,t);if(a!==i&&!a.startsWith(`${i}${sep}`))throw new RuntimeCommandError("INVALID_PARAMS","skill path escapes workspace");return a}function isRecord(e){return null!==e&&"object"==typeof e}function nativeSkillKey(e){return"string"==typeof e.skillKey?e.skillKey:"string"==typeof e.name?e.name:""}function installedDirectoryKey(e,t){if("string"!=typeof e.filePath||!isAbsolute(e.filePath)||"SKILL.md"!==basename(e.filePath))return;const i=relative(resolve(t),dirname(e.filePath));return/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(i)?i:void 0}function normalizeSkills(e,t){return(e??[]).flatMap(e=>{if(!e||"object"!=typeof e)return[];const i=e,a=installedDirectoryKey(i,t),s=a??nativeSkillKey(i);return s?[{skillKey:s,slug:a??("string"==typeof i.slug?i.slug:s),name:"string"==typeof i.name?i.name:s,description:"string"==typeof i.description?i.description:"",enabled:!0!==i.disabled,..."string"==typeof i.emoji?{emoji:i.emoji}:{},..."boolean"==typeof i.bundled?{bundled:i.bundled}:{}}]:[]})}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.87",
|
|
4
4
|
"description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BeeOS <dev@beeos.ai>",
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"brace-expansion": ">=5.0.9",
|
|
91
91
|
"@beeos-ai/beeos-types": "$@beeos-ai/beeos-types"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "58807754c730eb60e1c7b4278de5bc37ea6e47f1"
|
|
94
94
|
}
|