hiwork-core 0.0.18 → 0.0.19

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,426 @@
1
+ /**
2
+ * `/hiwork-pms-todo` loopback RPC 的 Host 实现:PMS 个人待办的取数 + 勾完成/取消 + 凭据托管。
3
+ *
4
+ * 契约见 spec `docs/superpowers/specs/2026-09-17-pms-todo-integration-design.md`:
5
+ * - §3 D2 —— 认证 v1 是**凭据托管**:用户在 HiWork 里填一次 PMS 账号密码,Host 用它换 PMS JWT
6
+ * (10 小时),并把 refresh token(7 天)一并存下;凭据获取的全部逻辑收敛在本文件的
7
+ * {@link PmsTodoStore.authorize} 里,v2 换 IAM SSO 时只替换它,UI 与 RPC 契约不动(§3 D3);
8
+ * - §3 D4 / §4.2 —— 待办的三条现有接口(today / statistics / setStatus),未知枚举原样透传;
9
+ * - §3 D7 —— **我的任务**:`todo_task` 之外的未完成项目任务(`GET /api/task/tasks`,`myTasks`)+ 完成任务
10
+ * (`PUT /api/task/tasks/{id}/status`,`completeTask`)。为什么必须有它:PMS 只在**任务创建那一刻**
11
+ * 生成待办,转派 / 走 SQL 建的历史任务没有待办——只读待办会让这些任务在面板上消失;
12
+ * - §3 D6 —— **JWT、账号密码一律只存在于 Host 半边**:客户端只发端点调用,c1 拿不到任何凭据;
13
+ * - §4.1 —— 八个端点(v1 的六个 + §3 D7 的两个)、`{ok,value}` / `{ok,error:{code,message}}` 信封、
14
+ * **永不抛**、六个稳定错误码(多了 `FORBIDDEN`,见 §3 D7 的权限提示要求);
15
+ * - §4.4 —— 凭据 ref `HIWORK_PMS_CREDENTIALS`,**只在 `remember` 为真时持久化**;
16
+ * - §7 —— 凭据失效走 `UNAUTHORIZED` 且**不自动重试**(避免用错密码连续触发 PMS 失败锁定)。
17
+ *
18
+ * 与 `/hiwork-workbench`、`/hiwork-managed-*` 那几条链的关系:信封、端点白名单、错误码收敛、
19
+ * 「永不抛」的形状沿用同一副骨架(`src/workbench-bridge.ts`),但这一条**多了一个别人没有的
20
+ * 东西**——凭据生命周期(存什么、什么时候写盘、什么时候 refresh、什么时候要求人重新输入)。
21
+ * 那也是本文件唯一复杂的部分,其余都是投影与校验。
22
+ *
23
+ * 三条铁律:
24
+ * - **token 只在 Host 内存(+ 可选的加密凭据记录)**:本文件没有任何返回值带 token,日志里也
25
+ * 不许出现 token / 密码 / refresh token(§7、§8 第 8 条由测试把守);
26
+ * - **不自动重试登录**:见 {@link PmsTodoStore.login} 的注释——用错密码连打两次就可能触发 PMS 的
27
+ * 失败锁定,而「自动重试」正是最常见的那种不小心连打;
28
+ * - **失败不清空已有会话**:refresh 失败只把这一次调用判为 `UNAUTHORIZED`,不把凭据记录删掉
29
+ * (用户可能是临时改密 / 网络抽风;删掉记录等于把「重新输入」变成「重新配置」)。
30
+ *
31
+ * 本模块不 import cordis 运行时,只依赖结构化的 {@link PmsTodoRpcDeps} 端口,便于单测直接伪造。
32
+ */
33
+ import { type PmsMyTasksOptions, type PmsTaskList as PmsTaskListView, type PmsTaskStatusResult as PmsTaskStatusResultView, type PmsTodoItem, type PmsTodoStatistics as PmsTodoStatisticsView, type PmsTodoStatusResult as PmsTodoStatusResultView, type PmsTodoStatusValue, type PmsTokenSet } from './pms-client.ts';
34
+ /** RPC 频道;必须与 client 半边 `src/client/pms-todo.ts` 的 `PMS_TODO_CHANNEL` 一致(由测试锁定)。 */
35
+ export declare const PMS_TODO_RPC_CHANNEL = "/hiwork-pms-todo";
36
+ /**
37
+ * 端点白名单(**顺序即字面量**,由测试钉死;v1 的六个 + §3 D7 追加的两个 = 八个)。
38
+ *
39
+ * `status` / `login` / `logout` 是凭据生命周期,`today` / `statistics` / `setStatus` 是待办数据,
40
+ * `myTasks` / `completeTask` 是**项目任务**(§3 D7:待办之外的未完成任务,以及完成它)。
41
+ * 白名单是客户端可用的**能力边界**:v1 不做新建 / 编辑 / 删除待办(§5.3),所以这里也没有
42
+ * `create` / `update` / `remove`——别为了「一次写完」提前登记一个没有实现的承诺。
43
+ *
44
+ * ⚠️ 两个新端点是**追加**在末尾的:client 半边(`src/client/pms-todo.ts`)的常量与这里逐字相等,
45
+ * 老 client 拿一个只有六项的清单去比对新 Host 的八项会不等——那是**有意的**(版本闸口),
46
+ * 消费方按可选成员降级,见 `PmsTodoFace.myTasks` 的注释。
47
+ */
48
+ export declare const PMS_TODO_RPC_ENDPOINTS: readonly ["status", "login", "logout", "today", "statistics", "setStatus", "myTasks", "completeTask"];
49
+ export type PmsTodoRpcEndpoint = (typeof PMS_TODO_RPC_ENDPOINTS)[number];
50
+ /**
51
+ * 凭据 ref(spec §4.4,credentials 服务,加密落盘)。
52
+ *
53
+ * 值是一段 JSON:`{username, password?, refreshToken?, expiresAt?}`。**只有 `remember` 为真时
54
+ * 才写这个 ref**;不记住时凭据只活在进程内存里。别把它挪进 settings —— settings 是明文 YAML,
55
+ * 而且会出现在设置页快照里(§7 第一条)。
56
+ */
57
+ export declare const PMS_CREDENTIALS_REF = "HIWORK_PMS_CREDENTIALS";
58
+ /**
59
+ * 凭据记录里 `expiresAt` 缺失时的兜底有效期:10 小时(与 `src/pms-client.ts` 同值)。
60
+ * 正常路径一律写服务端给的 `expiresIn` 算出来的绝对时刻。
61
+ */
62
+ export declare const PMS_TODO_ACCESS_TOKEN_TTL_MS: number;
63
+ /**
64
+ * Host 侧六个稳定错误码(spec §4.1 的五个 + §3 D7 追加的 `FORBIDDEN`;**只允许**这六个)。
65
+ *
66
+ * `FORBIDDEN` 与 `UNAUTHORIZED` 分开:权限不足(`task:list:edit`)**重新登录也解决不了**,
67
+ * 引导用户改密码只会绕圈;UI 要按 §3 D7 明说「在 HiWork 里没有完成任务的权限,请到 PMS 操作」。
68
+ */
69
+ export type PmsTodoErrorCode = 'NOT_CONFIGURED' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'UPSTREAM_UNAVAILABLE' | 'BAD_REQUEST' | 'INTERNAL';
70
+ /** 从未配置凭据的文案(UI 据此渲染引导卡片,不做静默失败)。 */
71
+ export declare const PMS_TODO_NOT_CONFIGURED_MESSAGE = "\u5C1A\u672A\u914D\u7F6E PMS \u8D26\u53F7\uFF0C\u8BF7\u5148\u8F93\u5165\u8D26\u53F7\u5BC6\u7801";
72
+ /** 凭据失效的文案(UI 据此要求重新输入,且保留上次数据可见)。 */
73
+ export declare const PMS_TODO_UNAUTHORIZED_MESSAGE = "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165 PMS \u8D26\u53F7\u5BC6\u7801";
74
+ /**
75
+ * 权限不足的文案(spec §3 D7 逐字要求;`PmsError` 的 `FORBIDDEN` 原样抬到 RPC 面)。
76
+ * UI 必须明说它,而不是静默失败或回滚得莫名其妙。
77
+ */
78
+ export declare const PMS_TODO_FORBIDDEN_MESSAGE = "\u5728 HiWork \u91CC\u6CA1\u6709\u5B8C\u6210\u4EFB\u52A1\u7684\u6743\u9650\uFF0C\u8BF7\u5230 PMS \u64CD\u4F5C";
79
+ /** 通用失败文案(上游不可用等)。 */
80
+ export declare const PMS_TODO_GENERIC_FAILURE_MESSAGE = "PMS \u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
81
+ /** 请求被取消的文案(客户端 abort;错误码仍走白名单里的 `BAD_REQUEST`)。 */
82
+ export declare const PMS_TODO_CANCELLED_MESSAGE = "PMS \u5F85\u529E\u8BF7\u6C42\u5DF2\u53D6\u6D88\u3002";
83
+ /**
84
+ * `status` / `login` / `logout` 的返回值(spec §4.1)。
85
+ *
86
+ * `configured` = 「有账号」(值在 credentials 里或内存里);`loggedIn` = 「不用再问用户就能继续用」
87
+ * (内存里有未过期的 access token,**或**手里有一个 refresh token)。refresh token 真失效时会在
88
+ * 下一次取数时收敛成 `UNAUTHORIZED`——这里刻意不做提前探测:探测本身就是一次网络请求,而用户
89
+ * 打开面板的动作已经会触发一次真实取数。
90
+ */
91
+ export interface PmsTodoStatus {
92
+ configured: boolean;
93
+ loggedIn: boolean;
94
+ /** 已配置账号时的用户名(给 UI 展示「当前是哪个账号」)。 */
95
+ username?: string;
96
+ /** access token 的绝对过期时刻(ISO 8601)。 */
97
+ expiresAt?: string;
98
+ }
99
+ /** `today` 的返回值(spec §4.1):条目 + 本次成功拉取的时刻。 */
100
+ export interface PmsTodoToday {
101
+ items: PmsTodoItem[];
102
+ /** 本次成功拉取的时刻(ISO 8601);UI 用它显示「更新于 …」。 */
103
+ fetchedAt: string;
104
+ }
105
+ /** `statistics` 的返回值(spec §4.1)。 */
106
+ export type PmsTodoStatistics = PmsTodoStatisticsView;
107
+ /** `setStatus` 的返回值(spec §4.1)。 */
108
+ export type PmsTodoStatusResult = PmsTodoStatusResultView;
109
+ /**
110
+ * `myTasks` 的返回值(§3 D7):我的未完成项目任务的**分页对象**投影。
111
+ * ⚠️ 它不是数组(与 `today` 的 `items` 不同),服务端回的是 `PageResult<TaskVO>`。
112
+ */
113
+ export type PmsTaskList = PmsTaskListView;
114
+ /** `completeTask` 的返回值(§3 D7):服务端回 void,所以按请求值回退。 */
115
+ export type PmsTaskStatusResult = PmsTaskStatusResultView;
116
+ /** `myTasks` 的可选项(页大小;缺省 50,由 `PmsClient` 兜底)。 */
117
+ export type PmsTodoMyTasksOptions = PmsMyTasksOptions;
118
+ /** 客户端可安全展示的失败。 */
119
+ export interface PmsTodoFailure {
120
+ code: PmsTodoErrorCode;
121
+ message: string;
122
+ /** DSH Connection 信封要求 `details` 是对象;协议层没有额外细节时补空对象。 */
123
+ details: object;
124
+ }
125
+ /** 端点结果信封(与 DSH Connection 的 `ConnectionRpcResult` 同形)。 */
126
+ export type PmsTodoRpcResult<T> = {
127
+ readonly ok: true;
128
+ readonly value: T;
129
+ } | {
130
+ readonly ok: false;
131
+ readonly error: PmsTodoFailure;
132
+ };
133
+ /**
134
+ * 请求侧错误:未知端点 / 非法 payload(`BAD_REQUEST`)。
135
+ *
136
+ * 用显式类而不是裸 Error:端点处理函数靠 `instanceof` 决定错误码,`error.name` 兜底(跨 bundle
137
+ * 场景见 `src/index.ts` 里 SettingsConflictError 的同款处理)。
138
+ */
139
+ export declare class PmsTodoRequestError extends Error {
140
+ readonly code: "BAD_REQUEST";
141
+ constructor(message: string);
142
+ }
143
+ /** Host 侧 RPC 依赖的最小上下文(结构兼容 cordis Context,便于单测伪造)。 */
144
+ export interface PmsTodoRpcConnection {
145
+ rpc: {
146
+ handle(channel: string, handler: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<unknown>, options: {
147
+ authority: 'loopback';
148
+ }): () => Promise<void> | void;
149
+ };
150
+ }
151
+ export interface PmsTodoRpcContext {
152
+ readonly connection: PmsTodoRpcConnection;
153
+ readonly logger: {
154
+ warn(message: string): void;
155
+ };
156
+ }
157
+ /** 凭据服务的子集(结构兼容 `ctx.credentials`,与 `CredentialsPort` 同形)。 */
158
+ export interface PmsTodoCredentialsPort {
159
+ resolve(ref: string): Promise<{
160
+ value: string;
161
+ } | undefined>;
162
+ set(ref: string, value: string): Promise<void>;
163
+ unset(ref: string): Promise<void>;
164
+ }
165
+ /**
166
+ * PMS 客户端的子集:只声明本频道真正调用的七个方法(结构兼容 `PmsClient`)。
167
+ * 端口化是单测能覆盖「refresh 失败时怎么做」这类分支的前提(真客户端要起 HTTP 服务)。
168
+ */
169
+ export interface PmsTodoPmsPort {
170
+ login(username: string, password: string): Promise<PmsTokenSet>;
171
+ refresh(refreshToken: string): Promise<PmsTokenSet>;
172
+ today(accessToken: string): Promise<PmsTodoItem[]>;
173
+ statistics(accessToken: string): Promise<PmsTodoStatistics>;
174
+ setStatus(accessToken: string, id: number, status: PmsTodoStatusValue): Promise<PmsTodoStatusResult>;
175
+ /** §3 D7:我的未完成项目任务(`userId` = 凭据记录里的 PMS userId,**不由客户端传**)。 */
176
+ myTasks(accessToken: string, userId: number, opts?: PmsMyTasksOptions): Promise<PmsTaskList>;
177
+ /** §3 D7:完成一条项目任务(需要 `task:list:edit` 权限,否则 `PmsError('FORBIDDEN')`)。 */
178
+ completeTask(accessToken: string, taskId: number): Promise<PmsTaskStatusResult>;
179
+ }
180
+ /** Host 侧依赖:凭据服务 + PMS 客户端 + 日志 + 时钟(全部可伪造)。 */
181
+ export interface PmsTodoRpcDeps {
182
+ /** credentials 服务的三个方法。**读写都经它**,本模块不直接 import DSH 包。 */
183
+ credentials: PmsTodoCredentialsPort;
184
+ /** PMS HTTP 客户端(生产是 `new PmsClient()`)。 */
185
+ pms: PmsTodoPmsPort;
186
+ /** Host 日志出口(只记事件与错误类型,不记账号 / 密码 / token)。 */
187
+ log?: ((message: string) => void) | undefined;
188
+ /** 时钟注入(测试用);缺省 `Date.now`。 */
189
+ now?: (() => number) | undefined;
190
+ }
191
+ /**
192
+ * 凭据记录(spec §4.4 的冻结值 + §3 D7 追加的 `userId`)。
193
+ *
194
+ * 落盘时序列化成 JSON;`password` 是**唯一**一个「不 remember 就不该出现在磁盘上」的字段
195
+ * (见 {@link PmsTodoStore.persist})。`expiresAt` 是绝对时刻(epoch ms,不是给界面看的 ISO 串)
196
+ * ——凭据记录是机器格式,少一层「解析出来的时刻对不对」的疑问。
197
+ *
198
+ * `userId`(§3 D7):登录响应里就有,是「我的任务」唯一的查询键(`assigneeId`)。它**不是凭据**
199
+ * (PMS 内部的个人数字 id),所以:写盘时跟别的字段一样存;老记录没有它时 `myTasks` 收敛成
200
+ * `UNAUTHORIZED`——用户重新登录一次即可拿到,不猜也不按用户名反查(那要额外的接口与权限)。
201
+ */
202
+ export interface PmsCredentialsRecord {
203
+ username: string;
204
+ password?: string;
205
+ refreshToken?: string;
206
+ expiresAt?: number;
207
+ userId?: number;
208
+ }
209
+ /** 端点入参的解析结果(按端点分派的联合类型)。 */
210
+ export type PmsTodoPayload = {
211
+ readonly endpoint: 'status' | 'logout' | 'today' | 'statistics' | 'myTasks';
212
+ } | {
213
+ readonly endpoint: 'login';
214
+ readonly username: string;
215
+ readonly password: string;
216
+ readonly remember: boolean;
217
+ } | {
218
+ readonly endpoint: 'setStatus';
219
+ readonly id: number;
220
+ readonly status: PmsTodoStatusValue;
221
+ } | {
222
+ readonly endpoint: 'completeTask';
223
+ readonly id: number;
224
+ };
225
+ /** 是否为白名单端点。 */
226
+ export declare function isPmsTodoRpcEndpoint(endpoint: string): endpoint is PmsTodoRpcEndpoint;
227
+ /**
228
+ * 校验一次端点入参(**手写**,风格照 `parseWorkbenchPayload`:白名单字段 + 类型 + 非空)。
229
+ *
230
+ * `setStatus` 的 `status` 在这里就收敛成两个字面量:它会被拼进 URL 的 query,越早收敛越安全;
231
+ * 而 `PENDING` / `COMPLETED` 是 PMS 侧唯一接受的两个值(其余的传过去只会得到一次业务失败)。
232
+ * `completeTask` 的 `id` 同理(会被拼进**路径**,更要先收敛成正整数)。
233
+ *
234
+ * `myTasks` **不接受任何入参**(尤其不接受 `userId`):查询的「谁」必须由 Host 从凭据记录里取,
235
+ * 否则客户端就能拿任意的 assigneeId 去查别人的任务——那正是 `PMS_TODO_RPC_ENDPOINTS` 白名单
236
+ * 想守住的能力边界(§3 D6 / §3 D7)。
237
+ */
238
+ export declare function parsePmsTodoPayload(endpoint: PmsTodoRpcEndpoint, raw: unknown): PmsTodoPayload;
239
+ /** 任意异常 → 可展示的失败(稳定错误码 + 消毒过的 message)。 */
240
+ export declare function pmsTodoErrorOf(error: unknown): PmsTodoFailure;
241
+ /**
242
+ * 上游 message 消毒(与 `workbench-bridge.ts` 的 `displayWorkbenchMessage` 同实现):
243
+ * 含 URL / HTTP 状态码 / baseURL 的原始消息一律换成稳定文案。
244
+ * 本模块的上游 message 本来就都是常量,这一层是**兜底**:将来有人往 `PmsError` 里塞一句
245
+ * 带细节的话,也不至于直接漏到用户面前。
246
+ */
247
+ export declare function displayPmsTodoMessage(code: string, message: unknown): string;
248
+ /**
249
+ * 本频道内部的凭据/会话错误(`NOT_CONFIGURED` / `UNAUTHORIZED`)。
250
+ *
251
+ * 用一个枚举样式的小类比裸字符串好:`PmsError` 只覆盖「上游说了什么」,而「本地判断出来
252
+ * 没有凭据 / 没有 refresh token」是 Host 自己的结论,两者混在一个类型里会让 `pmsTodoErrorOf`
253
+ * 不得不用字符串猜。
254
+ */
255
+ export declare class PmsTodoError extends Error {
256
+ readonly code: 'NOT_CONFIGURED' | 'UNAUTHORIZED' | 'INTERNAL';
257
+ constructor(code: 'NOT_CONFIGURED' | 'UNAUTHORIZED' | 'INTERNAL', message: string);
258
+ }
259
+ /**
260
+ * 解析凭据记录。
261
+ *
262
+ * 记录是我们自己写的,但磁盘上的字节仍可能来自**旧版本的自己**,所以逐字段校验、
263
+ * **未知字段忽略**(演进纪律),坏字段丢掉而不是让整条记录作废——一次 `format` 变更不该把用户
264
+ * 变成「从未配置」。
265
+ */
266
+ export declare function parseCredentialsRecord(raw: string): PmsCredentialsRecord | undefined;
267
+ /** 序列化凭据记录(**唯一**写盘路径)。缺字段不写键,避免把 `undefined` 变成 JSON 里的 `null`。 */
268
+ export declare function serializeCredentialsRecord(record: PmsCredentialsRecord): string;
269
+ /**
270
+ * PMS 待办的状态仓库:持有凭据记录与 access token,按需续期,并暴露五个取数 / 命令动作
271
+ * (today / statistics / setStatus + §3 D7 的 myTasks / completeTask)。
272
+ *
273
+ * 一个刻意的分工:**凭据(账号密码 / refresh token)与 access token 只在本类里出现**。
274
+ * 端点处理函数({@link handlePmsTodoEndpoint})只把返回值转发出去,因此「响应里没有 token」
275
+ * 这件事是**结构上成立**的,而不是靠每一处返回值自觉。
276
+ */
277
+ export declare class PmsTodoStore {
278
+ private readonly deps;
279
+ /** 内存里的凭据记录(不 remember 时它是这份凭据唯一的家)。 */
280
+ private record;
281
+ /** 是否已经从 credentials 服务读过一次(避免每次调用都读盘)。 */
282
+ private loaded;
283
+ /**
284
+ * 当前会话是否**属于磁盘上的那条记录**(remember=true 的登录、或启动时从记录里恢复)。
285
+ * 只有它为真时,refresh 换来的新 token 才回写记录——否则一次「不记住」的登录会顺手把
286
+ * refresh token 写给下一个人(登出后磁盘上仍有可用凭据,这正是不 remember 的相反面)。
287
+ */
288
+ private persisted;
289
+ /** access token(**只在内存**)。 */
290
+ private access;
291
+ /** 世代号:`login` / `logout` 各 +1,用来作废它之前发起的在飞请求(换账号的语义同工作台)。 */
292
+ private generation;
293
+ /** 在飞请求**连同世代与端点**记账:同一世代 + 同一端点才允许合并。 */
294
+ private inFlight;
295
+ constructor(deps: PmsTodoRpcDeps);
296
+ private now;
297
+ /** 当前状态(同步语义的取数,异步实现:可能读一次 credentials)。**永不抛**。 */
298
+ status(): Promise<PmsTodoStatus>;
299
+ /**
300
+ * 用账号密码登录并**换取** JWT。
301
+ *
302
+ * ⚠️ **绝不自动重试**(spec §7 的硬要求):PMS 对连续失败登录有锁定策略,而「自动重试」正是
303
+ * 最容易不小心连打两次的地方——用户手一抖打错一位密码,第二次尝试就会把账号推向锁定。
304
+ * 所以本方法**只发一次**请求,失败即返回错误(UI 引导用户自己再输一次)。
305
+ *
306
+ * `remember` 为真才落盘(§4.4)。落盘写的是**完整**记录(含密码),因为下次启动时要靠它
307
+ * 继续换 token——这不是「记住密码」的 UI 便利,而是 v1 认证方案本身(§3 D2)。
308
+ */
309
+ login(input: {
310
+ username: string;
311
+ password: string;
312
+ remember: boolean;
313
+ }): Promise<PmsTodoStatus>;
314
+ /**
315
+ * 登出:清内存 + 清凭据记录(spec §4.1 的 `logout`)。
316
+ *
317
+ * 不清 PMS 侧的服务端会话:PMS 的 JWT 是无状态签发的(没有「撤销单个 token」的接口),
318
+ * 我们能做且该做的是**不再持有**它——删掉本地凭据后,下一次取数一定要求重新输入。
319
+ */
320
+ logout(): Promise<PmsTodoStatus>;
321
+ /** `GET /api/todo/today` + 本次拉取时刻。同一时刻的并发调用合并成一次 PMS 请求。 */
322
+ today(): Promise<PmsTodoToday>;
323
+ /** `GET /api/todo/statistics`。并发调用同样合并。 */
324
+ statistics(): Promise<PmsTodoStatistics>;
325
+ /**
326
+ * `PUT /api/todo/{id}/status`。
327
+ *
328
+ * **不合并**并发调用:这是命令(用户点了两次就是两次动作),合并会让第二次点击被悄悄吞掉。
329
+ * 顺序也不由本层控制(谁先到谁先发),UI 侧要的是「失败就回滚那一条」,不是「服务端按我提交
330
+ * 的顺序处理」——那个语义 PMS 也不提供。
331
+ */
332
+ setStatus(input: {
333
+ id: number;
334
+ status: PmsTodoStatusValue;
335
+ }): Promise<PmsTodoStatusResult>;
336
+ /**
337
+ * `GET /api/task/tasks`:**我的未完成项目任务**(spec §3 D7)。
338
+ *
339
+ * `userId` 只从凭据记录里取(客户端传不进来,见 `parsePmsTodoPayload` 的注释)——这是
340
+ * 「只能查自己」唯一可靠的落点:端点白名单里也没有任何「按 id 查别人」的形状。
341
+ *
342
+ * 记录里**没有** `userId`(2026-09-18 之前登录的旧记录)时收敛成 `UNAUTHORIZED`:用户重新登录
343
+ * 一次即可拿到它。这里刻意**不**回退成「按用户名去查用户列表」之类的动作——那需要额外的接口与
344
+ * 权限,而且会在 Host 里凭空多一条拿别人数据的路径。
345
+ *
346
+ * 并发调用合并(同 `today`):这是**取数**,同一时刻问两遍没有意义。
347
+ */
348
+ myTasks(opts?: PmsMyTasksOptions): Promise<PmsTaskList>;
349
+ /**
350
+ * `PUT /api/task/tasks/{id}/status?status=2`:完成一条项目任务。
351
+ *
352
+ * **不合并**并发调用(与 {@link PmsTodoStore.setStatus} 同理):这是命令,用户点了两次就是两次
353
+ * 动作,合并会让第二次点击被悄悄吞掉。
354
+ *
355
+ * ⚠️ 权限:没有 `task:list:edit` 的角色会拿到 `FORBIDDEN`(`PmsError` → RPC 面的同名码),
356
+ * UI 按 §3 D7 提示「请到 PMS 操作」——**不是**重新登录。
357
+ */
358
+ completeTask(input: {
359
+ id: number;
360
+ }): Promise<PmsTaskStatusResult>;
361
+ /**
362
+ * 取一个能用的 access token(凭证获取的唯一入口,spec §3 D3 说 v2 只替换这里)。
363
+ *
364
+ * 顺序是刻意的:
365
+ * 1. 内存里有未过期的 access token → 直接用(零网络请求);
366
+ * 2. 过期了 → 先试 `refresh(refreshToken)`(§D2:refresh 有效就完全不必打扰用户);
367
+ * refresh 成功会**顺手**把新的 access / refresh token 写回内存(原来是持久化的会话则一并落盘);
368
+ * 3. refresh 失败(401 / 记录里根本没有 refresh token)→ `UNAUTHORIZED`,UI 要求重新输入。
369
+ *
370
+ * ⚠️ 这里**没有**「拿存下来的密码重新登录」这一支,而且是刻意的:§7 要求凭据失效时
371
+ * **不自动重试**——用存下来的密码自动重登 = 每次 token 过期都自动替用户登一次,密码一旦改成
372
+ * 别的,就会变成一条稳定的「定时用错密码打 PMS」的流水,直到账号被锁。密码只在用户**明确**
373
+ * 提交一次 `login` 时才用(那时它也确实是用户刚输入的)。
374
+ */
375
+ private authorize;
376
+ /**
377
+ * 状态投影(不读盘、不发网络)。
378
+ *
379
+ * `expiresAt` **只报还没过期的那个时刻**:记录里那份可能是上一次会话留下的(进程重启后内存里
380
+ * 没有 access token,只有磁盘上的旧 `expiresAt`),把它照实报出去,界面就会显示一个已经过去的
381
+ * 「有效期至」,看着像还能用。过期了就不写这个键——refresh 会顺手把它换成新的。
382
+ */
383
+ private statusOf;
384
+ /**
385
+ * 当前凭据记录:内存优先,首次调用时从 credentials 服务读一次。
386
+ *
387
+ * 「读一次就记住 `loaded`」是有意的:凭据的写入方只有本类(`login` / `logout` / refresh 回写),
388
+ * 因此在同一进程里,读盘只需要一次。跨进程变更(别的进程写了同一个 ref)本就不可观测,
389
+ * 硬要每次重新读只会让「不 remember 的登录」被磁盘上的旧记录当场覆盖。
390
+ */
391
+ private currentRecord;
392
+ /**
393
+ * 落盘(**唯一**写 credentials 的地方,只在 remember 为真时被调用)。
394
+ *
395
+ * 失败**不**把一次成功的登录判为失败:会话已经拿到了,内存里照常可用;密码存不下的后果只是
396
+ * 「重启后要重新输入」。反过来把登录判为失败会让用户以为密码错了。
397
+ */
398
+ private persist;
399
+ /**
400
+ * 抹掉磁盘上的记录(logout / 不记住的登录)。
401
+ *
402
+ * 先 `resolve` 一次再 `unset`:没有记录时一次写都不做(测试与磁盘都清清爽爽),有记录时
403
+ * **必须**清掉——否则一条旧的 account+refresh token 会在下次启动时被当成「这个人配置过凭据」,
404
+ * 用户明明刚选了「不记住」却在重启后仍是登录态。
405
+ */
406
+ private forgetStored;
407
+ /**
408
+ * 并发合并:同一世代 + 同一端点的在飞请求共用一次调用与同一个返回值。
409
+ *
410
+ * 世代判据与 `src/client/managed-workbench.ts` 的 `dedupe` 同形:`login` / `logout` 之后
411
+ * 账上那条属于**上一个人**,谁都不许复用(复用等于把上一个人的待办显示给新账号)。
412
+ */
413
+ private dedupe;
414
+ }
415
+ /**
416
+ * 处理一次端点调用:返回 DSH Connection 的结果信封,**永不抛异常**。
417
+ * 导出以便单测直接驱动(无需伪造 `ctx.connection`)。
418
+ *
419
+ * 日志纪律:只记**端点名 + 异常类型**。入参里有 `password`(`login`),返回值里有 access token 的
420
+ * 派生状态,两者都不许进日志(§7)。
421
+ */
422
+ export declare function handlePmsTodoEndpoint(endpoint: string, rawPayload: unknown, deps: PmsTodoRpcDeps, store: PmsTodoStore, signal?: AbortSignal): Promise<PmsTodoRpcResult<unknown>>;
423
+ /**
424
+ * 注册 `/hiwork-pms-todo` 频道,返回异步 disposer(交给 `ctx.effect` 管理生命周期)。
425
+ */
426
+ export declare function registerPmsTodoRpc(ctx: PmsTodoRpcContext, deps: PmsTodoRpcDeps, store: PmsTodoStore): () => Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hiwork-core",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "description": "HiWork 企业统一登录与模型自动供给插件(ai-router 网关登录,Web 表单,无 CLI 认证)",
6
6
  "license": "UNLICENSED",