@qilin-agent/schedule 0.0.0-stage → 3.1.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kkutysllb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,76 @@
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/schedule/schedule/README.md
5
+ /:
6
+ en: 1fc7b2d7d5139f74
7
+ zh: 195be90460ef9e44
8
+ /qilin-schedule:
9
+ en: 66b8d3ef30f71980
10
+ zh: e484f57ecbba1f5f
11
+ /qilin-schedule/summary:
12
+ en: 6cc0239faf70fdfc
13
+ zh: 65b6b974c0b30e8b
14
+ /qilin-schedule/table-of-contents:
15
+ en: 23386452a66b2fbd
16
+ zh: 4a8d91b2f313fead
17
+ /qilin-schedule/use-this-package:
18
+ en: e784afed20abe8f4
19
+ zh: 62ab4b73859d9be0
20
+ /qilin-schedule/understand-the-implementation:
21
+ en: 611c89f03a2e478f
22
+ zh: 2b7ebdb078899a5c
23
+ /qilin-schedule/further-exploration:
24
+ en: 1659c88eaeb69632
25
+ zh: 91ffd91bd045159a
26
+ /qilin-schedule/model-experience:
27
+ en: 215e7ba838619b7b
28
+ zh: a311da3843709f90
29
+ /qilin-schedule/model-experience/tool-schemas-on-root-agents:
30
+ en: c4c31a3f6826626d
31
+ zh: 5aa01bfa311b2f4c
32
+ /qilin-schedule/model-experience/tool-schemas-on-root-agents/what-the-model-sees:
33
+ en: b22c6d3521b97d38
34
+ zh: 20cafd17b4b4e48d
35
+ /qilin-schedule/model-experience/tool-schemas-on-root-agents/token-effect:
36
+ en: ae0eb4d2609d4329
37
+ zh: 4fe0a26e37c272dc
38
+ /qilin-schedule/model-experience/tool-schemas-on-root-agents/kv-cache-effect:
39
+ en: 7bf33bbe17e1da03
40
+ zh: 2af9a2f842b4f430
41
+ /qilin-schedule/model-experience/tool-results-after-management-calls:
42
+ en: 2a940dee68296bc7
43
+ zh: 5ad3f71bd69b3c4c
44
+ /qilin-schedule/model-experience/tool-results-after-management-calls/what-the-model-sees:
45
+ en: f30a961076f7c964
46
+ zh: 3d9bd2f09cea748f
47
+ /qilin-schedule/model-experience/tool-results-after-management-calls/token-effect:
48
+ en: c86ffe9c10e1b42f
49
+ zh: 11539c343d70dc4a
50
+ /qilin-schedule/model-experience/tool-results-after-management-calls/kv-cache-effect:
51
+ en: e358118905df355b
52
+ zh: a98fc6617fcf4335
53
+ /qilin-schedule/model-experience/due-reminders-in-the-original-session:
54
+ en: 39a747e5d135e241
55
+ zh: ad505b3d5bc27acf
56
+ /qilin-schedule/model-experience/due-reminders-in-the-original-session/what-the-model-sees:
57
+ en: 7b05d635d4cc9714
58
+ zh: d1c908f51115e4f5
59
+ /qilin-schedule/model-experience/due-reminders-in-the-original-session/what-the-model-sees/one-shot-framing:
60
+ en: d8635e0075bacc3f
61
+ zh: 685e16d812319f85
62
+ /qilin-schedule/model-experience/due-reminders-in-the-original-session/what-the-model-sees/recurring-batch-framing:
63
+ en: 400bfbaf7808ff3c
64
+ zh: ea984dc90b3ff5dd
65
+ /qilin-schedule/model-experience/due-reminders-in-the-original-session/token-effect:
66
+ en: cee966cc62db0b77
67
+ zh: 3ff87d41d7a2caac
68
+ /qilin-schedule/model-experience/due-reminders-in-the-original-session/kv-cache-effect:
69
+ en: 1006325fa610366d
70
+ zh: 476215d8a3c70fe4
71
+ /qilin-schedule/known-limitations-and-deferred-work:
72
+ en: fd511d607cbc7b8d
73
+ zh: f45a14991cfa47e6
74
+ /qilin-schedule/known-limitations-and-deferred-work/dev-note:
75
+ en: e67f633dc6fe7773
76
+ zh: 8ec3e73528ddb1a6
package/README.md CHANGED
@@ -1,3 +1,171 @@
1
- # Temporary Holding Version
1
+ ---
2
+ description: "Host-wide durable reminders and shared Session-bound task management."
3
+ kind: "package-reference"
4
+ ---
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # @qilin-agent/schedule
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ Schedule delivers one-shot, fixed-rate, daily, weekly, and cron wall-clock reminders as follow-up messages in their original Session. Tasks remain available after Host restart, and each recurring task contributes only its latest missed occurrence. The Host restores a cold Session when delivery is due. Active and inactive tasks remain inspectable until explicit deletion, and deletion removes the task row together with its saved delivery records.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ <a id="use-this-package"></a>
24
+ ## Use this package
25
+
26
+ The shipped Web bundle mounts the service alongside storage-domain and the Session controller. Its `Config` states `deliveryHistoryDays` (default 30) and `deliveryHistoryRecords` (default 200). Storage backend routing belongs to storage-domain; Session model and preset restoration belong to the Session controller. Schedule cannot be mounted alone in a headless or SDK-only composition: delivery requires the Host Web Session controller and a Session persistence backend, because a delivery commits only after the Session acknowledges `session/flush`.
27
+
28
+ The Agent receives `schedule_create`, `schedule_list`, `schedule_delete`, and `schedule_update`. Update replaces the name, instruction, or timing of one reminder in place and keeps its id and saved records; it is not offered for the relative `after` delay. Creation requires a non-empty prompt, a title, and exactly one of six selectors:
29
+
30
+ | Selector | Example | Timing |
31
+ |---|---|---|
32
+ | `after_seconds` | `{"prompt":"Check the build","title":"Build check","after_seconds":600}` | Positive safe-integer delay. |
33
+ | `at` | `{"prompt":"Review the release","title":"Release review","at":"2099-01-01T09:00:00+08:00"}` | Strictly future absolute instant; a local date/time object with an explicit zone is also accepted. |
34
+ | `every_seconds` | `{"prompt":"Check the queue","title":"Queue check","every_seconds":300}` | Fixed safe-integer interval of at least 60 seconds, initially aligned to creation. |
35
+ | `daily` | `{"prompt":"Review today's tasks","title":"Daily review","daily":{"time":"23:00:00","time_zone":"Asia/Shanghai"}}` | Local wall-clock time in an explicit IANA zone. |
36
+ | `weekly` | `{"prompt":"Review the week","title":"Weekly review","weekly":{"time":"09:00:00","time_zone":"Asia/Shanghai","weekdays":[1,3]}}` | Local wall-clock time on an explicit ISO weekday set in an explicit IANA zone. |
37
+ | `cron` | `{"prompt":"Check the deploy","title":"Deploy check","cron":{"expression":"*/15 9-17 * * 1-5","time_zone":"Asia/Shanghai"}}` | Five-field Vixie cron expression evaluated in an explicit IANA zone. |
38
+
39
+ Every creation must supply `title`, which names the task in the model views, the task list, the detail heading, and the reminder catalog. The title is trimmed and must remain non-empty and at most 120 characters; a missing, blank, or over-long title returns `invalid_prompt`. Creation never derives a title from the instruction. Decoding requires the stored title too: a record whose `title` is missing, blank after trimming, untrimmed, or over-long is rejected, so records written before titles existed are not read.
40
+
41
+ Daily input accepts `HH:mm:ss` with optional one-to-three fractional digits. It stores normalized `time` and `timeZone` alongside the next UTC `scheduledAt`. The first target is strictly future; missing local times or dates are skipped, and overlaps use only the earlier instant once per date. `every_seconds: 86400` is a fixed interval, not a substitute for daily wall-clock timing. See [daily timing](../../../docs/subsystems/schedule.md#daily-wall-clock-input) for catch-up and time-zone-data limits.
42
+
43
+ Weekly input adds `weekdays`, a non-empty set of ISO weekday numbers from Monday `1` through Sunday `7`. The stored record normalizes the set to unique ascending numbers, so duplicate, out-of-range, and non-integer entries are rejected. The first target is the first strictly future instant whose local date in that zone carries one of the selected weekdays; the same gap skip and earlier-overlap rules as Daily apply per date.
44
+
45
+ Cron input carries `expression` and `time_zone`. The expression is the standard five-field Vixie form `minute hour day-of-month month day-of-week`: minute 0-59, hour 0-23, day-of-month 1-31, month 1-12, and day-of-week 0-7 where both `0` and `7` mean Sunday. Every field accepts `*`, a single value, an `a-b` range, a `*/n` or `a-b/n` step with `n >= 1`, and a comma-separated list of those forms. `L`, `W`, `#`, `JAN`/`MON` names, `@daily`-style macros, six-field expressions, out-of-range values, inverted ranges, zero steps, and empty fields are rejected with `invalid_rule` and a message naming the offending field. Because the dialect has five fields, the smallest interval is one minute. Creation stores a canonical expression: repeated values collapse, adjacent values and ranges merge, a uniform step is written as `a-b/n` or, for a field that started with `*`, as a star-step (`*` for every value, otherwise the widest star-step walk plus any remaining values), a step of `1` is dropped, and Sunday is written as `0`. A field that did not start with `*` never becomes a star-step, so the day rule below survives storage. The durable decoder rejects a non-canonical stored expression, so the record always holds the canonical text. When either day-of-month or day-of-week is a star, a local date matches only when both fields match; when neither is a star, either field matching is enough. A field is a star when its text starts with `*`, independently of the values it matches, so a stepped star constrains alongside the other field instead of substituting for it. The first target is the first strictly future instant whose local date and time in that zone match, using the same gap skip and earlier-overlap rules as Daily. See [cron timing](../../../docs/subsystems/schedule.md#cron-wall-clock-input) for the full dialect and canonicalization rules.
46
+
47
+ A reminder is bound to the calling Agent's Session. The shared `schedule` Remote namespace exposes `catalog` for active and inactive Host tasks with their original Session ids, and `list`, `history`, `update`, and `delete` with an explicit Session id. Remote `list` and model `schedule_list` return only active tasks. Catalog entries retain only the latest receipt in `lastDelivery`; catalog and list responses omit saved delivery history. None of these operations activates an Agent or reads Session logs. Explicit deletion stops future delivery, leaves the original Session and already queued messages intact, and removes the stored task row together with its saved delivery records: the task leaves `list` and `catalog`, never schedules again, and `history` answers `schedule_not_found` for the same `(sessionId, id)`.
48
+
49
+ Archiving a Session with active reminders is refused until they stop, and choosing to stop them deletes every active reminder; unarchiving does not bring them back.
50
+
51
+ `history({sessionId, id, limit, before?})` reads saved deliveries for one stored task. Callers must provide an integer `limit` from 1 through 100; an invalid limit rejects with `invalid_rule`. Records return newest-first in append order, even when wall time moves backward. The optional `before` message-id cursor is exclusive; `nextBefore` is the oldest returned message id and appears only when more saved records remain. A missing task or wrong Session binding returns `schedule_not_found`; an unknown cursor returns `delivery_cursor_not_found`. A successful empty page is distinct from either failure.
52
+
53
+ `update(ScheduleUpdateRequest)` edits an active task's name, instruction, and timing using its original `sessionId` and `id`, the complete `expected: ScheduleRecord` captured before editing, and any combination of optional `title`, optional `prompt`, and an optional discriminated `change` (`at`, `every`, `daily`, `weekly`, or `cron`). A supplied `title` must be non-empty after trimming and at most 120 characters; a supplied `prompt` must be non-empty after trimming. An omitted field keeps its stored value: a supplied name or instruction, or no `change`, keeps the stored rule kind and committed target, while a timing change re-anchors. The change kind may differ from the stored record's kind; every combination is accepted, and the new rule computes its target exactly as creation would from the accepted-save time. Daily, weekly, and cron time or zone changes select the first future target under the same DST skip/earlier-overlap rules; a weekly change also carries the complete weekday set, and a cron change carries its complete expression. A changed Every interval must be a safe integer of at least 60 seconds; its new first target is the Host's accepted-save time plus that interval. An absolute `at` target must be strictly future and stores `kind: "at"` with the same id. One-shot records store only the UTC instant, not the input zone. An equivalent normalized rule within the same kind is a no-op: it performs no write or target reset, an unchanged one-shot keeps its stored `after`/`at` spelling, a cron change compares canonical expressions and zones, and the same Every interval does not reanchor.
54
+
55
+ Every update is a complete-record compare-and-set inside the same FIFO as creation and deletion, and it preserves the task id, original Session binding, status, latest receipt, and complete saved history. Missing tasks or wrong bindings return `schedule_not_found`; inactive tasks return `schedule_ended`. If delivery, target advancement, or another edit changed the expected record, the update returns `schedule_conflict` without overwriting it. Name, instruction, and timing validation errors retain their specific codes; storage failures reject rather than report persisted success. Refresh the catalog and capture a new expected record before retrying a conflict. See the [task page](../../client/ui-schedule/README.md) for the staged form's Save and Cancel behavior.
56
+
57
+ <a id="understand-the-implementation"></a>
58
+ ## Understand the implementation
59
+
60
+ <details>
61
+ <summary>Storage, dispatch, and ownership</summary>
62
+
63
+ `ScheduleService` owns one version-1 `schedule` domain with globally unique task ids and one Host timer. Every task stores its Session binding, record, and `active` or `inactive` status together; only active tasks drive the timer. Stored records without a status normalize to `active`, without scanning or recreating historical Sessions. Management and dispatch writes share one FIFO. Updates compare the complete expected record and sample `Date.now()` inside that queue; one task put changes the rule, name, or instruction without replacing the binding or delivery history. Creation, deletion, and update recheck supplied cancellation after queueing, before persistence; once a write begins, cancellation does not roll it back. The timer rechecks the wall clock, including due members after Session restoration if the clock rolls backward, and segments delays beyond the platform timer limit. Due Every, Daily, Weekly, and Cron tasks for the same Session share one message; each task advances independently after the delivery decision. Successfully advanced tasks remain eligible for the next Host timer even if another batch member fails to persist.
64
+
65
+ Delivery resolves the original Session through `sessionController.resolveAgent`. A plugin-sourced `followup()` synchronously appends the message to the Session inbox; successful Session flush acknowledges durable delivery. One task-row put then updates `lastDelivery`, appends the actual receipt and sent prompt snapshot to `deliveryHistory.records`, and stores the one-shot's `inactive` status or the recurring task's next target. Receipts contain the occurrence's `scheduledAt`, acknowledgment time `deliveredAt`, and `messageId`; they acknowledge inbox delivery, not model execution. Failed flush or task put publishes no new saved record. Session persistence and the task put are separate durable writes; a crash or task-write failure after Session flush can leave a delivered message unrecorded and deliver the same reminder again.
66
+
67
+ The optional version-1 `deliveryHistory` stays in the task row so its oldest-first `records` and `earlierRecordsUnavailable` flag share the status and target commit. New tasks start with empty records and a false flag. Reading a task without history exposes only its existing `lastDelivery`, if any, with no prompt snapshot and a true flag; it neither rewrites the task nor reconstructs missing deliveries or prompts from Session logs or the current prompt. Future appends preserve that true flag and retain the existing receipt. Stored history rejects duplicate message ids or a latest receipt that differs from `lastDelivery`.
68
+
69
+ The optional `earlierRecordsPruned` flag records confirmed removal by an append, remains true after later deliveries and restarts, and is never inferred from `earlierRecordsUnavailable`. Missing flags in existing rows mean pruning is unconfirmed. History responses expose this distinction and the current Host retention limits; reads do not prune records.
70
+
71
+ `schedule/changed` notifies clients after committed task changes. Timing updates request timer recomputation only after the task put commits. Tool and browser consumers use the same service; list and delete read storage directly. Recomputations keep at most one pending timer, including changes during delivery. Shutdown cancels it and drains accepted work before closing the domain. Dispatch admission failures are logged without automatic retry and do not invalidate an already-persisted management result. Storage validation and cleanup registration failures still reject initialization. The Session controller owns Agents it restores.
72
+
73
+ The Schedule domain declares the whole-unit layout because tasks are authoritative. When routed to the JSON backend, an unreadable file, malformed document, unsupported version, or invalid task rejects startup instead of publishing a partial task catalog. Failed recovery leaves `schedule.json` unchanged, while an initially absent file opens as an empty domain. Repairing the file permits reopening the same task identities. Registered startup awaits storage validation and runtime setup through `Service.init`; unloading during open releases the acquired domain without starting dispatch.
74
+
75
+ Historical `schedule/change` events retain `LegacyScheduleRecord` (`after`, `at`, and `every`) in their decoder and fold. The Host record decoder separately accepts `daily`, `weekly`, and `cron`; it preserves committed UTC targets and valid stored zone aliases across canonical-name changes. The Host record decoder requires a stored `title`: a task record whose title is missing, blank after trimming, untrimmed, or longer than 120 characters fails to decode with `ScheduleLogError`. The historical change decoder tolerates an absent `title` so an already-written Session log stays readable, and it applies the same validation when the member is present. The task schema declares no backup-and-skip policy, so one such stored task rejects the whole domain open instead of being dropped. Historical events do not populate the Host task table. Loading a Session with active historical reminders logs a warning to recreate them with `schedule_create`; the Host does not scan historical Sessions, migrate tasks implicitly, or convert existing `at` tasks into daily, weekly, or cron rules.
76
+
77
+ The `schedule.archiveAdmission()` effect answers the Workspace registry's archive admission ([seam](../../workspace/workspace/README.md)) for every Session. Host tasks outlive their Session's Agent, so admission reads the stored rows rather than a live runtime or a Session-log fold: `workspace/session-activity` reports that Session's active Host tasks as the `schedule` family, one item per task with its stored id and title as label, and prepends that family to `next()` so other families keep their entries; `workspace/session-stop` removes those rows in one slot of the same serialized queue the tools use, so the stop is ordered behind a create whose write is still in flight; it deletes the rows directly, because re-entering the public `delete` from inside that queue would deadlock. A Session with no active Host task reports nothing and has nothing to stop.
78
+
79
+ </details>
80
+
81
+ <a id="further-exploration"></a>
82
+ ## Further Exploration
83
+
84
+ - [Schedule domain helpers](src/domain.ts) define selectors, recurrence arithmetic, and reminder framing.
85
+ - [Timing updates](src/update.ts) define expected-record comparison and no-op normalization.
86
+ - [Storage declaration](src/storage.ts) defines durable task validation.
87
+ - [Host runtime](src/runtime.ts) owns timer and enqueue ordering.
88
+ - [Schedule subsystem](../../../docs/subsystems/schedule.md) describes composition and consumers.
89
+
90
+ <a id="model-experience"></a>
91
+ ## Model Experience
92
+
93
+ ### Tool schemas on root Agents
94
+
95
+ #### What the model sees
96
+
97
+ The [generated tool catalog](../../../docs/tool-catalog.md#qilin-agenttool-schedule) contains the descriptions and schemas for `schedule_create`, `schedule_list`, `schedule_delete`, and `schedule_update`, contributed by the tool-schedule package the presets mount.
98
+
99
+ #### Token effect
100
+
101
+ The four schemas contribute fixed request-context tokens while available. Stored tasks and browser catalog queries add no schema tokens.
102
+
103
+ #### KV Cache effect
104
+
105
+ Unchanged schemas preserve their repeated prefix. Loading, unloading, or changing the tool definitions can change request-prefix tokens; provider cache availability remains outside this package.
106
+
107
+ ### Tool results after management calls
108
+
109
+ #### What the model sees
110
+
111
+ Tools render their values as JSON text. Create and update return one reminder view; list returns an array of active reminder views. Update answers with the committed view, or a non-mutating `schedule_not_found`, `schedule_ended`, or `schedule_conflict` miss. Each view contains `id`, `kind`, `title`, `prompt`, `scheduledAt`, `state`, and `deliveryMode: "host"`, plus `afterSeconds`, `everySeconds`, or the wall-clock rule's normalized `time`, stored `timeZone`, and — for `weekly` — ascending `weekdays` or — for `cron` — the canonical `expression`, for the corresponding kind. Delete returns `id` and `deleted`, with `code: "schedule_not_found"` when absent. Failures return `code` and `message`; internal failures use `"The schedule operation failed."`.
112
+
113
+ #### Token effect
114
+
115
+ Result tokens depend on reminder content, list length, or the returned deletion and error fields. Browser-only management does not append tool results.
116
+
117
+ #### KV Cache effect
118
+
119
+ Tool results append to conversation history. Creating, listing, or deleting tasks does not rewrite earlier model-visible messages.
120
+
121
+ ### Due reminders in the original Session
122
+
123
+ #### What the model sees
124
+
125
+ Due reminders enter as user-role messages with producer kind `schedule`. One-shot messages append `schedule_id_json`, `occurrence_at`, and `reminder_prompt_json` after the fixed text below; the id and prompt are JSON-encoded. Recurring batches append `reminders_json`, an array containing `schedule_id`, `occurrence_at`, and `reminder_prompt` for each latest due occurrence.
126
+
127
+ ##### One-shot framing
128
+
129
+ ```markdown
130
+ [SCHEDULE REMINDER]
131
+ This is a scheduled message from the user
132
+ ```
133
+
134
+ ##### Recurring batch framing
135
+
136
+ ```markdown
137
+ [SCHEDULE REMINDER BATCH]
138
+ This is a scheduled message from the user
139
+ ```
140
+
141
+ #### Token effect
142
+
143
+ Each delivery adds fixed framing and content-dependent payload tokens. A recurring batch includes only the latest missed occurrence per task. Storage records and timer checks do not issue model requests.
144
+
145
+ #### KV Cache effect
146
+
147
+ Reminder messages append to the original Session's history and preserve earlier message content; they do not replace the existing request prefix.
148
+
149
+ ## Known Limitations and Deferred Work
150
+
151
+ <a id="known-limitations-and-deferred-work"></a>
152
+
153
+ - The Host must be running to deliver reminders. Failed restoration, enqueue, or persistence leaves tasks stored and reports a warning; there is no automatic retry timer. A later task-management change, another scheduled wake, or Host restart can retry pending tasks.
154
+ - Enqueue and task writes are not atomic, so crash recovery does not guarantee exactly-once delivery. A shutdown can repeat a delivery for the same reason: sibling fibers dispose concurrently, so the storage facility can close before the delivery drain writes its acknowledgment.
155
+ - Deletion removes the task row together with its saved delivery records: future delivery stops, the task leaves `list` and `catalog`, and `history` no longer resolves it.
156
+ - Old Session-log reminders require explicit recreation. Previously physically deleted tasks are not restored or fabricated.
157
+ - Management edits are limited to active tasks. Pause, execution status, delivery outside the original Session, and a new Session for each run are not supported. Name, instruction, and timing updates are available to the model through `schedule_update` for its own Session, and to the Web detail for the selected task; a cross-Session relay workflow is not supported, product permission policy remains undecided, and the Session-binding check is not caller authorization.
158
+ - Cron uses the five-field Vixie dialect, so the smallest interval is one minute and sub-minute scheduling is unsupported. Secondary expressions are not accepted: `L`, `W`, `#`, month or weekday names, `@daily`-style macros, and a seconds field are rejected. The stored record keeps only the canonical expression, so the exact spelling supplied at creation is not retained.
159
+ - Daily, weekly, and cron future targets use the Host's current IANA data; decoding and restarting never recompute an already committed target. Only UTC target years 0001–9999 are supported; exhaustion retains the task as inactive after delivery.
160
+ - Saved delivery records are pruned on append to the configured `deliveryHistoryDays` window, measured back from each receipt's `deliveredAt`, and to the `deliveryHistoryRecords` cap; the appended latest receipt always survives, and a pruned window marks the task's earlier records unavailable. The JSON backend stores the Schedule domain in one `schedule.json` document, so every task mutation rewrites all retained tasks and their histories, and the Host loads all retained history into memory. History pagination bounds returned record count, not storage growth, retained memory, prompt bytes, or write cost.
161
+ - Tasks without saved history expose only their existing latest receipt until new deliveries append records. Unsaved earlier deliveries and prompt snapshots cannot be recovered; saved records do not establish model execution results.
162
+
163
+ <a id="dev-note"></a>
164
+ ### Dev Note
165
+
166
+ <details>
167
+ <summary>Working context for maintainers — click to expand</summary>
168
+
169
+ None.
170
+
171
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,171 @@
1
+ ---
2
+ description: "宿主级持久提醒与按会话绑定的共享任务管理。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @qilin-agent/schedule
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ Schedule 将一次性、固定周期、按每日、按每周以及 cron 本地钟表时间触发的提醒作为后续消息投递到原会话。宿主重启后任务仍然可用,每个重复任务只补发最近一次错过的发生时点。任务到期时,宿主恢复冷会话。活动和未运行任务在显式删除前均可查看,删除会移除任务行及其已保存的发送记录。
13
+
14
+ ## 目录
15
+
16
+ - [使用此包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步阅读](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与后续工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ <a id="use-this-package"></a>
24
+ ## 使用此包
25
+
26
+ 发布的 Web bundle 将此服务与 storage-domain、Session controller 一起挂载。其 `Config` 声明 `deliveryHistoryDays`(默认 30)与 `deliveryHistoryRecords`(默认 200)。存储后端路由由 storage-domain 管理;会话模型与 preset 恢复由 Session controller 管理。Schedule 无法在 headless 或仅 SDK 的组合中单独挂载:投递需要 Host 的 Web Session controller 和 Session 持久化后端,因为只有在 Session 确认 `session/flush` 之后一次投递才会提交。
27
+
28
+ Agent 获得 `schedule_create`、`schedule_list`、`schedule_delete` 和 `schedule_update`。更新原地替换一条提醒的名称、指令或时间,保留其 id 与已保存记录;相对的 `after` 延迟不支持更新。创建时需要非空提示文本、标题,且必须只提供以下六个选择器之一:
29
+
30
+ | 选择器 | 示例 | 时间语义 |
31
+ |---|---|---|
32
+ | `after_seconds` | `{"prompt":"Check the build","title":"Build check","after_seconds":600}` | 正安全整数秒数的延迟。 |
33
+ | `at` | `{"prompt":"Review the release","title":"Release review","at":"2099-01-01T09:00:00+08:00"}` | 严格未来的绝对时点;也接受带显式时区的本地日期时间对象。 |
34
+ | `every_seconds` | `{"prompt":"Check the queue","title":"Queue check","every_seconds":300}` | 至少 60 秒的固定安全整数间隔,初始对齐创建时间。 |
35
+ | `daily` | `{"prompt":"Review today's tasks","title":"Daily review","daily":{"time":"23:00:00","time_zone":"Asia/Shanghai"}}` | 显式 IANA 时区中的本地钟表时间。 |
36
+ | `weekly` | `{"prompt":"Review the week","title":"Weekly review","weekly":{"time":"09:00:00","time_zone":"Asia/Shanghai","weekdays":[1,3]}}` | 显式 IANA 时区中、按显式 ISO 星期集合触发的本地钟表时间。 |
37
+ | `cron` | `{"prompt":"Check the deploy","title":"Deploy check","cron":{"expression":"*/15 9-17 * * 1-5","time_zone":"Asia/Shanghai"}}` | 在显式 IANA 时区中求值的五字段 Vixie cron 表达式。 |
38
+
39
+ 每次创建都必须提供 `title`,用于在模型视图、任务列表、详情标题和提醒目录中命名任务。标题会去除首尾空白,之后必须仍然非空且不超过 120 个字符;缺失、空白或过长时返回 `invalid_prompt`。创建过程绝不从指令派生标题。解码同样要求已存储的标题:`title` 缺失、去除首尾空白后为空、带首尾空白或超过 120 个字符的记录会被拒绝,因此在标题存在之前写入的记录不会被读取。
40
+
41
+ 每日输入接受 `HH:mm:ss` 及可选的一至三位小数秒,将规范化后的 `time`、`timeZone` 与下一 UTC `scheduledAt` 一起存储。首个目标严格晚于当前时间;缺失的本地时间或日期被跳过,重叠时间仅使用较早时点,每个日期一次。`every_seconds: 86400` 是固定间隔,不能替代按每日本地钟表时间触发的规则。补发与时区数据限制见[每日时间语义](../../../docs/subsystems/schedule.zh.md#daily-wall-clock-input)。
42
+
43
+ 每周输入额外接受 `weekdays`,即从周一 `1` 到周日 `7` 的非空 ISO 星期集合。存储记录将集合规范化为唯一且升序的数字,重复、超出范围和非整数项都会被拒绝。首个目标是第一个严格未来的时点,其在该时区中的本地日期属于所选星期之一;每个日期遵循与 Daily 相同的缺口跳过与重叠取较早时点规则。
44
+
45
+ cron 输入携带 `expression` 和 `time_zone`。表达式是标准五字段 Vixie 形式 `minute hour day-of-month month day-of-week`:minute 0-59、hour 0-23、day-of-month 1-31、month 1-12、day-of-week 0-7,其中 `0` 和 `7` 都表示周日。每个字段接受 `*`、单个值、`a-b` 范围、`n >= 1` 的 `*/n` 与 `a-b/n` 步长,以及由这些形式组成的逗号分隔列表。`L`、`W`、`#`、`JAN`/`MON` 名称、`@daily` 风格宏、六字段表达式、超出范围的值、反向范围、零步长和空字段都会以 `invalid_rule` 拒绝,且错误信息会指出违规字段。由于该方言只有五个字段,最小间隔为一分钟。创建时存储规范化表达式:重复值合并,相邻的值与范围合并,均匀步长写为 `a-b/n`,而以 `*` 开头的字段写为星号步长(匹配全部取值时写 `*`,否则写最宽的星号步长并追加其余取值),步长 `1` 被移除,周日统一写为 `0`。不以 `*` 开头的字段绝不会变成星号步长,因此下文的日规则在存储后保持不变。持久化解码器拒绝非规范化的已存储表达式,因此记录始终保存规范化文本。day-of-month 或 day-of-week 中任一为星号字段时,本地日期需两个字段都匹配;两者都不是星号字段时,匹配任一字段即可。字段文本以 `*` 开头即为星号字段,与它匹配的取值集合无关,因此步长星号字段与其他字段同时约束,而不是替代它。首个目标是该时区内本地日期与时间匹配的第一个严格未来时点,使用与 Daily 相同的缺口跳过与重叠取较早时点规则。完整方言与规范化规则见 [cron 时间语义](../../../docs/subsystems/schedule.zh.md#cron-wall-clock-input)。
46
+
47
+ 提醒绑定到调用 Agent 的会话。共享的 `schedule` Remote namespace 提供 `catalog`,返回活动和未运行的宿主任务及其原始会话 id,并提供带明确会话 id 的 `list`、`history`、`update` 和 `delete`。Remote `list` 和模型 `schedule_list` 仅返回活动任务。目录条目的 `lastDelivery` 仅保留最近一次回执;目录和列表响应均不包含已保存的发送历史。这些操作均不激活 Agent 或读取会话日志。显式删除会阻止后续投递,保留原会话和已经入队的消息,并移除已存储的任务行及其已保存的发送记录:任务从 `list` 和 `catalog` 中消失、不再调度,`history` 对相同的 `(sessionId, id)` 返回 `schedule_not_found`。
48
+
49
+ 归档仍有活动提醒的会话会被拒绝,直到这些提醒停止;选择停止它们会删除全部活动提醒,取消归档不会把它们带回来。
50
+
51
+ `history({sessionId, id, limit, before?})` 读取一个已存储任务的已保存发送记录。调用方必须提供 1 至 100 的整数 `limit`;非法值以 `invalid_rule` 拒绝。记录按追加顺序从新到旧返回,即使实际时间回拨也不改变顺序。可选的 `before` 消息 id 游标不包含自身;`nextBefore` 是本页最早记录的消息 id,仅在还有更早的已保存记录时出现。任务不存在或会话绑定不符时返回 `schedule_not_found`;未知游标返回 `delivery_cursor_not_found`。查询成功的空页与这两类失败相互区分。
52
+
53
+ `update(ScheduleUpdateRequest)` 使用活动任务的原始 `sessionId` 和 `id`、编辑前捕获的完整 `expected: ScheduleRecord`,以及可选的 `title`、可选的 `prompt` 和可选的带判别字段的 `change`(`at`、`every`、`daily`、`weekly` 或 `cron`)的任意组合,修改任务名称、指令和时间。提供的 `title` 去除首尾空白后必须非空且不超过 120 个字符;提供的 `prompt` 去除首尾空白后必须非空。省略的字段保留已存储的值:提供名称或指令,或省略 `change`,都保留已存储的规则种类和已提交目标,而时间变更会重新确定起算时间。change 的种类可与已存记录的种类不同;所有组合均被接受,新规则按创建时相同的方式、以接受保存的时间计算目标。Daily、Weekly 与 Cron 的时间或时区变化时,按相同的夏令时缺失跳过、重叠选较早时点规则选择首个未来目标;Weekly 变更还会携带完整的星期集合,Cron 变更携带完整表达式。Every 的新间隔必须是至少 60 秒的安全整数;新的首个目标为宿主接受保存的时间加上该间隔。绝对 `at` 目标必须严格未来,并以相同 id 保存为 `kind: "at"`。一次性记录只存储 UTC 时点,不保留输入时区。同一种类内规范化后等价的规则不产生变更:不写入或重设目标,目标不变的一次性记录保留原 `after`/`at` 拼写,Cron 变更比较规范化表达式与时区,相同的 Every 间隔也不会重新确定起算时间。
54
+
55
+ 每次更新都是与创建、删除共用同一 FIFO 的完整记录 compare-and-set,并保留任务 id、原会话绑定、状态、最近一次回执和全部已保存历史。任务不存在或绑定不符返回 `schedule_not_found`;未运行任务返回 `schedule_ended`。若投递、目标推进或其他编辑改变了预期记录,更新返回 `schedule_conflict`,不覆盖该记录。名称、指令和时间校验错误保留各自的错误码;存储失败会拒绝,而非报告持久化成功。冲突后应刷新目录并重新捕获预期记录,再尝试修改。分阶段表单的保存和取消行为见[任务页面](../../client/ui-schedule/README.zh.md)。
56
+
57
+ <a id="understand-the-implementation"></a>
58
+ ## 理解实现
59
+
60
+ <details>
61
+ <summary>存储、投递与所有权</summary>
62
+
63
+ `ScheduleService` 拥有一个版本 1 的 `schedule` domain、全局唯一的任务 id 和一个宿主定时器。每条任务同时存储会话绑定、记录及 `active` 或 `inactive` 状态;仅活动任务驱动定时器。已存储但缺少状态的记录规范化为 `active`,不会扫描或重建历史会话。管理写入与投递写入共用一个 FIFO。更新在队列内比较完整预期记录并采样 `Date.now()`;一次任务 put 修改规则、名称或指令,不替换绑定或发送历史。创建、删除和更新在排队结束、持久化开始前重新检查传入的取消信号;写入一旦开始,取消不会将其回滚。定时器重新读取实际时间,包括时钟回拨时在会话恢复后重新核对到期成员,并分段处理超过平台定时器上限的延迟。同一会话到期的 Every、Daily、Weekly 和 Cron 任务合并为一条消息;每条任务在投递判断后分别推进。即使批次中另一条任务持久化失败,已成功推进的任务仍参与宿主下一次定时计算。
64
+
65
+ 投递通过 `sessionController.resolveAgent` 解析原会话。插件来源的 `followup()` 同步将消息追加到会话收件箱;会话 flush 成功后确认持久投递。随后一次任务行 put 更新 `lastDelivery`,将实际回执与已发送提示文本快照追加到 `deliveryHistory.records`,并保存单次任务的 `inactive` 状态或周期任务的下一目标时间。回执包含发生时点 `scheduledAt`、确认时间 `deliveredAt` 和 `messageId`;它确认收件箱投递,不表示模型执行。flush 或任务 put 失败均不发布新的已保存记录。会话持久化与任务 put 是两次独立的持久写入;会话 flush 后崩溃或任务写入失败可能留下未记入发送记录的已投递消息,并再次投递相同提醒。
66
+
67
+ 版本 1 的可选 `deliveryHistory` 保留在任务行中,其从旧到新排列的 `records` 和 `earlierRecordsUnavailable` 标志与状态及目标时间共同提交。新任务的记录为空,标志为 false。读取没有历史的任务时,仅呈现其已有的 `lastDelivery`(若存在),不含提示文本快照,标志为 true;读取不重写任务,也不从会话日志或当前提示文本重建缺失的投递或提示文本。后续追加保留 true 标志及已有回执。存储历史拒绝重复消息 id,以及与 `lastDelivery` 不一致的最新回执。
68
+
69
+ 可选的 `earlierRecordsPruned` 标志记录追加时确实删除过旧记录的事实,在后续投递和重启后保持为 true,不从 `earlierRecordsUnavailable` 推断。已有任务行缺少此标志时,裁剪情况视为未确认。历史接口返回该标志及当前宿主保留上限;读取不裁剪记录。
70
+
71
+ `schedule/changed` 在任务变更提交后通知客户端。时间更新仅在任务 put 提交后请求重新计算定时器。工具与浏览器使用同一服务;查询和删除直接读取存储。重新计算后至多保留一个待触发定时器,包括投递期间发生的变更。关闭时取消它,等待已接受的工作结束,再关闭 domain。投递调度准入失败会记录日志而不自动重试,不会使已持久化的管理结果失效。存储校验和清理注册失败仍会拒绝初始化。恢复的 Agent 由 Session controller 拥有。
72
+
73
+ Schedule domain 声明整 unit 布局,因为任务是权威数据。路由到 JSON 后端时,文件不可读、文档损坏、版本不受支持或任务非法都会拒绝启动,而不是发布部分任务目录。恢复失败不会改写 `schedule.json`,初始不存在的文件则作为空 domain 打开;修复该文件后可按相同任务身份重新打开。注册启动通过 `Service.init` 等待存储校验与运行时初始化;打开期间卸载会释放已取得的 domain,而不开始投递。
74
+
75
+ 历史 `schedule/change` 事件在解码器和 fold 中保留 `LegacyScheduleRecord`(`after`、`at` 和 `every`)。宿主记录解码器单独接受 `daily`、`weekly` 和 `cron`,保留已提交的 UTC 目标,并在规范名称变化后继续接受有效的已存储时区别名。宿主记录解码器要求已存储的 `title`:标题缺失、去除首尾空白后为空、带首尾空白或超过 120 个字符的任务记录都以 `ScheduleLogError` 拒绝解码。历史变更解码器容忍缺失的 `title`,以便已写入的 Session 日志仍可读取;该成员存在时按同样的规则校验。任务 schema 未声明备份并跳过的策略,因此一条这样的已存储任务会拒绝整个 domain 的打开,而不是被丢弃。历史事件不填充宿主任务表。加载含有活动历史提醒的会话时,日志会提示通过 `schedule_create` 重新创建;宿主不扫描历史会话、不隐式迁移任务,也不将已有 `at` 任务转换为每日、每周或 cron 规则。
76
+
77
+ `schedule.archiveAdmission()` effect 为每个会话回答 Workspace 注册表的归档准入([接缝](../../workspace/workspace/README.zh.md))。宿主任务比其会话的 Agent 活得更久,因此准入读取已存储的行,而不是活 runtime 或会话日志 fold:`workspace/session-activity` 把该会话的活动宿主任务作为 `schedule` 族报告,每条任务一项、以其已存储 id 与 title 作名称,并把该族前插到 `next()` 的结果之前,使其他族保留各自条目;`workspace/session-stop` 在与工具相同的串行队列的一个槽位里删除这些行,因此停止会排在写入尚未完成的创建之后;它直接删行,因为在队列内重入公开的 `delete` 会自锁。没有活动宿主任务的会话不报告任何内容,也没有可停的提醒。
78
+
79
+ </details>
80
+
81
+ <a id="further-exploration"></a>
82
+ ## 进一步阅读
83
+
84
+ - [Schedule 领域函数](src/domain.ts) 定义选择器、周期计算与提醒正文。
85
+ - [时间更新](src/update.ts) 定义预期记录比较与无变更规范化。
86
+ - [存储声明](src/storage.ts) 定义持久任务校验。
87
+ - [宿主运行时](src/runtime.ts) 拥有定时器与入队顺序。
88
+ - [Schedule 子系统](../../../docs/subsystems/schedule.zh.md) 描述组装与消费者。
89
+
90
+ <a id="model-experience"></a>
91
+ ## 模型体验
92
+
93
+ ### 根 Agent 的工具 schema
94
+
95
+ #### 模型看到什么
96
+
97
+ [生成的工具目录](../../../docs/tool-catalog.zh.md#qilin-agenttool-schedule) 包含 `schedule_create`、`schedule_list`、`schedule_delete` 和 `schedule_update` 的描述与 schema;这些工具由 preset 挂载的 tool-schedule 包提供。
98
+
99
+ #### Token 影响
100
+
101
+ 四个 schema 在可用期间向请求上下文贡献固定的 token。已存储任务和浏览器目录查询不增加 schema token。
102
+
103
+ #### KV Cache 影响
104
+
105
+ 未变更的 schema 保留重复使用的前缀。加载、卸载或修改工具定义可能改变请求前缀中的 token;提供方的缓存可用性不由本包保证。
106
+
107
+ ### 管理调用后的工具结果
108
+
109
+ #### 模型看到什么
110
+
111
+ 工具将返回值渲染为 JSON 文本。创建与更新各返回一条提醒视图;列举返回活动提醒的视图数组。更新返回已提交的视图,或 `schedule_not_found`、`schedule_ended`、`schedule_conflict` 这类不改动存储的未命中结果。每个视图包含 `id`、`kind`、`title`、`prompt`、`scheduledAt`、`state` 和 `deliveryMode: "host"`,对应规则种类还包含 `afterSeconds`、`everySeconds`,或钟表规则规范化后的 `time` 和存储的 `timeZone`(`weekly` 另含升序 `weekdays`,`cron` 另含规范化 `expression`)。删除返回 `id` 和 `deleted`,任务不存在时带有 `code: "schedule_not_found"`。失败返回 `code` 和 `message`;内部失败使用 `"The schedule operation failed."`。
112
+
113
+ #### Token 影响
114
+
115
+ 结果 token 取决于提醒内容、列表长度或返回的删除与错误字段。仅在浏览器中执行的管理操作不追加工具结果。
116
+
117
+ #### KV Cache 影响
118
+
119
+ 工具结果追加到对话历史。创建、列举或删除任务不重写既有模型可见消息。
120
+
121
+ ### 原会话中的到期提醒
122
+
123
+ #### 模型看到什么
124
+
125
+ 到期提醒以生产者 kind 为 `schedule` 的 user-role 消息进入会话。单次提醒在下方固定文本之后追加 `schedule_id_json`、`occurrence_at` 和 `reminder_prompt_json`;id 和提示词使用 JSON 编码。周期提醒批次追加 `reminders_json` 数组,每个最新到期时点包含 `schedule_id`、`occurrence_at` 和 `reminder_prompt`。
126
+
127
+ ##### 单次提醒固定文本
128
+
129
+ ```markdown
130
+ [SCHEDULE REMINDER]
131
+ This is a scheduled message from the user
132
+ ```
133
+
134
+ ##### 周期提醒批次固定文本
135
+
136
+ ```markdown
137
+ [SCHEDULE REMINDER BATCH]
138
+ This is a scheduled message from the user
139
+ ```
140
+
141
+ #### Token 影响
142
+
143
+ 每次投递增加固定文本和取决于内容的载荷 token。周期提醒批次仅包含每条任务最近一次错过的触发时点。存储记录和定时检查不发起模型请求。
144
+
145
+ #### KV Cache 影响
146
+
147
+ 提醒消息追加到原会话历史并保留既有消息内容,不替换已有请求前缀。
148
+
149
+ ## 已知限制与后续工作
150
+
151
+ <a id="known-limitations-and-deferred-work"></a>
152
+
153
+ - 宿主必须运行才能投递提醒。恢复、入队或持久化失败时保留任务并记录警告,没有自动重试定时器。后续任务管理变更、其他计划唤醒或宿主重启可重试未完成投递。
154
+ - 入队与任务写入不具有原子性,因此崩溃恢复不保证恰好投递一次。关闭宿主也会出于同一原因重复投递:同级的 fiber 并发拆卸,存储 facility 可能先于投递排空的确认写入而关闭。
155
+ - 删除会移除任务行及其已保存的投递记录:后续投递停止,任务离开 `list` 与 `catalog`,`history` 也不再解析到它。
156
+ - 旧会话日志提醒需要明确重新创建。此前已物理删除的任务不会被恢复或虚构。
157
+ - 仅可修改活动任务的管理字段。不支持暂停、执行状态、原会话以外的投递或每次运行新建会话。名称、指令和时间更新可由模型通过 `schedule_update` 修改其自身 Session 内的提醒,也可在 Web 详情中对所选任务修改;不支持跨会话转交工作流,产品权限策略尚未确定,会话绑定校验不等于调用者鉴权。
158
+ - Cron 使用五字段 Vixie 方言,因此最小间隔为一分钟,不支持亚分钟级调度。不接受扩展表达式:`L`、`W`、`#`、月份或星期名称、`@daily` 风格宏以及秒字段都会被拒绝。存储记录只保留规范化表达式,创建时提供的原始拼写不会被保留。
159
+ - Daily、Weekly 与 Cron 的未来目标使用宿主当前的 IANA 数据;解码和重启绝不重新计算已提交的目标。UTC 目标仅支持 0001–9999 年;没有后续目标时,任务在投递后保留为未运行状态。
160
+ - 已保存的发送记录在追加确认时按配置的 `deliveryHistoryDays` 窗口(以每条回执的 `deliveredAt` 向前计算)与 `deliveryHistoryRecords` 条数上限裁剪;新追加的最近一次回执始终保留,发生裁剪时任务标记更早记录不可用。JSON 后端把 Schedule domain 存在一个 `schedule.json` 文档中,因此每次任务变更都会重写全部保留任务及其历史,宿主也会把全部保留历史加载到内存。历史分页仅限制返回的记录数,不限制存储增长、保留的内存、提示文本字节数或写入成本。
161
+ - 没有已保存历史的任务仅呈现已有的最近一次回执,直到新投递追加记录。未保存的更早投递和提示文本快照无法恢复;已保存的记录不证明模型执行结果。
162
+
163
+ <a id="dev-note"></a>
164
+ ### 开发备注
165
+
166
+ <details>
167
+ <summary>维护者工作上下文 — 点击展开</summary>
168
+
169
+ 无。
170
+
171
+ </details>