hiwork-core 0.0.2 → 0.0.6

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.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * 「每日一句」的客户端取数层:经 `/hiwork-daily` loopback RPC 拉取网关当日条目,
3
+ * 成功结果按服务端日期做全天缓存、失败 5 分钟 TTL 防抖、随机条目永不入缓存。
4
+ *
5
+ * 铁律:
6
+ * - 客户端**不持有**会话 cookie,也**不直接 fetch 网关**——一切经 Host 频道;
7
+ * - `null` 即「暂无可展示内容」(数据未就绪 / RPC 失败 / 当日池空一律归一为 null),
8
+ * 视图对各种情况一视同仁:不插卡片、不出任何错误 UI;
9
+ * - rAF 观察循环在流式输出时逐帧触发,**任何未定状态都不得重复打网关**:
10
+ * 成功按服务端 `date` 缓存全天(含「池空」这种 `item:null` 的成功)、失败 5 分钟内
11
+ * 直接短路、并发请求合并复用同一次 RPC。
12
+ *
13
+ * client bundle 纯度:本文件不 import 任何 `@deepseek-ai/*` 运行时值,也不 import
14
+ * Host 代码(网关 JSON 字段在这里按结构重声明,字段名与网关一致)。
15
+ */
16
+ /** RPC 频道;必须与 Host `src/daily-rpc.ts` 的 `DAILY_RPC_CHANNEL` 一致(由测试锁定)。 */
17
+ export declare const DAILY_CHANNEL = "/hiwork-daily";
18
+ /** 端点白名单;必须与 Host `DAILY_RPC_ENDPOINTS` 一致(由测试锁定)。 */
19
+ export declare const DAILY_ENDPOINTS: readonly ["today", "random"];
20
+ export type DailyEndpoint = (typeof DAILY_ENDPOINTS)[number];
21
+ /** 失败防抖 TTL:5 分钟——观察循环逐帧触发,绝不逐帧重试网关。 */
22
+ export declare const DAILY_FAILURE_TTL_MS: number;
23
+ /**
24
+ * 单条每日一句的视图形状(网关 JSON 按结构重声明后的归一结果):
25
+ * 可选字段缺省一律归一为空串 / 空数组,视图据此隐藏对应区块。
26
+ */
27
+ export interface DailyQuoteView {
28
+ id: number;
29
+ /** 卡片主行(蓝渐变大字)。 */
30
+ content: string;
31
+ /** 卡片主行上方小字;空串 = 缺省(隐藏)。 */
32
+ subtitle: string;
33
+ /** 弹窗标题;空串 = 缺省(弹窗只展示 content)。 */
34
+ title: string;
35
+ /** 弹窗 meta 出处;空串 = 缺省(隐藏)。 */
36
+ source: string;
37
+ /** 弹窗正文段落;空数组 = 缺省(隐藏整块)。 */
38
+ body: readonly string[];
39
+ }
40
+ /** 当日条目视图:附带服务端东八区日期(YYYY-MM-DD,弹窗与跨日重建判断直接使用)。 */
41
+ export interface DailyTodayView extends DailyQuoteView {
42
+ date: string;
43
+ }
44
+ /** 宿主 loopback RPC 调用面(结构兼容 `ctx.connection.rpc`)。 */
45
+ export interface DailyRpcPort {
46
+ call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
47
+ }
48
+ /** 带稳定 code 的取数错误(message 已由 Host 消毒,可直接展示)。 */
49
+ export declare class DailyRpcError extends Error {
50
+ readonly code: string;
51
+ constructor(code: string, message: string);
52
+ }
53
+ /** 组件消费的最小取数面(便于视图与测试注入假实现)。 */
54
+ export interface DailyQuoteLoader {
55
+ getToday(): Promise<DailyQuoteView | null>;
56
+ getRandom(excludeId?: number): Promise<DailyQuoteView | null>;
57
+ }
58
+ /**
59
+ * 解开 DSH Connection 的结果信封。
60
+ * 宿主返回 `{ok:true,value}` / `{ok:false,error:{code,message}}`;形状不合法时抛
61
+ * 通用失败,绝不把未知结构当数据渲染。
62
+ */
63
+ export declare function unwrapDailyResult<T>(value: unknown): T;
64
+ /**
65
+ * 结构化校验单条条目:id 必须是正整数、content 必填;可选字段缺省归一为空串 / 空数组。
66
+ * 形状非法返回 undefined(调用方按「本次失败」处理,绝不渲染未知结构)。
67
+ */
68
+ export declare function parseQuoteItem(value: unknown): DailyQuoteView | undefined;
69
+ /**
70
+ * 结构化校验 today 响应:`date` 必须是 YYYY-MM-DD、`item` 为 null(池空)或合法条目。
71
+ * 形状非法返回 undefined(按失败处理)。
72
+ */
73
+ export declare function parseTodayResponse(value: unknown): {
74
+ date: string;
75
+ item: DailyQuoteView | null;
76
+ } | undefined;
77
+ /** 结构化校验 random 响应:`item` 为 null 或合法条目;形状非法返回 undefined。 */
78
+ export declare function parseRandomResponse(value: unknown): {
79
+ item: DailyQuoteView | null;
80
+ } | undefined;
81
+ /**
82
+ * 本地日历日的纪元日数(跨日检测的唯一时钟):观察循环的 60 秒轮询与本仓库的
83
+ * 缓存过期判断共用同一公式,跨午夜时由任一侧触发重拉。
84
+ */
85
+ export declare function localDayIndex(date?: Date): number;
86
+ /**
87
+ * 每日一句取数仓库:全天缓存 + 失败防抖 + 并发合并;随机条目永不入缓存。
88
+ */
89
+ export declare class DailyDataStore implements DailyQuoteLoader {
90
+ private readonly rpc;
91
+ private entry;
92
+ private failureAt;
93
+ private pending;
94
+ private readonly now;
95
+ private readonly localDay;
96
+ private readonly failureTtlMs;
97
+ constructor(rpc: DailyRpcPort, options?: {
98
+ now?: (() => number) | undefined;
99
+ localDay?: (() => number) | undefined;
100
+ failureTtlMs?: number | undefined;
101
+ });
102
+ /** 清空缓存与防抖状态(登出 / 换账号时调用)。 */
103
+ clear(): void;
104
+ /**
105
+ * 当日条目:同一本地日内命中缓存直接返回(不发 RPC);失败防抖期内返回 null 不
106
+ * 重复打网关;并发调用合并为同一次 RPC。返回 null 即「暂无可展示内容」。
107
+ */
108
+ getToday(): Promise<DailyTodayView | null>;
109
+ /**
110
+ * 换一句:排除当前条目的随机条目;**永不入缓存**(与「临时预览」语义一致),
111
+ * 失败 / 暂无随机条目返回 null(视图保持当前条目、无任何提示)。
112
+ */
113
+ getRandom(excludeId?: number): Promise<DailyQuoteView | null>;
114
+ /** 单次 today 拉取:成功(含池空)写缓存;任何失败进入 5 分钟防抖并返回 null。 */
115
+ private fetchToday;
116
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
2
2
  export type { FeatureRenderContext, FeatureRegistry, HiWorkFeature } from './feature-registry.ts';
3
3
  export type { HiWorkCoreClientFace } from './contracts.ts';
4
- /** 浏览器插件契约(dsh client module system)。connection 随 IApiClient 移除后不再依赖。 */
4
+ /** 浏览器插件契约(dsh client module system)。connection 承载 loopback RPC(见 src/client/usage-data.ts)。 */
5
5
  export declare const inject: string[];
6
6
  /** 挂载网关模型设置 section(一级 tab)与未登录弹窗。 */
7
7
  export declare function apply(ctx: ClientContext): void;
@@ -1,6 +1,15 @@
1
1
  import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
2
2
  import type { CredentialStorePort } from './controller.ts';
3
+ import type { LoginError } from './types.ts';
3
4
  import { type AiRouterCardSettings } from './controller.ts';
5
+ /**
6
+ * 登录错误文案:把 Host 的稳定错误码翻成用户能理解的说法。
7
+ * - `GATEWAY_UNREACHABLE` 的 Host 文案会带上 `baseURL` / `HTTP 500` 这类实现细节,统一换成人话;
8
+ * - `AUTH_FAILED` 的三种成因(一次性 code 过期 / state 校验失败 / 授权被拒)Host 已按成因
9
+ * 给出中文文案,优先沿用;文案缺失时才用覆盖全部成因的兜底句;
10
+ * - 其余错误码保持既有行为(原样展示 Host message),不影响 NO_KEY / KEY_* 等既有呈现。
11
+ */
12
+ export declare function describeLoginError(error: LoginError | undefined): string | undefined;
4
13
  export declare function loginGateVisible(snapshot: {
5
14
  status: string;
6
15
  value?: {
@@ -0,0 +1,119 @@
1
+ import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { TurnTailChatData, TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client';
3
+ /** 计价端点名;必须与 Host `src/usage-rpc.ts` 的 `USAGE_RPC_ENDPOINTS` 一致(由测试锁定)。 */
4
+ export declare const SESSION_COST_ENDPOINT = "estimate";
5
+ /** 缓存 TTL:5 分钟(与设置页取数一致;同一轮的金额不会随滚动反复请求)。 */
6
+ export declare const SESSION_COST_CACHE_TTL_MS: number;
7
+ /** 悬停说明(设计 §7:明确「按网关价格估算」)。 */
8
+ export declare const SESSION_COST_HINT = "\u6309\u7F51\u5173\u4EF7\u683C\u4F30\u7B97\uFF0C\u4EC5\u4F9B\u53C2\u8003\uFF1B\u4EE5\u300C\u7528\u91CF\u4E0E\u6210\u672C\u300D\u4E2D\u7684\u6743\u5A01\u8D26\u5355\u4E3A\u51C6";
9
+ /** 估算标记(多线路/多模型无法拆分,或渠道未解析)。 */
10
+ export declare const SESSION_COST_ESTIMATED_SUFFIX = "\uFF08\u4F30\u7B97\uFF09";
11
+ /** 提交给 Host `estimate` 端点的单项(`route` 是 **DSH 线路名**,翻译在 Host 侧做)。 */
12
+ export interface SessionCostItem {
13
+ route: string;
14
+ model: string;
15
+ input_tokens: number;
16
+ output_tokens: number;
17
+ cache_read_tokens?: number;
18
+ cache_write_tokens?: number;
19
+ /** 该轮结束时刻(RFC3339):网关据此取当时的时段价。 */
20
+ at?: string;
21
+ }
22
+ /** `select` 的返回值(链槽的 `matched` 共享)。 */
23
+ export interface SessionCostMatch {
24
+ /** 该轮要计价的项目(当前 DSH 契约下恒为 1 项,见文件头注释)。 */
25
+ items: SessionCostItem[];
26
+ /** 该轮 turn 号(`TurnTailChatData.turn`),缓存键的一部分。 */
27
+ turn: number;
28
+ /** 该轮出现多个 (线路, 模型) 组合 → 金额只能按首条估,UI 必须标注「估算」。 */
29
+ ambiguous: boolean;
30
+ }
31
+ /** 一次计价的结果(只保留可展示所需的最小字段)。 */
32
+ export interface SessionCostResult {
33
+ cost: number;
34
+ channelResolved: boolean;
35
+ }
36
+ /** Host loopback RPC 调用面(结构兼容 `ctx.connection.rpc`)。 */
37
+ export interface SessionCostRpcPort {
38
+ call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
39
+ }
40
+ /**
41
+ * 从 turn-tail 数据派生该轮的计价项。
42
+ *
43
+ * 入参用 ui-chat 的**真实类型** `TurnTailChatData`:DSH 改字段名时这里先编译失败,
44
+ * 而不是悄悄取不到用量;运行时仍逐字段校验(宿主 DSH 版本可能与开发依赖不同版本)。
45
+ *
46
+ * 返回 `null`(→ 不挂载、不显示任何东西)的情形:无数据、无 `tokenUsage`、
47
+ * `routes` 缺失/为空/归属不完整、四个桶全为 0(没有可计价的内容)、形状不合法。
48
+ * @param data - `owner.turn.data.get('turn-tail')` 的原始值。
49
+ * @returns 计价项 + turn 号 + 归属是否歧义;无法计价时为 null。
50
+ */
51
+ export declare function sessionCostMatchOf(data: TurnTailChatData | undefined): SessionCostMatch | null;
52
+ /**
53
+ * 链槽 `conversation.chat.turnTail` 的路由选择器:只有该轮拿得到用量时才占用位置。
54
+ * @param owner - turn-tail owner(turn / seq / openFile)。
55
+ * @returns 计价项;取不到用量时 null(框架不挂载组件)。
56
+ */
57
+ export declare function selectSessionCost(owner: TurnTailOwnerProps): SessionCostMatch | null;
58
+ /** 缓存键:会话 + 轮次 + 计价项内容(滚动/重挂载不会重复请求同一轮)。 */
59
+ export declare function sessionCostKey(sessionId: string, match: Pick<SessionCostMatch, 'turn' | 'items'>): string;
60
+ /**
61
+ * 归一化 Host `estimate` 的返回值:只接受 `priced:true` 且金额可信的结果。
62
+ *
63
+ * `priced:false`(无会话 / 无可用线路 / 网关未计价)与形状非法一律返回 undefined——
64
+ * 视图据此**不显示任何金额**。
65
+ * @param value - 信封解开后的值。
66
+ * @returns 可展示的金额与渠道解析标记;不可展示时 undefined。
67
+ */
68
+ export declare function normalizeSessionCost(value: unknown): SessionCostResult | undefined;
69
+ /**
70
+ * 逐轮计价仓库:按 {@link sessionCostKey} 缓存(TTL 5 分钟)+ 在途请求去重。
71
+ *
72
+ * 语义:
73
+ * - 命中未过期缓存 → 不发 RPC;
74
+ * - 同一键已有在途请求 → 复用同一个 Promise(一次挂载/重挂载只发一次);
75
+ * - 只有 `priced:true` 的结果才进缓存:未计价 / 失败不缓存,登录后重挂载即可重试;
76
+ * - 任何异常都收敛成 undefined,绝不抛给对话渲染。
77
+ */
78
+ export declare class SessionCostStore {
79
+ private readonly rpc;
80
+ private readonly entries;
81
+ private readonly pending;
82
+ private readonly now;
83
+ private readonly ttlMs;
84
+ constructor(rpc: SessionCostRpcPort, options?: {
85
+ now?: (() => number) | undefined;
86
+ ttlMs?: number | undefined;
87
+ });
88
+ /** 当前缓存条目数(测试断言用)。 */
89
+ get size(): number;
90
+ /** 清空缓存(登出 / 切换账号)。 */
91
+ clear(): void;
92
+ estimate(input: {
93
+ sessionId: string;
94
+ turn: number;
95
+ items: readonly SessionCostItem[];
96
+ signal?: AbortSignal | undefined;
97
+ }): Promise<SessionCostResult | undefined>;
98
+ private request;
99
+ }
100
+ /** 组件注入面:逐轮计价仓库(客户端不持有会话 cookie,也不直连网关)。 */
101
+ export interface SessionCostFace {
102
+ store: SessionCostStore;
103
+ }
104
+ /**
105
+ * 链槽组件 props:owner(turn/seq/openFile)+ 会话标准件(`sessionId`)+ 注入面 + `matched`。
106
+ * `sessionId` 由框架下发(ui-session 的 SessionStandardProps),不在这里重复声明。
107
+ */
108
+ export type SessionCostProps = PropsRuntime<'conversation.chat.turnTail'> & InjectFace<SessionCostFace> & {
109
+ matched: SessionCostMatch;
110
+ };
111
+ /**
112
+ * 渲染一轮的估算金额。
113
+ *
114
+ * 三种状态:全部计价且渠道已解析、单组合 → `≈ ¥x`;渠道未解析或该轮多组合 →
115
+ * `≈ ¥x(估算)`;其余(未计价 / 失败 / 加载中)→ **不渲染任何内容**。
116
+ * @param props - 链槽 props。
117
+ * @returns 金额文本节点,或 null(不影响对话渲染)。
118
+ */
119
+ export declare function SessionCost({ matched, sessionId, store }: SessionCostProps): import("react").JSX.Element | null;
@@ -48,10 +48,22 @@ export declare const textInput: CSSProperties;
48
48
  * 内容区根节点类名是 CSS module 哈希前缀(如 wSkVaW_root),语义后缀稳定;
49
49
  * 用直接子链 `[class*="_centerCol"] > div > [class*="_root"]` 恰好命中会话区
50
50
  * 根节点(实测唯一),避免误伤内容区里 10 个嵌套组件的 _root。
51
+ * 框架底色同理不能只认 `_frame` 后缀:该后缀被 6 个 CSS module 复用(主框架
52
+ * AppFrame 之外还有会话右侧快速索引 TurnNavigator、附件图 MessageImage、
53
+ * 提问面板 QuestionComposer/PlanReviewPanel、只读输入框 SubagentReadOnlyComposer),
54
+ * 裸 `[class*="_frame"]` 会把这些 `.frame` 一起涂成侧栏灰(快速索引原本是透明底)。
55
+ * 故用 frameSurfaceSelector 的 `:has(> _sidebarCol)` 锁死到 AppFrame 本体——
56
+ * 只有主框架直接包含侧栏列。
51
57
  * 圆角裁切天然成立:会话区根节点自带 overflow:hidden,头部页签/气泡/输入框
52
58
  * 都会被裁切;颜色全部走主题变量,亮暗自动切换。
53
59
  * 侧栏右缘 0.5px 原生分界线一并归零(与内容区无边框保持一致)。
54
60
  */
61
+ /**
62
+ * 主框架底色的命中选择器:`[class*="_frame"]` 只锁定直接包含侧栏列的 AppFrame。
63
+ * 不用裸后缀,避免误伤同名 `.frame` 的其他组件(见上方注释);
64
+ * 后续要改框架底色,一律复用此选择器。
65
+ */
66
+ export declare const frameSurfaceSelector = "[class*=\"_frame\"]:has(> [class*=\"_sidebarCol\"])";
55
67
  export declare const frameCss: string;
56
68
  /**
57
69
  * 隐藏宿主动作入口:
@@ -1,5 +1,5 @@
1
1
  /** 稳定错误码,供 settings card 分支渲染。 */
2
- export type LoginErrorCode = 'AUTH_FAILED' | 'RATE_LIMITED' | 'GATEWAY_UNREACHABLE' | 'NO_KEY_ASSIGNED' | 'SESSION_EXPIRED' | 'KEY_DISABLED' | 'KEY_INVALID';
2
+ export type LoginErrorCode = 'AUTH_FAILED' | 'RATE_LIMITED' | 'GATEWAY_UNREACHABLE' | 'NO_KEY_ASSIGNED' | 'SESSION_EXPIRED' | 'KEY_DISABLED' | 'KEY_INVALID' | 'NOT_FOUND';
3
3
  /** 一个稳定的、可安全展示的登录/刷新错误。 */
4
4
  export interface LoginError {
5
5
  code: LoginErrorCode;
@@ -0,0 +1,276 @@
1
+ /**
2
+ * 设置页「用量与成本」的客户端取数层:经 `/hiwork-usage` loopback RPC 拉取网关
3
+ * 权威账单,做 5 分钟缓存、部分失败保留上次成功数据、全部失败进入降级态。
4
+ *
5
+ * 铁律:
6
+ * - 客户端**不持有**会话 cookie,也**不直接 fetch 网关**——一切经 Host 频道;
7
+ * - 客户端不做任何定价 / 折算公式,只做**展示口径的聚合**(同一份权威数字求和、
8
+ * 按自然月过滤趋势桶);
9
+ * - 失败绝**不**降级成 0 或占位数字:缺数据的字段一律 `undefined`,由视图渲染 `—`。
10
+ *
11
+ * client bundle 纯度:本文件不 import 任何 `@deepseek-ai/*` 运行时值,也不 import
12
+ * Host 代码(`src/**` 之外的类型在这里按结构重声明,字段名与网关 JSON 一致)。
13
+ */
14
+ /** RPC 频道;必须与 Host `src/usage-rpc.ts` 的 `USAGE_RPC_CHANNEL` 一致(由测试锁定)。 */
15
+ export declare const USAGE_CHANNEL = "/hiwork-usage";
16
+ /** 端点白名单;必须与 Host `USAGE_RPC_ENDPOINTS` 一致(由测试锁定)。 */
17
+ export declare const USAGE_ENDPOINTS: readonly ["summary", "trends", "model-trends", "recharges", "keys", "key", "estimate"];
18
+ export type UsageEndpoint = (typeof USAGE_ENDPOINTS)[number];
19
+ /** 缓存 TTL:5 分钟(spec §5「结果缓存 5 分钟」)。 */
20
+ export declare const USAGE_CACHE_TTL_MS: number;
21
+ /** 趋势与按模型分布使用的时间窗口(plan Task B2 Step 1:`trends(30d)`)。 */
22
+ export declare const USAGE_TREND_RANGE: "30d";
23
+ /** KPI「今日花费」的窗口:网关按自然日切分。 */
24
+ export declare const USAGE_TODAY_RANGE: "today";
25
+ /** KPI「本月花费 / 本月请求数」的窗口:本月 1 日 00:00 → 现在(网关 `meRange` 的 `mtd`)。 */
26
+ export declare const USAGE_MONTH_RANGE: "mtd";
27
+ /**
28
+ * 旧网关不支持 `mtd` 时的提示:网关不认该值会按缺省回退成 `24h` 并把 `range` 回填
29
+ * `24h`,此时本月 KPI 必须显示 `—`——**绝不拿 24 小时的数字冒充本月**。
30
+ * 文案只讲用户能懂的事,不带接口参数 / 地址等实现细节。
31
+ */
32
+ export declare const USAGE_MONTH_RANGE_UNSUPPORTED = "\u5F53\u524D\u7F51\u5173\u7248\u672C\u6682\u4E0D\u652F\u6301\u672C\u6708\u7EDF\u8BA1\uFF0C\u672C\u6708\u82B1\u8D39\u4E0E\u8BF7\u6C42\u6570\u4E0D\u53EF\u7528";
33
+ /** 部分失败时的提示文案(保留上次成功数据)。 */
34
+ export declare const USAGE_STALE_TEXT = "\u6570\u636E\u53EF\u80FD\u4E0D\u662F\u6700\u65B0";
35
+ /** 全部失败时的降级文案(不渲染任何数字)。 */
36
+ export declare const USAGE_UNAVAILABLE_TEXT = "\u7528\u91CF\u6570\u636E\u6682\u4E0D\u53EF\u7528";
37
+ /** 加载中提示。 */
38
+ export declare const USAGE_LOADING_TEXT = "\u6B63\u5728\u52A0\u8F7D\u7528\u91CF\u6570\u636E\u2026";
39
+ /** 无任何用量记录时的空态文案。 */
40
+ export declare const USAGE_EMPTY_TEXT = "\u6682\u65E0\u7528\u91CF\u8BB0\u5F55";
41
+ /** 无充值记录时的空态文案。 */
42
+ export declare const USAGE_NO_RECHARGE_TEXT = "\u6682\u65E0\u5145\u503C\u8BB0\u5F55";
43
+ /** 缺失单个数据源时占位符(**不是数字**)。 */
44
+ export declare const USAGE_MISSING_TEXT = "\u2014";
45
+ /** 网关支持的用量时间窗口(`mtd` 需网关侧支持,见 USAGE_MONTH_RANGE_UNSUPPORTED)。 */
46
+ export type UsageRange = 'today' | 'yesterday' | '24h' | '7d' | '30d' | 'mtd';
47
+ /** GET /api/me/analytics/summary 的按模型汇总项。 */
48
+ export interface UsageModelSummary {
49
+ model: string;
50
+ request_count: number;
51
+ total_tokens: number;
52
+ total_cost: number;
53
+ cache_hit_rate: number;
54
+ }
55
+ /** GET /api/me/analytics/summary 响应。 */
56
+ export interface UsageSummary {
57
+ range: string;
58
+ items: UsageModelSummary[];
59
+ }
60
+ /** 趋势桶(`period` 为 RFC3339)。 */
61
+ export interface UsageTrendPoint {
62
+ period: string;
63
+ prompt_tokens: number;
64
+ completion_tokens: number;
65
+ total_tokens: number;
66
+ request_count: number;
67
+ total_cost: number;
68
+ }
69
+ /** GET /api/me/analytics/trends 响应。 */
70
+ export interface UsageTrends {
71
+ range: string;
72
+ group_by: string;
73
+ trends: UsageTrendPoint[];
74
+ }
75
+ /** 趋势桶的按模型拆分项。 */
76
+ export interface UsageModelTrendBreakdown {
77
+ name: string;
78
+ total_tokens: number;
79
+ request_count: number;
80
+ total_cost: number;
81
+ }
82
+ /** 按模型拆分的趋势桶。 */
83
+ export interface UsageModelTrendPoint extends UsageTrendPoint {
84
+ breakdown?: UsageModelTrendBreakdown[];
85
+ }
86
+ /** GET /api/me/analytics/model-trends 响应。 */
87
+ export interface UsageModelTrends {
88
+ range: string;
89
+ group_by: string;
90
+ trends: UsageModelTrendPoint[];
91
+ }
92
+ /** GET /api/me/key/recharges 的一项(充值台账)。 */
93
+ export interface UsageRecharge {
94
+ id: string;
95
+ model_name: string;
96
+ amount: number;
97
+ type: string;
98
+ operator: string;
99
+ note: string;
100
+ created_at: string;
101
+ }
102
+ /** GET /api/me/keys 的轻量 Key 项(多 Key 切换器数据源)。 */
103
+ export interface UsageKey {
104
+ id: string;
105
+ employee_name?: string;
106
+ team_name?: string;
107
+ enabled?: boolean;
108
+ key_masked?: string;
109
+ }
110
+ /** GET /api/me/key 的单个模型额度项(KPI「剩余预算」的权威来源)。 */
111
+ export interface UsageBudget {
112
+ model_name: string;
113
+ total_budget?: number | null;
114
+ consumed_budget?: number | null;
115
+ remaining_budget?: number | null;
116
+ }
117
+ /** GET /api/me/key 响应的相关子集。 */
118
+ export interface UsageKeyDetail {
119
+ key: {
120
+ id: string;
121
+ enabled?: boolean;
122
+ employee_name?: string;
123
+ };
124
+ budgets: UsageBudget[];
125
+ }
126
+ /** 各端点的最近一次成功响应;未成功过 / 尚未请求时该字段为 undefined。 */
127
+ export interface UsageData {
128
+ /** `summary(range: 'today')`:KPI「今日花费」。 */
129
+ summary: UsageSummary | undefined;
130
+ /**
131
+ * `summary(range: 'mtd')`:KPI「本月花费 / 本月请求数」。
132
+ * 旧网关不支持 `mtd` 时这里保持 undefined(守卫见 {@link isMonthRangeSupported})。
133
+ */
134
+ summaryMonth: UsageSummary | undefined;
135
+ trends: UsageTrends | undefined;
136
+ modelTrends: UsageModelTrends | undefined;
137
+ recharges: UsageRecharge[] | undefined;
138
+ keys: UsageKey[] | undefined;
139
+ key: UsageKeyDetail | undefined;
140
+ }
141
+ /** 空数据(降级态使用;不含任何数字)。 */
142
+ export declare const EMPTY_USAGE_DATA: UsageData;
143
+ /** 取数结果。 */
144
+ export interface UsageLoadResult {
145
+ /** 可用数据;`unavailable` 为 true 时是空数据。 */
146
+ data: UsageData;
147
+ /** 至少一个端点失败(其余数据仍是上次成功值)。 */
148
+ stale: boolean;
149
+ /** 全部端点失败:视图必须显示降级文案,不得渲染任何数字。 */
150
+ unavailable: boolean;
151
+ /** 首个失败的可展示错误(code 来自 Host,message 已消毒)。 */
152
+ error: {
153
+ code: string;
154
+ message: string;
155
+ } | undefined;
156
+ /** 本次数据的抓取时刻(毫秒);降级态为 undefined。 */
157
+ fetchedAt: number | undefined;
158
+ /** 是否命中缓存(未发起任何 RPC)。 */
159
+ cached: boolean;
160
+ }
161
+ /** 取数入参。 */
162
+ export interface UsageLoadInput {
163
+ keyId?: string | undefined;
164
+ range?: UsageRange | undefined;
165
+ /** 手动刷新:强制绕过缓存。 */
166
+ force?: boolean | undefined;
167
+ signal?: AbortSignal | undefined;
168
+ }
169
+ /** 组件消费的最小取数面(便于测试注入假实现)。 */
170
+ export interface UsageLoader {
171
+ load(input?: UsageLoadInput): Promise<UsageLoadResult>;
172
+ }
173
+ /** 宿主 loopback RPC 调用面(结构兼容 `ctx.connection.rpc`)。 */
174
+ export interface UsageRpcPort {
175
+ call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
176
+ }
177
+ /** 带稳定 code 的取数错误(message 已由 Host 消毒,可直接展示)。 */
178
+ export declare class UsageRpcError extends Error {
179
+ readonly code: string;
180
+ constructor(code: string, message: string);
181
+ }
182
+ /**
183
+ * 解开 DSH Connection 的结果信封。
184
+ * 宿主返回 `{ok:true,value}` / `{ok:false,error:{code,message}}`;形状不合法时抛
185
+ * 通用失败,绝不把未知结构当数据渲染。
186
+ */
187
+ export declare function unwrapUsageResult<T>(value: unknown): T;
188
+ /** 任意异常 → 可展示的 `{code, message}`(传输层异常不暴露原始 message)。 */
189
+ export declare function usageErrorOf(error: unknown): {
190
+ code: string;
191
+ message: string;
192
+ };
193
+ /** 缓存键:`keyId + range`(spec §5 / plan Task B2)。 */
194
+ export declare function usageCacheKey(keyId: string | undefined, range: UsageRange): string;
195
+ /** 单个端点的请求计划。 */
196
+ interface EndpointPlan {
197
+ endpoint: UsageEndpoint;
198
+ field: keyof UsageData;
199
+ payload: {
200
+ keyId?: string | undefined;
201
+ range?: UsageRange | undefined;
202
+ };
203
+ }
204
+ /**
205
+ * 并发请求计划:
206
+ * - `summary('today')` → 今日花费;`summary('mtd')` → 本月花费 / 请求数(同一端点两次,
207
+ * 各自独立成败,旧网关的 `mtd` 回退只影响本月 KPI);
208
+ * - 趋势与按模型分布走选定窗口(默认 30 天);台账 / key 带 keyId;keys 不带。
209
+ */
210
+ export declare function usageEndpointPlans(input: {
211
+ keyId?: string | undefined;
212
+ range: UsageRange;
213
+ }): EndpointPlan[];
214
+ /**
215
+ * 旧网关守卫:请求了 `range=mtd`,响应 `range` 必须原样回填 `mtd`。
216
+ * 网关不认该值时按缺省回退成 `24h`(响应 `range='24h'`),此时**不得**把它当本月数字。
217
+ */
218
+ export declare function isMonthRangeSupported(value: unknown): boolean;
219
+ /**
220
+ * 用量取数仓库:缓存 + 降级 + 保留上次成功数据。
221
+ *
222
+ * 语义:
223
+ * - 命中未过期缓存 → 不发任何 RPC(`cached: true`);
224
+ * - 任一端点失败 → 该端点沿用上次成功值,`stale: true`;
225
+ * - **全部端点失败** → 返回 `unavailable: true` 且 `data` 为空,并清掉本键缓存
226
+ * (下次挂载 / 刷新会重新请求,而不是继续端出旧数字)。
227
+ */
228
+ export declare class UsageDataStore implements UsageLoader {
229
+ private readonly rpc;
230
+ private readonly entries;
231
+ private readonly now;
232
+ private readonly ttlMs;
233
+ constructor(rpc: UsageRpcPort, options?: {
234
+ now?: (() => number) | undefined;
235
+ ttlMs?: number | undefined;
236
+ });
237
+ /** 当前缓存条目数(测试断言用)。 */
238
+ get size(): number;
239
+ /** 清空缓存(例如登出、切换账号)。 */
240
+ clear(): void;
241
+ load(input?: UsageLoadInput): Promise<UsageLoadResult>;
242
+ }
243
+ /** 金额格式化:与网关 `formatCost` 同口径(`¥` 前缀、最多 4 位小数);缺失显示 `—`。 */
244
+ export declare function formatYuan(value: number | undefined | null): string;
245
+ /** KPI「今日花费」:summary(`today`) 的按模型费用合计;summary 缺失时 undefined。 */
246
+ export declare function todayCostOf(summary: UsageSummary | undefined): number | undefined;
247
+ /**
248
+ * KPI「本月花费 / 本月请求数」:网关 `summary(range: 'mtd')` 的按模型合计
249
+ * (一次调用同时给出 `total_cost` 与 `request_count`)。
250
+ *
251
+ * 客户端**不做**任何自然月过滤——月份边界完全由网关的 `meRange` 决定;响应的 `range`
252
+ * 必须是 `mtd`,否则视为旧网关回退(KPI 显示 `—`,见 {@link isMonthRangeSupported})。
253
+ */
254
+ export declare function monthToDateOf(summary: UsageSummary | undefined): {
255
+ cost: number;
256
+ requests: number;
257
+ } | undefined;
258
+ /** KPI「剩余预算」:`/api/me/key` budgets 的剩余额度合计;没有任何数值时 undefined。 */
259
+ export declare function remainingBudgetOf(key: UsageKeyDetail | undefined): number | undefined;
260
+ /** 按模型分布行(金额降序)。 */
261
+ export interface ModelDistributionRow {
262
+ model: string;
263
+ totalCost: number;
264
+ requestCount: number;
265
+ totalTokens: number;
266
+ }
267
+ /**
268
+ * 「按模型分布」:把 model-trends 各桶的 breakdown 按模型名合并。
269
+ * breakdown 缺失 / 为空时返回空数组(视图渲染空态,不编数字)。
270
+ */
271
+ export declare function modelDistributionOf(modelTrends: UsageModelTrends | undefined): ModelDistributionRow[];
272
+ /** 趋势点(用于柱状图):按时间升序。 */
273
+ export declare function trendPointsOf(trends: UsageTrends | undefined): UsageTrendPoint[];
274
+ /** 充值台账:按时间倒序(最新在前)。 */
275
+ export declare function rechargesOf(recharges: readonly UsageRecharge[] | undefined): UsageRecharge[];
276
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
2
+ import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import type { AiRouterCardState } from './controller.ts';
4
+ import { type UsageKey, type UsageLoader } from './usage-data.ts';
5
+ /** 分区注入面:设置作用域的 Key 选择(默认 Key)+ 取数仓库。 */
6
+ export interface UsageSectionFace {
7
+ hooks: {
8
+ aiRouterCard: SnapshotStore<AiRouterCardState>;
9
+ };
10
+ usage: UsageLoader;
11
+ }
12
+ export type UsageSectionProps = PropsRuntime<'settings.section'> & InjectFace<UsageSectionFace>;
13
+ /** Key 展示名:掩码优先,回退员工名 / id 前缀,附团队与禁用标记。 */
14
+ export declare function keyLabel(key: UsageKey): string;
15
+ /** 充值类型:网关自定义枚举 → 中文(未知值原样展示)。 */
16
+ export declare function rechargeTypeLabel(type: string): string;
17
+ /** 设置页「用量与成本」分区。 */
18
+ export declare function UsageSection(props: UsageSectionProps): import("react").JSX.Element;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 客户端版本策略(spec §6):`GET /api/client/policy` 的免登录只读拉取。
3
+ *
4
+ * 网关返回的是**最终判定**(`loginMode` 等),插件只做投影不做策略逻辑,因此管理端
5
+ * 调整 `client.login_mode` 后重启插件即生效,无需发版(spec §6.1)。
6
+ *
7
+ * 本模块只负责「拉 + 严格解析」,投影与 fail-open 缺省由 `src/index.ts` 的
8
+ * `projectLoginMode` 完成:**非 200 / 非 JSON / 枚举非法 / 超时 / 网络异常一律返回
9
+ * undefined**,调用方据此回退 `DEFAULT_LOGIN_MODE`,绝不把用户锁在门外(spec §5.4 / §6.4)。
10
+ */
11
+ /**
12
+ * 登录方式(与 `src/client/controller.ts` 的 `LoginMode` 同值;client 半边不 import
13
+ * Host 代码,两处各自声明同一联合类型,改取值必须两侧同步)。
14
+ */
15
+ export type LoginMode = 'browser' | 'form' | 'both';
16
+ /** fail-open 缺省值:策略缺失 / 失败 / 非法时一律按此投影(spec §5.4)。 */
17
+ export declare const DEFAULT_LOGIN_MODE: LoginMode;
18
+ /** 策略端点:域名一律来自 baseURL,不硬编码。 */
19
+ export declare const CLIENT_POLICY_PATH = "/api/client/policy";
20
+ /** 策略拉取默认超时:免登录只读接口,超过 5s 视同不可达(fail-open)。 */
21
+ export declare const CLIENT_POLICY_TIMEOUT_MS = 5000;
22
+ /** 策略响应中本插件消费的部分。 */
23
+ export interface ClientPolicy {
24
+ loginMode: LoginMode;
25
+ }
26
+ /** 拉取策略所需入参(deviceId 由 `resolveDeviceInfo` 解析,永非空)。 */
27
+ export interface FetchClientPolicyOptions {
28
+ /** 网关地址;缺省 / 空串直接返回 undefined(不发起请求)。 */
29
+ baseURL: string | undefined;
30
+ /** 客户端版本(生产传 `PKG_VERSION`)。 */
31
+ version: string;
32
+ /** 平台 / 架构(生产传 `process.platform` / `process.arch`)。 */
33
+ platform: string;
34
+ arch: string;
35
+ deviceId: string;
36
+ /** 超时毫秒数;缺省 {@link CLIENT_POLICY_TIMEOUT_MS}。 */
37
+ timeoutMs?: number;
38
+ }
39
+ /**
40
+ * 拼策略 URL:`{baseURL}/api/client/policy?platform=&arch=&version=&device_id=`。
41
+ * 参数固定顺序、逐个 `encodeURIComponent`(device_id 可能是任意 ASCII,含 `+/=` 也必须安全);
42
+ * 空值参数直接省略(不产生 `version=` 这样的空参数);baseURL 尾斜杠归一。
43
+ * @returns 完整 URL;缺 baseURL 时 undefined(调用方据此跳过请求)。
44
+ */
45
+ export declare function buildClientPolicyURL(opts: FetchClientPolicyOptions): string | undefined;
46
+ /**
47
+ * 拉取并解析客户端策略。**永不抛异常**:任何失败都返回 undefined(fail-open)。
48
+ *
49
+ * 不记录响应原文——策略接口当前无敏感字段,但保持与其他网关调用一致的日志纪律。
50
+ */
51
+ export declare function fetchClientPolicy(opts: FetchClientPolicyOptions): Promise<ClientPolicy | undefined>;