diff --git a/README.i18n.yaml b/README.i18n.yaml index 23094d8e96..53b0af8024 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -1,5 +1,2 @@ -# Bilingual-pair consistency record: the git blob hash of each side as of the last -# confirmed-consistent state. Both languages carry equal authority. Update both files -# and re-record their hashes after editing either side. -README.md: 88fa1de7794bccc9841e53719e4542272472f871 -README.zh-CN.md: 4c409b2075e4b9acd264958908d0b1b353f66ba4 +README.md: 0001e932629d1d60393f7d5dfab8da9ac5a681fa +README.zh-CN.md: 2739465b697fd7ae4beaa39f230964b25b82b32b diff --git a/README.md b/README.md index 861ea6c4f2..0001e93262 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

WorkDSH brings material, experts, skills, and connectors into one workspace, with the SkillHub catalog and installable DSH community plugins.

Download Desktop · Explore the workflow · Personal and enterprise · User guide · 简体中文

-[![Desktop release](https://img.shields.io/badge/Desktop-2.0.6--alpha.2-176BFF)](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.2) [![GitHub stars](https://img.shields.io/github/stars/techflag/workdsh?label=stars)](https://github.com/techflag/workdsh) [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[![Desktop release](https://img.shields.io/badge/Desktop-2.0.6--alpha.3-176BFF)](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.3) [![GitHub stars](https://img.shields.io/github/stars/techflag/workdsh?label=stars)](https://github.com/techflag/workdsh) [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE) ![WorkDSH projects home with project templates and the complete desktop sidebar](apps/web/assets/screenshots/workdsh-projects-alpha8-dark.png) @@ -81,7 +81,14 @@ The enterprise backend is developed and deployed independently in **[techflag/wo Administrators should follow the [workdsh-admin deployment and usage guide](https://github.com/techflag/workdsh-admin#readme), then provide members with the backend address and company accounts. Members install this repository's desktop app and Enterprise Connection plugin and connect to that address; the Agent and tools continue to run on their computer. -### Install enterprise plugins +### Enterprise Connection: download, install and sign in + +**Enterprise features are enabled by an explicitly installed Enterprise Connection plugin, not bundled in the base Desktop.** One package provides company accounts, colleague collaboration and authenticated requests for independent business plugins. + +1. Download `workdsh-enterprise-connection-0.1.0-alpha.2.tgz` from this release and retain the file. +2. In personal mode, open Plugins → Add plugin, enter the full tgz path, install and click Enable now. +3. Open Settings → Enterprise account → Connect enterprise and sign in using the company backend address and member account. +4. Open Collaboration to review shares and configure the company API manually in model settings. Developers can inject `workdshEnterprise`; see the [integration guide](docs/ENTERPRISE-PLUGIN-AUTH.md). The Enterprise Connection package is delivered independently through our [GitHub Releases](https://github.com/techflag/workdsh/releases), without a third-party marketplace. Download `workdsh-enterprise-connection-.tgz`, retain the file and enter its full path in Add plugin. Release assets include the compatible DSH version manifest and `SHA256SUMS`. @@ -148,13 +155,13 @@ WorkDSH Admin — organization overview: ## Download Desktop -The planned desktop installer release is **2.0.6-alpha.2**. Its download links will become available after the [GitHub Release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.2) is published: +The planned desktop installer release is **2.0.6-alpha.3**. Its download links will become available after the [GitHub Release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.3) is published: | Platform | Download | | --- | --- | -| Windows x64 | [WorkDSH Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.2/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.6-alpha.2-x64-Setup.exe) | -| macOS Apple Silicon | [WorkDSH arm64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.2/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.6-alpha.2-arm64.dmg) | -| macOS Intel | [WorkDSH x64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.2/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.6-alpha.2-x64.dmg) | +| Windows x64 | [WorkDSH Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.3/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.6-alpha.3-x64-Setup.exe) | +| macOS Apple Silicon | [WorkDSH arm64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.3/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.6-alpha.3-arm64.dmg) | +| macOS Intel | [WorkDSH x64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.3/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.6-alpha.3-x64.dmg) | Desktop installers include Node.js, pnpm and Python runtimes by default, so users do not need to install them separately. This is an **Alpha release**: end-to-end project document references, expert execution, and different Office formats are still being validated. The macOS DMGs are unsigned; download updates from [Releases](https://github.com/techflag/workdsh/releases). Start with the [user guide](docs/user-guide.en.md) and [FAQ](docs/faq.en.md). @@ -162,6 +169,10 @@ The base Desktop package does not preinstall enterprise plugins. Install them se The **Tools → Terminal command dsh (optional)** menu lets you inspect, install, repair or remove the terminal command. Desktop chat does not require it; its dialog appears only when you select the menu. Installation creates a command entry using the packaged Node, pnpm and official CLI. Removing it does not delete the app or workspace. Commands display the active Desktop space; use `--workdsh-space=personal` or `--workdsh-space=enterprise` to select it explicitly. Initialize the space in Desktop first and keep Desktop signed in for enterprise operations. Plugin changes apply to that space’s Profile. +### Build plugins with enterprise authentication + +Host plugins inject `workdshEnterprise` and call `request({ plugin: "reports", operation: "list", method: "POST", body: { page: 1 } })`. Desktop attaches the current member authentication without exposing tokens. The backend must authorize every business operation. See the [integration guide](docs/ENTERPRISE-PLUGIN-AUTH.md) for code, type-package setup and the required new Desktop/plugin versions. + ## Development and documentation Source ownership: [WorkDSH feature packages and Web](apps/web/README.md) · [Desktop carrier](apps/desktop/README.md) · [Architecture](docs/architecture.en.md) · [All documentation](docs/README.en.md). Running from source requires Node.js 22.19+ or 24+, Corepack, and Yarn 4.18.0: diff --git a/README.zh-CN.md b/README.zh-CN.md index 406c6fc107..2739465b69 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,7 +4,7 @@

WorkDSH 将资料、专家、技能和连接器带入同一工作台;接入 SkillHub 技能目录,并支持安装 DSH 社区插件。

下载桌面版 · 了解工作流 · 个人与企业 · 使用指南 · English

-[![Desktop release](https://img.shields.io/badge/Desktop-2.0.6--alpha.2-176BFF)](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.2) [![GitHub stars](https://img.shields.io/github/stars/techflag/workdsh?label=stars)](https://github.com/techflag/workdsh) [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[![Desktop release](https://img.shields.io/badge/Desktop-2.0.6--alpha.3-176BFF)](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.3) [![GitHub stars](https://img.shields.io/github/stars/techflag/workdsh?label=stars)](https://github.com/techflag/workdsh) [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE) ![WorkDSH 项目主页:项目、模板与完整桌面侧栏](apps/web/assets/screenshots/workdsh-projects-alpha8-dark.png) @@ -81,7 +81,14 @@ Desktop 的个人与企业空间分别保存数据和凭据。企业账号插件 管理员请按 [workdsh-admin 的部署与使用说明](https://github.com/techflag/workdsh-admin#readme) 部署后台,并向成员提供后台地址和公司账号。成员安装本仓库的桌面端与企业连接插件后连接该地址;Agent 与工具仍在成员本机执行。 -### 安装企业插件 +### 企业连接插件:下载、安装与登录 + +**企业功能通过企业连接插件按需启用,基础桌面包不预装。** 一个包提供企业账号、@同事协作,并向独立业务插件提供认证请求服务。 + +1. 下载本次发行的 `workdsh-enterprise-connection-0.1.0-alpha.2.tgz`,保留文件。 +2. 在个人空间打开“插件 → 添加插件”,填写 tgz 完整路径,安装后点击“立即启用”。 +3. 打开“设置 → 企业账号 → 连接企业”,填写公司后台地址和成员账号登录。 +4. 在“协作”查看分享,在模型设置中手动配置公司内部 API。开发者可注入 `workdshEnterprise` 复用认证,详见[调用说明](docs/ENTERPRISE-PLUGIN-AUTH.md)。 企业连接包由本项目 [GitHub Releases](https://github.com/techflag/workdsh/releases) 独立交付,不依赖第三方插件市场。下载 `workdsh-enterprise-connection-<版本>.tgz`,保留文件并在添加插件时填写完整路径;发行附件包含兼容 DSH 版本的清单及 `SHA256SUMS`。 @@ -156,13 +163,13 @@ WorkDSH Admin:组织概览。 ## 下载桌面版 -计划发布的桌面安装包版本为 **2.0.6-alpha.2**。发布 [GitHub Release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.2) 后,以下下载链接才会生效: +计划发布的桌面安装包版本为 **2.0.6-alpha.3**。发布 [GitHub Release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.3) 后,以下下载链接才会生效: | 平台 | 下载 | | --- | --- | -| Windows x64 | [WorkDSH Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.2/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.6-alpha.2-x64-Setup.exe) | -| macOS Apple Silicon | [WorkDSH arm64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.2/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.6-alpha.2-arm64.dmg) | -| macOS Intel | [WorkDSH x64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.2/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.6-alpha.2-x64.dmg) | +| Windows x64 | [WorkDSH Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.3/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.6-alpha.3-x64-Setup.exe) | +| macOS Apple Silicon | [WorkDSH arm64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.3/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.6-alpha.3-arm64.dmg) | +| macOS Intel | [WorkDSH x64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.3/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.6-alpha.3-x64.dmg) | Desktop 安装包默认内置 Node.js、pnpm 和 Python 运行时,普通用户无需单独安装。当前为 **Alpha 版**:项目资料引用、专家执行及不同 Office 格式的端到端体验仍在验收中。macOS DMG 未签名;更新请从 [Releases](https://github.com/techflag/workdsh/releases) 下载。开始使用前请阅读[用户指南](docs/user-guide.md)和[常见问题](docs/faq.md)。 @@ -170,6 +177,10 @@ Desktop 基础包不预装企业插件,安装企业插件后启用企业登录 桌面菜单“工具 → 终端命令 dsh(可选)”可查看、安装、修复和移除终端命令。普通桌面聊天无需安装此命令,只有主动点击菜单才显示管理弹窗。安装只创建命令入口,使用随包的 Node、pnpm 和官方 CLI;移除命令不会删除应用或工作区。命令默认操作当前 Desktop 工作区,启动时显示空间名称;`--workdsh-space=personal` 或 `--workdsh-space=enterprise` 可明确选择。企业空间须保持 Desktop 登录,首次使用前先在 Desktop 打开对应空间。插件安装、更新和移除遵循该空间的官方 Profile。 +### 开发需要企业认证的插件 + +企业业务插件注入 `workdshEnterprise`,通过 `request({ plugin: "reports", operation: "list", method: "POST", body: { page: 1 } })` 请求公司后台。Desktop 自动携带当前成员认证,插件不读取或保存 Token;后台仍检查成员和业务权限。调用示例、SDK 类型依赖及新旧版本要求见[企业插件认证接入](docs/ENTERPRISE-PLUGIN-AUTH.md)。 + ## 开发与文档 源码分工:[WorkDSH 功能包与 Web](apps/web/README.zh-CN.md) · [Desktop 外壳](apps/desktop/README.zh.md) · [架构](docs/architecture.md) · [全部文档](docs/README.md)。从源码运行需要 Node.js 22.19+ 或 24+、Corepack 和 Yarn 4.18.0: diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 8381256add..b64c134b46 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "dsh-plugin-desktop", - "version": "2.0.6-alpha.2", + "version": "2.0.6-alpha.3", "description": "WorkDSH Electron carrier for a pinned DeepSeek Harness runtime Profile", "license": "MIT", "publishConfig": { diff --git a/apps/desktop/src/enterprise-auth.ts b/apps/desktop/src/enterprise-auth.ts index 94337bc1d5..72c72b3a9d 100644 --- a/apps/desktop/src/enterprise-auth.ts +++ b/apps/desktop/src/enterprise-auth.ts @@ -117,7 +117,8 @@ export async function startEnterpriseAuthority(login: EnterpriseLogin, deviceId: const auth = ['/auth/me', '/api/auth/me'].includes(url.pathname) && req.method === 'GET' && !url.search const ingest = url.pathname === '/visible-sessions/ingest' && ['GET', 'POST', 'DELETE'].includes(req.method ?? '') const collaboration = !url.search && ((req.method === 'GET' && /^\/api\/collaboration\/(?:contract|colleagues|inbox|sent|notifications|(?:materials|handoffs)\/[\w-]{1,160}|handoffs\/[\w-]{1,160}\/messages)$/.test(url.pathname)) || (req.method === 'POST' && /^\/api\/collaboration\/(?:materials|handoffs|notifications\/[\w-]{1,160}\/read|handoffs\/[\w-]{1,160}\/(?:messages|complete))$/.test(url.pathname))); - if (!auth && !ingest && !collaboration) { reject(404, 'Unknown Desktop operation'); return } + const extension = !url.search && ['GET', 'POST'].includes(req.method ?? '') && /^\/api\/extensions\/[a-z][a-z0-9-]{0,63}\/[a-z][a-z0-9-]{0,63}$/.test(url.pathname) + if (!auth && !ingest && !collaboration && !extension) { reject(404, 'Unknown Desktop operation'); return } let current: EnterpriseMember try { current = await login.verify() } catch { reject(401, 'Enterprise authorization unavailable') @@ -128,7 +129,7 @@ export async function startEnterpriseAuthority(login: EnterpriseLogin, deviceId: if (auth) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ...current, deviceId, backendUrl: login.backendUrl })); return } - if (collaboration) { + if (collaboration || extension) { try { let body: string | undefined; if(req.method==='POST') { @@ -139,9 +140,9 @@ export async function startEnterpriseAuthority(login: EnterpriseLogin, deviceId: } const response=await login.request(url.pathname,req.method,body); if(closing)return; - if(!response.ok){reject(response.status,'Enterprise collaboration rejected');return} + if(!response.ok){reject(response.status, extension ? 'Enterprise extension rejected' : 'Enterprise collaboration rejected');return} res.writeHead(200,{'Content-Type':'application/json'});res.end(JSON.stringify(await response.json())); - } catch {reject(502,'Enterprise collaboration unavailable')} + } catch {reject(502, extension ? 'Enterprise extension unavailable' : 'Enterprise collaboration unavailable')} return; } try { diff --git a/apps/desktop/tests/enterprise-auth.spec.ts b/apps/desktop/tests/enterprise-auth.spec.ts index 76ea2aa3a6..4ce6fd0cb8 100644 --- a/apps/desktop/tests/enterprise-auth.spec.ts +++ b/apps/desktop/tests/enterprise-auth.spec.ts @@ -127,3 +127,33 @@ describe('Desktop Main enterprise authority', () => { await expect(login.verify()).rejects.toThrow('已退出') }) }) + + describe('enterprise extension transport', () => { + it('forwards only extension operations with Main-owned authentication and rejects other routes', async () => { + const calls: Array<{url: string; init?: RequestInit}> = []; + let active = true; + const request = (async (url: string | URL | Request, init?: RequestInit) => { + calls.push({url: String(url), ...(init ? {init} : {})}); + if (String(url).endsWith('/login')) return json({token, member: actor}); + if (String(url).endsWith('/me')) return json(actor, active ? 200 : 401); + return json({reports: [1]}); + }) as typeof fetch; + const login = await EnterpriseLogin.login('https://company.test', actor.email, 'password', request); + bridge = await startEnterpriseAuthority(login, 'device-a', () => {}, () => {}); + const headers = {Authorization: `Bearer ${bridge.key}`, 'Content-Type': 'application/json'}; + const response = await fetch(`${bridge.url}/api/extensions/reports/list`, {method: 'POST', headers, body: '{"page":1}'}); + expect(await response.json()).toEqual({reports: [1]}); + const forwarded = calls.find(call => call.url.endsWith('/api/extensions/reports/list'))!; + expect(forwarded.init?.headers).toMatchObject({Authorization: `Bearer ${token}`}); + expect(forwarded.init?.body).toBe('{"page":1}'); + for (const path of ['/api/admin/members', '/api/extensions/reports/list?url=https://evil.test', '/api/extensions/reports/list/extra']) { + expect((await fetch(bridge.url + path, {headers})).status).toBe(404); + } + expect((await fetch(`${bridge.url}/api/extensions/reports/list`, {headers: {...headers, Origin: 'https://evil.test'}})).status).toBe(403); + expect((await fetch(`${bridge.url}/api/extensions/reports/list`, {method: 'DELETE', headers})).status).toBe(404); + expect(calls.filter(call => call.url.includes('/api/extensions/'))).toHaveLength(1); + active = false; + expect((await fetch(`${bridge.url}/api/extensions/reports/list`, {headers})).status).toBe(401); + expect(calls.filter(call => call.url.includes('/api/extensions/'))).toHaveLength(1); + }); + }); diff --git a/docs/ENTERPRISE-PLUGIN-AUTH.md b/docs/ENTERPRISE-PLUGIN-AUTH.md new file mode 100644 index 0000000000..e016bee7b5 --- /dev/null +++ b/docs/ENTERPRISE-PLUGIN-AUTH.md @@ -0,0 +1,49 @@ +# 企业业务插件认证接入 + +企业 Desktop 的业务插件注入 `workdshEnterprise`,复用当前成员登录。服务不返回后台 Token、本机桥接密钥或账号密码。此接口由企业身份插件的 Desktop 入口提供;普通个人空间不提供此服务,页面可提示安装企业连接插件并登录。它不是远程企业 Web Host 的认证接口。 + +## 独立插件调用 + +以下代码在插件的 Host 入口执行,不在浏览器页面直接执行。类型从 `workdsh-contracts/enterprise` 导入,Cordis 版本和 DSH 版本须与当前发行包一致。 + +```ts +import type { Context } from '@deepseek-ai/cordis' +import type { EnterpriseService } from 'workdsh-contracts/enterprise' + +declare module '@deepseek-ai/cordis' { + interface Context { workdshEnterprise: EnterpriseService } +} + +export default { + name: 'company-reports', + inject: ['workdshEnterprise'], + async apply(ctx: Context) { + const member = await ctx.workdshEnterprise.identity() + const reports = await ctx.workdshEnterprise.request<{ items: unknown[] }>({ + plugin: 'reports', + operation: 'list', + method: 'POST', + body: { page: 1 }, + }) + // 将结果交给你的业务服务;不要把身份当作后台授权凭据。 + }, +} +``` + +`workdsh-contracts` 当前是仓库内的私有包,并非已发布 npm SDK。仓库内使用 workspace 依赖;独立项目可构建并打包该包,再使用生成的 tgz 作为类型依赖。不要运行 `npm install workdsh-contracts` 并假设公共 registry 已发布。企业插件运行包按现有 GitHub Releases 独立交付。 + +## 后台接口约定 + +上述请求映射为当前公司后台的 `POST /api/extensions/reports/list`,Main 自动补上 `Authorization: Bearer <当前成员登录凭据>`。后台开发者需要实现这个接口,沿用现有成员认证,并从认证上下文确定组织与成员,再检查具体业务权限。SDK 不自动创建报表后台接口。 + +第一版只接受 GET、POST。插件标识和操作名必须是小写字母开头、后续为小写字母/数字/短横线,长度 1~64;不允许任意 URL、查询字符串或嵌套路径。GET 不携带 body;分页与过滤建议使用 POST JSON。请求体上限 8 MiB,返回 JSON;请求超时 10 秒,无自动重试。写操作如需重试,由业务后台实现幂等键。 + +`plugin` 是路由标识,不是可信的插件身份,也不授予权限。所有已安装 Host 插件可调用这个服务,后台必须依当前成员校验权限,不能凭插件名称、客户端传来的组织 ID 或角色授权。安装插件本身不提供成员资格;插件仍属于可信本机代码,服务不是恶意代码沙箱。 + +## 页面与生命周期 + +业务页面通过自己的 `connection.fetch` 受控接口调用 Host 业务服务,再由业务服务调用 `workdshEnterprise`。不要把 Token 下发到页面,不开放一个让页面任意指定操作和路径的转发接口。页面接口应限定业务操作、校验参数及会话访问权限。 + +服务在每次业务请求前后核验当前成员。退出、撤权、身份变化或桥接关闭时请求拒绝;可传入 AbortSignal 取消请求。不要复制凭据或缓存身份作为持久授权。管理员权限接口不在桥接范围内,公司模型内部 Key 不能替代成员登录。 + +此能力从 Desktop `2.0.6-alpha.3` 与企业连接包 `0.1.0-alpha.2` 开始提供;已发布的 2.0.6-alpha.2 不含该接口,仅升级独立业务插件不能使旧 Desktop 获得扩展桥接能力。 diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 16a652527c..c4ff9b4f96 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -34,6 +34,10 @@ "./projects": { "types": "./dist/projects.d.ts", "default": "./dist/projects.js" + }, + "./enterprise": { + "types": "./dist/enterprise.d.ts", + "default": "./dist/enterprise.js" } }, "files": [ diff --git a/packages/contracts/src/enterprise.ts b/packages/contracts/src/enterprise.ts new file mode 100644 index 0000000000..c19f8b9a52 --- /dev/null +++ b/packages/contracts/src/enterprise.ts @@ -0,0 +1,18 @@ +/** Host-only authenticated enterprise transport. Never exposes backend or bridge credentials. */ +export interface EnterpriseRequest { + plugin: string; + operation: string; + method: 'GET' | 'POST'; + body?: unknown; + signal?: AbortSignal; +} +export interface EnterpriseService { + identity(signal?: AbortSignal): Promise<{ memberId: string; organizationId: string }>; + request(request: EnterpriseRequest): Promise; +} +/** Fixed extension namespace; arbitrary URLs, query strings and administrative routes are forbidden. */ +export function enterpriseExtensionPath(plugin: string, operation: string): string { + const identifier = /^[a-z][a-z0-9-]{0,63}$/; + if (!identifier.test(plugin) || !identifier.test(operation)) throw new Error('Invalid enterprise extension operation'); + return `/api/extensions/${plugin}/${operation}`; +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index a92659e7b1..a8fc72274c 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -3,3 +3,4 @@ export * from './experts.js'; export * from './skill-revisions.js'; export * from './library.js'; export * from './projects.js'; +export * from './enterprise.js'; diff --git a/packages/enterprise-connection/package.json b/packages/enterprise-connection/package.json index b48020c1b4..9bad280bc3 100644 --- a/packages/enterprise-connection/package.json +++ b/packages/enterprise-connection/package.json @@ -1,9 +1,9 @@ { "name": "workdsh-enterprise-connection", - "version": "0.1.0-alpha.1", + "version": "0.1.0-alpha.2", "private": true, "type": "module", - "description": "企业连接:安装后连接公司账号与协作,权限由后台登录验证", + "description": "\u4f01\u4e1a\u8fde\u63a5\uff1a\u5b89\u88c5\u540e\u8fde\u63a5\u516c\u53f8\u8d26\u53f7\u4e0e\u534f\u4f5c\uff0c\u6743\u9650\u7531\u540e\u53f0\u767b\u5f55\u9a8c\u8bc1", "exports": { ".": "./index.mjs", "./client": "./client.browser.js" @@ -32,7 +32,7 @@ } }, "dependencies": { - "workdsh-provider-identity-enterprise": "0.1.0-alpha.2", + "workdsh-provider-identity-enterprise": "0.1.0-alpha.3", "workdsh-plugin-enterprise-collaboration": "0.1.0-alpha.1" }, "bundledDependencies": [ diff --git a/packages/providers/identity-enterprise/README.md b/packages/providers/identity-enterprise/README.md index d8a46c32e0..418d5fa9bc 100644 --- a/packages/providers/identity-enterprise/README.md +++ b/packages/providers/identity-enterprise/README.md @@ -40,3 +40,7 @@ 构建和类型检查必须覆盖本包以及共同 access;身份测试覆盖后台认证、固定成员、同步重试和删除、过期和撤权拒绝、认证文件保护以及 process 生命周期。通过源码和 headless 检查后,仍需分别验收企业服务器部署与 Desktop 安装、登录、退出、重启、升级及 Windows/macOS 图形行为。模型和工具任务须通过实际授权的完整官方客户端验收;文件目录或 Profile 名称不能替代身份与资源授权。 共享功能与交付要求见 [功能开发契约](../../../apps/web/docs/FEATURE-DEVELOPMENT-CONTRACT.md)、[企业需求](../../../apps/web/docs/ENTERPRISE-REQUIREMENTS.md) 和 [验收要求](../../../apps/web/docs/ACCEPTANCE.md)。 + +## 独立业务插件认证 + +Desktop 身份入口提供 `workdshEnterprise` 服务,业务插件通过 Cordis 注入,使用 `identity()` 与 `request()` 调用当前公司后台。凭据留在 Main,不读取 Token;允许的路径为 `/api/extensions//`。完整代码、后台权限要求及版本限制见[企业插件认证接入](../../../docs/ENTERPRISE-PLUGIN-AUTH.md)。 diff --git a/packages/providers/identity-enterprise/package.json b/packages/providers/identity-enterprise/package.json index 9842d88c17..c8fd99dc25 100644 --- a/packages/providers/identity-enterprise/package.json +++ b/packages/providers/identity-enterprise/package.json @@ -1,6 +1,6 @@ { "name": "workdsh-provider-identity-enterprise", - "version": "0.1.0-alpha.2", + "version": "0.1.0-alpha.3", "private": true, "type": "module", "exports": { diff --git a/packages/providers/identity-enterprise/src/desktop-authority.ts b/packages/providers/identity-enterprise/src/desktop-authority.ts index e3a3a1c84f..d9d0a291e7 100644 --- a/packages/providers/identity-enterprise/src/desktop-authority.ts +++ b/packages/providers/identity-enterprise/src/desktop-authority.ts @@ -1,3 +1,4 @@ +import type { EnterpriseRequest } from 'workdsh-contracts'; import { readProtectedFile } from './protected-file.js'; import { isAbsolute } from 'node:path'; import type { VerifiedMember } from './request-context.js'; @@ -102,6 +103,16 @@ export class DesktopAuthority { const value = await this.request(path, method, body, signal); await this.account(signal); return value; } + async extension(input: EnterpriseRequest): Promise { + const identifier = /^[a-z][a-z0-9-]{0,63}$/; + if (!identifier.test(input.plugin) || !identifier.test(input.operation)) throw new Error('Invalid enterprise extension operation'); + const path = `/api/extensions/${input.plugin}/${input.operation}`; + if (!['GET', 'POST'].includes(input.method) || (input.method === 'GET' && input.body !== undefined)) throw new Error('Invalid enterprise extension request'); + await this.account(input.signal); + const value = await this.request(path, input.method, input.body, input.signal); + await this.account(input.signal); + return value as T; + } async collaborationBinding(signal?: AbortSignal): Promise<{url:string;authorization:string}> { await this.account(signal); const binding = await this.credentials(); diff --git a/packages/providers/identity-enterprise/src/desktop.ts b/packages/providers/identity-enterprise/src/desktop.ts index fc53566a05..f57503f8c5 100644 --- a/packages/providers/identity-enterprise/src/desktop.ts +++ b/packages/providers/identity-enterprise/src/desktop.ts @@ -1,5 +1,5 @@ import { Service, type Context } from '@deepseek-ai/cordis'; -import type { IdentityResolutionContext, IdentityService } from 'workdsh-contracts'; +import type { EnterpriseService, EnterpriseRequest, IdentityResolutionContext, IdentityService } from 'workdsh-contracts'; import type {} from '@deepseek-ai/dsh-client-connection'; import type {} from '@deepseek-ai/dsh-api-session-controller'; import { EnterpriseMemberIdentity } from './member-identity.js'; @@ -7,7 +7,7 @@ import { DesktopAuthority, type EnterpriseDesktopConfig } from './desktop-author import { DesktopBodySync, observeDesktopSessions } from './desktop-body-sync.js'; export type { EnterpriseDesktopConfig } from './desktop-authority.js'; -declare module '@deepseek-ai/cordis' { interface Context { workdshIdentity: IdentityService } } +declare module '@deepseek-ai/cordis' { interface Context { workdshIdentity: IdentityService; workdshEnterprise: EnterpriseService } } type SessionAccess = { inspect: Context['sessionController']['inspect'] }; /** Explicit enterprise Desktop composition. It cannot authenticate as a personal identity. */ @@ -22,6 +22,8 @@ export default class EnterpriseDesktopIdentity extends Service implements Identi constructor(ctx: Context, config: EnterpriseDesktopConfig) { super(ctx, 'workdshIdentity'); this.authority = new DesktopAuthority(Object.freeze({ ...config })); + ctx.plugin(EnterpriseTransport, this); + ctx.effect(() => () => { this.identity?.revoke(); this.authority.close(); this.sync?.close(); }, 'workdsh.enterprise-desktop.authority'); const register = (path: string, methods: Array<'GET' | 'POST'>, fetch: (request: Request) => Promise) => { ctx.effect(() => ctx.connection.fetch.register({ path, methods, requestBody: 'buffered', fetch }), `workdsh.enterprise-desktop.${path}`); @@ -66,6 +68,14 @@ export default class EnterpriseDesktopIdentity extends Service implements Identi } catch { this.syncError = '正文同步初始化失败;请检查企业认证及受保护的本机同步文件。'; } }); } + async enterpriseIdentity(signal?: AbortSignal) { + await this.resolve(undefined, signal); + return { memberId: this.authority.config.principalId, organizationId: this.authority.config.organizationId }; + } + async enterpriseRequest(input: EnterpriseRequest): Promise { + await this.resolve(undefined, input.signal); + return this.authority.extension(input); + } collaborationBinding(signal?: AbortSignal) { return this.authority.collaborationBinding(signal); } profile() { if (!this.identity) throw new Error('Enterprise Desktop authentication required'); return this.identity.profile(); } membership(organizationId: string, principalId: string) { return this.identity?.membership(organizationId, principalId); } @@ -74,3 +84,10 @@ export default class EnterpriseDesktopIdentity extends Service implements Identi return this.identity.resolve(evidence, signal); } } + +/** Public plugin dependency. Lifecycle is scoped to the installed enterprise identity. */ +class EnterpriseTransport extends Service implements EnterpriseService { + constructor(ctx: Context, private readonly owner: EnterpriseDesktopIdentity) { super(ctx, 'workdshEnterprise'); } + identity(signal?: AbortSignal) { return this.owner.enterpriseIdentity(signal); } + request(input: EnterpriseRequest): Promise { return this.owner.enterpriseRequest(input); } +} diff --git a/packages/providers/identity-enterprise/tests/desktop.test.mjs b/packages/providers/identity-enterprise/tests/desktop.test.mjs index 92f373d2bd..d5b8e1f1e4 100644 --- a/packages/providers/identity-enterprise/tests/desktop.test.mjs +++ b/packages/providers/identity-enterprise/tests/desktop.test.mjs @@ -28,6 +28,7 @@ async function fixture() { if (!state.active) return send({}, 401); if (request.url === '/auth/me') return send({ id: state.member, organizationId: state.organization, deviceId: state.device, backendUrl: state.backend, role: state.role, mustChangePassword: state.mustChangePassword, displayName: 'Member A', organizationName: 'Company A', email: 'a@company.example', unexpected: 'not-relayed' }); + if (request.url === '/api/extensions/reports/list') return send({ reports: ['demo'] }); if (!request.url.startsWith('/visible-sessions/ingest')) return send({}, 404); const id = new URL(request.url, 'http://127.0.0.1').searchParams.get('sessionId'); if (request.method === 'DELETE') { @@ -212,3 +213,21 @@ test('lost deletion acknowledgement persists a delete intent, pauses upload and assert.equal(events.length, 2); } finally { sync?.close(); f.authority.close(); await f.close(); } }); + +test('independent plugin injects public enterprise service without accessing credentials', async () => { + const f = await fixture(); const ctx = new Context(); + ctx.provide('connection', { fetch: { register() { return () => {}; } } }); + try { + await ctx.plugin(Identity, f.config); + let result; + await ctx.plugin({inject: ['workdshEnterprise'], async apply(consumer) { + assert.deepEqual(await consumer.workdshEnterprise.identity(), {memberId: 'member-a', organizationId: 'org-a'}); + result = await consumer.workdshEnterprise.request({plugin: 'reports', operation: 'list', method: 'GET'}); + await assert.rejects(consumer.workdshEnterprise.request({plugin: '../auth', operation: 'me', method: 'GET'})); + await assert.rejects(consumer.workdshEnterprise.request({plugin: 'reports', operation: 'list', method: 'GET', body: {}})); + }}); + assert.deepEqual(result, {reports: ['demo']}); + f.state.active = false; + await assert.rejects(ctx.workdshEnterprise.request({plugin: 'reports', operation: 'list', method: 'GET'})); + } finally { await ctx.fiber.dispose(); await f.close(); } +});