hiwork-core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # hiwork-core
2
+
3
+ HiWork 的企业统一登录与模型自动供给插件,基于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)插件机制实现,随 HiWork 桌面端打包分发。
4
+
5
+ 员工在登录页用企业 LDAP 账号登录企业 AI 网关(ai-router),插件自动拉取该账号绑定的虚拟 Key 与预算内模型,写入凭据库与 `llm-pi-ai` 配置 —— Models 页即刻出现该员工可用的模型,请求经网关统一计费。**全程零手动模型配置,登录面为 Web 表单(无 CLI 认证)。**
6
+
7
+ - 完整设计:[docs/superpowers/specs/2026-08-19-hiwork-core-design.md](docs/superpowers/specs/2026-08-19-hiwork-core-design.md)
8
+ - 对齐的 DSH 版本与升级核对表:[docs/DSH-VERSION.md](docs/DSH-VERSION.md)
9
+ - 目标网关:自托管 ai-router,`/api/me` 自助组 + `/v1` OpenAI 兼容代理(默认走 `/v1/responses`)
10
+
11
+ ## 架构一瞥
12
+
13
+ ```
14
+ login gate / settings card(client)──credentials.set / settings.mutate──▶ Host onChange(server)
15
+ ▶ POST /api/admin/auth/login → GET /api/me/keys + GET /api/me/key?key_id=
16
+ ▶ 写 AI_ROUTER_API_KEY + session 凭据,写 llm-pi-ai.providers.<协议路由>
17
+ ▶ pi-ai 热重载 → Models 页出现模型 → 请求走网关
18
+ ```
19
+
20
+ 复用 `@deepseek-ai/dsh-llm-pi-ai` 的手工路由,不新写 LLM 适配器。
21
+
22
+ ### 多协议路由(协议随模型走)
23
+
24
+ 网关按「入站协议 + 模型」匹配模型映射(`GET /api/me/key` 的 `model_mapping`),而 pi-ai 的 `api` 是**路由级**的 —— 因此插件把预算内模型按映射拆到协议路由,每路由一个协议、baseURL 按协议取对:
25
+
26
+ | 路由 | 协议 | baseURL 后缀 | 说明 |
27
+ |---|---|---|---|
28
+ | `providers.ai-router` | `openai-responses` | `…/v1`(SDK 追加 `/responses`) | 主路由(兼容 `agent-default-model: provider: ai-router`),默认请求走 `/v1/responses` |
29
+ | `providers.ai-router-anthropic` | `anthropic-messages` | 裸 base | Anthropic SDK 自带 `v1/` 前缀 |
30
+
31
+ - 归属规则:精确 EqualFold 优先、`*`/`?` 通配(裸 `*` 兜底不参与归属);OpenAI 兼容映射(chat/responses)统一落主路由(responses),anthropic 映射落 anthropic 路由;无映射命中的模型落主路由(默认 responses)。
32
+ - 好处:DSH 里切换模型自动落到对应协议的路由;默认 DeepSeek 模型经 `https://aigateway.hivery.cn/v1/responses`;anthropic 系(如 k3/k3-256k/kimi-for-coding)不再 404(`/v1/v1/messages`)也不用手动整路切协议。
33
+
34
+ ## 登录门禁
35
+
36
+ - **打开即检测登录(硬门禁)**:页面加载后检测登录状态,未登录时全屏登录窗覆盖整个应用(不可交互、不可聚焦,防隐私泄漏),登录成功才放行;不可跳过。
37
+ - 登录页为 HiWork 品牌视觉:左侧品牌标语「智联万物,从智联工作开始。」,右侧企业账号登录卡。
38
+ - 网关地址固定为 `https://aigateway.hivery.cn`(无需、也不提供用户填写)。
39
+ - 设置页「**AI Router 网关**」为**一级 tab**,可刷新模型 / 退出登录。
40
+ - **多虚拟 KEY 支持**:一个账号绑定多把虚拟 Key 时,已登录页显示 KEY 选择器(掩码明文 + 团队名),切换后自动按新 Key 重拉额度与模型。
41
+ - **已下架模型自动隐藏**:网关 `/api/me/key` 的 `model_status` 为 `inactive`(目录已下架、不可调用)时,插件直接隐藏该模型——额度展示列表与 Models 页可用模型均不含它;重新上架后下次刷新自动恢复。旧网关缺省该字段,行为不变。
42
+
43
+ ## 分发与开发
44
+
45
+ 本插件为 HiWork 内部项目,随桌面端离线打包,版本在 [hiwork-desktop/src/bundled-plugins.ts](../hiwork-desktop/src/bundled-plugins.ts) 的 `BUNDLED_PLUGINS` 中锁定;`scripts/publish-to-desktop.mjs` 负责把构建产物同步进桌面端离线仓库。
46
+
47
+ 要求 Node.js 22+ 与 pnpm:
48
+
49
+ ```sh
50
+ pnpm install
51
+ pnpm build # host(tsc + host-build)+ client(tsc + esbuild 单文件 bundle)
52
+ pnpm test # vitest
53
+ ```
54
+
55
+ 客户端 bundle 由 `scripts/client-build.mjs` 用 esbuild 打成单文件:`@deepseek-ai/*` 与 react 走宿主模块表 external,品牌图片内联为 data URL。
56
+
57
+ ## 目录结构
58
+
59
+ - [src/index.ts](src/index.ts):宿主入口,登录编排、凭据写入与依赖接口。
60
+ - [src/orchestration.ts](src/orchestration.ts):登录后模型供给编排(拉 Key、写路由、热重载)。
61
+ - [src/client/login-gate.tsx](src/client/login-gate.tsx):全屏登录门禁页。
62
+ - [src/client/section.tsx](src/client/section.tsx):设置页「AI Router 网关」卡片。
63
+ - [src/client/user-badge.tsx](src/client/user-badge.tsx):已登录身份与 KEY 选择器。
Binary file
Binary file
@@ -0,0 +1,12 @@
1
+ # hiwork-core bundle:叠加进 profile 后挂载网关插件。
2
+ # 用法:profile 的 package.json 依赖本包,dsh.profile.bundles 追加 "hiwork-core"。
3
+ #
4
+ # 注意:不在这里挂 llm-pi-ai —— 标准 profile 的 dsh-base 已挂载它(dormant),
5
+ # 重复 insert 同一 id 会在 loader 建 EntryGroup 时报 "duplicate loader entry id"。
6
+ # hiwork-core 写入的正是 llm-pi-ai 的配置段;若部署缺 llm-pi-ai,写配置会 fail loud。
7
+ - insert:
8
+ - id: hiwork-core
9
+ name: 'hiwork-core'
10
+ config:
11
+ # 企业网关地址固定配置,不提供用户填写入口。
12
+ baseURL: 'https://aigateway.hivery.cn'
@@ -0,0 +1,101 @@
1
+ /** 网关错误:携带稳定 code,供编排层映射。 */
2
+ export class GatewayError extends Error {
3
+ code;
4
+ status;
5
+ constructor(code, message, status) {
6
+ super(message);
7
+ this.code = code;
8
+ this.status = status;
9
+ this.name = 'GatewayError';
10
+ }
11
+ }
12
+ /** fetch + 解析 ai-router HTTP 面。零 dsh 依赖。 */
13
+ export class AiRouterClient {
14
+ baseURL;
15
+ timeoutMs;
16
+ constructor(baseURL, timeoutMs = 10_000) {
17
+ this.baseURL = baseURL;
18
+ this.timeoutMs = timeoutMs;
19
+ }
20
+ async login(username, password) {
21
+ const res = await this.request('/api/admin/auth/login', { method: 'POST', body: { username, password } });
22
+ const sid = extractSessionId(res.headers);
23
+ if (res.status === 429)
24
+ throw new GatewayError('RATE_LIMITED', '登录尝试过于频繁,请稍后再试', 429);
25
+ // 仅 401 视为确定性认证失败(编排层据此清除记住的密码);其余非 2xx 一律按网关不可达处理,
26
+ // 避免 5xx/反代错误页被误报成密码错误而作废记住的密码。
27
+ if (res.status === 401)
28
+ throw new GatewayError('AUTH_FAILED', '用户名或密码错误', 401);
29
+ if (!res.ok)
30
+ throw new GatewayError('GATEWAY_UNREACHABLE', `网关返回错误(HTTP ${res.status})`, res.status);
31
+ if (sid === undefined)
32
+ throw new GatewayError('GATEWAY_UNREACHABLE', '网关登录成功但未返回会话 Cookie', res.status);
33
+ const user = await res.json();
34
+ return { sessionCookie: `session_id=${sid}`, user };
35
+ }
36
+ async getMyKey(sessionCookie, keyId) {
37
+ const qs = keyId === undefined ? '' : `?key_id=${encodeURIComponent(keyId)}`;
38
+ const res = await this.request(`/api/me/key${qs}`, { method: 'GET', sessionCookie });
39
+ if (res.status === 401)
40
+ throw new GatewayError('SESSION_EXPIRED', '网关会话已过期,请重新登录', 401);
41
+ if (res.status === 403) {
42
+ throw new GatewayError('NO_KEY_ASSIGNED', '当前账号尚未被分配虚拟 KEY,请联系管理员分配 KEY 后再登录/刷新', 403);
43
+ }
44
+ if (res.status === 400) {
45
+ throw new GatewayError('KEY_INVALID', '所选 KEY 无效或已被删除,请重新选择 KEY 后刷新', 400);
46
+ }
47
+ if (!res.ok)
48
+ throw new GatewayError('GATEWAY_UNREACHABLE', `网关返回错误(HTTP ${res.status})`, res.status);
49
+ return await res.json();
50
+ }
51
+ /** GET /api/me/keys:当前用户全部虚拟 Key 的轻量列表(多 Key 选择器数据源)。 */
52
+ async getMyKeys(sessionCookie) {
53
+ const res = await this.request('/api/me/keys', { method: 'GET', sessionCookie });
54
+ if (res.status === 401)
55
+ throw new GatewayError('SESSION_EXPIRED', '网关会话已过期,请重新登录', 401);
56
+ if (res.status === 403) {
57
+ throw new GatewayError('NO_KEY_ASSIGNED', '当前账号尚未被分配虚拟 KEY,请联系管理员分配 KEY 后再登录/刷新', 403);
58
+ }
59
+ if (!res.ok)
60
+ throw new GatewayError('GATEWAY_UNREACHABLE', `网关返回错误(HTTP ${res.status})`, res.status);
61
+ return await res.json();
62
+ }
63
+ async logout(sessionCookie) {
64
+ try {
65
+ await this.request('/api/admin/auth/logout', { method: 'POST', sessionCookie });
66
+ }
67
+ catch {
68
+ // 登出 best-effort:网络失败不阻塞本地清理。
69
+ }
70
+ }
71
+ async request(path, opts) {
72
+ const headers = { 'user-agent': 'hiwork-core/0.1.14' };
73
+ if (opts.body !== undefined)
74
+ headers['content-type'] = 'application/json';
75
+ if (opts.sessionCookie !== undefined)
76
+ headers['cookie'] = opts.sessionCookie;
77
+ const init = {
78
+ method: opts.method,
79
+ headers,
80
+ signal: AbortSignal.timeout(this.timeoutMs),
81
+ };
82
+ if (opts.body !== undefined)
83
+ init.body = JSON.stringify(opts.body);
84
+ return fetch(`${this.baseURL}${path}`, init);
85
+ }
86
+ }
87
+ /**
88
+ * 从 Set-Cookie 头解析 session_id:遍历全部 Set-Cookie(网关可能先下发 CSRF 等其他 cookie);
89
+ * 旧运行时回退 get('set-cookie') 时可能拿到逗号合并串,故分隔符同时接受 `,` 与 `;`。
90
+ */
91
+ export function extractSessionId(headers) {
92
+ const raws = typeof headers.getSetCookie === 'function'
93
+ ? headers.getSetCookie()
94
+ : [headers.get('set-cookie')].filter((raw) => raw !== null);
95
+ for (const raw of raws) {
96
+ const match = /(?:^|[,;]\s*)session_id=([^;]+)/.exec(raw);
97
+ if (match !== null)
98
+ return match[1];
99
+ }
100
+ return undefined;
101
+ }
@@ -0,0 +1 @@
1
+ export {};