hiwork-core 0.0.18 → 0.0.21

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,357 @@
1
+ /**
2
+ * 「PMS 个人待办」客户端取数层:经 `/hiwork-pms-todo` loopback RPC 拿到自己的待办、计数,
3
+ * 并托管登录 / 登出(凭据本身**不经过本层**,见下),交给工作台插件(hiwork-workbench)的
4
+ * 右栏 tab 渲染。
5
+ *
6
+ * 契约见 spec `docs/superpowers/specs/2026-09-17-pms-todo-integration-design.md` §4.1(Host 频道)、
7
+ * §4.2(`TodoItem`)、§4.3(本面)、§3 D7(**我的任务**:待办之外还要直接查未完成的项目任务)。
8
+ * 本文件与 `src/client/managed-workbench.ts` 同款,是同一副骨架的**第四条链**,差别只有一处:
9
+ * 那条链是纯只读,这一条还带「登录 / 登出」两个动作。
10
+ *
11
+ * 铁律:
12
+ * - **本层不持有任何凭据**:账号密码只在 `login()` 的入参里出现一次,随即交给 Host;access /
13
+ * refresh token、PMS 的域名与路径**全都不在本文件里**(client bundle 里搜不到 PMS 凭据,§7);
14
+ * - **不 fetch、不直连 PMS**:一切经 Host 频道——这正是 §3 D6「凭据与数据流都在 Host」在
15
+ * 客户端一侧的落地形状;
16
+ * - **错误码原样透传**:Host 的六个稳定码(`NOT_CONFIGURED` / `UNAUTHORIZED` / `FORBIDDEN` /
17
+ * `UPSTREAM_UNAVAILABLE` / `BAD_REQUEST` / `INTERNAL`)由消费方决定文案(未配置 → 引导填
18
+ * 凭据;`UNAUTHORIZED` → 要求重新输入;`FORBIDDEN` → 「到 PMS 操作」,**不是**重新登录)。
19
+ * 本层**不改写、不吞掉**它们。
20
+ *
21
+ * client bundle 纯度:本文件不 import 任何 `@deepseek-ai/*` 运行时值,也不 import Host 代码
22
+ * (类型按结构重声明,字段名与契约一致)。
23
+ */
24
+ /** RPC 频道;必须与 Host `src/pms-todo-bridge.ts` 的 `PMS_TODO_RPC_CHANNEL` 一致(由测试锁定)。 */
25
+ export declare const PMS_TODO_CHANNEL = "/hiwork-pms-todo";
26
+ /**
27
+ * 端点白名单;必须与 Host `PMS_TODO_RPC_ENDPOINTS` 一致(由测试锁定)。
28
+ * 顺序即冻结契约里的顺序(`status` / `login` / `logout` 是凭据生命周期,
29
+ * `today` / `statistics` / `setStatus` 是待办数据,`myTasks` / `completeTask` 是项目任务 —— §3 D7)。
30
+ */
31
+ export declare const PMS_TODO_ENDPOINTS: readonly ["status", "login", "logout", "today", "statistics", "setStatus", "myTasks", "completeTask"];
32
+ export type PmsTodoEndpoint = (typeof PMS_TODO_ENDPOINTS)[number];
33
+ /** 勾选用的两个字面量(PMS 只认这两个)。 */
34
+ export type PmsTodoStatusValue = 'PENDING' | 'COMPLETED';
35
+ /**
36
+ * 一条待办(**冻结契约**,spec §4.2 的 `TodoItem` 投影,字段名与 Host 一一对应)。
37
+ *
38
+ * 两处开放的形状(消费方按它降级,与工作台清单的 `executor.kind` 同款纪律):
39
+ * - `status` / `priority` / `category` 都是**开放集合**:不认识的取值必须原样出现在这里,
40
+ * 由消费方自己决定怎么渲染(例如不认识的 `priority` 就不显示徽标),**绝不**因为它不认识
41
+ * 就把这条待办判为无效——那会让用户看到一份莫名变少的列表;
42
+ * - 可选字段按服务端给没给来(没给就没有这个键)。
43
+ */
44
+ export interface PmsTodoItem {
45
+ id: number;
46
+ title: string;
47
+ description?: string;
48
+ /** `TODO` | `TEMP` | 将来新增的值。 */
49
+ category?: string;
50
+ /** `HIGH` | `MEDIUM` | `LOW` | 将来新增的值。 */
51
+ priority?: string;
52
+ /** `PENDING` | `COMPLETED` | 将来新增的值(**原样透传**)。 */
53
+ status: string;
54
+ startDate?: string;
55
+ dueDate?: string;
56
+ completedTime?: string;
57
+ /** 服务端判定(客户端不重算)。 */
58
+ isOverdue?: boolean;
59
+ isDueSoon?: boolean;
60
+ }
61
+ /** `status` / `login` / `logout` 的返回值(spec §4.1)。 */
62
+ export interface PmsTodoStatus {
63
+ configured: boolean;
64
+ loggedIn: boolean;
65
+ username?: string;
66
+ /** access token 的绝对过期时刻(ISO 8601)。**不是** token 本身。 */
67
+ expiresAt?: string;
68
+ }
69
+ /** `today` 的返回值。 */
70
+ export interface PmsTodoToday {
71
+ items: PmsTodoItem[];
72
+ /** 本次成功拉取的时刻(ISO 8601);界面用它显示「更新于 …」。 */
73
+ fetchedAt: string;
74
+ }
75
+ /** `statistics` 的返回值。 */
76
+ export interface PmsTodoStatistics {
77
+ total: number;
78
+ pending: number;
79
+ completed: number;
80
+ overdue: number;
81
+ }
82
+ /** setStatus 的返回值(面里折成 void,仓库里保留给诊断与测试)。 */
83
+ export interface PmsTodoSetStatusResult {
84
+ id: number;
85
+ status: PmsTodoStatusValue;
86
+ completedTime?: string;
87
+ }
88
+ /**
89
+ * 一条**项目任务**(spec §3 D7;Host `PmsTaskItem` 的**结构重声明**——本层不 import Host 代码)。
90
+ * 数字 / 字符串字段与 PMS `TaskVO` 一一对应;`projectStageName` / `requirementName` /
91
+ * `moduleName` / `estimatedHours` / `actualHours` 是设计里明确可选的五项(服务端没给就没有这个键)。
92
+ *
93
+ * 演进纪律(与 {@link PmsTodoItem} 同款):`status`(0-待办 / 1-进行中 / 2-已完成 / 3-逾期)与
94
+ * `priority` 是**数字**,将来 PMS 加档位照样原样透传;`statusName` / `priorityName` 是给人看的
95
+ * 名字,同样不校验取值。**未知字段忽略**(老 client 不认识的新字段不进契约面)。
96
+ */
97
+ export interface PmsTaskItem {
98
+ id: number;
99
+ taskName: string;
100
+ description: string;
101
+ projectId: number;
102
+ projectName: string;
103
+ projectStageName?: string;
104
+ assigneeId: number;
105
+ assigneeName: string;
106
+ /** `yyyy-MM-dd HH:mm:ss`;没有截止时间时是空串。 */
107
+ deadline: string;
108
+ /** 0-待办 / 1-进行中 / 2-已完成 / 3-逾期。 */
109
+ status: number;
110
+ statusName: string;
111
+ /** 1-低 / 2-中 / 3-高。 */
112
+ priority: number;
113
+ priorityName: string;
114
+ taskType: string;
115
+ taskTypeName: string;
116
+ /** 0-待审核 / 1-已通过 / 2-已驳回。 */
117
+ auditStatus: number;
118
+ auditStatusName: string;
119
+ requirementName?: string;
120
+ moduleName?: string;
121
+ estimatedHours?: number;
122
+ actualHours?: number;
123
+ }
124
+ /**
125
+ * `myTasks` 的返回值(§3 D7):**分页对象**,不是数组。
126
+ * 与 `today`(`{items, fetchedAt}`)形状不同是有意的:任务那边服务端就是分页的(`PageResult`),
127
+ * 本层把 `records` 收进 `items` 但**保留** total / current / size,免得界面想显示「共 N 条」时
128
+ * 只能数当前这一页。
129
+ */
130
+ export interface PmsTaskList {
131
+ items: PmsTaskItem[];
132
+ total: number;
133
+ current: number;
134
+ size: number;
135
+ }
136
+ /** `completeTask` 的返回值(面里折成 void,仓库里保留给诊断与测试)。 */
137
+ export interface PmsTaskCompleteResult {
138
+ id: number;
139
+ /** 任务状态数字(完成是 2)。 */
140
+ status: number;
141
+ }
142
+ /** 登录入参。 */
143
+ export interface PmsTodoLoginInput {
144
+ username: string;
145
+ password: string;
146
+ /** 是否把凭据交给 credentials 服务加密保存(Host 侧语义,见 spec §4.4)。 */
147
+ remember?: boolean;
148
+ }
149
+ /**
150
+ * 「PMS 待办」面(spec §4.3 冻结的七项 + §3 D7 追加的两项)。
151
+ *
152
+ * 它挂在 `HiWorkCoreClientFace` 上作为**可选成员** `pmsTodo?`:老 core 没有这个面,老
153
+ * workbench 也不会来取;两侧因此不必同发一个版本——老 core 遇新 workbench 就是
154
+ * 「请升级 HiWork 客户端」(spec §6 的降级矩阵第一行)。
155
+ *
156
+ * ⚠️ **`myTasks` / `completeTask` 是这一面里的两级可选**(版本闸口):§3 D7 是 2026-09-18 追加的,
157
+ * 装着的旧 core 有 `pmsTodo` 面但**没有**这两项。所以它们是**可选成员**,消费方必须按
158
+ * `face.pmsTodo.myTasks !== undefined` 判断(拿不到就只显示待办那一半,而不是崩页、也不是把
159
+ * 「我的任务」渲染成空列表——那会让人以为任务都完成了)。反过来说,**不许**把它们改成必填:
160
+ * 那会让新插件在旧 core 上直接抛 TypeError(测试用「只实现旧七项的 face 仍满足本接口」钉死这一点)。
161
+ */
162
+ export interface PmsTodoFace {
163
+ /**
164
+ * 本地镜像(**同步读**,可能是 `undefined`)。
165
+ *
166
+ * ⚠️ 它是镜像是**有意的**:界面渲染是同步的,而「有没有配置凭据」是异步来的。镜像由
167
+ * `login` / `logout` 的返回值、以及**首次调用本方法时的一次惰性探测**写入(结果到达后通知
168
+ * 订阅者),所以调用方必须 `subscribe()` ——首次渲染很可能拿到 `undefined`,等通知到了再重渲。
169
+ */
170
+ readonly status: () => PmsTodoStatus | undefined;
171
+ /** 登录(凭据经 Host 换成 JWT;密码只在这一次调用里出现,本层不留存)。失败抛错。 */
172
+ readonly login: (input: PmsTodoLoginInput) => Promise<PmsTodoStatus>;
173
+ /** 登出(Host 清内存凭据 + 抹掉 credentials 记录)。 */
174
+ readonly logout: () => Promise<PmsTodoStatus>;
175
+ /** 今日待办(⚠️ 口径比名字宽:今天 + 明天 + 无截止日期,spec §D4)。失败抛错。 */
176
+ readonly today: (signal?: AbortSignal) => Promise<PmsTodoToday>;
177
+ /** total / pending / completed / overdue 计数。失败抛错。 */
178
+ readonly statistics: (signal?: AbortSignal) => Promise<PmsTodoStatistics>;
179
+ /**
180
+ * 勾完成 / 取消完成。
181
+ *
182
+ * ⚠️ 副作用(spec §D5):这是某个任务的最后一条待办时,**PMS 会把项目任务一并标记完成**。
183
+ * 确认文案是 UI 的职责,本层不替它决定要不要问。
184
+ */
185
+ readonly setStatus: (input: {
186
+ id: number;
187
+ status: PmsTodoStatusValue;
188
+ }) => Promise<void>;
189
+ /**
190
+ * **我的未完成项目任务**(spec §3 D7)。**可选成员**——旧 core 上没有它(见本接口的抬头)。
191
+ *
192
+ * 「谁的任务」由 Host 从凭据记录里取(客户端传不进来),所以这里没有 `userId` 入参。
193
+ * 失败抛错,错误码可判 `FORBIDDEN`(没有完成任务权限的角色会在**完成**时报它)。
194
+ */
195
+ readonly myTasks?: (signal?: AbortSignal) => Promise<PmsTaskList>;
196
+ /**
197
+ * 完成一条项目任务(`status=2`)。**可选成员**——旧 core 上没有它(见本接口的抬头)。
198
+ *
199
+ * ⚠️ 与 `setStatus`(待办)是**两条不同的上游路径**:待办那条没有权限注解,这一条需要
200
+ * `task:list:edit`;缺权限时抛 `FORBIDDEN`,界面要按 §3 D7 说「在 HiWork 里没有完成任务的权限,
201
+ * 请到 PMS 操作」,**不要**当成「登录过期」让用户重新输密码。
202
+ */
203
+ readonly completeTask?: (input: {
204
+ id: number;
205
+ }) => Promise<void>;
206
+ /** 订阅镜像变化(登出 / 换账号时会被清空)。返回退订函数。 */
207
+ readonly subscribe: (listener: () => void) => () => void;
208
+ }
209
+ /** 宿主 loopback RPC 调用面(结构兼容 `ctx.connection.rpc`)。 */
210
+ export interface PmsTodoRpcPort {
211
+ call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
212
+ }
213
+ /** 带稳定 code 的取数错误(message 已由 Host 消毒,可直接展示)。 */
214
+ export declare class PmsTodoRpcError extends Error {
215
+ readonly code: string;
216
+ constructor(code: string, message: string);
217
+ }
218
+ /**
219
+ * 解开 DSH Connection 的结果信封。
220
+ * 宿主返回 `{ok:true,value}` / `{ok:false,error:{code,message}}`;形状不合法时抛
221
+ * 通用失败,绝不把未知结构当数据用。
222
+ *
223
+ * `ok:false` 一律**抛错**——这是「拉取失败」与「拉到一份空列表」之间的那道闸:把它读成
224
+ * `value`,一次网络抖动就会变成「今天没有待办」(用户以为活都干完了)。返回 `value` 的唯一
225
+ * 前提是 `ok === true`。
226
+ */
227
+ export declare function unwrapPmsTodoResult<T>(value: unknown): T;
228
+ /** 任意异常 → 可展示的 `{code, message}`(传输层异常不暴露原始 message)。 */
229
+ export declare function pmsTodoErrorOf(error: unknown): {
230
+ code: string;
231
+ message: string;
232
+ };
233
+ /**
234
+ * PMS 待办的取数仓库。
235
+ *
236
+ * 本地镜像(`status()` 同步读)是有必要的:DSH 的 client 半边在不同插件之间只通过这个面同步取
237
+ * 状态,而「配没配凭据」是异步来的。镜像**只由成功响应覆写**,失败时不改动它。
238
+ */
239
+ export declare class PmsTodoStore {
240
+ private readonly rpc;
241
+ private current;
242
+ private readonly listeners;
243
+ /**
244
+ * 在飞请求**连同它所属的世代与端点**一起记账。
245
+ *
246
+ * 世代:`clear()`(登出 / 换账号)之后,账上那条是**上一个人**发起的,它的待办属于上一个人
247
+ * ——复用它等于让新账号看到上一个人的待办。端点:`today` 与 `statistics` 的返回值形状不同,
248
+ * 只有同一个端点的并发调用才允许合并。
249
+ */
250
+ private inFlight;
251
+ /** 世代号:`clear()` 每调用一次 +1,用来作废它之前发起的在飞请求。 */
252
+ private generation;
253
+ /** 是否已经为「镜像还空着」的情况探过一次 `status`(避免每次同步读都发一次 RPC)。 */
254
+ private statusProbed;
255
+ constructor(rpc: PmsTodoRpcPort);
256
+ /**
257
+ * 本地镜像(同步)。从没同步过返回 `undefined`。
258
+ *
259
+ * ⚠️ 本方法有一次**惰性探测**:镜像还空着且从没探过时,它顺手发起一次 `status` 端点调用
260
+ * (结果到达后写镜像 + 通知订阅者)。理由——渲染是同步的,而「配没配凭据」只有主机知道;
261
+ * 没有这一步,`status` 端点就永远没有调用方,界面也只能靠一次失败的 `today()` 去反推。
262
+ * 因此调用方看到 `undefined` **不是**「未配置」,而是「还不知道」:必须订阅。
263
+ */
264
+ status(): PmsTodoStatus | undefined;
265
+ /**
266
+ * 显式去主机取一次状态并写镜像(`status()` 的异步入口)。
267
+ *
268
+ * 失败**不改镜像**(保留上一次已知状态),也**不在镜像里记错误**:错误属于这一次调用,
269
+ * 调用方(或下一次 `today()`)自己决定怎么展示。
270
+ */
271
+ refreshStatus(signal?: AbortSignal): Promise<PmsTodoStatus>;
272
+ /**
273
+ * 登录(凭据交给 Host;**本层不留存密码、不写任何缓存**)。
274
+ *
275
+ * 成功时把返回的状态写进镜像——登录是「身份刚变过」,镜像必须跟着变。
276
+ */
277
+ login(input: PmsTodoLoginInput): Promise<PmsTodoStatus>;
278
+ /** 登出(Host 侧清凭据);返回登出后的状态并写镜像。 */
279
+ logout(): Promise<PmsTodoStatus>;
280
+ /**
281
+ * 今日待办。
282
+ *
283
+ * 不做本地缓存(返回**刚拉到的**那一份):待办的时效性就是它的全部价值,把上一次的结果当
284
+ * 「今日」交出去正是这个功能最容易犯的错;界面要离线态时自己留一份 + 标陈旧(spec §6)。
285
+ */
286
+ today(signal?: AbortSignal): Promise<PmsTodoToday>;
287
+ /** total / pending / completed / overdue 计数。 */
288
+ statistics(signal?: AbortSignal): Promise<PmsTodoStatistics>;
289
+ /**
290
+ * 勾完成 / 取消完成。
291
+ *
292
+ * 返回值里带服务端回声的 id / status(`PmsTodoFace.setStatus` 折成 `void`;面里不需要它,
293
+ * 但仓库保留着——回滚那一条待办时用它认人最稳)。
294
+ */
295
+ setStatus(input: {
296
+ id: number;
297
+ status: PmsTodoStatusValue;
298
+ }): Promise<PmsTodoSetStatusResult>;
299
+ /**
300
+ * **我的未完成项目任务**(spec §3 D7):`myTasks` 端点的取数。
301
+ *
302
+ * 入参是**空对象**,没有 `userId`——「谁的任务」由 Host 从凭据记录里取(客户端连自己的
303
+ * PMS userId 都拿不到,这是 §3 D6 形状的一部分)。
304
+ *
305
+ * 与 `today` 同款:**不做本地缓存**(返回刚拉到的那一份),但**并发调用合并**(同一时刻问两遍
306
+ * 没有意义;世代判据同其他端点,换账号之后不复用)。
307
+ */
308
+ myTasks(signal?: AbortSignal): Promise<PmsTaskList>;
309
+ /**
310
+ * **完成一条项目任务**(spec §3 D7)。
311
+ *
312
+ * 返回值里带服务端回声的 id / status(`PmsTodoFace.completeTask` 折成 `void`;仓库保留着
313
+ * ——回滚那一条任务时用它认人最稳)。缺权限时抛 `PmsTodoRpcError('FORBIDDEN')`。
314
+ *
315
+ * ⚠️ **刻意不走 {@link PmsTodoStore.call}**(即不参与并发合并):`dedupe` 的键是**端点名**,
316
+ * 而这是带参数的命令——把它并进同一格会让「几乎同时完成 A 和 B」的第二次调用**复用第一次的
317
+ * 结果**,用户以为 B 完成了,其实请求根本没发出去。Host 侧的 `completeTask` 同样不合并
318
+ * (那里也注明「点了两次就是两次动作」)。
319
+ */
320
+ completeTask(input: {
321
+ id: number;
322
+ }): Promise<PmsTaskCompleteResult>;
323
+ /** 订阅镜像变化(右栏 tab 据此重渲染)。返回退订函数。 */
324
+ subscribe(listener: () => void): () => void;
325
+ /**
326
+ * 清空镜像(登出 / 换账号:状态是**按人**的,留着会串号),并**作废在飞的请求**。
327
+ *
328
+ * 只清镜像是不够的:清空那一刻正有一次拉取在飞时,那次拉取是**上一个人**发起的。所以世代
329
+ * +1:那之后的调用者一律不复用旧世代的那条,自己重新发一次 RPC。用世代号而**不是**把
330
+ * `inFlight` 直接抹成 undefined,理由与 `src/client/managed-workbench.ts` 的同名方法逐字相同
331
+ * (旧 promise 的 `finally` 会回头把新发起的那条记账清掉,两者互相踩)。
332
+ *
333
+ * 同时把 `statusProbed` 复位:新账号的镜像必须能再探一次,否则换账号后 `status()` 会一直
334
+ * 返回 `undefined` 且再也不去问主机。
335
+ */
336
+ clear(): void;
337
+ /**
338
+ * 发一次 RPC、解开信封、过第二道形状校验。
339
+ *
340
+ * 返回值里带上**发起时的世代**:只有它等于当前世代时,调用方才写镜像(见 {@link adopt})。
341
+ */
342
+ private call;
343
+ /**
344
+ * 并发合并:同一世代 + 同一端点撞在一起(面板挂载与手动刷新常常同拍)时共用一次请求,
345
+ * 且**共用同一个返回值**——两边对「这一次拉到了什么」的判断必须一致。
346
+ */
347
+ private dedupe;
348
+ /**
349
+ * 采纳一份成功状态:写镜像 + 通知订阅者。
350
+ *
351
+ * 世代不符的那一份**不写镜像**(但仍原样返回给调用方):它是 `clear()` 之前发起的请求,
352
+ * 属于上一个人。允许它写回等于「清空之后又被上一个人的状态占住」,而换账号时界面正好靠
353
+ * `status()` 判断该显示引导还是列表。
354
+ */
355
+ private adopt;
356
+ private emit;
357
+ }
@@ -0,0 +1,64 @@
1
+ import type { GatewayKey } from './ai-router-client.ts';
2
+ /** cordis 服务名(hiwork-memory 等消费方按此名 `ctx.get` 探测)。 */
3
+ export declare const HIWORK_IDENTITY_SERVICE = "hiworkIdentity";
4
+ /** 当前登录身份的稳定视图;未登录是 `{ loggedIn: false }`(其余字段可省略)。 */
5
+ export interface HiworkIdentityView {
6
+ /** 是否已登录(有账号名即视为已登录)。false 时其余字段无意义。 */
7
+ loggedIn: boolean;
8
+ /**
9
+ * 隔离键:`u_` + `sha256(账号名)` 前 16 位 hex。
10
+ * **恒不是账号名原文**;同一账号恒等、不同账号不同。消费方把它当不透明字符串用。
11
+ */
12
+ subject?: string;
13
+ /** 登录账号(网关 LDAP 账号名);供界面显示,**不得**当隔离键用。 */
14
+ username?: string;
15
+ /** 账号显示名(中文名);命名空间里没有时缺省(界面按既有约定回退 username)。 */
16
+ displayName?: string;
17
+ /** 员工姓名(来自 keys 的 `employee_name`);还没有 keys 或缺省时为 undefined。 */
18
+ employeeName?: string;
19
+ /** 团队名(来自 keys 的 `team_name`);还没有 keys 或缺省时为 undefined。 */
20
+ teamName?: string;
21
+ }
22
+ /** 本服务对宿主的可调用面(只读、同步、永不抛异常)。 */
23
+ export interface HiworkIdentityService {
24
+ /** 当前登录身份的稳定视图;未登录返回 { loggedIn: false }。 */
25
+ current(): HiworkIdentityView;
26
+ }
27
+ /** 身份的原始来源:`ai-router` 命名空间的相关字段 + 最近一次拉取到的 keys 列表。 */
28
+ export interface IdentityState {
29
+ username?: string | undefined;
30
+ /** 登录账号的显示名(网关 LDAP displayName);缺省时视图里也不出现。 */
31
+ displayName?: string | undefined;
32
+ /** 最近一次拉取到的当前用户全部虚拟 Key(`employee_name` / `team_name` 的来源)。 */
33
+ keys?: readonly GatewayKey[] | undefined;
34
+ }
35
+ /** Host 侧依赖:当前命名空间快照读取 + 日志出口。 */
36
+ export interface IdentityDeps {
37
+ /**
38
+ * 读一次当前 `ai-router` 命名空间值。**每次 `current()` 都会调用**(本服务不缓存);
39
+ * 传 `() => current()` 即可(`AiRouterNamespaceValue` 结构兼容 {@link IdentityState})。
40
+ */
41
+ current(): IdentityState;
42
+ /** Host 日志出口(只记错误类型,不记账号名之外的任何凭据)。 */
43
+ log?: ((message: string) => void) | undefined;
44
+ }
45
+ /** `ctx.provide` 的最小端口(结构兼容 cordis Context,便于单测伪造)。 */
46
+ export interface IdentityContext {
47
+ provide(name: string, value?: unknown): unknown;
48
+ }
49
+ /** 视图字段名的语义名(供消费方日志 / 文档引用,避免各处硬编码字符串)。 */
50
+ export declare const IDENTITY_SUBJECT_PREFIX = "u_";
51
+ /**
52
+ * 由登录账号派生稳定 subject:`u_` + `sha256(账号名).hex` 的前 16 位。
53
+ *
54
+ * 输入先裁掉两侧空白(与视图里的 `username` 同口径),因此 `' alice '` 与 `'alice'`
55
+ * 派生同一个 subject;大小写不折叠——账号名在网关侧就是区分大小写的。
56
+ * **本函数绝不返回账号名原文**:subject 会落进存储键 / 目录名,哈希是隔离键
57
+ * 既能稳定标识同一员工、又不外泄账号的那条界线。
58
+ */
59
+ export declare function deriveSubject(username: string): string;
60
+ /**
61
+ * 提供 host 侧服务 `hiworkIdentity`;返回 disposer(交给 `ctx.effect` 管理生命周期,
62
+ * 插件卸载时服务随 fiber 一并注销)。
63
+ */
64
+ export declare function provideIdentity(ctx: IdentityContext, deps: IdentityDeps): () => void;
@@ -0,0 +1,113 @@
1
+ /** cordis 服务名(hiwork-memory 的 Host 半边按此名 `ctx.get` 探测)。 */
2
+ export declare const HIWORK_MEMORY_SERVICE = "hiworkMemory";
3
+ /** 网关侧端点(跨仓冻结;与 `/api/me/office-config` 同一条会话鉴权)。 */
4
+ export declare const ME_MEMORY_CONFIG_PATH = "/api/me/memory-config";
5
+ /** 成功结果的缓存 TTL:5 分钟。 */
6
+ export declare const MEMORY_CONFIG_TTL_MS: number;
7
+ /** 出网超时(与 `AiRouterClient` 的缺省一致)。 */
8
+ export declare const MEMORY_CONFIG_TIMEOUT_MS = 10000;
9
+ /**
10
+ * 失败原因(稳定枚举,消费方据此决定提示与自愈策略):
11
+ * - `not-configured`:管理员还没开通团队记忆(网关 503 `MEMORY_NOT_CONFIGURED`);
12
+ * - `unauthenticated`:没有可用会话(未登录 / 会话已失效);
13
+ * - `unreachable`:超时、网络不通、网关地址未配置,或记忆服务自身暂时不可用(504);
14
+ * - `error`:其余非 200、响应不符合契约、宿主内部故障等。
15
+ */
16
+ export type HiworkMemoryFailureReason = 'not-configured' | 'unauthenticated' | 'unreachable' | 'error';
17
+ /**
18
+ * 对外视图(camelCase,插件侧唯一接口面):
19
+ * 成功时 `ok: true` 且带 `panelUrl` / `serviceId` / `teamIds` / `apiKey`;
20
+ * 失败时 `ok: false` 且带 `reason` / `message`,**不带任何半个字段**。
21
+ */
22
+ export interface HiworkMemoryConfigView {
23
+ ok: boolean;
24
+ panelUrl?: string;
25
+ serviceId?: string;
26
+ teamIds?: string[];
27
+ apiKey?: string;
28
+ reason?: HiworkMemoryFailureReason;
29
+ message?: string;
30
+ }
31
+ /** 本服务对宿主的可调用面(只读、永不抛异常)。 */
32
+ export interface HiworkMemoryService {
33
+ /** 取当前登录员工的记忆配置;force=true 绕过缓存(用于「重新获取配置」与 401 自愈)。 */
34
+ config(opts?: {
35
+ force?: boolean;
36
+ }): Promise<HiworkMemoryConfigView>;
37
+ }
38
+ /**
39
+ * 网关侧原始响应(**snake_case**,跨仓冻结契约;本文件是唯一读它的地方)。
40
+ * 仅供文档与类型自证——真正的读取走宽容投影 {@link projectMemoryConfig}。
41
+ */
42
+ export interface GatewayMemoryConfig {
43
+ panel_url: string;
44
+ service_id: string;
45
+ /** 按优先级排好;**空数组 / 缺省 = 只用个人域**,不是错误。 */
46
+ team_ids?: string[];
47
+ api_key: string;
48
+ provisioned_at?: string;
49
+ }
50
+ /**
51
+ * 传输层结果:**失败是数据而不是异常**(本模块不依赖调用方捕获异常来判失败)。
52
+ * - `kind: 'http'`:拿到了非 200 状态码;
53
+ * - `kind: 'network'`:超时 / 连接失败 / DNS / 请求被中断;
54
+ * - `kind: 'invalid'`:200 但响应体不是可用的 JSON 对象(契约破坏)。
55
+ */
56
+ export type MemoryConfigOutcome = {
57
+ readonly ok: true;
58
+ readonly body: unknown;
59
+ } | {
60
+ readonly ok: false;
61
+ readonly kind: 'http';
62
+ readonly status: number;
63
+ } | {
64
+ readonly ok: false;
65
+ readonly kind: 'network';
66
+ } | {
67
+ readonly ok: false;
68
+ readonly kind: 'invalid';
69
+ };
70
+ /** 网关客户端的 memory-config 子集:只声明本模块真正调用的方法(便于单测打桩计数)。 */
71
+ export interface MemoryConfigGatewayPort {
72
+ /** 调 `GET /api/me/memory-config`,返回 snake_case 原始响应或一个稳定失败原因。 */
73
+ getMyMemoryConfig(sessionCookie: string): Promise<MemoryConfigOutcome>;
74
+ }
75
+ /** Host 侧取数依赖:会话解析 + 当前 baseURL 下的网关出网端口。 */
76
+ export interface MemoryConfigDeps {
77
+ /**
78
+ * 解析网关会话 cookie。未登录 / 会话缺失返回 undefined。
79
+ * **返回值只在本模块内部使用**(缓存判等 + 出网),绝不出现在视图与日志里。
80
+ */
81
+ resolveSession(): Promise<string | undefined>;
82
+ /** 当前 baseURL 对应的网关出网端口;baseURL 未配置时返回 undefined。 */
83
+ gateway(): MemoryConfigGatewayPort | undefined;
84
+ /** Host 日志出口(只记事件与异常类型名,绝不记凭据 / URL / 响应内容)。 */
85
+ log?: ((message: string) => void) | undefined;
86
+ /** 时钟注入(测试用);缺省 `Date.now`。 */
87
+ now?: (() => number) | undefined;
88
+ }
89
+ /** `ctx.provide` 的最小端口(结构兼容 cordis Context,便于单测伪造)。 */
90
+ export interface MemoryConfigContext {
91
+ provide(name: string, value?: unknown): unknown;
92
+ }
93
+ /**
94
+ * 提供 host 侧服务 `hiworkMemory`;返回 disposer(交给 `ctx.effect` 管理生命周期,
95
+ * 插件卸载时服务随 fiber 一并注销)。
96
+ */
97
+ export declare function provideMemoryConfig(ctx: MemoryConfigContext, deps: MemoryConfigDeps): () => void;
98
+ /**
99
+ * 本端点的出网实现(形状与 `AiRouterClient` 的 `request()` 同款:user-agent 带包版本、
100
+ * 会话 cookie 走 `cookie` 头、超时用 `AbortSignal.timeout`)。
101
+ *
102
+ * **不新造鉴权**:只复用 Host 侧已有的登录会话 cookie(由 `resolveSession` 从凭据库取),
103
+ * 没有任何密钥/令牌在这一层生成或保存。
104
+ *
105
+ * **永不抛异常、绝不回显响应原文**:失败收敛成 {@link MemoryConfigOutcome},日志与
106
+ * 异常里都不会出现 URL、状态码或响应体——响应体里带的是该员工自己的记忆凭据。
107
+ *
108
+ * `fetchImpl` / `timeoutMs` 仅用于测试注入。
109
+ */
110
+ export declare function createMemoryConfigGateway(baseURL: string, opts?: {
111
+ timeoutMs?: number;
112
+ fetchImpl?: typeof fetch;
113
+ }): MemoryConfigGatewayPort;