@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 +1 -1
- package/src/cron/index.ts +115 -0
- package/src/index.ts +3 -2
- package/src/media/media-parser.ts +105 -105
package/package.json
CHANGED
|
@@ -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 => {
|