@zhushanwen/pi-scheduler 0.4.2 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-scheduler",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "xyz-agent": {
@@ -29,7 +29,7 @@
29
29
  "vitest.config.ts"
30
30
  ],
31
31
  "peerDependencies": {
32
- "@earendil-works/pi-coding-agent": "^0.84.1",
32
+ "@earendil-works/pi-coding-agent": "^0.84.4",
33
33
  "croner": "^9.0.0",
34
34
  "typebox": "*"
35
35
  },
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@xyz-agent/session-delivery": "0.3.0",
49
- "@zhushanwen/pi-extension-logger": "0.3.0"
49
+ "@zhushanwen/pi-extension-logger": "0.3.1"
50
50
  },
51
51
  "scripts": {
52
52
  "test": "vitest run",
package/src/format.ts CHANGED
@@ -1,6 +1,11 @@
1
- import { formatDuration } from './parsing.js'
1
+ import { formatDuration, MS_PER_DAY, MS_PER_HOUR, MS_PER_MINUTE, MS_PER_SECOND } from './parsing.js'
2
2
  import type { ScheduleSpec, TaskKind } from './types.js'
3
3
 
4
+ /** 相对时间显示的"现在"判定窗口(±5s 内视为 now)。 */
5
+ const NOW_THRESHOLD_MS = 5000
6
+ /** 省略号 "..." 的字符数(truncate 截断预留宽度)。 */
7
+ const ELLIPSIS_LENGTH = 3
8
+
4
9
  /** Format ScheduleSpec to readable string. kind 区分 once/recurring(once 显示 'once in X' 而非误导性的 'every X')。 */
5
10
  export function formatSchedule(spec: ScheduleSpec, kind?: TaskKind): string {
6
11
  if (spec.mode === 'interval') {
@@ -25,14 +30,14 @@ export function formatRelativeTime(timestamp: number, now?: number): string {
25
30
  const diff = timestamp - currentTime
26
31
 
27
32
  // 5秒内视为"现在"
28
- if (Math.abs(diff) < 5000) return 'now'
33
+ if (Math.abs(diff) < NOW_THRESHOLD_MS) return 'now'
29
34
 
30
35
  const absDiff = Math.abs(diff)
31
36
  const units: [string, number][] = [
32
- ['d', 86_400_000],
33
- ['h', 3_600_000],
34
- ['m', 60_000],
35
- ['s', 1000],
37
+ ['d', MS_PER_DAY],
38
+ ['h', MS_PER_HOUR],
39
+ ['m', MS_PER_MINUTE],
40
+ ['s', MS_PER_SECOND],
36
41
  ]
37
42
 
38
43
  let formatted = ''
@@ -45,7 +50,7 @@ export function formatRelativeTime(timestamp: number, now?: number): string {
45
50
  }
46
51
 
47
52
  if (!formatted) {
48
- formatted = `${Math.round(absDiff / 1000)}s`
53
+ formatted = `${Math.round(absDiff / MS_PER_SECOND)}s`
49
54
  }
50
55
 
51
56
  return diff > 0 ? `in ${formatted}` : `${formatted} ago`
@@ -56,24 +61,35 @@ export function formatRelativeTime(timestamp: number, now?: number): string {
56
61
  */
57
62
  export function truncate(text: string, maxLen: number): string {
58
63
  if (text.length <= maxLen) return text
59
- if (maxLen <= 3) return text.slice(0, maxLen)
60
- return text.slice(0, maxLen - 3) + '...'
64
+ if (maxLen <= ELLIPSIS_LENGTH) return text.slice(0, maxLen)
65
+ return text.slice(0, maxLen - ELLIPSIS_LENGTH) + '...'
61
66
  }
62
67
 
68
+ /** 生成任务 ID 的随机字节数(8 位 hex = 4 字节)。 */
69
+ const TASK_ID_RANDOM_BYTES = 4
70
+ const HEX_RADIX = 16
71
+ /** 每字节展开的 hex 字符数(padStart 宽度)。 */
72
+ const HEX_CHARS_PER_BYTE = 2
73
+
63
74
  /**
64
75
  * 生成任务 ID:8 位 hex。
65
76
  */
66
77
  export function generateTaskId(): string {
67
- const bytes = new Uint8Array(4)
78
+ const bytes = new Uint8Array(TASK_ID_RANDOM_BYTES)
68
79
  crypto.getRandomValues(bytes)
69
- return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
80
+ return Array.from(bytes, b => b.toString(HEX_RADIX).padStart(HEX_CHARS_PER_BYTE, '0')).join('')
70
81
  }
71
82
 
83
+ /** autoName 任务名最大长度。 */
84
+ const AUTO_NAME_MAX_LENGTH = 30
85
+ /** 截断后保留的长度(预留省略号宽度)。 */
86
+ const AUTO_NAME_KEEP_LENGTH = AUTO_NAME_MAX_LENGTH - ELLIPSIS_LENGTH
87
+
72
88
  /**
73
89
  * 从 prompt 自动生成任务名称:取前 30 字。
74
90
  */
75
91
  export function autoName(prompt: string): string {
76
92
  const trimmed = prompt.trim()
77
- if (trimmed.length <= 30) return trimmed
78
- return trimmed.slice(0, 27) + '...'
93
+ if (trimmed.length <= AUTO_NAME_MAX_LENGTH) return trimmed
94
+ return trimmed.slice(0, AUTO_NAME_KEEP_LENGTH) + '...'
79
95
  }
package/src/parsing.ts CHANGED
@@ -2,13 +2,19 @@ import type { ParseScheduleResult, ScheduleSpec } from './types.js'
2
2
 
3
3
  // ── Duration 解析 ──
4
4
 
5
+ // 时间单位毫秒数(包内共享:format.ts / runtime.ts 复用)
6
+ export const MS_PER_DAY = 86_400_000
7
+ export const MS_PER_HOUR = 3_600_000
8
+ export const MS_PER_MINUTE = 60_000
9
+ export const MS_PER_SECOND = 1000
10
+
5
11
  const DURATION_RE = /^(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?|d|days?)$/i
6
12
 
7
13
  const DURATION_MULTIPLIERS: Record<string, number> = {
8
- s: 1000, sec: 1000, second: 1000, seconds: 1000,
9
- m: 60_000, min: 60_000, minute: 60_000, minutes: 60_000,
10
- h: 3_600_000, hr: 3_600_000, hour: 3_600_000, hours: 3_600_000,
11
- d: 86_400_000, day: 86_400_000, days: 86_400_000,
14
+ s: MS_PER_SECOND, sec: MS_PER_SECOND, second: MS_PER_SECOND, seconds: MS_PER_SECOND,
15
+ m: MS_PER_MINUTE, min: MS_PER_MINUTE, minute: MS_PER_MINUTE, minutes: MS_PER_MINUTE,
16
+ h: MS_PER_HOUR, hr: MS_PER_HOUR, hour: MS_PER_HOUR, hours: MS_PER_HOUR,
17
+ d: MS_PER_DAY, day: MS_PER_DAY, days: MS_PER_DAY,
12
18
  }
13
19
 
14
20
  /**
@@ -34,10 +40,10 @@ export function formatDuration(ms: number): string {
34
40
  if (ms <= 0) return '0s'
35
41
 
36
42
  const units: [string, number][] = [
37
- ['d', 86_400_000],
38
- ['h', 3_600_000],
39
- ['m', 60_000],
40
- ['s', 1000],
43
+ ['d', MS_PER_DAY],
44
+ ['h', MS_PER_HOUR],
45
+ ['m', MS_PER_MINUTE],
46
+ ['s', MS_PER_SECOND],
41
47
  ]
42
48
 
43
49
  for (const [suffix, divisor] of units) {
@@ -47,11 +53,18 @@ export function formatDuration(ms: number): string {
47
53
  }
48
54
 
49
55
  // 兜底:用秒表示
50
- return `${Math.round(ms / 1000)}s`
56
+ return `${Math.round(ms / MS_PER_SECOND)}s`
51
57
  }
52
58
 
53
59
  // ── Cron 解析 ──
54
60
 
61
+ /** cron 标准字段数(分 时 日 月 周)。 */
62
+ const CRON_FIELD_COUNT = 5
63
+ /** 带秒字段的标准 cron 字段数(秒 分 时 日 月 周)。 */
64
+ const CRON_FIELD_COUNT_WITH_SECONDS = 6
65
+ /** computeNextCronRuns / computeNextRuns 默认返回的未来执行时间数。 */
66
+ const DEFAULT_NEXT_RUNS_COUNT = 5
67
+
55
68
  let cronerModule: typeof import('croner') | null | undefined
56
69
 
57
70
  async function getCroner(): Promise<typeof import('croner') | null> {
@@ -76,12 +89,12 @@ export function normalizeCronExpression(input: string): { expression: string; no
76
89
  const parts = trimmed.split(/\s+/)
77
90
 
78
91
  // 6 字段原样返回
79
- if (parts.length === 6) {
92
+ if (parts.length === CRON_FIELD_COUNT_WITH_SECONDS) {
80
93
  return { expression: trimmed }
81
94
  }
82
95
 
83
96
  // 5 字段补秒字段
84
- if (parts.length === 5) {
97
+ if (parts.length === CRON_FIELD_COUNT) {
85
98
  return {
86
99
  expression: `0 ${trimmed}`,
87
100
  note: 'Auto-prepended seconds field (0)',
@@ -121,7 +134,7 @@ export async function computeNextCronRunAt(
121
134
  export async function computeNextCronRuns(
122
135
  expression: string,
123
136
  from?: number,
124
- count = 5,
137
+ count = DEFAULT_NEXT_RUNS_COUNT,
125
138
  ): Promise<number[]> {
126
139
  const croner = await getCroner()
127
140
  if (!croner) return []
@@ -215,7 +228,7 @@ export async function computeNextRunAt(
215
228
  export async function computeNextRuns(
216
229
  spec: ScheduleSpec,
217
230
  from?: number,
218
- count = 5,
231
+ count = DEFAULT_NEXT_RUNS_COUNT,
219
232
  ): Promise<number[]> {
220
233
  if (spec.mode === 'interval') {
221
234
  const start = from ?? Date.now()
package/src/runtime.ts CHANGED
@@ -5,7 +5,7 @@ import type { DeliveryHandle, DeliveryMessage } from '@xyz-agent/session-deliver
5
5
 
6
6
  import type { SchedulerBackend } from './backend.js'
7
7
  import { autoName, generateTaskId } from './format.js'
8
- import { computeNextRunAt, parseDuration } from './parsing.js'
8
+ import { computeNextRunAt, MS_PER_DAY, MS_PER_MINUTE, parseDuration } from './parsing.js'
9
9
  import type {
10
10
  AddOptions,
11
11
  ScheduledTask,
@@ -17,11 +17,13 @@ import type {
17
17
  const logger = getLogger('scheduler')
18
18
 
19
19
  const MAX_TASKS = 50
20
- // 入队防重标记 TTL(合批非首条任务无终态回调,过期后放行重投;10 min >> 合批窗口)
21
- const QUEUE_DEDUPE_TTL_MS = 10 * 60 * 1000
20
+ // 入队防重标记 TTL 分钟数(合批非首条任务无终态回调,过期后放行重投;10 min >> 合批窗口)
21
+ const QUEUE_DEDUPE_TTL_MINUTES = 10
22
+ const QUEUE_DEDUPE_TTL_MS = QUEUE_DEDUPE_TTL_MINUTES * MS_PER_MINUTE
22
23
  const RATE_LIMIT_PER_MINUTE = 6
23
24
  const TICK_INTERVAL_MS = 30_000
24
- const DEFAULT_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
25
+ const DEFAULT_EXPIRY_DAYS = 7
26
+ const DEFAULT_EXPIRY_MS = DEFAULT_EXPIRY_DAYS * MS_PER_DAY // 7 days
25
27
  const HISTORY_LIMIT = 20 // 与 replayFoldEntries 的裁剪上限一致(advance 折叠 / dispatch 累积共用)
26
28
  // pi ExtensionRunner 在 session 替换后访问 stale ctx 时抛出的错误文案片段。
27
29
  // 兜底通道(防御纵深):G1 模块级代际检测(isCtxStale)为主判,覆盖同模块环境内的 session
@@ -466,7 +468,7 @@ export class SchedulerRuntime {
466
468
  }
467
469
 
468
470
  private hasDispatchCapacity(now: number): boolean {
469
- const oneMinuteAgo = now - 60_000
471
+ const oneMinuteAgo = now - MS_PER_MINUTE
470
472
  this.dispatchTimestamps = this.dispatchTimestamps.filter(t => t > oneMinuteAgo)
471
473
  return this.dispatchTimestamps.length < RATE_LIMIT_PER_MINUTE
472
474
  }
package/src/widget.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { formatRelativeTime, truncate } from './format.js'
2
2
  import type { ScheduledTask } from './types.js'
3
3
 
4
+ /** widget 中任务名的最大显示宽度(列)。 */
5
+ const WIDGET_NAME_MAX_WIDTH = 20
6
+
4
7
  /**
5
8
  * 渲染 TUI status bar widget(string[],配合 SDK setWidget 第一重载)。
6
9
  * 格式:[scheduler] 3 scheduled · check-build in 4m · 1 overdue
@@ -23,7 +26,7 @@ export function renderSchedulerWidget(tasks: ScheduledTask[]): string[] {
23
26
 
24
27
  if (upcoming.length > 0) {
25
28
  const next = upcoming[0]!
26
- parts.push(`${truncate(next.name, 20)} ${formatRelativeTime(next.nextRunAt)}`)
29
+ parts.push(`${truncate(next.name, WIDGET_NAME_MAX_WIDTH)} ${formatRelativeTime(next.nextRunAt)}`)
27
30
  }
28
31
 
29
32
  if (overdue.length > 0) {