@openclaw-china/shared 0.1.22 → 0.1.23

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": "@openclaw-china/shared",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -0,0 +1,115 @@
1
+ const CRON_HIDDEN_PROMPT = `
2
+ When creating or updating a cron task, use the main session's last route as the delivery target.
3
+ - Do not ask the user for userId/chatId
4
+ - sessionTarget="isolated"
5
+ - payload.kind="agentTurn"
6
+ - payload.deliver=true
7
+ - Do not set payload.channel/to (fall back to last route automatically)
8
+ - Do not call the message tool to send`;
9
+
10
+ const CRON_TRIGGER_KEYWORDS = [
11
+ "定时",
12
+ "提醒",
13
+ "每分钟",
14
+ "每小时",
15
+ "每天",
16
+ "每周",
17
+ "几点",
18
+ "早上",
19
+ "晚上",
20
+ "工作日",
21
+ "cron",
22
+ "remind",
23
+ "reminder",
24
+ "schedule",
25
+ "scheduled",
26
+ "every minute",
27
+ "every hour",
28
+ "every day",
29
+ "daily",
30
+ "every week",
31
+ "weekly",
32
+ "weekday",
33
+ "workday",
34
+ "morning",
35
+ "evening",
36
+ ];
37
+
38
+ const CRON_TRIGGER_PATTERNS = [
39
+ /提醒我/u,
40
+ /帮我定时/u,
41
+ /每.+提醒/u,
42
+ /每天.+发/u,
43
+ /remind me/iu,
44
+ /set (a )?reminder/iu,
45
+ /every .+ remind/iu,
46
+ /every day .+ (send|post|notify)/iu,
47
+ /schedule .+ (reminder|message|notification)/iu,
48
+ ];
49
+
50
+ const CRON_EXCLUDE_PATTERNS = [
51
+ /是什么意思/u,
52
+ /区别/u,
53
+ /为什么/u,
54
+ /\bhelp\b/iu,
55
+ /文档/u,
56
+ /怎么用/u,
57
+ /what does|what's|meaning of/iu,
58
+ /difference/iu,
59
+ /why/iu,
60
+ /\bdocs?\b/iu,
61
+ /documentation/iu,
62
+ /how to/iu,
63
+ /usage/iu,
64
+ ];
65
+
66
+ export function shouldInjectCronHiddenPrompt(text: string): boolean {
67
+ const normalized = text.trim();
68
+ if (!normalized) return false;
69
+ const lowered = normalized.toLowerCase();
70
+
71
+ for (const pattern of CRON_EXCLUDE_PATTERNS) {
72
+ if (pattern.test(lowered)) return false;
73
+ }
74
+
75
+ for (const keyword of CRON_TRIGGER_KEYWORDS) {
76
+ if (lowered.includes(keyword.toLowerCase())) return true;
77
+ }
78
+
79
+ return CRON_TRIGGER_PATTERNS.some((pattern) => pattern.test(normalized));
80
+ }
81
+
82
+ export function splitCronHiddenPrompt(text: string): { base: string; prompt?: string } {
83
+ const idx = text.indexOf(CRON_HIDDEN_PROMPT);
84
+ if (idx === -1) {
85
+ return { base: text };
86
+ }
87
+ const base = text.slice(0, idx).trimEnd();
88
+ return { base, prompt: CRON_HIDDEN_PROMPT };
89
+ }
90
+
91
+ export function appendCronHiddenPrompt(text: string): string {
92
+ if (!shouldInjectCronHiddenPrompt(text)) return text;
93
+ if (text.includes(CRON_HIDDEN_PROMPT)) return text;
94
+ return `${text}\n\n${CRON_HIDDEN_PROMPT}`;
95
+ }
96
+
97
+ export function applyCronHiddenPromptToContext<
98
+ T extends { Body?: string; RawBody?: string; CommandBody?: string }
99
+ >(ctx: T): boolean {
100
+ const base =
101
+ (typeof ctx.RawBody === "string" && ctx.RawBody) ||
102
+ (typeof ctx.Body === "string" && ctx.Body) ||
103
+ (typeof ctx.CommandBody === "string" && ctx.CommandBody) ||
104
+ "";
105
+
106
+ if (!base) return false;
107
+
108
+ const next = appendCronHiddenPrompt(base);
109
+ if (next === base) return false;
110
+
111
+ ctx.CommandBody = next;
112
+ return true;
113
+ }
114
+
115
+ export { CRON_HIDDEN_PROMPT };
package/src/index.ts CHANGED
@@ -5,5 +5,6 @@ export * from "./logger/index.js";
5
5
  export * from "./policy/index.js";
6
6
  export * from "./http/index.js";
7
7
  export * from "./types/common.js";
8
- export * from "./file/index.js";
9
- export * from "./media/index.js";
8
+ export * from "./file/index.js";
9
+ export * from "./media/index.js";
10
+ export * from "./cron/index.js";
@@ -63,20 +63,20 @@ export interface MediaParseResult {
63
63
  /**
64
64
  * 媒体解析选项
65
65
  */
66
- export interface MediaParseOptions {
67
- /** 是否从文本中移除媒体标记,默认 true */
68
- removeFromText?: boolean;
69
- /** 是否检查本地文件存在性,默认 false */
70
- checkExists?: boolean;
71
- /** 文件存在性检查函数(用于依赖注入) */
72
- existsSync?: (path: string) => boolean;
73
- /** 是否解析行首 MEDIA: 指令,默认 false */
74
- parseMediaLines?: boolean;
75
- /** 是否解析 Markdown 图片,默认 true */
76
- parseMarkdownImages?: boolean;
77
- /** 是否解析 HTML img 标签,默认 true */
78
- parseHtmlImages?: boolean;
79
- /** 是否解析裸露的本地路径,默认 true */
66
+ export interface MediaParseOptions {
67
+ /** 是否从文本中移除媒体标记,默认 true */
68
+ removeFromText?: boolean;
69
+ /** 是否检查本地文件存在性,默认 false */
70
+ checkExists?: boolean;
71
+ /** 文件存在性检查函数(用于依赖注入) */
72
+ existsSync?: (path: string) => boolean;
73
+ /** 是否解析行首 MEDIA: 指令,默认 false */
74
+ parseMediaLines?: boolean;
75
+ /** 是否解析 Markdown 图片,默认 true */
76
+ parseMarkdownImages?: boolean;
77
+ /** 是否解析 HTML img 标签,默认 true */
78
+ parseHtmlImages?: boolean;
79
+ /** 是否解析裸露的本地路径,默认 true */
80
80
  parseBarePaths?: boolean;
81
81
  /** 是否解析 Markdown 链接中的文件,默认 true */
82
82
  parseMarkdownLinks?: boolean;
@@ -195,7 +195,7 @@ const HTML_IMAGE_RE =
195
195
  /**
196
196
  * Markdown 链接语法: [label](path)
197
197
  */
198
- const MARKDOWN_LINK_RE = /\[([^\]]*)\]\(([^)]+)\)/g;
198
+ const MARKDOWN_LINK_RE = /\[([^\]]*)\]\(([^)]+)\)/g;
199
199
 
200
200
  /**
201
201
  * 本地图片路径(裸露的,非 Markdown 格式)
@@ -212,33 +212,33 @@ const NON_IMAGE_EXT_PATTERN = Array.from(NON_IMAGE_EXTENSIONS).join("|");
212
212
  const WINDOWS_PATH_SEP = String.raw`(?:\\\\|\\)`;
213
213
  const WINDOWS_FILE_PATH = String.raw`[A-Za-z]:${WINDOWS_PATH_SEP}(?:[^\\/:*?"<>|\r\n]+${WINDOWS_PATH_SEP})*[^\\/:*?"<>|\r\n]+`;
214
214
  const UNIX_FILE_PATH = String.raw`\/(?:tmp|var|private|Users|home|root)\/[^\s'",)]+`;
215
- const BARE_FILE_PATH_RE = new RegExp(
216
- String.raw`\`?((?:${UNIX_FILE_PATH}|${WINDOWS_FILE_PATH})\.(?:${NON_IMAGE_EXT_PATTERN}))\`?`,
217
- "gi"
218
- );
219
-
220
- // MEDIA: 行解析辅助
221
- const MEDIA_LINE_PREFIX = "MEDIA:";
222
-
223
- function unwrapMediaLinePayload(value: string): string | undefined {
224
- const trimmed = value.trim();
225
- if (trimmed.length < 2) return undefined;
226
- const first = trimmed[0];
227
- const last = trimmed[trimmed.length - 1];
228
- if (first !== last) return undefined;
229
- if (first !== `"` && first !== "'" && first !== "`") return undefined;
230
- return trimmed.slice(1, -1).trim();
231
- }
232
-
233
- function cleanMediaLineCandidate(value: string): string {
234
- return value.replace(/^[`"'[{(<]+/, "").replace(/[`"'\])}>.,;]+$/, "");
235
- }
236
-
237
- function splitMediaLineCandidates(payload: string): string[] {
238
- const unwrapped = unwrapMediaLinePayload(payload);
239
- if (unwrapped) return [unwrapped];
240
- return payload.split(/\s+/).filter(Boolean);
241
- }
215
+ const BARE_FILE_PATH_RE = new RegExp(
216
+ String.raw`\`?((?:${UNIX_FILE_PATH}|${WINDOWS_FILE_PATH})\.(?:${NON_IMAGE_EXT_PATTERN}))\`?`,
217
+ "gi"
218
+ );
219
+
220
+ // MEDIA: 行解析辅助
221
+ const MEDIA_LINE_PREFIX = "MEDIA:";
222
+
223
+ function unwrapMediaLinePayload(value: string): string | undefined {
224
+ const trimmed = value.trim();
225
+ if (trimmed.length < 2) return undefined;
226
+ const first = trimmed[0];
227
+ const last = trimmed[trimmed.length - 1];
228
+ if (first !== last) return undefined;
229
+ if (first !== `"` && first !== "'" && first !== "`") return undefined;
230
+ return trimmed.slice(1, -1).trim();
231
+ }
232
+
233
+ function cleanMediaLineCandidate(value: string): string {
234
+ return value.replace(/^[`"'[{(<]+/, "").replace(/[`"'\])}>.,;]+$/, "");
235
+ }
236
+
237
+ function splitMediaLineCandidates(payload: string): string[] {
238
+ const unwrapped = unwrapMediaLinePayload(payload);
239
+ if (unwrapped) return [unwrapped];
240
+ return payload.split(/\s+/).filter(Boolean);
241
+ }
242
242
 
243
243
  // ============================================================================
244
244
  // 路径处理函数
@@ -421,28 +421,28 @@ function createExtractedMedia(
421
421
  * @param options - 解析选项
422
422
  * @returns 解析结果,包含清理后的文本和提取的媒体列表
423
423
  */
424
- export function extractMediaFromText(
425
- text: string,
426
- options: MediaParseOptions = {}
427
- ): MediaParseResult {
428
- const {
429
- removeFromText = true,
430
- checkExists = false,
431
- existsSync,
432
- parseMediaLines = false,
433
- parseMarkdownImages = true,
434
- parseHtmlImages = true,
435
- parseBarePaths = true,
436
- parseMarkdownLinks = true,
437
- } = options;
424
+ export function extractMediaFromText(
425
+ text: string,
426
+ options: MediaParseOptions = {}
427
+ ): MediaParseResult {
428
+ const {
429
+ removeFromText = true,
430
+ checkExists = false,
431
+ existsSync,
432
+ parseMediaLines = false,
433
+ parseMarkdownImages = true,
434
+ parseHtmlImages = true,
435
+ parseBarePaths = true,
436
+ parseMarkdownLinks = true,
437
+ } = options;
438
438
 
439
439
  const images: ExtractedMedia[] = [];
440
440
  const files: ExtractedMedia[] = [];
441
441
  const seenSources = new Set<string>();
442
442
  let result = text;
443
443
 
444
- // 辅助函数:添加媒体项(去重)
445
- const addMedia = (media: ExtractedMedia): boolean => {
444
+ // 辅助函数:添加媒体项(去重)
445
+ const addMedia = (media: ExtractedMedia): boolean => {
446
446
  const key = media.localPath || media.source;
447
447
  if (seenSources.has(key)) return false;
448
448
 
@@ -461,53 +461,53 @@ export function extractMediaFromText(
461
461
  } else {
462
462
  files.push(media);
463
463
  }
464
- return true;
465
- };
466
-
467
- // 0. 解析行首 MEDIA: 指令
468
- if (parseMediaLines) {
469
- const lines = result.split("\n");
470
- const keptLines: string[] = [];
471
- for (const line of lines) {
472
- const trimmedStart = line.trimStart();
473
- if (!trimmedStart.startsWith(MEDIA_LINE_PREFIX)) {
474
- keptLines.push(line);
475
- continue;
476
- }
477
-
478
- const payload = trimmedStart.slice(MEDIA_LINE_PREFIX.length).trim();
479
- if (!payload) {
480
- keptLines.push(line);
481
- continue;
482
- }
483
-
484
- const candidates = splitMediaLineCandidates(payload);
485
- let addedAny = false;
486
- for (const raw of candidates) {
487
- const candidate = stripTitleFromUrl(cleanMediaLineCandidate(raw));
488
- if (!candidate) continue;
489
- if (!isHttpUrl(candidate) && !isLocalReference(candidate)) {
490
- continue;
491
- }
492
- const media = createExtractedMedia(candidate, "bare", options);
493
- if (addMedia(media)) {
494
- addedAny = true;
495
- }
496
- }
497
-
498
- if (!addedAny || !removeFromText) {
499
- keptLines.push(line);
500
- }
501
- }
502
-
503
- if (removeFromText) {
504
- result = keptLines.join("\n");
505
- }
506
- }
507
-
508
- // 收集需要替换的位置(用于安全替换)
509
- type Replacement = { start: number; end: number; replacement: string };
510
- const replacements: Replacement[] = [];
464
+ return true;
465
+ };
466
+
467
+ // 0. 解析行首 MEDIA: 指令
468
+ if (parseMediaLines) {
469
+ const lines = result.split("\n");
470
+ const keptLines: string[] = [];
471
+ for (const line of lines) {
472
+ const trimmedStart = line.trimStart();
473
+ if (!trimmedStart.startsWith(MEDIA_LINE_PREFIX)) {
474
+ keptLines.push(line);
475
+ continue;
476
+ }
477
+
478
+ const payload = trimmedStart.slice(MEDIA_LINE_PREFIX.length).trim();
479
+ if (!payload) {
480
+ keptLines.push(line);
481
+ continue;
482
+ }
483
+
484
+ const candidates = splitMediaLineCandidates(payload);
485
+ let addedAny = false;
486
+ for (const raw of candidates) {
487
+ const candidate = stripTitleFromUrl(cleanMediaLineCandidate(raw));
488
+ if (!candidate) continue;
489
+ if (!isHttpUrl(candidate) && !isLocalReference(candidate)) {
490
+ continue;
491
+ }
492
+ const media = createExtractedMedia(candidate, "bare", options);
493
+ if (addMedia(media)) {
494
+ addedAny = true;
495
+ }
496
+ }
497
+
498
+ if (!addedAny || !removeFromText) {
499
+ keptLines.push(line);
500
+ }
501
+ }
502
+
503
+ if (removeFromText) {
504
+ result = keptLines.join("\n");
505
+ }
506
+ }
507
+
508
+ // 收集需要替换的位置(用于安全替换)
509
+ type Replacement = { start: number; end: number; replacement: string };
510
+ const replacements: Replacement[] = [];
511
511
 
512
512
  // 辅助函数:应用替换(从后向前,避免索引错位)
513
513
  const applyReplacements = (): void => {