diff --git a/builtin-skills.json b/builtin-skills.json index 2d217c7..e6aa41a 100644 --- a/builtin-skills.json +++ b/builtin-skills.json @@ -1,5 +1,6 @@ { "skills": [ + "app-developer", "baidu-poi-search", "ccgp-gov-procurement", "clone-agent", diff --git a/manifest.json b/manifest.json index 739707d..83eeb3a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "DesireCore Official Market", - "version": "1.5.12", + "version": "1.5.13", "schemaVersion": "1.1.0", "supportedLocales": [ "zh-CN", @@ -31,7 +31,7 @@ "stats": { "totalAgents": 8, "totalTeams": 2, - "totalSkills": 143, + "totalSkills": 144, "lastUpdated": "2026-10-18" }, "features": [ diff --git a/skills/app-developer/LICENSE b/skills/app-developer/LICENSE new file mode 100644 index 0000000..66a0037 --- /dev/null +++ b/skills/app-developer/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 DesireCore + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/skills/app-developer/SKILL.md b/skills/app-developer/SKILL.md new file mode 100644 index 0000000..db27ee7 --- /dev/null +++ b/skills/app-developer/SKILL.md @@ -0,0 +1,108 @@ +--- +{ + "name": "app-developer", + "description": "用户要求开发、修改或发布自己的工作台应用,或把固定应用版本分发到 GitHub/Gitea 与 Registry 时使用。按所选技术栈使用的外部 Node/Docker 运行时、托管、Git 服务和数据库须有真实环境或连接及适用条款,可能另行部署或收费;缺能力时说明原因。", + "license": "MIT", + "compatibility": "Requires DesireCore app-authoring-v1 capability handshake. Depending on the chosen stack, Node/Docker runtimes, hosting, GitHub/Gitea and databases require actual authorized environments or connections and applicable licensing/deployment terms; separate costs may apply. Unavailable capabilities stop the affected operation.", + "version": "1.0.0", + "requiredClientVersion": "10.0.180", + "type": "procedural", + "risk_level": "high", + "status": "enabled", + "disable-model-invocation": true, + "provides": { + "tools": [ + "AppManage" + ] + }, + "tags": [ + "application", + "workbench", + "development", + "publishing" + ], + "metadata": { + "author": "desirecore", + "updated_at": "2026-10-13", + "i18n": { + "default_locale": "en-US", + "source_locale": "zh-CN", + "locales": [ + "zh-CN", + "en-US" + ], + "zh-CN": { + "name": "应用开发与发布", + "short_desc": "开发个人工作台并发布到我的应用,按需分发固定版本", + "description": "用户要求开发、修改或发布自己的工作台应用,或把固定应用版本分发到 GitHub/Gitea 与 Registry 时使用。按所选技术栈使用的外部 Node/Docker 运行时、托管、Git 服务和数据库须有真实环境或连接及适用条款,可能另行部署或收费;缺能力时说明原因。", + "body": "./SKILL.zh-CN.md", + "source_hash": "sha256:b12f9f3a566f3195", + "translated_by": "ai:gpt-6", + "translated_at": "2026-10-13" + }, + "en-US": { + "name": "Application Development and Publishing", + "short_desc": "Develop a personal workbench, publish it to My Apps, and distribute fixed releases on request", + "description": "Use when the user asks to develop, modify or publish a personal workbench application, or distribute a fixed release through GitHub/Gitea and Registry. Node/Docker runtimes, hosting, Git services and databases used by the chosen stack need actual environments or connections and applicable terms; separate deployment or costs may apply. Report unavailable capabilities.", + "body": "./SKILL.md", + "source_hash": "sha256:b12f9f3a566f3195", + "translated_by": "ai:gpt-6", + "translated_at": "2026-10-13" + } + } + }, + "market": { + "icon": "", + "category": "development", + "maintainer": { + "name": "DesireCore Official", + "verified": true + }, + "channel": "latest" + } +} +--- + +# Application Development and Publishing + +## L0: Responsibility + +The Agent chooses the implementation, framework and working method. The platform verifies material identity, scope, publication, runtime and collaboration facts. Reuse file and execution tools, Task/Run and the existing installation writer. Do not create a fixed business workflow or execute commands extracted from installation prose. + +## L1: Read Capabilities and Context + +1. Read or create the task's single file-based Plan, preserving the actual Agent/session, device, authorized work directory and development Task. Continue within the user's existing requirements and authorization. +2. Use the original `AppManage` action `authoring_capabilities`. Require `capabilities.protocol: app-authoring-v1` and inspect supported, authorized, available and reason independently. Its `fileRoots` are real readable roots for the current Agent, and `registryTargetIds` are registered catalog targets. These read facts grant neither upload nor filesystem write authority. The minimum version is a code baseline, not proof that a shipped client supports this protocol. Authoring actions enter the original domain owners without passing platform credentials to the model. +3. Natural-language development needs no additional handoff protocol. Installation, registration, update or uninstall continues through ApplicationManagement-v2 and the single writer. Read `app-install-manager` when available; never edit installed-entries.json directly. +4. Read [authoring interfaces](references/authoring-api.zh-CN.md) and [distribution](references/distribution.zh-CN.md) on demand. The candidate main-repository authority is `docs/功能/市场系统/API/应用开发发布接口.md`; an external PR does not establish a shipped client or official seed. `examples/static-workbench/` and `examples/service-workbench/` are editable teaching materials, not evidence of autonomous Agent development or production acceptance. + +## L2: Development, Preview and Personal Publication + +- Establish the user's actual work, data ownership and collaboration needs. Choose suitable languages and frameworks; examples do not prescribe a stack. Reuse the actual Task, Plan and originating context. +- Separate program material, business data and secrets. The portable `application.json` contains no sourceId, installation identity, host path, builtin route or credential. Its product identity, version and entrypoints are the single source for release and catalog views. +- Create fixed `app-release.json` from an explicit file whitelist, actual byte hashes, entrypoint mappings, data contract and truthful platform evidence. Exclude the manifest itself from files. `scripts/create-release.mjs` builds the example manifest; the service independently verifies actual bytes. +- Deliver `app-draft.json` with real fileRef, Plan, Agent/session, stable draftId and target installationId. Its workdir points to the actual release material directory; do not guess a release/ subdirectory or fabricate a fileRef. Read the actual authorized file using `AppManage.read_draft` with `file_ref`, preserving the returned draft. Return a clickable absolute path so the user can read and continue it in the application development workspace. +- Call `AppManage.preview_prepare` with the original draft/mode/expectedRevision in `owner_request`, including release for snapshot. Use preview_read to read and preview_end to end it. Snapshot preview and formal installation use separate identities and data. Freeze a snapshot before publishing a live preview. Replacement uses the actual revision. Ending preview revokes its bridge and disposes only its owned temporary runtime while preserving data and development material. Closing a page does not stop a formal application or accepted Task. Never convert a temporary URL/token into a formal entrypoint. +- Enroll a real authorized source development server using `AppManage.development_enroll` with the original prepareRef/draft/terminalId/endpoint in `owner_request` for live preview. Its development-only contract cannot become formal startup; mutable source and fixed releases remain separate. The original command must respect owner-injected PORT and APP_DATA_DIR, using shell `${VAR:-default}` only for initial defaults rather than overriding preview ports or formal data roots. +- Use `AppManage.prepare_release` with the complete `release_request` to freeze real material. Construct materialRoot as a FileResourceRef from the returned readable roots. Inspect the candidate, then call commit_release with the complete original AppReleaseCommitRequest in `release_request`, retaining prepareRef, installationId, expectedRevision and submissionId. Do not reduce its installation material to a name or digest. Continue authorized publication without repeated per-command questions; new targets or additional data/external effects follow existing governance. +- Saving a release and making it runnable are separate facts. Static files use fixed mappings; services use `AppManage.runtime_enroll` to bind original-owner spawn, generation and handshake. Use runtime_read/start/stop for formal operation; startupPolicy is not permanent execution authority. Docker releases declare complete registry/repository@sha256 imageRefs, then use `AppManage.docker_prepare` to receive owned CID/env paths and the actual command. Execute it through original TaskHost approval, then use docker_enroll with the original enrollmentRef/terminalId and verify daemon/CID/RepoDigest/mount/port facts. Normal create/start/stop repeats current approval; internal transient preview disposal compensates only its originally approved owned CID; candidate, formal and preview roots/nonces differ. Reconcile unknown outcomes against the original CID before another effect. Missing adapters or supported environments keep the original Agent-managed lifecycle with an honest reason. A CLI exit, format check, port, PID or historical observation does not prove availability. +- For timeout, lost response or disconnection, preserve the original mutation/submission/clientRequest ID and body. Query publication using `AppManage.lookup_release_submission` with the original installation_id/submission_id, and read actual runtime/preview state separately. Do not blindly replay unknown writes with a new ID. Check current material and data compatibility before an update. Investigate interrupted migrations; code rollback is not data recovery. + +## L3: Webpage and Agent Collaboration + +The webpage uses the finite `window.desirecoreApp.call(method,payload)` and `onEvent` API; Web uses a MessagePort from an independent origin. There is no Conveyor, arbitrary invoke, platform administrator token, arbitrary file proxy or general fetch tool. Disable unavailable or unauthorized actions with the actual reason. + +- Formal `connect.application` supplies only the application's source/app/instance prefix. Object namespace, ID and revision need an actual owner. The examples reference the declared notes source file with namespace data-files, objectId notes and the actual readData ETag; they do not invent an owner for individual JSON note objects. The selected note's ID, record version and visible fields remain in the summary. Preview discusses the summary without borrowing a formal instance identity. +- Persist only declared data keys through readData/saveData with expectedAbsent or expectedETag. Recover unknown writes using the original mutationId. A native backend needs a separately authorized data root and must not write business databases into a fixed release directory. +- Publish selections if needed and call prepareWork to propose work. The platform owns final acceptance: native uses real input proof and one-use intent, while Web uses the original trusted chat admission transport. A webpage cannot sign native physical input proof. A proposalRef is not a Task receipt. Only actual reserved/committed work-accepted results establish acceptance; queued does not mean completed. +- Reconnect and lookup preserve the original `{agentId,clientRequestId}`. A reset_required gap needs a fresh snapshot and valid cursor. controlWork uses only actual availableActions, currently abort, without general resume. readWork.resources contains original Task-owned task-binding locators; `openResource({locator})` returning navigation-requested requests platform navigation and does not prove file content loaded. data-files objects use exact authorized references in a read-only inspector. Never fabricate FileRefs/URLs or treat unsupported navigate as a grant. +- Business content stays with its data owner. View state defaults to memory. Device persistence needs an explicit user choice, a declared viewStateContract with finite keys/bytes/format, and saveViewState permission. connect returns actual device readiness and per-key revisions. Keep the original mutationId, expectedRevision and body for unknown CAS writes. Store necessary filter/selection/position only, without business content or credentials. +- Web hosting needs owner-configured TLS, domain, DNS and actual public port. The trusted shell obtains its ticket and explicitly requests SDK bootstrap. Missing host/SDK configuration remains unavailable; never disable TLS verification or allow an application-selected upstream URL/management port. + +## L4: Distribution and Evidence + +Personal publication is private and local by default. When the user requests distribution, call `AppManage.export_release` with the fixed complete whitelist in `owner_request`, and review the exact files and actual target. Exclude databases, .env, credentials, chats, Task/Plan, logs, machine paths and installation identity. Environment templates can describe names without real secret values. + +To deliver ZIP bytes to an upload tool, also pass top-level `export_file_ref`, a destination FileResourceRef under the original tool's actual write authority, with exactly one of `expected_absent:true` or the original `expected_etag`. The returned `archiveFile` contains the actual fileRef/sha256/size/etag/replayed. Verify sha256 equals archiveDigest before handing the file to an already authorized GitHub/Gitea upload tool and inspecting immutable source/artifact results. For a lost response, preserve the same destination, condition and complete body; never overwrite an unknown file. Readable fileRoots do not grant write authority. Then call `AppManage.distribution_prepare` with `owner_request` containing the same exportRef, a registryTargetIds target, the actual immutable source and metadata to generate Registry manifest.json, catalog-metadata.v1.json and install.md. The draft is still neither uploaded nor submitted; report missing targets and upload channels separately. Ordinary HttpRequest does not automatically inject platform credentials; never extract instance tokens or change Origin to bypass 401. The target Provider injects catalog identity; the personal installation remains unchanged. Verify archiveDigest, releaseDigest and source-materialized descriptorDigest independently. A different payload under the same version requires a new revision, not overwriting fixed material. + +Report code/functional verification, preview, personal saving, runtime availability, upload, catalog PR/admission and second-user installation independently. A candidate 4.3 Schema, external PR or generated ZIP does not establish official-market availability, client capability release or another device's runtime acceptance. External hosting, databases and Git services require actual connections and applicable licensing/deployment terms and can carry separate costs; when unavailable, stop the affected external operation. diff --git a/skills/app-developer/SKILL.zh-CN.md b/skills/app-developer/SKILL.zh-CN.md new file mode 100644 index 0000000..25ba26f --- /dev/null +++ b/skills/app-developer/SKILL.zh-CN.md @@ -0,0 +1,43 @@ +# 应用开发与发布 + +## L0:职责 + +Agent 选择技术栈、构建方法与工作方式;平台核验材料、作用域、发布、运行和协作事实。复用文件、执行、Task/Run 与现有安装 writer,不生成固定业务 DAG,不从安装说明自动执行命令。 + +## L1:先读能力与当前上下文 + +1. 读取或创建本任务唯一文件化 Plan,保存实际来源 Agent/会话、目标设备、工作目录及开发 Task。已有用户需求和授权直接沿用。 +2. 使用原 `AppManage` 的 `authoring_capabilities` 动作。返回的 `capabilities` 必须为 `protocol: app-authoring-v1`,分别看 features 的 supported、authorized、available 和 reason;`fileRoots` 是当前 Agent 可读的真实文件根,`registryTargetIds` 是已登记的目录目标;这些只读事实不授予目录上传或文件写权限。10.0.180 只是代码基线,已发行的同版本客户端不因此支持新协议。作者动作直接进入原领域 owner,不把平台凭据交给模型。 +3. 普通自然语言开发不要求新的交接协议。若要登记、安装、更新或卸载,读取 `app-install-manager`;继续沿 ApplicationManagement-v2 解析和唯一 writer,不直接编辑 installed-entries.json。 +4. 按需读 [作者接口](references/authoring-api.zh-CN.md) 和 [分发指南](references/distribution.zh-CN.md)。候选主仓正文为 `docs/功能/市场系统/API/应用开发发布接口.md`,按实际能力读取;外仓 PR 不表示客户端或官方 seed 已发行。两份示例位于 `examples/static-workbench/` 与 `examples/service-workbench/`,只是可修改的教学材料,不能代替真实 Agent 开发和业务验收。 + +## L2:开发、预览和个人发布 + +- 先确认要解决的工作、数据位置与必要协作。可以使用任何适合的语言/框架,示例不固定技术栈。小任务直接完成;复杂工作保留原 Task、Plan 和用户来源,不另造开发状态机。 +- 程序材料、业务数据与秘密分开。作者定义 `application.json` 不包含 sourceId、安装实例、宿主路径、builtin 路由或权限凭据。release 的产品 ID/名称/版本/入口只有这份定义。 +- 固定 `app-release.json`:按显式文件白名单、原始字节摘要、入口映射、数据合同和真实平台证据生成;清单自身不进入 files。可用 `scripts/create-release.mjs` 为示例生成清单,平台仍重新核验实际字节。 +- 向平台交付 `app-draft.json`,记录真实 fileRef、Plan、Agent/session、稳定 draftId 与目标 installationId。workdir 指向实际完成的 release 材料目录,不猜测 release/ 子目录,也不编造 fileRef。用 `AppManage.read_draft` 的 `file_ref` 读取授权根内实际文件,保留返回的 draft。返回此文件的可点击绝对路径,说明可在应用开发工作区读取并继续。 +- 预览沿 `AppManage.preview_prepare` 的 `owner_request` 传原 draft/mode/expectedRevision,snapshot 另给 release;读取用 preview_read,结束用 preview_end。snapshot 预览与正式安装隔离,数据不自动转移。live 预览必须先固定 snapshot 才可发布;刷新/替换用实际 revision。结束预览撤销当前桥并回收本次 owned 临时运行资源,保留数据与开发材料;普通关页不停止正式应用或已接纳的后台 Task,不将预览临时 URL/token 保存为正式入口。 +- 原开发服务器/HMR 使用真实授权的 draft 工作目录,由 `AppManage.development_enroll` 的 `owner_request` 传原 prepareRef/draft/terminalId/endpoint 接入。它只用于 live 预览,不能成为正式 startup;可变源码与固定 release 分开。原命令须尊重 owner 注入的 PORT 和 APP_DATA_DIR,初次开发默认值用 shell 的 `${VAR:-default}`,不能写死赋值盖掉预览端口或正式数据根。 +- 个人发布由 `AppManage.prepare_release` 的 `release_request` 固定真实材料:materialRoot 使用能力返回的授权根构造 FileResourceRef。检查候选后,`commit_release` 的 `release_request` 使用完整原 AppReleaseCommitRequest,保留精确 prepareRef、installationId、expectedRevision 与原 submissionId;不能把安装 material 对象简化为产品名称或摘要。旧会话里明确授权的发布可继续,不逐命令重复确认。新目标或超出授权的数据/外部副作用沿现有治理处理。 +- 发布保存与运行可用分开。静态包通过固定映射承载;带服务应用用 `AppManage.runtime_enroll` 固定原执行 owner 的真实 spawn、代际身份和握手;正式运行用 runtime_read/start/stop,不能把 startupPolicy 当长期执行授权。Docker release 声明完整 imageRefs(注册表/repository@sha256),先用 `AppManage.docker_prepare` 获得私有短期 CID/env 文件及实际命令,沿原审批执行后以 docker_enroll 的原 enrollmentRef/terminalId 核验真实 daemon/CID/RepoDigest/挂载/端口。普通用户启动/停止沿当前审批,私有临时预览回收只补偿原批准的 owned CID;候选、正式和预览的数据根与 nonce 分开。unknown 先检查原 CID,不重复创建;缺 adapter 或支持的环境时保留原 Agent 管理并明确原因。端口响应、Docker CLI 退出、PID、历史 observation 或格式检查不能替代运行绑定;失败如实说明“版本已保存,启动待修复”。 +- 写入超时、响应丢失或断线,保留原 mutation/submission/clientRequest ID 与正文;发布用 `AppManage.lookup_release_submission` 的原 installation_id/submission_id 查询,运行和预览分别重读实际状态。unknown 时不创建新 ID 盲重放。修改候选前重读当前版本与数据兼容,迁移中断先观察;代码回退不代表业务数据恢复。 + +## L3:网页与 Agent 协作 + +应用网页只使用有限 `window.desirecoreApp.call(method,payload)`/`onEvent`,Web 由独立来源的 MessagePort SDK 连接。没有 Conveyor、任意 invoke、平台管理 token、任意文件代理或通用 fetch 工具。缺宿主/授权时禁用相应操作并给出真实原因。 + +- `connect.application` 仅为正式实例提供来源/应用/实例前缀;对象 namespace/objectId/revision 由业务 owner 提供并再次校验。预览不借用正式实例,缺对象引用可以只讨论摘要。 +- 持久数据只用已声明 key 的 readData/saveData;新文件 expectedAbsent,已有文件 expectedETag,unknown 沿原 mutationId 查询。原生后端使用单独获授权数据根,不能把数据库写入固定 release 目录。 +- 选中对象可 publishSelection。发起工作先 prepareWork,最终接纳由平台自己的控件处理:桌面沿真实输入证明与单次 intent,Web 沿原可信聊天接纳传输,不让网页自签原生输入证明。prepare/proposalRef 不等于 Task 已创建;只在 work-accepted 的实际 reserved/committed 回执后显示接纳,queued 不说执行完成。 +- 请求恢复沿原 `{agentId,clientRequestId}`;订阅出现 reset_required 时读快照并从合法 cursor 重订阅。controlWork 只用实际 availableActions,当前为 abort,不提供任意 resume。readWork.resources 中的真实 task-binding locator 可以 `openResource({locator})` 定位;navigation-requested 只表示发出定位请求,原文件 owner 继续核验与加载。data-files 对象沿精确业务引用在只读检查器中呈现;不构造自由 FileRef/URL,navigate 未获真实 grant 不使用。 +- 业务正文仍由数据 owner 保存。视图恢复默认内存;用户明确选择设备记忆时,先声明 viewStateContract 的有限 key/预算/格式,再取得 saveViewState grant。connect 返回真实 device 状态和各 key revision,device 保存沿原 mutationId、expectedRevision 和正文 CAS;超时保留同一请求,不能换 ID 盲覆盖。只保存必要的筛选/选择/位置,不把笔记正文或凭据作为视图偏好。 +- Web 承载需要部署 owner 已配置实际 TLS、域名、DNS 和公开端口,由可信 shell 取得本次 ticket 并明确 bootstrapSdk 注入。SDK/宿主缺失时说明不可用;不关闭 TLS 验证、不让应用选择上游 URL/管理端口,不把 HTTP200 或本机 loopback 当远端浏览器可达。 + +## L4:分发与交付证据 + +个人发布默认私有本机,不隐含上传。用户要求分发后,用 `AppManage.export_release` 的 `owner_request` 导出固定版本的完整白名单,审阅确切文件与目标。业务库、.env、凭据、聊天/Task/Plan、日志、机器路径和安装身份不得进入包;环境模板可以包含名称和说明,不能包含真实值。 + +需要交给上传工具的 ZIP 时,export_release 另在顶层提供 `export_file_ref`(原工具真实写权限目录中的目标 FileResourceRef),并二选一给 `expected_absent:true` 或原 `expected_etag`。返回的 `archiveFile` 包含实际 fileRef/sha256/size/etag/replayed;核验 sha256 等于 archiveDigest,再交已授权的 GitHub/Gitea 上传工具并核读不可变提交/制品 SHA。响应丢失保留同一目标、条件与完整正文查询/重试,不覆盖未知文件;只有只读 fileRoots 不代表有写权限。随后用 `AppManage.distribution_prepare` 的 `owner_request`,以同一 exportRef、能力返回的 registryTargetIds 中的目标、实际固定来源和 metadata 生成 Registry `manifest.json`、`catalog-metadata.v1.json`、`install.md`。草稿仍未上传、未提交目录;缺少真实目标或上传通道时分别说明不可用。普通 HttpRequest 不自动注入平台凭据,不能读取实例 token 或变更 Origin 绕过 401。目录身份由目标 Provider 注入,个人安装仍是原实例。archiveDigest、releaseDigest、物化 descriptorDigest 分别核验;同版本异内容创建新修订,不覆盖旧固定材料。 + +按真实情况分别报告:代码/业务验证、预览、个人发布、启动、来源上传、目录 PR/准入、第二用户安装。候选 4.3 Schema、外仓 PR 或 ZIP 生成都不代表官方市场已开放,也不能代替客户端能力发行或另一设备运行。外部托管/数据库/Git服务的登录、许可、部署及费用由其真实连接和条款决定,缺少时停止相应外部调用。 diff --git a/skills/app-developer/catalog-metadata.v1.json b/skills/app-developer/catalog-metadata.v1.json new file mode 100644 index 0000000..7926649 --- /dev/null +++ b/skills/app-developer/catalog-metadata.v1.json @@ -0,0 +1,77 @@ +{ + "$schema": "../../schemas/catalog-metadata.v1.schema.json", + "schemaVersion": 1, + "identity": { + "kind": "skill", + "id": "app-developer" + }, + "presentation": { + "defaultLocale": "en-US", + "i18n": { + "zh-CN": { + "name": "应用开发与发布", + "summary": "开发个人工作台并发布到我的应用,按需分发固定版本", + "description": "用户要求开发、修改或发布自己的工作台应用,或把固定应用版本分发到 GitHub/Gitea 与 Registry 时使用。按所选技术栈使用的外部 Node/Docker 运行时、托管、Git 服务和数据库须有真实环境或连接及适用条款,可能另行部署或收费;缺能力时说明原因。" + }, + "en-US": { + "name": "Application Development and Publishing", + "summary": "Develop a personal workbench, publish it to My Apps, and distribute fixed releases on request", + "description": "Use when the user asks to develop, modify or publish a personal workbench application, or distribute a fixed release through GitHub/Gitea and Registry. Node/Docker runtimes, hosting, Git services and databases used by the chosen stack need actual environments or connections and applicable terms; separate deployment or costs may apply. Report unavailable capabilities." + } + }, + "category": "development", + "tags": [ + "application", + "workbench", + "development", + "publishing" + ] + }, + "release": { + "state": "known", + "version": "1.0.0", + "versionScheme": "semver" + }, + "timestamps": { + "catalogUpdatedAt": { + "state": "known", + "value": "2026-10-13", + "precision": "day" + }, + "releasePublishedAt": { + "state": "unknown" + }, + "reviewedAt": { + "state": "unknown" + }, + "upstreamObservedAt": { + "state": "unknown" + } + }, + "provenance": {}, + "governance": { + "stewardship": "official", + "availability": "installable", + "license": { + "state": "known", + "value": "MIT", + "evidencePath": "LICENSE" + }, + "redistribution": "verify-package-terms", + "listingMaintainer": { + "name": "DesireCore Official", + "verified": true + } + }, + "compatibility": { + "platforms": { + "state": "unknown" + }, + "requiredClientVersion": "10.0.180" + }, + "spec": { + "kind": "skill", + "riskLevel": "high", + "skillType": "procedural" + } +} diff --git a/skills/app-developer/examples/service-workbench/LICENSE b/skills/app-developer/examples/service-workbench/LICENSE new file mode 100644 index 0000000..66a0037 --- /dev/null +++ b/skills/app-developer/examples/service-workbench/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 DesireCore + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/skills/app-developer/examples/service-workbench/app-release.json b/skills/app-developer/examples/service-workbench/app-release.json new file mode 100644 index 0000000..231440e --- /dev/null +++ b/skills/app-developer/examples/service-workbench/app-release.json @@ -0,0 +1,147 @@ +{ + "schemaVersion": 1, + "canonicalization": "app-release-c14n-v1", + "definition": { + "id": "service-notes-workbench", + "name": "服务笔记工作台", + "version": "1.0.0", + "productKind": "application", + "icon": "PanelsTopLeft", + "iconLetter": "台", + "entrypoints": [ + { + "id": "main", + "name": "笔记", + "kind": "embedded-web", + "serviceRef": "workbench:main" + } + ], + "placementPolicy": { + "showInMyApps": true, + "allowNavPin": true, + "allowAgentBinding": true + } + }, + "delivery": { + "type": "native-app", + "runtimeInstructionsPath": "runtime/运行说明.md" + }, + "files": [ + { + "path": "LICENSE", + "role": "license", + "sha256": "e863481f423546e8984c07ffa00609d338b7f7533f60e89392301028163aa89e", + "size": 1067 + }, + { + "path": "application.json", + "role": "definition", + "sha256": "34f3ef80d33f3182f461f93e646d3fdb13865fb07214b505b55a64514c1284e1", + "size": 433 + }, + { + "path": "data-contract.json", + "role": "data-contract", + "sha256": "e195b2fd60a2798141c4c75ab57e0927d115bfba7888b0881b69dffa9dcb26cc", + "size": 266 + }, + { + "path": "runtime/server.mjs", + "role": "runtime", + "sha256": "3ccea1e06f3371e98cc8277d2b82d70cdcca7dd43ff8c40ab43a3c14ba0ee456", + "size": 2633 + }, + { + "path": "runtime/运行说明.md", + "role": "documentation", + "sha256": "743e8eaa0ae53e605563eca8620ba09eddf4adaf4ad9dd3c5593d080bedfb8c1", + "size": 3081 + }, + { + "path": "ui/app.js", + "role": "ui", + "sha256": "5ece9785b32efa8d30531354a8775b93ee1c4b6a470ffc37e86625d9c01b2b35", + "size": 9111 + }, + { + "path": "ui/bridge.js", + "role": "ui", + "sha256": "b227966a71d10002fadc2fba8d278ef85b7ab6aa4bb86afca4599734809bcb63", + "size": 2984 + }, + { + "path": "ui/index.html", + "role": "ui", + "sha256": "d2796efba92f7a8bf4da5d467311ba191d7f6af1eee1872b0f0707031f876973", + "size": 2624 + }, + { + "path": "说明.md", + "role": "documentation", + "sha256": "2e2393b602ba22fd16d070d1610fe9b9a48f1c2a0134f8dfe13df1220dac5419", + "size": 2506 + } + ], + "source": { + "kind": "worktree", + "sha256": "f2b9a34e87370518a1f9c267c2fce453f1c599ea3c006e8b53b762bb5a354547" + }, + "supportedPlatforms": [ + "darwin", + "win32", + "linux" + ], + "minimumClientVersion": "10.0.180", + "requiredCapabilities": [ + "app-authoring-v1", + "release", + "runtime-binding", + "bridge", + "data", + "view-state" + ], + "dataContract": { + "schemaVersion": 1, + "formatVersion": "notes-v1", + "readableVersions": [ + "notes-v1" + ], + "writableVersions": [ + "notes-v1" + ], + "files": [ + { + "key": "notes", + "path": "notes.json", + "maxBytes": 131072, + "contentType": "application/json" + } + ], + "uninstallRetention": "retain" + }, + "bridgeCapabilities": [ + "connect", + "capabilities", + "readData", + "saveData", + "lookupDataMutation", + "publishSelection", + "requestWork", + "lookupRequest", + "readWork", + "controlWork", + "openResource", + "readResource", + "saveViewState" + ], + "viewStateContract": { + "schemaVersion": 1, + "keys": [ + "selection" + ], + "maxBytes": 1024, + "formatVersion": "1" + }, + "instructionsPath": "说明.md", + "releaseDigest": "c3ef146b7393d530db5ed44eb8de637551abe18b13e98f9c441cbbf36dd92c68" +} diff --git a/skills/app-developer/examples/service-workbench/application.json b/skills/app-developer/examples/service-workbench/application.json new file mode 100644 index 0000000..b124ac1 --- /dev/null +++ b/skills/app-developer/examples/service-workbench/application.json @@ -0,0 +1,21 @@ +{ + "id": "service-notes-workbench", + "name": "服务笔记工作台", + "version": "1.0.0", + "productKind": "application", + "icon": "PanelsTopLeft", + "iconLetter": "台", + "entrypoints": [ + { + "id": "main", + "name": "笔记", + "kind": "embedded-web", + "serviceRef": "workbench:main" + } + ], + "placementPolicy": { + "showInMyApps": true, + "allowNavPin": true, + "allowAgentBinding": true + } +} diff --git a/skills/app-developer/examples/service-workbench/data-contract.json b/skills/app-developer/examples/service-workbench/data-contract.json new file mode 100644 index 0000000..03c2cb5 --- /dev/null +++ b/skills/app-developer/examples/service-workbench/data-contract.json @@ -0,0 +1,5 @@ +{ + "schemaVersion": 1, "formatVersion": "notes-v1", "readableVersions": ["notes-v1"], "writableVersions": ["notes-v1"], + "files": [{ "key": "notes", "path": "notes.json", "maxBytes": 131072, "contentType": "application/json" }], + "uninstallRetention": "retain" +} diff --git a/skills/app-developer/examples/service-workbench/release-inputs.json b/skills/app-developer/examples/service-workbench/release-inputs.json new file mode 100644 index 0000000..8ea0e4b --- /dev/null +++ b/skills/app-developer/examples/service-workbench/release-inputs.json @@ -0,0 +1,82 @@ +{ + "description": "显式收录网页与只读分析服务;笔记使用平台有限data key,不写固定程序目录。", + "delivery": { + "type": "native-app", + "runtimeInstructionsPath": "runtime/运行说明.md" + }, + "supportedPlatforms": [ + "darwin", + "win32", + "linux" + ], + "requiredCapabilities": [ + "app-authoring-v1", + "release", + "runtime-binding", + "bridge", + "data", + "view-state" + ], + "bridgeCapabilities": [ + "connect", + "capabilities", + "readData", + "saveData", + "lookupDataMutation", + "publishSelection", + "requestWork", + "lookupRequest", + "readWork", + "controlWork", + "openResource", + "readResource", + "saveViewState" + ], + "dataContractPath": "data-contract.json", + "files": [ + { + "path": "application.json", + "role": "definition" + }, + { + "path": "ui/index.html", + "role": "ui" + }, + { + "path": "ui/bridge.js", + "role": "ui" + }, + { + "path": "ui/app.js", + "role": "ui" + }, + { + "path": "data-contract.json", + "role": "data-contract" + }, + { + "path": "说明.md", + "role": "documentation" + }, + { + "path": "LICENSE", + "role": "license" + }, + { + "path": "runtime/server.mjs", + "role": "runtime" + }, + { + "path": "runtime/运行说明.md", + "role": "documentation" + } + ], + "viewStateContract": { + "schemaVersion": 1, + "keys": [ + "selection" + ], + "maxBytes": 1024, + "formatVersion": "1" + } +} diff --git a/skills/app-developer/examples/service-workbench/runtime/server.mjs b/skills/app-developer/examples/service-workbench/runtime/server.mjs new file mode 100644 index 0000000..06c7d73 --- /dev/null +++ b/skills/app-developer/examples/service-workbench/runtime/server.mjs @@ -0,0 +1,40 @@ +/** 只读应用服务;业务数据在平台实例data key中,绝不写固定release目录。 */ +import http from 'node:http' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +const port = Number(process.env.PORT) +if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('PORT_required') +const files = new Map([ + ['/', ['../ui/index.html','text/html; charset=utf-8']], + ['/app.js',['../ui/app.js','text/javascript; charset=utf-8']], + ['/bridge.js',['../ui/bridge.js','text/javascript; charset=utf-8']], +]) +const server = http.createServer(async (request,response) => { + const url = new URL(request.url ?? '/',`http://127.0.0.1:${port}`) + response.setHeader('X-Content-Type-Options','nosniff') + response.setHeader('Referrer-Policy','origin') + if (request.method !== 'GET') { response.writeHead(405); return response.end() } + if (url.pathname === '/.desirecore-app-identity') { + response.setHeader('Content-Type','application/json') + response.setHeader('Cache-Control','no-store') + const identity = {proof:process.env.DESIRECORE_APP_RUNTIME_PROOF,releaseDigest:process.env.DESIRECORE_APP_RELEASE_DIGEST, + scopeId:process.env.DESIRECORE_APP_SCOPE_ID,runtimeGeneration:process.env.DESIRECORE_APP_RUNTIME_GENERATION} + if (Object.values(identity).some(value => typeof value !== 'string' || !value)) { response.writeHead(503); return response.end(JSON.stringify({error:'managed_runtime_identity_unavailable'})) } + return response.end(JSON.stringify(identity)) + } + if (url.pathname === '/api/summary') { + const count = Number(url.searchParams.get('count')) + if (!Number.isInteger(count) || count < 0 || count > 10000) { response.writeHead(400); return response.end() } + response.setHeader('Content-Type','application/json') + return response.end(JSON.stringify({count,summary:`当前工作台有 ${count} 条笔记。`,dataAccess:'none'})) + } + const entry = files.get(url.pathname) + if (!entry) { response.writeHead(404); return response.end() } + response.setHeader('Content-Type',entry[1]) + response.setHeader('Content-Security-Policy',"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none'; base-uri 'none'") + try { response.end(readFileSync(fileURLToPath(new URL(entry[0],import.meta.url)))) } + catch { response.writeHead(503); response.end('material_unavailable') } +}) +server.listen(port,'127.0.0.1',() => console.log(`Application service listening on loopback port ${port}`)) +for (const signal of ['SIGINT','SIGTERM']) process.on(signal,() => server.close(() => process.exit(0))) diff --git a/skills/app-developer/examples/service-workbench/runtime/运行说明.md b/skills/app-developer/examples/service-workbench/runtime/运行说明.md new file mode 100644 index 0000000..f2b1877 --- /dev/null +++ b/skills/app-developer/examples/service-workbench/runtime/运行说明.md @@ -0,0 +1,15 @@ +# 运行服务笔记工作台 + +需要用户实际选择并可用的 Node.js 运行时。此示例不安装 Node、Docker 或第三方数据库,不向平台注册 MCP。Node 服务只提供固定网页、只读计数分析和当前 TaskHost 运行身份握手;业务笔记使用平台有限 data key,与静态示例采用同一CAS保存约定。 + +在获授权的材料目录生成 release 并 prepare 后,使用原执行工具实际启动 `PORT="${PORT:-32000}" node runtime/server.mjs`,初次默认值须替换为实际空闲端口。保留该默认表达式,不能用写死的 PORT 赋值覆盖 Harness 后续注入的预览端口。端口需固定在原接受的执行合同,不能靠网页声明任意 endpoint。身份由原 TaskHost 在受控正式 spawn 时注入:DESIRECORE_APP_RUNTIME_PROOF、DESIRECORE_APP_RELEASE_DIGEST、DESIRECORE_APP_SCOPE_ID、DESIRECORE_APP_RUNTIME_GENERATION。应用不能自报固定 PID 或伪造 proof,不能将 proof 用于其他 API 或日志。 + +通过原 `AppManage.runtime_enroll` 的 `owner_request` 给真实 prepareRef、installationId、terminalId、endpoint.port 与 identityPath `/.desirecore-app-identity`,以及用户选择的 manual/on-open。普通 HttpRequest 不自动注入平台凭据,不能读取实例 token 或改 Origin 绕过认证。纳管读取原accepted-command冻结执行合同;由原执行工具明确停掉此前命令,commit后由原owner启动正式新代际,再核读运行身份和应用功能。原先裸启动的服务在没有managed身份时会明确返回503,它只能帮助开发调试,不能证明正式可信直开。 + +公开执行环境不接受任意.env。若改造应用需要后端直接读写数据,须使用实际数据owner授予并注入的独立根;缺授权时不写盘,不默认使用cwd或release目录。此教学服务没有后端数据写入,因此不需要APP_DATA_DIR,不持有平台凭据。 + +停止、更新、失败恢复沿原TaskHost/Run,不重复开第二个后台进程。scope/release/generation改变后旧连接失效;重启须重新取得owner身份与审批事实。端口被占用不接管,不把HTTP200作为运行归属证明。 + +开发时若需要直接看授权源码的修改,可用当前真实 draft、原 source 目录启动的 terminalId 和已固定 prepareRef 通过 `AppManage.development_enroll` 的 `owner_request` 登记原 prepareRef/draft/terminalId/endpoint。它只承接 live 预览,正式发布须另固定 snapshot 并登记正式运行合同;普通读取静态源码的变化不等于框架 dev-server/HMR 协议已经验收。 + +要将此服务改为 Docker release,须由作者实际选择并验证固定 Node 镜像 RepoDigest、声明全部 imageRefs,容器服务监听 `0.0.0.0` 而宿主只发布 `127.0.0.1`,并沿 AppManage.docker_prepare/docker_enroll 的实际 argv、nonce、CID 和 data mount 核验。当前文件是 native Node 教学样例,不能只改 delivery 类型就声称容器运行通过;镜像及 Docker 许可、安装与运维属于其实际外部 owner。 diff --git a/skills/app-developer/examples/service-workbench/ui/app.js b/skills/app-developer/examples/service-workbench/ui/app.js new file mode 100644 index 0000000..1201253 --- /dev/null +++ b/skills/app-developer/examples/service-workbench/ui/app.js @@ -0,0 +1,114 @@ +import { connectBridge } from './bridge.js' +const element = id => document.getElementById(id) +let bridge, connection, etag, notes = [], selected, rememberedNoteId, pendingSave, pendingWork, pendingViewState, viewRevision = 'none' +const status = message => { element('status').textContent = message } +function render() { + element('notes').replaceChildren() + for (const note of notes) { + const row = document.createElement('li'), text = document.createElement('div'), button = document.createElement('button') + text.textContent = note.text; button.textContent = selected?.id === note.id ? '已选中' : '选择此笔记' + button.addEventListener('click', () => { selected = note; render() }); row.append(text,button); element('notes').append(row) + } +} +async function read() { + const result = await bridge.call('readData',{key:'notes'}) + const value = result.exists ? JSON.parse(result.content) : {schemaVersion:1,notes:[]} + if (value.schemaVersion !== 1 || !Array.isArray(value.notes)) throw new Error('notes_format_unsupported') + notes = value.notes; etag = result.etag; selected = notes.find(note => note.id === (selected?.id ?? rememberedNoteId)); render() + status('已读取当前实例的数据。') +} +async function save() { + if (!pendingSave) { + const text = element('note').value.trim() + if (!text) return status('先输入笔记。') + pendingSave = {key:'notes',mutationId:crypto.randomUUID(),content:JSON.stringify({schemaVersion:1,notes:[...notes,{id:crypto.randomUUID(),revision:'1',text}]}), + ...(etag ? {expectedETag:etag} : {expectedAbsent:true})} + } + try { + const receipt = await bridge.call('saveData',pendingSave) + if (receipt.status !== 'saved') throw new Error('save_result_unknown') + pendingSave = undefined; element('recover').disabled = true; element('note').value = ''; await read(); status('保存回执已核验。') + } catch (error) { element('recover').disabled = false; status(`保存尚未确认:${error.message}。保留原ID和正文,先查询原保存;未确认前不要添加另一条笔记。`) } +} +element('save').addEventListener('click',save) +element('refresh').addEventListener('click',() => pendingSave ? status('还有结果不明的原保存,请先查询。') : read().catch(error => status(error.message))) +element('recover').addEventListener('click',async () => { + if (!pendingSave) return + try { + const result = await bridge.call('lookupDataMutation',{key:'notes',mutationId:pendingSave.mutationId}) + if (result.status === 'saved') { pendingSave = undefined; element('recover').disabled = true; element('note').value = ''; await read() } + else status(result.status === 'absent' ? '原保存明确未发生。可以用“保存笔记”重试同ID和原正文,冲突时读取并人工合并。' : '原保存仍不明确,请保留当前页面和原ID。') + } catch (error) { status(error.message) } +}) +element('request').addEventListener('click',async () => { + const agentId = element('agent').value.trim() + if (!agentId || !selected) return element('work-status').textContent = '先选择笔记和实际Agent。' + if (pendingWork) return element('work-status').textContent = '已有待查询请求,请沿原ID恢复。' + pendingWork = {clientRequestId:crypto.randomUUID(),userMessageId:crypto.randomUUID(),agentId, + prompt:`请讨论这条笔记并给出可执行建议:${selected.text}`,scopeDescription:'读取notes来源文件核验所选笔记,只讨论该记录,不修改其他记录。', + selection:{schemaVersion:1,selectionRevision:crypto.randomUUID(),summary:`笔记ID:${selected.id},记录版本:${selected.revision},内容:${selected.text};正式实例对象引用授予读取notes来源文件。`,objects:connection.application && etag ? + [{schemaVersion:1,...connection.application,namespace:'data-files',objectId:'notes',revision:etag}] : []}} + element('work').disabled = false + try { + const proposal = await bridge.call('prepareWork',pendingWork) + element('work-status').textContent = `提案已准备,等待平台控件接纳。提案引用:${proposal.proposalRef}` + } catch (error) { element('work-status').textContent = `提案/请求结果尚未确认:${error.message};保留原请求ID查询。` } +}) +element('work').addEventListener('click',async () => { + if (!pendingWork) return + try { element('work-status').textContent = JSON.stringify(await bridge.call('lookupRequest',{agentId:pendingWork.agentId,clientRequestId:pendingWork.clientRequestId}),null,2) } + catch (error) { element('work-status').textContent = error.message } +}) +element('progress').addEventListener('click',async () => { + if (!pendingWork) return + try { + const snapshot = await bridge.call('readWork',{agentId:pendingWork.agentId,clientRequestId:pendingWork.clientRequestId}) + element('work-status').textContent = `准入:${snapshot.admission};执行:${snapshot.execution}。执行完成后仍需核读实际成果。` + element('cancel').disabled = !connection.capabilities?.includes('controlWork') || !snapshot.availableActions?.includes('abort') + element('results').replaceChildren() + for (const [index, resource] of (snapshot.resources ?? []).entries()) { + const row = document.createElement('li'), button = document.createElement('button') + button.textContent = `${resource.role === 'evidence' ? '查看证据' : '打开成果'} ${index + 1}` + button.disabled = !connection.capabilities?.includes('openResource') + button.addEventListener('click',async () => { + try { + const result = await bridge.call('openResource',{locator:resource.locator}) + element('work-status').textContent = result.state === 'navigation-requested' ? '已请求平台定位该成果,文件正文继续由原授权入口核验。' : result.opened === true ? '平台已打开获授权对象。' : '定位结果尚未确认。' + } catch (error) { element('work-status').textContent = `成果未打开:${error.message}` } + }) + row.append(button); element('results').append(row) + } + } catch (error) { element('work-status').textContent = `进度暂不可读:${error.message}` } +}) +element('cancel').addEventListener('click',async () => { + if (!pendingWork) return + try { + const receipt = await bridge.call('controlWork',{agentId:pendingWork.agentId,clientRequestId:pendingWork.clientRequestId,action:'abort'}) + element('work-status').textContent = `原工作停止结果:${receipt.status}。requested 仅表示停止意图已接纳,结果未知时沿原请求查询。` + } catch (error) { element('work-status').textContent = `停止未确认:${error.message}` } +}) +element('remember').addEventListener('click',async () => { + if (!pendingViewState) pendingViewState = {schemaVersion:1,key:'selection',state:JSON.stringify({selectedNoteId:selected?.id ?? null}), + persistence:'device',expectedRevision:viewRevision,mutationId:crypto.randomUUID()} + try { + const receipt = await bridge.call('saveViewState',pendingViewState) + if (receipt.persistence !== 'device' || typeof receipt.revision !== 'string') throw new Error('view_save_result_unknown') + viewRevision = receipt.revision; pendingViewState = undefined + element('view-status').textContent = '已在这台设备记住所选位置。笔记正文仍由实例数据单独保存。' + } catch (error) { element('view-status').textContent = `位置保存未确认:${error.message}。再次点击重试原ID、正文与修订,不覆盖其他更新。` } +}) +try { + bridge = await connectBridge(); connection = await bridge.call('connect') + const remembered = connection.viewStates?.find(value => value.key === 'selection' && value.persistence === 'device') + if (remembered) { + viewRevision = remembered.revision + try { const state = JSON.parse(remembered.state); if (typeof state.selectedNoteId === 'string') rememberedNoteId = state.selectedNoteId } catch { element('view-status').textContent = '此前位置格式不可读,已保留原偏好。' } + } + bridge.onEvent(event => { if (event?.type === 'work-accepted' && event.clientRequestId === pendingWork?.clientRequestId) + element('work-status').textContent = `平台接纳回执:${JSON.stringify(event.result)}。通过原请求继续查询,接纳不等于业务完成。` }) + await read(); element('save').disabled = !connection.capabilities?.includes('saveData'); element('refresh').disabled = false + element('request').disabled = !connection.capabilities?.includes('requestWork') + element('progress').disabled = !connection.capabilities?.includes('readWork') + element('remember').disabled = !connection.capabilities?.includes('saveViewState') || connection.deviceViewState?.state !== 'available' + if (element('remember').disabled) element('view-status').textContent = `设备位置记忆不可用:${connection.deviceViewState?.reason ?? '未授权'}。业务笔记仍可按实际数据能力保存。` +} catch (error) { status(`当前持久数据或协作能力不可用:${error.message}。未保存任何业务数据。`) } diff --git a/skills/app-developer/examples/service-workbench/ui/bridge.js b/skills/app-developer/examples/service-workbench/ui/bridge.js new file mode 100644 index 0000000..567e835 --- /dev/null +++ b/skills/app-developer/examples/service-workbench/ui/bridge.js @@ -0,0 +1,46 @@ +/** 教学SDK适配:桌面有限preload;Web严格parent来源/nonce握手,没有管理token或任意invoke。 */ +const methods = new Set(['connect','capabilities','publishSelection','prepareWork','lookupRequest','readWork','controlWork','openResource','readData','saveData','lookupDataMutation','saveViewState']) +export async function connectBridge() { + if (window.desirecoreApp?.call) return window.desirecoreApp + if (window.parent === window) throw new Error('app_bridge_host_unavailable') + let origin + try { origin = new URL(document.referrer).origin } catch { throw new Error('app_bridge_host_origin_required') } + if (origin === 'null' || origin === location.origin) throw new Error('app_bridge_independent_origin_required') + return new Promise((resolve, reject) => { + let nonce + const timer = setTimeout(() => { window.removeEventListener('message', receive); reject(new Error('app_bridge_host_unavailable')) }, 15000) + function receive(event) { + if (event.source !== window.parent || event.origin !== origin || !event.data || typeof event.data !== 'object') return + if (event.data.protocol === 'desirecore-app-hello-v1' && typeof event.data.nonce === 'string' && !nonce) { + nonce = event.data.nonce + window.parent.postMessage({ protocol:'desirecore-app-ready-v1',nonce },origin) + } else if (nonce && event.data.protocol === 'desirecore-app-port-v1' && event.data.nonce === nonce && event.ports.length === 1) { + window.removeEventListener('message',receive); clearTimeout(timer); resolve(portClient(event.ports[0])) + } + } + window.addEventListener('message',receive) + }) +} +function portClient(port) { + const pending = new Map(), listeners = new Set() + port.onmessage = event => { + const message = event.data + if (message?.protocol !== 'desirecore-app-bridge-v1') return + const request = pending.get(message.requestId) + if (request) { pending.delete(message.requestId); clearTimeout(request.timer); message.error ? request.reject(new Error(message.error)) : request.resolve(message.result) } + else if (message.event !== undefined) for (const listener of listeners) listener(message.event) + } + port.start() + return { + call(method,payload) { + if (!methods.has(method)) return Promise.reject(new Error('app_bridge_method_rejected')) + const requestId = crypto.randomUUID() + return new Promise((resolve,reject) => { + const timer = setTimeout(() => { pending.delete(requestId); reject(new Error('app_bridge_result_unknown')) },30000) + pending.set(requestId,{resolve,reject,timer}); port.postMessage({ protocol:'desirecore-app-bridge-v1',requestId,method,payload }) + }) + }, + onEvent(callback) { listeners.add(callback); return () => listeners.delete(callback) }, + close() { for (const request of pending.values()) { clearTimeout(request.timer); request.reject(new Error('app_bridge_closed')) }; pending.clear(); listeners.clear(); port.close() }, + } +} diff --git a/skills/app-developer/examples/service-workbench/ui/index.html b/skills/app-developer/examples/service-workbench/ui/index.html new file mode 100644 index 0000000..f9af66c --- /dev/null +++ b/skills/app-developer/examples/service-workbench/ui/index.html @@ -0,0 +1,15 @@ + + +
先把想法记下来,再把选中的内容交给 Agent。业务数据与应用程序版本分别保存。
+教学示例使用实际 Agent ID;应用可以自行设计选择器。工作须由平台控件接纳。
+ +先把想法记下来,再把选中的内容交给 Agent。业务数据与应用程序版本分别保存。
+教学示例使用实际 Agent ID;应用可以自行设计选择器。工作须由平台控件接纳。
+ +