OpenWork 桌面应用策略体系:从云端 DesktopConfig 下发到前端门控钩子的完整实现
【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork
OpenWork 桌面应用通过组织级的"桌面策略(Desktop Policy)"实现对企业成员本地能力的远程管控:管理员在云端配置策略,桌面端通过GET /v1/me/desktop-config拉取配置,并借助DesktopConfigProvider与一组 React 钩子在 UI 上执行门控。本文以 desktop-app-policies.md 为主线,结合packages/types、桌面应用前端与 Den API 的源码实现,完整讲解策略目录的定义方式、配置下发链路、四种消费钩子的选型与用法、加载刷新机制以及服务端的生效计算,帮助你安全、规范地在 OpenWork 桌面应用中接入或扩展策略门控能力。
策略体系总览:配置从哪里来、到哪里去
桌面应用策略配置的完整链路分为三层:
- 服务端计算:Den API 的
GET /v1/me/desktop-config路由(见 ee/apps/den-api/src/routes/me/index.ts)根据当前登录成员所属组织、其默认策略与已分配策略,调用calculateDesktopPolicyForOrgMember()计算生效策略,并叠加环境开关与组织品牌元数据后返回。 - 前端状态层:桌面应用的
DesktopConfigProvider(见 apps/app/src/react-app/domains/cloud/desktop-config-provider.tsx)负责缓存、拉取、对比并应用配置,通过 Context 对外暴露config、loading、refresh()与checkRestriction()。 - UI 消费层:业务代码只应通过该 Provider 导出的钩子(
useCheckDesktopRestriction、useDesktopRestriction、useDesktopConfig、useOrgRestrictions)读取策略状态。不要直接读取 Provider 内部的 ref 字段——那些 ref 仅用于在加载新配置时做安全对比与应用(源码注释明确声明了这一点)。
一个典型的GET /v1/me/desktop-config响应(由服务端组装)大致如下:
{ "allowCustomProviders": true, "allowZenModel": false, "allowMultipleWorkspaces": true, "allowControlSettings": true, "allowManageExtensions": true, "allowBuiltInExtensions": true, "allowAlphaUpdates": true, "showWelcomePage": true, "execution": { "commands": "deny", "blockedCommands": ["git push origin main --force"], "browserOrigins": ["https://docs.example.com"], "blockBrowserUploads": false }, "allowedDesktopVersions": ["1.2.3", "1.2.4-beta.1"], "brandAppName": "Acme Work", "brandAccentColor": "blue", "automationsEnabled": true, "dashboardEnabled": true, "connectEnabled": false, "onboardingPrompts": ["帮我梳理本周代码评审", "总结当前分支改动", "起草发布说明"], "onboardingPromptDescriptions": ["代码评审", "改动总结", "发布说明"] }其中布尔策略键决定"功能是否受限",而execution、allowedDesktopVersions、品牌字段、onboardingPrompts等非布尔字段则承载命令执行、版本白名单、品牌定制与新用户引导等扩展能力。
策略目录:desktopPolicyDefinitions 是唯一的权威清单
规范的策略目录(canonical policy catalog)位于 packages/types/src/den/desktop-policies.ts,以desktopPolicyDefinitions数组定义。新增或修改策略项时必须先改这里,这样 API、Den Web 与桌面应用才能共享同一套 ID 与文案(name、teamLabel、description、userNotice)。
不要在任何应用文档或功能代码里重复维护一份策略 ID 列表,除非某个功能确实在单独检查某个策略;需要完整目录时从共享包导入:
import { desktopPolicyKeys } from "@openwork/types/den/desktop-policies";当前策略项一览
每个定义条目包含id、name、teamLabel、description、userNotice、defaultValue,以及restrictedValue+group(受限模式锁定值与其所属分组)。当前目录共 8 个布尔策略:
| id | 分组 | 名称(teamLabel) | 默认值 | Restricted 模式锁定值 |
|---|---|---|---|---|
allowCustomProviders | ai | Add AI providers | true | false |
allowZenModel | ai | Use OpenCode models | true | false |
allowMultipleWorkspaces | app | Create more workspaces | true | false |
allowControlSettings | app | Change app settings | true | false |
allowManageExtensions | tools | Add local tools, skills & MCP servers | true | false |
allowBuiltInExtensions | tools | Use built-in extensions | true | false |
allowAlphaUpdates | app | Try experimental updates | true | false |
showWelcomePage | display | Show welcome page | true | null(不锁定,保持可编辑) |
类型定义(DesktopPolicyDefinitionEntry)强制要求:每个受限能力都必须能在团队编辑器中找到对应位置,而显示类偏好(group: "display",如showWelcomePage)在 Restricted 模式下仍保持可编辑,因此其restrictedValue为null。
布尔语义与校验 Schema
- 布尔策略键语义:
false表示该功能被限制或禁用;true或undefined表示应用不应在本地阻止该功能。 - Schema 自动生成:
desktopPolicyValueSchema由定义列表中的 ID 自动生成(每个键都是z.boolean().optional()),不要手工编辑。源码注释给出了新增策略的六步流程:加定义 → 选择安全默认值 → 选择restrictedValue→ 用钩子接线桌面行为 → 如需影响 Den Web 编辑文案则统一维护name/description/teamLabel/group/userNotice→ 不要手改 Schema。 - 从定义派生出的辅助导出包括
DesktopPolicyKey、desktopPolicyKeys、desktopPolicyDefaults以及desktopPolicyUserNotices(应用在策略阻止能力时展示的用户提示文案)。
非布尔配置项
allowedDesktopVersions是 desktop config 响应的一部分,但它不是desktopPolicyDefinitions中的布尔策略项。它在响应 Schema(desktopConfigSchema)中作为独立数组存在,且经过版本字符串归一化:去除首字母v,并要求匹配\d+\.\d+\.\d+加可选 pre-release/build 段的正则,非法项被过滤、重复项被去重(见 packages/types/src/den/desktop-policies.ts)。
执行策略与文档 Schema:不止布尔开关
除布尔开关外,策略文档还支持access(团队访问)与execution(执行限制)两个结构化字段(见desktopPolicyDocumentSchema):
execution.commands:"allow"或"deny",默认allow;一旦任一匹配策略为deny,生效结果即为deny。execution.blockedCommands:被阻止的命令模式数组,单条长度 1–500,最多 100 条。execution.browserOrigins:允许的浏览器来源(必须是http:/https:、无路径、无凭据、无 query/hash 的站点,最多 100 条),多个策略间取交集。execution.blockBrowserUploads:是否阻止浏览器上传,任一策略为true则生效。
resolveDesktopExecutionPolicy()将多个策略文档的执行配置合并为一份生效值:命令模式取并集去重、浏览器来源取交集、上传限制取或。这套执行策略正是 desktop-policy-engine-rollback.md 中所述"命令与浏览器控制仍然可见"的底层依据——原生内置的浏览器请求、来源与上传门控不依赖被移除的 managed 插件。
组织 Prompt 建议:新用户引导卡片
桌面策略文档还可以携带onboardingPrompts与onboardingPromptDescriptions,用于组织自定义新用户的引导内容。共享 Schema 的约束如下(见 packages/types/src/den/desktop-policies.ts):
onboardingPrompts:要求2 或 3 个prompt,每个 trim 后 1–500 字符;onboardingPromptDescriptions:要求2 或 3 个描述,每个 trim 后最多 120 字符,且描述数组长度必须与 prompt 数组一致。
在桌面应用中,onboardingPromptDescriptions成为 prompt 卡片的标题,onboardingPrompts成为卡片上可见的描述,同时也是用户点击卡片时插入到 composer 中的文本;点击不会自动发送,用户仍需自行触发。
Prompt 配置的选择逻辑:与布尔策略不同
布尔策略键由calculateEffectiveDesktopPolicy()对匹配策略做OR 并集;而 Prompt 建议走的是另一条路径:selectEffectiveOnboardingPromptConfig()(实现见 packages/types/src/den/desktop-policies.ts)按以下次序只选一个匹配的 prompt 配置:
priority最高者优先;- 优先级相同时取
createdAt最早者; - 仍相同时按策略
id字典序(localeCompare)决定; - 没有任何定向 prompt 配置命中时,回退到默认策略的 prompt 配置。
服务端在calculateDesktopPolicyForOrgMember()中同样调用selectEffectiveOnboardingPromptConfig完成计算,保证 API 返回值与桌面端选择逻辑一致。
门控首选:useCheckDesktopRestriction()
当需要给应用行为做门控时,优先使用useCheckDesktopRestriction()。它返回一个稳定的检查函数,入参为策略键,返回true表示该功能被限制(与策略键的false语义互为镜像):
import { useCheckDesktopRestriction } from "../domains/cloud/desktop-config-provider"; function Example() { const checkDesktopRestriction = useCheckDesktopRestriction(); const zenModelsRestricted = checkDesktopRestriction({ restriction: "allowZenModel", }); return zenModelsRestricted ? null : <ZenModelPicker />; }该钩子的底层实现(desktop-config-provider.tsx)返回 Context 中稳定的checkRestriction函数,其形态与 Solid 版本 store 传入的DesktopAppRestrictionChecker一致,便于在非 Hook 代码路径中复用。
单策略钩子:useDesktopRestriction(key)
当组件只需要一个策略值时,用useDesktopRestriction()更简洁——它内部就是对checkRestriction({ restriction })的封装:
import { useDesktopRestriction } from "../domains/cloud/desktop-config-provider"; function AddWorkspaceButton() { const multipleWorkspacesRestricted = useDesktopRestriction( "allowMultipleWorkspaces", ); return ( <button disabled={multipleWorkspacesRestricted}> Add workspace </button> ); }返回值同样是"是否被限制"的布尔值,适合直接驱动disabled、隐藏分支等一次性判断。
原始配置:useDesktopConfig() 与 useOrgRestrictions()
当需要原始配置、加载状态或手动刷新函数时,使用useDesktopConfig()。它返回完整的DesktopConfigStore,包含config、loading、freshConfigStatus、refresh()、refreshFresh()、checkRestriction与connectPolicySync:
import { useDesktopConfig } from "../domains/cloud/desktop-config-provider"; function DesktopPolicyDebug() { const desktopConfig = useDesktopConfig(); return ( <pre> {JSON.stringify({ loading: desktopConfig.loading, config: desktopConfig.config, }, null, 2)} </pre> ); }useOrgRestrictions()只在需要不带loading、refresh、checkRestriction的裸配置对象时使用——它直接返回useDesktopConfig().config(见 desktop-config-provider.tsx):
import { useOrgRestrictions } from "../domains/cloud/desktop-config-provider"; function Example() { const config = useOrgRestrictions(); const customProvidersRestricted = config.allowCustomProviders === false; return customProvidersRestricted ? <RestrictedNotice /> : <ProviderForm />; }模型与 Provider 专用助手函数
针对模型/Provider 的门控,使用 apps/app/src/app/cloud/desktop-app-restrictions.ts 中的助手函数:
checkDesktopAppRestriction({ config, restriction }):核心判定,即config[restriction] === false;isDesktopProviderBlocked({ providerId, checkRestriction }):当 providerId 为opencode时映射到allowZenModel策略;isDesktopModelBlocked({ model, checkRestriction }):基于模型的providerID转发到上者;isSettingsTabAllowed({ tab, checkRestriction }):当allowControlSettings被限制时,仅保留cloud-account标签页,其余设置页隐藏并重定向;desktopRestrictionNotice(restriction):读取desktopPolicyUserNotices中的组织提示文案。
结合钩子的典型用法:
import { isDesktopModelBlocked } from "../../app/cloud/desktop-app-restrictions"; import { useCheckDesktopRestriction } from "../domains/cloud/desktop-config-provider"; function ModelOption({ model }: { model: ModelRef }) { const checkDesktopRestriction = useCheckDesktopRestriction(); const blocked = isDesktopModelBlocked({ model, checkRestriction: checkDesktopRestriction, }); return <ModelRow model={model} disabled={blocked} />; }加载与刷新机制:缓存优先 + 事件驱动 + 小时级兜底
DesktopConfigProvider的加载策略是先读缓存、再取最新,具体流程(见 desktop-config-provider.tsx):
- 从
localStorage同步读取该组织缓存(缓存键为baseUrl::activeOrgId,见 den.ts 中的 getDenDesktopConfigCacheKey),同步应用,保证被门控的 UI 不会因为 HTTP 未完成而闪现"未受限"状态; - 调用
createDenClient(...).getDesktopConfig(activeOrgId)发起GET /v1/me/desktop-config(客户端封装见 den.ts); - 成功后写回缓存、diff 并应用变更;失败则回退到缓存(无缓存回退空配置),并把
freshConfigStatus置为failed; - 若服务端返回
organization_not_found(404),会触发ensureDenActiveOrganization({ forceServerSync: true })重新同步组织,供下次刷新命中有效组织。
刷新时机(三路触发):
- 登录 / 会话变化(
denSessionUpdatedEvent)与 Den 设置变化(denSettingsChangedEvent)事件; - 一小时定时器(
DESKTOP_CONFIG_REFRESH_MS = 60 * 60 * 1000,见 desktop-config-provider.tsx); - 手动刷新:
useDesktopConfig().refresh()(也提供refreshFresh()强制要求新鲜数据,失败即抛错):
const desktopConfig = useDesktopConfig(); await desktopConfig.refresh();此外 Provider 还承担了配置的"副作用应用":brandAppName会写入document.title并同步到 shell、brandIconUrl会通过 IPC 应用到桌面图标,且会把品牌信息回写到desktop-bootstrap.json,防止清除品牌后在下次启动时被快照复活(syncBootstrapBranding)。开发模式下还暴露了window.__openworkApplyDesktopConfig等桥接接口,便于 eval 直接注入配置。
服务端视角:生效策略如何计算
GET /v1/me/desktop-config的处理流程(desktop-policies.ts):
- 查询该组织下所有未删除的桌面策略(含
isDefault、isEnabled、priority、createdAt); - 组织没有任何策略时,直接返回"全部放行"(
allDesktopPolicies(true)); - 找到默认策略(
isDefault && isEnabled),再查询成员角色、所属团队,通过matchingDesktopPolicyAssignmentRoles与团队/成员分配表找到所有匹配的已分配策略; - 调用共享包中的
calculateEffectiveDesktopPolicy():以"全部false"为起点,把默认策略与所有已分配策略中值为true的键做 OR 并集;随后对每个携带access(团队访问)字段的策略,按mode为custom(直接用 capabilities)或locked(用applyRestrictedDesktopPolicy锁定为受限值)解析出能力,把其中为false的键写回结果——访问限制是对布尔并集之后的二次收窄(见 packages/types/src/den/desktop-policies.ts)。
最后,路由把计算出的策略与以下字段组装成响应(me/index.ts):
automationsEnabled:来自env.automations.enabled;dashboardEnabled:来自env.dashboardsEnabled;connectEnabled:来自memberFacingMcpConnectionsEnabled(...);allowedDesktopVersions、brandAppName、brandLogoUrl、brandIconUrl、brandAccentColor:来自组织元数据(normalizeOrganizationMetadata)。
使用准则:API 选型顺序
按照官方文档给出的优先级选用 API:
useCheckDesktopRestriction()——绝大多数功能门控的首选;useDesktopRestriction(key)——单次一次性组件检查;useDesktopConfig()——需要 loading / refresh / 原始配置时;useOrgRestrictions()——仅做裸配置读取时。
常见注意事项
- 不要绕过钩子:应用代码一律通过上述钩子读取策略状态;Provider 内部的 ref(如
currentDesktopConfigRef)只用于配置对比与应用,直接读取会破坏其设计约定。 - 不要重复维护策略 ID:需要完整目录时从
@openwork/types/den/desktop-policies导入,新增/改名策略先改desktopPolicyDefinitions。 - 布尔语义以
false为受限:true或缺失表示本地不应阻止;allowedDesktopVersions等非布尔字段不参与此语义。 - Prompt 与布尔策略的计算路径不同:前者按 priority → createdAt → id 单选,后者按 OR 并集叠加,混用两种心智模型容易出错。
- 执行策略与回滚背景:命令与浏览器门控的本地执行层独立于被回滚的 managed 插件(详见 desktop-policy-engine-rollback.md),重启 OpenWork 及其托管引擎后新配置才完全生效;生成配置的改写不会卸载运行中引擎已加载的钩子。
【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考