ppxans-harness 2.4.0 → 3.2.0
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/LICENSE +201 -201
- package/README.md +218 -265
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +15 -151
- package/config/ppx.json.example +143 -0
- package/package.json +17 -10
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1340 -717
- package/src/agent/prompts.js +47 -3
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -592
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -188
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +188 -446
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -105
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -97
- package/src/mcp/admin.js +326 -0
- package/src/mcp/client.js +487 -375
- package/src/mcp/http.js +203 -0
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +392 -0
- package/src/mcp/tasks.js +133 -0
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -212
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/utils/json-state.js - agent 作用域 JSON 状态文件 (唯一实现, 2026-09-18 重构收敛)
|
|
2
|
+
// 原先 ans/{eviction,proactive,reward}.js 各写一份逐字节相同的 loadState/saveState
|
|
3
|
+
// (读: 缺失/损坏 → {}; 写: ensureDir + 同步写, 失败静默降级不打断主流程)。
|
|
4
|
+
// 统一后改语义只动这一处; 各模块仍导出自己的 loadState/saveState 保持对外接口不变。
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { readJson, writeJson } from "./store.js";
|
|
7
|
+
|
|
8
|
+
// 读: 缺失/损坏 → {} (对象语义); 复用 store.readJson (含去 BOM)
|
|
9
|
+
// statePath: 由调用方给出完整路径 (通常 agent.dataDir/memory/<name>.json)
|
|
10
|
+
export function loadStateFile(file) {
|
|
11
|
+
const s = readJson(file, null);
|
|
12
|
+
return (s && typeof s === "object") ? s : {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 写: 原子落盘; 失败静默降级不打断主流程
|
|
16
|
+
export function saveStateFile(file, state) {
|
|
17
|
+
try {
|
|
18
|
+
writeJson(file, state);
|
|
19
|
+
} catch { /* 状态落盘失败不影响主流程 */ }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// agent 作用域便捷封装: name 为 memory/<name>.json 的文件名
|
|
23
|
+
export function agentStatePath(agent, name) {
|
|
24
|
+
return path.join(agent.dataDir, "memory", `${name}.json`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loadAgentState(agent, name) {
|
|
28
|
+
return loadStateFile(agentStatePath(agent, name));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function saveAgentState(agent, name, state) {
|
|
32
|
+
return saveStateFile(agentStatePath(agent, name), state);
|
|
33
|
+
}
|
package/src/utils/logger.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// src/utils/logger.js - 分级日志
|
|
2
|
-
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
3
|
-
|
|
4
|
-
let minLevel = LEVELS.info;
|
|
5
|
-
|
|
6
|
-
export function setLevel(lv) {
|
|
7
|
-
if (lv in LEVELS) minLevel = LEVELS[lv];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function ts() {
|
|
11
|
-
return new Date().toISOString();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function debug(...a) { if (minLevel <= LEVELS.debug) console.log(`[${ts()}] [debug]`, ...a); }
|
|
15
|
-
export function info(...a) { if (minLevel <= LEVELS.info) console.log(`[${ts()}] [info]`, ...a); }
|
|
16
|
-
export function warn(...a) { if (minLevel <= LEVELS.warn) console.log(`[${ts()}] [warn]`, ...a); }
|
|
17
|
-
export function error(...a) { if (minLevel <= LEVELS.error) console.error(`[${ts()}] [error]`, ...a); }
|
|
1
|
+
// src/utils/logger.js - 分级日志
|
|
2
|
+
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
3
|
+
|
|
4
|
+
let minLevel = LEVELS.info;
|
|
5
|
+
|
|
6
|
+
export function setLevel(lv) {
|
|
7
|
+
if (lv in LEVELS) minLevel = LEVELS[lv];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ts() {
|
|
11
|
+
return new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function debug(...a) { if (minLevel <= LEVELS.debug) console.log(`[${ts()}] [debug]`, ...a); }
|
|
15
|
+
export function info(...a) { if (minLevel <= LEVELS.info) console.log(`[${ts()}] [info]`, ...a); }
|
|
16
|
+
export function warn(...a) { if (minLevel <= LEVELS.warn) console.log(`[${ts()}] [warn]`, ...a); }
|
|
17
|
+
export function error(...a) { if (minLevel <= LEVELS.error) console.error(`[${ts()}] [error]`, ...a); }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/utils/ndjson.js - 尾随 NDJSON (换行分隔 JSON) 流工具 (唯一实现, 2026-09-18 重构收敛)
|
|
2
|
+
// 背景: orchestrator/legion.js 读子进程 stdout 与 orchestrator/agent-worker.js 读 stdin,
|
|
3
|
+
// 各写一份逐字相同的「累积缓冲 → 按 \n 切行 → 跳过空行」循环; 写侧同样散落 6 处
|
|
4
|
+
// `stream.write(JSON.stringify(obj) + "\n")`。缓冲边界是最易出错的点, 收敛为单一实现。
|
|
5
|
+
|
|
6
|
+
// 创建行读取器: 把 data 分片喂进去, 每切出完整一行就回调 onLine(已 trim 的行内容)。
|
|
7
|
+
// 残行留在内部缓冲等下一片。返回的函数直接挂到 stream 的 "data" 事件上。
|
|
8
|
+
export function createLineReader(onLine) {
|
|
9
|
+
let buf = "";
|
|
10
|
+
return (chunk) => {
|
|
11
|
+
buf += chunk;
|
|
12
|
+
let idx;
|
|
13
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
14
|
+
const line = buf.slice(0, idx).trim();
|
|
15
|
+
buf = buf.slice(idx + 1);
|
|
16
|
+
if (!line) continue;
|
|
17
|
+
onLine(line);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 把对象序列化成一行 NDJSON 并写入流 (进程间消息收发的统一写法)
|
|
23
|
+
export function writeLine(stream, obj) {
|
|
24
|
+
stream.write(JSON.stringify(obj) + "\n");
|
|
25
|
+
}
|
package/src/utils/pii.js
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
// src/utils/pii.js - PII / 凭证检测与脱敏 (架构参考 openhanako pii-guard)
|
|
2
|
-
const HARD_PATTERNS = [
|
|
3
|
-
{ name: "api_key", regex: /\b(sk-[a-zA-Z0-9]{20,}|AKIA[A-Z0-9]{16}|gsk_[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|glpat-[a-zA-Z0-9_-]{20,}|xoxb-[a-zA-Z0-9-]+)\b/g },
|
|
4
|
-
// v1.0.8: 放宽 inline_secret 值域 (含 :/# 等) + 8 位起, 短密钥不漏检 (原 16+ 位且不含 :#)
|
|
5
|
-
// P0 (2026-09-15): key 后允许可选闭合引号 —— 原版只匹配 key=value / key: value,
|
|
6
|
-
// JSON 序列化形式 "key":"value" (key 后先有闭合引号) 完全漏检, 已修。
|
|
7
|
-
{ name: "inline_secret", regex: /\b(api[_-]?key|secret[_-]?key|access[_-]?token|auth[_-]?token|password|bearer)["']?\s*[:=]\s*["']?([a-zA-Z0-9_/+=\-.:#]{8,})["']?/gi },
|
|
8
|
-
{ name: "private_key", regex: /-----BEGIN\s+(RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
9
|
-
{ name: "credit_card", regex: /\b(?:\d{4}[- ]?){3}\d{4}\b/g },
|
|
10
|
-
{ name: "id_card", regex: /\b\d{6}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]\b/g },
|
|
11
|
-
{ name: "ssn", regex: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
12
|
-
// v1.0.8: 邮箱 + 中国大陆手机号 (11 位, 1[3-9] 开头)
|
|
13
|
-
{ name: "email", regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g },
|
|
14
|
-
{ name: "phone", regex: /\b1[3-9]\d{9}\b/g },
|
|
15
|
-
// v1.6.0-dev: URL query 里的敏感参数值 (token/key/secret/sign 等) — 最常见的凭证泄漏渠道, 此前未覆盖
|
|
16
|
-
// 仅匹配参数名白名单, 避免误伤合法 URL 参数; 保留参数名, 只替换值
|
|
17
|
-
{ name: "url_secret", regex: /([?&](?:token|key|secret|api[_-]?key|access[_-]?token|auth|sign|sig|password)=)[^&#\s"']*/gi, redact: (m, pre) => pre + "[REDACTED]" },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
export function scrubPII(text) {
|
|
21
|
-
if (!text) return { cleaned: text, detected: [] };
|
|
22
|
-
const detected = [];
|
|
23
|
-
let cleaned = text;
|
|
24
|
-
for (const { name, regex, redact } of HARD_PATTERNS) {
|
|
25
|
-
regex.lastIndex = 0;
|
|
26
|
-
if (regex.test(cleaned)) {
|
|
27
|
-
detected.push(name);
|
|
28
|
-
regex.lastIndex = 0;
|
|
29
|
-
cleaned = cleaned.replace(regex, redact ? redact : "[REDACTED]");
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return { cleaned, detected };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function hasPII(text) {
|
|
36
|
-
if (!text) return false;
|
|
37
|
-
for (const { regex } of HARD_PATTERNS) {
|
|
38
|
-
regex.lastIndex = 0;
|
|
39
|
-
if (regex.test(text)) return true;
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
1
|
+
// src/utils/pii.js - PII / 凭证检测与脱敏 (架构参考 openhanako pii-guard)
|
|
2
|
+
const HARD_PATTERNS = [
|
|
3
|
+
{ name: "api_key", regex: /\b(sk-[a-zA-Z0-9]{20,}|AKIA[A-Z0-9]{16}|gsk_[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|glpat-[a-zA-Z0-9_-]{20,}|xoxb-[a-zA-Z0-9-]+)\b/g },
|
|
4
|
+
// v1.0.8: 放宽 inline_secret 值域 (含 :/# 等) + 8 位起, 短密钥不漏检 (原 16+ 位且不含 :#)
|
|
5
|
+
// P0 (2026-09-15): key 后允许可选闭合引号 —— 原版只匹配 key=value / key: value,
|
|
6
|
+
// JSON 序列化形式 "key":"value" (key 后先有闭合引号) 完全漏检, 已修。
|
|
7
|
+
{ name: "inline_secret", regex: /\b(api[_-]?key|secret[_-]?key|access[_-]?token|auth[_-]?token|password|bearer)["']?\s*[:=]\s*["']?([a-zA-Z0-9_/+=\-.:#]{8,})["']?/gi },
|
|
8
|
+
{ name: "private_key", regex: /-----BEGIN\s+(RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
9
|
+
{ name: "credit_card", regex: /\b(?:\d{4}[- ]?){3}\d{4}\b/g },
|
|
10
|
+
{ name: "id_card", regex: /\b\d{6}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx]\b/g },
|
|
11
|
+
{ name: "ssn", regex: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
12
|
+
// v1.0.8: 邮箱 + 中国大陆手机号 (11 位, 1[3-9] 开头)
|
|
13
|
+
{ name: "email", regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g },
|
|
14
|
+
{ name: "phone", regex: /\b1[3-9]\d{9}\b/g },
|
|
15
|
+
// v1.6.0-dev: URL query 里的敏感参数值 (token/key/secret/sign 等) — 最常见的凭证泄漏渠道, 此前未覆盖
|
|
16
|
+
// 仅匹配参数名白名单, 避免误伤合法 URL 参数; 保留参数名, 只替换值
|
|
17
|
+
{ name: "url_secret", regex: /([?&](?:token|key|secret|api[_-]?key|access[_-]?token|auth|sign|sig|password)=)[^&#\s"']*/gi, redact: (m, pre) => pre + "[REDACTED]" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function scrubPII(text) {
|
|
21
|
+
if (!text) return { cleaned: text, detected: [] };
|
|
22
|
+
const detected = [];
|
|
23
|
+
let cleaned = text;
|
|
24
|
+
for (const { name, regex, redact } of HARD_PATTERNS) {
|
|
25
|
+
regex.lastIndex = 0;
|
|
26
|
+
if (regex.test(cleaned)) {
|
|
27
|
+
detected.push(name);
|
|
28
|
+
regex.lastIndex = 0;
|
|
29
|
+
cleaned = cleaned.replace(regex, redact ? redact : "[REDACTED]");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { cleaned, detected };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function hasPII(text) {
|
|
36
|
+
if (!text) return false;
|
|
37
|
+
for (const { regex } of HARD_PATTERNS) {
|
|
38
|
+
regex.lastIndex = 0;
|
|
39
|
+
if (regex.test(text)) return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/utils/rate-limit.js - 每来源令牌桶限流 (唯一实现, 2026-09-18 重构收敛)
|
|
2
|
+
// 原先 channels/http.js 与 aml-server.js 各写一份同逻辑令牌桶 (后者注释自述"对齐 http.js"),
|
|
3
|
+
// 且 aml 那份缺过期回收 —— 桶表只增不减, 长期运行 + 多变源地址会持续吃内存。
|
|
4
|
+
// 统一后两处共享同一实现与回收策略。
|
|
5
|
+
//
|
|
6
|
+
// 用法:
|
|
7
|
+
// const limiter = new TokenBucket({ perMin: 60 });
|
|
8
|
+
// if (!limiter.take(ip)) { /* 回 429 */ }
|
|
9
|
+
export const RATE_WINDOW_MS = 60_000;
|
|
10
|
+
// 桶数超过此值时顺带回收过期桶 (摊还 O(1), 不引入定时器)
|
|
11
|
+
const DEFAULT_SWEEP_AT = 512;
|
|
12
|
+
|
|
13
|
+
export class TokenBucket {
|
|
14
|
+
constructor({ perMin = 60, windowMs = RATE_WINDOW_MS, sweepAt = DEFAULT_SWEEP_AT } = {}) {
|
|
15
|
+
this.perMin = perMin;
|
|
16
|
+
this.windowMs = windowMs;
|
|
17
|
+
this.sweepAt = sweepAt;
|
|
18
|
+
// 公开 Map: 调用方可直接观测/清理 (http 通道的测试据此注入过期桶)
|
|
19
|
+
this.buckets = new Map(); // key -> { tokens, last }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 取一个令牌。返回 false 表示本窗口已耗尽 (调用方负责写 429)。
|
|
23
|
+
take(key) {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
let b = this.buckets.get(key);
|
|
26
|
+
if (!b) {
|
|
27
|
+
this.sweep(now);
|
|
28
|
+
b = { tokens: this.perMin, last: now };
|
|
29
|
+
this.buckets.set(key, b);
|
|
30
|
+
}
|
|
31
|
+
// 按窗口整数倍补充令牌 (上限 perMin)
|
|
32
|
+
const refill = Math.floor((now - b.last) / this.windowMs);
|
|
33
|
+
if (refill > 0) {
|
|
34
|
+
b.tokens = Math.min(this.perMin, b.tokens + refill * this.perMin);
|
|
35
|
+
b.last = now;
|
|
36
|
+
}
|
|
37
|
+
if (b.tokens <= 0) return false;
|
|
38
|
+
b.tokens -= 1;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 回收连续两个窗口未活动的桶 (摊还成本, 防内存无界增长)
|
|
43
|
+
sweep(now = Date.now()) {
|
|
44
|
+
if (this.buckets.size <= this.sweepAt) return;
|
|
45
|
+
const stale = now - this.windowMs * 2;
|
|
46
|
+
for (const [k, b] of this.buckets) {
|
|
47
|
+
if (b.last < stale) this.buckets.delete(k);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/utils/schema.js - 数据文件 schema 版本 + 迁移钩子
|
|
2
|
+
// 目标: 记忆数据文件 (facts.json / scenes.json) 保持纯数组格式不变 (现有读取者 healer/测试全部无感),
|
|
3
|
+
// 版本号写在旁挂的 <file>.schema 小文件中; 数据结构升级时注册迁移函数, 启动自动迁移。
|
|
4
|
+
// 设计:
|
|
5
|
+
// - 无 .schema 文件的历史数据一律视为 v1 (基线, 纯数组格式)
|
|
6
|
+
// - registerMigration(name, from, to, fn) 注册迁移链, 不允许跳级, 不允许覆盖
|
|
7
|
+
// - migrateData() 读当前版本 -> 沿迁移链推进 -> 原子写回数据 + 更新 .schema 文件
|
|
8
|
+
// - 崩溃安全: 迁移在原子写回后版本才推进, 中断重跑幂等
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { atomicWrite, ensureDir, readJson } from "./store.js";
|
|
11
|
+
|
|
12
|
+
// 基线版本: 无 .schema 文件 (历史纯数组格式) 一律视为 v1
|
|
13
|
+
export const SCHEMA_BASE_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
// name -> Map<fromVersion, { to, fn }>
|
|
16
|
+
const MIGRATIONS = new Map();
|
|
17
|
+
|
|
18
|
+
// 注册迁移: from 版本 -> to 版本 (to 必须 > from)
|
|
19
|
+
// fn(data, { from, to }) => 迁移后的 data
|
|
20
|
+
export function registerMigration(name, from, to, fn) {
|
|
21
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || to <= from) {
|
|
22
|
+
throw new Error(`非法迁移版本: ${name} ${from}->${to}`);
|
|
23
|
+
}
|
|
24
|
+
if (typeof fn !== "function") throw new Error(`迁移函数缺失: ${name} v${from}`);
|
|
25
|
+
let chain = MIGRATIONS.get(name);
|
|
26
|
+
if (!chain) { chain = new Map(); MIGRATIONS.set(name, chain); }
|
|
27
|
+
if (chain.has(from)) throw new Error(`迁移冲突: ${name} v${from} 已注册`);
|
|
28
|
+
chain.set(from, { to, fn });
|
|
29
|
+
return { name, from, to };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function schemaFileOf(file) {
|
|
33
|
+
return file + ".schema";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 读当前数据文件版本 (无 .schema 文件视为基线 v1)
|
|
37
|
+
export function readSchema(file) {
|
|
38
|
+
const meta = readJson(schemaFileOf(file), null);
|
|
39
|
+
if (!meta || !Number.isInteger(meta.version)) return SCHEMA_BASE_VERSION;
|
|
40
|
+
return meta.version;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 写版本 (原子)
|
|
44
|
+
export function writeSchema(file, name, version) {
|
|
45
|
+
const p = schemaFileOf(file);
|
|
46
|
+
ensureDir(path.dirname(p));
|
|
47
|
+
atomicWrite(p, JSON.stringify({ name, version, updatedAt: new Date().toISOString() }, null, 2));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 把数据文件迁移到 currentVersion: 沿注册链逐级推进, 无迁移函数则安全跳过 (版本直接标记到目标, 数据不动)。
|
|
51
|
+
// 返回 { from(初始版本), to(最终版本), applied, data } —— applied 为实际执行的迁移步骤列表
|
|
52
|
+
export function migrateData({ file, name, data, currentVersion, logger = null }) {
|
|
53
|
+
const chain = MIGRATIONS.get(name) || new Map();
|
|
54
|
+
const start = readSchema(file);
|
|
55
|
+
let from = start;
|
|
56
|
+
let d = data;
|
|
57
|
+
const applied = [];
|
|
58
|
+
let guard = 0;
|
|
59
|
+
while (from < currentVersion) {
|
|
60
|
+
if (++guard > 20) throw new Error(`迁移链疑似死循环: ${name} v${from}`);
|
|
61
|
+
const step = chain.get(from);
|
|
62
|
+
if (!step) {
|
|
63
|
+
// 没有迁移函数但仍落后于目标: 记录说明并直接标记版本 (数据保持原样)
|
|
64
|
+
if (logger) logger(`schema ${name}: v${from} -> v${currentVersion} 无迁移函数, 数据保持原样`);
|
|
65
|
+
applied.push({ from, to: currentVersion, note: "no-migration" });
|
|
66
|
+
from = currentVersion;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (step.to > currentVersion) throw new Error(`迁移越界: ${name} v${from}->v${step.to} 超过目标 v${currentVersion}`);
|
|
70
|
+
d = step.fn(d, { from, to: step.to });
|
|
71
|
+
applied.push({ from, to: step.to });
|
|
72
|
+
from = step.to;
|
|
73
|
+
}
|
|
74
|
+
if (applied.length) {
|
|
75
|
+
// 迁移产生新数据: 原子写回 (rename 覆盖, 崩溃安全), 版本随 .schema 文件推进
|
|
76
|
+
atomicWrite(file, JSON.stringify(d, null, 2));
|
|
77
|
+
}
|
|
78
|
+
writeSchema(file, name, from);
|
|
79
|
+
return { from: start, to: from, applied, data: d };
|
|
80
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/utils/similarity.js - 中文文本相似度的公共基元 (唯一实现, 2026-09-18 重构收敛)
|
|
2
|
+
// 背景: "字符级 bigram + 交集" 的相似度思路在 fact-store / experience / eviction 三处各写一份,
|
|
3
|
+
// 集合运算 (交集大小 / Jaccard / overlap) 完全重复。这里只收敛「集合级运算 + 字符 bigram 分词」,
|
|
4
|
+
// 各模块保留自己的分词策略与阈值 (fact-store 用 scope 感知的 _bigramSet, 语义不同, 不强行合并)。
|
|
5
|
+
// 零依赖, 纯函数。
|
|
6
|
+
|
|
7
|
+
// 字符级 bigram: 去空白 + 按码点取相邻两字符 (对中文简繁/词序变化有容错)
|
|
8
|
+
// 与 experience/_bigrams、eviction/_bigramSet 原实现一致 (Array.from 按码点切分, astral 字符不劈半)
|
|
9
|
+
export function charBigrams(s) {
|
|
10
|
+
const chars = Array.from(String(s ?? "").replace(/\s+/g, ""));
|
|
11
|
+
const out = new Set();
|
|
12
|
+
for (let i = 0; i < chars.length - 1; i++) out.add(chars[i] + chars[i + 1]);
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 集合交集大小
|
|
17
|
+
export function intersectSize(A, B) {
|
|
18
|
+
let inter = 0;
|
|
19
|
+
for (const x of A) if (B.has(x)) inter++;
|
|
20
|
+
return inter;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Jaccard 系数: 交集 / 并集 (0~1)
|
|
24
|
+
export function setJaccard(A, B) {
|
|
25
|
+
if (!A.size || !B.size) return 0;
|
|
26
|
+
const union = A.size + B.size - intersectSize(A, B);
|
|
27
|
+
return union ? intersectSize(A, B) / union : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// overlap 系数: 交集 / 较短集合 (0~1) —— 对"共享核心词但词序/措辞大变"比 Jaccard 更敏感
|
|
31
|
+
export function setOverlap(A, B) {
|
|
32
|
+
if (!A.size || !B.size) return 0;
|
|
33
|
+
return intersectSize(A, B) / Math.min(A.size, B.size);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 文本级便捷封装 (字符 bigram)
|
|
37
|
+
export function overlapCoefficient(a, b) {
|
|
38
|
+
return setOverlap(charBigrams(a), charBigrams(b));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function jaccardCoefficient(a, b) {
|
|
42
|
+
return setJaccard(charBigrams(a), charBigrams(b));
|
|
43
|
+
}
|
package/src/utils/store.js
CHANGED
|
@@ -1,108 +1,170 @@
|
|
|
1
|
-
// src/utils/store.js - 零依赖文件存储 (JSON + 原子写)
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
1
|
+
// src/utils/store.js - 零依赖文件存储 (JSON + 原子写)
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { debug, warn } from "../utils/logger.js";
|
|
6
|
+
|
|
7
|
+
export function ensureDir(dir) {
|
|
8
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
9
|
+
return dir;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function atomicWrite(file, data) {
|
|
13
|
+
ensureDir(path.dirname(file));
|
|
14
|
+
const tmp = file + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
|
|
15
|
+
fs.writeFileSync(tmp, data, "utf8");
|
|
16
|
+
// v1.0.8: rename 覆盖已存在文件在 Windows 并发下可能 EPERM/EEXIST (短窗口), 重试 3 次;
|
|
17
|
+
// 不再降级为非原子直接写 (并发双写可交错损坏文件), 重试仍失败则抛错由调用方处理
|
|
18
|
+
for (let attempt = 0; ; attempt++) {
|
|
19
|
+
try {
|
|
20
|
+
fs.renameSync(tmp, file);
|
|
21
|
+
return;
|
|
22
|
+
} catch (e) {
|
|
23
|
+
if (attempt >= 2) {
|
|
24
|
+
try { fs.unlinkSync(tmp); } catch (e) { debug(`[utils/store] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
25
|
+
throw new Error(`原子写失败: ${file} (${e.message})`);
|
|
26
|
+
}
|
|
27
|
+
const end = Date.now() + 30;
|
|
28
|
+
while (Date.now() < end) {} // 短延迟后重试
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readJson(file, fallback = null) {
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(file)) return fallback;
|
|
36
|
+
let raw = fs.readFileSync(file, "utf8");
|
|
37
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1); // 去 BOM
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// v3.0.1 (P0#1): 解析失败不再完全静默 —— 至少留一条 warn。
|
|
41
|
+
// 注意: 此处**不动文件** (不自作主张改名/删除) —— healer 等组件对损坏文件
|
|
42
|
+
// 有自己的备份恢复契约 (如 healer.js 把 facts.json 改名为 .corrupt-<ts> 后重建),
|
|
43
|
+
// 低层 readJson 若抢先把文件改名会破坏该契约 (实测把 selfheal-bench 炸出 ENOENT)。
|
|
44
|
+
warn(`[utils/store] JSON 解析失败, 返回 fallback (文件保留): ${file} (${e && e.message ? e.message : e})`);
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 带信号的 readJson: 供"读-改-写"型存储 (如 FactStore) 区分「文件不存在」与「文件损坏」。
|
|
50
|
+
// 损坏时 (parseFailed=true) 调用方必须避免立即用 fallback 覆盖回写 —— 文件原地保留,
|
|
51
|
+
// 交给 healer / 人工恢复。非破坏式: 本函数同样不改名不删除。
|
|
52
|
+
export function readJsonGuarded(file, fallback = null) {
|
|
53
|
+
const existedBefore = fs.existsSync(file);
|
|
54
|
+
if (!existedBefore) return { data: fallback, existedBefore: false, parseFailed: false };
|
|
55
|
+
try {
|
|
56
|
+
let raw = fs.readFileSync(file, "utf8");
|
|
57
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
58
|
+
return { data: JSON.parse(raw), existedBefore: true, parseFailed: false };
|
|
59
|
+
} catch (e) {
|
|
60
|
+
warn(`[utils/store] JSON 损坏 (文件保留待恢复): ${file} (${e && e.message ? e.message : e})`);
|
|
61
|
+
return { data: fallback, existedBefore: true, parseFailed: true };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function writeJson(file, obj) {
|
|
66
|
+
atomicWrite(file, JSON.stringify(obj, null, 2));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 简单跨进程文件锁 (零依赖, 同步): 原子创建 .lock 文件, 临界区内执行 fn, finally 释放
|
|
70
|
+
// 用于跨 agent 共享文件的"读-改-写"临界区 (防并发覆盖, 如共享经验库)
|
|
71
|
+
// v3.0.1 (P1#5) 两处修复:
|
|
72
|
+
// ① 锁文件写 "pid:ts", 超时后先判持有者存活 —— 活进程持锁时不许抢 (原实现无脑强删锁文件,
|
|
73
|
+
// 持有者还在临界区就出现双写, 互斥被破坏 → 数据交错损坏); 死进程/超长持有/自进程残留才强取。
|
|
74
|
+
// ② 等待用 Atomics.wait (OS 级阻塞, 零 CPU), 替代原自旋空转 (20ms×150 轮纯烧 CPU 且卡事件循环)。
|
|
75
|
+
const _sleepArr = new Int32Array(new SharedArrayBuffer(4));
|
|
76
|
+
function _syncSleep(ms) {
|
|
77
|
+
try { Atomics.wait(_sleepArr, 0, 0, ms); } catch { const end = Date.now() + ms; while (Date.now() < end) {} }
|
|
78
|
+
}
|
|
79
|
+
function _pidAlive(pid) {
|
|
80
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e && e.code === "EPERM"; } // EPERM=存在但无权限
|
|
81
|
+
}
|
|
82
|
+
export function withFileLock(file, fn, { timeoutMs = 3000, pollMs = 20, staleMs = 15000 } = {}) {
|
|
83
|
+
const lock = file + ".lock";
|
|
84
|
+
const acquire = () => {
|
|
85
|
+
try {
|
|
86
|
+
const fd = fs.openSync(lock, "wx"); // 'wx': 已存在则抛错 (原子)
|
|
87
|
+
try { fs.writeSync(fd, `${process.pid}:${Date.now()}`); } catch (e) { debug(`[utils/store] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
88
|
+
fs.closeSync(fd);
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
// 陈旧锁判定 (v3.0.1 P1#5): 持有者进程已死 / 锁超长持有 / 自进程残留 → 可抢; 活跃进程持有 → 不可抢
|
|
95
|
+
const canSteal = () => {
|
|
96
|
+
try {
|
|
97
|
+
const raw = fs.readFileSync(lock, "utf8").trim();
|
|
98
|
+
const m = /^(\d+):(\d+)$/.exec(raw);
|
|
99
|
+
if (m) {
|
|
100
|
+
const pid = Number(m[1]);
|
|
101
|
+
if (pid === process.pid) return true; // 自进程残留
|
|
102
|
+
if (!_pidAlive(pid)) return true; // 持有者已死 (崩溃残留)
|
|
103
|
+
if (Date.now() - Number(m[2]) > staleMs) return true; // 超长持有视为陈旧 (防 pid 复用误判)
|
|
104
|
+
return false; // 活跃进程持锁: 不抢
|
|
105
|
+
}
|
|
106
|
+
// 兼容旧格式 (纯 pid) / 无法解析: 按 mtime 与 pid 存活判
|
|
107
|
+
const legacyPid = parseInt(raw, 10);
|
|
108
|
+
if (Number.isFinite(legacyPid) && legacyPid !== process.pid && !_pidAlive(legacyPid)) return true;
|
|
109
|
+
const st = fs.statSync(lock);
|
|
110
|
+
return Date.now() - st.mtimeMs > staleMs;
|
|
111
|
+
} catch {
|
|
112
|
+
return true; // 锁文件刚被释放/读不到 → 直接尝试抢
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const start = Date.now();
|
|
116
|
+
while (!acquire()) {
|
|
117
|
+
if (Date.now() - start > timeoutMs) {
|
|
118
|
+
// v3.0.1 (P1#5): 活跃进程持锁 → 抛错而非抢锁 (宁可失败不可双写损坏)
|
|
119
|
+
if (!canSteal()) throw new Error(`文件锁被活跃进程持有, 等待 ${timeoutMs}ms 后放弃: ` + lock);
|
|
120
|
+
try { fs.rmSync(lock, { force: true }); } catch (e) { debug(`[utils/store] 已忽略异常: ${e && e.message ? e.message : e}`); } // 陈旧锁强取
|
|
121
|
+
if (!acquire()) throw new Error("文件锁获取超时: " + lock);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
_syncSleep(pollMs);
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
return fn();
|
|
128
|
+
} finally {
|
|
129
|
+
try { fs.rmSync(lock, { force: true }); } catch (e) { debug(`[utils/store] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function readText(file, fallback = "") {
|
|
134
|
+
try {
|
|
135
|
+
if (!fs.existsSync(file)) return fallback;
|
|
136
|
+
return fs.readFileSync(file, "utf8");
|
|
137
|
+
} catch {
|
|
138
|
+
return fallback;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function writeText(file, text) {
|
|
143
|
+
atomicWrite(file, text);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function appendLine(file, line) {
|
|
147
|
+
ensureDir(path.dirname(file));
|
|
148
|
+
fs.appendFileSync(file, line + "\n", "utf8");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function nowISO() {
|
|
152
|
+
return new Date().toISOString();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function logicalDay(d = new Date()) {
|
|
156
|
+
// 逻辑日 (本地时区): 今日/归档/按天检索统一用本地年月日
|
|
157
|
+
// 与 eventsByDay() 的本地解析保持一致, 避免 8 小时时区偏移导致按天检索错位
|
|
158
|
+
const y = d.getFullYear();
|
|
159
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
160
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
161
|
+
return `${y}-${m}-${day}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 按逻辑日切分 JSONL 的日志目录 (工具轨迹 utils/trace.js 与事件流 core/trace.js 共用)。
|
|
165
|
+
// 两个写入器必须落在同一目录, 用同一函数取路径可避免字面量各写一份后悄悄漂移。
|
|
166
|
+
export function logsDir(dataDir, sub = "traces") {
|
|
167
|
+
const dir = path.join(dataDir, "logs", sub);
|
|
168
|
+
ensureDir(dir);
|
|
169
|
+
return dir;
|
|
170
|
+
}
|