hiwork-core 0.0.3 → 0.0.7

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.
Files changed (37) hide show
  1. package/README.md +13 -7
  2. package/cordis.patch.yml +21 -0
  3. package/lib/ai-router-client.js +258 -2
  4. package/lib/browser-login.js +241 -0
  5. package/lib/client-policy.js +82 -0
  6. package/lib/client.js +997 -1006
  7. package/lib/daily-rpc.js +161 -0
  8. package/lib/default-model.js +39 -0
  9. package/lib/index.js +1160 -73
  10. package/lib/orchestration.js +45 -75
  11. package/lib/provision-session.js +85 -0
  12. package/lib/provision.js +74 -24
  13. package/lib/types/ai-router-client.d.ts +255 -3
  14. package/lib/types/browser-login.d.ts +99 -0
  15. package/lib/types/client/account-cache.d.ts +46 -0
  16. package/lib/types/client/controller.d.ts +68 -0
  17. package/lib/types/client/daily-card.d.ts +29 -20
  18. package/lib/types/client/daily-data.d.ts +116 -0
  19. package/lib/types/client/hero-greeting.d.ts +13 -4
  20. package/lib/types/client/index.d.ts +1 -1
  21. package/lib/types/client/login-gate.d.ts +9 -0
  22. package/lib/types/client/session-cost.d.ts +119 -0
  23. package/lib/types/client/styles.d.ts +22 -4
  24. package/lib/types/client/types.d.ts +1 -1
  25. package/lib/types/client/usage-data.d.ts +276 -0
  26. package/lib/types/client/usage-section.d.ts +18 -0
  27. package/lib/types/client-policy.d.ts +51 -0
  28. package/lib/types/daily-rpc.d.ts +110 -0
  29. package/lib/types/default-model.d.ts +37 -0
  30. package/lib/types/index.d.ts +113 -0
  31. package/lib/types/orchestration.d.ts +28 -55
  32. package/lib/types/provision-session.d.ts +106 -0
  33. package/lib/types/provision.d.ts +48 -9
  34. package/lib/types/types.d.ts +12 -1
  35. package/lib/types/usage-rpc.d.ts +196 -0
  36. package/lib/usage-rpc.js +386 -0
  37. package/package.json +20 -16
@@ -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>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `/hiwork-daily` loopback RPC 的 Host 实现(新建任务 hero 页「每日一句」的唯一取数口)。
3
+ *
4
+ * 设计要点(与 `src/usage-rpc.ts` 完全同构):
5
+ * - 只注册一个 channel,endpoint 白名单在 {@link DAILY_RPC_ENDPOINTS},未知 endpoint
6
+ * 直接 `bad-request`;
7
+ * - payload 严格校验:`today` 只接受空对象,`random` 只接受 `{ excludeId?: 正整数 }`,
8
+ * 任何其它字段 / 类型不符一律 `bad-request`;
9
+ * - **网关会话 cookie 绝不离开 Host**:调用方只拿到端点原始响应,cookie 只在
10
+ * `resolveSession()` 内部出现;
11
+ * - 网关 `GatewayError` 的稳定 code 原样透传,但 message 先经
12
+ * {@link displayDailyGatewayMessage} 消毒——baseURL / HTTP 状态码这类实现细节不进 UI;
13
+ * - 其余异常收敛为通用失败(code `internal`),Host 只记日志、不把堆栈或原始消息
14
+ * 交给客户端。
15
+ *
16
+ * 消毒逻辑是**同构复制**而非复用 usage-rpc 的导出:后者的兜底文案是用量域的
17
+ * (「用量数据暂时无法获取…」),复用会让每日一句的失败吐出错误域的文案;且本仓库的
18
+ * 频道模块本就各自自带一份同实现(usage-rpc 与 hiwork-automation 的 rpc.ts 亦然),
19
+ * 保持模块自包含。
20
+ *
21
+ * 本模块不 import cordis 运行时,只依赖结构化的 {@link DailyRpcContext} 端口,
22
+ * 便于单测直接伪造。
23
+ */
24
+ import { type GatewayDailyQuoteRandomResponse, type GatewayDailyQuoteResponse } from './ai-router-client.ts';
25
+ /** RPC 频道;必须与 client 半边 `src/client/daily-data.ts` 的 `DAILY_CHANNEL` 一致(由测试锁定)。 */
26
+ export declare const DAILY_RPC_CHANNEL = "/hiwork-daily";
27
+ /** 端点白名单:当日条目 + 换一句(随机)。 */
28
+ export declare const DAILY_RPC_ENDPOINTS: readonly ["today", "random"];
29
+ export type DailyRpcEndpoint = (typeof DAILY_RPC_ENDPOINTS)[number];
30
+ /** 端点入参:只有 `random` 带可选 `excludeId`(当前展示条目 id,正整数)。 */
31
+ export interface DailyRpcPayload {
32
+ excludeId?: number | undefined;
33
+ }
34
+ /** 客户端可安全展示的失败。 */
35
+ export interface DailyRpcFailure {
36
+ code: string;
37
+ message: string;
38
+ /** DSH Connection 信封要求 `details` 是对象;协议层没有额外细节时补空对象。 */
39
+ details: object;
40
+ }
41
+ /** 端点结果信封(与 DSH Connection 的 `ConnectionRpcResult` 同形)。 */
42
+ export type DailyRpcResult<T> = {
43
+ readonly ok: true;
44
+ readonly value: T;
45
+ } | {
46
+ readonly ok: false;
47
+ readonly error: DailyRpcFailure;
48
+ };
49
+ /** 请求侧错误(只有 `bad-request`:未知端点 / 非法 payload)。 */
50
+ export declare class DailyRequestError extends Error {
51
+ readonly code: 'bad-request';
52
+ constructor(code: 'bad-request', message: string);
53
+ }
54
+ /** Host 侧 RPC 依赖的最小上下文(结构兼容 cordis Context,便于单测伪造)。 */
55
+ export interface DailyRpcConnection {
56
+ rpc: {
57
+ handle(channel: string, handler: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<unknown>, options: {
58
+ authority: 'loopback';
59
+ }): () => Promise<void> | void;
60
+ };
61
+ }
62
+ export interface DailyRpcContext {
63
+ readonly connection: DailyRpcConnection;
64
+ readonly logger: {
65
+ warn(message: string): void;
66
+ };
67
+ }
68
+ /** 网关客户端的每日一句子集:只声明本频道真正调用的方法(结构兼容 `GatewayClient` 实例)。 */
69
+ export interface DailyGatewayPort {
70
+ getMyDailyQuote(sessionCookie: string): Promise<GatewayDailyQuoteResponse>;
71
+ getMyDailyQuoteRandom(sessionCookie: string, excludeId?: number): Promise<GatewayDailyQuoteRandomResponse>;
72
+ }
73
+ /** Host 侧取数依赖:会话解析 + 当前 baseURL 下的网关客户端。 */
74
+ export interface DailyRpcDeps {
75
+ /**
76
+ * 解析网关会话 cookie。未登录 / 会话缺失返回 undefined。
77
+ * **返回值只在本模块内部使用,绝不出现在 RPC 响应里。**
78
+ */
79
+ resolveSession(): Promise<string | undefined>;
80
+ /** 当前 baseURL 对应的网关客户端;baseURL 未配置时返回 undefined。 */
81
+ gateway(): DailyGatewayPort | undefined;
82
+ /** Host 日志出口(只记事件与错误类型,不记会话值)。 */
83
+ log?: ((message: string) => void) | undefined;
84
+ }
85
+ /** 通用失败文案:不暴露任何网关细节。 */
86
+ export declare const DAILY_GENERIC_FAILURE_MESSAGE = "\u6BCF\u65E5\u4E00\u53E5\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
87
+ /** 未登录 / 会话失效的统一文案(与用量频道的 `SESSION_EXPIRED` 口径一致)。 */
88
+ export declare const DAILY_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
89
+ /**
90
+ * 网关 message 消毒(与 `usage-rpc.ts` 的 `displayGatewayMessage` 同实现):
91
+ * 含 baseURL / HTTP 状态码 / URL 的原始消息一律换成稳定文案,其余(网关自己写好的
92
+ * 中文业务文案)原样保留。code 不受影响,始终原样透传。
93
+ */
94
+ export declare function displayDailyGatewayMessage(code: string, message: unknown): string;
95
+ /**
96
+ * payload 校验:`today` 只放行空对象,`random` 只放行 `{ excludeId?: 正整数 }`;
97
+ * 其余字段 / 类型不符一律 `bad-request`。
98
+ */
99
+ export declare function parseDailyPayload(endpoint: DailyRpcEndpoint, raw: unknown): DailyRpcPayload;
100
+ /** 是否为白名单端点。 */
101
+ export declare function isDailyRpcEndpoint(endpoint: string): endpoint is DailyRpcEndpoint;
102
+ /**
103
+ * 处理一次端点调用:返回 DSH Connection 的结果信封,**永不抛异常**。
104
+ * 导出以便单测直接驱动(无需伪造 `ctx.connection`)。
105
+ */
106
+ export declare function handleDailyEndpoint(endpoint: string, rawPayload: unknown, deps: DailyRpcDeps, signal?: AbortSignal): Promise<DailyRpcResult<unknown>>;
107
+ /**
108
+ * 注册 `/hiwork-daily` 频道,返回异步 disposer(交给 `ctx.effect` 管理生命周期)。
109
+ */
110
+ export declare function registerDailyRpc(ctx: DailyRpcContext, deps: DailyRpcDeps): () => Promise<void>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 默认模型校正。
3
+ *
4
+ * `agent-default-model` 是 dsh 的默认模型选择(provider + model),dsh-base 的组合默认值
5
+ * 指向官方 DeepSeek 提供方(`deepseek-official/deepseek-v4-flash`)。本产品停用了官方提供方、
6
+ * 只供给网关线路,所以全新安装或提供方/模型变动后,这个默认值可能指向一个不再注册的
7
+ * provider 或已下架的 model —— 新会话第一次发请求会 UNKNOWN_MODEL,用户必须先去模型选择器
8
+ * 手动选一次。供给成功后在这里校正它:当前选择仍可路由就不动,否则回落到兜底线路的第一个模型。
9
+ *
10
+ * 不写 composition 配置、只写用户层,所以用户自己选过的模型只要仍然可用就不会被覆盖。
11
+ */
12
+ import type { RouteModels } from './provision.ts';
13
+ /** dsh-agent-default-model 的 settings 命名空间。 */
14
+ export declare const DEFAULT_MODEL_NS = "agent-default-model";
15
+ /** 一次默认模型选择。 */
16
+ export interface DefaultModelSelection {
17
+ provider: string;
18
+ model: string;
19
+ }
20
+ /** 校正默认模型所需的端口(便于单测注入假实现)。 */
21
+ export interface DefaultModelPorts {
22
+ /** `agent-default-model` 命名空间是否已注册;未注册的 profile 直接跳过。 */
23
+ available(): boolean;
24
+ /** 读当前默认选择;字段缺失或命名空间未注册时 undefined。 */
25
+ read(): DefaultModelSelection | undefined;
26
+ /** 非本插件线路的提供方:已注册且确实含该模型时为 true。 */
27
+ isRoutableElsewhere(selection: DefaultModelSelection): Promise<boolean>;
28
+ /** 写入校正后的选择(同时清掉可能不再合法的 reasoningEffort)。 */
29
+ write(selection: DefaultModelSelection): Promise<void>;
30
+ }
31
+ /**
32
+ * 供给成功后校正默认模型。
33
+ * @param routeModels - 本次供给出的各线路模型归属。
34
+ * @param ports - settings / llm 端口。
35
+ * @returns 实际写入的新选择;无需变更时 undefined。
36
+ */
37
+ export declare function ensureDefaultModel(routeModels: RouteModels, ports: DefaultModelPorts): Promise<DefaultModelSelection | undefined>;
@@ -6,7 +6,14 @@
6
6
  */
7
7
  import type { Context } from '@deepseek-ai/cordis';
8
8
  import z from '@deepseek-ai/schemastery';
9
+ import { type SettingsProvider } from '@deepseek-ai/dsh-settings';
10
+ import { fetchClientPolicy, type ClientPolicy, type LoginMode } from './client-policy.ts';
11
+ import { type CredentialsPort, type DefaultModelPort, type AiRouterStatePort } from './orchestration.ts';
9
12
  export declare const name = "hiwork-core";
13
+ /**
14
+ * 必需服务:`connection` 承载设置页「用量与成本」的 loopback RPC 频道
15
+ * (`/hiwork-usage`,见 `src/usage-rpc.ts`);其余三项是既有登录 / 供给依赖。
16
+ */
10
17
  export declare const inject: string[];
11
18
  /** 插件配置 + 运行时状态字段(全部可选;写操作按此 schema 校验)。 */
12
19
  export interface Config {
@@ -34,6 +41,21 @@ export interface Config {
34
41
  logoutRequest?: {
35
42
  at: string;
36
43
  };
44
+ /** 客户端请求发起浏览器授权登录(宿主发起后清掉,见 src/browser-login.ts)。 */
45
+ browserLoginRequest?: {
46
+ at: string;
47
+ };
48
+ /** 已发起的浏览器授权:授权页 URL + 发起时间,渲染端负责 window.open。 */
49
+ browserLogin?: {
50
+ authorizeUrl: string;
51
+ startedAt: string;
52
+ };
53
+ /**
54
+ * 登录方式:Host 从 `GET /api/client/policy` 投影(spec §5.4 / §6.1)。
55
+ * 取值与 `src/client/controller.ts` 的 `LoginMode` 一致,**不得改**;缺省 / 拉取失败
56
+ * 一律写 `both`(fail-open,绝不把用户锁在门外)。
57
+ */
58
+ loginMode?: LoginMode;
37
59
  budgets?: Array<{
38
60
  model_name: string;
39
61
  total_budget?: number | null;
@@ -50,4 +72,95 @@ export interface Config {
50
72
  }>;
51
73
  }
52
74
  export declare const Config: z<Config>;
75
+ /** 校正默认模型要用到的 llm 注册表部分(便于单测注入假实现)。 */
76
+ export interface LlmRouteRegistry {
77
+ listProviders(): readonly {
78
+ id: string;
79
+ }[];
80
+ listModels(provider: string): Promise<readonly {
81
+ id: string;
82
+ }[]>;
83
+ }
84
+ /**
85
+ * 组装默认模型端口:settings 读写 + llm 注册表的「提供方/模型是否还在」判断。
86
+ * 导出以便单测覆盖接线(编排只依赖 `DefaultModelPort` 接口)。
87
+ * @param opts.settings - settings 服务(读写 agent-default-model 用户层)。
88
+ * @param opts.llm - llm 注册表;缺省时其它提供方一律视为不可路由。
89
+ * @param opts.log - 校正成功时的日志出口。
90
+ * @returns 供编排调用的默认模型端口。
91
+ */
92
+ export declare function createDefaultModelPort(opts: {
93
+ settings: SettingsProvider;
94
+ llm: LlmRouteRegistry | undefined;
95
+ log?: (message: string) => void;
96
+ }): DefaultModelPort;
97
+ /**
98
+ * IPC 消息源最小面:Node 的 `process` 结构上满足(`on`/`off` 的 `'message'` 重载),
99
+ * 测试注入假对象——**不把 `process.on` 写死在不可测的地方**。
100
+ */
101
+ export interface IpcMessageSource {
102
+ on(event: 'message', listener: (message: unknown) => void): unknown;
103
+ off(event: 'message', listener: (message: unknown) => void): unknown;
104
+ }
105
+ /** 注册 IPC 监听所需端口(生产由 apply 用真实 process 与既有 ports 装配)。 */
106
+ export interface IpcCallbackPorts {
107
+ /** 消息源(生产传 `process`)。 */
108
+ source: IpcMessageSource;
109
+ /** 处理一条已通过校验的授权回调(生产为 `completeBrowserLogin`)。 */
110
+ complete: (input: {
111
+ code?: string;
112
+ state?: string;
113
+ error?: string;
114
+ }) => Promise<unknown>;
115
+ /** 记录桌面端推来的设备信息(生产为 `setDeviceInfo`)。 */
116
+ setDevice: (info: {
117
+ deviceId: string;
118
+ deviceName?: string;
119
+ appVersion?: string;
120
+ }) => void;
121
+ /** 把设备 id 持久化到 credentials 的 `hiwork_device_id`(生产为 `ports.credentials.set`)。 */
122
+ persistDeviceId: (deviceId: string) => Promise<void>;
123
+ /**
124
+ * 告警出口;缺省静默。**只记事件与字段名,绝不记 code / state / deviceId 的值**,
125
+ * 也绝不记完整回调 URL。
126
+ */
127
+ log?: (message: string) => void;
128
+ }
129
+ /**
130
+ * 注册桌面端 IPC 消息监听(`process.on('message', …)`),返回幂等 disposer。
131
+ *
132
+ * 分派规则:只处理 `hiwork:auth-callback` 与 `hiwork:device-info` 两种信封,其余一律
133
+ * 静默忽略(DSH 自有消息 / 其他插件消息不得被本插件触碰)。
134
+ *
135
+ * 并发守卫:`complete` 是异步的,连续到达的多条回调**只执行第一条,后到的直接忽略
136
+ * (不排队)**。理由——授权回调是一次性的(一个 code 只能兑换一次),排队执行会让
137
+ * 真实回调完成后仍去兑换已消费的 code,并给伪造的迟到消息改写 `lastError` 的机会;
138
+ * 忽略既最安全,也与 `completeBrowserLogin` 自身的「一次性」语义一致。
139
+ * 该守卫只约束授权回调:`hiwork:device-info` 同步处理 + 后台持久化,**不受阻塞**。
140
+ */
141
+ export declare function registerIpcMessageHandler(ports: IpcCallbackPorts): () => void;
142
+ /**
143
+ * 拉取客户端策略并把 `loginMode` 投影进 ai-router 命名空间(spec §5.4 / §6.1)。
144
+ *
145
+ * fail-open 是硬要求:缺 baseURL / 设备信息取不到 / 网络失败 / 响应非法一律投影
146
+ * `DEFAULT_LOGIN_MODE`(`both`),**且只写 `loginMode` 一个字段**——不碰 lastError、
147
+ * 不碰任何登录请求字段,策略拉取失败绝不影响登录与供给。**本函数永不抛异常**。
148
+ *
149
+ * @returns 最终投影的登录方式(测试与调用方均可断言)。
150
+ */
151
+ export declare function projectLoginMode(opts: {
152
+ baseURL: string | undefined;
153
+ credentials: CredentialsPort;
154
+ state: AiRouterStatePort;
155
+ /** 客户端版本;缺省 `PKG_VERSION`。 */
156
+ version?: string;
157
+ /** 平台 / 架构;缺省 `process.platform` / `process.arch`。 */
158
+ platform?: string;
159
+ arch?: string;
160
+ /** 策略拉取超时;缺省由 fetchClientPolicy 决定。 */
161
+ timeoutMs?: number;
162
+ /** 策略拉取实现(测试注入);缺省 `fetchClientPolicy`。 */
163
+ fetchPolicy?: (input: Parameters<typeof fetchClientPolicy>[0]) => Promise<ClientPolicy | undefined>;
164
+ log?: (message: string) => void;
165
+ }): Promise<LoginMode>;
53
166
  export declare function apply(ctx: Context, config: Config): void;
@@ -1,64 +1,16 @@
1
- import { type GatewayBudget, type GatewayClient, type GatewayKey } from './ai-router-client.ts';
1
+ import { type GatewayClient } from './ai-router-client.ts';
2
+ import { type AiRouterNamespaceValue, type AiRouterStatePort, type CredentialsPort, type DefaultModelPort, type PiAiPort } from './provision-session.ts';
2
3
  import type { LoginError } from './types.ts';
3
- /** 凭据端口:对 ctx.credentials 的抽象,便于测试。 */
4
- export interface CredentialsPort {
5
- resolve(ref: string): Promise<{
6
- value: string;
7
- } | undefined>;
8
- set(ref: string, value: string): Promise<void>;
9
- unset(ref: string): Promise<void>;
10
- }
11
- /** pi-ai 配置端口:写/清协议路由集合(多路由按协议拆分)。 */
12
- export interface PiAiPort {
13
- applyAiRouterProfiles(profiles: Record<string, Record<string, unknown>> | null): Promise<void>;
14
- }
15
- /** 对 ai-router 状态段的一次补丁。 */
16
- export interface AiRouterStatePatch {
17
- set?: Record<string, unknown>;
18
- unset?: readonly string[];
19
- }
20
- /** ai-router 状态端口:Host 的唯一写者。 */
21
- export interface AiRouterStatePort {
22
- patch(patch: AiRouterStatePatch): Promise<void>;
23
- }
24
- /** ai-router 命名空间解析后的值(相关字段)。 */
25
- export interface AiRouterNamespaceValue {
26
- baseURL?: string;
27
- keyId?: string;
28
- username?: string;
29
- /** 登录账号的显示名(网关 LDAP displayName,如中文名);缺省时 UI 回退 username。 */
30
- displayName?: string;
31
- pendingLogin?: {
32
- username: string;
33
- passwordRef: string;
34
- };
35
- rememberPassword?: boolean;
36
- refreshRequest?: {
37
- at?: string;
38
- };
39
- logoutRequest?: {
40
- at?: string;
41
- };
42
- /** 最近一次拉取到的各模型额度(登录/刷新时写入)。 */
43
- budgets?: readonly GatewayBudget[];
44
- /** 最近一次拉取到的当前用户全部虚拟 Key(多 Key 选择器数据源)。 */
45
- keys?: readonly GatewayKey[];
46
- }
47
- /**
48
- * 归一化 budgets:过滤空模型名与已下架模型(model_status === 'inactive',已不可调用,
49
- * 不进入额度展示),保留额度字段。状态缺省(旧网关)视同 active,正常保留。
50
- */
51
- export declare function computeBudgets(budgets: readonly GatewayBudget[]): GatewayBudget[];
52
- /** 归一化 keys:过滤无 id 项,只保留选择器需要的字段。 */
53
- export declare function computeKeys(keys: readonly GatewayKey[]): GatewayKey[];
54
- /** 任意抛错 → 稳定 LoginError;GatewayError 原样,其余归为 GATEWAY_UNREACHABLE。 */
55
- export declare function toLoginError(error: unknown, fallback: string): LoginError;
4
+ export { computeBudgets, computeKeys, provisionSession, toLoginError } from './provision-session.ts';
5
+ export type { AiRouterNamespaceValue, AiRouterStatePatch, AiRouterStatePort, CredentialsPort, DefaultModelPort, PiAiPort, } from './provision-session.ts';
56
6
  /** 执行一次登录:拉 Key + budgets → 写凭据 → 写 pi-ai 配置 → 写状态。 */
57
7
  export declare function runLogin(opts: {
58
8
  gateway: GatewayClient;
59
9
  credentials: CredentialsPort;
60
10
  piAi: PiAiPort;
61
11
  state: AiRouterStatePort;
12
+ /** 供给成功后校正默认模型;可选以便既有测试直接构造 opts。 */
13
+ defaultModel?: DefaultModelPort;
62
14
  /** 登录成功尾部触发(默认工作区 ensure 等);可选以便既有测试直接构造 opts。 */
63
15
  ensureWorkspace?: () => Promise<void>;
64
16
  value: AiRouterNamespaceValue;
@@ -69,6 +21,8 @@ export declare function runRefresh(opts: {
69
21
  credentials: CredentialsPort;
70
22
  piAi: PiAiPort;
71
23
  state: AiRouterStatePort;
24
+ /** 供给成功后校正默认模型;可选以便既有测试直接构造 opts。 */
25
+ defaultModel?: DefaultModelPort;
72
26
  /** 透传给重登路径 runLogin 的可选端口。 */
73
27
  ensureWorkspace?: () => Promise<void>;
74
28
  value: AiRouterNamespaceValue;
@@ -80,12 +34,31 @@ export declare function runLogout(opts: {
80
34
  piAi: PiAiPort;
81
35
  state: AiRouterStatePort;
82
36
  }): Promise<void>;
83
- /** 分发一次 ai-router 命名空间变更:三个请求字段至多存在一个。 */
37
+ /**
38
+ * 浏览器授权登录的发起:生成 PKCE 与 state,回写 `browserLogin` 供渲染端 window.open。
39
+ * 见 browser-login.ts;此处只做分发与错误码归一。
40
+ */
41
+ export declare function handleBrowserLoginRequest(opts: {
42
+ credentials: CredentialsPort;
43
+ state: AiRouterStatePort;
44
+ value: AiRouterNamespaceValue;
45
+ /** 告警出口(state 不匹配等安全事件);缺省静默。 */
46
+ log?: (message: string) => void;
47
+ /** 时钟注入(测试用);缺省 Date.now。 */
48
+ now?: () => number;
49
+ }): Promise<LoginError | null>;
50
+ /** 分发一次 ai-router 命名空间变更:请求字段至多存在一个。 */
84
51
  export declare function handleAiRouterSectionChange(opts: {
85
52
  gateway: GatewayClient;
86
53
  credentials: CredentialsPort;
87
54
  piAi: PiAiPort;
88
55
  state: AiRouterStatePort;
56
+ /** 供给成功后校正默认模型;可选以便既有测试直接构造 opts。 */
57
+ defaultModel?: DefaultModelPort;
89
58
  ensureWorkspace?: () => Promise<void>;
90
59
  value: AiRouterNamespaceValue;
60
+ /** 告警出口(浏览器授权 state 不匹配等);缺省静默。 */
61
+ log?: (message: string) => void;
62
+ /** 时钟注入(测试用);缺省 Date.now。 */
63
+ now?: () => number;
91
64
  }): Promise<LoginError | null>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * 会话供给的共享契约与实现:端口接口、状态字段形状、归一化与错误映射,
3
+ * 以及「会话已建立后」的供给尾部 `provisionSession`。
4
+ *
5
+ * 本模块被 `orchestration.ts`(表单登录 / 刷新 / 登出)与 `browser-login.ts`
6
+ * (浏览器授权登录)各自直接 import,**它自身不 import 这两者**,因此不存在循环依赖
7
+ * (打包产物里也不会出现模块级 TDZ 风险)。
8
+ */
9
+ import { type GatewayBudget, type GatewayClient, type GatewayKey, type GatewayUser } from './ai-router-client.ts';
10
+ import type { RouteModels } from './provision.ts';
11
+ import type { LoginError } from './types.ts';
12
+ /** 凭据端口:对 ctx.credentials 的抽象,便于测试。 */
13
+ export interface CredentialsPort {
14
+ resolve(ref: string): Promise<{
15
+ value: string;
16
+ } | undefined>;
17
+ set(ref: string, value: string): Promise<void>;
18
+ unset(ref: string): Promise<void>;
19
+ }
20
+ /** pi-ai 配置端口:写/清协议路由集合(多路由按协议拆分)。 */
21
+ export interface PiAiPort {
22
+ applyAiRouterProfiles(profiles: Record<string, Record<string, unknown>> | null): Promise<void>;
23
+ }
24
+ /** 默认模型端口:供给成功后校正 agent-default-model(见 src/default-model.ts)。 */
25
+ export interface DefaultModelPort {
26
+ ensure(routeModels: RouteModels): Promise<void>;
27
+ }
28
+ /** 对 ai-router 状态段的一次补丁。 */
29
+ export interface AiRouterStatePatch {
30
+ set?: Record<string, unknown>;
31
+ unset?: readonly string[];
32
+ }
33
+ /** ai-router 状态端口:Host 的唯一写者。 */
34
+ export interface AiRouterStatePort {
35
+ patch(patch: AiRouterStatePatch): Promise<void>;
36
+ }
37
+ /** ai-router 命名空间解析后的值(相关字段)。 */
38
+ export interface AiRouterNamespaceValue {
39
+ baseURL?: string;
40
+ keyId?: string;
41
+ username?: string;
42
+ /** 登录账号的显示名(网关 LDAP displayName,如中文名);缺省时 UI 回退 username。 */
43
+ displayName?: string;
44
+ pendingLogin?: {
45
+ username: string;
46
+ passwordRef: string;
47
+ };
48
+ rememberPassword?: boolean;
49
+ refreshRequest?: {
50
+ at?: string;
51
+ };
52
+ logoutRequest?: {
53
+ at?: string;
54
+ };
55
+ /** 客户端请求发起浏览器授权登录(渲染端写;宿主发起后清掉)。 */
56
+ browserLoginRequest?: {
57
+ at?: string;
58
+ };
59
+ /** 已发起的浏览器授权:授权页 URL + 发起时间,渲染端负责 window.open。 */
60
+ browserLogin?: {
61
+ authorizeUrl: string;
62
+ startedAt: string;
63
+ };
64
+ /** 最近一次拉取到的各模型额度(登录/刷新时写入)。 */
65
+ budgets?: readonly GatewayBudget[];
66
+ /** 最近一次拉取到的当前用户全部虚拟 Key(多 Key 选择器数据源)。 */
67
+ keys?: readonly GatewayKey[];
68
+ }
69
+ /**
70
+ * 归一化 budgets:过滤空模型名与已下架模型(model_status === 'inactive',已不可调用,
71
+ * 不进入额度展示),保留额度字段。状态缺省(旧网关)视同 active,正常保留。
72
+ */
73
+ export declare function computeBudgets(budgets: readonly GatewayBudget[]): GatewayBudget[];
74
+ /** 归一化 keys:过滤无 id 项,只保留选择器需要的字段。 */
75
+ export declare function computeKeys(keys: readonly GatewayKey[]): GatewayKey[];
76
+ /** 任意抛错 → 稳定 LoginError;GatewayError 原样,其余归为 GATEWAY_UNREACHABLE。 */
77
+ export declare function toLoginError(error: unknown, fallback: string): LoginError;
78
+ /**
79
+ * 会话已建立后的共享供给尾部:拉 Key → 写凭据 → 写 pi-ai 配置 → 校正默认模型 → 写状态 →
80
+ * ensure 工作区。表单登录(orchestration.ts 的 runLogin)与浏览器授权登录
81
+ * (browser-login.ts)共用同一段,保证两条登录路径的供给结果逐字段等价。
82
+ */
83
+ export declare function provisionSession(opts: {
84
+ gateway: GatewayClient;
85
+ credentials: CredentialsPort;
86
+ piAi: PiAiPort;
87
+ state: AiRouterStatePort;
88
+ /** 供给成功后校正默认模型;可选以便既有测试直接构造 opts。 */
89
+ defaultModel?: DefaultModelPort;
90
+ /** 供给成功后触发(默认工作区 ensure 等);可选以便既有测试直接构造 opts。 */
91
+ ensureWorkspace?: () => Promise<void>;
92
+ /** 本次供给使用的网关根地址(调用方已确认非空)。 */
93
+ baseURL: string;
94
+ /** ai-router 命名空间解析值(读 keyId)。 */
95
+ value: AiRouterNamespaceValue;
96
+ /** 已建立的网关会话 cookie。 */
97
+ sessionCookie: string;
98
+ /** 登录响应里的用户信息(写 displayName)。 */
99
+ user: GatewayUser;
100
+ /** 写入状态的账号名;缺省则不写(浏览器登录在网关未回传用户名时保留旧值)。 */
101
+ username?: string | undefined;
102
+ /** 成功时一并清掉的请求字段(表单登录 pendingLogin / 浏览器登录 browserLoginRequest)。 */
103
+ requestUnset?: readonly string[];
104
+ /** API_KEY 写好后触发的表单专属凭据整理(记住密码 / 临时密码);浏览器登录不传。 */
105
+ onApiKeyWritten?: () => Promise<void>;
106
+ }): Promise<void>;
@@ -10,15 +10,23 @@ export declare const REMEMBERED_PASSWORD_REF = "AI_ROUTER_REMEMBERED_PASSWORD";
10
10
  /**
11
11
  * 注册到 llm-pi-ai 的协议路由表:路由名 → pi-ai 协议。
12
12
  * 网关按「入站协议 + 模型」匹配模型映射(internal/service/provider_service.go),
13
- * 而 pi-ai 的 `api` 是路由级的 —— 因此每个协议一条路由,模型按映射归属到对应路由,
13
+ * 而 pi-ai 的 `api` 是路由级的 —— 因此每个协议一条路由,模型按映射进入对应路由,
14
14
  * 切模型时 dsh 自动落到讲该协议的路由,协议随模型走。
15
- * 主路由固定用 Responses 协议(/v1/responses),OpenAI 兼容模型统一落到它。
15
+ *
16
+ * 路由名固定为「线路C/M/R」,后缀即该路由讲的协议首字母(同一模型可出现在多条线路里):
17
+ * - 线路C `ai-router-chat`:openai-completions(chat/completions,兜底协议)
18
+ * - 线路M `ai-router-anthropic`:anthropic-messages
19
+ * - 线路R `ai-router`:openai-responses(key 与协议保持不变,兼容既有
20
+ * `agent-default-model: provider: ai-router`)
16
21
  */
17
22
  export declare const PROVIDER_ROUTES: {
18
- readonly 'ai-router': "openai-responses";
23
+ readonly 'ai-router-chat': "openai-completions";
19
24
  readonly 'ai-router-anthropic': "anthropic-messages";
25
+ readonly 'ai-router': "openai-responses";
20
26
  };
21
27
  export type ProviderRouteName = keyof typeof PROVIDER_ROUTES;
28
+ /** 各线路的模型归属(`computeRouteModels` 的输出);空线路不出现。 */
29
+ export type RouteModels = Partial<Record<ProviderRouteName, ModelDescriptor[]>>;
22
30
  /** 预算项的只读视图:模型名 + 可选目录状态(inactive = 已下架,不可调用)。 */
23
31
  export interface BudgetItem {
24
32
  model_name: string;
@@ -39,10 +47,27 @@ export interface ModelDescriptor {
39
47
  id: string;
40
48
  input: readonly PiAiModality[];
41
49
  }
42
- /** 固定写入顺序:先主路由(兼容 agent-default-model 的 provider: ai-router)。 */
50
+ /** 固定写入顺序:线路C → 线路M → 线路R(按后缀字母序,与模型选择器的分组顺序一致)。 */
43
51
  export declare const PROVIDER_ROUTE_NAMES: readonly ProviderRouteName[];
44
- /** 旧版独立 responses 路由(现已并入主路由);仅用于升级时清理残留 provider。 */
52
+ /** 旧版独立 responses 路由(现已并入线路R);仅用于升级时清理残留 provider。 */
45
53
  export declare const LEGACY_PROVIDER_ROUTE_NAMES: readonly string[];
54
+ /**
55
+ * 网关模型映射协议名(下划线式),即 `models.ProtocolType` 的取值集合
56
+ * (spec §6.2 词汇表)。计价端点 `POST /api/me/cost-estimate` 的 `route` 用这套名字,
57
+ * 而 DSH 会话里的 `routes[].provider` 是**线路名**(连字符式)——两者在 Host 侧用
58
+ * {@link routeToProtocol} 翻译,客户端不需要知道网关协议名。
59
+ */
60
+ export declare const GATEWAY_PROTOCOLS: readonly ["openai_chat_completions", "anthropic_messages", "openai_responses"];
61
+ export type GatewayProtocol = (typeof GATEWAY_PROTOCOLS)[number];
62
+ /**
63
+ * 线路名 → 网关协议名({@link GATEWAY_PROTOCOL_TO_ROUTE} 的反查,单点真相)。
64
+ *
65
+ * 未知线路与遗留线路(`ai-router-responses`,升级时已清理)一律返回 `undefined`——
66
+ * 调用方必须**丢弃**该轮而不是上报:网关对 `route` 做严格相等匹配,非法值会让整批 400。
67
+ * @param route - DSH 线路名(会话 `routes[].provider` 的取值)。
68
+ * @returns 网关协议名;不在映射内时为 undefined。
69
+ */
70
+ export declare function routeToProtocol(route: string): GatewayProtocol | undefined;
46
71
  /**
47
72
  * 从 budgets 归一化出去重、非空的模型 id 列表。已下架模型(model_status === 'inactive',
48
73
  * 网关 PreCheck 直接 402)不在「能成功调用」集合内,剔除;状态缺省(旧网关)视同 active。
@@ -59,12 +84,26 @@ export declare function parseModalities(raw: string | undefined): PiAiModality[]
59
84
  /** 由 budgets 构造每模型描述符(已剔除下架模型;模态按 budget 的 input_modalities 解析)。 */
60
85
  export declare function computeModelDescriptors(budgets: readonly BudgetItem[]): ModelDescriptor[];
61
86
  /**
62
- * 按网关模型映射把预算内模型拆到各协议路由:
63
- * 每个模型归属「优先级最高且映射命中(精确或通配)的协议」;
64
- * 无任何映射命中的模型落到主路由(默认 responses,网关侧仍有 `*` 兜底,保持预算内模型均可选)。
87
+ * 按网关模型映射把预算内模型拆到各协议路由(一条路由一个协议):
88
+ * 模型进入**每一条**「映射命中(精确或通配)的协议」对应的线路 —— 按协议分组,不按模型唯一归属,
89
+ * 所以同一个模型可以同时出现在多条线路里(例如既声明了 responses 又声明了 anthropic 映射的模型,
90
+ * 线路R 和线路M 都会列出它),不做去重:用户从哪条线路选它,请求就走那条线路的协议。
91
+ * 无任何映射命中的模型落到兜底线路 chat/completions:网关的裸 `*` 兜底映射通常声明在该协议下,
92
+ * 用 responses 入站反而匹配不到任何映射(网关按入站协议过滤映射)。
65
93
  * 返回每模型描述符(含 DSH 可接受的 input 模态),供 profile 注入。
66
94
  */
67
- export declare function computeRouteModels(budgets: readonly BudgetItem[], mapping: readonly GatewayModelMapping[]): Partial<Record<ProviderRouteName, ModelDescriptor[]>>;
95
+ export declare function computeRouteModels(budgets: readonly BudgetItem[], mapping: readonly GatewayModelMapping[]): RouteModels;
96
+ /**
97
+ * 兜底默认模型:按线路顺序 C→M→R 取第一条非空线路的第一个模型。
98
+ * 线路C 讲的 chat/completions 最通用(网关的裸 `*` 兜底映射也声明在该协议下),
99
+ * 因此优先;模型顺序沿用网关 `/api/me/key` 的 budgets 顺序。
100
+ * @param routeModels - 各线路的模型归属。
101
+ * @returns 兜底选择;所有线路都为空时 undefined。
102
+ */
103
+ export declare function pickFallbackDefaultModel(routeModels: RouteModels): {
104
+ provider: ProviderRouteName;
105
+ model: string;
106
+ } | undefined;
68
107
  /**
69
108
  * 各协议的 pi-ai baseURL:OpenAI 系 SDK 把 baseURL 视为根(追加 chat/completions、
70
109
  * responses),补 `/v1`;Anthropic SDK 自带 `v1/` 前缀,必须传裸 base,
@@ -1,4 +1,15 @@
1
1
  /** 稳定错误码/错误类型的定义在 src/client/types.ts(client 半边声明构建的 rootDir 内)。
2
- * 此处仅为 Host 半边(orchestration / ai-router-client)提供同源的类型再导出,
2
+ * 此处为 Host 半边(orchestration / ai-router-client)提供同源的类型再导出 + 宿主专属类型,
3
3
  * 避免两份定义漂移;声明构建时各自 emit 到 lib/types 对应位置。 */
4
4
  export type { LoginError, LoginErrorCode } from './client/types.ts';
5
+ /** GET /api/auth/sessions 的一项:当前账号的一台登录设备(网关 sessions 表投影)。 */
6
+ export interface DeviceSession {
7
+ id: string;
8
+ device_name: string;
9
+ platform: string;
10
+ app_version: string;
11
+ client_ip: string;
12
+ created_at: string;
13
+ last_seen_at?: string | null;
14
+ current: boolean;
15
+ }