hiwork-core 0.0.37 → 0.0.39

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.
@@ -1,4 +1,4 @@
1
- import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
1
+ import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
2
2
  import { type AiRouterCardFace } from './controller.ts';
3
3
  import { type PluginUpdatesStore } from './plugin-updates.ts';
4
4
  /**
@@ -19,6 +19,11 @@ import { type PluginUpdatesStore } from './plugin-updates.ts';
19
19
  export interface PluginUpdatesFace {
20
20
  pluginUpdates: PluginUpdatesStore;
21
21
  }
22
- export type UserBadgeProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<AiRouterCardFace & PluginUpdatesFace>;
22
+ export type UserBadgeProps = InjectFace<AiRouterCardFace & PluginUpdatesFace> & {
23
+ /** `settings.launcher` 槽位的渲染参数(ui-settings-general 的 SettingsRoot 下发)。 */
24
+ readonly wide: boolean | undefined;
25
+ /** 打开设置弹窗(壳拥有 open 状态);分区直达由 selectSettingsSection 补齐。 */
26
+ readonly openSettings: () => void;
27
+ };
23
28
  /** 底部账号信息行(宽栏 / 折叠栏共用;插件更新提示卡片也由它渲染,见下)。 */
24
29
  export declare function UserBadge(props: UserBadgeProps): import("react").JSX.Element | null;
@@ -2,9 +2,11 @@
2
2
  * 客户端版本策略(spec §6):`GET /api/client/policy` 的免登录只读拉取。
3
3
  *
4
4
  * 网关返回的是**最终判定**(`loginMode` 等),插件只做投影不做策略逻辑,因此管理端
5
- * 调整 `client.login_mode` 后重启插件即生效,无需发版(spec §6.1)。
5
+ * 调整 `client.login_mode` 后无需重启插件、无需发版:由 {@link createClientPolicyPoller}
6
+ * 每 6 小时重拉一次(对齐服务端 `Cache-Control: max-age=300` 的注释预期),失败按
7
+ * 1min → 10min → 1h 阶梯退避重试,紧急止血策略(如 `login_mode=form`)由此能到达在线客户端。
6
8
  *
7
- * 本模块只负责「拉 + 严格解析」,投影与 fail-open 缺省由 `src/index.ts` 的
9
+ * 本模块只负责「拉 + 严格解析 + 轮询节律」,投影与 fail-open 缺省由 `src/index.ts` 的
8
10
  * `projectLoginMode` 完成:**非 200 / 非 JSON / 枚举非法 / 超时 / 网络异常一律返回
9
11
  * undefined**,调用方据此回退 `DEFAULT_LOGIN_MODE`,绝不把用户锁在门外(spec §5.4 / §6.4)。
10
12
  */
@@ -57,3 +59,29 @@ export declare function buildClientPolicyURL(opts: FetchClientPolicyOptions): st
57
59
  * 不记录响应原文——策略接口当前无敏感字段,但保持与其他网关调用一致的日志纪律。
58
60
  */
59
61
  export declare function fetchClientPolicy(opts: FetchClientPolicyOptions): Promise<ClientPolicy | undefined>;
62
+ /** 策略轮询间隔(6h,对齐服务端 `Cache-Control: max-age=300` 的注释预期)。 */
63
+ export declare const CLIENT_POLICY_POLL_INTERVAL_MS: number;
64
+ /**
65
+ * 失败退避阶梯(1min → 10min → 1h),末档封顶循环。
66
+ * 退避只影响「多久后重试」,不影响已经投影好的策略值——失败时保留上一轮结果。
67
+ */
68
+ export declare const CLIENT_POLICY_RETRY_DELAYS_MS: readonly [60000, number, number];
69
+ export interface ClientPolicyPoller {
70
+ /** 请求一次拉取:**当前没有排程**(也没在跑)时立即发起,否则什么都不做。 */
71
+ trigger(): void;
72
+ /** 停止轮询并清掉已排程的定时器(调用后零回调)。 */
73
+ dispose(): void;
74
+ }
75
+ /**
76
+ * 自排程的策略轮询器(不用 setInterval:每轮的间隔由上一轮结果决定)。
77
+ * - `attempt` 返回 true(成功拉到并投影)→ 6h 后再来;
78
+ * - 返回 false 或抛异常 → 按 {@link CLIENT_POLICY_RETRY_DELAYS_MS} 退避,末档封顶循环;
79
+ * - 任意一次成功把退避档归零(下一次失败重新从 1min 起步);
80
+ * - 定时器 `unref`,不拖住进程退出;`trigger()` 幂等(已排程/在飞时是 no-op),
81
+ * 让宿主 200ms 消费循环可以无脑反复调用。
82
+ */
83
+ export declare function createClientPolicyPoller(deps: {
84
+ /** 发起一次拉取并投影;成功返回 true。baseURL 未配置时返回 false。 */
85
+ attempt: () => Promise<boolean>;
86
+ log?: (message: string) => void;
87
+ }): ClientPolicyPoller;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 登录身份的 runtime context 贡献(`hiwork:identity`):把「当前登录人是谁」送进
3
+ * AI 上下文,供 skill / 会话按人表述与个性化使用(spec
4
+ * `docs/superpowers/specs/2026-10-09-identity-runtime-context-design.md`)。
5
+ *
6
+ * 设计要点(与 `src/identity.ts` 同构):
7
+ * - **cordis-free 纯模块**:注册走结构化的 {@link SystemPromptPort} 窄端口,调用方
8
+ * (index.ts)传真实 `ctx.systemPrompt`;单测直接伪造。真实 PromptContext.text
9
+ * 接受 `(AssembleContext) => string`,本模块的 `() => string` 结构兼容(忽略入参);
10
+ * - **空串 = 不贡献**(DSH 原生语义):未登录、bundle 层关闭(`identityContext: false`)、
11
+ * 读失败一律返回空串——零暴露,且绝不向组装层抛异常;
12
+ * - **每次组装现读**:text 是每次求值的 provider,登录 / 登出 / 换号在下一步即时反映,
13
+ * 不缓存旧身份;
14
+ * - **身份文本不是凭证**:上下文里的身份只作展示 / 个性化 / 按人表述用,鉴权一律在
15
+ * 通道层(loopback RPC 带网关会话、MCP 断言→mint)。文案末尾那句限定语是对模型的
16
+ * 引导,不是防线;
17
+ * - **字段最小化**:只用 `username`(工号)与 `displayName`(姓名),归一化口径与
18
+ * identity.ts 的 textOrUndefined 一致(裁空白、空白串视同缺省)。keys / subject /
19
+ * 团队一律不进上下文。
20
+ */
21
+ import type { IdentityState } from './identity.ts';
22
+ /** runtime context 贡献名(DSH 侧 snapshot 按名归并,改动即破坏对齐面)。 */
23
+ export declare const IDENTITY_CONTEXT_NAME = "hiwork:identity";
24
+ /** 排在 sandbox:policy(110) / approval:policy(115) 之前——身份是最外层的环境事实。 */
25
+ export declare const IDENTITY_CONTEXT_ORDER = 105;
26
+ /**
27
+ * 渲染身份上下文一行:已登录输出 `HiWork 登录身份:姓名(工号 工号)。<限定语>`,
28
+ * displayName 缺省回退 `工号 <工号>`;**未登录(username 缺省 / 空白 / 非字符串)或
29
+ * state 缺失一律空串**——半份身份(只有 displayName)也不输出,姓名不得单独泄漏。
30
+ */
31
+ export declare function renderIdentityContextText(state: IdentityState | undefined): string;
32
+ /** `ctx.systemPrompt.context` 的最小端口(结构兼容 DSH 的 SystemPrompt,便于单测伪造)。 */
33
+ export interface SystemPromptPort {
34
+ /** 注册一条 runtime context 贡献;返回注销函数(或任何可注销值,见 registerIdentityContext)。 */
35
+ context(contribution: {
36
+ name: string;
37
+ order: number;
38
+ text: () => string;
39
+ }): unknown;
40
+ }
41
+ /** 注册侧依赖:登录态现读 + bundle 层开关 + 日志出口。 */
42
+ export interface IdentityContextDeps {
43
+ /** 读一次当前登录态(与 hiworkIdentity 同一数据源;每次组装都会调用)。 */
44
+ current(): IdentityState;
45
+ /** bundle 层固定开关(缺省开);关闭时贡献空文本。 */
46
+ enabled(): boolean;
47
+ /** Host 日志出口(只记错误类型,不记身份字段)。 */
48
+ log?(message: string): void;
49
+ }
50
+ /**
51
+ * 注册身份 runtime context 贡献;返回 disposer(交给 `ctx.effect` 管理生命周期)。
52
+ * 读失败收敛为空串并记一笔,绝不向组装层抛——身份上下文缺席只应意味着「未登录」。
53
+ */
54
+ export declare function registerIdentityContext(systemPrompt: SystemPromptPort, deps: IdentityContextDeps): () => void;
@@ -19,7 +19,8 @@ export declare const name = "hiwork-core";
19
19
  * (`ctx.web.registerSearchProvider`,见 `src/web-search.ts`;dsh-base 基座先于本
20
20
  * 插件挂载 dsh-web);`agents` 是根 Agent 注册表——`image_generate` 工具要挂在根 Agent
21
21
  * 上(`ctx.agents.roots()` / `agent/created`,见 `src/tools.ts`;与 knowledge 的
22
- * `inject` 同源);其余三项是既有登录 / 供给依赖。
22
+ * `inject` 同源);`systemPrompt` 承载登录身份的 runtime context 贡献
23
+ * (`hiwork:identity`,见 `src/identity-context.ts`);其余三项是既有登录 / 供给依赖。
23
24
  */
24
25
  export declare const inject: string[];
25
26
  /** 侧栏菜单偏好的解析值(client 侧 `MenuPreferences` 的同一份形状)。 */
@@ -44,9 +45,19 @@ export declare const MenuConfig: z<MenuConfigValue>;
44
45
  /** 插件配置 + 运行时状态字段(全部可选;写操作按此 schema 校验)。 */
45
46
  export interface Config {
46
47
  baseURL?: string;
48
+ /**
49
+ * 身份上下文开关(bundle 层固定配置,缺省开):把当前登录人(工号 + 姓名)以
50
+ * runtime context 贡献暴露进 AI 上下文;关闭时贡献空文本(零暴露)。
51
+ */
52
+ identityContext?: boolean;
47
53
  keyId?: string;
48
54
  username?: string;
49
55
  displayName?: string;
56
+ /**
57
+ * 网关会话的绝对寿命(ISO 8601;登录/授权兑换响应解析而来,Host 在供给成功时写入)。
58
+ * 未知时为 undefined——SessionSupervisor 退回「按假定寿命估算」而不是放弃保活。
59
+ */
60
+ sessionExpiresAt?: string;
50
61
  lastLogin?: {
51
62
  at: string;
52
63
  modelCount: number;
@@ -114,12 +125,13 @@ export declare const Config: z<Config>;
114
125
  /**
115
126
  * apply 收到的运行时 config:volatile 字段是稳定引用(schemastery 解析期包装,值为
116
127
  * undefined 时引用依然存在),随 profile 变更被框架原地更新——持有引用、每次现取,
117
- * 绝不缓存 `.get()` 的结果。baseURL 仍是普通组合层配置。
128
+ * 绝不缓存 `.get()` 的结果。baseURL 与 identityContext 仍是普通组合层配置。
118
129
  */
119
130
  export type RuntimeConfig = {
120
131
  baseURL?: string;
132
+ identityContext?: boolean;
121
133
  } & {
122
- [K in Exclude<keyof Config, 'baseURL'>]?: Volatile<Config[K]>;
134
+ [K in Exclude<keyof Config, 'baseURL' | 'identityContext'>]?: Volatile<Config[K]>;
123
135
  };
124
136
  /**
125
137
  * settings.mutate 的路径操作(rc.2 的 dsh-settings 不再导出 `SettingsPathOp` 类型,
@@ -15,8 +15,8 @@ export declare function runLogin(opts: {
15
15
  ensureWorkspace?: () => Promise<void>;
16
16
  value: AiRouterNamespaceValue;
17
17
  }): Promise<LoginError | null>;
18
- /** 用已存会话刷新模型列表。 */
19
- export declare function runRefresh(opts: {
18
+ /** `runRefresh` 的入参(也原样传给内部的 `refreshWith`)。 */
19
+ interface RunRefreshOptions {
20
20
  gateway: GatewayClient;
21
21
  credentials: CredentialsPort;
22
22
  piAi: PiAiPort;
@@ -26,7 +26,9 @@ export declare function runRefresh(opts: {
26
26
  /** 透传给重登路径 runLogin 的可选端口。 */
27
27
  ensureWorkspace?: () => Promise<void>;
28
28
  value: AiRouterNamespaceValue;
29
- }): Promise<LoginError | null>;
29
+ }
30
+ /** 用已存会话刷新模型列表(会话过期时先试 refresh grant 静默重认证,再回落记住密码/登录门)。 */
31
+ export declare function runRefresh(opts: RunRefreshOptions): Promise<LoginError | null>;
30
32
  /** 退出登录:best-effort 登出 + 清凭据与 pi-ai 配置。 */
31
33
  export declare function runLogout(opts: {
32
34
  gateway: GatewayClient;
@@ -37,6 +37,8 @@ export interface AiRouterStatePort {
37
37
  /** ai-router 命名空间解析后的值(相关字段)。 */
38
38
  export interface AiRouterNamespaceValue {
39
39
  baseURL?: string;
40
+ /** 身份上下文开关(bundle 层固定配置,缺省开);关闭时 `hiwork:identity` 贡献空文本。 */
41
+ identityContext?: boolean;
40
42
  keyId?: string;
41
43
  username?: string;
42
44
  /** 登录账号的显示名(网关 LDAP displayName,如中文名);缺省时 UI 回退 username。 */
@@ -46,6 +48,11 @@ export interface AiRouterNamespaceValue {
46
48
  passwordRef: string;
47
49
  };
48
50
  rememberPassword?: boolean;
51
+ /**
52
+ * 网关会话的绝对寿命(ISO 8601;登录/授权兑换响应解析而来)。
53
+ * 未知(旧网关不下发过期属性)时不写该键——保活决策会退回「按假定寿命估算」。
54
+ */
55
+ sessionExpiresAt?: string;
49
56
  refreshRequest?: {
50
57
  at?: string;
51
58
  };
@@ -99,6 +106,8 @@ export declare function provisionSession(opts: {
99
106
  user: GatewayUser;
100
107
  /** 写入状态的账号名;缺省则不写(浏览器登录在网关未回传用户名时保留旧值)。 */
101
108
  username?: string | undefined;
109
+ /** 网关回传的会话绝对寿命(ISO);缺省则不写该键(旧网关无过期信息)。 */
110
+ sessionExpiresAt?: string | undefined;
102
111
  /** 成功时一并清掉的请求字段(表单登录 pendingLogin / 浏览器登录 browserLoginRequest)。 */
103
112
  requestUnset?: readonly string[];
104
113
  /** API_KEY 写好后触发的表单专属凭据整理(记住密码 / 临时密码);浏览器登录不传。 */
@@ -7,6 +7,11 @@ export declare const SESSION_REF = "AI_ROUTER_SESSION";
7
7
  export declare const PENDING_PASSWORD_REF = "AI_ROUTER_PENDING_PASSWORD";
8
8
  /** 勾选“记住密码”后保存的凭据 ref(由 credentials provider 加密存储)。 */
9
9
  export declare const REMEMBERED_PASSWORD_REF = "AI_ROUTER_REMEMBERED_PASSWORD";
10
+ /**
11
+ * 网关 refresh grant 的凭据 ref 名(M2 起 login/token 响应下发,登出/换新时清理)。
12
+ * 仅供 `runRefresh` 的静默重认证使用:绝不出现在状态或日志里,老网关没有它时静默弃用。
13
+ */
14
+ export declare const REFRESH_TOKEN_REF = "AI_ROUTER_REFRESH_TOKEN";
10
15
  /**
11
16
  * 注册到 llm-pi-ai 的协议路由表:路由名 → pi-ai 协议。
12
17
  * 网关按「入站协议 + 模型」匹配模型映射(internal/service/provider_service.go),
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 会话供给成功后的联动刷新(spec §4.2)。
3
+ *
4
+ * 会话刚建立 / 刚续上时,**按人 + 按会话**下发的派生清单必须立刻对齐一次:client policy、
5
+ * 受管 MCP、受管技能、工作台应用。这些清单各自的 30 分钟(策略是 6 小时)预热只保证
6
+ * 「最迟那么久之后」收敛;换了账号、会话重建、或长期挂机后重新登录,旧清单在预热窗口内
7
+ * 就是错的。所以供给成功是一条事件,而不是等下一个定时器。
8
+ *
9
+ * 这里把「四件事依次触发」收成一个纯函数:任一 handler 抛异常只记日志、不影响其余
10
+ * ——刷新是尽力而为的旁路,绝不能反过来把登录 / 续期主链路带崩。
11
+ */
12
+ export declare function buildSessionFanout(handlers: {
13
+ onPolicyRefresh(): void;
14
+ onManagedMcpRefresh(): void;
15
+ onManagedSkillsRefresh(): void;
16
+ onWorkbenchRefresh(): void;
17
+ /** 中文日志出口;缺省静默(handler 自身已各有吞错日志,这是第二道网)。 */
18
+ log?: (message: string) => void;
19
+ }): () => void;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * 会话心跳保活(spec §4.1b)。
3
+ *
4
+ * 网关会话是**服务端滑动续期**的:任意已认证请求在剩余寿命 < TTL/2 时会被续满。但桌面端
5
+ * 长时间挂机(不点任何功能)就不会发出任何已认证请求,续期永远不会触发,24h 后会话必过期。
6
+ * 本模块用一个兜底定时器(每小时醒一次),在「近期没有任何已认证请求成功」且「会话可能快
7
+ * 到期」时主动打一个轻量会话请求,把会话按住。
8
+ *
9
+ * 寿命预算:客户端不一定知道服务端真实 TTL(旧网关的 Set-Cookie 不带过期属性、`/api/auth/token`
10
+ * 与表单登录也不回 expires_at),所以用 {@link SESSION_TTL_ASSUMPTION_MS}(24h,与服务端缺省
11
+ * `session.duration` 一致)作为**安全上界**估算:只要在「剩余不足假定寿命一半」时打一次心跳,
12
+ * 服务端就一定会把它续满。新网关在带会话的成功响应里下发 `X-Session-Expires-At`,
13
+ * 心跳响应也可能带精确寿命——这些值先进内存估计({@link SessionSupervisor.noteAuthedSuccess}),
14
+ * 用来判定「还要不要打心跳」;只有心跳成功才写 settings(热路径零写入)。
15
+ *
16
+ * 只读端口 + 单向动作:本模块不持有任何凭据、不写日志里的会话值;会话已失效(401)的恢复
17
+ * 动作由调用方经 401 汇聚接线(Task A3),这里只记中文日志、绝不向宿主抛。
18
+ */
19
+ /** 假定会话总寿命(服务端 `session.duration` 缺省 24h;滑动续期的安全上界)。 */
20
+ export declare const SESSION_TTL_ASSUMPTION_MS: number;
21
+ /** 心跳兜底检查间隔(1h)。 */
22
+ export declare const SESSION_CHECK_INTERVAL_MS: number;
23
+ /**
24
+ * 会话恢复的冷却窗口(60s):多通道并发 401(voice / usage / MCP …)只触发一次恢复,
25
+ * 且恢复动作本身(写 refreshRequest → 静默重登或开登录门)不会在窗口内被反复排队。
26
+ */
27
+ export declare const SESSION_RECOVERY_COOLDOWN_MS = 60000;
28
+ export interface SessionSupervisorDeps {
29
+ /** 解析当前会话 cookie(未登录返回 undefined)。 */
30
+ getSession: () => Promise<string | undefined>;
31
+ /** 当前已知的会话绝对寿命(ISO 8601);未知返回 undefined。 */
32
+ getSessionExpiresAt: () => string | undefined;
33
+ /** 网关根地址;未配置返回 undefined(没配置就一个请求都不该发)。 */
34
+ getBaseURL: () => string | undefined;
35
+ /** 轻量会话请求:会话已失效时抛 `GatewayError('SESSION_EXPIRED')`;带服务端下发的寿命时回传。 */
36
+ ping: (sessionCookie: string) => Promise<{
37
+ expiresAt?: string;
38
+ } | undefined>;
39
+ /** 心跳成功后回写会话寿命(ISO):服务端给了精确值就用它,否则是估算出的安全上界。 */
40
+ patchSessionExpiresAt: (iso: string) => Promise<void>;
41
+ /**
42
+ * 会话已失效时触发一次恢复:宿主写 `refreshRequest`,由既有 200ms 消费循环里的
43
+ * `runRefresh` 接手(记住密码静默重登 / 清会话 / 写 lastError 开登录门)。
44
+ * 本模块**不新增恢复逻辑**,只负责去抖后调一次。
45
+ */
46
+ requestRecovery: () => Promise<void>;
47
+ /** 时钟注入(测试用);缺省 `Date.now`。 */
48
+ now?: () => number;
49
+ /** 中文日志出口;**绝不记录会话 cookie 值**。缺省静默。 */
50
+ log?: (message: string) => void;
51
+ }
52
+ export declare class SessionSupervisor {
53
+ private readonly deps;
54
+ private timer;
55
+ /** 上次「已知会话存活」的时刻;undefined = 本进程内还没确认过(重启后的首次检查照打)。 */
56
+ private lastAuthedSuccessAt;
57
+ /**
58
+ * 最近一次带会话成功响应里 `X-Session-Expires-At` 给出的**内存**寿命估计(原样时刻串)。
59
+ * 只活在内存里——热路径绝不写 settings(Review Focus 5),只有心跳成功才落盘。
60
+ */
61
+ private lastHeaderExpiresAt;
62
+ /** 恢复请求是否在途(单飞去抖)。 */
63
+ private recoveryInFlight;
64
+ /** 上次触发恢复的时刻(冷却窗口基准)。 */
65
+ private lastRecoveryAt;
66
+ constructor(deps: SessionSupervisorDeps);
67
+ /** 开始兜底检查;重复调用不叠加定时器。 */
68
+ start(): void;
69
+ /** 停止兜底检查(调用后零活动)。 */
70
+ stop(): void;
71
+ /**
72
+ * 带会话的请求成功 → 记活动时间(由 ai-router-client 的活动汇聚驱动)。
73
+ * `expiresAt` 为响应头 `X-Session-Expires-At` 给出的会话寿命:**只更新内存估计**,
74
+ * 绝不在此写 settings——热路径上每个成功请求都写盘会打爆设置面(Review Focus 5)。
75
+ * 不可解析的值直接丢弃(当作没给),不覆盖既有估计。
76
+ */
77
+ noteAuthedSuccess(expiresAt?: string): void;
78
+ /**
79
+ * 带会话的请求收到 401 → 去抖后触发一次会话恢复。
80
+ * 去抖有两层:`recoveryInFlight`(单飞,覆盖「恢复请求还没落地,同刻又来了 5 条 401」)
81
+ * 与 `SESSION_RECOVERY_COOLDOWN_MS` 冷却(覆盖恢复完成后短时间内的余波 401)。
82
+ * 恢复失败只记日志——apply 后写入面需要几秒才就绪,那一侧的失败不该惊动宿主。
83
+ */
84
+ reportAuthed401(source: string): void;
85
+ private now;
86
+ private log;
87
+ /**
88
+ * 一次兜底检查。判定顺序(早退即不发请求):
89
+ * 无会话 / 无 baseURL → 返回;距上次已认证成功不足 TTL/2 → 返回;
90
+ * 已知剩余寿命 > TTL/2 → 返回;否则 ping。
91
+ * 寿命未知也 ping——重启后的首次检查即校验会话是否还活着。
92
+ * 「已知剩余寿命」取 settings 值与内存估计(响应头)的**较大者**:头值只会更晚更准,
93
+ * 用较大者才不会因为 settings 里的旧估算偏早而白打心跳。
94
+ */
95
+ private wake;
96
+ /**
97
+ * 已知的会话到期时刻(epoch ms):settings 值与内存头值里**有效且较晚**的那个;
98
+ * 两者都没有(或都不可解析)返回 undefined = 寿命未知。
99
+ */
100
+ private knownExpiresAtMs;
101
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 会话绑定的 TTL 缓存原语(spec §4.4)。
3
+ *
4
+ * knowledge / memory / image / usage 各自手写过一遍「会话配 TTL」的内存缓存,语义已经
5
+ * 开始漂移。原语只做三件事,把「成功才 put、失败不 put、force 旁路」留给调用方:
6
+ * - 值与**会话键**绑定:读别人的会话键一定 miss(换账号绝不串号);
7
+ * - 命中条件是 `now - putAt < ttlMs`(严格小于,与既有实现逐字一致);
8
+ * - `invalidate` 支持全清与单键清理。
9
+ *
10
+ * **只保留一条条目**(不是每个键一条):这与迁移前的既有实现逐字等价——写入新会话的
11
+ * 缓存会顶掉旧会话那条。这条语义被 knowledge 的既有测试钉着(换账号再换回来必须重新
12
+ * 出网),也是「一次只服务一个登录账号」的诚实表达,故不改成 Map。
13
+ *
14
+ * 余量(skew)不在这里做:不同调用方的提前量不同(如 knowledge 令牌要扣 30s),
15
+ * 由调用方在 `ttlMs` 里自己扣——原语只认一个 TTL,不猜语义。
16
+ */
17
+ export declare class TtlCache<V> {
18
+ private readonly ttlMs;
19
+ private entry;
20
+ constructor(opts: {
21
+ ttlMs: number;
22
+ });
23
+ /** 命中返回缓存值;会话键不同或已到期返回 undefined。`now` 可注入(缺省 Date.now)。 */
24
+ get(sessionKey: string, now?: number): V | undefined;
25
+ /** 写入/覆盖缓存(新会话键会顶掉旧会话那条)。 */
26
+ put(sessionKey: string, value: V, now?: number): void;
27
+ /** 清掉指定会话键的条目(不影响别的键,虽然只会存在一条);不传参数则清空。 */
28
+ invalidate(sessionKey?: string): void;
29
+ }
@@ -11,5 +11,11 @@ export interface DeviceSession {
11
11
  client_ip: string;
12
12
  created_at: string;
13
13
  last_seen_at?: string | null;
14
+ /**
15
+ * 该会话的绝对到期时刻(ISO 8601,网关将来下发时才有)。
16
+ * 当前服务端不回该字段:字段缺失时保持现有行为(不据此做任何判定),将来下发后
17
+ * 心跳即可顺带校验「本机设备会话是否仍在列表且未过期」。
18
+ */
19
+ expires_at?: string;
14
20
  current: boolean;
15
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hiwork-core",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "type": "module",
5
5
  "description": "HiWork 企业统一登录与模型自动供给插件(ai-router 网关登录,Web 表单,无 CLI 认证)",
6
6
  "license": "UNLICENSED",
@@ -42,33 +42,34 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@deepseek-ai/cordis": "~4.0.4",
45
- "@deepseek-ai/dsh-client-ui-layout": ">=0.1.0-rc.5 <0.2.0"
45
+ "@deepseek-ai/dsh-client-ui-layout": ">=0.1.0-rc.5 <0.2.0 || >=0.2.0-rc.1 <0.3.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@deepseek-ai/cordis": "4.0.4",
49
49
  "@deepseek-ai/cordis-plugin-loader": "1.0.5",
50
- "@deepseek-ai/dsh-agent-default-model": "0.1.7-rc.2",
51
- "@deepseek-ai/dsh-api-remotes": "0.1.7-rc.2",
52
- "@deepseek-ai/dsh-app-boot": "0.1.7-rc.2",
53
- "@deepseek-ai/dsh-attachment": "0.1.7-rc.2",
54
- "@deepseek-ai/dsh-client-store": "0.1.7-rc.2",
55
- "@deepseek-ai/dsh-client-ui-chat": "0.1.7-rc.2",
56
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2",
57
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.2",
58
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.2",
59
- "@deepseek-ai/dsh-client-ui-session": "0.1.7-rc.2",
60
- "@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.2",
61
- "@deepseek-ai/dsh-client-ui-sidebar": "0.1.7-rc.2",
62
- "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
63
- "@deepseek-ai/dsh-config-editor": "0.1.7-rc.2",
64
- "@deepseek-ai/dsh-credentials": "0.1.7-rc.2",
65
- "@deepseek-ai/dsh-credentials-local": "0.1.7-rc.2",
66
- "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
67
- "@deepseek-ai/dsh-llm-pi-ai": "0.1.7-rc.2",
68
- "@deepseek-ai/dsh-settings": "0.1.7-rc.2",
69
- "@deepseek-ai/dsh-tools": "0.1.7-rc.2",
70
- "@deepseek-ai/dsh-web": "0.1.7-rc.2",
71
- "@deepseek-ai/dsh-workspace": "0.1.7-rc.2",
50
+ "@deepseek-ai/dsh-agent-default-model": "0.2.0-rc.1",
51
+ "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.1",
52
+ "@deepseek-ai/dsh-app-boot": "0.2.0-rc.1",
53
+ "@deepseek-ai/dsh-attachment": "0.2.0-rc.1",
54
+ "@deepseek-ai/dsh-client-store": "0.2.0-rc.1",
55
+ "@deepseek-ai/dsh-client-ui-chat": "0.2.0-rc.1",
56
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.1",
57
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
58
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
59
+ "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.1",
60
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.1",
61
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.2.0-rc.1",
62
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
63
+ "@deepseek-ai/dsh-config-editor": "0.2.0-rc.1",
64
+ "@deepseek-ai/dsh-credentials": "0.2.0-rc.1",
65
+ "@deepseek-ai/dsh-credentials-local": "0.2.0-rc.1",
66
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.1",
67
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.0-rc.1",
68
+ "@deepseek-ai/dsh-settings": "0.2.0-rc.1",
69
+ "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.1",
70
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.1",
71
+ "@deepseek-ai/dsh-web": "0.2.0-rc.1",
72
+ "@deepseek-ai/dsh-workspace": "0.2.0-rc.1",
72
73
  "@deepseek-ai/schemastery": "3.18.4",
73
74
  "@types/node": "^22",
74
75
  "@types/react": "^18",
@@ -81,7 +82,7 @@
81
82
  "typescript": "^5",
82
83
  "vitest": "^3",
83
84
  "yaml": "^2.8.1",
84
- "@deepseek-ai/dsh-client-ui-layout": "0.1.7-rc.2"
85
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.0-rc.1"
85
86
  },
86
87
  "scripts": {
87
88
  "typecheck": "tsc --noEmit",