@gt-fe/eap-runtime 0.0.36 → 0.0.38
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 平台默认 Harness 原语:read_file / write_file / execute_command。
|
|
2
|
+
* 平台默认 Harness 原语:read_file / write_file / append_to_file / execute_command / datetime_to_unix / unix_to_datetime。
|
|
3
3
|
*
|
|
4
4
|
* 不写进 agent.manifest.toolsets。同名业务工具优先(问数 file-io 等)。
|
|
5
5
|
* 写:`dirname(agentDir)/users/<userId>/`(含用户自己的 skills)。产物默认 `files/`,公开副本 `public/`。
|
|
@@ -56,6 +56,7 @@ export declare class HarnessPrimitiveExecutor implements IToolExecutor {
|
|
|
56
56
|
private extraRootsFor;
|
|
57
57
|
private requireWorkdir;
|
|
58
58
|
execute(request: ToolCallRequest): Promise<ToolCallResult>;
|
|
59
|
+
private convertTime;
|
|
59
60
|
private readFile;
|
|
60
61
|
private writeFile;
|
|
61
62
|
private executeCommand;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 平台默认 Harness 原语:read_file / write_file / execute_command。
|
|
2
|
+
* 平台默认 Harness 原语:read_file / write_file / append_to_file / execute_command / datetime_to_unix / unix_to_datetime。
|
|
3
3
|
*
|
|
4
4
|
* 不写进 agent.manifest.toolsets。同名业务工具优先(问数 file-io 等)。
|
|
5
5
|
* 写:`dirname(agentDir)/users/<userId>/`(含用户自己的 skills)。产物默认 `files/`,公开副本 `public/`。
|
|
@@ -20,6 +20,7 @@ import { MAX_SNAPSHOT_ENUM, MAX_SNAPSHOT_URLS, PUBLIC_FILE_MODE, ensureUserSlots
|
|
|
20
20
|
import { BUILTIN_TOOL_SOURCE, DEFAULT_BUILTIN_TIMEOUT_MS, MAX_BUILTIN_TIMEOUT_MS, clampBuiltinTimeout, isBuiltinToolExecution, resolveBuiltinToolsConfig, } from '@gt-fe/eap-contracts';
|
|
21
21
|
import { ensureInterpreterShims, formatSpawnError, killProcessTree, resolveShellSpawn, sandboxEnv, spawnOptionsFor, substituteSkillDir, } from './harness-command.js';
|
|
22
22
|
import { expandSkillScriptRefs, isSkillEntryFile } from './skill-script-refs.js';
|
|
23
|
+
import { resolveDatetimeToUnix, resolveUnixToDatetime } from './unix-time.js';
|
|
23
24
|
/** 超时杀进程或 spawn 失败后等 `close`;未到则最多再等此时长再快照。 */
|
|
24
25
|
const SPAWN_CLOSE_GRACE_MS = 1000;
|
|
25
26
|
function builtinConfig() {
|
|
@@ -140,6 +141,64 @@ function harnessDefinitions() {
|
|
|
140
141
|
},
|
|
141
142
|
},
|
|
142
143
|
},
|
|
144
|
+
{
|
|
145
|
+
toolId: 'builtin:datetime_to_unix',
|
|
146
|
+
name: 'datetime_to_unix',
|
|
147
|
+
manifest: {
|
|
148
|
+
id: 'builtin:datetime_to_unix',
|
|
149
|
+
name: 'datetime_to_unix',
|
|
150
|
+
description: '把日期或时刻换成 Unix 秒和毫秒。已有时间戳要换成日期时用 unix_to_datetime。返回 JSON:timezone、datetime、unix_seconds、unix_milliseconds;只写日期时另有 note。unix_seconds 与 unix_milliseconds 是数字字符串。按秒的参数填 unix_seconds,按毫秒的参数填 unix_milliseconds。',
|
|
151
|
+
...common,
|
|
152
|
+
},
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
properties: {
|
|
157
|
+
datetime: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
description: '要换算的日期或时刻。省略或空字符串表示现在。只写日期按 timezone 当天 00:00:00,如 2027-01-01、2027年1月1日、2027/01/01。时刻如 2027-01-01 11:00,T 与空格相同。毫秒写在秒后,1 到 3 位,.1 表示 100 毫秒。不要写 Z、+08:00 或数字。',
|
|
160
|
+
},
|
|
161
|
+
timezone: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description: 'IANA 时区名,如 Asia/Shanghai、UTC、America/New_York。省略则用环境段的「时区」。',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
toolId: 'builtin:unix_to_datetime',
|
|
170
|
+
name: 'unix_to_datetime',
|
|
171
|
+
manifest: {
|
|
172
|
+
id: 'builtin:unix_to_datetime',
|
|
173
|
+
name: 'unix_to_datetime',
|
|
174
|
+
description: '把 Unix 时间戳换成日期文字。日期换时间戳用 datetime_to_unix。返回 JSON:timezone、unit、formatted、format、unix_seconds、unix_milliseconds。',
|
|
175
|
+
...common,
|
|
176
|
+
},
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
additionalProperties: false,
|
|
180
|
+
required: ['timestamp'],
|
|
181
|
+
properties: {
|
|
182
|
+
timestamp: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: '整数字符串。秒如 1798732800,毫秒如 1798732800000。不要传日期。',
|
|
185
|
+
},
|
|
186
|
+
unit: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
enum: ['s', 'ms'],
|
|
189
|
+
description: 's 为秒,ms 为毫秒。省略时,绝对值达到 1000000000000(13 位)视为毫秒,否则视为秒。',
|
|
190
|
+
},
|
|
191
|
+
timezone: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
description: 'IANA 时区名,如 Asia/Shanghai、UTC、America/New_York。省略则用环境段的「时区」。',
|
|
194
|
+
},
|
|
195
|
+
format: {
|
|
196
|
+
type: 'string',
|
|
197
|
+
description: 'formatted 的写法,默认 YYYY-MM-DD HH:mm:ss。记号只有 YYYY、MM(月)、DD、HH、mm(分)、ss、SSS。分钟写 mm。',
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
143
202
|
];
|
|
144
203
|
}
|
|
145
204
|
export function mergeHarnessPrimitives(loadedTools) {
|
|
@@ -401,6 +460,10 @@ export class HarnessPrimitiveExecutor {
|
|
|
401
460
|
return await this.writeFile(request, true);
|
|
402
461
|
case 'execute_command':
|
|
403
462
|
return await this.executeCommand(request);
|
|
463
|
+
case 'datetime_to_unix':
|
|
464
|
+
return this.convertTime(request, resolveDatetimeToUnix);
|
|
465
|
+
case 'unix_to_datetime':
|
|
466
|
+
return this.convertTime(request, resolveUnixToDatetime);
|
|
404
467
|
default:
|
|
405
468
|
return fail(request, `未知 Harness 原语:${request.toolName}`);
|
|
406
469
|
}
|
|
@@ -409,6 +472,12 @@ export class HarnessPrimitiveExecutor {
|
|
|
409
472
|
return fail(request, err instanceof Error ? err.message : String(err));
|
|
410
473
|
}
|
|
411
474
|
}
|
|
475
|
+
convertTime(request, resolve) {
|
|
476
|
+
const resolved = resolve(request.arguments);
|
|
477
|
+
if (!resolved.ok)
|
|
478
|
+
return fail(request, resolved.error);
|
|
479
|
+
return ok(request, JSON.stringify(resolved.value));
|
|
480
|
+
}
|
|
412
481
|
async readFile(request) {
|
|
413
482
|
const filePath = String(request.arguments.path ?? '');
|
|
414
483
|
const workdir = this.workdirFor(request);
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
*
|
|
21
21
|
* @see docs/status/module-status-and-backlog.md RT-5
|
|
22
22
|
*/
|
|
23
|
+
import { resolveBuiltinToolsConfig } from '@gt-fe/eap-contracts';
|
|
23
24
|
import { commandDialectLines } from './harness-command.js';
|
|
25
|
+
import { defaultTimezone } from './unix-time.js';
|
|
24
26
|
// ─────────────────────────────────────────────
|
|
25
27
|
// 平台规则常量(一年改不了两次,不值得搞配置表)
|
|
26
28
|
// ─────────────────────────────────────────────
|
|
@@ -64,6 +66,21 @@ const PLATFORM_RULES = `你是运行在企业智能体平台(EAP)上的 AI
|
|
|
64
66
|
|
|
65
67
|
## 规则优先级
|
|
66
68
|
以上平台规则最高。后面的身份、技能说明、Agent 基线、节点提示词与之冲突时,以平台规则为准。`;
|
|
69
|
+
/** 内置时间工具开着时,写进平台规则「时间」段。关掉内置工具则不写。 */
|
|
70
|
+
function timestampBan() {
|
|
71
|
+
return [
|
|
72
|
+
'- **禁止**自己计算 Unix 秒或毫秒。禁止心算,禁止用当前时间戳加减,禁止用 `execute_command`、`date` 或 python 换算。',
|
|
73
|
+
'- 环境段「当前时间戳(秒)」「当前时间戳(毫秒)」可以原样抄。',
|
|
74
|
+
'- 其它日期或时间戳**必须**调用 `datetime_to_unix`(日期换成时间戳)或 `unix_to_datetime`(时间戳换成日期),只使用返回的 `unix_seconds`、`unix_milliseconds` 或 `formatted`。',
|
|
75
|
+
'- 这两个工具省略 timezone 时,用环境段的「时区」。填入其它工具的秒或毫秒,必须是返回的 `unix_seconds` 或 `unix_milliseconds`。',
|
|
76
|
+
].join('\n');
|
|
77
|
+
}
|
|
78
|
+
function platformRules() {
|
|
79
|
+
if (!resolveBuiltinToolsConfig(process.env).enabled)
|
|
80
|
+
return PLATFORM_RULES;
|
|
81
|
+
const anchor = '- 遇到非法日期(如 2 月 30 日)先告诉用户,不要擅自改写后静默查询。';
|
|
82
|
+
return PLATFORM_RULES.replace(anchor, `${anchor}\n${timestampBan()}`);
|
|
83
|
+
}
|
|
67
84
|
export function toLoadedSkillMap(skills) {
|
|
68
85
|
if (!skills?.length)
|
|
69
86
|
return undefined;
|
|
@@ -136,7 +153,7 @@ export class SystemPromptBuilder {
|
|
|
136
153
|
return;
|
|
137
154
|
sections.push({ id, title, body: trimmed });
|
|
138
155
|
};
|
|
139
|
-
add('platform-rules', '平台规则',
|
|
156
|
+
add('platform-rules', '平台规则', platformRules());
|
|
140
157
|
add('environment', '环境', this.buildEnvironment(context));
|
|
141
158
|
add('identity', 'Agent 身份', this.buildAgentIdentity(context));
|
|
142
159
|
if (context.userId && context.userId.trim() && context.userId !== 'anonymous') {
|
|
@@ -154,15 +171,16 @@ export class SystemPromptBuilder {
|
|
|
154
171
|
}
|
|
155
172
|
buildEnvironment(context, now = new Date()) {
|
|
156
173
|
const windows = naturalTimeWindows(now);
|
|
174
|
+
const zone = defaultTimezone();
|
|
175
|
+
const ms = now.getTime();
|
|
157
176
|
const lines = [
|
|
158
177
|
'## 环境',
|
|
178
|
+
`- 时区:${zone}。当前时间、周和月都按此时区。`,
|
|
159
179
|
`- 当前时间:${windows.now}(${windows.weekday})`,
|
|
160
|
-
`-
|
|
161
|
-
`-
|
|
162
|
-
`- 上周末:${windows.lastWeekend}`,
|
|
163
|
-
`- 本月:${windows.thisMonth}`,
|
|
164
|
-
`- 上月:${windows.lastMonth}`,
|
|
180
|
+
`- 当前时间戳(秒):${Math.floor(ms / 1000)}`,
|
|
181
|
+
`- 当前时间戳(毫秒):${ms}`,
|
|
165
182
|
];
|
|
183
|
+
lines.push(`- 本周:${windows.thisWeek}`, `- 上周:${windows.lastWeek}`, `- 上周末:${windows.lastWeekend}`, `- 本月:${windows.thisMonth}`, `- 上月:${windows.lastMonth}`);
|
|
166
184
|
if (context.workingDirectory) {
|
|
167
185
|
lines.push(`- 工作目录:${context.workingDirectory}`);
|
|
168
186
|
}
|
|
@@ -255,7 +273,7 @@ export function naturalTimeWindows(now = new Date()) {
|
|
|
255
273
|
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
256
274
|
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0);
|
|
257
275
|
return {
|
|
258
|
-
now: `${formatDay(now)} ${pad2(now.getHours())}:${pad2(now.getMinutes())}`,
|
|
276
|
+
now: `${formatDay(now)} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`,
|
|
259
277
|
weekday: weekdays[now.getDay()] ?? '',
|
|
260
278
|
thisWeek: `${formatDay(thisMon)}(周一) ~ ${formatDay(thisSun)}(周日)`,
|
|
261
279
|
lastWeek: `${formatDay(lastMon)}(周一) ~ ${formatDay(lastSun)}(周日)`,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 两个方向分开:
|
|
3
|
+
* datetime_to_unix 只收日期字符串;unix_to_datetime 只收整数字符串时间戳。
|
|
4
|
+
* 省略 timezone 时用进程 IANA 时区,与环境段的当前时间相同。
|
|
5
|
+
*/
|
|
6
|
+
/** 省略 timezone 时用的时区:进程 IANA 名,没有则 Asia/Shanghai。 */
|
|
7
|
+
export declare function defaultTimezone(): string;
|
|
8
|
+
export interface DatetimeToUnixValue {
|
|
9
|
+
timezone: string;
|
|
10
|
+
datetime: string;
|
|
11
|
+
unix_seconds: string;
|
|
12
|
+
unix_milliseconds: string;
|
|
13
|
+
note?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface UnixToDatetimeValue {
|
|
16
|
+
timezone: string;
|
|
17
|
+
unit: 's' | 'ms';
|
|
18
|
+
formatted: string;
|
|
19
|
+
format: string;
|
|
20
|
+
unix_seconds: string;
|
|
21
|
+
unix_milliseconds: string;
|
|
22
|
+
}
|
|
23
|
+
export type TimeResolution<T> = {
|
|
24
|
+
ok: true;
|
|
25
|
+
value: T;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
error: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function resolveDatetimeToUnix(args: Record<string, unknown> | undefined): TimeResolution<DatetimeToUnixValue>;
|
|
31
|
+
export declare function resolveUnixToDatetime(args: Record<string, unknown> | undefined): TimeResolution<UnixToDatetimeValue>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 两个方向分开:
|
|
3
|
+
* datetime_to_unix 只收日期字符串;unix_to_datetime 只收整数字符串时间戳。
|
|
4
|
+
* 省略 timezone 时用进程 IANA 时区,与环境段的当前时间相同。
|
|
5
|
+
*/
|
|
6
|
+
const FALLBACK_TIMEZONE = 'Asia/Shanghai';
|
|
7
|
+
const DEFAULT_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
|
8
|
+
/** 省略 unit 时,绝对值达到此值视为毫秒,否则视为秒。 */
|
|
9
|
+
const MS_THRESHOLD = 1_000_000_000_000;
|
|
10
|
+
/** Date 可表示的绝对毫秒。超出后 Intl 会抛英文异常。 */
|
|
11
|
+
const MAX_ABS_MS = 8_640_000_000_000_000;
|
|
12
|
+
const FORMAT_TOKEN = 'YYYY|SSS|MM|DD|HH|mm|ss';
|
|
13
|
+
const DATE_EXAMPLE = '例如 2027-01-01、2027年1月1日、2027/01/01、2027-01-01 11:00、2027-01-01 11:00:00.123。';
|
|
14
|
+
const DATE_ONLY_NOTE = '只写了日期,按该时区当天 00:00:00。其它工具若要日期,传原来的日期,不要传这个零点的秒。';
|
|
15
|
+
/** 省略 timezone 时用的时区:进程 IANA 名,没有则 Asia/Shanghai。 */
|
|
16
|
+
export function defaultTimezone() {
|
|
17
|
+
try {
|
|
18
|
+
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
19
|
+
if (typeof zone === 'string' && zone.trim()) {
|
|
20
|
+
Intl.DateTimeFormat('en-US', { timeZone: zone });
|
|
21
|
+
return zone;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// 进程没有可用的 IANA 时区
|
|
26
|
+
}
|
|
27
|
+
return FALLBACK_TIMEZONE;
|
|
28
|
+
}
|
|
29
|
+
export function resolveDatetimeToUnix(args) {
|
|
30
|
+
const record = args ?? {};
|
|
31
|
+
const extra = unknownKeys(record, ['datetime', 'timezone']);
|
|
32
|
+
if (extra.length > 0) {
|
|
33
|
+
return fail(`datetime_to_unix 只接受 datetime、timezone,收到 ${extra.join('、')}。格式化时间戳请用 unix_to_datetime。`);
|
|
34
|
+
}
|
|
35
|
+
const timezone = readTimezone(record.timezone);
|
|
36
|
+
if (!timezone.ok)
|
|
37
|
+
return timezone;
|
|
38
|
+
const raw = record.datetime;
|
|
39
|
+
if (raw == null || (typeof raw === 'string' && raw.trim() === '')) {
|
|
40
|
+
return { ok: true, value: fromInstant(Date.now(), timezone.value, false) };
|
|
41
|
+
}
|
|
42
|
+
if (typeof raw === 'number' || (typeof raw === 'string' && /^[+-]?\d+$/.test(raw.trim()))) {
|
|
43
|
+
return fail('datetime 是数字。时间戳请用 unix_to_datetime 的 timestamp。');
|
|
44
|
+
}
|
|
45
|
+
if (typeof raw !== 'string') {
|
|
46
|
+
return fail('datetime 必须是字符串,例如 2027-01-01 或 2027-01-01 11:00。');
|
|
47
|
+
}
|
|
48
|
+
const text = raw.trim();
|
|
49
|
+
if (/[zZ]$/.test(text) || /[+-]\d{2}:?\d{2}$/.test(text)) {
|
|
50
|
+
return fail('datetime 带了时区。请去掉 Z 或偏移,改用 timezone。');
|
|
51
|
+
}
|
|
52
|
+
const parsed = parseWall(text);
|
|
53
|
+
if (!parsed.ok)
|
|
54
|
+
return parsed;
|
|
55
|
+
const ms = wallToInstant(parsed.value.civil, timezone.value);
|
|
56
|
+
if (ms == null)
|
|
57
|
+
return fail(`时区 ${timezone.value} 没有这个时刻:${text}。`);
|
|
58
|
+
return { ok: true, value: fromInstant(ms + parsed.value.millis, timezone.value, parsed.value.dateOnly) };
|
|
59
|
+
}
|
|
60
|
+
export function resolveUnixToDatetime(args) {
|
|
61
|
+
const record = args ?? {};
|
|
62
|
+
const extra = unknownKeys(record, ['timestamp', 'unit', 'timezone', 'format']);
|
|
63
|
+
if (extra.length > 0) {
|
|
64
|
+
return fail(`unix_to_datetime 只接受 timestamp、unit、timezone、format,收到 ${extra.join('、')}。日期换时间戳请用 datetime_to_unix。`);
|
|
65
|
+
}
|
|
66
|
+
const timezone = readTimezone(record.timezone);
|
|
67
|
+
if (!timezone.ok)
|
|
68
|
+
return timezone;
|
|
69
|
+
const format = readFormat(record.format);
|
|
70
|
+
if (!format.ok)
|
|
71
|
+
return format;
|
|
72
|
+
const unit = readUnit(record.unit);
|
|
73
|
+
if (!unit.ok)
|
|
74
|
+
return unit;
|
|
75
|
+
const raw = record.timestamp;
|
|
76
|
+
if (raw == null || (typeof raw === 'string' && raw.trim() === '')) {
|
|
77
|
+
return fail('timestamp 必填。秒如 1798732800,毫秒如 1798732800000。');
|
|
78
|
+
}
|
|
79
|
+
if (typeof raw !== 'number' && typeof raw !== 'string') {
|
|
80
|
+
return fail('timestamp 必须是整数字符串。秒如 1798732800,毫秒如 1798732800000。');
|
|
81
|
+
}
|
|
82
|
+
const text = typeof raw === 'number' ? String(raw) : raw.trim();
|
|
83
|
+
if (!/^[+-]?\d+$/.test(text)) {
|
|
84
|
+
return fail('timestamp 必须是整数字符串。日期请用 datetime_to_unix 的 datetime。');
|
|
85
|
+
}
|
|
86
|
+
if (text.replace(/^[+-]/, '').length > 15)
|
|
87
|
+
return fail('时间戳超出可表示范围。');
|
|
88
|
+
const n = Number(text);
|
|
89
|
+
if (!Number.isSafeInteger(n))
|
|
90
|
+
return fail('时间戳超出可表示范围。');
|
|
91
|
+
const resolved = unit.value ?? (Math.abs(n) >= MS_THRESHOLD ? 'ms' : 's');
|
|
92
|
+
const ms = resolved === 'ms' ? n : n * 1000;
|
|
93
|
+
if (!Number.isFinite(ms) || Math.abs(ms) > MAX_ABS_MS)
|
|
94
|
+
return fail('时间戳超出可表示范围。');
|
|
95
|
+
let civil;
|
|
96
|
+
try {
|
|
97
|
+
civil = partsInZone(ms, timezone.value);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return fail('时间戳超出可表示范围。');
|
|
101
|
+
}
|
|
102
|
+
if (!Number.isFinite(civil.year))
|
|
103
|
+
return fail('时间戳超出可表示范围。');
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
value: {
|
|
107
|
+
timezone: timezone.value,
|
|
108
|
+
unit: resolved,
|
|
109
|
+
formatted: applyFormat(civil, ms, format.value),
|
|
110
|
+
format: format.value,
|
|
111
|
+
unix_seconds: String(Math.floor(ms / 1000)),
|
|
112
|
+
unix_milliseconds: String(ms),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function fromInstant(ms, timezone, dateOnly) {
|
|
117
|
+
const civil = partsInZone(ms, timezone);
|
|
118
|
+
const frac = ((ms % 1000) + 1000) % 1000;
|
|
119
|
+
let datetime = applyFormat(civil, ms, DEFAULT_FORMAT);
|
|
120
|
+
if (frac !== 0)
|
|
121
|
+
datetime += `.${String(frac).padStart(3, '0')}`;
|
|
122
|
+
const value = {
|
|
123
|
+
timezone,
|
|
124
|
+
datetime,
|
|
125
|
+
unix_seconds: String(Math.floor(ms / 1000)),
|
|
126
|
+
unix_milliseconds: String(ms),
|
|
127
|
+
};
|
|
128
|
+
if (dateOnly)
|
|
129
|
+
value.note = DATE_ONLY_NOTE;
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
function unknownKeys(record, allowed) {
|
|
133
|
+
return Object.keys(record).filter((key) => !allowed.includes(key));
|
|
134
|
+
}
|
|
135
|
+
function readTimezone(raw) {
|
|
136
|
+
if (raw == null || (typeof raw === 'string' && raw.trim() === '')) {
|
|
137
|
+
return { ok: true, value: defaultTimezone() };
|
|
138
|
+
}
|
|
139
|
+
if (typeof raw !== 'string')
|
|
140
|
+
return fail('timezone 必须是 IANA 时区名,例如 Asia/Shanghai 或 UTC。');
|
|
141
|
+
const timezone = raw.trim();
|
|
142
|
+
try {
|
|
143
|
+
Intl.DateTimeFormat('en-US', { timeZone: timezone });
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return fail(`时区无效:${timezone}。请用 IANA 名,例如 Asia/Shanghai、UTC、America/New_York。`);
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, value: timezone };
|
|
149
|
+
}
|
|
150
|
+
function readFormat(raw) {
|
|
151
|
+
if (raw == null || (typeof raw === 'string' && raw.trim() === '')) {
|
|
152
|
+
return { ok: true, value: DEFAULT_FORMAT };
|
|
153
|
+
}
|
|
154
|
+
if (typeof raw !== 'string') {
|
|
155
|
+
return fail('format 必须是字符串。记号只有 YYYY、MM、DD、HH、mm、ss、SSS。');
|
|
156
|
+
}
|
|
157
|
+
const format = raw.trim();
|
|
158
|
+
const token = new RegExp(FORMAT_TOKEN, 'g');
|
|
159
|
+
const stripped = format.replace(token, '');
|
|
160
|
+
if (/[A-Za-z]/.test(stripped) || !new RegExp(FORMAT_TOKEN).test(format)) {
|
|
161
|
+
return fail(`format 无法使用:${format}。记号只有 YYYY、MM、DD、HH、mm、ss、SSS。月是 MM,分是 mm。例如 YYYY-MM-DD HH:mm:ss。`);
|
|
162
|
+
}
|
|
163
|
+
if (/HH[^A-Za-z]*MM/.test(format)) {
|
|
164
|
+
return fail(`format 把分钟写成了 MM:${format}。分钟用 mm,月份用 MM。例如 YYYY-MM-DD HH:mm:ss。`);
|
|
165
|
+
}
|
|
166
|
+
if (/YYYY[^A-Za-z]*mm/.test(format)) {
|
|
167
|
+
return fail(`format 把月份写成了 mm:${format}。月份用 MM,分钟用 mm。例如 YYYY-MM-DD HH:mm:ss。`);
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, value: format };
|
|
170
|
+
}
|
|
171
|
+
function readUnit(raw) {
|
|
172
|
+
if (raw == null || raw === '')
|
|
173
|
+
return { ok: true, value: undefined };
|
|
174
|
+
if (raw === 's' || raw === 'ms')
|
|
175
|
+
return { ok: true, value: raw };
|
|
176
|
+
return fail('unit 只能是 s 或 ms。s 为秒,ms 为毫秒。');
|
|
177
|
+
}
|
|
178
|
+
function parseWall(text) {
|
|
179
|
+
if (/\.\d{4,}/.test(text))
|
|
180
|
+
return fail(`毫秒最多三位。${DATE_EXAMPLE}`);
|
|
181
|
+
const isoDate = /^(\d{4})([-/])(\d{1,2})\2(\d{1,2})$/.exec(text);
|
|
182
|
+
if (isoDate)
|
|
183
|
+
return accept(text, true, isoDate[1], isoDate[3], isoDate[4]);
|
|
184
|
+
const isoTime = /^(\d{4})([-/])(\d{1,2})\2(\d{1,2})[T ](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:\.(\d{1,3}))?$/.exec(text);
|
|
185
|
+
if (isoTime)
|
|
186
|
+
return accept(text, false, isoTime[1], isoTime[3], isoTime[4], isoTime[5], isoTime[6], isoTime[7], isoTime[8]);
|
|
187
|
+
const cnDate = /^(\d{4})年(\d{1,2})月(\d{1,2})日$/.exec(text);
|
|
188
|
+
if (cnDate)
|
|
189
|
+
return accept(text, true, cnDate[1], cnDate[2], cnDate[3]);
|
|
190
|
+
const cnTime = /^(\d{4})年(\d{1,2})月(\d{1,2})日[ T]?(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:\.(\d{1,3}))?$/.exec(text);
|
|
191
|
+
if (cnTime)
|
|
192
|
+
return accept(text, false, cnTime[1], cnTime[2], cnTime[3], cnTime[4], cnTime[5], cnTime[6], cnTime[7]);
|
|
193
|
+
const cnHan = /^(\d{4})年(\d{1,2})月(\d{1,2})日[ T]?(\d{1,2})时(\d{1,2})分(?:(\d{1,2})秒)?$/.exec(text);
|
|
194
|
+
if (cnHan)
|
|
195
|
+
return accept(text, false, cnHan[1], cnHan[2], cnHan[3], cnHan[4], cnHan[5], cnHan[6]);
|
|
196
|
+
return fail(`看不懂这个时间:${text}。${DATE_EXAMPLE}`);
|
|
197
|
+
}
|
|
198
|
+
function accept(text, dateOnly, year, month, day, hour, minute, second, fraction) {
|
|
199
|
+
const civil = checkedCivil({
|
|
200
|
+
year: Number(year),
|
|
201
|
+
month: Number(month),
|
|
202
|
+
day: Number(day),
|
|
203
|
+
hour: dateOnly ? 0 : Number(hour),
|
|
204
|
+
minute: dateOnly ? 0 : Number(minute),
|
|
205
|
+
second: dateOnly ? 0 : Number(second ?? 0),
|
|
206
|
+
});
|
|
207
|
+
if (!civil)
|
|
208
|
+
return fail(`${dateOnly ? '日期' : '时刻'}无效:${text}。`);
|
|
209
|
+
const millis = fraction ? Number(fraction.padEnd(3, '0')) : 0;
|
|
210
|
+
return { ok: true, value: { civil, millis, dateOnly } };
|
|
211
|
+
}
|
|
212
|
+
function checkedCivil(civil) {
|
|
213
|
+
if (civil.year < 1000 || civil.year > 9999)
|
|
214
|
+
return null;
|
|
215
|
+
if (civil.month < 1 || civil.month > 12 || civil.day < 1)
|
|
216
|
+
return null;
|
|
217
|
+
if (civil.hour > 23 || civil.minute > 59 || civil.second > 59)
|
|
218
|
+
return null;
|
|
219
|
+
const probe = new Date(Date.UTC(civil.year, civil.month - 1, civil.day));
|
|
220
|
+
if (probe.getUTCFullYear() !== civil.year ||
|
|
221
|
+
probe.getUTCMonth() !== civil.month - 1 ||
|
|
222
|
+
probe.getUTCDate() !== civil.day) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return civil;
|
|
226
|
+
}
|
|
227
|
+
/** 墙上时间换成 UTC 毫秒。重叠的本地时刻保留第一次校正后仍能对上的那个。缺口返回 null。 */
|
|
228
|
+
function wallToInstant(civil, timeZone) {
|
|
229
|
+
const desired = Date.UTC(civil.year, civil.month - 1, civil.day, civil.hour, civil.minute, civil.second);
|
|
230
|
+
let instant = desired - offsetMs(timeZone, desired);
|
|
231
|
+
const corrected = desired - offsetMs(timeZone, instant);
|
|
232
|
+
if (corrected !== instant)
|
|
233
|
+
instant = corrected;
|
|
234
|
+
const back = partsInZone(instant, timeZone);
|
|
235
|
+
if (back.year !== civil.year ||
|
|
236
|
+
back.month !== civil.month ||
|
|
237
|
+
back.day !== civil.day ||
|
|
238
|
+
back.hour !== civil.hour ||
|
|
239
|
+
back.minute !== civil.minute ||
|
|
240
|
+
back.second !== civil.second) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
return instant;
|
|
244
|
+
}
|
|
245
|
+
function offsetMs(timeZone, instant) {
|
|
246
|
+
const civil = partsInZone(instant, timeZone);
|
|
247
|
+
const asUtc = Date.UTC(civil.year, civil.month - 1, civil.day, civil.hour, civil.minute, civil.second);
|
|
248
|
+
return asUtc - instant;
|
|
249
|
+
}
|
|
250
|
+
function partsInZone(ms, timeZone) {
|
|
251
|
+
const formatted = new Intl.DateTimeFormat('en-US', {
|
|
252
|
+
timeZone,
|
|
253
|
+
hourCycle: 'h23',
|
|
254
|
+
year: 'numeric',
|
|
255
|
+
month: '2-digit',
|
|
256
|
+
day: '2-digit',
|
|
257
|
+
hour: '2-digit',
|
|
258
|
+
minute: '2-digit',
|
|
259
|
+
second: '2-digit',
|
|
260
|
+
}).formatToParts(new Date(ms));
|
|
261
|
+
const bag = {};
|
|
262
|
+
for (const part of formatted) {
|
|
263
|
+
if (part.type !== 'literal')
|
|
264
|
+
bag[part.type] = part.value;
|
|
265
|
+
}
|
|
266
|
+
let year = Number(bag.year);
|
|
267
|
+
let month = Number(bag.month);
|
|
268
|
+
let day = Number(bag.day);
|
|
269
|
+
let hour = Number(bag.hour);
|
|
270
|
+
if (hour === 24) {
|
|
271
|
+
hour = 0;
|
|
272
|
+
const next = new Date(Date.UTC(year, month - 1, day) + 24 * 60 * 60 * 1000);
|
|
273
|
+
year = next.getUTCFullYear();
|
|
274
|
+
month = next.getUTCMonth() + 1;
|
|
275
|
+
day = next.getUTCDate();
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
year,
|
|
279
|
+
month,
|
|
280
|
+
day,
|
|
281
|
+
hour,
|
|
282
|
+
minute: Number(bag.minute),
|
|
283
|
+
second: Number(bag.second),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function applyFormat(civil, ms, pattern) {
|
|
287
|
+
const sub = ((ms % 1000) + 1000) % 1000;
|
|
288
|
+
const map = {
|
|
289
|
+
YYYY: String(civil.year).padStart(4, '0'),
|
|
290
|
+
MM: pad(civil.month),
|
|
291
|
+
DD: pad(civil.day),
|
|
292
|
+
HH: pad(civil.hour),
|
|
293
|
+
mm: pad(civil.minute),
|
|
294
|
+
ss: pad(civil.second),
|
|
295
|
+
SSS: String(sub).padStart(3, '0'),
|
|
296
|
+
};
|
|
297
|
+
return pattern.replace(new RegExp(FORMAT_TOKEN, 'g'), (token) => map[token] ?? token);
|
|
298
|
+
}
|
|
299
|
+
function pad(n) {
|
|
300
|
+
return String(n).padStart(2, '0');
|
|
301
|
+
}
|
|
302
|
+
function fail(error) {
|
|
303
|
+
return { ok: false, error };
|
|
304
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gt-fe/eap-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"ioredis": "^5.4.0",
|
|
21
21
|
"pg": "^8.13.0",
|
|
22
22
|
"yaml": "^2.6.1",
|
|
23
|
-
"@gt-fe/eap-contracts": "0.0.
|
|
23
|
+
"@gt-fe/eap-contracts": "0.0.19"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"registry": "https://registry.npmjs.org/",
|