@gt-fe/eap-runtime 0.0.36 → 0.0.37
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/dist/graph/adapters/langgraph.adapter.js +1 -0
- package/dist/registry/graph-loader.js +21 -0
- package/dist/tool/dev-mcp-executor.js +2 -1
- package/dist/tool/harness-primitives.d.ts +2 -1
- package/dist/tool/harness-primitives.js +70 -1
- package/dist/tool/mcp-list.d.ts +7 -1
- package/dist/tool/mcp-list.js +17 -4
- package/dist/tool/mcp-tool-executor.d.ts +1 -1
- package/dist/tool/mcp-tool-executor.js +2 -1
- package/dist/tool/system-prompt-builder.js +25 -7
- package/dist/tool/tool-executor.interface.d.ts +6 -0
- package/dist/tool/unix-time.d.ts +31 -0
- package/dist/tool/unix-time.js +304 -0
- package/package.json +2 -2
|
@@ -1172,6 +1172,7 @@ export class LangGraphAdapter {
|
|
|
1172
1172
|
},
|
|
1173
1173
|
correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
|
|
1174
1174
|
taskId: typeof state['taskId'] === 'string' ? state['taskId'] : undefined,
|
|
1175
|
+
sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
|
|
1175
1176
|
onListFailure: ({ toolset, error }) => {
|
|
1176
1177
|
this.emitToolExecutionEvents({
|
|
1177
1178
|
toolCallId: `mcp-list:${toolset}`,
|
|
@@ -270,6 +270,26 @@ function resolveMcpServer(manifest) {
|
|
|
270
270
|
}
|
|
271
271
|
return undefined;
|
|
272
272
|
}
|
|
273
|
+
function mcpKnownToolNames(manifest) {
|
|
274
|
+
const tools = manifest.tools;
|
|
275
|
+
if (!Array.isArray(tools))
|
|
276
|
+
return [];
|
|
277
|
+
const names = [];
|
|
278
|
+
const seen = new Set();
|
|
279
|
+
for (const tool of tools) {
|
|
280
|
+
if (!tool || typeof tool !== 'object')
|
|
281
|
+
continue;
|
|
282
|
+
const name = tool.name;
|
|
283
|
+
if (typeof name !== 'string')
|
|
284
|
+
continue;
|
|
285
|
+
const trimmed = name.trim();
|
|
286
|
+
if (!trimmed || seen.has(trimmed))
|
|
287
|
+
continue;
|
|
288
|
+
seen.add(trimmed);
|
|
289
|
+
names.push(trimmed);
|
|
290
|
+
}
|
|
291
|
+
return names;
|
|
292
|
+
}
|
|
273
293
|
/** 门户把同一次注册的「用户确认」写在每个 tools[] 上。MCP 不展开这份清单,占位记录读这个值。 */
|
|
274
294
|
function mcpToolsRequireConfirmation(manifest) {
|
|
275
295
|
const tools = manifest.tools;
|
|
@@ -301,6 +321,7 @@ function mcpDiscoverStub(pre, options) {
|
|
|
301
321
|
if (options?.autoApprove && options.autoApprove.length > 0) {
|
|
302
322
|
execution.autoApprove = options.autoApprove;
|
|
303
323
|
}
|
|
324
|
+
execution.knownTools = mcpKnownToolNames(m);
|
|
304
325
|
return {
|
|
305
326
|
toolId: pre.toolId,
|
|
306
327
|
name: pre.name,
|
|
@@ -73,7 +73,8 @@ export class DevMcpExecutor {
|
|
|
73
73
|
const url = execution.server?.url ?? '';
|
|
74
74
|
const cmd = execution.server?.command ?? '';
|
|
75
75
|
const args = (execution.server?.args ?? []).join('\0');
|
|
76
|
-
const
|
|
76
|
+
const scope = request.sessionId?.trim() || request.taskId?.trim() || '';
|
|
77
|
+
const key = `${url || `stdio:${cmd}:${args}`}\0${request.authContext?.userId ?? ''}\0${request.authContext?.agentCode ?? ''}\0${scope}`;
|
|
77
78
|
const cached = this.listCache.get(key);
|
|
78
79
|
if (cached)
|
|
79
80
|
return cached;
|
|
@@ -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);
|
package/dist/tool/mcp-list.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface McpDiscoverExecution {
|
|
|
25
25
|
bundlePath?: string;
|
|
26
26
|
enabledTools?: string[];
|
|
27
27
|
autoApprove?: string[];
|
|
28
|
+
/** 预装清单里已有的子工具名。缺省表示沿用 stub 的确认规则。 */
|
|
29
|
+
knownTools?: string[];
|
|
28
30
|
}
|
|
29
31
|
export declare function readMcpDiscoverExecution(raw: unknown): McpDiscoverExecution | null;
|
|
30
32
|
export declare function isMcpDiscoverStub(def: LoadedToolDefinition): boolean;
|
|
@@ -71,6 +73,7 @@ export interface HydrateMcpToolsAuth {
|
|
|
71
73
|
authContext: AuthContext;
|
|
72
74
|
correlationId?: string;
|
|
73
75
|
taskId?: string;
|
|
76
|
+
sessionId?: string;
|
|
74
77
|
onListFailure?: (failure: {
|
|
75
78
|
toolset: string;
|
|
76
79
|
endpoint: string;
|
|
@@ -79,10 +82,13 @@ export interface HydrateMcpToolsAuth {
|
|
|
79
82
|
}
|
|
80
83
|
/**
|
|
81
84
|
* 将 MCP discover stub 替换为 tools/list 结果。
|
|
82
|
-
* 无 stub
|
|
85
|
+
* 无 stub 时原样返回(同一引用)。执行器按会话缓存 list。
|
|
83
86
|
* enabledTools 为可选白名单;autoApprove 的子工具 requireConfirmation=false。
|
|
87
|
+
* knownTools 之外的新工具,不是只读时要用户确认。
|
|
84
88
|
* 单个 endpoint 的 list 失败只跳过该工具集;全部失败才抛错(错误含工具集名)。
|
|
85
89
|
*/
|
|
86
90
|
export declare function hydrateMcpTools(loadedTools: LoadedToolMap | undefined, executor: IToolExecutor | undefined, auth: HydrateMcpToolsAuth): Promise<LoadedToolMap | undefined>;
|
|
91
|
+
/** 登记过的名字沿用 stub;新名字在不是只读时要确认。没有 knownTools 时全部沿用 stub。 */
|
|
92
|
+
export declare function mcpChildNeedsConfirmation(tool: McpListedTool, stub: LoadedToolDefinition, autoApprove: boolean, known: Set<string> | null): boolean;
|
|
87
93
|
/** 测试与 HITL 续跑:从已 hydrate 的 map 判断是否还要 list */
|
|
88
94
|
export declare function needsMcpHydration(loadedTools: LoadedToolMap | undefined): boolean;
|
package/dist/tool/mcp-list.js
CHANGED
|
@@ -24,6 +24,7 @@ export function parseListedTools(result) {
|
|
|
24
24
|
name: t.name,
|
|
25
25
|
description: typeof t.description === 'string' ? t.description : undefined,
|
|
26
26
|
inputSchema,
|
|
27
|
+
...(t.annotations?.readOnlyHint === true ? { readOnly: true } : {}),
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
return out;
|
|
@@ -144,8 +145,9 @@ export function toolTraceIdentity(loaded, toolName) {
|
|
|
144
145
|
}
|
|
145
146
|
/**
|
|
146
147
|
* 将 MCP discover stub 替换为 tools/list 结果。
|
|
147
|
-
* 无 stub
|
|
148
|
+
* 无 stub 时原样返回(同一引用)。执行器按会话缓存 list。
|
|
148
149
|
* enabledTools 为可选白名单;autoApprove 的子工具 requireConfirmation=false。
|
|
150
|
+
* knownTools 之外的新工具,不是只读时要用户确认。
|
|
149
151
|
* 单个 endpoint 的 list 失败只跳过该工具集;全部失败才抛错(错误含工具集名)。
|
|
150
152
|
*/
|
|
151
153
|
export async function hydrateMcpTools(loadedTools, executor, auth) {
|
|
@@ -193,6 +195,7 @@ export async function hydrateMcpTools(loadedTools, executor, auth) {
|
|
|
193
195
|
execution: primary.manifest.execution,
|
|
194
196
|
...(toolsetCode ? { toolsetCode } : {}),
|
|
195
197
|
...(primary.manifest.version ? { toolVersion: primary.manifest.version } : {}),
|
|
198
|
+
...(auth.sessionId ? { sessionId: auth.sessionId } : {}),
|
|
196
199
|
});
|
|
197
200
|
}
|
|
198
201
|
catch (err) {
|
|
@@ -213,12 +216,13 @@ export async function hydrateMcpTools(loadedTools, executor, auth) {
|
|
|
213
216
|
const enabled = exec?.enabledTools;
|
|
214
217
|
const enabledSet = enabled && enabled.length > 0 ? new Set(enabled) : null;
|
|
215
218
|
const autoApprove = new Set(exec?.autoApprove ?? []);
|
|
219
|
+
const known = Array.isArray(exec?.knownTools) ? new Set(exec.knownTools) : null;
|
|
216
220
|
const picked = enabledSet ? listed.filter((t) => enabledSet.has(t.name)) : listed;
|
|
217
221
|
for (const tool of picked) {
|
|
218
222
|
if (emitted.has(tool.name))
|
|
219
223
|
continue;
|
|
220
224
|
emitted.add(tool.name);
|
|
221
|
-
result[tool.name] = listedToolToDefinition(tool, stub, autoApprove.has(tool.name));
|
|
225
|
+
result[tool.name] = listedToolToDefinition(tool, stub, mcpChildNeedsConfirmation(tool, stub, autoApprove.has(tool.name), known));
|
|
222
226
|
}
|
|
223
227
|
}
|
|
224
228
|
}
|
|
@@ -227,11 +231,20 @@ export async function hydrateMcpTools(loadedTools, executor, auth) {
|
|
|
227
231
|
}
|
|
228
232
|
return result;
|
|
229
233
|
}
|
|
230
|
-
|
|
234
|
+
/** 登记过的名字沿用 stub;新名字在不是只读时要确认。没有 knownTools 时全部沿用 stub。 */
|
|
235
|
+
export function mcpChildNeedsConfirmation(tool, stub, autoApprove, known) {
|
|
236
|
+
if (autoApprove)
|
|
237
|
+
return false;
|
|
238
|
+
if (known === null || known.has(tool.name))
|
|
239
|
+
return Boolean(stub.manifest.requireConfirmation);
|
|
240
|
+
return tool.readOnly !== true;
|
|
241
|
+
}
|
|
242
|
+
function listedToolToDefinition(tool, stub, requireConfirmation) {
|
|
231
243
|
const exec = { ...(readMcpDiscoverExecution(stub.manifest.execution) ?? {}) };
|
|
232
244
|
delete exec.discover;
|
|
233
245
|
delete exec.enabledTools;
|
|
234
246
|
delete exec.autoApprove;
|
|
247
|
+
delete exec.knownTools;
|
|
235
248
|
const rest = exec;
|
|
236
249
|
return {
|
|
237
250
|
toolId: `${stub.toolId}:${tool.name}`,
|
|
@@ -245,7 +258,7 @@ function listedToolToDefinition(tool, stub, autoApprove) {
|
|
|
245
258
|
description: tool.description,
|
|
246
259
|
toolRuntime: stub.manifest.toolRuntime,
|
|
247
260
|
dataScope: stub.manifest.dataScope,
|
|
248
|
-
requireConfirmation
|
|
261
|
+
requireConfirmation,
|
|
249
262
|
execution: {
|
|
250
263
|
...rest,
|
|
251
264
|
protocol: 'mcp',
|
|
@@ -18,7 +18,7 @@ export declare class McpToolExecutor implements IToolExecutor {
|
|
|
18
18
|
private readonly mcpUrl;
|
|
19
19
|
private rpcId;
|
|
20
20
|
private initialized;
|
|
21
|
-
/**
|
|
21
|
+
/** 同一会话复用 tools/list。新会话或新 task 重新要。 */
|
|
22
22
|
private readonly listCache;
|
|
23
23
|
constructor(options: McpToolExecutorOptions);
|
|
24
24
|
execute(request: ToolCallRequest): Promise<ToolCallResult>;
|
|
@@ -81,6 +81,7 @@ function mcpListCacheKey(mcpUrl, request) {
|
|
|
81
81
|
request.authContext.agentCode,
|
|
82
82
|
request.toolsetCode?.trim() ?? '',
|
|
83
83
|
request.toolVersion?.trim() ?? '',
|
|
84
|
+
request.sessionId?.trim() || request.taskId?.trim() || '',
|
|
84
85
|
].join('\0');
|
|
85
86
|
}
|
|
86
87
|
export class McpToolExecutor {
|
|
@@ -89,7 +90,7 @@ export class McpToolExecutor {
|
|
|
89
90
|
mcpUrl;
|
|
90
91
|
rpcId = 1;
|
|
91
92
|
initialized = false;
|
|
92
|
-
/**
|
|
93
|
+
/** 同一会话复用 tools/list。新会话或新 task 重新要。 */
|
|
93
94
|
listCache = new Map();
|
|
94
95
|
constructor(options) {
|
|
95
96
|
if (!isAbsoluteHttpUrl(options.baseUrl)) {
|
|
@@ -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)}(周日)`,
|
|
@@ -38,6 +38,10 @@ export interface ToolCallRequest {
|
|
|
38
38
|
taskId?: string;
|
|
39
39
|
/** 链路关联 ID;缺省可用 toolCallId */
|
|
40
40
|
correlationId?: string;
|
|
41
|
+
/**
|
|
42
|
+
* 会话标识。MCP tools/list 按会话缓存:同一会话复用名单,新会话重新向 Gateway 要。
|
|
43
|
+
*/
|
|
44
|
+
sessionId?: string;
|
|
41
45
|
/** 路由用:manifest type */
|
|
42
46
|
toolType?: string;
|
|
43
47
|
/** 路由用:manifest runtime */
|
|
@@ -143,6 +147,8 @@ export interface McpListedTool {
|
|
|
143
147
|
name: string;
|
|
144
148
|
description?: string;
|
|
145
149
|
inputSchema?: Record<string, unknown>;
|
|
150
|
+
/** 上游 annotations.readOnlyHint 为 true。缺省视为会改数据。 */
|
|
151
|
+
readOnly?: boolean;
|
|
146
152
|
}
|
|
147
153
|
/**
|
|
148
154
|
* IToolExecutor — Tool 执行网关接口
|
|
@@ -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.37",
|
|
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/",
|