← 返回列表
需源码安装
Cloudflare 工具、一个 AI Gateway 模型提供方,以及面向 DeepSeek Harness 的…
暂不能直接安装(需源码编译或环境不满足):缺少 main/exports/bin 入口声明;engines.node 要求 ^22.20.3 || >=24.0.0,不满足 Node 22.19.0;仓库 package.json 标记 private,未发布到 npm,需从源码安装。 · 最近上游提交 2026/9/17 · 已提供中文文档
Cloudflare 工具、一个 AI Gateway 模型提供商,以及用于 DeepSeek Harness 的 MCP 直通。
综合分
29.6
GitHub 分
29.6
用户评分
—
★ Stars
0
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add d4551/cloudflare-dsh缺少 main/exports/bin 入口声明;engines.node 要求 ^22.20.3 || >=24.0.0,不满足 Node 22.19.0;仓库 package.json 标记 private,未发布到 npm,需从源码安装,改用 GitHub 源安装
信任档位:已验证本站已于 1 天前真实安装成功
- 是什么
- dsh 原生插件 · market
- 装得上吗
- 本站已真实安装成功(非静态推断)
- 安全吗
- 本站尚未对该插件做风险分级(暂未覆盖,不等同于无风险)
- 还在维护吗
- 活跃:最近一次提交在 8 天前
档位由下列信号合成:本站实装验证(真实安装,当前最高到 L4)· 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。下方各区块是它的证据明细。 验证判据与等级说明 →
🟢实装验证通过· 2026/9/25
由本站实装验证器在真实 dsh 环境安装成功,非静态推断。
数据截至 2026/9/21(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查需源码安装
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包cloudflare-dsh-workspace(未发布到 npm,仅可源码安装)
✗Node 引擎要求 ^22.20.3 || >=24.0.0 · 基线 Node 22.19 不满足
✓dsh CLI 依赖未声明 dsh 版本约束
✗入口文件缺少入口声明
缺少 main/exports/bin 入口声明;engines.node 要求 ^22.20.3 || >=24.0.0,不满足 Node 22.19.0;仓库 package.json 标记 private,未发布到 npm,需从源码安装
验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/22 03:52:38
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
由 DeepSeek 最新模型翻译生成cloudflare-dsh
Cloudflare 工具、一个 AI Gateway 模型提供方,以及面向 DeepSeek Harness 的 MCP 透传。
CI
Mutation score
Coverage
Accessibility
WCAG
TypeScript
Node
License: MIT
像给 5 岁小孩解释一样
想象你有一个非常能干的助手,可以为你编写和运行代码。
这个助手就是 DeepSeek Harness(DSH)。
现在再想象你还从 Cloudflare 租用了大量互联网基础设施:
用来存放文件的地方、用来存放数据库的地方、一个能替你打开网页的机器人,以及一组可以运行 AI 模型的 GPU。
你的助手自己无法触碰这些。它不知道你的账户存在。
这个项目就是那双手。 安装它之后,会发生三件事:
1. 助手获得了工具。 它现在可以说“把这个放进数据库”、
“读取那个网页”、“运行这个 AI 模型”——36 个具体的、带类型的操作,
而不是含糊的“在某处调用某个 API”。
2. 助手可以使用 Cloudflare 自己的 AI 来思考。 不只是把 Cloudflare 模型作为工具来 _调用_——
而是真正 _由它们驱动_,并通过你的 AI Gateway 路由。
3. 你能拿到凭据。 每一个这样的思考请求都会被打上它来自哪个对话的标记。
所以当你问“那个对话花了我多少钱?”时,会有一个来自 Cloudflare 账单数据的真实答案,
而不是估算。
第三点才是有意思的部分,也正是这个项目存在的原因,而不只是一个 API 包装器列表。
同样的事情,再高一层
DSH 插件是 Cordis fibers。这个仓库发布的是一个 bundle——一个 npm
包,声明了 dsh.bundle.patch,它会向某个 profile 的插件树贡献一层行。
其中一行提供一个服务(ctx.cloudflare);其他行消费它来注册工具和一个 LlmAdapter。
这个 provider 之所以能让会话级成本归因成为可能,是因为:DSH 会在每个 GenerateOptions 上传递
sessionId,provider 会把它映射到
cf-aig-metadata 请求头,然后 AI Gateway 会按这个值报告用量和成本。
这种关联是精确的,而不是根据时间戳推断出来的。
目录
- 你能得到什么
- 安装
- 架构
- 工具调用的流转方式
- 模型调用的流转方式
- 按会话的成本归因
- 工具目录
- 模型提供商
- 配置
- Web Client 界面
- 无障碍
- MCP 透传
- 安全模型
- 质量
- 开发
- 项目状态
- 许可证
你将获得
| | |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| 36 个工具 | Workers AI、AI Gateway、AI Search、Vectorize、KV、D1、Queues、R2、Browser Rendering,外加一个有边界的通用 REST 工具 |
| 一个模型提供商 | 两条路由——cloudflare-workers-ai 和 cloudflare-ai-gateway——注册为真正的 LlmAdapter,将 SSE 流式传输到 harness 分块契约中 |
| 会话成本归因 | cf-aig-metadata 携带 harness 会话 ID 和调用用途,因此网关日志和账单可以精确地关联到会话 |
| Web Client 界面 | 一张设置卡片、一个按会话的使用量标签,以及三个工具视图,全部符合 WCAG 2.2 AA |
| MCP 透传 | 用于 Cloudflare 托管 MCP 服务器的补丁行,默认关闭 |
包
| 包 | 角色 |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| @d4551/dsh-cloudflare-core | ctx.cloudflare 能力接缝——凭证解析、作用域、请求构造、错误规范化、分页、重试 |
| cloudflare-dsh | 捆绑包——四个工具组、模型提供商、MCP 行、预设,以及 cordis.patch.yml 层 |
| @d4551/dsh-cloudflare-client | Web Client 界面——设置卡片、会话使用量标签、工具视图 |
这三个包之所以存在,是因为它们有着真正不同的使用方。
另一个捆绑包可能想要 ctx.cloudflare 而不需要这些工具中的任何一个;客户端
包有自己的构建要求(React、客户端 tsconfig、dsh.client)
无头包不得继承。
安装
dsh plugin --profile add cloudflare-dsh @d4551/dsh-cloudflare-core
然后通过环境变量提供 API 令牌。配置中只指定变量名,从不保存值本身:
export CLOUDFLARE_API_TOKEN=...
对于 Web Client 界面,还需将客户端包安装到配置文件中:
dsh plugin --profile add @d4551/dsh-cloudflare-client
API 令牌权限范围
随附工具所需的最低权限组:
Workers AI Read/Write · AI Gateway Read/Write · Vectorize Write ·
Workers KV Storage Write · D1 Write · Workers R2 Storage Write ·
Queues Write · Account Settings Read
[!NOTE]
Cloudflare 仪表板将这些权限组标记为 Edit,而 API 调用中称为
Write。通过 API 创建令牌时请使用 Write。
当令牌只能访问一个账户时,账户 ID 是可选的:它会被自动发现一次并复用。如果令牌可以访问多个账户,自动发现会产生歧义,因此插件拒绝猜测,并要求提供 accountId,同时列出它看到的账户。
架构
该捆绑包围绕单一能力接缝进行组织。所有与 Cloudflare 通信的内容都通过 ctx.cloudflare;其他一切内容都是它的消费者。这正是将凭据集中在一处、错误处理集中在一处,并使工具模块摆脱传输层关注点的原因。
graph TD
subgraph harness["DeepSeek Harness"]
agent["Agent loop"]
tools["ctx.tools"]
llm["ctx.llm"]
slots["ctx.slots"]
creds["ctx.credentials"]
end
subgraph seam["@d4551/dsh-cloudflare-core"]
service["CloudflareService(ctx.cloudflare)"]
client["HTTP clientrequest · retry · paginate · errors"]
service --> client
end
subgraph bundle["cloudflare-dsh"]
tai["tools/ai — 18 tools"]
tdata["tools/data — 13 tools"]
tweb["tools/web — 3 tools"]
tmeta["tools/meta — 2 tools"]
provider["ai — CloudflareAiProvider"]
end
subgraph clientpkg["@d4551/dsh-cloudflare-client"]
ui["SettingsCard · SessionCostChip · 3 tool views"]
end
cf["Cloudflare REST APIapi.cloudflare.com/client/v4"]
gw["AI GatewayOpenAI-compatible endpoint"]
creds -.->|"resolve per operation"| service
tai & tdata & tweb & tmeta -->|"inject: cloudflare"| service
tai & tdata & tweb & tmeta -->|register| tools
provider -->|"inject: cloudflare"| service
provider -->|registerAdapter| llm
ui -->|register| slots
agent --> tools
agent --> llm
client --> cf
provider --> gw
能力接缝
DSH 自身的约定是,只有当三种角色都存在时,一项能力才算完整:服务定义、提供者和消费者。此处:
| 角色 | 位置 |
| ---------- | --------------------------------------------------------------------- |
| 定义 | CloudflareService 类及其类型化接口 |
| 提供者 | packages/core —— 将自身注册为 ctx.cloudflare |
| 消费者 | 每个工具组以及模型提供者,通过 inject: ['cloudflare'] |
为什么采用纯/非纯拆分
在 packages/core 中,只有一个模块负责派发请求:client.ts。该服务提供默认的 fetch,除此之外没有任何地方接触网络。其他所有内容——路径构建、作用域解析、查询序列化、错误映射、分页步进、重试策略——都是纯函数。提供者也是如此:transducer.ts 将整个流式契约保存为一个状态机,没有时钟、没有网络、没有随机性,而 provider.ts 只是它外面的一层薄壳。
这不是风格问题。这正是让 100% 变异分数能够被诚实达到的原因:每个重要的分支都可以由一组字面量夹具输入驱动,因此一个存活的变异体意味着真正的缺口,而不是一个无法测试的接缝。
一个请求可以携带什么
Cloudflare 的 REST 接口并非统一为 JSON,因此 RequestSpec 会说明如何读取以及如何写入,而客户端会严格照做。
| 方向 | 形态 |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 读取 | 信封(request、requestEnvelope);原始文本,用于 KV 值(requestText);带有服务器声明媒体类型的字节,用于截图(requestBytes) |
| 写入 | 客户端序列化为 JSON 的 body;或者调用方已经编码好的 encodedBody,以其自身的媒体类型原样发送——Vectorize 的写入采用 NDJSON,而不是 JSON |
accept 指明调用方可以读取什么,因此一个以图像响应的端点会被请求图像。这些路径中的每一条都经过相同的重试策略和相同的失败分类:当端点发送错误正文时,错误正文会按信封读取,无论成功时的形态会是什么。
工具调用的流转方式
sequenceDiagram
participant A as Agent 循环
participant R as ctx.tools 注册表
participant T as 工具(例如 cloudflare_d1_query)
participant S as ctx.cloudflare
participant C as HTTP 客户端
participant CF as Cloudflare REST
A->>R: 调用 cloudflare_d1_query({...})
R->>R: 根据 ParameterSchemaSpec 验证参数
R->>T: execute(args)
T->>S: accountRequest(spec)
S->>S: 解析账户 ID —— 发现、缓存,不是凭据
S->>C: request(spec)
C->>C: 从 ctx.credentials 解析 API 令牌
Note over C: 每个请求都会重新解析。从不缓存,因此轮换无需重启。
C->>CF: 带 Bearer token 的 HTTPS
CF-->>C: Cloudflare 信封
C->>C: 在 429/5xx 和传输失败时重试,带上限的退避 + 抖动
C->>C: 将信封规范化为类型化错误或结果
C-->>T: 结果
T-->>R: 一个规范 JSON 值
R->>R: 快照,根据 output.schema 验证,冻结
R-->>A: 冻结的值
R->>R: 为转录调用 output.render(args, value)
有两个特性值得特别指出。
规范值是一个编程 API。 DSH 以 PTC 模式运行工具,
这意味着生成的代码可以调用 await tools.cloudflare_kv_list_keys({...}) 并
直接接收该值。因此结果携带 id 和游标,用于馈送下一次
调用,而不是让模型去解析的散文。cloudflare_kv_list_keys 返回
{ keys, cursor, complete },正是为了让生成的循环能够分页,并且每个
列表工具都按其端点的方式分页:页码端点接受 page
和 perPage,并返回 page、perPage、total(当 Cloudflare 报告
一个时,否则为 null)和 complete;游标端点(cloudflare_kv_list_keys、
cloudflare_r2_bucket_list)返回 cursor 和 complete;而
cloudflare_queue_list,其端点不接受分页参数,返回
整个列表,并说明 API 是否报告了比它返回的更多内容。每个工具
都声明一个封闭的输出对象,其中每个字段都有描述且为必需;
注册表在模型或生成的代码看到它之前根据它验证值,
而呈现器读取类型化字段而不是进行强制转换。
呈现器是纯的。 output.render 在会话日志重放期间运行,因此它
不执行 I/O、不读取时钟,也不使用随机性。
取消会到达 Cloudflare。 工具上的 timeoutMs 是声明式的——
注册表不会中断函数体——因此每个工具都会将 exec.signal 转发到
其请求中,而拥有自己预算的工具(inferenceTimeoutMs、
renderTimeoutMs)会将那个预算也作为请求截止时间,这样 120 秒的
推理就不会被 30 秒的客户端默认值切断。
模型调用如何流转
提供者的工作是将一个 HTTP 响应转换为 harness 的 StreamChunk
序列,遵守 harness 所依赖的顺序保证。
sequenceDiagram
participant L as ctx.llm
participant AD as CloudflareAiProvider
participant EP as Endpoint resolver
participant GW as AI Gateway / Workers AI
participant TR as StreamTransducer
L->>AD: prepareCall(provider, model)
AD->>EP: resolveEndpoint + resolveModel,每次生成一次
EP->>GW: GET ai-gateway gateways URL endpoint(仅首次调用)
Note over EP: Base URL 来自 API,并按插件实例记住。token 从不被记住。
EP-->>AD: { url, token } + 模型事实(上下文窗口、模态)
L->>AD: prepared.stream(GenerateOptions)
AD->>AD: buildWireRequest — 图像投影为文本,推理被排除,不支持的选项被拒绝
AD->>AD: 先 attributionHeaders(),再 buildGatewayHeaders — cf-aig-metadata 携带 sessionId + purpose
AD->>GW: POST chat/completions(流式,调用方 AbortSignal 被转发)
loop 当流仍存活时
GW-->>AD: SSE 帧
AD->>TR: push(解析后的帧)
TR-->>AD: StreamChunk[]
AD-->>L: 产出块
end
GW-->>AD: data: [DONE]
AD->>AD: 没有打开任何内容块?→ EMPTY_RESPONSE
AD->>TR: end()
TR-->>AD: block-end… usage… finish
AD-->>L: 最终块
Note over AD: Reader 在 finally 中被取消,因此被放弃的轮次会释放连接。
流式契约
转导器(transducer)是契约所在之处,并且它通过构造而非约定来强制执行:
stateDiagram-v2
[] --> Open
Open --> Open: text-delta / reasoning-delta / tool-call-delta
Open --> Open: usage
Open --> Closing: 看到 finish_reason
Open --> Closing: 传输结束,调用 end
Closing --> Finished: 为每个打开的块按索引顺序发出 block-end
Finished --> []: finish
Finished --> Finished: 后续输入被忽略
| 义务 | 如何成立 |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| usage 在 finish 之前发出 | finish reason 会关闭这些块,但会被推迟到流结束,因此即使提供方在尾部块中报告 usage——即 stream_options.include_usage 产生的形态——也仍会被观察到,并且 finish 始终是最后一个 |
| finish 之后没有任何内容 | 一个 finished 标志使之后每次 push/end 都成为空操作 |
| 块索引按首次出现顺序分配并复用 | 一个分配器,一个以 wire index 为键的映射 |
| 工具调用的 arguments 保持为原始 JSON 字符串 | 片段以 argumentsDelta 累积,在 block-end 时重新拼接,从不解析 |
| 一次提供商调用就是一次尝试 | 没有内部重试——重试策略由 harness 负责 |
| options.signal 会被遵守 | 无条件转发给 fetch(null 是文档中说明的“无信号”) |
| 静默的流会以超时失败 | 每次读取都会与可配置的空闲预算竞争 |
| 不支持的选项会明确失败 | 在发出任何请求之前就被拒绝 |
| 每个提供商请求都带有归因信息 | attributionHeaders() 是请求上最先设置的内容;之后任何东西都无法替换该请求头 |
| 没有内容的补全就是失败 | EMPTY_RESPONSE,无论它是以完成原因、[DONE] 哨兵,还是套接字结束;仅凭用量不算内容 |
| 每个完成原因都有含义 | stop、tool_calls 和 length 映射到 harness 的类型;content_filter 以及任何未知值都是 error 完成,并指明原因 |
| 每个内容块都有明确的归宿 | 文本会被发送;工具调用和结果会往返传递,包括结果旁边的文本;图像通过 harness 自己的辅助函数变成 harness 的文本;推理内容会被省略,因为 OpenAI 兼容的推理 API 在输入中拒绝它 |
| 工具调用始终有 id | 提供商发送时使用提供商的 id;当提供商从不发送时,使用确定性的 call_,因为空 id 无法与其结果关联 |
按会话的成本归因
这就是架构围绕其构建的功能。
graph LR
A["Agent turnsessionId: abc123"] --> B["stream(GenerateOptions)"]
B --> C["cf-aig-metadata:{sessionId, purpose}"]
C --> D["AI Gateway"]
D --> E["Gateway logstagged with metadata"]
D --> F["Billing:usage · cost"]
E --> G["cloudflare_aigateway_session_cost"]
F --> G
G --> H["SessionCostChipin the session header"]
GenerateOptions.purpose 是额外的好处:DSH 将辅助调用标记为
compaction 或 session-title,因此内务操作可以与
真正的 agent 轮次分开归因。
[!IMPORTANT]
声明式路径(presets/pi-ai.yaml,使用 DSH
已内置的 llm-pi-ai 插件)从第一天起即可使用,且采用成本为零——但它无法设置
cf-aig- 请求头,因此无法获得会话关联。这就是
此 bundle 提供真实 provider 而非仅提供 preset 的具体原因。
工具目录
工具分为四个模块,作为单独的 patch 行挂载,因此
profile 只采用它想要的组。
AI — cloudflare-dsh/tools/ai (18)
| 工具 | 用途 |
| -------------------------------------------------------------------------------------- | ----------------------------------------------- |
| cloudflare_ai_run | 运行任意 Workers AI 模型,返回一个完整响应 |
| cloudflare_ai_models_search | 搜索模型目录 |
| cloudflare_ai_model_schema | 获取模型的实时 JSON schema |
| cloudflare_aigateway_list / cloudflare_aigateway_get | 枚举并检查 gateway |
| cloudflare_aigateway_logs | 分页查看 gateway 请求日志 |
| cloudflare_aigateway_log_body | 获取已记录的请求或响应正文 |
| cloudflare_aigateway_routes | 动态路由配置 |
| cloudflare_aigateway_cost | 额度余额、使用历史、发票预览 |
| cloudflare_aigateway_session_cost | 单个 harness 会话的使用量和成本 |
| cloudflare_aisearch_search / cloudflare_aisearch_chat / cloudflare_aisearch_sync | AI Search 查询、聊天补全、索引同步 |
| cloudflare_vectorize_index_list / cloudflare_vectorize_query | 向量索引列表和相似度查询 |
| cloudflare_vectorize_upsert | 以 NDJSON 写入向量,upsert 或 insert |
| cloudflare_vectorize_delete / cloudflare_vectorize_get | 按 id 删除和读回向量 |
Data — cloudflare-dsh/tools/data (13)
| 工具 | 用途 |
| ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| cloudflare_kv_namespace_list | 列出 KV 命名空间 |
| cloudflare_kv_list_keys | 分页列出键 — 返回用于 PTC 循环的游标 |
| cloudflare_kv_get | 读取单个键的值 |
| cloudflare_kv_put / cloudflare_kv_delete | 批量写入和删除,并报告 Cloudflare 接受了哪些操作 |
| cloudflare_d1_list | 列出 D1 数据库 |
| cloudflare_d1_query | 参数化 SQL,支持多语句 |
| cloudflare_queue_list / cloudflare_queue_send / cloudflare_queue_pull / cloudflare_queue_ack | 队列操作,基于租约 |
| cloudflare_r2_bucket_list / cloudflare_r2_bucket_create | R2 存储桶管理 |
Web — cloudflare-dsh/tools/web (3)
| 工具 | 用途 |
| --------------------------------------- | ------------------------------------------------------------------------------- |
| cloudflare_browser_render | Markdown、内容、链接、抓取、JSON |
| cloudflare_browser_screenshot | 截图,保留为持久图像附件并以图像块形式返回 |
| cloudflare_browser_accessibility_tree | 屏幕阅读器会为任意 URL 暴露的角色、名称和结构 |
cloudflare_browser_accessibility_tree 最为突出:它把辅助技术会消费的同一棵树交给智能体,
而这正是 WCAG 审查所需要的,也是截图无法提供的。
cloudflare_browser_screenshot 需要在组合中有一个附件存储
(随附的 dsh 中为 @deepseek-ai/dsh-attachment-local),因为该存储
是 harness 中图像字节的唯一持久归宿。接受图像的模型
会看到页面,客户端会显示它,而纯文本模型会被告知图像已被
省略。不提供 PDF 捕获:PDF 不是光栅图像,因此该存储
无法持有它,而会话日志中的 base64 PDF 会是一团没有任何东西能消费的 blob。
Meta — cloudflare-dsh/tools/meta (2)
| 工具 | 用途 |
| ------------------------- | ----------------------------------------------------------------- |
| cloudflare_account_list | 账户发现 |
| cloudflare_api | 针对本 bundle 未封装的资源的有界通用 REST 调用 |
模型提供方
作为 cloudflare-llm 行(cloudflare-dsh/ai)挂载,注册两条路由:
| 路由 | 端点 | 备注 |
| ----------------------- | ----------------------------------------------- | -------------------------------------------- |
| cloudflare-workers-ai | 该账户的 OpenAI 兼容 Workers AI 路径 | 使用默认值即可工作 |
| cloudflare-ai-gateway | 从网关 URL 端点解析出的基础 URL | 需要 gatewayId;缺失时会大声报错 |
注册在会话选择其中一条路由之前都是惰性的,因此挂载该行不产生任何开销。
错误映射
提供方故障会被映射到 harness 的规范词汇上,而不是原样暴露:
| 信号 | 代码 |
| ----------------------------------------- | ------------------------- |
| 上下文/令牌长度错误文本 | CONTEXT_WINDOW_EXCEEDED |
| 配额耗尽 | QUOTA_EXCEEDED |
| HTTP 429 | RATE_LIMIT |
| 空闲超过 streamIdleTimeoutMs | TIMEOUT |
| 完全没有内容块的补全 | EMPTY_RESPONSE |
| 被内容策略扣留的补全 | CONTENT_FILTER(完成) |
| 线格式无法表达的任何选项 | UNSUPPORTED_OPTION |
| 来自提供方的其他任何情况 | PROVIDER_ERROR |
有两个提供方钩子有意保留 harness 默认值:providerRetryPolicy,因为除了限流失败已经携带的 retry-after 之外,Cloudflare 没有发布任何路由自有的重试策略;以及 imageRequestPricing,因为 Cloudflare 对视觉输入按令牌计价,而不是按图像计价。
配置
每个随部署变化的值都是一个经过验证的 Schemastery 字段,可在 cordis.yml 中更改而无需编辑代码。没有任何可调项被隐藏为常量。
[!WARNING]
补丁层会替换某一行的整个 config 值,而不是合并进去。这就是为什么每一行的配置都小而局部:覆盖一个键永远不会迫使你重述其余部分。
cloudflare — 接缝(@d4551/dsh-cloudflare-core)
| 字段 | 默认值 | 含义 |
| ------------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| apiTokenRef | CLOUDFLARE_API_TOKEN | 凭据引用——一个 POSIX 环境变量名称,绝不是值本身 |
| accountId | '' | 要操作的账户;为空时在首次使用时自动发现 |
| baseUrl | https://api.cloudflare.com/client/v4 | REST 根地址;可覆盖以用于 API 兼容的代理 |
| requestTimeoutMs | 30000 | 单次尝试的截止时间,超时则中止请求。重试会获得全新的时间预算,因此这限制的是单次尝试而非整个重试操作;若某工具自身有更长的预算,则按请求传入 |
| maxRetries | 3 | 针对瞬时故障的重试预算 |
| retryBaseDelayMs | 250 | 首次退避步长 |
| retryMaxDelayMs | 10000 | 退避上限,同时也是应用于服务器 Retry-After 的上限 |
| maxPages | 100 | 单次列表调用所遍历页数的硬性上限 |
cloudflare-llm — 模型提供方(cloudflare-dsh/ai)
| 字段 | 默认值 | 含义 |
| ------------------------- | ------------------------- | ---------------------------------------------------------------------- |
| ------------------------- | ------------------------- | ---------------------------------------------------------------------- |
| gatewayId | '' | 用于路由的网关;网关路由必填 |
| gatewayProvider | workers-ai | 网关转发到的提供商 slug |
| chatCompletionsPath | /chat/completions | 追加到解析后的基础 URL 的路径 |
| workersAiPath | /ai/v1/chat/completions | 与 OpenAI 兼容的 Workers AI 路径,相对于账户作用域 |
| cacheTtlSeconds | 0 | cf-aig-cache-ttl |
| skipCache | false | cf-aig-skip-cache |
| collectLog | true | cf-aig-collect-log — 会话成本归因所必需 |
| tags | {} | 合并到 cf-aig-metadata 的静态标签 |
| customCostPerTokenIn | 0 | 网关记录的每 token 输入成本覆盖值;为零则不发送 |
| customCostPerTokenOut | 0 | 每 token 输出成本覆盖值,与输入值配对 |
| gatewayRequestTimeoutMs | 0 | 网关侧请求超时;为零则保留网关自身的默认值 |
| streamIdleTimeoutMs | 300000 | 流在因超时而失败之前可以静默多长时间 |
| models | [] | 公布的模型;为空表示查询目录 |
cloudflare-tools-ai (cloudflare-dsh/tools/ai)
| Field | Default | Meaning |
| -------------------- | -------- | --------------------------------------------------------------------------------------- |
| pageSize | 50 | 模型目录和网关列表的默认分页大小 |
| searchMaxResults | 10 | cloudflare_aisearch_search 返回的默认分块数量 |
| vectorTopK | 5 | cloudflare_vectorize_query 返回的默认匹配数量 |
| inferenceTimeoutMs | 120000 | 等待模型推理的工具的预算:工具自身的预算及其请求截止时间 |
cloudflare-tools-data (cloudflare-dsh/tools/data)
| Field | Default | Meaning |
| -------------------------- | ------- | ----------------------------------------------------------------- |
| pageSize | 50 | 命名空间、数据库和存储桶列表的默认分页大小 |
| keyListLimit | 1000 | cloudflare_kv_list_keys 每页返回的默认键数量 |
| renderLimit | 4000 | 截断前向模型展示的 KV 值字符数 |
| queueBatchSize | 10 | cloudflare_queue_pull 默认拉取的消息数量 |
| queueVisibilityTimeoutMs | 30000 | 拉取的消息对其他消费者保持不可见的默认时长 |
cloudflare-tools-web (cloudflare-dsh/tools/web)
| 字段 | 默认值 | 含义 |
| -------------------- | -------- | --------------------------------------------------------------------------- |
| renderLimit | 8000 | 截断前展示的渲染页面或无障碍树字符数 |
| renderTimeoutMs | 120000 | 真实浏览器渲染的预算:工具自身的预算及其请求截止时间 |
| screenshotType | png | 调用未指定时截图的图像编码 |
| screenshotFullPage | false | 调用未指定时截图是否捕获整个页面 |
cloudflare-tools-meta — 逃生舱 (cloudflare-dsh/tools/meta)
| 字段 | 默认值 | 含义 |
| ------------------ | ------- | -------------------------------------------------------------- |
| allowMutations | false | 为 false 时,cloudflare_api 拒绝除 GET/HEAD 之外的任何请求 |
| denyPathPrefixes | [] | cloudflare_api 直接拒绝的路径前缀 |
随附的补丁层
- insert:
- id: cloudflare
name: '@d4551/dsh-cloudflare-core'
config:
apiTokenRef: CLOUDFLARE_API_TOKEN
- id: cloudflare-tools-ai
name: 'cloudflare-dsh/tools/ai'
- id: cloudflare-tools-data
name: 'cloudflare-dsh/tools/data'
- id: cloudflare-tools-web
name: 'cloudflare-dsh/tools/web'
- id: cloudflare-llm
name: 'cloudflare-dsh/ai'
- id: cloudflare-tools-meta
name: 'cloudflare-dsh/tools/meta'
config:
allowMutations: false
denyPathPrefixes: []
Web 客户端界面
@d4551/dsh-cloudflare-client 向三个插槽贡献内容,每个都在插槽声明之后。组件从不接收 ctx;它们接收 props。列表插槽通过 id 放置其条目;键控工具视图插槽根据线上工具名称进行分发。
| 组件 | 插槽 | 显示内容 |
| ------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| SettingsCard | settings.plugins.tab → cloudflare | 凭据引用、账户和网关选择。机密信息只写不读 |
| SessionCostChip | conversation.session.header.actions | 本次会话的请求数、成本、缓存命中率和 token 计数 |
| D1Result | tool.call.toolview → cloudflare_d1_query | 一个带列标题的真实表格,最多 100 行,标题为查询语句和行数 |
| BrowserRender | tool.call.toolview → cloudflare_browser_render | 渲染后的文本,标题为它来自的页面 |
| AccessibilityTree | tool.call.toolview → cloudflare_browser_accessibility_tree | 以嵌套列表而非平铺转储形式呈现的树 |
| SessionCostChip | tool.call.toolview → cloudflare_aigateway_session_cost | 同一个 chip,显示单次调用的数据而非会话的数据 |
每个工具视图都是一个 figure,名称取自屏幕上已有的标题,并且刻意不作为 landmark:工具视图每次工具调用都会渲染一次,因此一个运行同一查询两次的会话否则会在页面上放置两个同名 landmark。
工具视图注册为面向所有者的视图,而不是卡片本身。插槽移交的是调用的所有者货币——callId、toolName 以及运行中或已结算的块——而不是工具的结果,因此一个 props 为查询及其行的组件接收不到它所声明的任何内容。结构化值通过 output.presentationMeta 到达客户端:每个工具投射其视图所需的字段,harness 将该投射与会话日志一起持久化,并且它在实时和重放路径上都作为已结算块的 meta 到达。视图验证该投射并渲染组件,或者当重放的日志带有它无法识别的形状时回退到结果文本——作为一个带标题的卡片,它滚动自身的溢出内容并可通过键盘访问,因为该回退与其他任何工具视图一样。
chip 有意出现两次。SessionUsage 被记录为 cloudflare_aigateway_session_cost 返回的形状,因此渲染会话头部的组件也渲染该工具的结果——它的加载和失败状态分别对应运行中的调用和失败的调用。
该包附带 cloudflare.css。颜色是 CSS 自定义属性,因此宿主主题在任何定义它们的地方都会胜出,并且样式表不声明自己的 color-scheme——light-dark() 读取它继承的方案,因此这些片段采用页面已经处于的任一方案。它们曾经声明过一个,这使得它们响应操作系统而页面响应自身:在一个白色文档上的深色卡片,而宿主什么都没做
不同寻常。浏览器通道现在能解析宿主声明与系统偏好全部六种组合下的颜色,并且有一条不变量将声明排除在外。
所有面向用户的文案都通过 locales/en.ts 中的类型化字典进行路由,包括无障碍名称——它们是界面的一部分,而非装饰。这是一道关卡,而非习惯:不变量通道会读取每个组件的语法树,并在任何用户会读到的字面量上失败。这是必须的,因为某个切换开关的整个无障碍名称曾是组件中的字面量,而上面那句话却在这里无人检查。
无障碍
上游 DSH 没有为插件作者提供无障碍指南。本项目设定了自己的标准:WCAG 2.2 AA,零 axe 违规,在 CI 中强制执行。
无障碍检查分五条通道运行,因为其中任何一条都无法覆盖全部标准:
graph LR
subgraph jsdom["通道 1 — jsdom,逐组件"]
A["角色、名称、结构"]
B["键盘可达性"]
C["ARIA 接线:describedby、invalid、实时区域"]
end
subgraph chromium["通道 2 — 通过 Playwright 使用真实 Chromium"]
D["文本对比度,浅色与深色"]
E["焦点环,通过键盘遍历"]
F["每一种状态,以及组装后的客户端"]
end
subgraph computed["通道 3 — 从样式表计算"]
H["非文本对比度:边框、焦点环"]
end
subgraph viewport["通道 4 — 布局后,320px 至 1920px"]
I["重排、目标尺寸、文本间距"]
J["hidden、强制颜色、谁的配色方案胜出、引擎下限"]
end
subgraph e2e["通道 5 — 挂载并操作"]
K["键盘与指针、表单与实时区域"]
end
jsdom --> G["0 违规,0 待审查"]
chromium --> G
computed --> G
viewport --> G
e2e --> G
每一处拆分都出于经过实测的原因。
axe-core 的颜色对比度规则在 jsdom 下运行,无法在那里做出判定:它对本包渲染的每个组件都返回 incomplete,这是实测结果,而非取自问题追踪器。因此文本对比度在真实浏览器中检查,规则在那里能得出裁决,覆盖两种配色方案,以及表面从其 props 可以渲染进入的每一种状态——空 chip、失败 chip、空结果集,各自渲染出其他状态不会渲染的文案。
单独通过的组件在组合后可能发生冲突,因此客户端还会在宿主组装时被扫描:会话 chip 和设置卡片各一次,每个工具视图两次,因为工具视图每次工具调用渲染一次。那次扫描物有所值——它第一次运行时发现工具视图注册为 region 地标,因此一个运行同一查询两次的对话会发布两个同名地标。它们现在是 figure 了。
axe 完全没有针对非文本对比度(SC 1.4.11)的规则,也没有针对焦点外观的规则,因此两者都不做假设。不变量通道读取发布的样式表,解析两种方案中的每个 --cf- token,并在任何
比率所需的颜色对比度——文本为 4.5:1,边框或焦点环为 3:1。浏览器通道用键盘遍历组装后的页面,并固定样式表声明的焦点环,因为当页面未提供焦点环时 Chromium 会自行绘制一个,而只询问某物是否被绘制的测试即使删除了焦点块也会通过。
还有三条标准只有在页面具有宽度后才能观察到,而 axe 对它们都没有规则。组装后的客户端以 320、390、430、768、1024、1280 和 1920 CSS 像素进行布局,并在每一个宽度下检查重排(SC 1.4.10——在 320 下,即 400% 缩放时的 1280px 窗口,任何内容都不得同时向两个方向滚动)、目标尺寸(SC 2.5.8——在渲染后的盒子上测量,而不是从声明中读取,并且在指针为粗指针时使用 44px 而非 24px)以及文本间距(SC 1.4.12——行高 1.5、字母间距 0.12em、单词间距 0.16em、段落间距 2em,且没有任何内容被裁剪)。
粗指针是被模拟的,而不是从窄视口推断出来的。hasTouch 是唯一能让 (pointer: coarse) 匹配的上下文选项,每次运行都会断言该特性确实已翻转,而在它翻转之前,样式表中整个手指尺寸目标块本可以被删除,且所有关卡仍然全绿——在这个页面下,它声称自己已被测量。
该通道在第一次运行时就发现了一个盒模型 bug:在没有 box-sizing 的情况下,一个 inline-size: 100% 的字段会将其内边距和边框加在给定宽度之上,导致页面在包括 1920 在内的每个宽度下都横向滚动。同一通道还会询问 hidden 计算为何,因为 jsdom 不应用任何 CSS——一个设置 display: grid 的类比用户代理的 [hidden] { display: none } 优先级更高,而该 chip 折叠的详情显示在屏幕上,却有一个通过的测试断言该属性已设置。
同一通道还会询问这些片段遵循谁的配色方案,涵盖页面声明的内容与读者系统偏好之间的全部六种组合。这是一个对比度问题(SC 1.4.3),而不是样式问题:当样式表声明了自己的 color-scheme 时,在深色系统下选择了浅色的页面会得到深色片段,而在浅色系统下选择了深色的页面会得到浅色片段——文本位于其自身背景之上,且宿主无法纠正。每个 fixture 都与系统一致,因此没有任何通道与之不一致,而截图在任何断言之前就已显示了这一点。
上述每一个通道都会将这些组件渲染为静态标记,其中没有附加 React:一个永不切换的开关、一个永不提交的表单和一个永不更新的实时区域,都会产生与正常工作的组件相同的标记。第五个通道将真实组件与真实 React 打包在一起,挂载它们并操作它们——用 Enter 和 Space 激活开关,因为真正的按钮对两者都有响应,而带有点击处理程序的 div 对两者都不响应;输入一个无效引用并读取断言性区域
说道;保存并观察 secret 字段清空;以及从字段中按 Enter 提交。该 bundle 是在运行期间构建的,而不是提交的,因此该 lane 不会偏离它声称要检验的源。
还有两件只有浏览器才能回答的事。在 forced-colours 模式下——Windows 高对比度——任何东西都不得退出系统调色板,并且字段和表格单元格仍必须由该模式能够重绘的边框界定,因为仅靠背景在那里会消失。而且这些界面不做任何动画:没有过渡、没有动画、没有关键帧,由 invariants lane 强制执行。这里曾有一个 prefers-reduced-motion 块,它给样式表从未赋予过渡的元素设置了 transition: none——这两个属性都不会被继承,因此宿主也不可能给它们赋予过渡。它什么也没有保护。现在引入动效会导致门禁失败,而到了这一步,就必须编写 reduced-motion 方案,而不是假定它存在。
该标准是针对此样式表所面向的引擎来评估的——Chrome 和 Edge 123、Firefox 120、Safari 17.5 及更高版本,即 light-dark() 设定的下限。低于该下限时会发生什么,是测量出来的,而不是描述出来的:该 lane 会重命名该函数,从而连同其 token 一起重写样式表自身的特性查询,并断言较旧引擎会渲染出什么——文本继承宿主的颜色,背景消失,而强调按钮仍然填充,因为 token 是回退方案唯一无法使用的东西,而特性查询会交给它们字面量。
没有 lane 会过滤 axe。 没有标签范围,没有禁用的规则,没有排除的选择器——并且运行结果既要看它失败的内容,也要看它留给人工审查的内容,因为 incomplete 中曾有一个被禁止的 aria-label,而门禁只读取了 violations。Chromium fixture 提供了宿主会提供的地标、页面标题和 chrome 颜色,因此页面范围的规则会因真实缺陷而失败,而不是因不切实际的测试装置而失败。
具体承诺,每一项都以其对应的测试命名。“由测试覆盖”只有在测试存在并运行的情况下才有价值,因此 readme.test.ts 会收集两个 lane,并在下列名称不在其中时失败:
| 承诺 | 测试 |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 每个输入都有程序化标签 | SettingsCard > gives API token reference a programmatic label、SettingsCard > gives API token a programmatic label、SettingsCard > gives Account ID a programmatic label、SettingsCard > gives AI Gateway ID a programmatic label |
| 密钥字段为 type=password 且不保存任何可供读回的值 | SettingsCard > keeps the token field write-only |
| 一个秘密字段通过 autocomplete=off 将密码管理器拒之门外 | SettingsCard > keeps password managers out of a harness credential |
| 一个无效字段同时指向其提示和错误(SC 3.3.1) | SettingsCard > points the invalid field at both its hint and its error |
| 错误区域保持挂载并留空,直到有内容要显示(SC 4.1.3) | SettingsCard > keeps the error region mounted but empty until validation fails, SettingsCard > announces the validation error and marks the field invalid |
| 状态消息进入一个礼貌的 role="status" 区域 | SettingsCard > confirms a save in a polite status region |
| 结果是带有表头单元格的真实表格 | D1Result > renders a real table with column headers |
| 表格的标题说明了生成它的查询 | D1Result > captions the table with the query that produced it |
| 滚动容器可通过键盘访问并具有名称 | D1Result > makes the scroll container reachable by keyboard and gives it a name、BrowserRender > renders text output in a keyboard-reachable region、D1ResultToolView > renders the fallback in a keyboard-reachable region named for the tool |
| 工具视图不是地标,因为工具视图会重复 | D1Result > keeps the result card out of the landmark map, since tool views repeat、BrowserRender > keeps the render card out of the landmark map, since tool views repeat、AccessibilityTree > keeps the tree card out of the landmark map, since tool views repeat、D1ResultToolView > keeps the fallback out of the landmark map, since tool views repeat |
| 绘制键盘焦点环,并且是测量得出而非假设的(SC 2.4.7) | accessibility in Chromium > draws the declared focus ring on every focus stop in light、accessibility in Chromium > draws the declared focus ring on every focus stop in dark |
| 客户端在宿主组装它时被扫描,而不是一次扫描一个界面 | accessibility in Chromium > the assembled client has no violations in light、accessibility in Chromium > the assembled client has no violations in dark |
| 一次运行不会给人类留下任何需要审查的内容,而不仅仅是没有失败项 | accessibility in Chromium > leaves nothing for a human to review, not merely nothing failing |
| 每一对颜色都达到其对比度要求,包括 axe 无法检查的非文本对比度(SC 1.4.11) | every colour pair the stylesheet ships clears its ratio > puts no colour beneath the ratio its use requires |
| 所有面向用户的文案都经由字典传递,包括可访问名称 | no client component carries inline copy > finds no inline copy in any of them |
| 内容在每种已布局的宽度下都会重排,只有表格和渲染后的正文会滚动(SC 1.4.10) | reflow > lays the assembled client out at 320 (reflow, 400% zoom) without scrolling the page sideways, reflow > lays the assembled client out at 390 (phone) without scrolling the page sideways, reflow > lays the assembled client out at 430 (large phone) without scrolling the page sideways, reflow > lays the assembled client out at 768 (tablet portrait) without scrolling the page sideways, reflow > lays the assembled client out at 1024 (tablet landscape) without scrolling the page sideways, reflow > lays the assembled client out at 1280 (desktop) without scrolling the page sideways, reflow > lays the assembled client out at 1920 (wide desktop) without scrolling the page sideways, reflow > keeps content that cannot reflow inside its own scroller at 320 (reflow, 400% zoom), reflow > keeps content that cannot reflow inside its own scroller at 390 (phone), reflow > keeps content that cannot reflow inside its own scroller at 430 (large phone), reflow > keeps content that cannot reflow inside its own scroller at 768 (tablet portrait), reflow > keeps content that cannot reflow inside its own scroller at 1024 (tablet landscape), reflow > keeps content that cannot reflow inside its own scroller at 1280 (desktop), reflow > keeps content that cannot reflow inside its own scroller at 1920 (wide desktop) |
| 每个控件的可点击区域至少为 24x24 CSS 像素,在指针为粗略的任何地方至少为 44x44(SC 2.5.8) | target size > gives every control at least 24px with a fine pointer at 320 (reflow, 400% zoom), target size > gives every control at least 24px with a fine pointer at 390 (phone), target size > gives every control at least 24px with a fine pointer at 430 (large phone), target size > gives every control at least 24px with a fine pointer at 768 (tablet portrait), target size > gives every control at least 24px with a fine pointer at 1024 (tablet landscape), target size > gives every control at least 24px with a fine pointer at 1280 (desktop), target size > gives every control at least 24px with a fine pointer at 1920 (wide desktop), target size > gives every control at least 44px with a coarse pointer at 320 (reflow, 400% zoom), target size > gives every control at least 44px with a coarse pointer at 390 (phone), target size > gives every control at least 44px with a coarse pointer at 430 (large phone), target size > gives every control at least 44px with a coarse pointer at 768 (tablet portrait), target size > gives every control at least 44px with a coarse pointer at 1024 (tablet landscape), target size > gives every control at least 44px with a coarse pointer at 1280 (desktop), target size > gives every control at least 44px with a coarse pointer at 1920 (wide desktop) |
| 在文本间距覆盖设置下,任何内容都不会被裁剪,在所有布局宽度下均如此(SC 1.4.12) | text spacing > loses no content under the text-spacing overrides at 320 (reflow, 400% zoom), text spacing > loses no content under the text-spacing overrides at 390 (phone), text spacing > loses no content under the text-spacing overrides at 430 (large phone), text spacing > loses no content under the text-spacing overrides at 768 (tablet portrait), text spacing > loses no content under the text-spacing overrides at 1024 (tablet landscape), text spacing > loses no content under the text-spacing overrides at 1280 (desktop), text spacing > loses no content under the text-spacing overrides at 1920 (wide desktop) |
| 当两者不一致时,这些片段采用宿主的配色方案,而非系统的配色方案(SC 1.4.3) | colour scheme > resolves light with a deferring page on a light system、colour scheme > resolves dark with a deferring page on a dark system、colour scheme > resolves light with a light page on a light system、colour scheme > resolves light with a light page on a dark system、colour scheme > resolves dark with a dark page on a light system、colour scheme > resolves dark with a dark page on a dark system |
| 在引擎下限以下,表面会回退到宿主的颜色,而强调按钮仍然填充 | without light-dark() > degrades to the host’s own colours, and keeps the accent buttons legible、without light-dark() > still resolves every colour on an engine that has it |
| 样式表所设置的每个类,都由车道扫描的某个表面渲染 | 此车道扫描的表面 > 渲染已发布样式表所设置的每个类 |
截图不在该列表中:cloudflare_browser_screenshot 将图像作为 harness 附件块返回,由宿主渲染,因此此包没有可供提供替代文本的截图表面。它曾经有过,而这一承诺在代码消失后又延续了两次重启。
MCP 直通
cloudflare-dsh/mcp 为 17 个托管 MCP 服务器导出补丁行——Cloudflare 发布的每一个——每个都带有一行摘要,以便配置文件无需离开文件即可选择。它们范围从 Code Mode 服务器(通过代码执行访问整个 Cloudflare API)到针对文档、绑定、构建、可观测性、容器、浏览器渲染、Logpush、AI Gateway、AutoRAG、审计日志、DNS 分析、数字体验监控、CASB、Radar、博客以及一个已发布演示的各产品服务器。
cloudflare-autorag 有意保留该名称:REST 产品已更名为 AI Search,这就是为什么这些工具寻址 /ai-search,但托管 MCP 服务器仍以 AutoRAG 发布在 AutoRAG 主机上。此列表按 Cloudflare 发布服务器的方式命名它们。
默认不启用任何内容。 每个服务器都是在 agent 沙箱之外访问的远程端点,因此启用其中一个是由配置文件所有者做出的有意行为。身份验证是浏览器 OAuth,这使它们适用于交互式配置文件,而不适合无头运行。
配置文件通过将其想要的行添加到自己的补丁层来选择加入。该模块构建它们,因此服务器名称会根据 MCP 客户端的
在任何内容挂载之前先设置约束:
import { mcpPatchRows } from 'cloudflare-dsh/mcp'
// One row per server, each mounting '@deepseek-ai/dsh-mcp-client' over
// streamable HTTP. Paste into the profile's patch layer.
const rows = mcpPatchRows(['cloudflare-docs', 'cloudflare-browser'])
安全模型
凭据
graph LR
A["cordis.ymlapiTokenRef: CLOUDFLARE_API_TOKEN"] -->|"a name, never a value"| B["ctx.credentials"]
B -->|"resolve() per request"| C["HTTP client"]
C -->|"Authorization: Bearer"| D["Cloudflare"]
E["SettingsCard"] -->|"write-only"| B
B -.->|"redacted descriptor only"| E
- 令牌会在每次请求时重新解析,且从不缓存,因此轮换无需重启即可生效。
- 配置和补丁 YAML 中保存的是引用名称,而绝不是密钥本身。
- 错误消息中只提及引用名称,而绝不提及值。
- 设置界面是只写的:它可以设置凭据并得知是否已存储凭据,但永远无法将其读回。
逃生舱口是有边界的
cloudflare_api 的存在,是为了让此 bundle 未封装的约 70 个 Cloudflare 资源仍然可达。它是一项有边界的能力,而非绕过手段:
1. 默认只读。 allowMutations 为 false,且除 GET/HEAD 之外的任何内容都会在 schema 层面被拒绝。
2. 路径拒绝列表。 denyPathPrefixes 会直接拒绝匹配的路径。
3. 经过验证,而非拼接。 没有 ..,没有绝对 URL,没有主机覆盖——它无法离开 REST 根路径,也无法访问其他主机。
质量
以下每一道关卡都会使构建失败。类型检查、lint、格式、不变量和覆盖率通道在 Node 22 和 24 上运行,打包也是如此,它会以任一版本上的使用者的方式加载构建产物。可访问性和变异测试通道仅在 Node 22 上运行——Chromium 扫描和变异测试运行不会随 Node 主版本而变化,将二者中的任何一个翻倍只会换来更长的等待,而不是更强的信号。test:invariants 会断言这种划分,因此这句话和工作流不会彼此偏离。
| 关卡 | 标准 |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| typecheck | tsc 严格模式,零错误 |
| lint | oxlint --deny-warnings |
| format:check | oxfmt --check —— 一种规范风格,无逐文件覆盖,.gitignore 之外的内容一律不排除 |
| test:invariants | 门禁配置本身即被断言,因此阈值无法被悄悄调低,且本页上的每个计数与图表都会对照代码树进行检查 |
| test:coverage | 100% 行、分支、函数、语句覆盖率 |
| test:dist | 构建产物以消费者解析它们的方式加载 |
| test:a11y | 真实 Chromium,两种配色方案,七种视口;零 axe 违规且无任何待审查项,不进行规则过滤 |
| stryker | 阈值为 100/100/100 且无文件排除 —— 无法在 TypeScript 7 上执行,因为它不导出 ts.parseConfigFileTextToJson 供其预处理器调用;见下方说明 |
| knip / publint | 无未使用的代码或依赖;包可发布 |
给贡献者的三条工具链说明,每一条都是带有理由的固定版本,而非被遗留下来的版本:
- 测试在 Node 上通过 Vitest 运行,而非 bun test,因为 Stryker 没有官方的 Bun 运行器,且 DSH 在 Node 上执行插件。Bun 是包管理器和脚本运行器。
- Vitest 被固定到 5.x,且该固定版本被断言,因此插入符(caret)无法在不触发门禁变红的情况下出现。它被固定到 4.x 是出于一个尚未消失的原因:在 Vitest 5 上,Stryker vitest 运行器的逐测试过滤器匹配不到任何内容,每个被覆盖的变异体都报告为存活
(stryker-js#6210),
这会使变异门禁变绿且毫无意义。现在下限是 5,因此该隐患是现实存在的而非被推迟的,而变异门禁正是它暴露的地方 —— 在下方点名而非假设其不存在。
- TypeScript 处于 7.x。它是原生编译器,不附带编程 API —— 其 lib/ 只包含 tsc.js,别无他物 —— 因此本页上每个读取语法树的门禁都改用 oxc 进行解析,正如 tests/parse.ts 所述,而 Stryker 完全无法针对它运行:其 TypeScript 预处理器调用 ts.parseConfigFileTextToJson,而 7 并不导出该函数。预计 7.1 会提供稳定 API。除此之外代码树已为此做好准备:baseUrl 已被移除,因为它在 7 中停止运作,且 ignoreDeprecations 被禁止 —— 让弃用警告静默,就是配置形式的抑制注释。
开发
bun install
| 脚本 | 作用 |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| bun run typecheck | tsc -b,严格模式,skipLibCheck: false |
| bun run lint | oxlint --deny-warnings . |
| bun run format:check | oxfmt --check;任何不符合规范风格的文件都会导致失败。bun run format 会使其符合规范 |
| bun run test | 在 Node 上运行 Vitest |
| bun run test:coverage | 相同,但使用 100% 阈值 |
| bun run test:invariants | 断言 Quality gates 中的每条规则、mutation guard 的每项检查,以及本页面的工具数量、工具名称、配置字段、无障碍承诺和 Mermaid 图表 |
| bun run test:a11y | 真实 Chromium,两种配色方案,axe 不过滤,以及从 320px 起的七种视口下的布局 |
| bun run build | tsdown,按包构建 |
| bun run test:dist | 以消费者解析它们的方式加载构建后的产物 |
| bun run stryker | 变异测试,然后是 escape guard |
| bun run knip | 未使用的文件、导出和依赖 |
| bun run publint | 三个包的发布健全性检查 |
开发历史见 QUALITY-LOOP.md。
仓库布局
packages/
core/ @d4551/dsh-cloudflare-core — ctx.cloudflare 接缝
src/ client.ts 是唯一执行 I/O 的模块;
config、credentials、errors、paginate、request、retry、scope 均为纯函数
bundle/ cloudflare-dsh — 捆绑包
src/seam.ts 访问 ctx.cloudflare,集中在一处而非五处
src/ai/ provider、transducer、sse、headers、request、errors
src/tools/ ai、data、web、meta
src/tools/_shared/ json、render、paging、batch — 每个工具组共享的内容
src/specs/ 请求规范,与工具接线分离
src/mcp/ 托管 MCP 服务器行
presets/ pi-ai.yaml — 零代码声明式路径
cordis.patch.yml
client/ @d4551/dsh-cloudflare-client — Web 客户端界面
scripts/ mutation-guard.ts — 逃逸守卫的检查,以纯函数形式
verify-mutation-files.ts — 在变异运行后将其应用于代码树
tests/ dist.test.ts — 构建产物测试套件
invariants.test.ts — 质量规则,以断言形式
mutation-guard.test.ts — 每个守卫检查,展示其失败
readme.test.ts — 本页的数量、名称、承诺、图表
test:dist 刻意不使用源码别名。单元测试和可访问性套件通过路径别名访问 src;而此套件以消费者的方式解析包,并拒绝 workspace: 范围残留到已发布的清单中。
项目状态
Pre-1.0,跟踪一个预稳定版 harness。DSH 是一个开发者预览版,其自身文档警告会有破坏兼容性的变更,因此 DSH 依赖被固定到精确版本。
尚未完成,但在依赖本项目之前值得了解:
- 合并按钮处没有任何机制强制要求绿色运行。 各关卡会使构建失败,但 main 没有分支保护,因此红色或未完成的运行不会阻止合并。将本捆绑包带入 main 的那次提交就是证据:它的运行被随后的推送取消,从未完成,因此没有任何关卡对其报告过。开启保护是仓库设置,不是此代码树能断言的事情。
- 这些包尚未发布到 npm。 构建和打包可用;发布是一个尚未采取的刻意步骤,因此上面的 dsh plugin add 命令尚无法解析它们。
- 缺少 R2 对象访问和 D1 数据库创建。 可以列出和创建存储桶,也可以读写 Vectorize 索引;R2 对象级工作需要 S3 API,尚未封装。
- Web 客户端的主机契约是建模的,而非编译的。 工具视图
现在接收宿主提供的 owner props,并读取每个工具的
output.presentationMeta 投影以获取它们渲染的值,这样它们就会渲染工具的结果,而不是什么都不渲染。编译器无法验证的是该契约的形状:ToolCallViewProps 并未从 @deepseek-ai/dsh-client-ui-tool 的包根导出,而且这些包的声明在关闭 skipLibCheck 时无法通过类型检查,因此 owner props 和 settled block 是根据已发布的声明建模,并由契约测试套件固定。上游重命名的字段是需要修复的测试,而不是编译器错误。
- presentCall 和 presentResult 未声明。 提供方中立的渲染意图会为待处理和已完成的卡片提供标题和类别;这些工具目前依赖 harness 的通用卡片。工具视图不依赖它们。
License
MIT — 参见 LICENSE。