@hanamesh/dsh-app-host 0.2.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +31 -0
- package/README.md +104 -0
- package/consistency.json +101 -0
- package/dist/client-ui.d.ts +19 -0
- package/dist/client-ui.js +271 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +42 -0
- package/dist/consistency.js +12 -0
- package/dist/descriptor.js +141 -0
- package/dist/dsh.d.ts +25 -0
- package/dist/dsh.js +194 -0
- package/dist/errors.js +25 -0
- package/dist/gateway.js +224 -0
- package/dist/guardian.js +113 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +15 -0
- package/dist/launcher.js +17 -0
- package/dist/library/catalog.js +108 -0
- package/dist/library/domain.js +8 -0
- package/dist/library/install.js +87 -0
- package/dist/library/installed.js +67 -0
- package/dist/library/locate.js +79 -0
- package/dist/library/routes.js +17 -0
- package/dist/library/service.js +76 -0
- package/dist/manager.js +605 -0
- package/dist/provision/LICENSE +6 -0
- package/dist/provision/THIRD_PARTY_NOTICES.md +13 -0
- package/dist/provision/archive.d.ts +28 -0
- package/dist/provision/archive.js +66 -0
- package/dist/provision/check.d.ts +10 -0
- package/dist/provision/check.js +51 -0
- package/dist/provision/checkpoints.d.ts +12 -0
- package/dist/provision/checkpoints.js +23 -0
- package/dist/provision/cli.d.ts +1 -0
- package/dist/provision/cli.js +169 -0
- package/dist/provision/download.d.ts +21 -0
- package/dist/provision/download.js +242 -0
- package/dist/provision/errors.d.ts +11 -0
- package/dist/provision/errors.js +13 -0
- package/dist/provision/extract.d.ts +17 -0
- package/dist/provision/extract.js +124 -0
- package/dist/provision/fsutil.d.ts +8 -0
- package/dist/provision/fsutil.js +71 -0
- package/dist/provision/index.d.ts +7 -0
- package/dist/provision/index.js +6 -0
- package/dist/provision/ledger.d.ts +14 -0
- package/dist/provision/ledger.js +50 -0
- package/dist/provision/manifest.d.ts +11 -0
- package/dist/provision/manifest.js +171 -0
- package/dist/provision/paths.d.ts +27 -0
- package/dist/provision/paths.js +95 -0
- package/dist/provision/platform.d.ts +6 -0
- package/dist/provision/platform.js +7 -0
- package/dist/provision/provision.d.ts +17 -0
- package/dist/provision/provision.js +454 -0
- package/dist/provision/tar.d.ts +7 -0
- package/dist/provision/tar.js +169 -0
- package/dist/provision/types.d.ts +174 -0
- package/dist/provision/types.js +7 -0
- package/dist/provision/zip.d.ts +4 -0
- package/dist/provision/zip.js +194 -0
- package/dist/router/broker.js +161 -0
- package/dist/router/domain.js +18 -0
- package/dist/router/receipts-domain.js +8 -0
- package/dist/router/receipts.js +156 -0
- package/dist/router/routes.js +48 -0
- package/dist/router/sources.js +87 -0
- package/dist/routes.js +74 -0
- package/dist/runtime.js +222 -0
- package/dist/store.js +250 -0
- package/dist/usage-evidence.js +117 -0
- package/docs/APP_PACKAGE.md +38 -0
- package/docs/CONTRACT.md +190 -0
- package/docs/DSH_INTEGRATION.md +43 -0
- package/docs/LIBRARY.md +45 -0
- package/docs/LICENSES.json +185 -0
- package/docs/PROVENANCE.json +63 -0
- package/docs/SECURITY.md +52 -0
- package/package.json +112 -0
- package/profile/cordis.patch.yml +4 -0
- package/schemas/catalog-provider-page.schema.json +376 -0
- package/schemas/catalog-source.schema.json +151 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
License scope for @hanamesh/dsh-app-host
|
|
2
|
+
|
|
3
|
+
The MIT terms below cover this package's AppHost code and documentation,
|
|
4
|
+
excluding dist/provision/**. That directory contains the inlined
|
|
5
|
+
@hanamesh/lib-provision@0.1.0-rc.1 component. All dist/provision/** files are
|
|
6
|
+
outside and excluded from this MIT license; see dist/provision/LICENSE for that
|
|
7
|
+
component's separate notice. Installed dependencies and host peers keep their
|
|
8
|
+
own licenses; their names, versions, sources, and purposes are recorded in
|
|
9
|
+
docs/LICENSES.json.
|
|
10
|
+
|
|
11
|
+
MIT License
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2026 HanaMesh
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
|
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
|
23
|
+
copies or substantial portions of the Software.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# HanaMesh app-host · 0.2.0-rc.2 本地候选
|
|
2
|
+
|
|
3
|
+
> NPM-APPHOST-01(2026-10-05):本地候选 `0.2.0-rc.2` 对齐官方 `@deepseek-ai/dsh@0.2.0-rc.2`、Cordis `4.0.4`、Schemastery `3.18.4` 与 storage-domain `0.2.0-rc.2`。`@hanamesh/lib-provision@0.1.0-rc.1` 仅以已核 SHA-256 的 vendor tgz 在构建期内联,公共 registry E404,因此不声明为运行时 peer。owner D-2026-10-05-02 已确认该自有代码可分发;内联代码仍为 `UNLICENSED`,不推断新增 MIT 授权。包级许可分区见 [`LICENSE`](LICENSE) 与 [`docs/LICENSES.json`](docs/LICENSES.json)。本地官方 DSH 装配不等于公开 npm 安装门或产品验收;当前卡证据见 BlueMap `NPM-APPHOST-01/REPORT.md`。
|
|
4
|
+
|
|
5
|
+
> rc.41(2026-10-01,P01-U05-RI 修复):真实原生市场重装 Vibe 时,受管 Python runtime 卸载后留有32个运行时生成的 `__pycache__/*.pyc`,后续原子提升因目标目录无所有权清单而拒绝(`E_PROMOTE`)。AppHost 在 lib-provision 精确移除受管文件后仅清理由该 runtime 生成的 Python 字节码及空目录;未知残留继续保留并报告 `RUNTIME_RESIDUE`,不以 force 覆盖。两项回归、模块 check/types 通过;仍需固定消费者组合和独立原生产品复验。
|
|
6
|
+
|
|
7
|
+
> rc.40(2026-10-01,P01 关闭→打开/闲置恢复候选):市场待启动视图每 250ms 只读查询实例状态,实例 ready 后仅调用一次 `/apps/resume` 获取应用 URL;长启动按原 lease TTL 续租,取消或超时释放精确 lease。已打开视图回到前台或 `pageshow` 时立即续租,遇 `LEASE_EXPIRED` / `INSTANCE_NOT_READY` 清除失效应用帧,保留市场「打开」供显式重试。未改租约 TTL、关闭即停机或应用启动路径。需原生 rc.19 + P01 组合独立验证。
|
|
8
|
+
|
|
9
|
+
> rc.38(2026-09-30):实机验证发现 DSH Settings 固定层级 1000 遮住工作区默认 overlay 20;仅当应用视图投射时把既有 shell.overlay 提到 1001,关闭后自动恢复默认层级。
|
|
10
|
+
> rc.39(2026-09-30):原生「扩展管理 › 市场」打开应用时,原有 iframe、关闭按钮和视图租约保持在 app-host Market owner 内,但应用帧固定铺满窗口并覆盖 DSH Settings(设置层 z-index 1000),关闭后返回市场。原版 Vibe 的 Sessions 入口需要至少 768 CSS px;rc.37/38 的 shell.overlay 投射在原生客户端被设置层遮住,均未作为交付版本。完整 P01 仍须实机独立复验。
|
|
11
|
+
|
|
12
|
+
> rc.36(2026-09-30):冷恢复时将中断实例的旧活跃视图租约与实例状态在同一持久镜像中失效;再次打开的新视图关闭后,不再等旧租约 TTL 才停机。原视图仍可按原身份显式恢复并轮换凭据。完整产品流程另行独立验证。
|
|
13
|
+
|
|
14
|
+
> rc.35(2026-09-30,本机有限修复):内置市场的「打开」和「关闭视图」在请求进行中立即显示「打开中…」「关闭中…」并阻止重复点击。owned 应用进程在 TERM 后已退出时,guardian 依据 launcher 的 `app-exit` 提前结束等待,仍清理并确认整个精确自有进程组,之后才释放锁和发布 stopped;顽固应用仍保留声明的 stopGraceMs。P01-U02/P08 真正客户端组合验收另行记录。
|
|
15
|
+
|
|
16
|
+
> rc.34(2026-09-29):内置市场的活跃应用视图按既有回执 expiresAt 周期消费 `/apps/heartbeat`;关闭视图、隐藏、卸载和 pagehide 取消续租并显式释放原 lease。服务端默认 TTL/sweep、runtime/provider/应用 UI 均未改变;完整产品独立真实 UI 验证另跑,详 `docs/acceptance/recovery-20260929-client-heartbeat/REPORT.md`。
|
|
17
|
+
|
|
18
|
+
> rc.33(2026-09-29,本机有限修复):owned 启动将死主锁/孤儿回收与新 launcher/app 握手分阶段限时;回收预算为既有 5s bootstrap + 声明 stopGraceMs + 2s kill 确认,所有权确认之后的新启动仍限 5s。未改安全身份判定、公开配置或 UI;完整 P01/P08 产品验证另由真实 profile 执行。
|
|
19
|
+
|
|
20
|
+
> rc.32(2026-09-22,设计定案 §4,T5):**客户端 `market` 席位 + dshmarket 互斥。** 客户端模块在 cordis 客户端上下文 `provide('market', {render(options), setSettingsVisible(visible)})`——席位名与形状取自钉死的壳面板 `dsh-tauri-panel-extension@1.0.0`(随包在 `hanamesh-desktop-tauri/src-tauri/resources/node_modules/`):它 `ctx.reflect.get('market')`,只在 `typeof value.render==='function'` 时认账,用 `render({preferredSubsectionId:'installed'})` 画「扩展管理 · 市场」标签页,并在自己接管期间 `setSettingsVisible(false)`、dispose 时 `true`。`render` 返回的就是原来的「HanaMesh 市场」页(`embedded`:常显、无「关闭」、`preferredSubsectionId==='installed'` 时「已安装」排在目录前);`setSettingsVisible(false)` 收起我们自己的侧栏「市场」入口与覆盖页。**互斥**:占座前先读 `reflect.get('market',false)`,再问一次 `GET /hanamesh/library/installedPlugins` 看 `dshmarket` 在不在,两者任一命中就不 provide(cordis 对同名第二次 `provide` 直接抛错,抢座会把对方打挂),改在宿主日志与市场页顶部给一条「两者只能其一」的提示,其它功能照常;库路由答不上来时 fail open。**重启**:壳 iframe 内的 `hanamesh://restart` 改走壳已有的入站 postMessage 桥(`window.parent.postMessage({type:'hanamesh://restart'},'*')`,与 `hanamesh://bound-refresh` 同一命名空间)——WKWebView 不会把 iframe 内发起的自定义 scheme 导航交给系统;`hanamesh://restart` 这个 URL 仍是系统级入口,由壳 rc.13 的 `deep_link.rs` 处理。市场本身的功能未改。AH-MS01–MS05 测试、M17 变异、X01 新增边界 `market-seat-exclusive`。
|
|
21
|
+
|
|
22
|
+
> rc.31(2026-09-22):修 rc.30 的回归——库入口 `dist/index.js` 经 `router/receipts.js` 静态引入了 DSH 专属 peer `@deepseek-ai/dsh-storage-domain`,使只装本包的应用包(如 Vibe)`import` 即 `ERR_MODULE_NOT_FOUND`。storage-domain 规格移到 `src/router/receipts-domain.js`(只由插件入口 `src/dsh.js` 打开),并加守卫测试:从 `dist/index.js` 可达的模块图里不得出现 `@deepseek-ai/*`。
|
|
23
|
+
|
|
24
|
+
> rc.30(2026-09-22):= rc.28(HanaMesh 市场)+ Router 使用回执:每小时 `{appId, providerId, model, hour, count}` 本地账(storage-domain `hanamesh_router_receipts`、`GET /hanamesh/router/receipts`),在 UTC 整点结束或该应用最后一个活实例停止时,随 `use` 事件经 usage 席位上报(`targetRef=appId` + 该小时请求最多的路由行作 `receipt`);需 usage ≥ 0.2.0-rc.7。
|
|
25
|
+
|
|
26
|
+
> 分支 `t6-usage-receipts`(2026-09-22,设计定案 §3「协议层」,T6,**未发版**,由派发 session 合入下一 rc):**Router 使用回执。** app-host 自己的 storage domain `hanamesh_router_receipts` 按小时记 `{appId, providerId, model, hour, count}`(Router 每次注入记路由与 `injections`,网关每次转发 `count+1`;不记内容、不记值),`GET /hanamesh/router/receipts[?appId=]` 可查本地账。usage 席位上报的 `use` 事件改为在 **UTC 小时结束或应用最后一个活实例停止时**发出(`occurredAt` 仍是该小时首个转发请求;同 key 一小时一条不变),并附 `targetRef=appId` 与该小时的 `receipt {providerId, model, count}`;无席位只记本地账不报错;宿主死机前未报的小时在下次启动由账本补报(席位按 key 去重)。需要 usage `0.2.0-rc.7`(席位接受三个可选键;更早版本会 `rejected INVALID_RECORD_INPUT`,只 debug 日志)。
|
|
27
|
+
|
|
28
|
+
> rc.28(2026-09-22,设计定案 §4「HanaMesh 市场」,T4):**「应用库」升级为 HanaMesh 市场——插件与应用同一入口、同一安装路径。** 侧栏入口与页面标题改为「市场」/「HanaMesh 市场」,设置段「应用库来源」改名「市场目录源」。目录默认列出**全部**类别(客户端显式 `category=`),筛选 = 全部 / 应用(`hanamesh-app`)/ 插件(客户端视图:非应用的 npm 条目)/ 目录页里出现过的每个类别;搜索、「更多」保留。每条目带 `kind`(`application` / `plugin` / `listing`)、`installed`(已装行)与 `upgradeAvailable`(目录 `latestVersion` 严格新于已装版本,内建 semver 比较,无依赖)。卡片状态:未装「安装」/ 已装(应用「打开」「卸载」,插件「卸载」)/ 可升级「升级到 x.y.z」(同一 `dsh plugin add --save-exact` 路径)/ 需重启。**已装列表含插件**:扫 profile `package.json` 的 `dependencies`(减去应用包),`bundle`(声明 `dsh.bundle`)与 `active`(在 `dsh.profile.bundles`);「需重启」由本插件启动时的依赖快照推出:启动后新增 → `installed-not-loaded`,启动后移除 → `uninstalled-not-unloaded`。任一操作完成或列表含上述状态时页面顶部出现「需重启 DSH」:在桌面壳内(DSH 页在壳 iframe 里)给出 `hanamesh://restart` 深链按钮,官方 DSH 直开浏览器时只提示「请手动重启 DSH」。新路由:`GET /hanamesh/library/installedPlugins` → `{plugins,apps,restartRequired}`;`POST /hanamesh/library/plugins/install {packageName}`(须在启用目录里能搜到该包;应用包走应用路径)与 `POST /hanamesh/library/plugins/uninstall {packageName}`(应用包转应用卸载:runtime + 运行中检查)→ 202 + 事件流;`POST /hanamesh/library/install` 现接受 `{itemId, packageName?}`(`packageName` 用于目录精确查找,插件条目也可装)。旧路由全部保留。**修 `LIB-PROVISION-INPUT`**:`provision` / `uninstall` / `plugins/*` 缺字段或字段非法 → 400 `INVALID_INPUT`,操作不启动(此前 TypeError → 事件 `LIBRARY_OPERATION_FAILED`)。受保护包 `hanamesh-core` / `hanamesh-usage` / `@hanamesh/dsh-app-host`(含旧写法)永不经市场装卸(`PACKAGE_DENIED`;rc.27 前阻止名单写的是从未存在过的 `@hanamesh/dsh-core`)。不做 T5(`provide('market')`、dshmarket 互斥、recommender 接线)。AH-M01–M08 测试,M13/M14 变异;真实门见 `docs/acceptance/t4-market-20260922/`。
|
|
29
|
+
|
|
30
|
+
> rc.27(2026-09-21,用户定,STATUS `P2-USE-EVENTS`):**应用的使用证据由 app-host 上报。** 此前 app-host 从不调用 usage 插件的 record 座位,应用既没有 `open` 也没有 `use` 事件。现在:`ctx.inject(['hanameshUsage'])` 可选取得座位(鸭子类型 `record()`,三插件仍互不 import;缺席/不兼容/抛错/`rejected` 只 debug 日志,永不阻止启动或转发);`open` = 实例到达 `ready` 一次(`open:<appId>:<instanceId>`);`use` = 该实例网关真正转发了已授权请求且应用回应非 401/403,每应用每 UTC 小时一次(`use:<appId>:<YYYYMMDDHH>`,时间取桶内第一个请求);引导票据、网关拒绝、宿主就绪探针不计。`hanaRef` = 应用 npm 包名:描述符新增可选 `packageName`(向后兼容),缺省由启动时的已安装扫描 `host.bindPackageName()` 绑定,两者都没有则该应用不产生证据。`sourcePlugin = '@hanamesh/dsh-app-host'`。新面:`host.packageName/bindPackageName/onActivity`、`FixedGateway({onForward})`、`createUsageEvidence()`、`list().apps[].packageName`。契约见 `docs/CONTRACT.md`「应用使用证据」;AH-U01–U07 测试(U07 为真实 Cordis 根,座位先于/后于本插件提供两种顺序),M11/M12 变异。
|
|
31
|
+
|
|
32
|
+
> rc.26(2026-09-21,O3 stage-4 官方 DSH 发行路径 PRD §A.8):**纯 DSH CLI profile(只 `dsh plugin add hanamesh-core` + 应用包,无桌面壳 overlay)下的应用库可用。** ① `library.sources` **未配置**时默认 `[{manifestUrl:'https://market.hanamesh.com/catalog-source.json',enabled:true}]`(首启仍只在存储为空时播种;显式 `sources: []` 表示不要来源,保持为空——schema 用 `union([array, undefined])` 区分「未设」与「空数组」,旧版 schemastery 把未设强制成 `[]` 的行为不再影响);② `library.profileDir` 缺省时从本包**自身真实安装位置**推断:`import.meta.url` 向上找到名为 `@hanamesh/dsh-app-host` 的包根并 `realpath`,再向上找第一个 `node_modules/@hanamesh/dsh-app-host` 解析到该真实根、且 `package.json` 的 `dependencies` / `dsh.profile.bundles` 提到 hanamesh 的祖先目录——pnpm `.pnpm` 存储路径永不被返回,只返回链接它的 profile;`profileName` = `basename(profileDir)`;`dshBin` = `process.argv[1]` **仅当**它以 `/@deepseek-ai/dsh/lib/bin.js` 结尾(启动本进程的 DSH CLI),否则再尝试从 profile 解析(失败不再抛错,只是安装不可用);`nodeBinary` = `process.execPath` **仅当** `process.versions.electron` 未定义(K5:Electron 宿主仍须显式配置,否则 `NODE_RUNTIME_REQUIRED` fail-closed)。显式配置永远优先(桌面壳 overlay 全部显式,行为不变);推断结果以一行结构化日志 `hanamesh-app-host library: inferred {...}` 打出。效果:CLI 安装且已注册的 Vibe 在库里显示为 `registered` → 卡片「打开」而非「安装」;安装/卸载/补齐 runtime 在纯 DSH 上可用(走 `node bin.js plugin --profile <name> add/remove`,与桌面桥完全相同)。AH-L07–L10 测试(含真实 Cordis 根经 Config schema 的 L07 plugin 用例),M10 变异。
|
|
33
|
+
> rc.25(2026-09-21,STATUS `APP-RUNTIME-LOCK`):桌面壳整棵进程树被强杀(DSH + guardian 一起死)后,应用数据根下的 `.runtime.lock` 不再永久卡住下一次「打开」。锁记录现在带 owner pid + 进程启动时间令牌、guardian 创建的子进程(launcher / app 的 pid + 启动令牌 + 可执行文件)和端口;再次 acquire 时:owner 活着 → 仍 `DATA_ROOT_BUSY`,`details.pid` 给出活着的 pid;owner 死了(pid 不存在,或同 pid 但启动时间不同 = pid 复用)→ 逐个核对记录的子进程:已消失或被别的进程复用 pid 的忽略,**只有 pid + 启动令牌 + 可执行文件三者都对得上的孤儿**才被 SIGTERM→SIGKILL 后接管锁;活着但证明不了是我们的 → 仍 `DATA_ROOT_BUSY`,`details.pid`/`details.ownerPid` 指出该进程。只用 Node 内建(macOS/Linux 用 `ps -o lstart=` / `/proc`,Windows 退化为只判 pid 存活、永不接管活进程)。AH-L01–L06 测试(L05/L06 真 SIGKILL guardian + 宿主后再开),M09 变异。
|
|
34
|
+
> rc.24(2026-09-21):模型随路由——路由表加「模型」列(所路由供应商的模型列表 / 文本框;「应用默认」= 清单默认值),`POST /hanamesh/router/model`;自动路由行选模型即成为同供应商的显式 grant。
|
|
35
|
+
> rc.23(2026-09-21):Router 不再替应用选模型——自动路由时 `{{model|默认}}` 落到应用声明的默认值,不再取供应商模型列表第一项(用户在 Vibe 里被塞了 `gpt-5.3-codex-spark` 的根因);无 `providers` 的派生槽位不自动路由、plan 标 `derived`、路由表不显示。
|
|
36
|
+
> rc.22(2026-09-21):「供应商」页压成两张表(来源目录 + 应用×槽位路由表),可扩展到多个应用;去掉网关开关(归 oauth 插件自己的设置页)和「应用自管」切换(自动路由 + 应用内设置优先已消解冲突)。
|
|
37
|
+
>
|
|
38
|
+
> rc.21(2026-09-21,用户定契约):Router **自动路由**——托管模式下,profile 里已配置、且应用声明接受的供应商在启动时直接注入,不再要求用户在宿主「供应商」页逐槽位选择;显式 grant 优先,「停用」= 该槽位退出自动路由;应用内部自己的设置优先于宿主注入。`coding-oauth-gateway`(dsh-coding-subscription-oauth 的本地 OpenAI 兼容 API)作为 `openai` 槽位的兜底候选。宿主「供应商」页改为:来源目录 + 网关开关(写明它只服务应用,DSH 对话不需要)+ 每个应用的只读路由状态(停用/恢复)。AH-R02/R02b 测试。
|
|
39
|
+
>
|
|
40
|
+
> rc.20(2026-09-21):在 rc.19 之上再修两处(同一个「桌面壳里打开应用空白」,在真实 WKWebView 抓到的请求头证实):① 引导 303 不再带 `referrer-policy: no-referrer`——它作用于跳转那一跳,`/` 导航到达时没有 Referer,rc.19 的无 cookie 授权路径永远匹配不上;② 新增配置 `frameAncestors: string[]`(壳自己的 webview origin,如 `tauri://localhost`)——`frame-ancestors` 对**所有**祖先生效,壳 → DSH → 应用三层时只写 DSH origin 会被 webview 拒绝渲染。H16 测试。rc.19 未发布到任何地方以外的 registry。
|
|
41
|
+
>
|
|
42
|
+
> rc.19(2026-09-21):网关不再只认 cookie——嵌入式 webview(HanaMesh 桌面 Tauri/WKWebView、开了跟踪防护的 WebView2)里应用 iframe 相对壳的顶层 origin 是第三方,`SameSite=Strict` 的引导 cookie 被丢弃,303 之后每个请求都 403(响应带 `frame-ancestors 'none'`)→ 用户看到空白 iframe(2026-09-21 桌面 rc.4 + Vibe 实测)。rc.19 在 cookie 缺席时改用 Fetch Metadata:`sec-fetch-site: same-origin`(应用自身子资源/XHR)、或 `same-site` + `iframe` + 父 origin referer(引导后的框架导航)、或应用 origin 的 WebSocket 握手,且必须已有一张为仍在有效期 lease 消费过的引导票;cross-site / `none`(地址栏)一律拒绝。H15 三条测试;浏览器直开仍走 cookie。
|
|
43
|
+
>
|
|
44
|
+
> rc.18(2026-09-20):应用库卡片跟踪安装/补齐运行时/卸载操作——按钮进入「…中…」,订阅 `/hanamesh/library/events` 直到 `-done`/`-failed`,失败时在卡片上显示错误码与可读原因(如 `REGISTRY_LOOKUP_FAILED`:应用包不在当前 registry),并提供「重试安装」。用户 2026-09-20 实测:点安装无任何反应。宿主逻辑不变。
|
|
45
|
+
|
|
46
|
+
**交付状态:rc.3 用户 ACCEPTED 2026-09-13;rc.4–rc.12 为增量 🧪;rc.13 只补上架前提(MIT 许可证、`repository` 字段、DSH peer 精确钉 `0.1.5-alpha.1`);rc.14 删除客户端里对 `hanameshCore` 的死读取(浏览器侧 cordis 上下文不含宿主服务,该读取恒为 null,且把「未安装 HanaMesh Core」误显给已装用户)——本包对 Core 现在零引用,Core 状态只在 Core 自己的设置段;仍待用户验收。**
|
|
47
|
+
|
|
48
|
+
> 收录不代表审核或推荐。新客户端 seed 对 Core、Usage、AppHost 各安装并启用一次;同一 profile 不得再手工插入第二条 `hanamesh-app-host` loader entry。
|
|
49
|
+
|
|
50
|
+
> rc.4 增加 `credentialEnv` / `credentialResolver`;rc.5 增加文件投射根与格式;rc.6 增加文件生命周期策略;rc.7 允许停止实例在下次打开时采用新定义;rc.8 增加 `credentialEnv[].sets` 声明。逐项证据见 `docs/acceptance/`。`ACCEPTED` 仍只有用户能给。
|
|
51
|
+
|
|
52
|
+
本仓完成可独立运行的应用实例管理、持久视图租约、受控网关和工作台 SDK。原型源码所在私有仓当时访问返回 404,因此这是新的实现候选,不是原型提取;原型许可证和 commit 尚未核实(原型现在可读,见 `<umbrella>/research/dsh-greenfield-2026-09-09/workspace/packages/hanamesh-app-host/`,契约差异未逐项对照)。
|
|
53
|
+
|
|
54
|
+
## 本次已实现
|
|
55
|
+
|
|
56
|
+
- **FIX-01:** 稳定 `viewId`、持久租约、generation fencing;同一 view 重开不增加匿名引用;原实例精确恢复;错误关闭拒绝;心跳超时自动回收;回执丢失后可由已认证 owner 显式恢复凭据,不重新绑定视图。
|
|
57
|
+
- **FIX-02:** 按 owner / app / deployment / data 身份在 spawn 前持久预留;并发 Open 合并为一个真实应用进程;多实例传入独立 data/HOME/XDG/TMP 目录,测试检查了应用真实写入的文件。
|
|
58
|
+
- **生命周期:** owned 进程由独立 guardian 管理;宿主被 SIGKILL 后清理自有进程组;attach 只释放附着;有占用时 Stop 拒绝并列出视图;确认清理后才发布 stopped 与逐 view 通知。
|
|
59
|
+
- **受控网关:** 固定数字回环上游、每代租约能力票据;只调整 XFO 和 CSP frame-ancestors,保留应用正文;HTTP 上传、SSE、WebSocket 可用;不向应用注入 SDK、不传宿主凭据、不改变应用 auth/CSRF/CORS。
|
|
60
|
+
- **交付契约:** ESM 包、TypeScript 声明、工作台侧 `./client` SDK、完整快照存储接口、DSH 插件入口、测试与崩溃一致性声明。
|
|
61
|
+
- **K5:** Electron 宿主必须显式给绝对且可执行的 `nodeBinary`;guardian/launcher 使用同一 Node,只继承 `DSH_HOME/HOME/LANG/TMPDIR/PATH` 白名单。未配置时 fail-closed 为 `NODE_RUNTIME_REQUIRED`。
|
|
62
|
+
- **Router:** 合并授权、撤销、文件投射 ledger 与 `sets`,来源为 DSH credentials/LLM 目录和 coding-oauth gateway;不搬 key 探测、录入或 OAuth 端口。
|
|
63
|
+
- **市场(原应用库):** 自带侧栏入口与覆盖页;目录源同一时刻只启用一项;插件与应用同一入口、同一 `dsh plugin add/remove` 路径;已装列表含插件;区分已注册、待重启、可升级与缺 runtime。
|
|
64
|
+
- **锁定 runtime 供给:** build 脚本直接读取 `vendor/hanamesh-lib-provision-0.1.0-rc.1.tgz`,校验 SHA-256 后内联到 `dist/provision/`。包清单没有该私有包的 dependency、peer 或 devDependency;来源与许可边界见 `docs/PROVENANCE.json`。
|
|
65
|
+
|
|
66
|
+
## 运行
|
|
67
|
+
|
|
68
|
+
NPM-APPHOST-01 的本地开发环境是 **macOS arm64 / Node 24.13.1 / npm 11.8.0 / 官方 DSH 0.2.0-rc.2**。干净安装使用锁文件,运行依赖为 `zod 4.5.4`;私有 `@hanamesh/lib-provision` 不在运行时依赖或 peer 中,其代码已获自有代码分发确认并按独立许可分区内联。
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm ci
|
|
72
|
+
npm run build
|
|
73
|
+
npm test
|
|
74
|
+
npm run test:mutation
|
|
75
|
+
npm run test:types
|
|
76
|
+
npm run demo -- --smoke
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`npm run demo` 启动一个受控示例应用,并让两个视图共用同一实例;终端打印应用 URL,Ctrl+C 清理自有进程和临时示例目录。它不是 DSH、工作台或两个上游应用的演示。
|
|
80
|
+
|
|
81
|
+
`typescript 5.8.3` 已固定在开发依赖中。rc.3 tarball 可用 `npm install --offline --ignore-scripts --legacy-peer-deps /绝对路径/hanamesh-dsh-app-host-0.1.0-rc.3.tgz` 安装到独立测试工程;真实 DSH 验收须用独立 `DSH_HOME`,不要装到个人 profile。
|
|
82
|
+
|
|
83
|
+
## 验收结果在哪里
|
|
84
|
+
|
|
85
|
+
[`docs/acceptance/REPORT.md`](docs/acceptance/REPORT.md) 分开记录真实进程、真实 SIGKILL、HTTP 传输、fixture、浏览器受阻及 DSH 未完成项;原始 TAP、4 组变异日志、干净目录包验证日志随完整源码 ZIP 一并交付;npm tarball 只含运行文件与契约,不含验收日志。浏览器测试不会修改管理员策略,也不会因为 skip 而把对应产品验收标为 PASS。
|
|
86
|
+
|
|
87
|
+
## 接入入口
|
|
88
|
+
|
|
89
|
+
| 消费者 | 入口 | 说明 |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| 受信宿主代码 | `@hanamesh/dsh-app-host` | `AppHost`、存储、路由、网关 |
|
|
92
|
+
| 工作台顶层页面 | `@hanamesh/dsh-app-host/client` | 明确的 Open 回执、恢复、心跳、Stop;禁止注入应用 iframe |
|
|
93
|
+
| DSH profile 适配 | bundle 自动落座包根;显式适配仍可用 `@hanamesh/dsh-app-host/dsh` | 包根懒加载 Cordis `apply`,使同一个 loader entry 同时被 client-modules 发现;见 `docs/DSH_INTEGRATION.md` |
|
|
94
|
+
| DSH 浏览器 UI | `@hanamesh/dsh-app-host/client-ui` | 设置里的「供应商」「市场目录源」,以及侧栏「市场」(HanaMesh 市场)与 `shell.overlay` 页面;Node 条件下 `./client` 仍解析到工作台 SDK |
|
|
95
|
+
|
|
96
|
+
契约详见 [`docs/CONTRACT.md`](docs/CONTRACT.md)、[`docs/ROUTER_CONTRACT.md`](docs/ROUTER_CONTRACT.md) 与 [`docs/LIBRARY.md`](docs/LIBRARY.md),未完成步骤见 [`docs/DSH_INTEGRATION.md`](docs/DSH_INTEGRATION.md),安全限制见 [`docs/SECURITY.md`](docs/SECURITY.md)。本仓不包含应用适配包、钱包权限或 provider runtime driver。
|
|
97
|
+
|
|
98
|
+
## GitHub 与来源
|
|
99
|
+
|
|
100
|
+
rc.3 修复只在本地隔离分支 `codex/app-host-acc-fix`;没有创建远程仓库、提交 PR 或推送。所需原型现在有本地副本,但尚未做逐项契约对照;不能把独立实现冒称为原型提取。
|
|
101
|
+
|
|
102
|
+
rc.1 原交付包的 ZIP 含本地 Git bundle 和提交信息。`scripts/publish-github.sh --create-private` 是**尚未执行**的历史发布辅助;执行前须重新核对目标仓与授权。
|
|
103
|
+
|
|
104
|
+
rc.1 原始输入当时标注 `UNLICENSED`,不是 MIT/Apache 授权声明;没有擅自给用户代码选择开源许可证。[`docs/PROVENANCE.json`](docs/PROVENANCE.json) 保留 rc.1 原始输入与当时环境记录;rc.3 的包摘要与复验证据见源码仓 `docs/acceptance/wave01-rc3-artifact.json`。
|
package/consistency.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"module": "app-host",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"exempt": false,
|
|
5
|
+
"groups": [
|
|
6
|
+
{
|
|
7
|
+
"id": "first-lease",
|
|
8
|
+
"name": "实例、首个租约、对应通知一起发布",
|
|
9
|
+
"facts": [
|
|
10
|
+
"instance-record:present",
|
|
11
|
+
"first-lease:owned",
|
|
12
|
+
"view.opened:journaled"
|
|
13
|
+
],
|
|
14
|
+
"medium": "single-snapshot:hanamesh_app_host",
|
|
15
|
+
"why": "一次完整镜像包含三项事实;本地后端用 fsync + rename;真实 DSH single domain 绑定仍待验证。",
|
|
16
|
+
"test": "tests/crash.test.mjs:X02 first-lease"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "stopped-views",
|
|
20
|
+
"name": "已停止状态、撤销全部视图、停止通知一起发布",
|
|
21
|
+
"facts": [
|
|
22
|
+
"instance-record:stopped",
|
|
23
|
+
"all-active-leases:stopped",
|
|
24
|
+
"view.stopped:journaled"
|
|
25
|
+
],
|
|
26
|
+
"medium": "single-snapshot:hanamesh_app_host",
|
|
27
|
+
"why": "物理清理完成后再原子发布状态和通知;不能用 UI 事件代替已停止事实。",
|
|
28
|
+
"test": "tests/crash.test.mjs:X02 stopped-views"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "library-install",
|
|
32
|
+
"name": "runtime 账本记录与受管安装记录一起发布",
|
|
33
|
+
"facts": [
|
|
34
|
+
"ledger:item",
|
|
35
|
+
"installed-record"
|
|
36
|
+
],
|
|
37
|
+
"medium": "provision-ledger",
|
|
38
|
+
"why": "lib-provision 是 runtime 目录与 ownership manifest/ledger 的单写者;应用包已经安装但 runtime 未完成时显式显示 runtime-missing,不冒充可打开。rc.28 插件走同一 dsh plugin add/remove 路径但不涉及账本:只有 profile package.json 一处事实。",
|
|
39
|
+
"test": "tests/library-install.test.mjs:AH-L04/L05"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "receipt-hour",
|
|
43
|
+
"name": "回执行的计数与已上报标记一起发布",
|
|
44
|
+
"facts": [
|
|
45
|
+
"receipt-row:count",
|
|
46
|
+
"receipt-row:reported"
|
|
47
|
+
],
|
|
48
|
+
"medium": "single-snapshot:hanamesh_router_receipts",
|
|
49
|
+
"why": "T6:同一 (app, 路由, 小时) 行的 count/injections/firstAt 与 reported 标记在一次去抖后的 global.set 里整体发布;不存在第二张回执表。强杀最多丢最后一个去抖窗口内的计数,永不出现 reported 却没有行。",
|
|
50
|
+
"test": "tests/usage-evidence.test.mjs:AH-U09"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"boundaries": [
|
|
54
|
+
{
|
|
55
|
+
"id": "reserve-launch",
|
|
56
|
+
"name": "持久所有权预留 → 实际进程及应用数据写入",
|
|
57
|
+
"sides": [
|
|
58
|
+
"single-snapshot:hanamesh_app_host",
|
|
59
|
+
"owned-process-and-app-data"
|
|
60
|
+
],
|
|
61
|
+
"safeDirection": "single-snapshot:hanamesh_app_host",
|
|
62
|
+
"why": "先记录 owner 和 lease 再 spawn。允许孤儿预留,不允许进程或写出的数据没有记录认领。宿主强杀后独立 guardian 收尾。",
|
|
63
|
+
"test": "tests/crash.test.mjs:X03 reserve-launch"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "stop-publish",
|
|
67
|
+
"name": "确认自有进程已退出 → 发布 stopped",
|
|
68
|
+
"sides": [
|
|
69
|
+
"confirmed-runtime-termination",
|
|
70
|
+
"snapshot:stopped-ack"
|
|
71
|
+
],
|
|
72
|
+
"safeDirection": "confirmed-runtime-termination",
|
|
73
|
+
"why": "宁可已经停了但状态仍是 stopping;不能记录 stopped 时后端仍在运行。attach 仅撤销附着,不参与发信号。",
|
|
74
|
+
"test": "tests/crash.test.mjs:X03 stop-publish"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "receipt-report",
|
|
78
|
+
"name": "usage 席位收到小时 use 事件 → 账本标记 reported",
|
|
79
|
+
"sides": [
|
|
80
|
+
"usage-seat:use-event",
|
|
81
|
+
"single-snapshot:hanamesh_router_receipts"
|
|
82
|
+
],
|
|
83
|
+
"safeDirection": "usage-seat:use-event",
|
|
84
|
+
"why": "先让席位 recorded/duplicate,再标 reported;允许席位已有而账本未标(下次重报得 duplicate),不允许账本已标而席位从未收到。rejected/withheld/席位缺席都不标。",
|
|
85
|
+
"test": "tests/usage-evidence.test.mjs:AH-U04"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "market-seat-exclusive",
|
|
89
|
+
"name": "判定 dshmarket 不在场 → 占用客户端 market 席位",
|
|
90
|
+
"sides": [
|
|
91
|
+
"dshmarket-absence-decided",
|
|
92
|
+
"client-service:market"
|
|
93
|
+
],
|
|
94
|
+
"safeDirection": "dshmarket-absence-decided",
|
|
95
|
+
"why": "rc.32:壳的「扩展管理」面板只认一个 market 席位(cordis 的 provide 对同名第二次注册直接抛错)。先读席位(reflect.get(\"market\",false))并问一次 /hanamesh/library/installedPlugins,确认 dshmarket 既没占座也没装,才 provide。宁可席位空着、只留侧栏「市场」入口加一条提示,也不抢座把对方的 provide 打挂;库路由答不上来时 fail open(席位本身不承载持久事实)。",
|
|
96
|
+
"test": "tests/market-seat.test.mjs:AH-MS01/AH-MS02/AH-MS03"
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"scope": "REAL_PROCESS + LOCAL_ATOMIC_FILE;不是 DSH 加载、真实浏览器、整机断电或 macOS 验收。",
|
|
100
|
+
"sessionLog": "零写入;不存在自创 Session.append 事件或跨 session-log 提交。"
|
|
101
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const name:'hanamesh-app-host-client';
|
|
2
|
+
export const inject:readonly ['slots','locale'];
|
|
3
|
+
export function apply(ctx:unknown):void;
|
|
4
|
+
export function ProvidersSection():unknown;
|
|
5
|
+
/** Settings section「市场目录源」(rc.28 name; the export keeps its rc.11 identifier). */
|
|
6
|
+
export function LibrarySourcesSection():unknown;
|
|
7
|
+
/** Sidebar entry「市场」; hidden while the `market` seat owner renders the market itself. */
|
|
8
|
+
export function LibraryAction():unknown;
|
|
9
|
+
/** The HanaMesh market page (applications + plugins; install / upgrade / uninstall / restart notice).
|
|
10
|
+
* `embedded` renders it inline for the Extension Management tab instead of as the shell overlay. */
|
|
11
|
+
export function LibraryOverlay(props?:{embedded?:boolean,preferredSubsectionId?:string}):unknown;
|
|
12
|
+
/** rc.32: the「只能其一」notice shown at the top of the market page when dshmarket holds the seat. */
|
|
13
|
+
export function MarketConflictNotice():unknown;
|
|
14
|
+
/** rc.32: the client-side `market` service object read by the shell's Extension Management panel. */
|
|
15
|
+
export function createMarketSeat():{render(options?:{preferredSubsectionId?:string}):unknown,setSettingsVisible(visible:boolean):void};
|
|
16
|
+
export const MARKET_SEAT:'market';
|
|
17
|
+
export const MARKET_CONFLICT_TEXT:string;
|
|
18
|
+
/** Test/inspection view of the module-level market state; not part of the DSH contract. */
|
|
19
|
+
export function marketState():{conflict:string,entryVisible:boolean};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/* Browser bundle for DSH client-modules. The Node condition of exports["./client"] remains the SDK. */
|
|
2
|
+
window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(require){'use strict';
|
|
3
|
+
const React=require('react');
|
|
4
|
+
const h=React.createElement;
|
|
5
|
+
const request=async(path,init={})=>{
|
|
6
|
+
const response=await globalThis.fetch.bind(globalThis)(path,{...init,headers:{'x-hanamesh-client':'workspace-v1',...(init.body?{'content-type':'application/json'}:{}),...init.headers}});
|
|
7
|
+
const value=await response.json();if(!response.ok)throw new Error(value?.error?.code??`HTTP_${response.status}`);return value;
|
|
8
|
+
};
|
|
9
|
+
const entryId=entry=>entry.projection==='file'?`file:${entry.base??'home'}:${entry.path}`:`env:${entry.env}`;
|
|
10
|
+
function ProvidersSection(){
|
|
11
|
+
const[providers,setProviders]=React.useState([]),[apps,setApps]=React.useState([]),[plans,setPlans]=React.useState({}),[error,setError]=React.useState('');
|
|
12
|
+
const load=React.useCallback(async()=>{try{setError('');const[p,a]=await Promise.all([request('/hanamesh/router/providers'),request('/hanamesh/apps')]);
|
|
13
|
+
setProviders(p.providers??[]);setApps(a.apps??[]);const rows=await Promise.all((a.apps??[]).map(async app=>[app.id,await request('/hanamesh/router/plan?appId='+encodeURIComponent(app.id))]));setPlans(Object.fromEntries(rows));}catch(e){setError(String(e.message??e));}},[]);
|
|
14
|
+
React.useEffect(()=>{void load();},[load]);
|
|
15
|
+
const mutate=async(path,body)=>{try{setError('');await request(path,{method:'POST',body:JSON.stringify(body)});await load();}catch(e){setError(String(e.message??e));}};
|
|
16
|
+
const sourceRows=providers.map(provider=>h('tr',{key:provider.id},h('td',null,provider.displayName),h('td',null,provider.source),h('td',null,provider.state),h('td',null,provider.keyHint??'—'),h('td',null,(provider.models??[]).join('、')||'—')));
|
|
17
|
+
const stateText={auto:'自动',granted:'手动指定',missing:'无可用供应商',revoked:'已停用','app-owned':'应用自管'};
|
|
18
|
+
const subjectText=item=>item.granted?(item.granted.kind==='provider'?item.granted.providerId:item.granted.ref??item.granted.key):'—';
|
|
19
|
+
const acceptable=entry=>providers.find(p=>p.state==='configured'&&(!entry.providers?.length||entry.providers.includes(p.id)||(p.id==='coding-oauth-gateway'&&entry.providers.includes('openai'))));
|
|
20
|
+
// One compact table for every app: app × slot → routed provider. Scales to many apps; no per-app card, no manual picker.
|
|
21
|
+
const routeRows=apps.flatMap(app=>{const plan=plans[app.id];if(!plan)return[];const slots=(plan.items??[]).filter(item=>!item.derived);return slots.map((item,index)=>{const id=entryId(item.entry);
|
|
22
|
+
const routed=item.state==='auto'||item.state==='granted',current=item.model??'',options=[...new Set([...(item.models??[]),...(current?[current]:[])])];
|
|
23
|
+
const modelCell=!routed?'—':options.length?h('select',{value:current,onChange:e=>void mutate('/hanamesh/router/model',{appId:app.id,entryId:id,model:e.target.value})},h('option',{value:''},item.defaultModel?`应用默认(${item.defaultModel})`:'应用默认'),...options.map(m=>h('option',{key:m,value:m},m))):
|
|
24
|
+
h('input',{type:'text',defaultValue:current,placeholder:item.defaultModel?`应用默认(${item.defaultModel})`:'应用默认',onBlur:e=>{if(e.target.value.trim()!==current)void mutate('/hanamesh/router/model',{appId:app.id,entryId:id,model:e.target.value.trim()});}});
|
|
25
|
+
return h('tr',{key:`${app.id}:${id}`},index===0?h('td',{rowSpan:slots.length},app.name):null,h('td',null,item.entry.env??item.entry.path),h('td',null,item.entry.purpose??'—'),h('td',null,subjectText(item)),h('td',null,modelCell),h('td',null,stateText[item.state]??item.state),
|
|
26
|
+
h('td',null,item.state==='revoked'?h('button',{type:'button',onClick:()=>{const provider=acceptable(item.entry);if(provider)void mutate('/hanamesh/router/grant',{appId:app.id,entryId:id,subject:provider.ref?{kind:'api-key',ref:provider.ref}:{kind:'provider',providerId:provider.id}});}},'恢复'):
|
|
27
|
+
(item.state==='auto'||item.state==='granted')?h('button',{type:'button',onClick:()=>void mutate('/hanamesh/router/revoke',{appId:app.id,entryId:id})},'停用'):null));});});
|
|
28
|
+
return h('section',{className:'hm-providers'},h('h2',null,'供应商'),h('p',null,'这里只显示来源、状态、提示与模型,不读取或展示密钥值。'),error?h('p',{role:'alert'},error):null,
|
|
29
|
+
h('table',null,h('thead',null,h('tr',null,...['名称','来源','状态','Key 提示','模型'].map(label=>h('th',{key:label},label)))),h('tbody',null,...sourceRows)),
|
|
30
|
+
h('p',{className:'hm-provider-hint'},'来源的开关在各自的地方:API key 在 DSH「Models」,Coding OAuth 本地网关在该插件自己的设置页(DSH 对话不需要它,只有按 OpenAI API 说话的应用需要)。'),
|
|
31
|
+
h('h3',null,'路由'),
|
|
32
|
+
h('p',{className:'hm-provider-hint'},'自动:profile 里已配置、且应用声明接受的供应商在应用启动时直接注入;模型随路由一起在这里选(留「应用默认」则用应用清单里的默认值)。「停用」让某一槽位退出自动路由。'),
|
|
33
|
+
routeRows.length?h('table',null,h('thead',null,h('tr',null,...['应用','槽位','用途','供应商','模型','状态',''].map((label,index)=>h('th',{key:index},label)))),h('tbody',null,...routeRows)):h('p',{className:'hm-provider-hint'},'还没有安装任何声明了供应商槽位的应用。'));
|
|
34
|
+
}
|
|
35
|
+
let libraryVisible=false;const libraryListeners=new Set();
|
|
36
|
+
const setLibraryVisible=value=>{libraryVisible=value;for(const listener of libraryListeners)listener(value);};
|
|
37
|
+
// rc.32 market seat. The shell's Extension Management panel (dsh-tauri-panel-extension) reads the client-side
|
|
38
|
+
// cordis service `market` via `ctx.reflect.get('market')` and shows a 「市场」 tab when it is there; it calls
|
|
39
|
+
// `render({preferredSubsectionId})` for the tab body and `setSettingsVisible(false)` while it owns the surface
|
|
40
|
+
// (restored to `true` on dispose). Only one plugin can hold the seat — dshmarket provides the same one.
|
|
41
|
+
const MARKET_SEAT='market',DSHMARKET_PACKAGE='dshmarket';
|
|
42
|
+
const MARKET_CONFLICT_TEXT='检测到 DSH Market(dshmarket):「扩展管理」的市场标签只能由一个插件提供,这次没有由 HanaMesh 市场接管。两者只能其一——要用 HanaMesh 市场请先卸载 dshmarket 并重启 DSH;要用 dshmarket 就从下方侧栏的「市场」入口使用 HanaMesh 市场。';
|
|
43
|
+
let marketConflict='';const conflictListeners=new Set();
|
|
44
|
+
const setMarketConflict=value=>{marketConflict=value;for(const listener of conflictListeners)listener(value);};
|
|
45
|
+
// The seat owner asks us to stand down our own entry points while it renders the market itself.
|
|
46
|
+
let entryVisible=true;const entryListeners=new Set();
|
|
47
|
+
const setEntryVisible=value=>{entryVisible=value;if(!value)setLibraryVisible(false);for(const listener of entryListeners)listener(value);};
|
|
48
|
+
function MarketConflictNotice(){
|
|
49
|
+
const[text,setText]=React.useState(marketConflict);
|
|
50
|
+
React.useEffect(()=>{conflictListeners.add(setText);return()=>conflictListeners.delete(setText);},[]);
|
|
51
|
+
if(!text)return null;
|
|
52
|
+
return h('p',{role:'status',className:'hm-market-conflict','data-hanamesh-market-conflict':DSHMARKET_PACKAGE},text);
|
|
53
|
+
}
|
|
54
|
+
function LibraryAction(){
|
|
55
|
+
const[shown,setShown]=React.useState(entryVisible);
|
|
56
|
+
React.useEffect(()=>{entryListeners.add(setShown);return()=>entryListeners.delete(setShown);},[]);
|
|
57
|
+
if(!shown)return null;
|
|
58
|
+
return h('button',{type:'button',className:'hm-library-action',onClick:()=>setLibraryVisible(true)},'市场');
|
|
59
|
+
}
|
|
60
|
+
function LibrarySourcesSection(){
|
|
61
|
+
const[sources,setSources]=React.useState([]),[draft,setDraft]=React.useState(''),[error,setError]=React.useState('');
|
|
62
|
+
const load=React.useCallback(async()=>{try{const value=await request('/hanamesh/library/sources');setSources(value.sources??[]);setError('');}catch(e){setError(String(e.message??e));}},[]);
|
|
63
|
+
React.useEffect(()=>{void load();},[load]);
|
|
64
|
+
const save=async next=>{try{await request('/hanamesh/library/sources',{method:'POST',body:JSON.stringify({sources:next})});await load();}catch(e){setError(String(e.message??e));}};
|
|
65
|
+
const move=(index,delta)=>{const next=[...sources],target=index+delta;if(target<0||target>=next.length)return;[next[index],next[target]]=[next[target],next[index]];void save(next);};
|
|
66
|
+
return h('section',{className:'hm-library-sources'},h('h2',null,'市场目录源'),h('p',null,'可保存多个来源,但浏览时只启用一个。'),error?h('p',{role:'alert'},error):null,
|
|
67
|
+
...sources.map((source,index)=>h('div',{className:'hm-source-row',key:source.manifestUrl},h('code',null,source.manifestUrl),h('button',{type:'button',disabled:source.enabled,onClick:()=>void save(sources.map((row,i)=>({...row,enabled:i===index})))},source.enabled?'已启用':'启用'),h('button',{type:'button',onClick:()=>move(index,-1)},'上移'),h('button',{type:'button',onClick:()=>move(index,1)},'下移'),h('button',{type:'button',onClick:()=>void save(sources.filter((_,i)=>i!==index))},'删除'))),
|
|
68
|
+
h('div',{className:'hm-source-add'},h('input',{value:draft,placeholder:'https://…/catalog-source.json',onChange:event=>setDraft(event.target.value)}),h('button',{type:'button',onClick:()=>{if(draft)void save([...sources.map(row=>({...row,enabled:false})),{manifestUrl:draft,enabled:true}]);setDraft('');}},'添加并启用')));
|
|
69
|
+
}
|
|
70
|
+
const FAILURE_REASONS={REGISTRY_LOOKUP_FAILED:'包不在当前 registry 上——尚未发布到 npm,或客户端 registry 设置不对',DSH_PLUGIN_FAILED:'DSH 安装命令失败,查看宿主日志',LIBRARY_INSTALL_UNAVAILABLE:'当前宿主未提供安装参数(profile / node 路径)',CATALOG_ITEM_MISSING:'目录里已没有这一项,刷新后重试',UNSTABLE_VERSION:'registry 上的最新版是预发布版(rc),客户端未允许安装预发布版',RUNTIME_ARCHIVE_UNAVAILABLE:'运行时归档下载失败,检查网络或来源',OPERATION_TIMEOUT:'10 分钟内没有结果,可稍后重试',PACKAGE_DENIED:'HanaMesh 套件自身不能从市场安装或卸载',INSTANCE_IN_USE:'先关闭该应用的所有视图再卸载'};
|
|
71
|
+
const failureReason=code=>FAILURE_REASONS[code]??'详细原因见宿主日志';
|
|
72
|
+
// rc.28 market: fixed filters first (everything / applications / plugins), then every category the catalog itself reported.
|
|
73
|
+
const BASE_FILTERS=[['','全部'],['hanamesh-app','应用'],['plugin','插件']];
|
|
74
|
+
const KIND_TEXT={application:'应用',plugin:'插件',listing:'仅收录'};
|
|
75
|
+
const STATE_TEXT={registered:'已安装',installed:'已安装','installed-not-loaded':'需重启','uninstalled-not-unloaded':'已卸载 · 需重启','runtime-missing':'缺运行时',invalid:'包无效'};
|
|
76
|
+
// The desktop shell hosts DSH inside its own webview frame and owns the `hanamesh://` scheme; official DSH in a plain browser has no frame.
|
|
77
|
+
const inShell=()=>{try{return globalThis.window.self!==globalThis.window.top;}catch{return true;}};
|
|
78
|
+
const RESTART_LINK='hanamesh://restart';
|
|
79
|
+
// rc.32: WKWebView never hands a custom-scheme navigation started inside the DSH iframe to the OS, so the
|
|
80
|
+
// deep link alone cannot reach the shell from here. The shell already listens for `hanamesh://…` messages on
|
|
81
|
+
// its inbound iframe bridge (the same namespace as `hanamesh://bound-refresh`), so in-shell we post
|
|
82
|
+
// `hanamesh://restart` to the parent and the shell calls its own restart. The `hanamesh://restart` URL stays
|
|
83
|
+
// the OS-level entry (an external browser or `open hanamesh://restart`), handled in the shell's deep_link.rs.
|
|
84
|
+
const requestRestart=()=>{try{globalThis.window.parent.postMessage({type:RESTART_LINK},'*');}catch{}};
|
|
85
|
+
// Mirrors PROTECTED_PACKAGES on the host: the suite is never offered an uninstall button (the host answers 403 anyway).
|
|
86
|
+
const PROTECTED=new Set(['@hanamesh/dsh-app-host','hanamesh-core','hanamesh-usage']);
|
|
87
|
+
function RestartNotice({reason}){
|
|
88
|
+
const shell=inShell();
|
|
89
|
+
return h('p',{role:'status',className:'hm-restart-notice','data-hanamesh-restart':'required'},h('strong',null,'需重启 DSH'),' ',reason??'插件更改在 DSH 重启后生效。',' ',
|
|
90
|
+
shell?h('a',{href:RESTART_LINK,className:'hm-restart-link',onClick:event=>{event.preventDefault();requestRestart();}},'重启 DSH'):h('span',{className:'hm-restart-hint'},'请手动重启 DSH。'));
|
|
91
|
+
}
|
|
92
|
+
// rc.32: the same market page serves two surfaces — the shell overlay behind the sidebar 「市场」 button
|
|
93
|
+
// (`embedded` false) and the Extension Management 「市场」 tab through the `market` seat (`embedded` true,
|
|
94
|
+
// always visible, no close button, `preferredSubsectionId` decides which subsection comes first).
|
|
95
|
+
// A view is a lease, not a permanent anonymous reference. The host's returned expiry determines
|
|
96
|
+
// the renew cadence; serial requests use the same credential and cleanup explicitly releases it.
|
|
97
|
+
function holdViewLease(receipt,onError){
|
|
98
|
+
const lease={viewId:receipt.lease.viewId,leaseToken:receipt.leaseToken};
|
|
99
|
+
let expiresAt=receipt.lease.expiresAt,timer,controller,closed=false,renewing=false,closeTask;
|
|
100
|
+
const terminal=new Set(['LEASE_EXPIRED','LEASE_NOT_OWNED','INSTANCE_NOT_READY','HOST_CLOSED','UNAUTHENTICATED','FORBIDDEN']);
|
|
101
|
+
const schedule=()=>{if(!closed&&expiresAt>Date.now())timer=setTimeout(()=>void renew(),Math.max(1,Math.floor((expiresAt-Date.now())/3)));};
|
|
102
|
+
const renew=async()=>{
|
|
103
|
+
if(closed||renewing)return;
|
|
104
|
+
renewing=true;clearTimeout(timer);
|
|
105
|
+
controller=new AbortController();
|
|
106
|
+
try{const next=await request('/apps/heartbeat',{method:'POST',body:JSON.stringify(lease),signal:controller.signal});
|
|
107
|
+
if(closed)return;expiresAt=next.expiresAt;schedule();
|
|
108
|
+
}catch(error){if(closed)return;const code=String(error.message??error);onError(code,terminal.has(code));if(!terminal.has(code))schedule();}
|
|
109
|
+
finally{renewing=false;}
|
|
110
|
+
};
|
|
111
|
+
schedule();
|
|
112
|
+
return{renew,close(){
|
|
113
|
+
closed=true;clearTimeout(timer);controller?.abort();
|
|
114
|
+
return closeTask??=request('/apps/close',{method:'POST',body:JSON.stringify(lease),keepalive:true}).catch(error=>{closeTask=undefined;throw error;});
|
|
115
|
+
}};
|
|
116
|
+
}
|
|
117
|
+
function LibraryOverlay({embedded,preferredSubsectionId}={}){
|
|
118
|
+
const[visible,setVisible]=React.useState(embedded?true:libraryVisible),[snapshot,setSnapshot]=React.useState(null),[error,setError]=React.useState(''),[receipt,setReceipt]=React.useState(null),[query,setQuery]=React.useState(''),[filter,setFilter]=React.useState(''),[extra,setExtra]=React.useState([]),[pending,setPending]=React.useState({}),[seenCategories,setSeenCategories]=React.useState([]),[changed,setChanged]=React.useState(false),[openingAppId,setOpeningAppId]=React.useState(null),[closing,setClosing]=React.useState(false);
|
|
119
|
+
React.useEffect(()=>{if(embedded)return;libraryListeners.add(setVisible);return()=>libraryListeners.delete(setVisible);},[embedded]);
|
|
120
|
+
const heldView=React.useRef(null),mounted=React.useRef(true),surfaceVisible=React.useRef(visible),surfaceEpoch=React.useRef(0),opening=React.useRef(null);
|
|
121
|
+
surfaceVisible.current=visible;
|
|
122
|
+
React.useEffect(()=>{mounted.current=true;return()=>{mounted.current=false;};},[]);
|
|
123
|
+
React.useEffect(()=>{
|
|
124
|
+
if(!receipt)return;
|
|
125
|
+
const view=holdViewLease(receipt,(code,terminal)=>{setError(code);if(terminal)setReceipt(null);});
|
|
126
|
+
if(!visible){void view.close().catch(()=>{});setReceipt(null);return;}
|
|
127
|
+
heldView.current=view;
|
|
128
|
+
const release=()=>{void view.close().catch(()=>{});};
|
|
129
|
+
const pagehide=()=>{release();setReceipt(null);};
|
|
130
|
+
const foreground=()=>{if(document.visibilityState==='visible')void view.renew();};
|
|
131
|
+
globalThis.addEventListener?.('pagehide',pagehide);
|
|
132
|
+
globalThis.addEventListener?.('pageshow',foreground);
|
|
133
|
+
document.addEventListener?.('visibilitychange',foreground);
|
|
134
|
+
return()=>{if(heldView.current===view)heldView.current=null;globalThis.removeEventListener?.('pagehide',pagehide);globalThis.removeEventListener?.('pageshow',foreground);document.removeEventListener?.('visibilitychange',foreground);release();};
|
|
135
|
+
},[receipt]);
|
|
136
|
+
React.useEffect(()=>{
|
|
137
|
+
surfaceEpoch.current+=1;
|
|
138
|
+
if(!visible){opening.current=null;setOpeningAppId(null);void heldView.current?.close().catch(()=>{});setReceipt(null);}
|
|
139
|
+
},[visible]);
|
|
140
|
+
// `plugin` is a client-side view (every npm entry that is not an application); every other filter is a provider category.
|
|
141
|
+
const params=React.useCallback(cursor=>{const p=new URLSearchParams();if(query)p.set('q',query);p.set('category',filter==='plugin'?'':filter);if(cursor)p.set('cursor',cursor);return'?'+p.toString();},[query,filter]);
|
|
142
|
+
const remember=page=>setSeenCategories(list=>[...new Set([...list,...(page.categories??[])])].sort());
|
|
143
|
+
const load=React.useCallback(async()=>{try{setError('');setExtra([]);const page=await request('/hanamesh/library'+params());setSnapshot(page);remember(page);}catch(e){setError(String(e.message??e));}},[params]);
|
|
144
|
+
const more=async()=>{try{const cursor=(extra.at(-1)??snapshot)?.page?.nextCursor;if(!cursor)return;const next=await request('/hanamesh/library'+params(cursor));setExtra(list=>[...list,next]);remember(next);}catch(e){setError(String(e.message??e));}};
|
|
145
|
+
React.useEffect(()=>{if(visible)void load();},[visible,load]);
|
|
146
|
+
// rc.18: install/provision/uninstall are 202 + events. Track the operation per card so the user sees
|
|
147
|
+
// 「安装中…」 and, on failure, the code with a readable reason (user 2026-09-20: clicked install, nothing happened).
|
|
148
|
+
const operate=async(path,body,key,label)=>{setError('');setPending(p=>({...p,[key]:{label,text:label+'中…'}}));try{const started=await request(path,{method:'POST',body:JSON.stringify(body)});const {operationId}=started;let after=0;const deadline=Date.now()+600000;let outcome=null;while(!outcome&&Date.now()<deadline){await new Promise(resolve=>setTimeout(resolve,1000));const feed=await request('/hanamesh/library/events?after='+after);after=feed.sequence??after;for(const event of feed.events??[]){if(event.operationId!==operationId)continue;if(event.type.endsWith('-done'))outcome={ok:true};else if(event.type.endsWith('-failed'))outcome={ok:false,code:event.code??'LIBRARY_OPERATION_FAILED'};}}
|
|
149
|
+
if(!outcome)outcome={ok:false,code:'OPERATION_TIMEOUT'};
|
|
150
|
+
if(outcome.ok)setChanged(true);
|
|
151
|
+
setPending(p=>({...p,[key]:outcome.ok?{label,text:label+'完成,重启 DSH 后生效'}:{label,text:label+'失败:'+outcome.code+'('+failureReason(outcome.code)+')',failed:true}}));
|
|
152
|
+
await load();}catch(e){setPending(p=>({...p,[key]:{label,text:label+'失败:'+String(e.message??e),failed:true}}));}};
|
|
153
|
+
const open=async installed=>{
|
|
154
|
+
if(opening.current)return;
|
|
155
|
+
const active={appId:installed.appId};opening.current=active;setOpeningAppId(installed.appId);
|
|
156
|
+
const epoch=surfaceEpoch.current;
|
|
157
|
+
let pendingReceipt=null;
|
|
158
|
+
const abandoned=async next=>{
|
|
159
|
+
if(mounted.current&&surfaceVisible.current&&surfaceEpoch.current===epoch)return false;
|
|
160
|
+
await request('/apps/close',{method:'POST',body:JSON.stringify({viewId:next.lease.viewId,leaseToken:next.leaseToken}),keepalive:true});pendingReceipt=null;return true;
|
|
161
|
+
};
|
|
162
|
+
try{setError('');const deployment=installed.definition.deployments[0],viewId='hml-'+globalThis.crypto.randomUUID();
|
|
163
|
+
let next=await request('/apps/open',{method:'POST',body:JSON.stringify({appId:installed.appId,deploymentId:deployment.id,viewId})});
|
|
164
|
+
pendingReceipt=next;
|
|
165
|
+
const deadline=Date.now()+120000;
|
|
166
|
+
let renewAt=Date.now()+Math.max(1,Math.floor((next.lease.expiresAt-Date.now())/3));
|
|
167
|
+
while(true){
|
|
168
|
+
if(await abandoned(next))return;
|
|
169
|
+
if(next.instance.status==='ready'){setReceipt(next);pendingReceipt=null;return;}
|
|
170
|
+
if(!['reserved','starting'].includes(next.instance.status)||Date.now()>deadline)throw new Error(next.instance.errorCode??'INSTANCE_NOT_READY');
|
|
171
|
+
await new Promise(resolve=>setTimeout(resolve,250));
|
|
172
|
+
if(await abandoned(next))return;
|
|
173
|
+
if(Date.now()>=renewAt){
|
|
174
|
+
const lease=await request('/apps/heartbeat',{method:'POST',body:JSON.stringify({viewId,leaseToken:next.leaseToken})});
|
|
175
|
+
next={...next,lease};renewAt=Date.now()+Math.max(1,Math.floor((lease.expiresAt-Date.now())/3));
|
|
176
|
+
}
|
|
177
|
+
const status=await request('/hanamesh/apps');
|
|
178
|
+
if(await abandoned(next))return;
|
|
179
|
+
const instance=(status.instances??[]).find(item=>item.id===next.instance.id);
|
|
180
|
+
if(!instance)throw new Error('INSTANCE_NOT_READY');
|
|
181
|
+
if(instance.status==='ready'){
|
|
182
|
+
next=await request('/apps/resume',{method:'POST',body:JSON.stringify({viewId,leaseToken:next.leaseToken})});
|
|
183
|
+
if(next.instance.status!=='ready')throw new Error(next.instance.errorCode??'INSTANCE_NOT_READY');
|
|
184
|
+
}
|
|
185
|
+
else next={...next,instance:{...next.instance,status:instance.status,errorCode:instance.errorCode}};
|
|
186
|
+
}
|
|
187
|
+
}catch(e){if(pendingReceipt){const lease=pendingReceipt;pendingReceipt=null;await request('/apps/close',{method:'POST',body:JSON.stringify({viewId:lease.lease.viewId,leaseToken:lease.leaseToken}),keepalive:true}).catch(()=>{});}if(mounted.current&&surfaceVisible.current)setError(String(e.message??e));}
|
|
188
|
+
finally{if(opening.current===active){opening.current=null;if(mounted.current)setOpeningAppId(null);}}
|
|
189
|
+
};
|
|
190
|
+
const close=async()=>{if(closing)return;setClosing(true);try{await heldView.current?.close();setReceipt(null);setError('');}catch(e){setError(String(e.message??e));}finally{if(mounted.current)setClosing(false);}};
|
|
191
|
+
if(!visible)return null;
|
|
192
|
+
const pages=[snapshot,...extra].filter(Boolean),all=pages.flatMap(page=>page.items??[]),items=filter==='plugin'?all.filter(item=>item.kind==='plugin'):all,installed=snapshot?.installed??[],plugins=snapshot?.plugins??[],nextCursor=(extra.at(-1)??snapshot)?.page?.nextCursor??null;
|
|
193
|
+
const restartRequired=changed||Boolean(snapshot?.restartRequired);
|
|
194
|
+
const filters=[...BASE_FILTERS,...seenCategories.filter(c=>c!=='hanamesh-app').map(c=>[c,c])];
|
|
195
|
+
const openButton=row=>h('button',{type:'button',disabled:Boolean(openingAppId),onClick:()=>void open(row)},openingAppId===row.appId?'打开中…':'打开');
|
|
196
|
+
const card=item=>{const row=item.installed,op=pending[item.id],busy=op&&!op.failed&&op.text.endsWith('中…');const installBody={itemId:item.id,...(item.package?.name?{packageName:item.package.name}:{})};
|
|
197
|
+
let action=h('span',null,'仅收录,不可安装'),state=null;
|
|
198
|
+
if(row)state=h('span',{className:'hm-market-state','data-hanamesh-state':row.state},(STATE_TEXT[row.state]??row.state)+(row.version?' '+row.version:''));
|
|
199
|
+
if(busy)action=h('button',{type:'button',disabled:true},op.text);
|
|
200
|
+
else if(item.kind!=='listing'&&!row)action=h('button',{type:'button',onClick:()=>void operate('/hanamesh/library/install',installBody,item.id,'安装')},op?.failed?'重试安装':'安装');
|
|
201
|
+
else if(row&&item.upgradeAvailable)action=h('span',{className:'hm-market-actions'},h('button',{type:'button',onClick:()=>void operate('/hanamesh/library/install',installBody,item.id,'升级')},'升级到 '+item.latestVersion),row.state==='registered'?openButton(row):null);
|
|
202
|
+
else if(row?.state==='runtime-missing')action=h('button',{type:'button',onClick:()=>void operate('/hanamesh/library/provision',{appId:row.appId,packageName:row.packageName,runtimeItem:row.runtimeItem},item.id,'补齐运行时')},'补齐运行时');
|
|
203
|
+
else if(row?.state==='registered')action=h('span',{className:'hm-market-actions'},openButton(row),h('button',{type:'button',onClick:()=>void operate('/hanamesh/library/uninstall',{appId:row.appId,packageName:row.packageName,runtimeItem:row.runtimeItem},item.id,'卸载')},'卸载'));
|
|
204
|
+
else if(row?.state==='installed')action=PROTECTED.has(row.packageName)?h('span',null,'HanaMesh 套件'):h('button',{type:'button',onClick:()=>void operate('/hanamesh/library/plugins/uninstall',{packageName:row.packageName},item.id,'卸载')},'卸载');
|
|
205
|
+
else if(row?.state==='uninstalled-not-unloaded')action=h('span',null,'已卸载,重启 DSH 后生效');
|
|
206
|
+
else if(row)action=h('span',null,'已安装,重启 DSH 后生效');
|
|
207
|
+
return h('article',{key:item.id,'data-hanamesh-library-item':item.id,'data-hanamesh-kind':item.kind},h('h3',null,item.displayName),h('p',null,item.summary),
|
|
208
|
+
h('small',null,KIND_TEXT[item.kind]+' · '+(item.publisher?.name??'未知发布者')+' · '+(item.latestVersion??'—')+((item.categories??[]).length?' · '+item.categories.join('、'):'')),
|
|
209
|
+
state,action,op&&!busy?h('p',{role:op.failed?'alert':'status',className:op.failed?'hm-library-failed':'hm-library-ok'},op.text):null);};
|
|
210
|
+
const catalogSection=receipt?h('div',{className:'hm-app-frame'},h('div',null,h('strong',null,receipt.instance.appId),h('button',{type:'button',disabled:closing,onClick:()=>void close()},closing?'关闭中…':'关闭视图')),h('iframe',{src:receipt.uiUrl,title:receipt.instance.appId,sandbox:'allow-forms allow-modals allow-popups allow-same-origin allow-scripts'})):
|
|
211
|
+
h('div',{className:'hm-library-grid',key:'catalog'},...items.map(card));
|
|
212
|
+
const moreButton=nextCursor&&!receipt?h('button',{type:'button',className:'hm-library-more',key:'more',onClick:()=>void more()},'更多'):null;
|
|
213
|
+
const installedSection=!receipt&&(installed.length||plugins.length)?h('section',{className:'hm-market-installed',key:'installed'},h('h2',null,'已安装'),h('ul',null,
|
|
214
|
+
...installed.map(row=>h('li',{key:'app:'+row.packageName,'data-hanamesh-installed':row.packageName},'应用 · '+(row.name??row.packageName)+' · '+row.packageName+' '+(row.version??'')+' · '+(STATE_TEXT[row.state]??row.state))),
|
|
215
|
+
...plugins.map(row=>h('li',{key:'plugin:'+row.packageName,'data-hanamesh-installed':row.packageName},'插件 · '+row.packageName+' '+(row.version??'')+' · '+(STATE_TEXT[row.state]??row.state)+(row.bundle&&!row.active&&row.state==='installed'?'(未启用为 profile 层)':''),
|
|
216
|
+
row.state==='installed'&&!PROTECTED.has(row.packageName)&&!pending['plugin:'+row.packageName]?h('button',{type:'button',onClick:()=>void operate('/hanamesh/library/plugins/uninstall',{packageName:row.packageName},'plugin:'+row.packageName,'卸载')},'卸载'):pending['plugin:'+row.packageName]?h('span',{className:pending['plugin:'+row.packageName].failed?'hm-library-failed':'hm-library-ok'},' '+pending['plugin:'+row.packageName].text):null)))):null;
|
|
217
|
+
// The seat owner may ask for a subsection to come first; `installed` is the only one it names today.
|
|
218
|
+
const body=preferredSubsectionId==='installed'?[installedSection,catalogSection,moreButton]:[catalogSection,moreButton,installedSection];
|
|
219
|
+
return h('section',{className:embedded?'hm-market hm-market-embedded':'hm-library-overlay hm-market'},
|
|
220
|
+
h('header',null,h('div',null,h('h1',null,'HanaMesh 市场')),embedded?null:h('button',{type:'button',onClick:()=>setLibraryVisible(false)},'关闭')),
|
|
221
|
+
h(MarketConflictNotice,null),
|
|
222
|
+
h('div',{className:'hm-library-toolbar'},h('input',{type:'search',placeholder:'搜索应用与插件',value:query,onChange:e=>setQuery(e.target.value),onKeyDown:e=>{if(e.key==='Enter')void load();}}),h('button',{type:'button',onClick:()=>void load()},'搜索'),
|
|
223
|
+
h('select',{'aria-label':'类别',value:filter,onChange:e=>setFilter(e.target.value)},...filters.map(([value,label])=>h('option',{key:value||'all',value},label)))),
|
|
224
|
+
restartRequired?h(RestartNotice,null):null,
|
|
225
|
+
error?h('p',{role:'alert'},error):null,
|
|
226
|
+
...body);
|
|
227
|
+
}
|
|
228
|
+
/** The `market` seat object read by the shell's Extension Management panel. */
|
|
229
|
+
function createMarketSeat(){
|
|
230
|
+
return{
|
|
231
|
+
render(options){return h(LibraryOverlay,{embedded:true,preferredSubsectionId:options?.preferredSubsectionId});},
|
|
232
|
+
setSettingsVisible(visible){setEntryVisible(visible!==false);},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** Another client plugin (dshmarket) already holds the seat — `reflect.get` is a sibling client service, not a host service. */
|
|
236
|
+
function marketSeatTaken(ctx){try{return ctx.reflect?.get?.(MARKET_SEAT,false)!==undefined;}catch{return false;}}
|
|
237
|
+
/** dshmarket installed in this profile, whether or not its client module has loaded yet. Unreachable host → fail open. */
|
|
238
|
+
async function dshmarketInstalled(){
|
|
239
|
+
try{const snapshot=await request('/hanamesh/library/installedPlugins');return(snapshot.plugins??[]).some(row=>row.packageName===DSHMARKET_PACKAGE);}
|
|
240
|
+
catch{return false;}
|
|
241
|
+
}
|
|
242
|
+
/** Take the seat only when dshmarket is provably absent; otherwise stand down with a readable notice and a log line. */
|
|
243
|
+
function claimMarketSeat(ctx){
|
|
244
|
+
let dispose,released=false;
|
|
245
|
+
// Both logs: the host logger when the client context carries one, and always the browser console —
|
|
246
|
+
// a DSH client logger is not visible from the page, and the console is where this is actually read.
|
|
247
|
+
const standDown=()=>{setMarketConflict(MARKET_CONFLICT_TEXT);
|
|
248
|
+
const line='hanamesh-app-host market: '+MARKET_CONFLICT_TEXT;
|
|
249
|
+
try{if(typeof ctx.logger?.warn==='function')ctx.logger.warn(line);}catch{}
|
|
250
|
+
try{globalThis.console?.warn?.(line);}catch{}};
|
|
251
|
+
void(async()=>{
|
|
252
|
+
const conflict=marketSeatTaken(ctx)||await dshmarketInstalled();
|
|
253
|
+
if(released)return;
|
|
254
|
+
if(conflict){standDown();return;}
|
|
255
|
+
// A seat that appears after the panel mounted is fine: cordis notifies every `inject(['market'])` fiber.
|
|
256
|
+
try{dispose=ctx.provide(MARKET_SEAT,createMarketSeat());}catch{standDown();}
|
|
257
|
+
})();
|
|
258
|
+
return()=>{released=true;setMarketConflict('');setEntryVisible(true);dispose?.();};
|
|
259
|
+
}
|
|
260
|
+
const name='hanamesh-app-host-client',inject=['slots','locale'];
|
|
261
|
+
// No cross-plugin reads here: the browser-side cordis context never carries host services such as hanameshCore
|
|
262
|
+
// (dsh-client-modules boots its own root context); Core status lives in Core's own Settings section.
|
|
263
|
+
function apply(ctx){ctx.effect(()=>{const style=document.createElement('style');style.textContent='.hm-providers,.hm-library-sources{display:grid;gap:16px}.hm-providers table{width:100%;border-collapse:collapse}.hm-providers th,.hm-providers td{padding:8px;text-align:left;border-bottom:1px solid color-mix(in srgb,currentColor 14%,transparent)}.hm-provider-card{padding:16px;border:1px solid color-mix(in srgb,currentColor 18%,transparent);border-radius:12px}.hm-provider-row{display:grid;grid-template-columns:minmax(120px,1fr) auto minmax(180px,1fr) auto;gap:8px;align-items:center;margin:8px 0}.hm-source-row{display:grid;grid-template-columns:minmax(0,1fr) repeat(4,auto);gap:8px}.hm-source-row code{overflow-wrap:anywhere}.hm-source-add{display:flex;gap:8px}.hm-source-add input{flex:1}.hm-library-overlay{position:absolute;inset:0;z-index:30;pointer-events:auto;overflow:auto;padding:24px;background:#fff;color:#18181b}.hm-library-overlay button,.hm-library-overlay input{color:#18181b}.hm-library-overlay>header{display:flex;justify-content:space-between;align-items:center}.hm-library-toolbar{display:flex;gap:8px;align-items:center;margin:8px 0}.hm-library-toolbar input[type=search]{flex:1;padding:6px 10px}.hm-library-more{margin:12px auto;display:block}.hm-library-failed{color:#b91c1c;margin:0}.hm-library-ok{color:#166534;margin:0}.hm-restart-notice{padding:10px 12px;border:1px solid #f59e0b;border-radius:8px;background:#fffbeb;margin:8px 0}.hm-restart-link{margin-left:4px;font-weight:600}.hm-market-state{font-size:12px;color:#3f3f46}.hm-market-actions{display:flex;gap:8px;flex-wrap:wrap}.hm-market-embedded{display:block}.hm-market-embedded>header{display:flex;justify-content:space-between;align-items:center}.hm-market-conflict{padding:10px 12px;border:1px solid #6366f1;border-radius:8px;background:color-mix(in srgb,#6366f1 8%,transparent);margin:8px 0}.hm-market-installed{margin-top:24px}.hm-market-installed ul{padding-left:18px;display:grid;gap:6px}.hm-library-toolbar select{padding:6px 10px}.hm-library-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:16px}.hm-library-grid article{display:grid;gap:10px;padding:16px;border:1px solid color-mix(in srgb,currentColor 16%,transparent);border-radius:12px}.hm-app-frame{display:grid;grid-template-rows:auto 1fr;height:calc(100vh - 130px)}.hm-market-embedded>.hm-app-frame{position:fixed;inset:0;z-index:1001;height:100vh;box-sizing:border-box;background:#fff;padding:12px 16px}.hm-app-frame>div{display:flex;justify-content:space-between}.hm-app-frame iframe{width:100%;height:100%;border:0}';document.head.append(style);return()=>style.remove();},'hanamesh-app-host:style');
|
|
264
|
+
ctx.effect(()=>ctx.slots.inject('settings.section',()=>ctx.slots.register({name:'settings.section',id:'hanamesh-providers',order:18,label:'供应商'},ProvidersSection)),'hanamesh-app-host:providers');
|
|
265
|
+
ctx.effect(()=>ctx.slots.inject('settings.section',()=>ctx.slots.register({name:'settings.section',id:'hanamesh-library-sources',order:19,label:'市场目录源'},LibrarySourcesSection)),'hanamesh-app-host:library-sources');
|
|
266
|
+
ctx.effect(()=>ctx.slots.inject('sidebar.footer.action',()=>ctx.slots.register({name:'sidebar.footer.action',id:'hanamesh-library',order:30},LibraryAction)),'hanamesh-app-host:library-action');
|
|
267
|
+
ctx.effect(()=>ctx.slots.inject('shell.overlay',()=>ctx.slots.register({name:'shell.overlay',id:'hanamesh-library-overlay',order:30},LibraryOverlay)),'hanamesh-app-host:library-overlay');
|
|
268
|
+
ctx.effect(()=>claimMarketSeat(ctx),'hanamesh-app-host:market-seat');}
|
|
269
|
+
return{name,inject,apply,ProvidersSection,LibrarySourcesSection,LibraryAction,LibraryOverlay,MarketConflictNotice,createMarketSeat,
|
|
270
|
+
MARKET_SEAT,MARKET_CONFLICT_TEXT,marketState:()=>({conflict:marketConflict,entryVisible})};
|
|
271
|
+
}});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { InstanceRecord,OpenReceipt,OpenRequest,LeaseRequest,RecoverRequest,ViewLease,AppListing,EventPage } from './index.js';
|
|
2
|
+
export type BrowserInstance = Pick<InstanceRecord,'id'|'appId'|'deploymentId'|'dataId'|'mode'|'status'|'runtimeId'|'errorCode'>;
|
|
3
|
+
export type BrowserReceipt = Omit<OpenReceipt,'instance'> & {instance:BrowserInstance;traceId:string};
|
|
4
|
+
export type BrowserListing = Omit<AppListing,'instances'> & {instances:BrowserInstance[];traceId:string};
|
|
5
|
+
export class WorkspaceAppClient {
|
|
6
|
+
constructor(options?:{origin?:string;fetch?:typeof globalThis.fetch;headers?:()=>Record<string,string>|Promise<Record<string,string>>});
|
|
7
|
+
list(signal?:AbortSignal):Promise<BrowserListing>;
|
|
8
|
+
open(input:OpenRequest,signal?:AbortSignal):Promise<BrowserReceipt>;
|
|
9
|
+
resume(lease:LeaseRequest,signal?:AbortSignal):Promise<BrowserReceipt>;
|
|
10
|
+
recoverView(input:RecoverRequest,signal?:AbortSignal):Promise<BrowserReceipt>;
|
|
11
|
+
close(lease:LeaseRequest,signal?:AbortSignal):Promise<{instanceId:string;alreadyClosed:boolean;traceId:string}>;
|
|
12
|
+
heartbeat(lease:LeaseRequest,signal?:AbortSignal):Promise<ViewLease & {traceId:string}>;
|
|
13
|
+
stop(instanceId:string,options?:{confirm?:boolean;signal?:AbortSignal}):Promise<{instance:BrowserInstance;traceId:string}>;
|
|
14
|
+
events(after?:number,signal?:AbortSignal):Promise<EventPage & {traceId:string}>;
|
|
15
|
+
waitUntilReady(receipt:BrowserReceipt,options?:{timeoutMs?:number;pollMs?:number;signal?:AbortSignal}):Promise<BrowserReceipt>;
|
|
16
|
+
}
|