From a514b55b68771ff58d76ed64ac5f42be350f789b Mon Sep 17 00:00:00 2001 From: Yige Date: Sun, 18 Oct 2026 17:53:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B6=E5=BD=95=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E5=8F=91=E5=B8=83=E6=8A=80=E8=83=BD=E4=B8=8E?= =?UTF-8?q?=E5=8F=8C=E8=AF=AD=E7=A4=BA=E4=BE=8B=20(#214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 中文 新增按需调用的内置技能 `app-developer`(应用开发与发布):中英正文、sidecar、MIT 许可、静态与服务两个通用工作台示例,以及作者接口与分发参考,并登记进 `builtin-skills.json`。作者管理优先使用 AppManage 的有限动作,执行前须通过 `authoring_capabilities` 确认 `app-authoring-v1`,能力不可用时停止受影响的操作;外部 Node/Docker 运行时、托管、Git 服务与数据库的条款和可能费用已在发现文案、compatibility 与正文披露。另将 dev-environment-setup 探测参考中的三处用户 home 示例改为通用路径。 与 #220、#212 合入后的 main 解决 manifest 冲突:技能总数 144,市场版本 1.5.13。验证:catalog 与 i18n 校验 0 errors / 106 warnings(含新技能尚未 ref-bound review 的治理提醒),translate --check 与 git diff --check 通过;远端 validate、translate、CodeQL 通过。Copilot 因额度用尽未能审查。 ## English Add the on-demand builtin `app-developer` skill (application development and publishing): English/Chinese bodies, sidecar, MIT license, static and service workbench examples, plus authoring-interface and distribution references, registered in `builtin-skills.json`. Authoring prefers AppManage's finite actions and must confirm `app-authoring-v1` through `authoring_capabilities` first, stopping affected operations when the capability is unavailable. External Node/Docker runtime, hosting, Git service and database terms and possible costs are disclosed. Three user-home examples in the dev-environment-setup probe reference are neutralized. The manifest conflict with main after #220 and #212 is resolved: 144 skills, market version 1.5.13. Catalog and i18n validation report 0 errors with 106 warnings (including the new skill's pending ref-bound review notice); translate --check and git diff --check pass; remote validate, translate and CodeQL passed. Copilot could not review because of its quota limit. --- builtin-skills.json | 1 + manifest.json | 4 +- skills/app-developer/LICENSE | 9 ++ skills/app-developer/SKILL.md | 108 +++++++++++++ skills/app-developer/SKILL.zh-CN.md | 43 +++++ skills/app-developer/catalog-metadata.v1.json | 77 +++++++++ .../examples/service-workbench/LICENSE | 9 ++ .../service-workbench/app-release.json | 147 ++++++++++++++++++ .../service-workbench/application.json | 21 +++ .../service-workbench/data-contract.json | 5 + .../service-workbench/release-inputs.json | 82 ++++++++++ .../service-workbench/runtime/server.mjs | 40 +++++ .../service-workbench/runtime/运行说明.md | 15 ++ .../examples/service-workbench/ui/app.js | 114 ++++++++++++++ .../examples/service-workbench/ui/bridge.js | 46 ++++++ .../examples/service-workbench/ui/index.html | 15 ++ .../examples/service-workbench/说明.md | 13 ++ .../examples/static-workbench/LICENSE | 9 ++ .../static-workbench/app-release.json | 141 +++++++++++++++++ .../static-workbench/application.json | 6 + .../static-workbench/data-contract.json | 5 + .../static-workbench/release-inputs.json | 80 ++++++++++ .../examples/static-workbench/ui/app.js | 114 ++++++++++++++ .../examples/static-workbench/ui/bridge.js | 46 ++++++ .../examples/static-workbench/ui/index.html | 15 ++ .../examples/static-workbench/说明.md | 17 ++ .../references/authoring-api.zh-CN.md | 76 +++++++++ .../references/distribution.zh-CN.md | 15 ++ .../app-developer/scripts/create-release.mjs | 34 ++++ .../references/probe-snapshot.md | 6 +- 30 files changed, 1308 insertions(+), 5 deletions(-) create mode 100644 skills/app-developer/LICENSE create mode 100644 skills/app-developer/SKILL.md create mode 100644 skills/app-developer/SKILL.zh-CN.md create mode 100644 skills/app-developer/catalog-metadata.v1.json create mode 100644 skills/app-developer/examples/service-workbench/LICENSE create mode 100644 skills/app-developer/examples/service-workbench/app-release.json create mode 100644 skills/app-developer/examples/service-workbench/application.json create mode 100644 skills/app-developer/examples/service-workbench/data-contract.json create mode 100644 skills/app-developer/examples/service-workbench/release-inputs.json create mode 100644 skills/app-developer/examples/service-workbench/runtime/server.mjs create mode 100644 skills/app-developer/examples/service-workbench/runtime/运行说明.md create mode 100644 skills/app-developer/examples/service-workbench/ui/app.js create mode 100644 skills/app-developer/examples/service-workbench/ui/bridge.js create mode 100644 skills/app-developer/examples/service-workbench/ui/index.html create mode 100644 skills/app-developer/examples/service-workbench/说明.md create mode 100644 skills/app-developer/examples/static-workbench/LICENSE create mode 100644 skills/app-developer/examples/static-workbench/app-release.json create mode 100644 skills/app-developer/examples/static-workbench/application.json create mode 100644 skills/app-developer/examples/static-workbench/data-contract.json create mode 100644 skills/app-developer/examples/static-workbench/release-inputs.json create mode 100644 skills/app-developer/examples/static-workbench/ui/app.js create mode 100644 skills/app-developer/examples/static-workbench/ui/bridge.js create mode 100644 skills/app-developer/examples/static-workbench/ui/index.html create mode 100644 skills/app-developer/examples/static-workbench/说明.md create mode 100644 skills/app-developer/references/authoring-api.zh-CN.md create mode 100644 skills/app-developer/references/distribution.zh-CN.md create mode 100644 skills/app-developer/scripts/create-release.mjs 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 协作

教学示例使用实际 Agent ID;应用可以自行设计选择器。工作须由平台控件接纳。

+ +尚未准备请求。
+ diff --git a/skills/app-developer/examples/service-workbench/说明.md b/skills/app-developer/examples/service-workbench/说明.md new file mode 100644 index 0000000..093719a --- /dev/null +++ b/skills/app-developer/examples/service-workbench/说明.md @@ -0,0 +1,13 @@ +# 服务笔记工作台教学示例 + +这是可修改的固定网页+原生Node只读服务,不是正式市场条目。先读本Skill作者接口和runtime/运行说明.md,在真实目标环境按目标组合原 AppManage.prepare_release/runtime_enroll/commit_release 动作,纳管原执行合同、正式发布并核验。缺TaskHost owner或真实身份时保持不可用,不能重标为静态网页来绕过。 + +笔记仍通过应用桥的notes key持久化,用户、实例和预览各有独立scope;Node端不写个人数据或固定程序目录。网页可在本源 `/api/summary?count=<当前笔记条数>` 请求只读统计;这个能力不使用平台token,也不接收任意文件路径或SQL。 + +与Agent协作、保存超时恢复及更新/data合同遵循静态示例的相同约定。应用是否能运行与外部Node环境、原执行owner和当前用户授权有关。包不附带用户数据库、.env、平台身份或proof值。分发必须取得真实不可变来源和目录审阅回执;第二用户安装需独立环境,不共享开发者进程或业务数据。 + +工作查询保留原 agentId 和 clientRequestId;只有 readWork 返回的真实 Task 成果 locator 才可交 openResource 定位,navigation-requested 不等于文件正文已打开。当前 controlWork 只按 availableActions 支持 abort;未关联 Task 的工作成果列表可以为空,不凭 UI 编造 Task。 + +用户可显式选择“在这台设备记住所选位置”。示例 viewStateContract 只允许 selection 键并限制 1024 字节,不保存笔记正文;需实际 saveViewState grant,设备保存保持原 mutationId、expectedRevision 和正文。能力缺失时保持业务数据与视图偏好分别报告,预览不借正式设备状态。 + +用户要求分发时,用原 AppManage.export_release 的 owner_request 显式导出全部 payload 与 app-release.json;另给原工具真实写权限内的 export_file_ref,首次 expected_absent:true,已有目标用原 expected_etag。返回 archiveFile 的实际 fileRef/sha256/size/etag,核验 sha256/size 与 export 的 archiveDigest/size 一致后交原授权上传工具。取得真实固定来源再用 distribution_prepare 的 owner_request,registryRepoId 从 authoring_capabilities.registryTargetIds 选本次实际目标,生成三文件草稿;它不上传、不提交目录,也不改变个人安装身份。普通 HTTP 不自动注入平台凭据,不能读取实例 token 或绕过认证。 diff --git a/skills/app-developer/examples/static-workbench/LICENSE b/skills/app-developer/examples/static-workbench/LICENSE new file mode 100644 index 0000000..66a0037 --- /dev/null +++ b/skills/app-developer/examples/static-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/static-workbench/app-release.json b/skills/app-developer/examples/static-workbench/app-release.json new file mode 100644 index 0000000..47244c4 --- /dev/null +++ b/skills/app-developer/examples/static-workbench/app-release.json @@ -0,0 +1,141 @@ +{ + "schemaVersion": 1, + "canonicalization": "app-release-c14n-v1", + "definition": { + "id": "personal-notes-workbench", + "name": "个人笔记工作台", + "version": "1.0.0", + "productKind": "application", + "icon": "Notebook", + "iconLetter": "记", + "entrypoints": [ + { + "id": "main", + "name": "笔记", + "kind": "embedded-web", + "serviceRef": "workbench:main" + } + ], + "placementPolicy": { + "showInMyApps": true, + "allowNavPin": true, + "allowAgentBinding": true + } + }, + "delivery": { + "type": "web-bundle", + "entrypoints": [ + { + "entrypointId": "main", + "path": "ui/index.html" + } + ] + }, + "files": [ + { + "path": "LICENSE", + "role": "license", + "sha256": "e863481f423546e8984c07ffa00609d338b7f7533f60e89392301028163aa89e", + "size": 1067 + }, + { + "path": "application.json", + "role": "definition", + "sha256": "6f173d257bbb07d85deefe203a18741ec31fed27f244dbce6e37ef42f6662a9a", + "size": 371 + }, + { + "path": "data-contract.json", + "role": "data-contract", + "sha256": "e195b2fd60a2798141c4c75ab57e0927d115bfba7888b0881b69dffa9dcb26cc", + "size": 266 + }, + { + "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": "83de1fc2216834dc4326d607af45f2174609772b08a03acf6a98197363eec5a8", + "size": 2624 + }, + { + "path": "说明.md", + "role": "documentation", + "sha256": "672c14b8ca2b0f63c006762e2e296a027894a2f49fc447665ce55960f5bbcba2", + "size": 3352 + } + ], + "source": { + "kind": "worktree", + "sha256": "a28c1ce637a81f4695c6cd642fc202553bdaf79ad9f8d4b790dd9925786c1516" + }, + "supportedPlatforms": [ + "darwin", + "win32", + "linux", + "web" + ], + "minimumClientVersion": "10.0.180", + "requiredCapabilities": [ + "app-authoring-v1", + "release", + "web-bundle", + "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": "87171e43d8196120556f97ff02813fad04e362ffdfd18e12851750b4124d071b" +} diff --git a/skills/app-developer/examples/static-workbench/application.json b/skills/app-developer/examples/static-workbench/application.json new file mode 100644 index 0000000..9348b45 --- /dev/null +++ b/skills/app-developer/examples/static-workbench/application.json @@ -0,0 +1,6 @@ +{ + "id": "personal-notes-workbench", "name": "个人笔记工作台", "version": "1.0.0", "productKind": "application", + "icon": "Notebook", "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/static-workbench/data-contract.json b/skills/app-developer/examples/static-workbench/data-contract.json new file mode 100644 index 0000000..03c2cb5 --- /dev/null +++ b/skills/app-developer/examples/static-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/static-workbench/release-inputs.json b/skills/app-developer/examples/static-workbench/release-inputs.json new file mode 100644 index 0000000..9857160 --- /dev/null +++ b/skills/app-developer/examples/static-workbench/release-inputs.json @@ -0,0 +1,80 @@ +{ + "description": "显式收录教学程序材料,业务notes.json只属于实例数据根,不进入发行包。", + "delivery": { + "type": "web-bundle", + "entrypoints": [ + { + "entrypointId": "main", + "path": "ui/index.html" + } + ] + }, + "supportedPlatforms": [ + "darwin", + "win32", + "linux", + "web" + ], + "requiredCapabilities": [ + "app-authoring-v1", + "release", + "web-bundle", + "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" + } + ], + "viewStateContract": { + "schemaVersion": 1, + "keys": [ + "selection" + ], + "maxBytes": 1024, + "formatVersion": "1" + } +} diff --git a/skills/app-developer/examples/static-workbench/ui/app.js b/skills/app-developer/examples/static-workbench/ui/app.js new file mode 100644 index 0000000..1201253 --- /dev/null +++ b/skills/app-developer/examples/static-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/static-workbench/ui/bridge.js b/skills/app-developer/examples/static-workbench/ui/bridge.js new file mode 100644 index 0000000..567e835 --- /dev/null +++ b/skills/app-developer/examples/static-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/static-workbench/ui/index.html b/skills/app-developer/examples/static-workbench/ui/index.html new file mode 100644 index 0000000..014c141 --- /dev/null +++ b/skills/app-developer/examples/static-workbench/ui/index.html @@ -0,0 +1,15 @@ + + +个人笔记工作台 + +

个人笔记工作台

先把想法记下来,再把选中的内容交给 Agent。业务数据与应用程序版本分别保存。

+
+ +正在连接应用宿主…
+
只保存所选笔记的位置,不保存笔记正文。
+

与 Agent 协作

教学示例使用实际 Agent ID;应用可以自行设计选择器。工作须由平台控件接纳。

+ +尚未准备请求。
+ diff --git a/skills/app-developer/examples/static-workbench/说明.md b/skills/app-developer/examples/static-workbench/说明.md new file mode 100644 index 0000000..58970b1 --- /dev/null +++ b/skills/app-developer/examples/static-workbench/说明.md @@ -0,0 +1,17 @@ +# 个人笔记工作台教学示例 + +这是可修改的最小材料,不是正式市场条目或已经完成的真实 Agent 验收。将示例复制到任务实际授权工作目录,用本 Skill 的 create-release.mjs 生成 app-release.json,再按目标组合原 AppManage.prepare_release/preview_prepare/commit_release 动作,准备固定材料、snapshot 预览与个人发布。 + +笔记保存在声明的 notes key,由平台按用户/实例/预览分配专用数据根。预览数据不自动成为正式数据;包中没有 notes.json。裸浏览器打开可看到页面,缺少可信桥时不会假装已持久保存。Web 须有独立 app origin 和真实 parent MessagePort;同源或无来源时桥拒绝。 + +保存使用 expectedETag/expectedAbsent;结果不明保留原 mutationId 和正文,先查原回执。示例只在当前页面保留待查询 ID,完整产品应通过获授权的视图状态或自身数据合同保存可恢复的请求引用,不能持久化平台 token。 + +“交给 Agent”先准备提案。真正提交在平台控件上确认,再从 work-accepted 或同请求 lookup 得到真实接纳/Task 状态;页面不能自己签 intent。正式实例引用已声明notes来源文件:namespace为data-files、objectId为notes、revision为readData的真实ETag,平台不猜测notes JSON内部业务schema。选中笔记的稳定id、记录版本及可见文字留在summary;授权范围明确包含读取来源文件并仅讨论所选记录。Agent读取真实文件后定位记录,不能把未实现的notes业务对象owner说成已支持。预览只发讨论摘要,不借正式实例身份。 + +更新保持原安装/data scope并先核验 notes-v1兼容;程序回退不恢复旧笔记。卸载默认保留业务数据。示例无需第三方服务,调用 Agent 仍取决于真实算力连接、权限和额度。平台能力未开放或未发行时保持不可用并说明原因。 + +工作查询保留原 agentId 和 clientRequestId;只有 readWork 返回的真实 Task 成果 locator 才可交 openResource 定位,navigation-requested 不等于文件正文已打开。当前 controlWork 只按 availableActions 支持 abort;未关联 Task 的工作成果列表可以为空,不凭 UI 编造 Task。 + +用户可显式选择“在这台设备记住所选位置”。示例 viewStateContract 只允许 selection 键并限制 1024 字节,不保存笔记正文;需实际 saveViewState grant,设备保存保持原 mutationId、expectedRevision 和正文。能力缺失时保持业务数据与视图偏好分别报告,预览不借正式设备状态。 + +用户要求分发时,用原 AppManage.export_release 的 owner_request 显式导出全部 payload 与 app-release.json;另给原工具真实写权限内的 export_file_ref,首次 expected_absent:true,已有目标用原 expected_etag。返回 archiveFile 的实际 fileRef/sha256/size/etag,核验 sha256/size 与 export 的 archiveDigest/size 一致后交原授权上传工具。取得真实固定来源再用 distribution_prepare 的 owner_request,registryRepoId 从 authoring_capabilities.registryTargetIds 选本次实际目标,生成三文件草稿;它不上传、不提交目录,也不改变个人安装身份。普通 HTTP 不自动注入平台凭据,不能读取实例 token 或绕过认证。 diff --git a/skills/app-developer/references/authoring-api.zh-CN.md b/skills/app-developer/references/authoring-api.zh-CN.md new file mode 100644 index 0000000..8fc907a --- /dev/null +++ b/skills/app-developer/references/authoring-api.zh-CN.md @@ -0,0 +1,76 @@ +# 作者接口与交付材料 + +本文是未发行候选协议的知识材料。中文权威接口正文位于主仓 `docs/功能/市场系统/API/应用开发发布接口.md`;该路径与外仓 PR 不声明在线文档、客户端或官方 seed 已更新。每次先读取实际 capability 结果和当前 Schema。 + +Agent 使用原 `AppManage` 的有限作者动作,直接进入已有 release、preview、runtime 或 Docker owner。原 ToolExecutor 调用源、真实 Run/用户/Agent/宿主、材料读取根、当前策略与审批在效果边界重新核验。参数不自报 owner,也不获得平台管理凭据。以下动作可按目标组合,表格不是固定开发步骤。 + +| AppManage action | 参数包络 | 返回事实 | +|---|---|---| +| `authoring_capabilities` | 无额外参数 | `capabilities` 的实际 features/policy/hostInstanceId 和当前 Agent 可读的 `fileRoots` 与已登记的 `registryTargetIds`;不授上传或文件写权限 | +| `read_draft` | `file_ref: FileResourceRef` | 从真实授权文件读取并核验的 `draft: AppDraftRef` | +| `prepare_release` | `release_request: AppReleasePrepareRequest` | owned candidate、真实 payload/descriptor 摘要、当前 expectedRevision | +| `commit_release` | `release_request: AppReleaseCommitRequest` | 唯一 writer CAS 保存;activation 与保存分别核验 | +| `lookup_release_submission` | `installation_id`、原 `submission_id` | 原提交 committed/absent/unknown,不换 ID 重装 | +| `preview_list` | 无额外参数 | 当前 owner 的实际 previews | +| `preview_read` | `draft_id` | 原草稿的当前预览与 revision | +| `preview_prepare` | `owner_request: AppPreviewRequest` | snapshot/live、实际 revision、隔离 runtime | +| `preview_end` | `owner_request:{draftId,expectedRevision}` | 只结束该原预览,保留 Task 和开发材料 | +| `runtime_read` | `installation_id`、可选 `entrypoint_id` | 精确实例入口的当前运行状态 | +| `runtime_start` | `installation_id` | 原执行 owner 当前再审批后的实际启动结果 | +| `runtime_stop` | `installation_id` | 原执行 owner 当前再审批后的实际停止结果 | +| `runtime_enroll` | `owner_request:{prepareRef,installationId,terminalId,endpoint:{port,identityPath},startupPolicy}` | 原真实已接纳命令的固定 native 执行合同 | +| `development_enroll` | `owner_request:{prepareRef,draft,terminalId,endpoint:{port,identityPath}}` | 原授权可变源码的 development-only 合同,只用于 live 预览 | +| `docker_prepare` | `owner_request: DockerPrepareEnrollmentInput` | 私有短期 CID/env 文件与实际 create 命令,不执行 Docker 命令 | +| `docker_enroll` | `owner_request:{enrollmentRef,terminalId}` | 真实 daemon/CID/RepoDigest/挂载/端口执行合同,不以 CLI 退出代替健康 | +| `release_history` | `installation_id` | 保存材料历史,不假称数据备份 | +| `export_release` | `owner_request: AppReleaseExportRequest`;可另给顶层 `export_file_ref` 与 `expected_absent:true` 或 `expected_etag` | exportRef、archiveDigest、releaseDigest、实际 files/size;显式导出时 archiveFile 包含真实 ZIP fileRef/sha256/size/etag/replayed,不上传 | +| `distribution_prepare` | `owner_request: AppDistributionPrepareRequest`(exportRef、registryRepoId、source、metadata) | 从真实 export 与 capability 中的目录目标生成三文件草稿,未上传/未提交 | + +例如能力调用为 `AppManage({action:'authoring_capabilities'})`。读草稿使用 `action:'read_draft'` 与返回的授权根对应的 `file_ref`;发布准备/提交分别使用 `release_request`,预览/运行纳管/导出使用 `owner_request`。这些包络不是互换字段,原工具及内部 owner 仍逐个严格核验完整 DTO。 + +`prepare_release` 的 AppReleasePrepareRequest 包含 `mutationId`、已授权 `materialRoot: FileResourceRef`、`manifestPath`、可选 expectedReleaseDigest、`target:{appId,installationId,deviceId,sourceId}`。FileResourceRef 的 schemaVersion/instanceId/agentId/rootId/path 来自真实 fileRoots 与本次材料;path 是授权根内相对路径,不能复制别的宿主/Agent/root。首次个人 sourceId 为服务约定的 `user-managed`,不能用名字冒充目录身份。 + +`commit_release` 使用原 prepareRef、expectedRevision(首次为 none,更新取原安装修订)、稳定 submissionId、完整 material(已解析的 ApplicationInstallationRequest)。登记失败先用 lookup_release_submission 查同安装同提交,不重新复制/启动软件。只读动作不触发修改审批;修改动作继续沿原工具审批和最终材料/调用身份复验。调用取消的短授权不保存为长期运行权限,已接纳的后台工作与 owned 临时预览补偿分别按其原 owner 管理。 + +`AppDraftRef` 文件有 `schemaVersion:1`、稳定 draftId/appId、实际授权 `workdir`、绝对 planPath、真实 `source:{agentId,sessionId}`,可选 targetInstallationId/baselineReleaseDigest。文件引用从当前请求或授权读取工具取得。preview_prepare 用原 draft、mode、expectedRevision,snapshot 还给 release;结束只传原 draftId/revision,不能猜最近草稿或 borrowed 正式作用域。 + +release 采用 `schemaVersion:1` / `canonicalization: app-release-c14n-v1`,包括 definition、delivery、files、source、supportedPlatforms、requiredCapabilities。files 的 role 为 definition/ui/runtime/dependency/documentation/license/evidence/data-contract;路径为包内规范 POSIX。definition 文件必须与 manifest.definition 一致。`app-release.json` 本身不放入 files,不能让摘要自引用。 + +web-bundle.delivery.entrypoints 固定 `{entrypointId,path,spaFallback?}`;native-app 保存 runtimeInstructionsPath;docker-app 同时必填 imageRefs,逐项固定显式注册表/repository@sha256,不附登录凭据;实际仍由原容器 owner 拉取、执行和身份核验。缺容器环境或 adapter 时明确不可用。平台不解释说明为命令 DSL。source 是实际 Git 完整 commit 或工作树内容 sha256;supportedPlatforms 为 darwin/win32/linux/web。最低客户端是 minimumClientVersion,实际能力仍从握手确认。 + +数据合同声明 formatVersion、readableVersions、writableVersions、有限 files key/path/maxBytes/contentType 和 uninstallRetention: retain。备份/迁移/恢复说明引用必须在 payload 白名单中;说明不授权自动执行,安装历史不代表真实数据备份。 + +动态 Node 示例的 `/.desirecore-app-identity` 返回当前 TaskHost 注入的 proof/releaseDigest/scopeId/runtimeGeneration;proof 仅用于本次运行身份握手,不当应用 API token,不写日志。`AppManage.runtime_enroll` 使用已 prepare 的材料、installationId、真实 terminalId、endpoint.port/identityPath 与 startupPolicy。服务固定原执行合同后,原 Bash 显式停原命令,正式 commit 后由原 owner 启动新代际再核验。具体环境、执行审批和数据根均由实际 owner 决定。 + +原执行命令的初始默认端口写成 `PORT="${PORT:-32000}" node runtime/server.mjs`,其中默认端口需实际可用。重启仍执行原真实 argv,Harness 注入的 preview PORT 不能被写死赋值盖掉。若应用后端需要数据,APP_DATA_DIR 也用已授权 seed 根作为初次默认,后续严格使用 owner 注入的独立根;不可将 seed/candidate 数据自动迁入正式实例。 + +Docker prepare 的 owner_request 为 `{prepareRef,installationId,endpoint:{port,containerPort,identityPath},startupPolicy}`,可提议清单内 imageRef、容器 commandArgs/entrypoint、materialMountPath/dataMountPath;这些不是执行授权。使用返回的实际 command/cwd 沿原获审批 Bash/TaskHost 执行,拿真实 terminalId 与 enrollmentRef 用 docker_enroll 登记。当前 adapter 只接本地 Linux daemon 上该已核验的固定单容器形态;其他环境/挂载/服务编排保持原 owner 并报告原因,不自动安装 Docker、pull 镜像或修改用户容器。RepoDigest 与 Image ID 分别验证;冷重连核读真实 StartedAt,CID 不存在或结果 unknown 时不补建第二实例。 + +## 平台 HTTP 管理消费者 + +HTTP 接口保留给平台 UI 或具有合法管理凭据的消费者,普通 HttpRequest 不自动注入实例凭据。Agent 缺少对应原工具能力时说明不可用,不读取实例 token、不改 Origin 绕过 401,也不把凭据交网页。HTTP 返回包络与原 AppManage 结果各按实际合同读取。 + +| 平台接口 | 用途 | +|---|---| +| `GET /api/apps/capabilities`、`POST /api/apps/authoring/drafts/read` | 平台能力与草稿读取 | +| `POST /api/apps/releases/prepare`、`commit` | 固定材料与个人发布 | +| `POST /api/apps/authoring/previews`、`:draftId/end` | 平台预览和结束 | +| `POST /api/apps/runtime/contracts`、`development-contracts` | 平台 native/源码纳管 | +| `POST /api/apps/runtime/docker/enrollments/prepare`、`enroll` | 平台 Docker 纳管 | +| `POST /api/apps/runtime/:installationId/start`、`stop` | 平台正式启动/停止 | +| `POST /api/apps/releases/export`、export 返回的 `downloadPath` | 生成归档和下载原始 ZIP 字节 | +| `POST /api/apps/distribution/prepare` | 读取真实 exportRef 生成 Registry 三文件,未上传/未提交 | + +downloadPath 仍是平台管理下载路径,不授予网页或普通 HTTP 工具访问。Agent 需要实际 ZIP 时,在 export_release 顶层另给 export_file_ref:使用当前 Agent 原写权限内的 FileResourceRef 目标,path 为该根内相对路径;不能把服务 exports 私有目录挂成工作根。首次给 expected_absent:true,覆盖已有文件给原 expected_etag,两者不能同时提供。返回 archiveFile={fileRef,sha256,size,etag,replayed},sha256/size 必须等于 export 的 archiveDigest/size;这才是可交原授权上传工具的实际文件。只读 fileRoots 不授写权限。响应丢失保持相同目标/条件/完整正文,已有同一固定字节可重放,其他内容必须按实际 ETag 决定,不能盲覆盖。 + +目录草稿用原 AppManage.distribution_prepare,完整 AppDistributionPrepareRequest 放在 owner_request;registryRepoId 从 authoring_capabilities.registryTargetIds 选本次真实目标,不自报 sourceId 或构造连接。source 指向用户已实际上传并核验的不可变来源,sha256 等于 archiveDigest。返回 draft 三文件及 mapping/facts;upload 仍为 unverified,catalog 仍为 not-submitted,sourceAcl 仍为 source-admin-managed。写入目录仓库、提交 PR、目录准入和第二用户获取各由原工具取得实际回执;没有目标或来源连接时说明不可用。 + +## 网页协作合同 + +网页工作查询使用原 `{agentId,clientRequestId}`,SDK 不猜最近的 Agent。controlWork 当前只允许原 snapshot.availableActions 中的 abort。readWork.resources 的 `{locator}` 由实际 Run/Task 成果 owner 返回,`openResource({locator})` 回 navigation-requested 不保证文件正文已打开;不要把调用者任意 FileRef、Task ID 或 URL 变成果授权。 + +设备视图偏好需 manifest.viewStateContract `{schemaVersion:1,keys,maxBytes,formatVersion?}` 与用户 saveViewState grant。connect.viewStates 为 array;device 项带 revision,deviceViewState 为 available(formatVersion/maxBytes)或 unavailable(reason)。默认 memory;显式 device 请求包含 schemaVersion/key/state/persistence/expectedRevision/mutationId,保持同正文同 ID 重试。设备偏好与业务数据、跨设备同步和数据备份是不同事实,preview 不借用正式 device 项。 + +Web 最终接纳沿可信聊天传输。实际部署需 owner 提供 TLS/domain/DNS/authority port,可信 shell 取得 web-host ticket 并显式 bootstrapSdk;页面不持平台 token,不伪造 native physical intent,不选择管理上游。未支持的 navigate、未配置的 Web 来源或未授权能力分别说明实际 reason。 + +长期 Web 呈现通过宿主 SDK 的有限同 session 续期维持。SDK connect 先确认原 parent 的当前呈现,再使用同应用来源的空 POST keepalive;它不执行业务命令、不扩权,不为已换版、已撤销或失效的页面延长会话。不要自行复制 ticket、cookie 或平台凭据来修复过期页。 diff --git a/skills/app-developer/references/distribution.zh-CN.md b/skills/app-developer/references/distribution.zh-CN.md new file mode 100644 index 0000000..2407652 --- /dev/null +++ b/skills/app-developer/references/distribution.zh-CN.md @@ -0,0 +1,15 @@ +# 固定应用版本分发 + +1. 确认用户要分发的精确版本、真实目标仓库/Registry、公共或私有范围、实际登录连接和材料许可。生成文件、个人发布或已有预览都不是上传授权;既有会话中的明确分发授权直接沿用。 +2. 用原 AppManage 的 export_release 动作,从 prepareRef,或 installationId + expectedRevision 导出;完整 AppReleaseExportRequest 放在 owner_request。`paths` 必须显式覆盖全部 payload + app-release.json;不包含服务物化 descriptor、owner 标记、个人数据和秘密。需要实际 ZIP 时,另在顶层给 export_file_ref(当前 Agent 原写权限目录的目标 FileResourceRef)与 expected_absent:true 或原 expected_etag;不能同时给两个条件或覆盖未知文件。核验响应的 files/size 和 archiveFile 的实际 fileRef/sha256/size/etag/replayed,sha256/size 必须等于 export 的 archiveDigest/size。读能力返回的 fileRoots 不授写权限;响应丢失保持同一目标、条件与完整正文恢复。downloadPath 是平台管理消费者路径,普通 HttpRequest 不自动注入凭据。 +3. 将真实 archiveFile.fileRef 交原实际授权上传工具;GitHub Release 上传此 ZIP 后,核读真实下载地址、固定 commit 和原始字节摘要;Gitea 私有仓库可提交 `releases/.zip`,取得完整 commit 与该路径。可变 tag/latest 只作展示,必须解析到固定引用。没有真实来源回执就报告“制品已生成,尚未上传”。 +4. 使用原 AppManage 的 distribution_prepare 动作,owner_request 为 AppDistributionPrepareRequest:`exportRef`、从 authoring_capabilities.registryTargetIds 选择的真实 registryRepoId、`source:{kind:'release'|'git',url,ref,sha256,path?}` 和实际 metadata。目录目标列表是配置事实,不授予源端上传权限;普通模型不读取实例 token 或改 Origin 绕过认证。git 必须给固定包路径,sha256 必须等于导出 archiveDigest。产品 ID/名称/版本/入口不由 metadata 另填。 +5. metadata 包含 author/description/tags/category/license/redistribution/availability/requiredClientVersion,以及实际维护者、品牌和复核材料。证据不足保留 listing-only;不要填不存在的审核者、认证、发布时间或平台验收。已复核的许可证路径应来自同一 release 的 license role 文件。 +6. 返回的是 `entries//manifest.json`、`catalog-metadata.v1.json`、`install.md` 内容和摘要。mapping 关联目标 sourceId、逻辑 ID、releaseDigest 与 archiveDigest,descriptor 安装时按真实来源物化。将三文件写入实际授权的目录仓库后审阅 diff,通过其原治理提交 PR;不能自动合并或说目录已开放。 +7. 查询真实目录可获取状态与目标客户端握手,再由第二用户/设备沿原 acquisition/app-install-manager 路径安装并核验。两来源同名不共享实例或数据;目录更新仅给更新候选,不改原安装快照。 + +候选 Registry 4.3 Schema 与客户端 reader 必须先分别 review;正式 Registry seed 只指已合入的受审提交。候选 reader 仅在明确受信的非官方测试来源开放,正式条目等待客户端能力发行,不能借 10.0.180 字符串跳过 app-authoring-v1。 + +私有源使用 `access: private` 的真实 Registry 连接,只读登记端点、不转公开镜像;认证失败拒绝新获取。字段不是 ACL,GitHub/Gitea 管理员仍负责实际成员权限。平台团队成员移除不自动改变源端 ACL,不能宣称已撤销。已下载字节不能收回,本机数据不自动删除。 + +离线可继续使用已核验的本地固定版本,远端依赖如实不可用;离线不能产生新的来源上传成功。目录撤回阻止新获取并保留理由,不自动卸载已安装应用。临时导出不修改当前个人发布,取消分发保留原工作台。 diff --git a/skills/app-developer/scripts/create-release.mjs b/skills/app-developer/scripts/create-release.mjs new file mode 100644 index 0000000..641e16b --- /dev/null +++ b/skills/app-developer/scripts/create-release.mjs @@ -0,0 +1,34 @@ +/** 教学清单生成器:读取显式inputs,固定原始字节;不是平台自动build/install器。 */ +import { createHash } from 'node:crypto' +import { lstatSync, readFileSync, writeFileSync } from 'node:fs' +import { resolve, join } from 'node:path' + +if (!process.argv[2]) throw new Error('example_directory_required') +const directory = resolve(process.argv[2]) +const input = JSON.parse(readFileSync(join(directory, 'release-inputs.json'), 'utf8')) +const hash = bytes => createHash('sha256').update(bytes).digest('hex') +const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' + ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value +const files = input.files.map(file => { + if (typeof file.path !== 'string' || /[\\\x00-\x1f?#:]/.test(file.path) || file.path.startsWith('/') || + file.path.split('/').some(part => !part || part === '.' || part === '..') || file.path === 'app-release.json') throw new Error('invalid_payload_path') + const parts = file.path.split('/') + for (let i = 1; i <= parts.length; i++) if (lstatSync(join(directory, ...parts.slice(0, i))).isSymbolicLink()) throw new Error('payload_link_rejected') + const bytes = readFileSync(join(directory, file.path)) + return { path: file.path, role: file.role, sha256: hash(bytes), size: bytes.length } +}).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0) +if (new Set(files.map(file => file.path)).size !== files.length) throw new Error('duplicate_payload_path') +if (!files.some(file => file.path === 'application.json' && file.role === 'definition') || + !files.some(file => file.path === '说明.md') || + (input.dataContractPath && !files.some(file => file.path === input.dataContractPath && file.role === 'data-contract'))) throw new Error('missing_declared_payload') +const definition = JSON.parse(readFileSync(join(directory, 'application.json'), 'utf8')) +const manifest = { schemaVersion: 1, canonicalization: 'app-release-c14n-v1', definition, delivery: input.delivery, files, + source: { kind: 'worktree', sha256: hash(JSON.stringify(canonical(files))) }, supportedPlatforms: input.supportedPlatforms, + minimumClientVersion: '10.0.180', requiredCapabilities: input.requiredCapabilities, + ...(input.dataContractPath ? { dataContract: JSON.parse(readFileSync(join(directory, input.dataContractPath), 'utf8')) } : {}), + ...(input.bridgeCapabilities ? { bridgeCapabilities: input.bridgeCapabilities } : {}), + ...(input.viewStateContract ? { viewStateContract: input.viewStateContract } : {}), + instructionsPath: '说明.md' } +manifest.releaseDigest = hash(JSON.stringify(canonical(manifest))) +writeFileSync(join(directory, 'app-release.json'), JSON.stringify(manifest, null, 2) + '\n') +console.log(JSON.stringify({ appId: definition.id, version: definition.version, releaseDigest: manifest.releaseDigest, files: files.length })) diff --git a/skills/dev-environment-setup/references/probe-snapshot.md b/skills/dev-environment-setup/references/probe-snapshot.md index dad7529..4b95adb 100644 --- a/skills/dev-environment-setup/references/probe-snapshot.md +++ b/skills/dev-environment-setup/references/probe-snapshot.md @@ -45,10 +45,10 @@ Windows 上 `wsl` 字段值类似 `{ "installed": true, "version": "2", "default "desirecore_api": "https://127.0.0.1:38291", "system_python": { "path": "/opt/homebrew/bin/python3", "version": "3.12.4" }, "system_pip": { "path": "/opt/homebrew/bin/pip3", "version": "24.0" }, - "hatch_path": "/Users/wangyi/.desirecore/runtime/hatch/hatch", + "hatch_path": "/Users/example/.desirecore/runtime/hatch/hatch", "hatch_version": "1.16.5", "hatch_versions": ["3.11", "3.12"], - "active_venv": "/Users/wangyi/proj/.venv", + "active_venv": "/Users/example/proj/.venv", "pep668": false, "pyenv_path": "", "conda_path": "" @@ -70,7 +70,7 @@ Windows 上 `wsl` 字段值类似 `{ "installed": true, "version": "2", "default "desirecore_api": "https://127.0.0.1:38291", "system_node": { "path": "/opt/homebrew/bin/node", "version": "22.4.1" }, "system_npm": { "path": "/opt/homebrew/bin/npm", "version": "10.8.1" }, - "volta_path": "/Users/wangyi/.desirecore/runtime/volta/volta", + "volta_path": "/Users/example/.desirecore/runtime/volta/volta", "volta_version": "2.0.2", "volta_tools": { "node": ["22.4.1"],