dsh-later 1.0.0 → 1.1.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.
@@ -0,0 +1,179 @@
1
+ /**
2
+ * dsh-schedule 0.1.5 → 0.1.7 的版本桥接层。
3
+ *
4
+ * 0.1.7 对 `dsh-schedule` 做了三处破坏性变更,本模块把差异收敛为三个函数,
5
+ * 使 `user-tools.ts` / `runtime.ts` 不必写版本分支:
6
+ *
7
+ * 1. `allocateScheduleId(folded)` → 删除。0.1.5 用确定性递增
8
+ * (`schedule-1`、`schedule-2`…,靠 `seenIds` 避让),0.1.7 内联改为
9
+ * `ScheduleId(\`schedule-${randomUUID()}\`)`(见 0.1.7 `ScheduleService.create`)。
10
+ * 本插件的 `ownership-store` 把 id 当**不透明字符串**处理(无排序、无数字
11
+ * 解析,见 docs/verification.md),故行为差异对功能无影响。
12
+ *
13
+ * 2. `createAfterScheduleRecord` 新增第 5 个必填参数 `title`。0.1.7 起
14
+ * `decodeStoredTitle` 会在读取时校验:缺失 / 未 trim / 超长(>120)的
15
+ * 存量记录抛 `ScheduleLogError`——**不是跳过,是整份日志拒读**。
16
+ *
17
+ * 3. `renderEveryReminderBatchFraming` → 改名
18
+ * `renderRecurringReminderBatchFraming`(语义与参数不变)。
19
+ *
20
+ * @module dsh-later/schedule-compat
21
+ */
22
+ import * as sched from '@deepseek-ai/dsh-schedule';
23
+ import { randomUUID } from 'node:crypto';
24
+ const { ScheduleId } = sched;
25
+ /** 0.1.7 `MAX_TITLE_LENGTH`;0.1.5 无此概念,该常量在此仅作截断阈值。 */
26
+ export const SCHEDULE_TITLE_MAX = 120;
27
+ /**
28
+ * 读取一个**可能不存在**的导出。
29
+ *
30
+ * 必须走 namespace + 属性访问,不能用具名 import:ESM 具名 import 在目标导出
31
+ * 缺失时是**链接期硬失败**(`SyntaxError: does not provide an export named …`),
32
+ * 无法用 `as unknown as` 兜底。0.1.7 删掉 `renderEveryReminderBatchFraming` 后,
33
+ * 具名 import 会让整个插件在 0.1.5 上也无法加载——正好与支持区间相反。
34
+ */
35
+ function pick(name) {
36
+ return sched[name];
37
+ }
38
+ /**
39
+ * 0.1.7 的 `allocateScheduleId` 等价物。
40
+ *
41
+ * 随机 UUID 而非递增序号:与官方 `ScheduleService.create` 行为一致,且并发
42
+ * 创建时天然无碰撞。0.1.5 下也可用(`ScheduleId` 是纯 brand 校验,任意
43
+ * 非空字符串通过),因此本函数在两个代次上语义相同,无需双轨。
44
+ *
45
+ * @param _folded 仅为**保持调用点不变**而接受 0.1.5 的 `folded` 参数;0.1.7
46
+ * 不再需要它来避让碰撞(UUID 本身唯一),故不使用。
47
+ */
48
+ export function allocateScheduleIdCompat(
49
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
50
+ _folded) {
51
+ return ScheduleId(`schedule-${randomUUID()}`);
52
+ }
53
+ /**
54
+ * 从提醒正文派生 `title`。
55
+ *
56
+ * 0.1.7 要求 title 非空、已 trim、≤120 字符。实测本插件历史 496 条提醒中
57
+ * 最长 575 字符(27 条超限,占 5.4%),故**必须截断**而非直接透传 prompt。
58
+ * 截断在词边界优先,找不到合适边界时按字符硬切并补省略号,保证结果始终
59
+ * 是合法 title(`scheduleTitle` 不会抛)。
60
+ */
61
+ export function deriveScheduleTitle(prompt) {
62
+ const normalized = prompt.trim();
63
+ if (normalized.length === 0)
64
+ return '定时提醒';
65
+ if (normalized.length <= SCHEDULE_TITLE_MAX)
66
+ return normalized;
67
+ const clipped = normalized.slice(0, SCHEDULE_TITLE_MAX - 1);
68
+ const lastBreak = Math.max(clipped.lastIndexOf(' '), clipped.lastIndexOf(' '));
69
+ const body = lastBreak >= SCHEDULE_TITLE_MAX * 0.6 ? clipped.slice(0, lastBreak) : clipped;
70
+ return `${body.trimEnd()}…`;
71
+ }
72
+ /**
73
+ * 调用一个 `create*ScheduleRecord`,按目标代次决定是否追加 `title`。
74
+ *
75
+ * 0.1.5 签名为 `(id, prompt, selector, now)`;0.1.7 追加第 5 位必填 `title`。
76
+ * 0.1.7 的 `scheduleTitle` 对空 / 未 trim / >120 字符一律抛错,故 title 必须
77
+ * 由 `deriveScheduleTitle` 保证合法。
78
+ */
79
+ function createRecord(name, id, prompt, selector, now) {
80
+ const fn = pick(name);
81
+ if (typeof fn !== 'function') {
82
+ throw new Error(`dsh-later: dsh-schedule does not export ${name} — unsupported kernel generation.`);
83
+ }
84
+ // 0.1.7 的 arity 为 5;0.1.5 为 4。按 arity 择一,无需版本号判断。
85
+ return fn.length >= 5
86
+ ? fn(id, prompt, selector, now, deriveScheduleTitle(prompt))
87
+ : fn(id, prompt, selector, now);
88
+ }
89
+ /** `createAfterScheduleRecord` 的跨版本包装。 */
90
+ export function createAfterRecord(id, prompt, afterSeconds, now) {
91
+ return createRecord('createAfterScheduleRecord', id, prompt, afterSeconds, now);
92
+ }
93
+ /** `createAtScheduleRecord` 的跨版本包装。 */
94
+ export function createAtRecord(id, prompt, scheduledAt, now) {
95
+ return createRecord('createAtScheduleRecord', id, prompt, scheduledAt, now);
96
+ }
97
+ /** `createEveryScheduleRecord` 的跨版本包装。 */
98
+ export function createEveryRecord(id, prompt, everySeconds, now) {
99
+ return createRecord('createEveryScheduleRecord', id, prompt, everySeconds, now);
100
+ }
101
+ /**
102
+ * 周期提醒批次的 framing 渲染。
103
+ *
104
+ * 0.1.5 导出 `renderEveryReminderBatchFraming`,0.1.7 改名为
105
+ * `renderRecurringReminderBatchFraming`(并扩展到 daily/weekly/cron,本插件
106
+ * 只产生 `every` 记录)。两代参数与返回结构一致,按存在性择一。
107
+ */
108
+ export function renderRecurringBatchFraming(reminders) {
109
+ const render = pick('renderEveryReminderBatchFraming') ??
110
+ pick('renderRecurringReminderBatchFraming');
111
+ if (typeof render !== 'function') {
112
+ throw new Error('dsh-later: dsh-schedule exposes neither renderEveryReminderBatchFraming ' +
113
+ 'nor renderRecurringReminderBatchFraming — unsupported kernel generation.');
114
+ }
115
+ return render(reminders);
116
+ }
117
+ /**
118
+ * 提醒注入消息的 `source`。
119
+ *
120
+ * 0.1.5 的 `MessageSourceMap` 含 `plugin: { kind:'plugin'; plugin:string }`;
121
+ * 0.1.7 **删除了 `plugin` 与 `host` 两个 kind**,改由内核为每个生产者登记
122
+ * 具名 kind。官方 `dsh-schedule` 在 0.1.7 里注入提醒用的正是
123
+ * `source: { kind: 'schedule' }`(见其 `lib/index.js` 的 `agent.followup` 调用点)
124
+ * —— 与本插件投递的语义一致,故 0.1.7 走 `schedule`。
125
+ *
126
+ * `form:'notice'` + `summary` 两代都在(`ContextFormed` 未变),保留。
127
+ *
128
+ * 不能用具名类型断言跨代:source 的字面量 union 在两代不同,断言只会把
129
+ * 编译错误推迟到运行期。
130
+ */
131
+ export function scheduleMessageSource(boundedSummary) {
132
+ const base = { form: 'notice', summary: boundedSummary };
133
+ // 0.1.7 已删除 'plugin';`schedule` 与 renderRecurringReminderBatchFraming
134
+ // 同属 0.1.7 引入,二者同时存在即为 0.1.7+。
135
+ const isV17 = pick('renderRecurringReminderBatchFraming') !== undefined;
136
+ return isV17
137
+ ? { kind: 'schedule', ...base }
138
+ : { kind: 'plugin', plugin: PLUGIN_LABEL, ...base };
139
+ }
140
+ /**
141
+ * `foldScheduleEvents` 结果的读视图收窄。
142
+ *
143
+ * 0.1.5 的 `FoldedSchedules.active` 是 `ScheduleRecord[]`;0.1.7 改为
144
+ * `LegacyScheduleRecord[]`(title 可选)。本插件只消费 `id` / `kind` /
145
+ * `prompt` / `afterSeconds` / `everySeconds` / `scheduledAt`,这些字段在
146
+ * `Legacy*` 视图中全部存在,故收窄无损。
147
+ */
148
+ export function asLegacySchedules(folded) {
149
+ return folded;
150
+ }
151
+ /** 本插件在 0.1.5 时代的 source 标签(0.1.7 已无 plugin kind,仅作回退标识)。 */
152
+ const PLUGIN_LABEL = '定时提醒';
153
+ /**
154
+ * `schedule/change` 事件里 `create` 操作的载荷类型。
155
+ *
156
+ * 0.1.7 起内核把「事件载荷」与「内部记录」拆成两个类型:
157
+ * - `ScheduleCreateChange.schedule` 是 **`LegacyScheduleRecord`**(title 可选)
158
+ * - `ScheduleRecord` 是 Host 内部的新格式(title 必填,且含 daily/weekly/cron)
159
+ *
160
+ * 本插件只产生 `after` / `at` / `every` 三种(见 buildScheduleRecord),三者都是
161
+ * `Legacy*` 的对应分支去掉「title 必填」后的结果,故结构兼容。
162
+ *
163
+ * 但 TS 层面 `ScheduleRecord`(6 变体联合)对 `LegacyScheduleRecord`(3 变体)
164
+ * 不可赋值——**这不是运行时问题,是联合宽度问题**。故在此显式收窄:本插件
165
+ * 的产出已经过 `createAfter/At/EveryRecord` 限定,不含 daily/weekly/cron。
166
+ */
167
+ export function toChangePayload(record) {
168
+ return record;
169
+ }
170
+ /**
171
+ * `foldScheduleEvents().active` 元素的读取侧类型。
172
+ *
173
+ * 0.1.5 直接返回 `ScheduleRecord`;0.1.7 返回 `LegacyScheduleRecord`
174
+ * (title 可选,因为历史事件没有)。本插件读路径只在 title 存在时展示,
175
+ * 缺失时回退到 prompt——故按 Legacy 收窄是**语义正确**的,而非绕过检查。
176
+ */
177
+ export function asLegacyRecords(records) {
178
+ return records;
179
+ }
@@ -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;
@@ -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 & {
@@ -12,16 +12,24 @@
12
12
  * @module dsh-later
13
13
  */
14
14
  import type { Context } from '@deepseek-ai/cordis';
15
- import s from '@deepseek-ai/schemastery';
15
+ import type z from '@deepseek-ai/schemastery';
16
16
  import { name } from './domain.js';
17
17
  /** 插件配置 schema(全可选,默认开箱即用;经 cordis config 覆盖)。 */
18
- export declare const Config: s<Schemastery.ObjectS<{
19
- /** 单 session 用户任务上限。 */
20
- maxSchedules: s<number, number>;
21
- }>, Schemastery.ObjectT<{
22
- /** 单 session 用户任务上限。 */
23
- maxSchedules: s<number, number>;
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 recordOwnership(sessionId: string, id: string, delivery: UserScheduleDelivery): void;
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;
@@ -19,21 +19,60 @@ 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;
22
26
  }
23
27
  /** 由一组活动任务归纳的 badge 概要。 */
24
28
  export interface PresenceSummary {
25
- /** 可解析触发时刻的活动任务数量。 */
29
+ /** 计入提醒数量的任务数(**含**暂停项)。 */
26
30
  readonly count: number;
27
- /** 最早触发时刻(epoch ms);全部不可解析时缺省。 */
31
+ /** 最早触发时刻(epoch ms);没有任何"会触发"的任务时缺省。 */
28
32
  readonly nextAt?: number;
33
+ /** 暂停中的任务数(用于文案区分「已冻结」与「等待发送」)。 */
34
+ readonly pausedCount: number;
29
35
  }
30
36
  /**
31
- * 归纳:取最早触发时刻 + 可解析数量。`every` 类任务在投影里携带推进后的
32
- * `scheduledAt`(下次触发点),与一次性任务同构参与排序。
37
+ * 归纳:取最早触发时刻 + 任务数量。
38
+ *
39
+ * ⚠️ **暂停项不得参与 `nextAt`**(这是一个真实缺陷的根因)。暂停 = 日志 delete +
40
+ * sidecar 留档;wire 里的 `scheduled_at` 是 `originalScheduledAt`(**原目标时刻**),
41
+ * 而该时刻在暂停时通常已成过去。若照常参与取最小值,侧栏会算出 `nextAt <= now`
42
+ * → 显示「有 N 条定时提醒已到期,等待发送」,但这其实是一条**已冻结、不会触发**的
43
+ * 提醒,用户看到的是自相矛盾的状态(实测复现)。
44
+ *
45
+ * 正确语义:暂停项**计入 `count`**(它确实是待办提醒),但**不参与 `nextAt`**
46
+ * ——它没有排定的触发时刻,只在恢复后才重新拥有。
47
+ *
48
+ * 全部都是暂停项时 `nextAt` 为 undefined:调用方据此显示中性的「已暂停」而非
49
+ * 「已到期」或"下次 X 分后"。
33
50
  */
34
51
  export declare function summarizeSchedules(schedules: readonly PresenceScheduleLike[] | undefined): PresenceSummary;
35
52
  /** 由当前时刻判定 badge 状态。 */
36
53
  export declare function badgeStateFor(nextAt: number | undefined, now: number): PresenceBadgeState;
54
+ /**
55
+ * 悬浮卡片状态行的完整文案(与宿主 `.hoverStatus` 行同构:一个状态点 + 一句
56
+ * 描述)。宿主的状态链对定时任务无感知,本插件**只追加**这样一行,不改写也
57
+ * 不替换宿主已有的 `空闲` / `进行中` 等行,因此既有状态优先级天然不变。
58
+ *
59
+ * @param entry - 该会话的定时概要(调用方保证 count > 0)。
60
+ * @param now - 当前时刻(epoch ms),用于判定 overdue 与相对时间。
61
+ * @param labels - 相对时间文案槽位。
62
+ * @param strings - 当前语言的字典。
63
+ * @param formatHhmm - 时钟面格式化(注入以便单测)。
64
+ * @param formatDate - 日期面格式化(注入以便单测)。
65
+ * @returns 状态文案 + 视觉状态(`done` 为中性信息态,与宿主同款状态点)。
66
+ */
67
+ export declare function hoverScheduleStatus(entry: PresenceSummary, now: number, labels: RelativeFireLabels, strings: {
68
+ readonly hoverScheduleStatus: string;
69
+ readonly hoverScheduleOverdue: string;
70
+ /** 全部任务暂停时的文案;缺省回退到 overdue 文案,保持向后兼容。 */
71
+ readonly hoverSchedulePaused?: string;
72
+ }, formatHhmm: (epoch: number) => string, formatDate: (epoch: number) => string): {
73
+ readonly label: string;
74
+ readonly tone: PresenceBadgeState;
75
+ };
37
76
  /** 相对触发时间的文案槽位(由调用方按语言准备;时间串经 formatHhmm 注入)。 */
38
77
  export interface RelativeFireLabels {
39
78
  /** ≤1 分钟:即将发送。 */