hiwork-core 0.0.30 → 0.0.37

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 (41) hide show
  1. package/cordis.patch.yml +15 -1
  2. package/lib/ai-router-client.js +394 -2
  3. package/lib/browser-login.js +42 -16
  4. package/lib/client-policy.js +2 -2
  5. package/lib/client.js +2496 -500
  6. package/lib/image-rpc.js +306 -0
  7. package/lib/imagegen.js +424 -0
  8. package/lib/index.js +18639 -7060
  9. package/lib/knowledge-config.js +91 -1
  10. package/lib/provision-session.js +10 -2
  11. package/lib/tool-names.js +10 -0
  12. package/lib/tools.js +205 -0
  13. package/lib/types/ai-router-client.d.ts +255 -2
  14. package/lib/types/browser-login.d.ts +10 -1
  15. package/lib/types/client/about-section.d.ts +27 -7
  16. package/lib/types/client/controller.d.ts +17 -9
  17. package/lib/types/client/daily-data.d.ts +31 -3
  18. package/lib/types/client/feature-center.d.ts +46 -1
  19. package/lib/types/client/feature-registry.d.ts +36 -3
  20. package/lib/types/client/image-cards.d.ts +218 -0
  21. package/lib/types/client/image-styles.d.ts +9 -0
  22. package/lib/types/client/images-data.d.ts +127 -0
  23. package/lib/types/client/images-page.d.ts +97 -0
  24. package/lib/types/client/index.d.ts +0 -1
  25. package/lib/types/client/login-gate.d.ts +2 -2
  26. package/lib/types/client/logo.d.ts +7 -2
  27. package/lib/types/client/menu-preferences.d.ts +163 -0
  28. package/lib/types/client/menu-section.d.ts +5 -0
  29. package/lib/types/client/menu-store.d.ts +101 -0
  30. package/lib/types/client/styles.d.ts +16 -0
  31. package/lib/types/client/types.d.ts +9 -0
  32. package/lib/types/image-rpc.d.ts +142 -0
  33. package/lib/types/imagegen.d.ts +241 -0
  34. package/lib/types/index.d.ts +89 -8
  35. package/lib/types/knowledge-config.d.ts +50 -2
  36. package/lib/types/tool-names.d.ts +11 -0
  37. package/lib/types/tools.d.ts +80 -0
  38. package/lib/types/user-agent.d.ts +31 -0
  39. package/lib/user-agent.js +54 -0
  40. package/lib/web-search.js +2 -2
  41. package/package.json +34 -24
@@ -0,0 +1,142 @@
1
+ /**
2
+ * `/hiwork-image` loopback RPC 的 Host 实现(浏览器半边 ⇄ 生图的唯一通道)。
3
+ *
4
+ * 设计要点(与 `src/voice-rpc.ts` / `src/daily-rpc.ts` 完全同构):
5
+ * - 只注册**一个** channel,五个 endpoint 的白名单在 {@link IMAGE_RPC_ENDPOINTS},未知
6
+ * endpoint 直接 `bad-request`;一个能力一个频道,`list`/`fetch`/`save` 与
7
+ * `status`/`generate` 共用它(与 managed-mcp 的 snapshot/refresh/assertion 同款);
8
+ * - payload 严格校验:每个 endpoint 只放行它声明的字段与类型,任何其它字段一律
9
+ * `bad-request`(拼错字段的失败方式必须是响的,不是静默用默认值);
10
+ * - **网关会话 cookie 绝不离开 Host**:调用方只拿到端点结果,cookie 只在
11
+ * `resolveSession()` 内部出现;
12
+ * - 网关 `GatewayError` 的稳定 code 原样透传,message 先经
13
+ * {@link displayImageGatewayMessage} 消毒——baseURL / HTTP 状态码这类实现细节不进 UI;
14
+ * - 其余异常收敛为通用失败(code `internal`),Host 只记日志、不把堆栈或原始消息交给客户端。
15
+ *
16
+ * 两处**与 voice 不同**的语义,都写在这里免得后来的人「对齐」掉:
17
+ * 1. `status` 的失败**不是错误信封**——网关故障时回落 {@link IMAGE_FALLBACK_CONFIG}
18
+ * (`enabled:false`)。理由与语音逐字相同:这条端点回答的是「该不该显示生图能力」,
19
+ * 网关挂了就报红字,只会让一个本来就没有的功能显得像坏了;
20
+ * 2. `fetch` 有**体积闸**:超过 {@link MAX_IMAGE_BYTES} 直接拒绝(`IMAGE_TOO_LARGE`),
21
+ * 不把巨图塞进 loopback RPC 信封(语音那条的大对象在**上行**,本条的在下行)。
22
+ *
23
+ * 本模块不 import cordis 运行时,只依赖结构化的 {@link ImageRpcContext} 端口,便于单测伪造。
24
+ */
25
+ import { type GatewayImageConfig } from './ai-router-client.ts';
26
+ import { type ImageGatewayPort, type ImageWorkspacePort } from './imagegen.ts';
27
+ /** RPC 频道;必须与 client 半边 `src/client/images-data.ts` 的 `IMAGE_CHANNEL` 一致(由测试锁定)。 */
28
+ export declare const IMAGE_RPC_CHANNEL = "/hiwork-image";
29
+ /** 端点白名单:生图(`status`/`generate`)与「我的图片」画廊页(`list`/`fetch`/`save`)。 */
30
+ export declare const IMAGE_RPC_ENDPOINTS: readonly ["status", "generate", "list", "fetch", "save"];
31
+ export type ImageRpcEndpoint = (typeof IMAGE_RPC_ENDPOINTS)[number];
32
+ /** 画廊页 `list` 的分页缺省与上限(Host 侧夹紧:网关的 page_size 是它自己的缺省)。 */
33
+ export declare const IMAGE_LIST_DEFAULT_PAGE = 1;
34
+ export declare const IMAGE_LIST_DEFAULT_PAGE_SIZE = 20;
35
+ export declare const IMAGE_LIST_MAX_PAGE_SIZE = 100;
36
+ /**
37
+ * 端点入参(一次性把所有端点的可放行字段列在这里,解析函数按 endpoint 取子集)。
38
+ *
39
+ * `save` 的 `prompt` 是**有意的增量**:设计 §6.5 冻结的入参是 `{ id, workspaceId? }`,
40
+ * 但文件名规则要求「slug 来自该记录的 prompt」——记录本体在网关,而 `save` 只拿到 id,
41
+ * 于是把页面手里**已经有**的那条 prompt 顺带传上来(页面没传就用 `image` 兜底)。
42
+ */
43
+ export interface ImageRpcPayload {
44
+ prompt?: string | undefined;
45
+ size?: string | undefined;
46
+ page?: number | undefined;
47
+ pageSize?: number | undefined;
48
+ id?: number | undefined;
49
+ workspaceId?: string | undefined;
50
+ }
51
+ /** 客户端可安全展示的失败。 */
52
+ export interface ImageRpcFailure {
53
+ code: string;
54
+ message: string;
55
+ /** DSH Connection 信封要求 `details` 是对象;协议层没有额外细节时补空对象。 */
56
+ details: object;
57
+ }
58
+ /** 端点结果信封(与 DSH Connection 的 `ConnectionRpcResult` 同形)。 */
59
+ export type ImageRpcResult<T> = {
60
+ readonly ok: true;
61
+ readonly value: T;
62
+ } | {
63
+ readonly ok: false;
64
+ readonly error: ImageRpcFailure;
65
+ };
66
+ /** 请求侧错误(只有 `bad-request`:未知端点 / 非法 payload)。 */
67
+ export declare class ImageRequestError extends Error {
68
+ readonly code: 'bad-request';
69
+ constructor(code: 'bad-request', message: string);
70
+ }
71
+ /** Host 侧 RPC 依赖的最小上下文(结构兼容 cordis Context,便于单测伪造)。 */
72
+ export interface ImageRpcConnection {
73
+ rpc: {
74
+ handle(channel: string, handler: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<unknown>, options: {
75
+ authority: 'loopback';
76
+ }): () => Promise<void> | void;
77
+ };
78
+ }
79
+ export interface ImageRpcContext {
80
+ readonly connection: ImageRpcConnection;
81
+ readonly logger: {
82
+ warn(message: string): void;
83
+ };
84
+ }
85
+ /**
86
+ * Host 侧取数依赖:会话解析 + 当前 baseURL 下的网关客户端 + 工作区(画廊页 `save` 用)。
87
+ * 形状就是 `src/imagegen.ts` 的 {@link ImagegenDeps} 再加一个 `workspaces`。
88
+ */
89
+ export interface ImageRpcDeps {
90
+ /**
91
+ * 解析网关会话 cookie。未登录 / 会话缺失返回 undefined。
92
+ * **返回值只在本模块内部使用,绝不出现在 RPC 响应里。**
93
+ */
94
+ resolveSession(): Promise<string | undefined>;
95
+ /** 当前 baseURL 对应的网关客户端;baseURL 未配置时返回 undefined。 */
96
+ gateway(): ImageGatewayPort | undefined;
97
+ /** 工作区注册表读取面(画廊页 `save` 的 `workspaceId` → 目录路径)。 */
98
+ workspaces?: ImageWorkspacePort | undefined;
99
+ /** `image-config` 读取器(带 ≤5 分钟缓存);缺省直接读网关。 */
100
+ imageConfig?: ((sessionCookie: string) => Promise<GatewayImageConfig | undefined>) | undefined;
101
+ /** 时钟(测试注入)。 */
102
+ now?: (() => Date) | undefined;
103
+ /** Host 日志出口(只记事件与错误类型,不记会话值、不记 base64)。 */
104
+ log?: ((message: string) => void) | undefined;
105
+ }
106
+ /** 各稳定错误码的用户可见文案(与卡片侧 `IMAGE_ERROR_TEXT` 逐字对齐)。 */
107
+ export { IMAGE_GENERIC_FAILURE_MESSAGE, IMAGE_MESSAGE_FALLBACK, IMAGE_SESSION_EXPIRED_MESSAGE, displayImageGatewayMessage, } from './imagegen.ts';
108
+ /**
109
+ * `status` 端点读不到配置时的回落值(**成功信封**,见模块头注释第 1 条):
110
+ * 能力消失,而不是弹一条用户看不懂的错误。
111
+ */
112
+ export declare const IMAGE_FALLBACK_CONFIG: GatewayImageConfig;
113
+ /**
114
+ * 网关 message 消毒 / 错误码兜底文案都从 `imagegen.ts` 再导出(那里是唯一实现,
115
+ * 因为工具的失败收敛与这里的信封都要用它,而反向 import 会成环)。
116
+ *
117
+ * 为什么还要 `export`:`/hiwork-image` 的消费方(以及 spec §6.7 点名的用例)拿的是
118
+ * **频道模块**的公开面——消毒规则属于这条频道的契约,不该逼调用方去 import 服务层。
119
+ */
120
+ /** 是否为白名单端点。 */
121
+ export declare function isImageRpcEndpoint(endpoint: string): endpoint is ImageRpcEndpoint;
122
+ /**
123
+ * payload 校验:每个 endpoint 只放行它声明的字段,其余字段 / 类型不符一律 `bad-request`。
124
+ *
125
+ * 缺省 / null 一律等价于空对象(页面挂载时 `status` 的调用就是 `{}` 或不带 payload)。
126
+ * **不在这里做业务校验**(prompt 长度、size 枚举由网关与 `imagegen.ts` 负责)——
127
+ * 这里只做协议形状,越早失败越便宜,但重复实现一遍业务规则只会多一处漂移。
128
+ */
129
+ export declare function parseImagePayload(endpoint: ImageRpcEndpoint, raw: unknown): ImageRpcPayload;
130
+ /**
131
+ * 处理一次端点调用:返回 DSH Connection 的结果信封,**永不抛异常**。
132
+ * 导出以便单测直接驱动(无需伪造 `ctx.connection`)。
133
+ *
134
+ * `status` 走 {@link readImageConfigSafe} 的**降级**分支:网关故障 → 成功信封里的
135
+ * `enabled:false`。「没有会话」「网关地址未配置」两条**不是**降级——那时还没有任何功能
136
+ * 状态可言,照常回错误信封。
137
+ */
138
+ export declare function handleImageEndpoint(endpoint: string, rawPayload: unknown, deps: ImageRpcDeps, signal?: AbortSignal): Promise<ImageRpcResult<unknown>>;
139
+ /**
140
+ * 注册 `/hiwork-image` 频道,返回异步 disposer(交给 `ctx.effect` 管理生命周期)。
141
+ */
142
+ export declare function registerImageRpc(ctx: ImageRpcContext, deps: ImageRpcDeps): () => Promise<void>;
@@ -0,0 +1,241 @@
1
+ import { type GatewayImageConfig, type GatewayImageFile, type GatewayImageGenerateResult, type GatewayImageRow, type GatewayImagesView } from './ai-router-client.ts';
2
+ /**
3
+ * 允许的尺寸三值。**跨仓冻结契约**(与网关 `image.default_size` 的枚举、Agent 工具 schema
4
+ * 逐项一致):`defineTool` 在注册期就把 schema 冻死,没法让枚举跟着 `image-config` 走。
5
+ */
6
+ export declare const IMAGE_SIZES: readonly ["1024x1024", "1024x1536", "1536x1024"];
7
+ export type ImageSize = (typeof IMAGE_SIZES)[number];
8
+ /** 尺寸缺省值(网关 `image-config` 读不到时的兜底)。 */
9
+ export declare const IMAGE_DEFAULT_SIZE: ImageSize;
10
+ /** 工作区里存放生成图的目录名(设计 §6.2:`<会话工作区>/图片/`)。 */
11
+ export declare const IMAGE_DIR_NAME = "\u56FE\u7247";
12
+ /**
13
+ * 单张图上限 20 MiB(与桌面端附件上限同值)。`fetch` 端点超过它直接拒绝——
14
+ * 绝不把几十 MB 的 base64 塞进 loopback RPC 信封。
15
+ */
16
+ export declare const MAX_IMAGE_BYTES: number;
17
+ /** 本次调用用的工作区根取不到时的兜底(无会话上下文的画廊页 `save` 也用它)。 */
18
+ export declare const IMAGE_FALLBACK_WORKSPACE: string;
19
+ /** `image-config` 的 Host 侧缓存时长(设计 §6.3:≤5 分钟)。 */
20
+ export declare const IMAGE_CONFIG_CACHE_TTL_MS: number;
21
+ /** 落盘成功的一张图。`path` 是**绝对路径**,落在会话工作区内。 */
22
+ export interface GeneratedImageFile {
23
+ path: string;
24
+ mimeType: string;
25
+ byteSize: number;
26
+ revisedPrompt?: string;
27
+ }
28
+ /** 生图成功:图片已写进工作区。 */
29
+ export interface ImageGenerateSuccess {
30
+ ok: true;
31
+ files: GeneratedImageFile[];
32
+ prompt: string;
33
+ model: string;
34
+ /** 网关回显的实际尺寸(仅用于展示/日志)。 */
35
+ size: string;
36
+ generationMs: number;
37
+ }
38
+ /**
39
+ * 生图失败。`code` 直接透传网关的 `IMAGE_*`(卡片按 code 分文案,设计 §6.6)。
40
+ *
41
+ * 写盘失败也走这一支(`code: 'write-failed'`):上游已经计费、图片已经生成,
42
+ * 只把 `files` 置空并把上游信息带上,让卡片说「图片已生成但没能保存到工作区」——
43
+ * **不让员工以为白花了钱**。
44
+ */
45
+ export interface ImageGenerateFailure {
46
+ ok: false;
47
+ code: string;
48
+ message: string;
49
+ files: readonly GeneratedImageFile[];
50
+ prompt?: string;
51
+ model?: string;
52
+ size?: string;
53
+ generationMs?: number;
54
+ }
55
+ export type GenerateOutcome = ImageGenerateSuccess | ImageGenerateFailure;
56
+ /** 画廊页 `save` 的结果:工作区/写盘这类**业务**失败用 `{ok:false, code, reason}`(设计 §6.5)。 */
57
+ export type SaveImageOutcome = {
58
+ ok: true;
59
+ path: string;
60
+ } | {
61
+ ok: false;
62
+ code: 'IMAGE_WORKSPACE_NOT_FOUND' | 'IMAGE_SAVE_FAILED';
63
+ reason: string;
64
+ };
65
+ /**
66
+ * 网关客户端的生图子集:只声明本模块真正调用的方法(结构兼容 `AiRouterClient` 实例),
67
+ * 单测因此不需要伪造整个客户端。**四**个方法——「存到工作区」不是网关端点,
68
+ * 它就是本模块的 {@link saveImageToWorkspace}(取本体 + 写盘)。
69
+ */
70
+ export interface ImageGatewayPort {
71
+ getMyImageConfig(sessionCookie: string): Promise<GatewayImageConfig>;
72
+ generateImage(sessionCookie: string, req: {
73
+ prompt: string;
74
+ size?: string;
75
+ quality?: string;
76
+ }, keyId?: string, timeoutMs?: number): Promise<GatewayImageGenerateResult>;
77
+ getMyImages(sessionCookie: string, req: {
78
+ page?: number;
79
+ pageSize?: number;
80
+ }): Promise<GatewayImagesView>;
81
+ fetchMyImage(sessionCookie: string, id: number): Promise<GatewayImageFile>;
82
+ }
83
+ /** 工作区注册表的读取面(画廊页 `save` 的 `workspaceId` → 目录路径)。 */
84
+ export interface ImageWorkspacePort {
85
+ /** 取不到该 id 时返回 undefined(页面据此显示原因,而不是落进别的工作区)。 */
86
+ resolveWorkspacePath(workspaceId: string): string | undefined;
87
+ }
88
+ /** Host 侧生图依赖:会话 + 网关 + 工作区 + 日志。 */
89
+ export interface ImagegenDeps {
90
+ /**
91
+ * 解析网关会话 cookie。未登录 / 会话缺失返回 undefined。
92
+ * **返回值只在本模块内部使用,绝不出现在响应里。**
93
+ */
94
+ resolveSession(): Promise<string | undefined>;
95
+ /** 当前 baseURL 对应的网关客户端;baseURL 未配置时返回 undefined。 */
96
+ gateway(): ImageGatewayPort | undefined;
97
+ /**
98
+ * 工作区根(**本次调用**的会话工作区,由工具侧从 `exec.agent.session.header.cwd` 取)。
99
+ * 缺省回落 {@link IMAGE_FALLBACK_WORKSPACE}。`request.workspaceRoot` 优先于它。
100
+ */
101
+ workspaceRoot?: string | undefined;
102
+ /**
103
+ * `image-config` 读取器(Host 侧缓存 ≤5 分钟)。缺省直接读网关、失败即 undefined
104
+ * (尺寸拿不到就落 {@link IMAGE_DEFAULT_SIZE},不让配置读取失败挡住生图)。
105
+ */
106
+ imageConfig?: ((sessionCookie: string) => Promise<GatewayImageConfig | undefined>) | undefined;
107
+ /** 工作区注册表读取面(画廊页 `save` 用)。 */
108
+ workspaces?: ImageWorkspacePort | undefined;
109
+ /** 时钟(测试注入;缺省 `new Date()`)。 */
110
+ now?: (() => Date) | undefined;
111
+ /** Host 日志出口(只记元数据:尺寸 / 耗时 / 字节数 / 错误类型)。 */
112
+ log?: ((message: string) => void) | undefined;
113
+ }
114
+ /** 一次生图请求。`size` / `quality` 缺省由 Host / 网关补默认值。 */
115
+ export interface ImageGenerateRequest {
116
+ prompt: string;
117
+ size?: string | undefined;
118
+ quality?: string | undefined;
119
+ /** 本次调用的工作区根(工具侧从会话 cwd 取);优先于 `deps.workspaceRoot`。 */
120
+ workspaceRoot?: string | undefined;
121
+ }
122
+ /** 越出工作区的路径(内部异常,映射成 `path-outside-workspace`)。 */
123
+ export declare class ImagePathError extends Error {
124
+ readonly target: string;
125
+ constructor(target: string);
126
+ }
127
+ /** 工作区里的图片目录。 */
128
+ export declare function imageDirectory(workspaceRoot: string): string;
129
+ /**
130
+ * `target` 是否严格落在 `workspaceRoot` 内(`workspaceRoot` 本身不算)。
131
+ *
132
+ * 用 `path.relative` 判定而不是字符串前缀:前缀法会被 `/tmp/ws-other` 这种「同前缀的
133
+ * 兄弟目录」骗过去。两侧都只做 `resolve`(不做 `realpath`)——拼路径与校验用同一套
134
+ * 拼法,符号链接的归一化是文件路由那边的事(设计 §1.3 路径 ①)。
135
+ */
136
+ export declare function isInsideWorkspace(workspaceRoot: string, target: string): boolean;
137
+ /**
138
+ * 拼一张图的落盘绝对路径并做越界校验(**唯一**的拼法,写盘与校验共用它)。
139
+ * 越界即抛 {@link ImagePathError}。
140
+ *
141
+ * **两道都要过**:
142
+ * 1. 落在工作区根内(设计 §6.2 的前缀校验);
143
+ * 2. 落在 `<工作区>/图片/` 内(「只写这个目录」)——`../x.png` 会滑进工作区根,
144
+ * 虽然没出工作区,但不在约定目录里,同样拒绝。
145
+ */
146
+ export declare function imageFilePath(workspaceRoot: string, fileName: string): string;
147
+ /**
148
+ * prompt → 文件名 slug:取前 24 个字符(先 trim,免得名字以 `-` 开头)、非法字符换成 `-`。
149
+ * 结果为空 / 只剩 `-`(纯标点、纯空白)时用 `image` 兜底。
150
+ *
151
+ * 中文与 emoji 是**合法**字符,原样保留(文件系统支持)。
152
+ */
153
+ export declare function slugFromPrompt(prompt: string): string;
154
+ /** `<yyyyMMdd-HHmmss>`(本地时区,与用户看到的文件时间一致)。 */
155
+ export declare function imageStamp(when: Date): string;
156
+ /**
157
+ * 扩展名:**按图片自身的格式**取,未知格式落 `png`。
158
+ *
159
+ * 刻意的偏离(设计 §6.2 写的是 `<…>.png`):客户端同源文件路由按**扩展名**定 MIME
160
+ * (设计 §1.3),一张 JPEG 存成 `.png` 会被当成 PNG 下发。上游常态是 PNG,
161
+ * 所以正常路径与文档逐字一致,只有非 PNG 才不同。
162
+ */
163
+ export declare function imageExtension(format: string, mimeType: string): string;
164
+ /**
165
+ * 写一张图到 `<workspaceRoot>/图片/`:目录只 `mkdir recursive` 一次,同名加 `-2` / `-3` 后缀。
166
+ *
167
+ * 用 `flag: 'wx'`(不存在才创建)而不是「先 exists 再写」:两者之间没有 TOCTOU 窗口,
168
+ * 命名冲突与真正的写盘失败因此能被分开(前者换名字重试,后者如实上报)。
169
+ *
170
+ * @returns 写入的**绝对路径**。
171
+ */
172
+ export declare function writeImageBytes(workspaceRoot: string, baseName: string, extension: string, bytes: Uint8Array): Promise<string>;
173
+ /** 直接读网关配置;**永不抛异常**(读不到返回 undefined,调用方落默认值)。 */
174
+ export declare function readImageConfig(gateway: ImageGatewayPort, sessionCookie: string, log?: ((message: string) => void) | undefined): Promise<GatewayImageConfig | undefined>;
175
+ /**
176
+ * 包一层 ≤5 分钟缓存的 `image-config` 读取器(设计 §6.3:工具补默认尺寸时读它)。
177
+ *
178
+ * 缓存只存**配置**(开关/模型名/尺寸枚举),不存 cookie、不存任何凭据;同一 Host 进程
179
+ * 只服务一个登录账号,所以缓存不需要按账号分键。
180
+ *
181
+ * 但**要按 `key` 分**:`key` 是调用方给的缓存键(生产传 gateway baseURL)——用户在
182
+ * 「网关模型」里换了地址之后,旧网关的默认尺寸与模型名不该继续生效 5 分钟。
183
+ * 只保留最近一个键(换地址是罕见操作,为此维护一张有界 LRU 不值)。
184
+ */
185
+ export declare function createCachedImageConfigReader(options?: {
186
+ ttlMs?: number;
187
+ now?: () => number;
188
+ }): (gateway: ImageGatewayPort, sessionCookie: string, log?: ((message: string) => void) | undefined, key?: string) => Promise<GatewayImageConfig | undefined>;
189
+ /** 通用失败文案(未知 code / message 不可用时的兜底):不暴露任何网关细节。 */
190
+ export declare const IMAGE_GENERIC_FAILURE_MESSAGE = "\u56FE\u7247\u751F\u6210\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
191
+ /** 未登录 / 会话失效的统一文案(用量、语音、生图三条频道同口径)。 */
192
+ export declare const IMAGE_SESSION_EXPIRED_MESSAGE = "\u767B\u5F55\u72B6\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55";
193
+ /**
194
+ * 各稳定错误码的用户可见文案。
195
+ *
196
+ * **唯一的事实源**:`generate` 的失败收敛与 `/hiwork-image` 的消毒(`image-rpc.ts` 从这里
197
+ * 再导出 {@link displayImageGatewayMessage})都读它。文案与卡片侧
198
+ * `src/client/image-cards.tsx` 的 `IMAGE_ERROR_TEXT` **逐字对齐**——卡片按 code 分文案,
199
+ * 这里只是「网关 message 不可用时的兜底」,两处写着同一句话是有意的:
200
+ * 卡片不依赖 Host 文案,Host 也不依赖卡片(谁先加载都成立)。
201
+ */
202
+ export declare const IMAGE_MESSAGE_FALLBACK: Record<string, string>;
203
+ /**
204
+ * 网关 message 消毒(与 `voice-rpc.ts` 的 `displayVoiceGatewayMessage` 同实现):
205
+ * 含 baseURL / HTTP 状态码 / URL 的原始消息一律换成稳定文案,其余(网关自己写好的
206
+ * 中文业务文案)原样保留。**code 不受影响**,始终原样透传。
207
+ *
208
+ * 放在 `imagegen.ts` 是因为**两条消费路径都要它**(工具的失败收敛 + RPC 的信封),
209
+ * 而 `image-rpc.ts` 依赖 `imagegen.ts`(反向 import 会成环)。
210
+ */
211
+ export declare function displayImageGatewayMessage(code: string, message: unknown): string;
212
+ /**
213
+ * 生图并写盘(设计 §6.2)。
214
+ *
215
+ * 顺序:① prompt 校验 → ② 会话 → ③ 网关 → ④ 尺寸缺省时取 `image-config` 的默认值
216
+ * → ⑤ 调网关(带 timeoutMs 的那一条,见 `ai-router-client.ts`)→ ⑥ 解码写盘 → ⑦ 组装结果。
217
+ *
218
+ * 写盘失败**不回滚**:回 `code: 'write-failed'` + 上游信息(prompt/model/size/generationMs),
219
+ * `files` 置空。越界路径回 `code: 'path-outside-workspace'`。
220
+ */
221
+ export declare function generateImageFile(deps: ImagegenDeps, request: ImageGenerateRequest): Promise<GenerateOutcome>;
222
+ /**
223
+ * 画廊页 `save`(设计 §6.5):取本体 → 按生图同一套规则落盘 → 回绝对路径。
224
+ *
225
+ * 落点是**默认工作区** `~/HiWork/Default`(画廊页是中央页,没有会话上下文);给了
226
+ * `workspaceId` 就用注册表里那个工作区的路径,**取不到即拒绝**(绝不落到别处)。
227
+ *
228
+ * 文件名沿用 §6.2 的规则,slug 来自该记录的 `prompt`。记录本体在网关,而冻结入参只有
229
+ * `{ id, workspaceId? }`(页面按契约只传 id),所以这里**尽力**回查一次列表拿 prompt:
230
+ * 查不到、或第一页里没有这条记录,就用 `image` 兜底——**不为一个文件名把保存变成硬依赖**。
231
+ *
232
+ * 失败分两类:网关/会话错误**抛** `GatewayError`(由 RPC 层映射成稳定 code);
233
+ * 工作区解析 / 写盘这类业务失败回 `{ok:false, code, reason}`。
234
+ */
235
+ export declare function saveImageToWorkspace(deps: ImagegenDeps, request: {
236
+ id: number;
237
+ workspaceId?: string | undefined;
238
+ prompt?: string | undefined;
239
+ }): Promise<SaveImageOutcome>;
240
+ /** 画廊页 `list` 的一行(冻结契约 `ImageRow`,设计 §6.5)。 */
241
+ export type ImageRow = GatewayImageRow;
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * hiwork-core:企业 ai-router 网关登录 + 模型自动加载插件(Host 半边)。
3
- * 注册 `ai-router` settings 命名空间;卡片写入的请求字段(pendingLogin /
4
- * refreshRequest / logoutRequest)经 settings.installSection 的 onChange 触发
5
- * 编排,编排结果回写该命名空间的状态字段(lastLogin / lastError)。
3
+ *
4
+ * rc.2 起 dsh-settings 的 seam 重写:不再有 installSection / register —— 配置面改为
5
+ * 「插件 Config schema 的 `.volatile()` 字段经 configEditor 投影成表单」,settings 命名空间
6
+ * 就是本插件在 profile 里的 entry id(`ai-router`,见 cordis.patch.yml 的 insert 行,两处必须
7
+ * 逐字一致)。卡片写入的请求字段(pendingLogin / refreshRequest / logoutRequest)经
8
+ * `settings/document-updated` 事件触发编排,编排结果回写本 entry 的 volatile 字段。
6
9
  */
7
- import type { Context } from '@deepseek-ai/cordis';
10
+ import type { Context, Volatile } from '@deepseek-ai/cordis';
8
11
  import z from '@deepseek-ai/schemastery';
9
- import { type SettingsProvider } from '@deepseek-ai/dsh-settings';
12
+ import type SettingsForms from '@deepseek-ai/dsh-settings';
10
13
  import { fetchClientPolicy, type ClientPolicy, type LoginMode } from './client-policy.ts';
11
14
  import { type CredentialsPort, type DefaultModelPort, type AiRouterStatePort } from './orchestration.ts';
12
15
  export declare const name = "hiwork-core";
@@ -14,9 +17,30 @@ export declare const name = "hiwork-core";
14
17
  * 必需服务:`connection` 承载设置页「用量与成本」的 loopback RPC 频道
15
18
  * (`/hiwork-usage`,见 `src/usage-rpc.ts`);`web` 承载联网搜索 provider 的注册
16
19
  * (`ctx.web.registerSearchProvider`,见 `src/web-search.ts`;dsh-base 基座先于本
17
- * 插件挂载 dsh-web);其余三项是既有登录 / 供给依赖。
20
+ * 插件挂载 dsh-web);`agents` 是根 Agent 注册表——`image_generate` 工具要挂在根 Agent
21
+ * 上(`ctx.agents.roots()` / `agent/created`,见 `src/tools.ts`;与 knowledge 的
22
+ * `inject` 同源);其余三项是既有登录 / 供给依赖。
18
23
  */
19
24
  export declare const inject: string[];
25
+ /** 侧栏菜单偏好的解析值(client 侧 `MenuPreferences` 的同一份形状)。 */
26
+ export interface MenuConfigValue {
27
+ /** 展示顺序(feature id)。 */
28
+ order: string[];
29
+ /** 明确显示在侧栏的 id。 */
30
+ sidebar: string[];
31
+ /** 明确收进「更多」的 id。 */
32
+ more: string[];
33
+ /** 明确隐藏的 id(侧栏与「更多」都不出现)。 */
34
+ hidden: string[];
35
+ }
36
+ /**
37
+ * 菜单偏好 schema:四个 id 数组,缺省全空(= 用户从未自定义 → 由 client 按默认规则分组)。
38
+ *
39
+ * 这里**不做枚举校验**:合法的 id 集合由「当前装配了哪些 feature」决定,是运行期事实,写死在 host
40
+ * 的 schema 里会立刻过期(插件装了/卸了都会变)。归一化与清理在 client 侧完成(`normalizeMenuPreferences`),
41
+ * 未知 id 在渲染时被忽略而不是拒绝写入——否则用户装过一次又卸掉的插件会永久毒化这份偏好。
42
+ */
43
+ export declare const MenuConfig: z<MenuConfigValue>;
20
44
  /** 插件配置 + 运行时状态字段(全部可选;写操作按此 schema 校验)。 */
21
45
  export interface Config {
22
46
  baseURL?: string;
@@ -79,8 +103,55 @@ export interface Config {
79
103
  max_concurrency?: number;
80
104
  key_masked?: string;
81
105
  }>;
106
+ /**
107
+ * 侧栏菜单偏好(rc.2 起并入本 entry;此前是独立的 `hiwork-menu` settings 命名空间,
108
+ * 而 rc.2 的 settings 只服务「有 profile entry 的 Config」,纯客户端偏好没有自己的 entry
109
+ * 可挂,遂改挂本 entry 的 volatile 字段。旧值由 `importLegacyMenuSection` 一次性搬迁)。
110
+ */
111
+ menu?: MenuConfigValue;
82
112
  }
83
113
  export declare const Config: z<Config>;
114
+ /**
115
+ * apply 收到的运行时 config:volatile 字段是稳定引用(schemastery 解析期包装,值为
116
+ * undefined 时引用依然存在),随 profile 变更被框架原地更新——持有引用、每次现取,
117
+ * 绝不缓存 `.get()` 的结果。baseURL 仍是普通组合层配置。
118
+ */
119
+ export type RuntimeConfig = {
120
+ baseURL?: string;
121
+ } & {
122
+ [K in Exclude<keyof Config, 'baseURL'>]?: Volatile<Config[K]>;
123
+ };
124
+ /**
125
+ * settings.mutate 的路径操作(rc.2 的 dsh-settings 不再导出 `SettingsPathOp` 类型,
126
+ * 这里按其结构本地声明——形状逐字对齐上游 `packages/settings/settings/src/index.ts`)。
127
+ */
128
+ export type SettingsPathOp = {
129
+ op: 'set';
130
+ path: readonly string[];
131
+ value: unknown;
132
+ } | {
133
+ op: 'unset';
134
+ path: readonly string[];
135
+ };
136
+ /** 失败是否是「事务内写」——调用方据此判断轮询上下文已被 HMR 事务污染(见 apply)。 */
137
+ export declare function isHmrNestedWriteError(error: unknown): boolean;
138
+ /**
139
+ * 写一个命名空间(= profile entry id)。
140
+ *
141
+ * 每次尝试都重读 `describe()` 取最新 revision;可重试错误在截止时间前带退避重试,超时后
142
+ * 抛出**真实**错误——不降级成「conflicted repeatedly」,免得把根因从日志里抹掉。
143
+ * 退避上限 1s:命名空间注册与 HMR 事务都在秒级内收敛,重试代价可忽略。
144
+ *
145
+ * @param settings - settings 服务。
146
+ * @param ns - 命名空间(profile entry id)。
147
+ * @param ops - 路径操作。
148
+ * @param opts - 重试窗口与退避(单测用来把窗口压到毫秒级)。
149
+ */
150
+ export declare function writeNamespace(settings: SettingsForms, ns: string, ops: SettingsPathOp[], opts?: {
151
+ deadlineMs?: number;
152
+ minBackoffMs?: number;
153
+ maxBackoffMs?: number;
154
+ }): Promise<void>;
84
155
  /** 校正默认模型要用到的 llm 注册表部分(便于单测注入假实现)。 */
85
156
  export interface LlmRouteRegistry {
86
157
  listProviders(): readonly {
@@ -99,7 +170,7 @@ export interface LlmRouteRegistry {
99
170
  * @returns 供编排调用的默认模型端口。
100
171
  */
101
172
  export declare function createDefaultModelPort(opts: {
102
- settings: SettingsProvider;
173
+ settings: SettingsForms;
103
174
  llm: LlmRouteRegistry | undefined;
104
175
  log?: (message: string) => void;
105
176
  }): DefaultModelPort;
@@ -174,4 +245,14 @@ export declare function projectLoginMode(opts: {
174
245
  fetchPolicy?: (input: Parameters<typeof fetchClientPolicy>[0]) => Promise<ClientPolicy | undefined>;
175
246
  log?: (message: string) => void;
176
247
  }): Promise<LoginMode>;
177
- export declare function apply(ctx: Context, config: Config): void;
248
+ /**
249
+ * 把运行时 config 的 volatile 引用解成普通值快照(浅层遍历;只处理本插件 Config 的
250
+ * 数据形状)。识别 Volatile 引用用鸭子类型:cosmokit 的 createVolatile 返回「只有一
251
+ * 个字符串键 get 的冻结对象」,本插件 Config 的数据形状里没有任何带 get 方法的成员,
252
+ * 不会误伤。rc.2 起 cordis 只 re-export Volatile 类型、不导出运行时 isVolatile,
253
+ * 为避免依赖树外的 cosmokit 直依赖,这里本地判定。
254
+ *
255
+ * 导出仅供 tests/plain-value.spec.ts 锁定判定契约(见该文件头),不是对外的插件 API。
256
+ */
257
+ export declare function plainValue(value: unknown): unknown;
258
+ export declare function apply(ctx: Context, config: RuntimeConfig): void;
@@ -2,8 +2,18 @@
2
2
  export declare const HIWORK_KNOWLEDGE_SERVICE = "hiworkKnowledge";
3
3
  /** 网关侧端点(跨仓冻结;与 `/api/me/memory-config` 同一条会话鉴权)。 */
4
4
  export declare const ME_KNOWLEDGE_CONFIG_PATH = "/api/me/knowledge-config";
5
- /** 成功结果的缓存 TTL:5 分钟。 */
5
+ /** 成功结果的缓存 TTL:5 分钟(与网关签发的令牌寿命同量级)。 */
6
6
  export declare const KNOWLEDGE_CONFIG_TTL_MS: number;
7
+ /**
8
+ * **令牌**缓存的提前量:令牌寿命与缓存 TTL 都是 5 分钟时,缓存到期时刻恒晚于令牌 `exp`
9
+ * (响应到达 = 签发 + 一次往返),那段时间里命中的令牌**必然已过期** → 上游 401 → 插件
10
+ * 强制刷新一次再重试。留 30 秒余量把这条无谓的 401 往返消掉。
11
+ *
12
+ * (配置缓存的 TTL 不需要余量:`base_url`/`api_key` 没有自带寿命。)
13
+ */
14
+ export declare const KNOWLEDGE_TOKEN_CACHE_MARGIN_MS = 30000;
15
+ /** 令牌缓存的 TTL:比令牌寿命短一点(见上面的理由)。 */
16
+ export declare const KNOWLEDGE_TOKEN_CACHE_TTL_MS: number;
7
17
  /** 出网超时(与 `AiRouterClient` 的缺省一致)。 */
8
18
  export declare const KNOWLEDGE_CONFIG_TIMEOUT_MS = 10000;
9
19
  /**
@@ -27,15 +37,51 @@ export interface HiworkKnowledgeConfigView {
27
37
  agentId?: string;
28
38
  chatModelId?: string;
29
39
  tenantId?: string;
40
+ /**
41
+ * 网关下发的**展示**范围(设计 §3.1 的第六个字段 `knowledge_bases`):去空 ID、按 ID 去重、
42
+ * 名称 trim。缺省即空数组——**展示字段不是权限依据**,权限依据是 `token()` 面的
43
+ * `knowledgeBaseIds`(设计 §3.1「展示名称不是权限依据」)。
44
+ *
45
+ * 为什么必须带出来:网关(hiwork-admin)确实在下发它(`me_knowledge_handler.go` 的
46
+ * `knowledge_bases`,同样做了去空/去重/trim),而这里过去只投影五个字段、把它丢了 ——
47
+ * 结果是插件侧读它的那段兼容代码**永远是死代码**,且宿主没有 token 面时(老 core 形状)
48
+ * 授权范围只能恒为空。展示与兜底都需要它,所以按视图口径翻成 camelCase 带出来。
49
+ */
50
+ knowledgeBases?: readonly {
51
+ readonly id: string;
52
+ readonly name: string;
53
+ }[];
30
54
  reason?: HiworkKnowledgeFailureReason;
31
55
  message?: string;
32
56
  }
33
57
  /** 本服务对宿主的可调用面(只读、永不抛异常)。 */
58
+ export interface HiworkKnowledgeTokenView {
59
+ ok: boolean;
60
+ token?: string;
61
+ audience?: string;
62
+ knowledgeBaseIds?: string[];
63
+ /**
64
+ * 授权范围的展示信息(`token()` 面:**只保留在 ID 范围内的项**,见下方测试)。
65
+ * 写成只读,与 `HiworkKnowledgeConfigView.knowledgeBases` 同形——两个视图在
66
+ * `failure()` 返回值上是同一份数据的两种键名,形状不一致会让 token 面的失败返回
67
+ * 连类型都过不去。
68
+ */
69
+ knowledgeBases?: readonly {
70
+ readonly id: string;
71
+ readonly name: string;
72
+ }[];
73
+ reason?: HiworkKnowledgeFailureReason;
74
+ message?: string;
75
+ }
34
76
  export interface HiworkKnowledgeService {
35
- /** 取当前登录员工的知识库配置;force=true 绕过缓存(设置页「重新获取配置」与 401 自愈)。 */
77
+ /** 取当前登录员工的知识库配置;force=true 绕过缓存。 */
36
78
  config(opts?: {
37
79
  force?: boolean;
38
80
  }): Promise<HiworkKnowledgeConfigView>;
81
+ /** 取当前会话的短期知识库 token;仅 Host-to-Host 服务面可见。 */
82
+ token(opts?: {
83
+ force?: boolean;
84
+ }): Promise<HiworkKnowledgeTokenView>;
39
85
  }
40
86
  /**
41
87
  * 网关侧原始响应(**snake_case**,跨仓冻结契约;本文件是唯一读它的地方)。
@@ -73,6 +119,8 @@ export type KnowledgeConfigOutcome = {
73
119
  export interface KnowledgeConfigGatewayPort {
74
120
  /** 调 `GET /api/me/knowledge-config`,返回 snake_case 原始响应或一个稳定失败原因。 */
75
121
  getMyKnowledgeConfig(sessionCookie: string): Promise<KnowledgeConfigOutcome>;
122
+ /** 为当前会话签发短期知识库令牌;老 core 可省略。 */
123
+ getMyKnowledgeToken?: (sessionCookie: string) => Promise<unknown>;
76
124
  }
77
125
  /** Host 侧取数依赖:会话解析 + 当前 baseURL 下的网关出网端口。 */
78
126
  export interface KnowledgeConfigDeps {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 生图工具名的**单一事实源**。
3
+ *
4
+ * Web 半边(`src/client/ImageCards.tsx` 的 `tool.call.toolview` keyed 座位)与 Host 注册
5
+ * 都从这里读同一个常量:keyed 座位**按线上工具名认领**,名字拼错的失败方式是**静默**的
6
+ * ——卡片永不渲染、通用工具行照常出现,没有任何测试会因为它变红(knowledge 已踩过)。
7
+ * 所以这里既导出元组,也导出它的元素类型(`Record<ImageToolName, …>` 这种写法能在
8
+ * 编译期挡住「新增工具忘了补卡片」)。
9
+ */
10
+ export declare const IMAGE_TOOL_NAMES: readonly ["image_generate"];
11
+ export type ImageToolName = (typeof IMAGE_TOOL_NAMES)[number];