# SDK 侦查笔记(dsh-web-app 实现用 API 小抄) > 证据路径约定:`@deepseek-ai/*` 均相对 `.scratch\dsh-src\dsh\node_modules\@deepseek-ai\`;`compact-manager` 相对 `D:\vibe_coding_projects\dsh-compact-manager\`;`genui` 相对 `C:\Users\huama\.dsh\profiles\desktop\node_modules\@changfenhuang\dsh-genui\`。 > 所有结论均附代码证据;拿不准的明确标「未证实」。 --- ## 1. webServer 路由的鉴权(Q1,生死攸关) **结论:插件自己 `webServer.register` 的路由没有任何鉴权——iframe 直接 `src="/dsh-web-app/apps//"` 可以不带任何凭证拿到 200。有鉴权的只有 `/api` 前缀(client-connection 挂载)和 SPA index.html 本身。** ### 路由匹配与处理管线 - `WebServer` 是裸 `node:http` server:每个请求先查 exact 表 → 最长前缀表 → fallback(SPA dist 伺服)。**命中任何插件注册的路由就直接交给 handler,全程无 token/会话校验。** 证据:`dsh-host-webserver/lib/index.js:229-245`(handle:`const route = this.match(rawPath); if (route) { await route.handler(req,res); return; }`)、`:322-332`(match:exact → longest-prefix)。 - 绑定地址只允许 `127.0.0.1` 或 `0.0.0.0`。桌面 GUI 的 19387 端口即该 server(`dsh-desktop-host/lib/index.js:231-235` 固定 `--port 19387`)。默认回环绑定 ⇒ 「无鉴权」的实际暴露面是本机进程;但若以 `0.0.0.0` + `trustedHosts` 做 LAN 部署,任何人都能访问插件路由——我们的路由会伺服用户 HTML,属于暴露面,需靠设计中的「按次发票 token」兜底。 证据:`dsh-host-webserver/lib/index.js:141-147`(Config host union)、`dsh-client-connection/lib/index.js:127-141`(trustedHosts 只作用于 /api 的 fence,注释明说 "this fence is not an auth layer")。 ### /api 前缀(特殊,有鉴权) - `client-connection` host 插件注册 `{kind:'prefix', path:'/api'}`,handler 先 `connection.admit(req)`:Host/Origin fence(Host 必须 loopback/trusted、`sec-fetch-site: cross-site` 拒绝、Origin 必须同 authority)→ 再验签名 cookie(`dsh-auth-*`,HttpOnly + SameSite=Strict,由 `/?token=` 303 交换发放)。不过则 401/403。 证据:`dsh-client-connection/lib/index.js:586-594`(requestRejection/admit)、`:829-844`(/api 路由注册 + admit)、`:388-451`(authorizeIndex / isAuthenticated / writeUnauthorized)、`:205-219`(isTrustedApiRequest)。 - 推论:**`connection.fetch.register` 的 exact 路由全部挂在 `/api` 之下**(路径必须以 `/api/` 开头,断言见 `:758-762`),因此它们需要 GUI 页面的签名 cookie。GUI 页面内同源 `fetch('/api/...')` 会自动带 cookie(compact-manager 就是这么干的,`compact-manager/lib/client.js:22-23,305`)。但 **sandbox iframe(无 `allow-same-origin`)是 opaque origin**,它发出的请求若带 `Origin: null` 会被 fence 拒(`dsh-client-connection/lib/index.js:212-218`);GET 不带 Origin 能过 fence 但过不了 cookie 401。**⇒ webapp 的 iframe 不要走 `/api`,全部数据通道放插件自己的 `webServer` 前缀路由**(如 `/dsh-web-app/api/...`,无鉴权,靠门票 token)。 - SPA index.html 也要求 cookie/token 交换;**非 index 静态资源公开**。 证据:`dsh-host-frontend-static/lib/index.js:49-75`(`authorizeIndex` 只包 index)、`:87-96`。 ### client 如何合法获得带凭证 URL - `ctx.connection.authenticatedUrl(baseUrl)` 把进程 launch token 写成 `?token=` 拼到 URL 上;仅 host 侧可用(`dsh-desktop-host/lib/index.js:337`、`dsh-web-app(官方)/lib/index.js:198-206` 打印/打开浏览器用)。**client(浏览器)侧没有对应 API**——也不需要:页面本身已持 cookie。 证据:`dsh-client-connection/lib/index.js:374-378,600-602`。 - `window.__DSH_BOOT__` 由 `webserver/index-inject` 行渲染进 index.html(`kind:"global"` 行 → ``),它是 client 插件图(不是凭证)。 证据:`dsh-host-webserver/lib/index.js:24-52`(renderRow)、`dsh-client-modules/lib/client.js:108-173`(parseBootManifest)。 ### 最小示例(host 半静态伺服 + 盖章) ```js ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: '/dsh-web-app/apps', // 匹配 /dsh-web-app/apps//... handler: async (req, res) => { // node 风格 req/res,无鉴权层 // 读 registry → 按 deployedRef 读文件 → 替换 {{__TOKEN__}} 等占位符 → res.end(html) }, }), 'dsh-web-app: app route') ``` 坑: - 重复注册同 (kind,path) 会 throw(`dsh-host-webserver/lib/index.js:177-184`)。 - 路由只按 pathname 匹配(query 不参与,`new URL(req.url,'http://x').pathname`,`:232`)。 - 前缀匹配要求 `pathname === prefix || pathname.startsWith(prefix + '/')`(`:327-328`)——裸 `/dsh-web-app/apps`(无尾斜杠)也匹配。 - 压缩:`compression: 'gzip'` 时由 server 级 middleware 处理(`:106-131`),与路由无关。 - `/api` 是保留前缀:插件不能注册名为 `/api` 的 rpc channel(`dsh-client-connection/lib/index.js:755-757`),但注册 `/api/xxx` 的 **webServer exact 路由会 shadow** connection 的共享 handler(exact 表优先)——别这么干。 --- ## 2. host.call 的 host 侧应答(Q2) **结论:`host.call(method,args)` 只存在于「dynamic Cordis 包」的 client 半(evaluator 用 `new Function` 注入 `React/console/styles/host/harness` 等闭包参数);手写 classic-script 插件(本项目形态,同 compact-manager/genui)拿不到 `host` builtin。机制存在且可用(HTTP POST `/api/dynamicCordisRunner/invoke`,host 半用 `harness.handle(method, fn)` 注册),但不适合本项目——推荐替代:插件自己注册 `webServer` JSON 路由(iframe 内 `fetch('/dsh-web-app/rpc/...')`)或 client 半 `fetch('/api/...')` 走 connection.fetch。** ### 机制细节(dynamic 包专用) - client 半函数签名:factory/evaluate 形参含 `host`;`host.call(method, args)` → `env.invoke(pluginId, pluginRunId, method, args)` → `ctx.remote.dynamicCordisRunner.invoke(...)` → HTTP POST 同域 `api/dynamicCordisRunner/invoke`(unary 走 HTTP,WebSocket 只用于 `/api/remote.mux` stream mux)。信封 `{type:'client-request',rpcId,method,payload}` / `{type:'server-response',rpcId,result:{ok,value}|{ok:false,error}}`。 证据:`dsh-cordis-client-runner/lib/client.js:54-55,66-70,152-181,535,6162-6164,6539-6560,6603-6611`、`dsh-api-gateway/lib/client.js:1785-1807,2043-2045`、`dsh-client-connection/lib/client.js:1209-1239,1288-1312`。 - host 半注册 API:`harness.handle(method, fn)`(**没有** `registerMethod`)。校验 method 非空 string、fn 为 function;返回 dispose。fn 只收一个参数(wire JSON 解码后的 args,省略为 null),**不注入 ctx**——需闭包捕获 host 半 `apply(ctx)` 的 ctx。返回值经跨 realm JSON 克隆(返回 `undefined` 报错,建议 `return null`)。错误码:`plugin-not-running / stale-run / method-not-found / handler-error`。 证据:`dsh-cordis-host-runner/lib/index.js:525-533,1207-1224,2115-2148,2264-2279`、`dsh-cordis-host-runner/lib/types/guard.js:540-552`、`dsh-api-gateway/lib/index.js:624,631-643`。 - **第三方实证**:genui 源码 0 处用 `host.call`;它走 `ctx.inject` + host 半 `webServer.register({kind:'prefix',...})` 伺服资产。证据:`genui/src/plugin/index.ts:216-222`。 - 死胡同备忘:`dsh-client-modules` 是 client 模块加载器,与 RPC 无关;`dsh-sdk-jsonrpc-server` 是进程外 SDK 的 stdio JSON-RPC,插件用不上。 ### 推荐替代(本项目) client 半(classic script)⇄ host 半通信,二选一: 1. **webServer JSON 路由**(推荐,iframe 内可用):host 半 `ctx.webServer.register({kind:'prefix', path:'/dsh-web-app/rpc'})`,client 半(卡片组件,非 iframe)`fetch('/dsh-web-app/rpc/list')`;iframe 内部也可用(同源、无 cookie 要求)。 2. **connection.fetch**:host 半 `ctx.connection.fetch.register({path:'/api/dsh-web-app/state', methods:['GET'], requestBody:'buffered', fetch})`;client 半卡片组件里 `fetch('/api/dsh-web-app/state')`(cookie 自动携带)。**iframe 内不可用**(sandbox opaque origin)。 --- ## 3. 插件数据目录(Q3) **结论:没有「插件数据目录」cordis 服务。第一方约定是 `$DSH_HOME//`(纯函数库 `dsh-home-paths` 解析 home,README 明说 "Use it as a direct library dependency, not through cordis.yml")。本项目用 `dshHomePath('dsh-web-app', 'apps', name)` 放 Git 仓库。** - `dsh-home-paths` 是纯库(非服务),导出:`resolveDshHome(configured?, env?)`、`dshHomePath(...segments)`、`dshCachePath(seg?, ...)`、`expandHomePath(p)`、`dshHomeDisplay(home)`。 优先级:显式 configured > `$DSH_HOME` 环境变量(空白视为未设)> `~/.dsh`(Windows 即 `%USERPROFILE%\.dsh`)。 证据:`dsh-home-paths/lib/index.js:11-15,49-51,73-84`;`dsh-home-paths/README.md:31-41`。 - 第一方使用先例(均为直接 import + `join(resolveDshHome(), '', ...)`): - `dsh-llm-deepseek/lib/index.js:804` → `$DSH_HOME/llm-deepseek/files-v3.json` - `dsh-desktop-host/lib/index.js:329` → `$DSH_HOME/dsh-runtimes/dsh-primary-runtime` - `dsh-credentials-local/lib/index.js:58` → `$DSH_HOME/.credentials.yaml` - `dsh-attachment-local/lib/index.js:995-997` → `$DSH_HOME/cache/attachments` - 配置式 home:`dsh-agent-instructions/lib/index.js:69`、`dsh-shell-env/lib/index.js:50`(`resolveDshHome(config.dshHome)`)。 - Profile(桌面 GUI 实例)目录是 `$DSH_HOME/profiles/`,插件装在 profile 的 `node_modules` 下。证据:`dsh-app-boot/lib/index.js:466-485,524-526`。 ### 推荐用法(host 半) ```js import { dshHomePath } from '@deepseek-ai/dsh-home-paths' const appsRoot = dshHomePath('dsh-web-app', 'apps') // \dsh-web-app\apps\\ ``` - 跨会话共享 ✔(home 级,不在 profile 下);随 `$DSH_HOME` 迁移 ✔;harness 升级不影响 ✔。 - 坑:`dshHomePath` 只算路径不建目录(`README.md:41`);目录创建/权限归插件。若不想依赖该包,fallback = `join(os.homedir(), '.dsh', 'dsh-web-app', 'apps')`,但会漏掉 `$DSH_HOME` 覆盖(**不推荐**,标「备用方案」)。 - 备选:compact-manager 用 `ctx.storageDomain.open(domain)` 做 JSON 持久化(适合注册表 registry.json!),但它是 KV 表不是文件系统,放不了 Git 仓库。证据:`compact-manager/lib/index.js:120-127`(`storageDomain` 服务)。**建议:registry.json 用 storageDomain 或 home 下 JSON 文件均可;apps// Git 仓库必须落文件系统 ⇒ dshHomePath。** --- ## 4. classic-script client 的模块加载(Q4) **结论:`window.__ModuleLoader__.load({id, factory:(require)=>...})` 的 require 解析顺序 = ① seed(平台种子表)→ ② 已物化缓存 → ③ boot 图行(其他插件包)→ ④ 已注册 factory;找不到即 throw。builtin(React/host/styles/console)对 static bundle 插件而言:React 从 seed require;`host`/`styles` 拿不到(dynamic 包专属,见 Q2);CSS 靠 `