hiwork-core 0.0.1 → 0.0.3

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 CHANGED
@@ -54,9 +54,76 @@ pnpm test # vitest
54
54
 
55
55
  客户端 bundle 由 `scripts/client-build.mjs` 用 esbuild 打成单文件:`@deepseek-ai/*` 与 react 走宿主模块表 external,品牌图片内联为 data URL。
56
56
 
57
+ ## Feature Center(扩展注册面)
58
+
59
+ `hiwork-core` 的 client 半边提供 cordis 服务 `hiworkFeatureCenter`,把左侧 HiWork
60
+ 菜单与中央内容区做成可复用机制:业务插件(`hiwork-automation`、未来的
61
+ `hiwork-knowledge`)只注册一个 feature,即可出现在菜单里并在中央区独立渲染,
62
+ 不需要再碰侧栏 DOM 或中央区状态机。
63
+
64
+ ```ts
65
+ interface HiWorkFeature {
66
+ readonly id: string // 稳定 id,同时写入 data-hiwork-active-page
67
+ readonly label: () => string // 函数形式,跟随语言切换
68
+ readonly icon?: string // 内联 SVG 字符串
69
+ readonly order?: number // 缺省 1000,按 order → id 稳定排序
70
+ readonly render: (context: FeatureRenderContext) => React.ReactNode
71
+ }
72
+ interface FeatureRenderContext {
73
+ readonly openNativeSession: (sessionId: string) => void // 先退出 takeover,再交给宿主
74
+ readonly openNativeHome: () => void
75
+ }
76
+ interface HiWorkCoreClientFace {
77
+ readonly featureRegistry: FeatureRegistry // register / list / activeId / open / openNative / subscribe
78
+ readonly openFeature: (id: string) => boolean
79
+ readonly openNativeSession: (sessionId: string) => void
80
+ readonly openNativeHome: () => void
81
+ }
82
+ ```
83
+
84
+ 业务插件按服务名发现本面(**不** runtime-import 本包的值):
85
+
86
+ ```ts
87
+ ctx.inject(['hiworkFeatureCenter'], (scope) => {
88
+ const face = scope.get('hiworkFeatureCenter') as HiWorkCoreClientFace
89
+ ctx.effect(() => face.featureRegistry.register({
90
+ id: 'knowledge',
91
+ label: () => '知识库',
92
+ order: 30,
93
+ render: (context) => React.createElement(KnowledgeView, { openSession: context.openNativeSession }),
94
+ }), 'hiwork-knowledge: feature center')
95
+ })
96
+ ```
97
+
98
+ 机制要点(实现见 [src/client/feature-registry.ts](src/client/feature-registry.ts) 与
99
+ [src/client/feature-center.ts](src/client/feature-center.ts)):
100
+
101
+ - **侧栏菜单注入**:隐藏原生「新会话」按钮(保留 DOM 与 click handler,只加隐藏属性),
102
+ 在同位置插入 HiWork 菜单;首项「新会话」直接代理原生按钮(图标克隆原生按钮的 SVG),
103
+ 其余项按 order 排序。`icon` 是内联 SVG 字符串;缺省用兜底图标,保证侧栏折叠成
104
+ 56px 图标栏时每个入口仍有可辨识图形(折叠态由宿主加在侧栏根上的 `*_collapsed`
105
+ 类驱动纯 CSS:隐藏文字、图标放大到 18px,与原生控件同形)。
106
+ 折叠态悬停 500ms 弹出提示气泡,黑底白字、圆角与字号逐项对齐框架 `ui-primitives`
107
+ 的 `Tooltip`(`--dsw-alias-tooltip-bg`),取代浏览器自带的浅色 `title` 提示;
108
+ 展开态文字已可见,与原生控件一致(`Tooltip disabled={wide}`)不出气泡。
109
+ 找不到原生按钮时不抛错、不隐藏任何入口,菜单只列 feature(安全降级)。
110
+ - **中央区 takeover**:挂一个 `[data-hiwork-feature-root]` React root,用
111
+ `html[data-hiwork-active-page]` 上的纯 CSS 规则隐藏原生会话子树(保持挂载、保留状态);
112
+ 点击任意原生会话行或调用 `openNativeSession` 时,先卸载 root、移除属性恢复原生内容,
113
+ 再交给宿主。切换 feature 先卸载旧 root,任意时刻只有一个业务页面。
114
+ takeover root 自带 `background: var(--dsw-alias-bg-base)`——与原生会话根同一个主题变量,
115
+ 否则会透出框架灰底、与侧栏同色而不是原生页面的底色。
116
+ - **自愈与清理**:`MutationObserver` + 有界重试处理延迟挂载、语言切换与壳重建;
117
+ render 抛错由错误边界捕获并恢复原生会话区;注入节点带实例令牌,HMR/重复 apply
118
+ 不会出现两份菜单,旧实例的 dispose 也不会删掉新实例的节点。
119
+ - **Host 纯度**:全部 DOM 逻辑只在 `src/client/**`,Host build 不引用 `document`。
120
+
57
121
  ## 目录结构
58
122
 
59
123
  - [src/index.ts](src/index.ts):宿主入口,登录编排、凭据写入与依赖接口。
124
+ - [src/client/feature-registry.ts](src/client/feature-registry.ts):扩展注册表(排序、校验、活动页)。
125
+ - [src/client/feature-center.ts](src/client/feature-center.ts):侧栏菜单注入 + 中央区 takeover。
126
+ - [src/client/contracts.ts](src/client/contracts.ts):业务插件可见的 client 面与服务名。
60
127
  - [src/orchestration.ts](src/orchestration.ts):登录后模型供给编排(拉 Key、写路由、热重载)。
61
128
  - [src/client/login-gate.tsx](src/client/login-gate.tsx):全屏登录门禁页。
62
129
  - [src/client/section.tsx](src/client/section.tsx):设置页「AI Router 网关」卡片。
@@ -1,3 +1,4 @@
1
+ import { PKG_VERSION } from "./version.js";
1
2
  /** 网关错误:携带稳定 code,供编排层映射。 */
2
3
  export class GatewayError extends Error {
3
4
  code;
@@ -69,7 +70,7 @@ export class AiRouterClient {
69
70
  }
70
71
  }
71
72
  async request(path, opts) {
72
- const headers = { 'user-agent': 'hiwork-core/0.1.14' };
73
+ const headers = { 'user-agent': `hiwork-core/${PKG_VERSION}` };
73
74
  if (opts.body !== undefined)
74
75
  headers['content-type'] = 'application/json';
75
76
  if (opts.sessionCookie !== undefined)
package/lib/client.js CHANGED
@@ -2270,6 +2270,510 @@ function mountLoginGate(scope, storeCredential) {
2270
2270
  };
2271
2271
  }
2272
2272
 
2273
+ // src/client/feature-registry.ts
2274
+ var DEFAULT_FEATURE_ORDER = 1e3;
2275
+ function compareFeatures(left, right) {
2276
+ const leftOrder = left.order ?? DEFAULT_FEATURE_ORDER;
2277
+ const rightOrder = right.order ?? DEFAULT_FEATURE_ORDER;
2278
+ if (leftOrder !== rightOrder) return leftOrder - rightOrder;
2279
+ if (left.id === right.id) return 0;
2280
+ return left.id < right.id ? -1 : 1;
2281
+ }
2282
+ function createFeatureRegistry() {
2283
+ const features = /* @__PURE__ */ new Map();
2284
+ const listeners = /* @__PURE__ */ new Set();
2285
+ let activeId = null;
2286
+ const notify = () => {
2287
+ for (const listener of [...listeners]) {
2288
+ try {
2289
+ listener();
2290
+ } catch (error) {
2291
+ console.error("hiwork feature center subscriber failed:", error);
2292
+ }
2293
+ }
2294
+ };
2295
+ return {
2296
+ register(feature) {
2297
+ const id = feature.id.trim();
2298
+ if (id === "") throw new Error("hiwork feature id must not be blank");
2299
+ if (features.has(id)) throw new Error(`hiwork feature "${id}" is already registered`);
2300
+ const entry = { ...feature, id };
2301
+ features.set(id, entry);
2302
+ notify();
2303
+ let disposed = false;
2304
+ return () => {
2305
+ if (disposed) return;
2306
+ disposed = true;
2307
+ if (features.get(id) !== entry) return;
2308
+ features.delete(id);
2309
+ if (activeId === id) activeId = null;
2310
+ notify();
2311
+ };
2312
+ },
2313
+ list() {
2314
+ return [...features.values()].sort(compareFeatures);
2315
+ },
2316
+ activeId() {
2317
+ return activeId;
2318
+ },
2319
+ open(id) {
2320
+ if (!features.has(id)) return false;
2321
+ if (activeId === id) return true;
2322
+ activeId = id;
2323
+ notify();
2324
+ return true;
2325
+ },
2326
+ openNative() {
2327
+ if (activeId === null) return;
2328
+ activeId = null;
2329
+ notify();
2330
+ },
2331
+ subscribe(listener) {
2332
+ listeners.add(listener);
2333
+ return () => {
2334
+ listeners.delete(listener);
2335
+ };
2336
+ }
2337
+ };
2338
+ }
2339
+
2340
+ // src/client/feature-center.ts
2341
+ var import_react5 = require("react");
2342
+ var import_client2 = require("react-dom/client");
2343
+ var SIDEBAR_SELECTOR = '[data-pane="sidebar"], [class*="sidebarCol"]';
2344
+ var CONVERSATION_SELECTOR = '[data-pane="conversation"], [class*="centerCol"]';
2345
+ var REGION_SELECTOR = '[class*="regionArea"]';
2346
+ var NEW_SESSION_SELECTOR = [
2347
+ 'button[data-testid="new-session"]',
2348
+ 'button[data-action="new-session"]',
2349
+ 'button[class*="newSession"]'
2350
+ ].join(", ");
2351
+ var MENU_ATTRIBUTE = "data-hiwork-feature-menu";
2352
+ var MENU_ITEM_ATTRIBUTE = "data-hiwork-feature-menu-item";
2353
+ var MENU_ICON_ATTRIBUTE = "data-hiwork-feature-menu-icon";
2354
+ var MENU_LABEL_ATTRIBUTE = "data-hiwork-feature-menu-label";
2355
+ var TOOLTIP_ATTRIBUTE = "data-hiwork-feature-tooltip";
2356
+ var TOOLTIP_VISIBLE_ATTRIBUTE = "data-hiwork-feature-tooltip-visible";
2357
+ var NATIVE_HIDDEN_ATTRIBUTE = "data-hiwork-native-new-session";
2358
+ var FEATURE_ROOT_ATTRIBUTE = "data-hiwork-feature-root";
2359
+ var OWNER_ATTRIBUTE = "data-hiwork-owner";
2360
+ var ACTIVE_PAGE_ATTRIBUTE = "data-hiwork-active-page";
2361
+ var NATIVE_MENU_ITEM_ID = "__native_new_session__";
2362
+ var DEFAULT_NEW_SESSION_LABEL = "\u65B0\u4F1A\u8BDD";
2363
+ var STYLE_ELEMENT_ID = "hiwork-feature-center-styles";
2364
+ var TOOLTIP_DELAY_MS = 500;
2365
+ var TOOLTIP_GAP_PX = 8;
2366
+ var DEFAULT_NEW_SESSION_ICON = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8 3.4v9.2M3.4 8h9.2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
2367
+ var DEFAULT_FEATURE_ICON = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect x="2.75" y="2.75" width="10.5" height="10.5" rx="3" stroke="currentColor" stroke-width="1.5"/><circle cx="8" cy="8" r="1.4" fill="currentColor"/></svg>';
2368
+ var SESSION_ROW_SELECTOR = [
2369
+ '[class*="sessionRow"]',
2370
+ '[class*="projectRow"]',
2371
+ '[class*="searchResultRow"]',
2372
+ '[class*="searchResultWorkspace"]',
2373
+ '[class*="newSession"]'
2374
+ ].join(", ");
2375
+ var featureCenterCss = [
2376
+ `[${NATIVE_HIDDEN_ATTRIBUTE}="hidden"] { display: none !important; }`,
2377
+ `[${MENU_ATTRIBUTE}] { display: flex; flex-direction: column; gap: 2px; margin: 0 2px 8px; }`,
2378
+ `[${MENU_ITEM_ATTRIBUTE}] { display: flex; align-items: center; gap: 8px; width: 100%; box-sizing: border-box; padding: 8px 10px; border: 0; border-radius: 10px; background: transparent; color: var(--dsw-alias-label-primary, #0f1115); font-size: 14px; line-height: 22px; text-align: left; cursor: pointer; }`,
2379
+ `[${MENU_ITEM_ATTRIBUTE}]:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15, 17, 21, 0.06)); }`,
2380
+ `[${MENU_ITEM_ATTRIBUTE}][data-hiwork-feature-active="true"] { background: var(--dsw-alias-interactive-bg-active, rgba(15, 17, 21, 0.1)); }`,
2381
+ `[${MENU_ICON_ATTRIBUTE}] { display: inline-flex; flex: none; width: 16px; height: 16px; }`,
2382
+ `[${MENU_ICON_ATTRIBUTE}] > svg { display: block; width: 16px; height: 16px; }`,
2383
+ // 侧栏折叠:宿主在侧栏根元素上加 `*_collapsed`(rc.1 为 hHd-Xa_collapsed),
2384
+ // 原生控件此时只显示图标;注入菜单必须同形,否则文字会在 56px 宽栏里竖排。
2385
+ `[class*="sidebarCol"] [class*="collapsed"] [${MENU_ATTRIBUTE}] { margin: 0 4px 8px; }`,
2386
+ `[class*="sidebarCol"] [class*="collapsed"] [${MENU_LABEL_ATTRIBUTE}] { display: none; }`,
2387
+ `[class*="sidebarCol"] [class*="collapsed"] [${MENU_ITEM_ATTRIBUTE}] { justify-content: center; padding: 8px 0; }`,
2388
+ `[class*="sidebarCol"] [class*="collapsed"] [${MENU_ICON_ATTRIBUTE}], [class*="sidebarCol"] [class*="collapsed"] [${MENU_ICON_ATTRIBUTE}] > svg { width: 18px; height: 18px; }`,
2389
+ // 折叠态提示气泡:几何与配色逐项对齐框架 `ui-primitives` 的 `Tooltip.module.css`
2390
+ // (黑底白字、3px 7px 内边距、8px 圆角、13/20 字号、150ms 淡入、最大半屏宽)。
2391
+ `[${TOOLTIP_ATTRIBUTE}] { position: fixed; z-index: 100; display: none; width: max-content; max-width: 50vw; padding: 3px 7px; border-radius: 8px; background: var(--dsw-alias-tooltip-bg, #2c2c2e); color: var(--dsw-static-neutral-bluish-00, #fff); font-size: 13px; line-height: 20px; white-space: pre-line; overflow-wrap: break-word; pointer-events: none; transform: translateY(-50%); }`,
2392
+ `[${TOOLTIP_ATTRIBUTE}][${TOOLTIP_VISIBLE_ATTRIBUTE}="true"] { display: block; animation: hiwork-feature-tooltip-in 150ms var(--ds-ease-in-out, ease); }`,
2393
+ `@keyframes hiwork-feature-tooltip-in { from { opacity: 0; } }`,
2394
+ `@media (prefers-reduced-motion: reduce) { [${TOOLTIP_ATTRIBUTE}][${TOOLTIP_VISIBLE_ATTRIBUTE}="true"] { animation: none; } }`,
2395
+ // 与原生会话根同一背景(`background: var(--dsw-alias-bg-base)`);不显式上色的话
2396
+ // takeover 区会透出框架灰底,和侧栏同色而不是原生页面的白色/深色。
2397
+ `[${FEATURE_ROOT_ATTRIBUTE}] { display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; overflow: hidden; background: var(--dsw-alias-bg-base, #fff); }`,
2398
+ `html[${ACTIVE_PAGE_ATTRIBUTE}] [data-pane="conversation"] > *:not([${FEATURE_ROOT_ATTRIBUTE}]), html[${ACTIVE_PAGE_ATTRIBUTE}] [class*="centerCol"] > *:not([${FEATURE_ROOT_ATTRIBUTE}]) { display: none !important; }`
2399
+ ].join("\n");
2400
+ var FeatureErrorBoundary = class extends import_react5.Component {
2401
+ state = { failed: false };
2402
+ static getDerivedStateFromError() {
2403
+ return { failed: true };
2404
+ }
2405
+ componentDidCatch(error, _info) {
2406
+ this.props.onError(error);
2407
+ }
2408
+ render() {
2409
+ return this.state.failed ? null : this.props.children ?? null;
2410
+ }
2411
+ };
2412
+ var ownerCounter = 0;
2413
+ var activeOwner = null;
2414
+ function sidebarRoot() {
2415
+ const column = document.querySelector(SIDEBAR_SELECTOR);
2416
+ if (column === null) return void 0;
2417
+ const logoOwner = column.querySelector('[class*="logoRow"]')?.parentElement;
2418
+ return logoOwner ?? column.firstElementChild ?? void 0;
2419
+ }
2420
+ function nativeNewSessionButton(root) {
2421
+ const matched = root.querySelector(NEW_SESSION_SELECTOR);
2422
+ if (matched !== null) return matched;
2423
+ for (const child of Array.from(root.children)) {
2424
+ if (child.tagName === "BUTTON") return child;
2425
+ }
2426
+ return void 0;
2427
+ }
2428
+ function conversationContainer() {
2429
+ return document.querySelector(CONVERSATION_SELECTOR) ?? void 0;
2430
+ }
2431
+ function isCollapsedRail(item) {
2432
+ const host = item.closest('[class*="collapsed"]');
2433
+ if (host === null) return false;
2434
+ const column = item.closest(SIDEBAR_SELECTOR);
2435
+ return column === null || column.contains(host);
2436
+ }
2437
+ function mountFeatureCenter(options) {
2438
+ const { registry } = options;
2439
+ const interval = options.retryIntervalMs ?? 200;
2440
+ const budget = options.retryBudgetMs ?? 1e4;
2441
+ const maxRetries = Math.max(1, Math.floor(budget / Math.max(1, interval)));
2442
+ const owner = `hiwork-core-${++ownerCounter}`;
2443
+ if (typeof document === "undefined") {
2444
+ return { dispose: () => void 0, openNativeHome: () => void 0 };
2445
+ }
2446
+ const isActiveOwner = () => activeOwner === owner;
2447
+ for (const stale of Array.from(document.querySelectorAll(`[${MENU_ATTRIBUTE}], [${FEATURE_ROOT_ATTRIBUTE}], [${TOOLTIP_ATTRIBUTE}], #${STYLE_ELEMENT_ID}`))) {
2448
+ stale.remove();
2449
+ }
2450
+ for (const hidden of Array.from(document.querySelectorAll(`[${NATIVE_HIDDEN_ATTRIBUTE}]`))) {
2451
+ hidden.removeAttribute(NATIVE_HIDDEN_ATTRIBUTE);
2452
+ }
2453
+ document.documentElement.removeAttribute(ACTIVE_PAGE_ATTRIBUTE);
2454
+ activeOwner = owner;
2455
+ const style = document.createElement("style");
2456
+ style.id = STYLE_ELEMENT_ID;
2457
+ style.setAttribute(OWNER_ATTRIBUTE, owner);
2458
+ style.textContent = featureCenterCss;
2459
+ document.head.appendChild(style);
2460
+ let menuElement;
2461
+ let menuSignature = "";
2462
+ let root;
2463
+ let container;
2464
+ let mountedId = null;
2465
+ let retryTimer;
2466
+ let retries = 0;
2467
+ let disposed = false;
2468
+ let tooltipElement;
2469
+ let tooltipTimer;
2470
+ let tooltipItem;
2471
+ const hideTooltip = () => {
2472
+ if (tooltipTimer !== void 0) {
2473
+ clearTimeout(tooltipTimer);
2474
+ tooltipTimer = void 0;
2475
+ }
2476
+ tooltipItem = void 0;
2477
+ tooltipElement?.removeAttribute(TOOLTIP_VISIBLE_ATTRIBUTE);
2478
+ };
2479
+ const ensureTooltip = () => {
2480
+ let element = tooltipElement;
2481
+ if (element === void 0) {
2482
+ element = document.createElement("div");
2483
+ element.setAttribute(TOOLTIP_ATTRIBUTE, "");
2484
+ element.setAttribute(OWNER_ATTRIBUTE, owner);
2485
+ element.setAttribute("role", "tooltip");
2486
+ document.body.appendChild(element);
2487
+ tooltipElement = element;
2488
+ }
2489
+ return element;
2490
+ };
2491
+ const placeTooltip = (item, element) => {
2492
+ const rect = item.getBoundingClientRect();
2493
+ const width = element.offsetWidth;
2494
+ const overflowRight = rect.right + TOOLTIP_GAP_PX + width > window.innerWidth;
2495
+ const left = overflowRight ? rect.left - TOOLTIP_GAP_PX - width : rect.right + TOOLTIP_GAP_PX;
2496
+ element.style.left = `${Math.max(0, left)}px`;
2497
+ element.style.top = `${rect.top + rect.height / 2}px`;
2498
+ };
2499
+ const showTooltip = (item, label) => {
2500
+ const element = ensureTooltip();
2501
+ element.textContent = label;
2502
+ element.setAttribute(TOOLTIP_VISIBLE_ATTRIBUTE, "true");
2503
+ tooltipItem = item;
2504
+ placeTooltip(item, element);
2505
+ };
2506
+ const bindTooltip = (item, label) => {
2507
+ item.addEventListener("pointerenter", () => {
2508
+ hideTooltip();
2509
+ tooltipTimer = setTimeout(() => {
2510
+ tooltipTimer = void 0;
2511
+ if (disposed || !item.isConnected || !isCollapsedRail(item)) return;
2512
+ showTooltip(item, label);
2513
+ }, TOOLTIP_DELAY_MS);
2514
+ });
2515
+ item.addEventListener("pointerleave", () => {
2516
+ if (tooltipItem === item || tooltipTimer !== void 0) hideTooltip();
2517
+ });
2518
+ };
2519
+ document.addEventListener("scroll", hideTooltip, true);
2520
+ const clearTakeover = () => {
2521
+ const reactRoot = root;
2522
+ root = void 0;
2523
+ mountedId = null;
2524
+ if (reactRoot !== void 0) {
2525
+ try {
2526
+ reactRoot.unmount();
2527
+ } catch (error) {
2528
+ console.warn("hiwork feature center: unmount failed", error);
2529
+ }
2530
+ }
2531
+ const element = container;
2532
+ container = void 0;
2533
+ if (element !== void 0 && element.getAttribute(OWNER_ATTRIBUTE) === owner) element.remove();
2534
+ if (isActiveOwner()) document.documentElement.removeAttribute(ACTIVE_PAGE_ATTRIBUTE);
2535
+ };
2536
+ const onRenderError = (error) => {
2537
+ console.warn("hiwork feature render failed:", error);
2538
+ const failedId = mountedId;
2539
+ queueMicrotask(() => {
2540
+ if (disposed) return;
2541
+ if (registry.activeId() === failedId) registry.openNative();
2542
+ else if (mountedId === failedId) clearTakeover();
2543
+ });
2544
+ };
2545
+ const renderContext = {
2546
+ openNativeSession: (sessionId) => {
2547
+ registry.openNative();
2548
+ options.openNativeSession(sessionId);
2549
+ },
2550
+ openNativeHome: () => {
2551
+ registry.openNative();
2552
+ clickNativeNewSession();
2553
+ }
2554
+ };
2555
+ function clickNativeNewSession() {
2556
+ const root_ = sidebarRoot();
2557
+ const button = root_ === void 0 ? void 0 : nativeNewSessionButton(root_);
2558
+ button?.click();
2559
+ }
2560
+ const onMenuClick = (event) => {
2561
+ const target = event.target;
2562
+ if (!(target instanceof Element)) return;
2563
+ const item = target.closest(`[${MENU_ITEM_ATTRIBUTE}]`);
2564
+ if (item === null || !(menuElement?.contains(item) ?? false)) return;
2565
+ hideTooltip();
2566
+ const id = item.getAttribute(MENU_ITEM_ATTRIBUTE);
2567
+ if (id === null) return;
2568
+ if (id === NATIVE_MENU_ITEM_ID) {
2569
+ renderContext.openNativeHome();
2570
+ return;
2571
+ }
2572
+ registry.open(id);
2573
+ };
2574
+ const createMenuItem = (id, label, icon, active) => {
2575
+ const item = document.createElement("button");
2576
+ item.type = "button";
2577
+ item.setAttribute(MENU_ITEM_ATTRIBUTE, id);
2578
+ if (active) item.setAttribute("data-hiwork-feature-active", "true");
2579
+ item.setAttribute("aria-label", label);
2580
+ bindTooltip(item, label);
2581
+ const iconSpan = document.createElement("span");
2582
+ iconSpan.setAttribute(MENU_ICON_ATTRIBUTE, "");
2583
+ iconSpan.innerHTML = icon;
2584
+ item.append(iconSpan);
2585
+ const labelSpan = document.createElement("span");
2586
+ labelSpan.setAttribute(MENU_LABEL_ATTRIBUTE, "");
2587
+ labelSpan.textContent = label;
2588
+ item.append(labelSpan);
2589
+ return item;
2590
+ };
2591
+ const nativeIcon = (native) => {
2592
+ const svg = native?.querySelector("svg");
2593
+ return svg === null || svg === void 0 ? DEFAULT_NEW_SESSION_ICON : svg.outerHTML;
2594
+ };
2595
+ const rebuildMenuItems = () => {
2596
+ const element = menuElement;
2597
+ if (element === void 0) return;
2598
+ const active = registry.activeId();
2599
+ const features = registry.list();
2600
+ const sidebar = sidebarRoot();
2601
+ const native = sidebar === void 0 ? void 0 : nativeNewSessionButton(sidebar);
2602
+ const newSessionLabel = options.newSessionLabel?.() ?? DEFAULT_NEW_SESSION_LABEL;
2603
+ const signature = [
2604
+ native === void 0 ? "no-native" : `native:${nativeIcon(native)}`,
2605
+ newSessionLabel,
2606
+ active ?? "",
2607
+ ...features.map((item) => `${item.id}:${item.order ?? ""}:${item.label()}:${item.icon ?? ""}`)
2608
+ ].join("|");
2609
+ if (signature === menuSignature) return;
2610
+ menuSignature = signature;
2611
+ hideTooltip();
2612
+ const items = [];
2613
+ if (native !== void 0) items.push(createMenuItem(NATIVE_MENU_ITEM_ID, newSessionLabel, nativeIcon(native), false));
2614
+ for (const item of features) {
2615
+ items.push(createMenuItem(item.id, item.label(), item.icon ?? DEFAULT_FEATURE_ICON, item.id === active));
2616
+ }
2617
+ element.replaceChildren(...items);
2618
+ };
2619
+ const ensureMenu = (sidebar, native) => {
2620
+ let element = menuElement;
2621
+ if (element === void 0) {
2622
+ element = document.createElement("div");
2623
+ element.setAttribute(MENU_ATTRIBUTE, "");
2624
+ element.setAttribute(OWNER_ATTRIBUTE, owner);
2625
+ element.setAttribute("role", "navigation");
2626
+ element.addEventListener("click", onMenuClick);
2627
+ menuElement = element;
2628
+ menuSignature = "";
2629
+ }
2630
+ if (element.parentElement !== sidebar) {
2631
+ const region = sidebar.querySelector(REGION_SELECTOR);
2632
+ if (native !== void 0 && native.parentElement === sidebar) sidebar.insertBefore(element, native.nextElementSibling);
2633
+ else if (region !== null && region.parentElement === sidebar) sidebar.insertBefore(element, region);
2634
+ else sidebar.appendChild(element);
2635
+ }
2636
+ rebuildMenuItems();
2637
+ };
2638
+ const dedupe = (selector) => {
2639
+ for (const node of Array.from(document.querySelectorAll(selector))) {
2640
+ if (node.getAttribute(OWNER_ATTRIBUTE) !== owner) node.remove();
2641
+ }
2642
+ };
2643
+ const syncMenu = () => {
2644
+ dedupe(`[${MENU_ATTRIBUTE}]`);
2645
+ const sidebar = sidebarRoot();
2646
+ if (sidebar === void 0) return;
2647
+ const native = nativeNewSessionButton(sidebar);
2648
+ if (native !== void 0) native.setAttribute(NATIVE_HIDDEN_ATTRIBUTE, "hidden");
2649
+ ensureMenu(sidebar, native);
2650
+ };
2651
+ const syncTakeover = () => {
2652
+ dedupe(`[${FEATURE_ROOT_ATTRIBUTE}]`);
2653
+ const activeId = registry.activeId();
2654
+ if (activeId === null) {
2655
+ clearTakeover();
2656
+ return;
2657
+ }
2658
+ const target = registry.list().find((item) => item.id === activeId);
2659
+ if (target === void 0) {
2660
+ clearTakeover();
2661
+ return;
2662
+ }
2663
+ const host = conversationContainer();
2664
+ if (host === void 0) {
2665
+ clearTakeover();
2666
+ return;
2667
+ }
2668
+ if (container !== void 0 && container.isConnected && container.parentElement === host && mountedId === activeId) return;
2669
+ clearTakeover();
2670
+ const element = document.createElement("div");
2671
+ element.setAttribute(FEATURE_ROOT_ATTRIBUTE, "");
2672
+ element.setAttribute(OWNER_ATTRIBUTE, owner);
2673
+ element.dataset.hiworkFeatureId = activeId;
2674
+ host.appendChild(element);
2675
+ document.documentElement.setAttribute(ACTIVE_PAGE_ATTRIBUTE, activeId);
2676
+ container = element;
2677
+ mountedId = activeId;
2678
+ const reactRoot = (0, import_client2.createRoot)(element);
2679
+ root = reactRoot;
2680
+ try {
2681
+ reactRoot.render((0, import_react5.createElement)(FeatureErrorBoundary, { onError: onRenderError }, target.render(renderContext)));
2682
+ } catch (error) {
2683
+ onRenderError(error);
2684
+ }
2685
+ };
2686
+ const pending = () => {
2687
+ const sidebar = sidebarRoot();
2688
+ if (sidebar === void 0) return true;
2689
+ if (menuElement === void 0 || !sidebar.contains(menuElement)) return true;
2690
+ if (registry.activeId() === null) return false;
2691
+ const host = conversationContainer();
2692
+ if (host === void 0) return true;
2693
+ return container === void 0 || !host.contains(container);
2694
+ };
2695
+ const scheduleRetry = () => {
2696
+ if (disposed || retryTimer !== void 0 || retries >= maxRetries) return;
2697
+ retryTimer = setTimeout(() => {
2698
+ retryTimer = void 0;
2699
+ retries += 1;
2700
+ sync();
2701
+ if (pending()) scheduleRetry();
2702
+ }, interval);
2703
+ };
2704
+ const onDocumentClickCapture = (event) => {
2705
+ if (registry.activeId() === null) return;
2706
+ const target = event.target;
2707
+ if (!(target instanceof Element)) return;
2708
+ if (target.closest(`[${FEATURE_ROOT_ATTRIBUTE}]`) !== null) return;
2709
+ if (target.closest(`[${MENU_ATTRIBUTE}]`) !== null) return;
2710
+ if (target.closest(SESSION_ROW_SELECTOR) === null) return;
2711
+ registry.openNative();
2712
+ };
2713
+ const unsubscribeRegistry = registry.subscribe(() => {
2714
+ sync();
2715
+ });
2716
+ let unsubscribeLocale;
2717
+ if (options.subscribeLocale !== void 0) {
2718
+ try {
2719
+ unsubscribeLocale = options.subscribeLocale(() => {
2720
+ menuSignature = "";
2721
+ rebuildMenuItems();
2722
+ });
2723
+ } catch (error) {
2724
+ console.warn("hiwork feature center: locale subscription unavailable", error);
2725
+ }
2726
+ }
2727
+ const observer = new MutationObserver(() => {
2728
+ sync();
2729
+ });
2730
+ observer.observe(document.body, { childList: true, subtree: true });
2731
+ document.addEventListener("click", onDocumentClickCapture, true);
2732
+ const sync = () => {
2733
+ if (disposed || !isActiveOwner()) return;
2734
+ syncMenu();
2735
+ syncTakeover();
2736
+ if (pending()) scheduleRetry();
2737
+ };
2738
+ sync();
2739
+ return {
2740
+ dispose: () => {
2741
+ if (disposed) return;
2742
+ disposed = true;
2743
+ observer.disconnect();
2744
+ if (retryTimer !== void 0) clearTimeout(retryTimer);
2745
+ retryTimer = void 0;
2746
+ unsubscribeRegistry();
2747
+ unsubscribeLocale?.();
2748
+ document.removeEventListener("click", onDocumentClickCapture, true);
2749
+ document.removeEventListener("scroll", hideTooltip, true);
2750
+ hideTooltip();
2751
+ const tooltip = tooltipElement;
2752
+ tooltipElement = void 0;
2753
+ if (tooltip !== void 0 && tooltip.getAttribute(OWNER_ATTRIBUTE) === owner) tooltip.remove();
2754
+ clearTakeover();
2755
+ const element = menuElement;
2756
+ menuElement = void 0;
2757
+ if (element !== void 0) {
2758
+ element.removeEventListener("click", onMenuClick);
2759
+ if (element.getAttribute(OWNER_ATTRIBUTE) === owner) element.remove();
2760
+ }
2761
+ if (!isActiveOwner()) return;
2762
+ activeOwner = null;
2763
+ for (const hidden of Array.from(document.querySelectorAll(`[${NATIVE_HIDDEN_ATTRIBUTE}]`))) {
2764
+ hidden.removeAttribute(NATIVE_HIDDEN_ATTRIBUTE);
2765
+ }
2766
+ if (style.getAttribute(OWNER_ATTRIBUTE) === owner) style.remove();
2767
+ },
2768
+ openNativeHome: () => {
2769
+ renderContext.openNativeHome();
2770
+ }
2771
+ };
2772
+ }
2773
+
2774
+ // src/client/contracts.ts
2775
+ var HIWORK_FEATURE_CENTER_SERVICE = "hiworkFeatureCenter";
2776
+
2273
2777
  // src/client/index.ts
2274
2778
  var inject = ["slots", "locale", "remote", "remote.credentials", "settingsScope"];
2275
2779
  var INTERACTION_STYLE_ID = "hiwork-core-interaction-styles";
@@ -2304,10 +2808,12 @@ function apply(ctx) {
2304
2808
  }, AboutSection));
2305
2809
  ctx.effect(() => observeAboutNavIcon(), "hiwork-core: about nav icon");
2306
2810
  ctx.slots.inject("sidebar.brand.mark", () => ctx.slots.register({
2307
- name: "sidebar.brand.mark"
2811
+ name: "sidebar.brand.mark",
2812
+ priority: -1
2308
2813
  }, BrandMark));
2309
2814
  ctx.slots.inject("sidebar.brand.name", () => ctx.slots.register({
2310
- name: "sidebar.brand.name"
2815
+ name: "sidebar.brand.name",
2816
+ priority: -1
2311
2817
  }, BrandName));
2312
2818
  ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
2313
2819
  name: "sidebar.footer.action",
@@ -2316,5 +2822,42 @@ function apply(ctx) {
2316
2822
  }, UserBadge));
2317
2823
  ctx.effect(() => mountLoginGate(scope, storeCredential), "hiwork-core: login gate");
2318
2824
  ctx.effect(() => observeHeroDecorations(), "hiwork-core: hero decorations");
2825
+ installFeatureCenter(ctx);
2826
+ }
2827
+ function installFeatureCenter(ctx) {
2828
+ const featureRegistry = createFeatureRegistry();
2829
+ let center;
2830
+ const sessions = ctx.get("sessions");
2831
+ const locale = ctx.get("locale");
2832
+ const face = {
2833
+ featureRegistry,
2834
+ openFeature: (id) => featureRegistry.open(id),
2835
+ // 顺序固定:先退出 takeover 恢复原生会话区,再交给宿主导航。
2836
+ openNativeSession: (sessionId) => {
2837
+ featureRegistry.openNative();
2838
+ sessions?.open(sessionId);
2839
+ },
2840
+ openNativeHome: () => {
2841
+ if (center !== void 0) {
2842
+ center.openNativeHome();
2843
+ return;
2844
+ }
2845
+ featureRegistry.openNative();
2846
+ }
2847
+ };
2848
+ ctx.provide(HIWORK_FEATURE_CENTER_SERVICE, face);
2849
+ ctx.effect(() => {
2850
+ center = mountFeatureCenter({
2851
+ registry: featureRegistry,
2852
+ openNativeSession: (sessionId) => {
2853
+ sessions?.open(sessionId);
2854
+ },
2855
+ ...locale === void 0 ? {} : { subscribeLocale: (listener) => locale.subscribe(listener) }
2856
+ });
2857
+ return () => {
2858
+ center?.dispose();
2859
+ center = void 0;
2860
+ };
2861
+ }, "hiwork-core: feature center");
2319
2862
  }
2320
2863
  return module.exports; } });
package/lib/index.js CHANGED
@@ -3136,6 +3136,9 @@ function isUnloading(ctx) {
3136
3136
  return state === FIBER_UNLOADING || state === FIBER_DISPOSED;
3137
3137
  }
3138
3138
 
3139
+ // src/version.ts
3140
+ var PKG_VERSION = false ? "dev" : "0.0.3";
3141
+
3139
3142
  // src/ai-router-client.ts
3140
3143
  var GatewayError = class extends Error {
3141
3144
  constructor(code, message, status) {
@@ -3190,7 +3193,7 @@ var AiRouterClient = class {
3190
3193
  }
3191
3194
  }
3192
3195
  async request(path, opts) {
3193
- const headers = { "user-agent": "hiwork-core/0.1.14" };
3196
+ const headers = { "user-agent": `hiwork-core/${PKG_VERSION}` };
3194
3197
  if (opts.body !== void 0) headers["content-type"] = "application/json";
3195
3198
  if (opts.sessionCookie !== void 0) headers["cookie"] = opts.sessionCookie;
3196
3199
  const init = {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `hiwork-core` 对业务插件公开的 client 面。
3
+ *
4
+ * 业务插件(`hiwork-automation`、未来的 `hiwork-knowledge` …)不在运行时 import
5
+ * 本包的值——DSH 的 client 模块表要求「feature 插件不 runtime-import 另一个
6
+ * feature 插件」。它们改为通过 cordis 服务发现本面:
7
+ *
8
+ * ```ts
9
+ * ctx.inject(['hiworkFeatureCenter'], (scope) => {
10
+ * const face = scope.get('hiworkFeatureCenter') as HiWorkCoreClientFace
11
+ * ctx.effect(() => face.featureRegistry.register({ ... }), 'my-plugin: feature')
12
+ * })
13
+ * ```
14
+ *
15
+ * 本文件同时是服务名的唯一来源({@link HIWORK_FEATURE_CENTER_SERVICE}),
16
+ * 避免两侧字符串漂移。
17
+ *
18
+ * 这里**不做** `declare module '@deepseek-ai/cordis'` 的 Context 合并:实测它会让
19
+ * `tsconfig.client-build.json`(只包含 src/client)丢掉 cordis 内部的 `ctx.effect`
20
+ * 声明合并,导致 client 声明构建失败。业务插件在自己的结构契约里声明同名的可选成员
21
+ * 即可(见 `hiwork-automation` 的 `contracts.ts`),运行期只按服务名读取。
22
+ */
23
+ import type { FeatureRegistry } from './feature-registry.ts';
24
+ /** cordis 服务名:`hiwork-core` 的 client 半边提供,业务插件按名注入。 */
25
+ export declare const HIWORK_FEATURE_CENTER_SERVICE = "hiworkFeatureCenter";
26
+ /**
27
+ * 业务插件能看到的全部能力。
28
+ *
29
+ * 只暴露注册与导航:菜单、中央区 takeover、原生按钮代理都由 `hiwork-core` 自己
30
+ * 实现,业务插件不需要(也不应该)触碰 DOM。
31
+ */
32
+ export interface HiWorkCoreClientFace {
33
+ /** 扩展注册表:注册/注销 feature,读取当前活动页。 */
34
+ readonly featureRegistry: FeatureRegistry;
35
+ /** 打开一个已注册 feature 的中央页;未注册返回 false。 */
36
+ readonly openFeature: (id: string) => boolean;
37
+ /** 打开原生会话:先退出 takeover,再交给宿主会话导航。 */
38
+ readonly openNativeSession: (sessionId: string) => void;
39
+ /** 回到原生「新会话」首页:先退出 takeover,再触发原生新会话动作。 */
40
+ readonly openNativeHome: () => void;
41
+ }
@@ -0,0 +1,38 @@
1
+ import type { FeatureRegistry } from './feature-registry.ts';
2
+ /** 中央区 takeover 的活动页标记(纯 CSS 隐藏原生内容的开关)。 */
3
+ export declare const ACTIVE_PAGE_ATTRIBUTE = "data-hiwork-active-page";
4
+ /**
5
+ * Feature Center 的全局样式。
6
+ *
7
+ * 隐藏原生会话内容用的是 `html[data-hiwork-active-page]` 上的纯 CSS 规则:移除属性
8
+ * 即完全恢复,不会留下任何内联 `display:none`。隐藏原生按钮同理走属性规则。
9
+ */
10
+ export declare const featureCenterCss: string;
11
+ /** 中央区适配器配置。 */
12
+ export interface FeatureCenterOptions {
13
+ /** hiwork-core 的扩展注册表。 */
14
+ readonly registry: FeatureRegistry;
15
+ /** 宿主会话导航(`ctx.sessions.open`)。 */
16
+ readonly openNativeSession: (sessionId: string) => void;
17
+ /** 「新会话」菜单文案;缺省「新会话」。 */
18
+ readonly newSessionLabel?: () => string;
19
+ /** 语言变化源(`ctx.locale.subscribe`);变化时重建菜单文案。 */
20
+ readonly subscribeLocale?: (listener: () => void) => () => void;
21
+ /** 未就绪时的重试间隔(毫秒),缺省 200。 */
22
+ readonly retryIntervalMs?: number;
23
+ /** 未就绪时的重试总预算(毫秒),缺省 10_000;预算耗尽后只靠 observer 自愈。 */
24
+ readonly retryBudgetMs?: number;
25
+ }
26
+ /** 中央区适配器句柄。 */
27
+ export interface FeatureCenterHandle {
28
+ /** 断开 observer、卸载 takeover、移除菜单与样式、恢复原生按钮可见性;可重复调用。 */
29
+ dispose(): void;
30
+ /** 回到原生「新会话」首页:先退出 takeover,再触发原生按钮动作。 */
31
+ openNativeHome(): void;
32
+ }
33
+ /**
34
+ * 挂载 Feature Center:注入侧栏菜单并管理中央区 takeover。
35
+ * @param options - 注册表、宿主导航与重试预算。
36
+ * @returns 句柄;`dispose()` 恢复原生 DOM。
37
+ */
38
+ export declare function mountFeatureCenter(options: FeatureCenterOptions): FeatureCenterHandle;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * HiWork Feature Center 的扩展注册表(浏览器半边,无 DOM 依赖)。
3
+ *
4
+ * 业务插件(`hiwork-automation`、未来的 `hiwork-knowledge` …)通过
5
+ * `featureRegistry.register(feature)` 贡献一个中央页入口;`hiwork-core` 的侧栏菜单
6
+ * 与中央区 takeover 只读这份注册表,不持有任何业务状态。
7
+ *
8
+ * 契约要点:
9
+ * - `id` 必须非空且全局唯一,重复注册抛错(fail loud,不静默覆盖);
10
+ * - `register` 返回幂等 disposer,可重复调用;
11
+ * - `list()` 按 `order`(缺省 {@link DEFAULT_FEATURE_ORDER})再按 `id` 稳定排序;
12
+ * - `open` / `openNative` 只在可观察状态真的变化时通知订阅者。
13
+ */
14
+ import type { ReactNode } from 'react';
15
+ /** 缺省排序权重:未声明 `order` 的 feature 排在声明了正权重之后、按 id 稳定。 */
16
+ export declare const DEFAULT_FEATURE_ORDER = 1000;
17
+ /**
18
+ * 业务插件渲染中央页时拿到的上下文。
19
+ *
20
+ * 只提供导航能力,registry 不向插件暴露任务/知识库等业务状态:业务状态仍由插件
21
+ * 自己的 runtime 持有。
22
+ */
23
+ export interface FeatureRenderContext {
24
+ /** 打开一条原生 DSH 会话;调用前会先退出 takeover 并恢复原生会话区。 */
25
+ readonly openNativeSession: (sessionId: string) => void;
26
+ /** 回到原生「新会话」首页;调用前会先退出 takeover。 */
27
+ readonly openNativeHome: () => void;
28
+ }
29
+ /** 一个可注册的 HiWork 扩展入口。 */
30
+ export interface HiWorkFeature {
31
+ /** 稳定 id,同时作为菜单项与 `data-hiwork-active-page` 的值。 */
32
+ readonly id: string;
33
+ /** 菜单文案;函数形式以便语言切换后重新求值。 */
34
+ readonly label: () => string;
35
+ /** 可选图标标记(内联 SVG 字符串),由侧栏适配器渲染。 */
36
+ readonly icon?: string;
37
+ /** 菜单排序权重,越小越靠前;缺省 {@link DEFAULT_FEATURE_ORDER}。 */
38
+ readonly order?: number;
39
+ /** 渲染中央页内容;抛错由中央区适配器捕获并恢复原生会话区。 */
40
+ readonly render: (context: FeatureRenderContext) => ReactNode;
41
+ }
42
+ /** 扩展注册表。 */
43
+ export interface FeatureRegistry {
44
+ /** 注册一个 feature;返回幂等 disposer。id 为空或重复时抛错。 */
45
+ register(feature: HiWorkFeature): () => void;
46
+ /** 当前已注册 feature 的稳定排序快照。 */
47
+ list(): readonly HiWorkFeature[];
48
+ /** 当前打开的 feature id;未打开扩展时为 `null`。 */
49
+ activeId(): string | null;
50
+ /** 打开指定 feature;不存在返回 false,已打开时返回 true 但不重复通知。 */
51
+ open(id: string): boolean;
52
+ /** 退出扩展、回到原生会话区;未打开扩展时是 no-op。 */
53
+ openNative(): void;
54
+ /** 订阅注册/注销/活动页变化;返回退订函数。 */
55
+ subscribe(listener: () => void): () => void;
56
+ }
57
+ /**
58
+ * 创建一份独立的注册表。每次 `apply()` 只创建一份,随插件 fiber 一起释放。
59
+ * @returns 扩展注册表。
60
+ */
61
+ export declare function createFeatureRegistry(): FeatureRegistry;
@@ -1,4 +1,6 @@
1
1
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
2
+ export type { FeatureRenderContext, FeatureRegistry, HiWorkFeature } from './feature-registry.ts';
3
+ export type { HiWorkCoreClientFace } from './contracts.ts';
2
4
  /** 浏览器插件契约(dsh client module system)。connection 随 IApiClient 移除后不再依赖。 */
3
5
  export declare const inject: string[];
4
6
  /** 挂载网关模型设置 section(一级 tab)与未登录弹窗。 */
@@ -0,0 +1 @@
1
+ export declare const PKG_VERSION: string;
package/lib/version.js ADDED
@@ -0,0 +1 @@
1
+ export const PKG_VERSION = typeof __PKG_VERSION__ === 'undefined' ? 'dev' : __PKG_VERSION__;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hiwork-core",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "description": "HiWork 企业统一登录与模型自动供给插件(ai-router 网关登录,Web 表单,无 CLI 认证)",
6
6
  "license": "UNLICENSED",