dsh-later 1.0.0 → 1.2.0
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.
- package/CHANGELOG.md +247 -0
- package/README.en.md +55 -2
- package/README.md +53 -2
- package/icon.svg +10 -0
- package/lib/bar-geometry.js +139 -0
- package/lib/client.js +1349 -423
- package/lib/command-outcome.js +15 -0
- package/lib/commands.js +56 -8
- package/lib/delivery.js +66 -0
- package/lib/domain.js +6 -0
- package/lib/index.js +152 -29
- package/lib/ownership-store.js +113 -15
- package/lib/presence.js +122 -4
- package/lib/projection.js +149 -20
- package/lib/runtime.js +160 -24
- package/lib/schedule-compat.js +179 -0
- package/lib/schedule-summary-store.js +160 -0
- package/lib/schedule-summary.js +113 -0
- package/lib/types/bar-geometry.d.ts +111 -0
- package/lib/types/command-outcome.d.ts +19 -0
- package/lib/types/delivery.d.ts +51 -0
- package/lib/types/domain.d.ts +32 -0
- package/lib/types/index.d.ts +28 -8
- package/lib/types/ownership-store.d.ts +42 -2
- package/lib/types/presence.d.ts +75 -4
- package/lib/types/projection.d.ts +68 -6
- package/lib/types/runtime.d.ts +58 -1
- package/lib/types/schedule-compat.d.ts +111 -0
- package/lib/types/schedule-summary-store.d.ts +75 -0
- package/lib/types/schedule-summary.d.ts +92 -0
- package/lib/types/upstream-compat.d.ts +123 -0
- package/lib/types/user-tools.d.ts +48 -7
- package/lib/upstream-compat.js +143 -0
- package/lib/user-tools.js +268 -44
- package/package.json +25 -22
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 定时任务摘要:dsh-later 对外暴露的**公共数据面**(跨会话 / 跨插件)。
|
|
3
|
+
*
|
|
4
|
+
* 关键事实(本插件「路线 B」的数据源):`userSchedules` 是 client-visible 投影,
|
|
5
|
+
* 宿主在 `session.list` 基线里为**每个**会话携带 projections 块(live 会话走注册
|
|
6
|
+
* 表现值、冷会话走持久化投影缓存),客户端把它合入
|
|
7
|
+
* `SessionSummary.projectionValues`。因此**任何**持有 `sessions` 服务的客户端插件
|
|
8
|
+
* 都能读到全会话的定时任务,本模块只负责把它折叠成稳定形状。
|
|
9
|
+
*
|
|
10
|
+
* 契约形状(issue #17):`Map<SessionId, { count, nextAt, state }>`——即
|
|
11
|
+
* {@link ScheduleSummaryMap}。dsh-later 通过 `dsh-later/client` 导出
|
|
12
|
+
* {@link buildScheduleSummary}(纯函数)、`createScheduleSummaryStore`(可订阅
|
|
13
|
+
* store)与 `useScheduleSummary`(React hook),消费方按需选用。
|
|
14
|
+
*
|
|
15
|
+
* 本模块零 DOM / 零 React 依赖:宿主、浏览器与 `node --test` 都能直接跑。
|
|
16
|
+
* @module dsh-later/schedule-summary
|
|
17
|
+
*/
|
|
18
|
+
import { badgeStateFor, summarizeSchedules, } from './presence.js';
|
|
19
|
+
/** `userSchedules` 投影值在 `projectionValues` 里的键名。 */
|
|
20
|
+
export const SCHEDULE_PROJECTION_KEY = 'userSchedules';
|
|
21
|
+
/**
|
|
22
|
+
* 从 `projectionValues` 里取出任务数组(容错:形状不对一律当作无任务)。
|
|
23
|
+
* @param projectionValues - 会话行的投影现值集合。
|
|
24
|
+
* @returns 任务数组;缺失或形状非法时 undefined。
|
|
25
|
+
*/
|
|
26
|
+
export function schedulesOfProjection(projectionValues) {
|
|
27
|
+
const value = projectionValues?.[SCHEDULE_PROJECTION_KEY];
|
|
28
|
+
if (typeof value !== 'object' || value === null)
|
|
29
|
+
return undefined;
|
|
30
|
+
const schedules = value.schedules;
|
|
31
|
+
return Array.isArray(schedules) ? schedules : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 全会话归纳:把会话列表快照折叠成 {@link ScheduleSummaryMap}。
|
|
35
|
+
* @param source - 会话列表快照(`sessions.list.getSnapshot()`)。
|
|
36
|
+
* @param now - 判定 `state` 的当前时刻(epoch ms)。
|
|
37
|
+
* @returns 会话 id → 摘要;无任务的会话不出现在 map 里。
|
|
38
|
+
*/
|
|
39
|
+
export function buildScheduleSummary(source, now) {
|
|
40
|
+
const summary = new Map();
|
|
41
|
+
for (const [sessionId, row] of Object.entries(source.byId)) {
|
|
42
|
+
if (row === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
const entry = summarizeSchedules(schedulesOfProjection(row.projectionValues));
|
|
45
|
+
if (entry.count === 0)
|
|
46
|
+
continue;
|
|
47
|
+
summary.set(sessionId, { ...entry, state: badgeStateFor(entry.nextAt, now) });
|
|
48
|
+
}
|
|
49
|
+
return summary;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 两份摘要是否等价(逐条比对五个字段)。store 用它决定是否需要通知订阅者,
|
|
53
|
+
* 避免每次重算都产生新的 Map 引用导致 React 无谓重渲染。
|
|
54
|
+
*
|
|
55
|
+
* `progressFrom` 一并比对:它虽然通常随 `nextAt` 同进同退,但暂停→恢复会让
|
|
56
|
+
* 同一条任务重新拥有窗口起点,此时 `nextAt` 可能未变而窗口已换 —— 漏比会
|
|
57
|
+
* 让进度环停在上一个窗口的比例上。
|
|
58
|
+
* @param a - 旧摘要。
|
|
59
|
+
* @param b - 新摘要。
|
|
60
|
+
* @returns 键集合与每条内容都相同时为 true。
|
|
61
|
+
*/
|
|
62
|
+
export function sameScheduleSummary(a, b) {
|
|
63
|
+
if (a === b)
|
|
64
|
+
return true;
|
|
65
|
+
if (a.size !== b.size)
|
|
66
|
+
return false;
|
|
67
|
+
for (const [sessionId, left] of a) {
|
|
68
|
+
const right = b.get(sessionId);
|
|
69
|
+
if (right === undefined)
|
|
70
|
+
return false;
|
|
71
|
+
if (left.count !== right.count ||
|
|
72
|
+
left.pausedCount !== right.pausedCount ||
|
|
73
|
+
left.nextAt !== right.nextAt ||
|
|
74
|
+
left.progressFrom !== right.progressFrom ||
|
|
75
|
+
left.state !== right.state) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
/** 状态优先级(越大越紧急)。 */
|
|
82
|
+
const STATE_RANK = { scheduled: 0, urgent: 1, overdue: 2 };
|
|
83
|
+
/**
|
|
84
|
+
* 按**分组**聚合摘要:给定组内会话 id,折叠出可直接渲染的分组事实。
|
|
85
|
+
*
|
|
86
|
+
* 消费方(如 dsh-group-status 的 `GroupLiveStatus`)只需把组内会话 id 传进来,
|
|
87
|
+
* 无需理解 `nextAt`/暂停语义。
|
|
88
|
+
* @param summary - {@link ScheduleSummaryMap}。
|
|
89
|
+
* @param sessionIds - 组内会话 id(顺序无关;不在摘要里的会被忽略)。
|
|
90
|
+
* @returns 分组级摘要;组内无任务时 `sessions`/`count` 为 0、`state` 缺省。
|
|
91
|
+
*/
|
|
92
|
+
export function summarizeScheduleGroup(summary, sessionIds) {
|
|
93
|
+
let sessions = 0;
|
|
94
|
+
let count = 0;
|
|
95
|
+
let pausedCount = 0;
|
|
96
|
+
let nextAt;
|
|
97
|
+
let state;
|
|
98
|
+
for (const sessionId of sessionIds) {
|
|
99
|
+
const entry = summary.get(sessionId);
|
|
100
|
+
if (entry === undefined)
|
|
101
|
+
continue;
|
|
102
|
+
sessions += 1;
|
|
103
|
+
count += entry.count;
|
|
104
|
+
pausedCount += entry.pausedCount;
|
|
105
|
+
if (entry.nextAt !== undefined && (nextAt === undefined || entry.nextAt < nextAt))
|
|
106
|
+
nextAt = entry.nextAt;
|
|
107
|
+
if (state === undefined || STATE_RANK[entry.state] > STATE_RANK[state])
|
|
108
|
+
state = entry.state;
|
|
109
|
+
}
|
|
110
|
+
return nextAt === undefined
|
|
111
|
+
? { sessions, count, pausedCount, state }
|
|
112
|
+
: { sessions, count, pausedCount, nextAt, state };
|
|
113
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进度条几何:活动期与暂停冻结期**共用同一套时间基准**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要单独成模块并写清楚:进度条在「活动 ↔ 暂停 ↔ 恢复」之间切换时,
|
|
5
|
+
* 稍有不一致就会让用户看到条长跳变。历史上有两类真实缺陷:
|
|
6
|
+
*
|
|
7
|
+
* 1. 两套基准:冻结比曾由 `remaining_seconds`(整数秒、ceil)反推,与活动期
|
|
8
|
+
* 精确 ms 的 `now` 恒差一个取整 → 非整秒暂停必然变长。
|
|
9
|
+
* 2. 两个时刻:`paused_at` 是**宿主**处理暂停命令的时刻,比用户点下暂停时最后
|
|
10
|
+
* 渲染的那一帧晚(命令通道往返,实测约 0.3–0.5s)。直接用它做冻结帧,条长
|
|
11
|
+
* 会比前一帧更长 → 「点暂停,条忽然变长」。
|
|
12
|
+
*
|
|
13
|
+
* 现在统一为:`barWindow()` 求窗口,冻结帧只把 `now` 换成冻结时刻,并把冻结
|
|
14
|
+
* 时刻**钳到 ≤ 客户端已渲染时刻**。因此暂停瞬间条长与点击前严格一致。
|
|
15
|
+
*
|
|
16
|
+
* 本模块被 host 与 client 两侧共用(与 command-outcome / smart-window /
|
|
17
|
+
* time-utils 同款:同时列出在 tsconfig.json 与 tsconfig.client.json 的 include 中)。
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-later/bar-geometry
|
|
20
|
+
*/
|
|
21
|
+
/** 每根进度条代表的时长(1 小时 = 一根满管)。 */
|
|
22
|
+
export declare const BAR_UNIT_MS = 3600000;
|
|
23
|
+
/** 参与几何计算的 schedule 最小视图(与 client `ClientSchedule` 结构兼容)。 */
|
|
24
|
+
export interface BarScheduleView {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly kind: 'after' | 'at' | 'every';
|
|
27
|
+
readonly scheduled_at: string;
|
|
28
|
+
readonly after_seconds?: number;
|
|
29
|
+
readonly every_seconds?: number;
|
|
30
|
+
readonly created_at?: string;
|
|
31
|
+
/** 进度条总窗口(秒);resume 后 after_seconds 只是剩余,此字段保留原间隔。 */
|
|
32
|
+
readonly window_seconds?: number;
|
|
33
|
+
readonly paused_at?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 推导进度条起点与总窗口(暂停/活动期共用同一套时间基准)。
|
|
37
|
+
* - `after`:`window_seconds`(resume 后保留的原间隔)→ `after_seconds` → `created_at` → 首见时刻
|
|
38
|
+
* - `every`:`every_seconds` 反推
|
|
39
|
+
* - 其余:`created_at` → 首见时刻
|
|
40
|
+
*/
|
|
41
|
+
export declare function barWindow(item: BarScheduleView, end: number, now: number, firstSeen: ReadonlyMap<string, number>): {
|
|
42
|
+
readonly start: number;
|
|
43
|
+
readonly totalMs: number;
|
|
44
|
+
};
|
|
45
|
+
/** 活动期进度比:elapsed / total。 */
|
|
46
|
+
export declare function barSegments(item: BarScheduleView, now: number, firstSeen: ReadonlyMap<string, number>): {
|
|
47
|
+
readonly totalRatio: number;
|
|
48
|
+
readonly count: number;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* 暂停冻结进度比。
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ 关键:**不能用 `scheduled_at` 反推**。暂停后 wire 的 `scheduled_at` 会从
|
|
54
|
+
* 「resume 后的新目标」换成 `originalScheduledAt`(原目标),而 `window_seconds`
|
|
55
|
+
* 始终是原窗口。若沿用活动期那套 `end - window` 的算法,暂停瞬间窗口基准整体
|
|
56
|
+
* 跳变,条长会剧烈塌缩(实测 25.7% → 3.3%)。
|
|
57
|
+
*
|
|
58
|
+
* 因此冻结帧改成**自洽的纯算术**,不依赖任何可能变义的字段:
|
|
59
|
+
* ratio = (window - remaining) / window
|
|
60
|
+
* 其中 window 取 `window_seconds ?? after_seconds`,remaining 取
|
|
61
|
+
* `max(0, frozenLeftMs)`(即 `remaining_seconds * 1000`)。
|
|
62
|
+
*
|
|
63
|
+
* 这样冻结比例天然等于「暂停那一刻已经过掉的部分」,与暂停前后用的是同一个
|
|
64
|
+
* 窗口;`paused_at` 只用于可选的上界钳制(见 `renderedAt`)。
|
|
65
|
+
*
|
|
66
|
+
* @param frozenLeftMs - 冻结剩余毫秒(`remaining_seconds * 1000`)。
|
|
67
|
+
* @param renderedAt - 可选:客户端已渲染过的最新时刻上界;提供且早于暂停时刻时,
|
|
68
|
+
* 按该上界折算,保证冻结帧不超过用户已看到的进度。
|
|
69
|
+
*/
|
|
70
|
+
export declare function frozenBarSegments(item: BarScheduleView, frozenLeftMs: number, firstSeen: ReadonlyMap<string, number>, renderedAt?: number): {
|
|
71
|
+
readonly totalRatio: number;
|
|
72
|
+
readonly count: number;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* 活动期「CSS 动画」计划:把进度条的推进交给 CSS,而不是每秒一次 JS 重渲染。
|
|
76
|
+
*
|
|
77
|
+
* 为什么需要:旧实现由 `useNow(1000)` 每秒更新一次 `now`,宽度随之每秒跳一次,
|
|
78
|
+
* 再用 `transition: width 1s linear` 补间。1 分钟任务的轨道上每秒要跳约 8px,
|
|
79
|
+
* 形成肉眼可见的锯齿/顿挫(长条上尤其明显)。
|
|
80
|
+
*
|
|
81
|
+
* ⚠️ 关键约束一:参数必须**只依赖任务自身的时间轴**(`start` / `end`),
|
|
82
|
+
* 不能依赖「当前渲染时刻」。否则每次 tick 重渲染都会改变参数 → 浏览器重启动画
|
|
83
|
+
* → 每秒一次固定跳变(实测 deltas 呈 `1.7×5, 10.2` 周期尖峰)。
|
|
84
|
+
*
|
|
85
|
+
* ⚠️ 关键约束二:**CSS 动画在元素被重建时会从头播放**。切到别的会话再切回来,
|
|
86
|
+
* dock 会卸载并重新挂载,动画 `currentTime` 归零——若只靠"动画自己走",进度条
|
|
87
|
+
* 会退回 0% 附近(实测:真实已过 184s/600s 应为 30.7%,切回后却显示 2.55%,
|
|
88
|
+
* 恰好等于「重挂载后流逝的 15.3s / 600s」)。因此必须用**负 `animation-delay`**
|
|
89
|
+
* 在挂载瞬间就把播放头**定位到正确的绝对相位**:
|
|
90
|
+
*
|
|
91
|
+
* delay = -(已过去时长) → 一上屏就处在 elapsed/total 处
|
|
92
|
+
*
|
|
93
|
+
* 这样无论挂载多少次、间隔多久,画面都落在任务真实时间轴的正确位置。
|
|
94
|
+
*
|
|
95
|
+
* 由于负 delay 只由 `start`/`end`/`now` 决定,而这三者都是绝对时间,
|
|
96
|
+
* 「切走再切回」与「一直看着」得到的相位完全一致。
|
|
97
|
+
*/
|
|
98
|
+
export interface BarAnimationPlan {
|
|
99
|
+
/** 动画总时长(ms)= 完整窗口,保证播放头按绝对进度线性推进。 */
|
|
100
|
+
readonly durationMs: number;
|
|
101
|
+
/** 负延迟(ms):把播放头定位到「已过去」处,抵消重挂载导致的归零。 */
|
|
102
|
+
readonly delayMs: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 依据窗口与当前时刻推导动画计划。
|
|
106
|
+
*
|
|
107
|
+
* @param totalMs - 总窗口毫秒(> 0)。
|
|
108
|
+
* @param elapsedMs - 已过去毫秒(钳到 [0, totalMs])。
|
|
109
|
+
* @returns 计划;`totalMs <= 0`、非有限值、或已到点(剩余为 0)时返回 `undefined`。
|
|
110
|
+
*/
|
|
111
|
+
export declare function planBarAnimation(totalMs: number, elapsedMs: number): BarAnimationPlan | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slash 命令的执行结果:**只表达「是否被受理」**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独成模块并写清楚:`dsh-commands` 的 admission 语义只回
|
|
5
|
+
* `RemoteResult<{matched}>`,handler 的业务结果不从这里返回(它落在会话记录 /
|
|
6
|
+
* 投影里)。因此客户端**不得**用本类型判断「业务是否真的发生了」——例如删除
|
|
7
|
+
* 暂停项时 host 可能返回 `deleted:false`,而这里照样是 `matched: true`。
|
|
8
|
+
* 需要业务事实时以 `userSchedules` 投影为准(见 ScheduleDock 的观察窗对账)。
|
|
9
|
+
*
|
|
10
|
+
* 本模块被 host 与 client 两侧共用(与 smart-window / time-utils 同款:
|
|
11
|
+
* 同时列出在 tsconfig.json 与 tsconfig.client.json 的 include 中)。
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-later/command-outcome
|
|
14
|
+
*/
|
|
15
|
+
/** 一条命令的执行结果。 */
|
|
16
|
+
export interface CommandOutcome {
|
|
17
|
+
/** 命令是否被受理(matched)。false 表示未受理(无会话面 / 网络异常 / 无此命令)。 */
|
|
18
|
+
readonly matched: boolean;
|
|
19
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插话投递的「投递 + 送达确认」原语。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要:`agent.steer()` 把消息放进 `next-step`,语义是「下一个 step 边界
|
|
5
|
+
* 消费」。若插话恰好撞上 `turn/start`,核心的 `inbox.claim('next-step', …)` 会把
|
|
6
|
+
* **整批** `next-step` 取走交给一个**请求上下文已定**的 step —— 消息落进 surface
|
|
7
|
+
* (日志里确有一条 `user/message`)却不进实际请求,即**静默丢失**。实测时序:
|
|
8
|
+
*
|
|
9
|
+
* t+0ms agent/inbox/spliced → 插入 next-step
|
|
10
|
+
* t+1ms turn/start → turn 17 开始(同一毫秒)
|
|
11
|
+
* t+1ms agent/inbox/spliced → removedCount:1, inserted:[] ← 被 claim 吞掉
|
|
12
|
+
* t+17ms request/header → 请求已按旧上下文构造
|
|
13
|
+
*
|
|
14
|
+
* `agent.followup()` 入 `next-turn`,与 `next-step` 是两条独立队列,
|
|
15
|
+
* `claim('next-step', …)` 不触碰它,**结构上不存在**这个竞态 —— 故默认用它。
|
|
16
|
+
*
|
|
17
|
+
* 送达确认**只读会话日志**,不监听 `inbox/claimed` / `request/header` 等运行时
|
|
18
|
+
* 事件:那些事件的顺序是核心内部实现细节,监听它们等于把插件耦合到核心时序上,
|
|
19
|
+
* 核心一改就静默失效。日志格式是插件已经在依赖的稳定面(`foldUserState` 同源)。
|
|
20
|
+
* @module dsh-later/delivery
|
|
21
|
+
*/
|
|
22
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
23
|
+
import type { UserMessage } from '@deepseek-ai/dsh-llm';
|
|
24
|
+
/** 投递方式(与 settings 的 steerDelivery 同源,避免循环依赖故在此重述)。 */
|
|
25
|
+
export type DeliveryMode = 'followup' | 'steer';
|
|
26
|
+
/**
|
|
27
|
+
* 投递确认的超时预算。
|
|
28
|
+
*
|
|
29
|
+
* 正常核对在**毫秒级**完成(消息 append 后同一次宏任务内即可读到);2s 是留给
|
|
30
|
+
* 极端调度延迟的宽裕上限,超时即判「未确认」——宁可让用户看到可重试的提示,
|
|
31
|
+
* 也不要静默当成成功。
|
|
32
|
+
*/
|
|
33
|
+
export declare const DELIVERY_CONFIRM_TIMEOUT_MS = 2000;
|
|
34
|
+
/**
|
|
35
|
+
* 日志中是否出现该 message id。
|
|
36
|
+
*
|
|
37
|
+
* 判据是消息**真的进了会话日志**。这里刻意不进一步区分「进了 surface」与
|
|
38
|
+
* 「进了实际请求」:`agent.followup` 路径下二者由核心保证一致(消息成为独立一轮
|
|
39
|
+
* 的唯一条消息);而 `steer` 路径的丢失发生在**插入后同毫秒被 claim 移除**,
|
|
40
|
+
* 那种情况下连 `user/message` 都不会留下,本判据足以识别。
|
|
41
|
+
*/
|
|
42
|
+
export declare function hasMessageInLog(session: Session, messageId: string): boolean;
|
|
43
|
+
/** 等待投递被日志确认;超时返回 false。 */
|
|
44
|
+
export declare function confirmDelivery(session: Session, messageId: string, timeoutMs?: number): Promise<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* 取消息 id(`createUserMessage` 会生成 uuid)。
|
|
47
|
+
*
|
|
48
|
+
* `UserMessage.id` 在类型上是必填,但运行时若某代内核未填则这里回落空串,
|
|
49
|
+
* 调用方的确认逻辑对空 id 一律判「无法确认」。
|
|
50
|
+
*/
|
|
51
|
+
export declare function messageIdOf(message: UserMessage): string;
|
package/lib/types/domain.d.ts
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
17
17
|
/** 插件名(与 cordis 条目 id / manifest 一致)。 */
|
|
18
18
|
export declare const name = "dsh-later";
|
|
19
|
+
/**
|
|
20
|
+
* settings 命名空间。0.1.1 要求 branded `SettingsNamespace`、0.1.2 已删除
|
|
21
|
+
* `settingsNamespace()` helper——调用处以 `'dsh-later' as SettingsNamespace`
|
|
22
|
+
* 断言注册(见 index.ts),字面量只在这里维护一份。
|
|
23
|
+
*/
|
|
24
|
+
export declare const SETTINGS_NAMESPACE = "dsh-later";
|
|
19
25
|
/** 会话投影键:GUI 通过 `useProjection` / `faceOf` 读取用户任务列表。 */
|
|
20
26
|
export declare const PROJECTION_KEY = "userSchedules";
|
|
21
27
|
/**
|
|
@@ -49,6 +55,7 @@ export type UserScheduleDelivery = 'context' | 'user';
|
|
|
49
55
|
* 字段用 snake_case,与 `user_schedule_list` 工具响应保持一致。
|
|
50
56
|
*/
|
|
51
57
|
export interface UserScheduleWireItem {
|
|
58
|
+
/** 稳定身份:active 优先 ownership.uid,否则 scheduleId;paused 恒为 uid。 */
|
|
52
59
|
readonly id: string;
|
|
53
60
|
readonly kind: 'after' | 'at' | 'every';
|
|
54
61
|
readonly prompt: string;
|
|
@@ -58,6 +65,19 @@ export interface UserScheduleWireItem {
|
|
|
58
65
|
/** 创建时刻(来自 create 事件的 time;旧日志/未知时缺省)。 */
|
|
59
66
|
readonly created_at?: string;
|
|
60
67
|
readonly delivery_mode: 'session-local';
|
|
68
|
+
/** active 可省略(旧客户端);paused 必带。 */
|
|
69
|
+
readonly status?: 'active' | 'paused';
|
|
70
|
+
/** 暂停冻结的剩余秒数(整数秒,仅用于倒计时文案)。 */
|
|
71
|
+
readonly remaining_seconds?: number;
|
|
72
|
+
/** 暂停时刻(ISO;客户端据此冻结进度条,避免整数秒取整造成的长度跳变)。 */
|
|
73
|
+
readonly paused_at?: string;
|
|
74
|
+
/** 日志 schedule id(pause 前 / active 当前)。 */
|
|
75
|
+
readonly schedule_id?: string;
|
|
76
|
+
/**
|
|
77
|
+
* 进度条总窗口(秒)。resume 后 `after_seconds` 只是剩余时长;
|
|
78
|
+
* 此字段保留原 after 间隔,使进度条从冻结处继续而不是从 0 重跑。
|
|
79
|
+
*/
|
|
80
|
+
readonly window_seconds?: number;
|
|
61
81
|
}
|
|
62
82
|
/** 投影输出值。 */
|
|
63
83
|
export interface UserScheduleProjectionValue {
|
|
@@ -102,6 +122,18 @@ export interface UserScheduleProjectionState {
|
|
|
102
122
|
createdAt?: number;
|
|
103
123
|
}[];
|
|
104
124
|
readonly seedSeq: number;
|
|
125
|
+
/** sidecar 暂停留档的 plain-JSON 镜像(init/事件后刷新)。 */
|
|
126
|
+
readonly paused: readonly {
|
|
127
|
+
uid: string;
|
|
128
|
+
prompt: string;
|
|
129
|
+
delivery: UserScheduleDelivery;
|
|
130
|
+
kind: 'after';
|
|
131
|
+
remainingSeconds: number;
|
|
132
|
+
originalScheduledAt: string;
|
|
133
|
+
originalAfterSeconds?: number;
|
|
134
|
+
lastScheduleId: string;
|
|
135
|
+
pausedAt: number;
|
|
136
|
+
}[];
|
|
105
137
|
}
|
|
106
138
|
/** 判断事件是否属于本插件所有权流。 */
|
|
107
139
|
export declare function isOwnedEvent(event: SessionEvent): event is SessionEvent & {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -12,16 +12,24 @@
|
|
|
12
12
|
* @module dsh-later
|
|
13
13
|
*/
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
-
import
|
|
15
|
+
import type z from '@deepseek-ai/schemastery';
|
|
16
16
|
import { name } from './domain.js';
|
|
17
17
|
/** 插件配置 schema(全可选,默认开箱即用;经 cordis config 覆盖)。 */
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
/**
|
|
19
|
+
* 插件配置 schema(**单一事实来源**)。
|
|
20
|
+
*
|
|
21
|
+
* 0.2.0 起 `ctx.settings.describe()` 直接读 `entry.fiber.runtime.Config`
|
|
22
|
+
* (见 dsh-settings `schema(entry)`:「`entry.fiber?.runtime?.Config`,且需有
|
|
23
|
+
* `toJSON`」),据此生成该插件设置页的表单。因此**可编辑字段必须全部声明在
|
|
24
|
+
* `Config` 上**——只放在 `SchedulerSettingsSchema` 里的字段在 0.2.0 下不会
|
|
25
|
+
* 出现在设置 UI 中。
|
|
26
|
+
*
|
|
27
|
+
* 历史:0.1.5 用 `settingsProvider.register(ns, schema, …)` 注册一个**独立于
|
|
28
|
+
* Config 的** schema(即 `SchedulerSettingsSchema`),0.1.7 删除了该 API。
|
|
29
|
+
* 两代并存期间两份 schema 内容一致,现统一为 `Config` 一份,`SchedulerSettingsSchema`
|
|
30
|
+
* 保留为别名以免大范围改动调用点。
|
|
31
|
+
*/
|
|
32
|
+
export declare const Config: z;
|
|
25
33
|
/** 所需服务(延后激活,缺失时插件不加载)。 */
|
|
26
34
|
export declare const inject: string[];
|
|
27
35
|
/** 宿主持有的 settings 形状(settings getter 返回值与 register* 接受入参)。 */
|
|
@@ -29,7 +37,19 @@ export interface SchedulerSettings {
|
|
|
29
37
|
readonly maxSchedules: number;
|
|
30
38
|
readonly allowLongPrompts: boolean;
|
|
31
39
|
readonly maxPromptChars: number;
|
|
40
|
+
/** 插话投递方式(见 Config.steerDelivery)。 */
|
|
41
|
+
readonly steerDelivery: UserScheduleSteerDelivery;
|
|
42
|
+
/** 是否核对投递是否真正送达。 */
|
|
43
|
+
readonly confirmDelivery: boolean;
|
|
44
|
+
/** 投递未确认时:保留可重试 / 丢弃。 */
|
|
45
|
+
readonly onUnconfirmed: UserScheduleOnUnconfirmed;
|
|
46
|
+
/** 时钟图标风格(auto / modern / classic)。 */
|
|
47
|
+
readonly clockIconStyle: 'auto' | 'modern' | 'classic';
|
|
32
48
|
}
|
|
49
|
+
/** 插话投递方式。 */
|
|
50
|
+
export type UserScheduleSteerDelivery = 'followup' | 'steer';
|
|
51
|
+
/** 投递未确认时的处理。 */
|
|
52
|
+
export type UserScheduleOnUnconfirmed = 'keep' | 'discard';
|
|
33
53
|
export declare function apply(ctx: Context, config?: {
|
|
34
54
|
maxSchedules?: number;
|
|
35
55
|
}): void;
|
|
@@ -5,6 +5,32 @@ export interface OwnedEntry {
|
|
|
5
5
|
readonly delivery: UserScheduleDelivery;
|
|
6
6
|
/** 创建时刻(epoch ms,诊断用)。 */
|
|
7
7
|
readonly createdAt: number;
|
|
8
|
+
/**
|
|
9
|
+
* 稳定插件身份(跨 pause/resume/日志 id 变更)。创建时生成;
|
|
10
|
+
* 旧条目可缺省(wire 回退用 scheduleId)。
|
|
11
|
+
*/
|
|
12
|
+
readonly uid?: string;
|
|
13
|
+
/**
|
|
14
|
+
* 进度条总窗口(秒)。resume 后日志里的 `afterSeconds` 只是剩余时长,
|
|
15
|
+
* 若直接当窗口会从 0 重跑;此处保留原 after 间隔供 wire/进度条使用。
|
|
16
|
+
*/
|
|
17
|
+
readonly windowSeconds?: number;
|
|
18
|
+
}
|
|
19
|
+
/** 暂停留档(仅 kind==='after';不进会话日志)。 */
|
|
20
|
+
export interface PausedEntry {
|
|
21
|
+
/** 稳定身份;resume 后仍用此 uid。 */
|
|
22
|
+
readonly uid: string;
|
|
23
|
+
readonly prompt: string;
|
|
24
|
+
readonly delivery: UserScheduleDelivery;
|
|
25
|
+
readonly kind: 'after';
|
|
26
|
+
/** 暂停瞬间的剩余秒数(resume 时 after_seconds)。 */
|
|
27
|
+
readonly remainingSeconds: number;
|
|
28
|
+
/** 原目标时刻(仅展示「原点 HH:MM」)。 */
|
|
29
|
+
readonly originalScheduledAt: string;
|
|
30
|
+
readonly originalAfterSeconds?: number;
|
|
31
|
+
/** 暂停前的日志 schedule id(审计/关联)。 */
|
|
32
|
+
readonly lastScheduleId: string;
|
|
33
|
+
readonly pausedAt: number;
|
|
8
34
|
}
|
|
9
35
|
/** 读取某会话的全部所有权记录(缓存优先;返回副本语义,调用方不得变更)。 */
|
|
10
36
|
export declare function allOwnership(sessionId: string): Readonly<Record<string, OwnedEntry>>;
|
|
@@ -12,9 +38,23 @@ export declare function allOwnership(sessionId: string): Readonly<Record<string,
|
|
|
12
38
|
export declare function hasOwnership(sessionId: string, id: string): boolean;
|
|
13
39
|
/** 某条所有权记录(缺失返回 undefined)。 */
|
|
14
40
|
export declare function getOwnership(sessionId: string, id: string): OwnedEntry | undefined;
|
|
15
|
-
/**
|
|
16
|
-
export declare function
|
|
41
|
+
/** 生成稳定插件 uid(pause/resume 跨日志 id 变更)。 */
|
|
42
|
+
export declare function newTaskUid(): string;
|
|
43
|
+
/** 记录(新增或覆盖)一条所有权;幂等。仅显式传入 `uid` 时写入稳定身份。 */
|
|
44
|
+
export declare function recordOwnership(sessionId: string, id: string, delivery: UserScheduleDelivery, uid?: string, windowSeconds?: number): OwnedEntry;
|
|
17
45
|
/** 撤销一条所有权;幂等(不存在时无操作)。 */
|
|
18
46
|
export declare function removeOwnership(sessionId: string, id: string): void;
|
|
47
|
+
/** 按 uid 找当前仍 active 的 schedule id(缺失 undefined)。 */
|
|
48
|
+
export declare function findScheduleIdByUid(sessionId: string, uid: string): string | undefined;
|
|
49
|
+
/** 全部暂停留档(uid → entry)。 */
|
|
50
|
+
export declare function allPaused(sessionId: string): Readonly<Record<string, PausedEntry>>;
|
|
51
|
+
/** 一条暂停留档。 */
|
|
52
|
+
export declare function getPaused(sessionId: string, uid: string): PausedEntry | undefined;
|
|
53
|
+
/** 按暂停前 schedule id 找留档(pause 刚写完、投影 fold 见 delete 时用)。 */
|
|
54
|
+
export declare function findPausedByScheduleId(sessionId: string, scheduleId: string): PausedEntry | undefined;
|
|
55
|
+
/** 写入/覆盖一条暂停留档。 */
|
|
56
|
+
export declare function recordPaused(sessionId: string, entry: PausedEntry): void;
|
|
57
|
+
/** 移除一条暂停留档;幂等。 */
|
|
58
|
+
export declare function removePaused(sessionId: string, uid: string): void;
|
|
19
59
|
/** 测试辅助:清空进程内缓存(不删文件)。 */
|
|
20
60
|
export declare function resetCacheForTests(): void;
|
package/lib/types/presence.d.ts
CHANGED
|
@@ -19,21 +19,92 @@ export declare const PRESENCE_URGENT_WINDOW_MS: number;
|
|
|
19
19
|
export interface PresenceScheduleLike {
|
|
20
20
|
readonly scheduled_at?: string;
|
|
21
21
|
readonly scheduledAt?: string;
|
|
22
|
+
/** `'paused'` 时该任务**没有排定的触发时刻**(冻结剩余秒数,等待恢复)。 */
|
|
23
|
+
readonly status?: string;
|
|
24
|
+
/** 暂停冻结的剩余秒数;仅在 `status === 'paused'` 时有意义。 */
|
|
25
|
+
readonly remaining_seconds?: number;
|
|
26
|
+
/** 任务类型。决定进度窗口起点按哪个口径推导(见 {@link windowStartOf})。 */
|
|
27
|
+
readonly kind?: string;
|
|
28
|
+
/** `after` 类型的总倒计时秒数(进度起点 = 触发时刻 − 该值)。 */
|
|
29
|
+
readonly after_seconds?: number;
|
|
30
|
+
readonly afterSeconds?: number;
|
|
31
|
+
/** `every` 类型的周期间隔秒数(进度起点 = 触发时刻 − 该值,即本周期起点)。 */
|
|
32
|
+
readonly every_seconds?: number;
|
|
33
|
+
readonly everySeconds?: number;
|
|
34
|
+
/** 宿主/投影直接给的显式总窗口秒数;给了就优先于按 kind 推导。 */
|
|
35
|
+
readonly window_seconds?: number;
|
|
36
|
+
readonly windowSeconds?: number;
|
|
37
|
+
/** `at` 类型的进度起点(任务创建时刻,ISO)。`at` 没有天然周期,只能用它。 */
|
|
38
|
+
readonly created_at?: string;
|
|
39
|
+
readonly createdAt?: number;
|
|
22
40
|
}
|
|
23
41
|
/** 由一组活动任务归纳的 badge 概要。 */
|
|
24
42
|
export interface PresenceSummary {
|
|
25
|
-
/**
|
|
43
|
+
/** 计入提醒数量的任务数(**含**暂停项)。 */
|
|
26
44
|
readonly count: number;
|
|
27
|
-
/** 最早触发时刻(epoch ms
|
|
45
|
+
/** 最早触发时刻(epoch ms);没有任何"会触发"的任务时缺省。 */
|
|
28
46
|
readonly nextAt?: number;
|
|
47
|
+
/** 暂停中的任务数(用于文案区分「已冻结」与「等待发送」)。 */
|
|
48
|
+
readonly pausedCount: number;
|
|
49
|
+
/**
|
|
50
|
+
* 进度环起点(epoch ms):`nextAt` 那条任务的窗口开始时刻。
|
|
51
|
+
*
|
|
52
|
+
* **只跟随 `nextAt` 那条**:环表达的是"距离最近一次触发还剩多少",若取别条
|
|
53
|
+
* 任务的窗口就与 `nextAt` 对不上了。无从推导时缺省,此时调用方应退回恒定
|
|
54
|
+
* 整环({@link progressOf} 返回 `undefined`)。
|
|
55
|
+
*/
|
|
56
|
+
readonly progressFrom?: number;
|
|
29
57
|
}
|
|
30
58
|
/**
|
|
31
|
-
* 归纳:取最早触发时刻 +
|
|
32
|
-
*
|
|
59
|
+
* 归纳:取最早触发时刻 + 任务数量。
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ **暂停项不得参与 `nextAt`**(这是一个真实缺陷的根因)。暂停 = 日志 delete +
|
|
62
|
+
* sidecar 留档;wire 里的 `scheduled_at` 是 `originalScheduledAt`(**原目标时刻**),
|
|
63
|
+
* 而该时刻在暂停时通常已成过去。若照常参与取最小值,侧栏会算出 `nextAt <= now`
|
|
64
|
+
* → 显示「有 N 条定时提醒已到期,等待发送」,但这其实是一条**已冻结、不会触发**的
|
|
65
|
+
* 提醒,用户看到的是自相矛盾的状态(实测复现)。
|
|
66
|
+
*
|
|
67
|
+
* 正确语义:暂停项**计入 `count`**(它确实是待办提醒),但**不参与 `nextAt`**
|
|
68
|
+
* ——它没有排定的触发时刻,只在恢复后才重新拥有。
|
|
69
|
+
*
|
|
70
|
+
* 全部都是暂停项时 `nextAt` 为 undefined:调用方据此显示中性的「已暂停」而非
|
|
71
|
+
* 「已到期」或"下次 X 分后"。
|
|
33
72
|
*/
|
|
34
73
|
export declare function summarizeSchedules(schedules: readonly PresenceScheduleLike[] | undefined): PresenceSummary;
|
|
74
|
+
/**
|
|
75
|
+
* 进度环比例(0–1):`now` 在 `[progressFrom, nextAt]` 中的位置。
|
|
76
|
+
*
|
|
77
|
+
* 返回 `undefined` 表示**无从算起**(缺窗口起点,或窗口已退化),调用方此时应
|
|
78
|
+
* 退回「恒定整环」的旧表现,而不是画一个 0% 的空环 —— 后者是错误信息:
|
|
79
|
+
* 0% 是"刚开始",`undefined` 是"算不出来"。
|
|
80
|
+
*
|
|
81
|
+
* 两端夹住:`overdue` 时 `now > nextAt`,环停在满环而不是绕出去。
|
|
82
|
+
*/
|
|
83
|
+
export declare function progressOf(entry: PresenceSummary, now: number): number | undefined;
|
|
35
84
|
/** 由当前时刻判定 badge 状态。 */
|
|
36
85
|
export declare function badgeStateFor(nextAt: number | undefined, now: number): PresenceBadgeState;
|
|
86
|
+
/**
|
|
87
|
+
* 悬浮卡片状态行的完整文案(与宿主 `.hoverStatus` 行同构:一个状态点 + 一句
|
|
88
|
+
* 描述)。宿主的状态链对定时任务无感知,本插件**只追加**这样一行,不改写也
|
|
89
|
+
* 不替换宿主已有的 `空闲` / `进行中` 等行,因此既有状态优先级天然不变。
|
|
90
|
+
*
|
|
91
|
+
* @param entry - 该会话的定时概要(调用方保证 count > 0)。
|
|
92
|
+
* @param now - 当前时刻(epoch ms),用于判定 overdue 与相对时间。
|
|
93
|
+
* @param labels - 相对时间文案槽位。
|
|
94
|
+
* @param strings - 当前语言的字典。
|
|
95
|
+
* @param formatHhmm - 时钟面格式化(注入以便单测)。
|
|
96
|
+
* @param formatDate - 日期面格式化(注入以便单测)。
|
|
97
|
+
* @returns 状态文案 + 视觉状态(`done` 为中性信息态,与宿主同款状态点)。
|
|
98
|
+
*/
|
|
99
|
+
export declare function hoverScheduleStatus(entry: PresenceSummary, now: number, labels: RelativeFireLabels, strings: {
|
|
100
|
+
readonly hoverScheduleStatus: string;
|
|
101
|
+
readonly hoverScheduleOverdue: string;
|
|
102
|
+
/** 全部任务暂停时的文案;缺省回退到 overdue 文案,保持向后兼容。 */
|
|
103
|
+
readonly hoverSchedulePaused?: string;
|
|
104
|
+
}, formatHhmm: (epoch: number) => string, formatDate: (epoch: number) => string): {
|
|
105
|
+
readonly label: string;
|
|
106
|
+
readonly tone: PresenceBadgeState;
|
|
107
|
+
};
|
|
37
108
|
/** 相对触发时间的文案槽位(由调用方按语言准备;时间串经 formatHhmm 注入)。 */
|
|
38
109
|
export interface RelativeFireLabels {
|
|
39
110
|
/** ≤1 分钟:即将发送。 */
|