hiwork-core
v0.0.30
Published
HiWork 企业统一登录与模型自动供给插件(ai-router 网关登录,Web 表单,无 CLI 认证)
Readme
hiwork-core
HiWork 的企业统一登录与模型自动供给插件,基于 DeepSeek Harness(DSH)插件机制实现,随 HiWork 桌面端打包分发。
员工在登录页用企业 LDAP 账号登录企业 AI 网关(ai-router),插件自动拉取该账号绑定的虚拟 Key 与预算内模型,写入凭据库与 llm-pi-ai 配置 —— Models 页即刻出现该员工可用的模型,请求经网关统一计费。全程零手动模型配置,登录面为 Web 表单(无 CLI 认证)。
- 完整设计:docs/superpowers/specs/2026-08-19-hiwork-core-design.md
- 对齐的 DSH 版本与升级核对表:docs/DSH-VERSION.md
- 目标网关:自托管 ai-router,
/api/me自助组 +/v1OpenAI 兼容代理(默认走/v1/responses)
架构一瞥
login gate / settings card(client)──credentials.set / settings.mutate──▶ Host onChange(server)
▶ POST /api/admin/auth/login → GET /api/me/keys + GET /api/me/key?key_id=
▶ 写 AI_ROUTER_API_KEY + session 凭据,写 llm-pi-ai.providers.<协议路由>
▶ pi-ai 热重载 → Models 页出现模型 → 请求走网关复用 @deepseek-ai/dsh-llm-pi-ai 的手工路由,不新写 LLM 适配器。
多协议路由(协议随模型走)
网关按「入站协议 + 模型」匹配模型映射(GET /api/me/key 的 model_mapping),而 pi-ai 的 api 是路由级的 —— 因此插件把预算内模型按协议拆到路由,每路由一个协议、baseURL 按协议取对。路由在模型选择器里的分组标题用「线路C/M/R」,后缀即该路由讲的协议首字母:
| 路由 | 展示名 | 协议 | baseURL 后缀 | 说明 |
|---|---|---|---|---|
| providers.ai-router-chat | 线路C | openai-completions | …/v1(SDK 追加 /chat/completions) | chat/completions,兜底协议 |
| providers.ai-router-anthropic | 线路M | anthropic-messages | 裸 base | Anthropic SDK 自带 v1/ 前缀 |
| providers.ai-router | 线路R | openai-responses | …/v1(SDK 追加 /responses) | key 与协议保持不变,兼容 agent-default-model: provider: ai-router |
- 归属规则:精确 EqualFold 优先、
*/?通配(裸*兜底不参与归属);模型进入每一条映射命中的线路 —— 按协议分组、不做唯一归属,同一模型可以同时出现在多条线路里(例如同时声明 responses 与 anthropic 映射的模型,线路R 和线路M 都列出它),从哪条线路选它,请求就走那条线路的协议;无任何映射命中的模型落兜底线路C(网关的裸*兜底映射通常声明在 chat/completions 下,用 responses 入站反而匹配不到映射)。 - 好处:DSH 里切换模型自动落到对应协议的路由;responses 系模型经
https://aigateway.hivery.cn/v1/responses;anthropic 系(如 k3/k3-256k/kimi-for-coding)不再 404(/v1/v1/messages)也不用手动整路切协议;只声明 chat/completions 映射的模型落线路C,不再被塞进 responses 入站。 - 空线路不写入 profile(写入侧同时清掉该路由,旧版独立的
ai-router-responses路由也会被清理)。 - 官方 DeepSeek 提供方在本 bundle 内停用:
cordis.patch.yml用- id: llm-deepseek+disabled: true关掉deepseek-official。官方适配器讲 DeepSeek 私有协议(请求体带thinking、响应读reasoning_content、图片走 Files API),网关只实现 OpenAI/Anthropic 兼容协议,改 baseURL 也对接不上,留着只会让模型选择器多出一个必然失败的分组。需要保留官方的部署,在 profile 的cordis.patch.yml里写同 id +disabled: false覆盖(profile 层优先级高于 bundle 层)。 - 官方 web 搜索同样停用:
web-search-deepseek也走DEEPSEEK_API_KEY(企业用户没有),故- id: web-search-deepseek+disabled: true停用提供方(设置页「Web 搜索」卡片随命名空间消失),并把tool-web的search关掉,只保留不依赖凭据的web_fetch。 - 默认模型校正:
agent-default-model的组合默认值指向官方提供方(deepseek-official/deepseek-v4-flash)。供给成功后(登录/刷新)校正一次:当前选择仍可路由(本插件线路内查表,或其它已注册提供方 + 模型仍在列)就保持不变,否则落到兜底线路C 的第一个模型,避免新会话第一次请求就UNKNOWN_MODEL。只写用户层、不写 composition,用户自己选的模型只要仍可用就不会被覆盖。
登录门禁
- 打开即检测登录(硬门禁):页面加载后检测登录状态,未登录时全屏登录窗覆盖整个应用(不可交互、不可聚焦,防隐私泄漏),登录成功才放行;不可跳过。
- 登录页为 HiWork 品牌视觉:左侧品牌标语「智联万物,从智联工作开始。」,右侧企业账号登录卡。
- 网关地址固定为
https://aigateway.hivery.cn(无需、也不提供用户填写)。 - 设置页「AI Router 网关」为一级 tab,可刷新模型 / 退出登录。
- 多虚拟 KEY 支持:一个账号绑定多把虚拟 Key 时,已登录页显示 KEY 选择器(掩码明文 + 团队名),切换后自动按新 Key 重拉额度与模型。
- 已下架模型自动隐藏:网关
/api/me/key的model_status为inactive(目录已下架、不可调用)时,插件直接隐藏该模型——额度展示列表与 Models 页可用模型均不含它;重新上架后下次刷新自动恢复。旧网关缺省该字段,行为不变。
分发与开发
本插件为 HiWork 内部项目,随桌面端离线打包,版本在 hiwork-desktop/src/bundled-plugins.ts 的 BUNDLED_PLUGINS 中锁定;scripts/publish-to-desktop.mjs 负责把构建产物同步进桌面端离线仓库。
要求 Node.js 22+ 与 pnpm:
pnpm install
pnpm build # host(tsc + host-build)+ client(tsc + esbuild 单文件 bundle)
pnpm test # vitest客户端 bundle 由 scripts/client-build.mjs 用 esbuild 打成单文件:@deepseek-ai/* 与 react 走宿主模块表 external,品牌图片内联为 data URL。
Feature Center(扩展注册面)
hiwork-core 的 client 半边提供 cordis 服务 hiworkFeatureCenter,把左侧 HiWork
菜单与中央内容区做成可复用机制:业务插件(hiwork-automation、未来的
hiwork-knowledge)只注册一个 feature,即可出现在菜单里并在中央区独立渲染,
不需要再碰侧栏 DOM 或中央区状态机。
interface HiWorkFeature {
readonly id: string // 稳定 id,同时写入 data-hiwork-active-page
readonly label: () => string // 函数形式,跟随语言切换
readonly icon?: string // 内联 SVG 字符串
readonly order?: number // 缺省 1000,按 order → id 稳定排序
readonly render: (context: FeatureRenderContext) => React.ReactNode
}
interface FeatureRenderContext {
readonly openNativeSession: (sessionId: string) => void // 先退出 takeover,再交给宿主
readonly openNativeHome: () => void
}
interface HiWorkCoreClientFace {
readonly featureRegistry: FeatureRegistry // register / list / activeId / open / openNative / subscribe
readonly openFeature: (id: string) => boolean
readonly openNativeSession: (sessionId: string) => void
readonly openNativeHome: () => void
}业务插件按服务名发现本面(不 runtime-import 本包的值):
ctx.inject(['hiworkFeatureCenter'], (scope) => {
const face = scope.get('hiworkFeatureCenter') as HiWorkCoreClientFace
ctx.effect(() => face.featureRegistry.register({
id: 'knowledge',
label: () => '知识库',
order: 30,
render: (context) => React.createElement(KnowledgeView, { openSession: context.openNativeSession }),
}), 'hiwork-knowledge: feature center')
})机制要点(实现见 src/client/feature-registry.ts 与 src/client/feature-center.ts):
- 侧栏菜单注入:隐藏原生「新会话」按钮(保留 DOM 与 click handler,只加隐藏属性),
在同位置插入 HiWork 菜单;首项「新会话」直接代理原生按钮(图标克隆原生按钮的 SVG),
其余项按 order 排序。
icon是内联 SVG 字符串;缺省用兜底图标,保证侧栏折叠成 56px 图标栏时每个入口仍有可辨识图形(折叠态由宿主加在侧栏根上的*_collapsed类驱动纯 CSS:隐藏文字、图标放大到 18px,与原生控件同形)。 折叠态悬停 500ms 弹出提示气泡,黑底白字、圆角与字号逐项对齐框架ui-primitives的Tooltip(--dsw-alias-tooltip-bg),取代浏览器自带的浅色title提示; 展开态文字已可见,与原生控件一致(Tooltip disabled={wide})不出气泡。 找不到原生按钮时不抛错、不隐藏任何入口,菜单只列 feature(安全降级)。 - 中央区 takeover:挂一个
[data-hiwork-feature-root]React root,用html[data-hiwork-active-page]上的纯 CSS 规则隐藏原生会话子树(保持挂载、保留状态); 点击任意原生会话行或调用openNativeSession时,先卸载 root、移除属性恢复原生内容, 再交给宿主。切换 feature 先卸载旧 root,任意时刻只有一个业务页面。 takeover root 自带background: var(--dsw-alias-bg-base)——与原生会话根同一个主题变量, 否则会透出框架灰底、与侧栏同色而不是原生页面的底色。 - 自愈与清理:
MutationObserver+ 有界重试处理延迟挂载、语言切换与壳重建; render 抛错由错误边界捕获并恢复原生会话区;注入节点带实例令牌,HMR/重复 apply 不会出现两份菜单,旧实例的 dispose 也不会删掉新实例的节点。 - Host 纯度:全部 DOM 逻辑只在
src/client/**,Host build 不引用document。
目录结构
- src/index.ts:宿主入口,登录编排、凭据写入与依赖接口。
- src/client/feature-registry.ts:扩展注册表(排序、校验、活动页)。
- src/client/feature-center.ts:侧栏菜单注入 + 中央区 takeover。
- src/client/contracts.ts:业务插件可见的 client 面与服务名。
- src/orchestration.ts:登录后模型供给编排(拉 Key、写路由、热重载)。
- src/office-credentials.ts:OnlyOffice 集成凭据的 host 侧服务
(
hiworkOfficeCredentials)——hiwork-office 按登录态经它取 Document Server 地址与共享 JWT 密钥(网关GET /api/me/office-config),终端零配置;网关会话 cookie 不出 Host。 - src/office-relay.ts:文档中继的 host 侧服务(
hiworkOfficeRelay)—— 跨 NAT 的在线编辑:插件把文档暂存到 DS 能访问的服务器侧(上传/轮询/取回/关闭), 用户侧不再需要入向可达性;同样只在 Host 持 cookie、失败用稳定信封。 - src/identity.ts:当前登录身份的只读 host 服务(
hiworkIdentity)—— 业务插件(hiwork-memory 的记忆隔离键等)经ctx.get('hiworkIdentity')取{ loggedIn, subject, username, displayName, employeeName, teamName };subject是 账号名的 sha256 前 16 位(u_前缀,不含账号原文),未登录回{ loggedIn: false }。 - src/memory-config.ts:记忆系统配置下发的只读 host 服务(
hiworkMemory)—— hiwork-memory 经ctx.get('hiworkMemory').config()取网关下发的panelUrl/serviceId/teamIds/apiKey(网关GET /api/me/memory-config;snake_case → camelCase 的转换只在 Host 做), 成功结果 5 分钟内存缓存、force可绕过;凭据只在内存里活、不落盘、不进日志。 - src/default-model.ts:供给成功后校正
agent-default-model(默认模型落点)。 - src/client/login-gate.tsx:全屏登录门禁页。
- src/client/section.tsx:设置页「AI Router 网关」卡片。
- src/client/user-badge.tsx:已登录身份与 KEY 选择器。
