skyloom 1.13.6 → 1.13.8
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/.github/workflows/ci.yml +36 -36
- package/README.md +220 -159
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/commands_md.d.ts +41 -0
- package/dist/cli/commands_md.d.ts.map +1 -0
- package/dist/cli/commands_md.js +140 -0
- package/dist/cli/commands_md.js.map +1 -0
- package/dist/cli/input_macros.d.ts +28 -0
- package/dist/cli/input_macros.d.ts.map +1 -0
- package/dist/cli/input_macros.js +120 -0
- package/dist/cli/input_macros.js.map +1 -0
- package/dist/cli/loom.d.ts +220 -0
- package/dist/cli/loom.d.ts.map +1 -0
- package/dist/cli/loom.js +1094 -0
- package/dist/cli/loom.js.map +1 -0
- package/dist/cli/loom_chat.d.ts +20 -0
- package/dist/cli/loom_chat.d.ts.map +1 -0
- package/dist/cli/loom_chat.js +685 -0
- package/dist/cli/loom_chat.js.map +1 -0
- package/dist/cli/main.js +310 -14
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +7 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent.d.ts +20 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +199 -16
- package/dist/core/agent.js.map +1 -1
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +34 -2
- package/dist/core/factory.js.map +1 -1
- package/dist/core/file_checkpoint.d.ts +57 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -0
- package/dist/core/file_checkpoint.js +162 -0
- package/dist/core/file_checkpoint.js.map +1 -0
- package/dist/core/hooks.d.ts +43 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +110 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +15 -9
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/mcp.d.ts +16 -0
- package/dist/core/mcp.d.ts.map +1 -1
- package/dist/core/mcp.js +55 -0
- package/dist/core/mcp.js.map +1 -1
- package/dist/core/model_config.d.ts +40 -0
- package/dist/core/model_config.d.ts.map +1 -0
- package/dist/core/model_config.js +191 -0
- package/dist/core/model_config.js.map +1 -0
- package/dist/core/skill.d.ts +7 -0
- package/dist/core/skill.d.ts.map +1 -1
- package/dist/core/skill.js +47 -0
- package/dist/core/skill.js.map +1 -1
- package/dist/core/skymd.d.ts +39 -0
- package/dist/core/skymd.d.ts.map +1 -0
- package/dist/core/skymd.js +177 -0
- package/dist/core/skymd.js.map +1 -0
- package/dist/core/tool.d.ts +12 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +30 -0
- package/dist/core/tool.js.map +1 -1
- package/dist/core/verify.d.ts +27 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/core/verify.js +62 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/skills/loader.d.ts +22 -2
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +45 -15
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +13 -3
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/model_tool.d.ts +11 -0
- package/dist/tools/model_tool.d.ts.map +1 -0
- package/dist/tools/model_tool.js +71 -0
- package/dist/tools/model_tool.js.map +1 -0
- package/dist/tools/todo.d.ts +30 -0
- package/dist/tools/todo.d.ts.map +1 -0
- package/dist/tools/todo.js +78 -0
- package/dist/tools/todo.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -144
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +68 -68
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/commands_md.ts +112 -0
- package/src/cli/input_macros.ts +83 -0
- package/src/cli/loom.ts +982 -0
- package/src/cli/loom_chat.ts +598 -0
- package/src/cli/main.ts +255 -9
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +228 -222
- package/src/core/agent/guard.ts +134 -134
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +195 -16
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -178
- package/src/core/checkpoint.ts +94 -94
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +31 -2
- package/src/core/file_checkpoint.ts +136 -0
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/hooks.ts +126 -0
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +15 -9
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp.ts +48 -0
- package/src/core/mcp_server.ts +176 -176
- package/src/core/model_config.ts +157 -0
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +42 -0
- package/src/core/skymd.ts +143 -0
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +30 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/verify.ts +71 -0
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +45 -16
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +13 -3
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/tools/model_tool.ts +74 -0
- package/src/tools/todo.ts +76 -0
- package/src/web/tts.ts +93 -93
- package/tests/agent.test.ts +159 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -0
- package/tests/claude_compat.test.ts +110 -0
- package/tests/config.test.ts +41 -41
- package/tests/guard.test.ts +75 -75
- package/tests/icons.test.ts +45 -45
- package/tests/loom.test.ts +248 -0
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/skymd.test.ts +146 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/tests/tui.test.ts +67 -67
- package/vitest.config.ts +17 -17
- package/=12 +0 -0
- package/=8 +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件级检查点 — snapshot files before agents mutate them; /rewind restores.
|
|
3
|
+
*
|
|
4
|
+
* Every chat turn / task opens a checkpoint "turn". Before write_file /
|
|
5
|
+
* edit_file / delete_file executes, the target's current content (or its
|
|
6
|
+
* absence) is snapshotted — first touch per path per turn wins, so a rewind
|
|
7
|
+
* restores the state from *before* the turn began. Lets users hand agents
|
|
8
|
+
* risky changes and undo them in one command, without involving git.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately session-scoped and in-memory (like Claude Code checkpoints):
|
|
11
|
+
* not a git replacement, and `run_bash` side effects cannot be rewound.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { getLogger } from './logger';
|
|
17
|
+
|
|
18
|
+
const log = getLogger('checkpoint');
|
|
19
|
+
|
|
20
|
+
interface FileSnapshot {
|
|
21
|
+
/** Absolute path. */
|
|
22
|
+
path: string;
|
|
23
|
+
/** Content before the turn, or null if the file did not exist. */
|
|
24
|
+
content: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CheckpointTurn {
|
|
28
|
+
id: number;
|
|
29
|
+
label: string;
|
|
30
|
+
at: Date;
|
|
31
|
+
snapshots: Map<string, FileSnapshot>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MAX_TURNS = 50;
|
|
35
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024; // skip snapshotting monsters
|
|
36
|
+
const MUTATING_TOOL_RE = /^(write_file|edit_file|delete_file)$/;
|
|
37
|
+
|
|
38
|
+
class FileCheckpointStore {
|
|
39
|
+
private turns: CheckpointTurn[] = [];
|
|
40
|
+
private current: CheckpointTurn | null = null;
|
|
41
|
+
private seq = 0;
|
|
42
|
+
|
|
43
|
+
/** Open a new turn; subsequent snapshots attach to it. */
|
|
44
|
+
beginTurn(label: string): void {
|
|
45
|
+
// An empty previous turn is replaced, not stacked.
|
|
46
|
+
if (this.current && this.current.snapshots.size === 0) {
|
|
47
|
+
this.turns.pop();
|
|
48
|
+
}
|
|
49
|
+
this.current = { id: ++this.seq, label: label.slice(0, 60), at: new Date(), snapshots: new Map() };
|
|
50
|
+
this.turns.push(this.current);
|
|
51
|
+
if (this.turns.length > MAX_TURNS) this.turns.shift();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Snapshot a path before mutation (first touch per turn wins). */
|
|
55
|
+
snapshot(rawPath: string): void {
|
|
56
|
+
if (!this.current) this.beginTurn('(implicit)');
|
|
57
|
+
const abs = path.resolve(rawPath);
|
|
58
|
+
if (this.current!.snapshots.has(abs)) return;
|
|
59
|
+
let content: string | null = null;
|
|
60
|
+
try {
|
|
61
|
+
if (fs.existsSync(abs)) {
|
|
62
|
+
const stat = fs.statSync(abs);
|
|
63
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return;
|
|
64
|
+
content = fs.readFileSync(abs, 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
log.warn('snapshot_failed', { path: abs, error: String(e) });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.current!.snapshots.set(abs, { path: abs, content });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Should this tool call be snapshotted? Returns the path to snapshot. */
|
|
74
|
+
pathToSnapshot(toolName: string, args: Record<string, any>): string | null {
|
|
75
|
+
if (!MUTATING_TOOL_RE.test(toolName)) return null;
|
|
76
|
+
const p = args?.path;
|
|
77
|
+
return typeof p === 'string' && p.trim() ? p : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Turns that actually captured changes, newest first. */
|
|
81
|
+
list(): Array<{ id: number; label: string; at: Date; files: string[] }> {
|
|
82
|
+
return this.turns
|
|
83
|
+
.filter(t => t.snapshots.size > 0)
|
|
84
|
+
.map(t => ({ id: t.id, label: t.label, at: t.at, files: [...t.snapshots.keys()] }))
|
|
85
|
+
.reverse();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Restore the last `count` non-empty turns (newest backwards). When the
|
|
90
|
+
* same file appears in several turns, the oldest snapshot wins — that is
|
|
91
|
+
* the state from before the earliest rewound turn.
|
|
92
|
+
*/
|
|
93
|
+
rewind(count: number = 1): { restored: string[]; deleted: string[]; turns: number } {
|
|
94
|
+
const nonEmpty = this.turns.filter(t => t.snapshots.size > 0);
|
|
95
|
+
const target = nonEmpty.slice(-count);
|
|
96
|
+
if (target.length === 0) return { restored: [], deleted: [], turns: 0 };
|
|
97
|
+
|
|
98
|
+
// oldest-first iteration: later assignments overwrite, so the oldest
|
|
99
|
+
// snapshot per path ends up in the map
|
|
100
|
+
const finalState = new Map<string, FileSnapshot>();
|
|
101
|
+
for (let i = target.length - 1; i >= 0; i--) {
|
|
102
|
+
for (const snap of target[i].snapshots.values()) finalState.set(snap.path, snap);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const restored: string[] = [];
|
|
106
|
+
const deleted: string[] = [];
|
|
107
|
+
for (const snap of finalState.values()) {
|
|
108
|
+
try {
|
|
109
|
+
if (snap.content === null) {
|
|
110
|
+
if (fs.existsSync(snap.path)) { fs.unlinkSync(snap.path); deleted.push(snap.path); }
|
|
111
|
+
} else {
|
|
112
|
+
fs.mkdirSync(path.dirname(snap.path), { recursive: true });
|
|
113
|
+
fs.writeFileSync(snap.path, snap.content, 'utf-8');
|
|
114
|
+
restored.push(snap.path);
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
log.warn('rewind_failed', { path: snap.path, error: String(e) });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Rewound turns are consumed.
|
|
122
|
+
const ids = new Set(target.map(t => t.id));
|
|
123
|
+
this.turns = this.turns.filter(t => !ids.has(t.id));
|
|
124
|
+
if (this.current && ids.has(this.current.id)) this.current = null;
|
|
125
|
+
return { restored, deleted, turns: target.length };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Test/reset hook. */
|
|
129
|
+
clear(): void { this.turns = []; this.current = null; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let _store: FileCheckpointStore | null = null;
|
|
133
|
+
export function getFileCheckpoints(): FileCheckpointStore {
|
|
134
|
+
if (!_store) _store = new FileCheckpointStore();
|
|
135
|
+
return _store;
|
|
136
|
+
}
|
package/src/core/filter.ts
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 输出过滤模块 — sensitive information sanitization.
|
|
3
|
-
*
|
|
4
|
-
* Before agent responses reach the user (or are persisted),
|
|
5
|
-
* scan for and redact sensitive patterns like API keys,
|
|
6
|
-
* tokens, passwords, PII, and internal paths.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/* ═══════════════════════════════════════
|
|
10
|
-
Detection patterns — compiled once at module load
|
|
11
|
-
═══════════════════════════════════════ */
|
|
12
|
-
const SENSITIVE_PATTERNS: Array<[RegExp, string]> = [
|
|
13
|
-
// API keys & tokens
|
|
14
|
-
[/sk-[a-zA-Z0-9]{32,}/g, "[REDACTED:API_KEY]"],
|
|
15
|
-
[/(?:api_key|apikey|secret_key|access_token|auth_token)\s*[:=]\s*["']?[^\s"']{8,}["']?/gi, "$1: [REDACTED]"],
|
|
16
|
-
[/ghp_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
|
|
17
|
-
[/gho_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
|
|
18
|
-
|
|
19
|
-
// AWS credentials
|
|
20
|
-
[/AKIA[0-9A-Z]{16}/g, "[REDACTED:AWS_KEY]"],
|
|
21
|
-
[/(?:aws_access_key_id|aws_secret_access_key)\s*[:=]\s*["']?[^\s"']+/gi, "$1: [REDACTED]"],
|
|
22
|
-
|
|
23
|
-
// Passwords
|
|
24
|
-
[/(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"']{4,}["']?/gi, "$1: [REDACTED]"],
|
|
25
|
-
[/(?:密码|口令)\s*[:=]\s*["']?[^\s"']{2,}["']?/g, "$1: [已脱敏]"],
|
|
26
|
-
|
|
27
|
-
// Connection strings
|
|
28
|
-
[/(?:mongodb|postgres|mysql|redis):\/\/[^\s]+/g, "[REDACTED:DB_URI]"],
|
|
29
|
-
[/(?:jdbc|odbc):[^\s]+/g, "[REDACTED:DB_URI]"],
|
|
30
|
-
|
|
31
|
-
// Private keys
|
|
32
|
-
[/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END .*?PRIVATE KEY-----/g, "[REDACTED:PRIVATE_KEY]"],
|
|
33
|
-
|
|
34
|
-
// IP addresses (local only)
|
|
35
|
-
[/192\.168\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
36
|
-
[/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
37
|
-
[/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
38
|
-
|
|
39
|
-
// File paths
|
|
40
|
-
[/(?:\/etc\/(?:passwd|shadow|hosts|sudoers))/g, "[REDACTED:SYSTEM_PATH]"],
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
/* Email masking (function-based, handled separately) */
|
|
44
|
-
const EMAIL_RE = /([a-zA-Z0-9._%+-]{3,})@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
/* ═══════════════════════════════════════
|
|
48
|
-
Filter function
|
|
49
|
-
═══════════════════════════════════════ */
|
|
50
|
-
export interface FilterResult {
|
|
51
|
-
clean: string;
|
|
52
|
-
redacted: boolean;
|
|
53
|
-
count: number;
|
|
54
|
-
details: string[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function filterOutput(text: string): FilterResult {
|
|
58
|
-
if (!text) return { clean: "", redacted: false, count: 0, details: [] };
|
|
59
|
-
|
|
60
|
-
let clean = text;
|
|
61
|
-
let count = 0;
|
|
62
|
-
const details: string[] = [];
|
|
63
|
-
|
|
64
|
-
// Email masking (function-based replacement)
|
|
65
|
-
let emailCount = 0;
|
|
66
|
-
clean = clean.replace(EMAIL_RE, (full, user, domain) => {
|
|
67
|
-
emailCount++;
|
|
68
|
-
return (user as string).slice(0, 2) + "***@" + (domain as string);
|
|
69
|
-
});
|
|
70
|
-
if (emailCount > 0) {
|
|
71
|
-
count += emailCount;
|
|
72
|
-
details.push(`Masked ${emailCount}x email addresses`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (const [pattern, replacement] of SENSITIVE_PATTERNS) {
|
|
76
|
-
const matches = clean.match(pattern);
|
|
77
|
-
if (matches) {
|
|
78
|
-
count += matches.length;
|
|
79
|
-
if (typeof replacement === "string") {
|
|
80
|
-
details.push(`Redacted ${matches.length}x ${pattern.source.slice(0, 30)}`);
|
|
81
|
-
} else {
|
|
82
|
-
details.push(`Masked ${matches.length}x email addresses`);
|
|
83
|
-
}
|
|
84
|
-
clean = clean.replace(pattern, replacement as string);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { clean, redacted: count > 0, count, details };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/* ═══════════════════════════════════════
|
|
92
|
-
Quick check — is filtering needed?
|
|
93
|
-
═══════════════════════════════════════ */
|
|
94
|
-
export function needsFiltering(text: string): boolean {
|
|
95
|
-
if (!text) return false;
|
|
96
|
-
// Quick scan with the most common patterns
|
|
97
|
-
if (/sk-[a-zA-Z0-9]{32,}/.test(text)) return true;
|
|
98
|
-
if (/api_key.*[:=]/.test(text)) return true;
|
|
99
|
-
if (/password.*[:=]/.test(text)) return true;
|
|
100
|
-
if (/-----BEGIN.*PRIVATE KEY-----/.test(text)) return true;
|
|
101
|
-
if (EMAIL_RE.test(text)) return true;
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 输出过滤模块 — sensitive information sanitization.
|
|
3
|
+
*
|
|
4
|
+
* Before agent responses reach the user (or are persisted),
|
|
5
|
+
* scan for and redact sensitive patterns like API keys,
|
|
6
|
+
* tokens, passwords, PII, and internal paths.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* ═══════════════════════════════════════
|
|
10
|
+
Detection patterns — compiled once at module load
|
|
11
|
+
═══════════════════════════════════════ */
|
|
12
|
+
const SENSITIVE_PATTERNS: Array<[RegExp, string]> = [
|
|
13
|
+
// API keys & tokens
|
|
14
|
+
[/sk-[a-zA-Z0-9]{32,}/g, "[REDACTED:API_KEY]"],
|
|
15
|
+
[/(?:api_key|apikey|secret_key|access_token|auth_token)\s*[:=]\s*["']?[^\s"']{8,}["']?/gi, "$1: [REDACTED]"],
|
|
16
|
+
[/ghp_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
|
|
17
|
+
[/gho_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
|
|
18
|
+
|
|
19
|
+
// AWS credentials
|
|
20
|
+
[/AKIA[0-9A-Z]{16}/g, "[REDACTED:AWS_KEY]"],
|
|
21
|
+
[/(?:aws_access_key_id|aws_secret_access_key)\s*[:=]\s*["']?[^\s"']+/gi, "$1: [REDACTED]"],
|
|
22
|
+
|
|
23
|
+
// Passwords
|
|
24
|
+
[/(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"']{4,}["']?/gi, "$1: [REDACTED]"],
|
|
25
|
+
[/(?:密码|口令)\s*[:=]\s*["']?[^\s"']{2,}["']?/g, "$1: [已脱敏]"],
|
|
26
|
+
|
|
27
|
+
// Connection strings
|
|
28
|
+
[/(?:mongodb|postgres|mysql|redis):\/\/[^\s]+/g, "[REDACTED:DB_URI]"],
|
|
29
|
+
[/(?:jdbc|odbc):[^\s]+/g, "[REDACTED:DB_URI]"],
|
|
30
|
+
|
|
31
|
+
// Private keys
|
|
32
|
+
[/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END .*?PRIVATE KEY-----/g, "[REDACTED:PRIVATE_KEY]"],
|
|
33
|
+
|
|
34
|
+
// IP addresses (local only)
|
|
35
|
+
[/192\.168\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
36
|
+
[/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
37
|
+
[/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
38
|
+
|
|
39
|
+
// File paths
|
|
40
|
+
[/(?:\/etc\/(?:passwd|shadow|hosts|sudoers))/g, "[REDACTED:SYSTEM_PATH]"],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/* Email masking (function-based, handled separately) */
|
|
44
|
+
const EMAIL_RE = /([a-zA-Z0-9._%+-]{3,})@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/* ═══════════════════════════════════════
|
|
48
|
+
Filter function
|
|
49
|
+
═══════════════════════════════════════ */
|
|
50
|
+
export interface FilterResult {
|
|
51
|
+
clean: string;
|
|
52
|
+
redacted: boolean;
|
|
53
|
+
count: number;
|
|
54
|
+
details: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function filterOutput(text: string): FilterResult {
|
|
58
|
+
if (!text) return { clean: "", redacted: false, count: 0, details: [] };
|
|
59
|
+
|
|
60
|
+
let clean = text;
|
|
61
|
+
let count = 0;
|
|
62
|
+
const details: string[] = [];
|
|
63
|
+
|
|
64
|
+
// Email masking (function-based replacement)
|
|
65
|
+
let emailCount = 0;
|
|
66
|
+
clean = clean.replace(EMAIL_RE, (full, user, domain) => {
|
|
67
|
+
emailCount++;
|
|
68
|
+
return (user as string).slice(0, 2) + "***@" + (domain as string);
|
|
69
|
+
});
|
|
70
|
+
if (emailCount > 0) {
|
|
71
|
+
count += emailCount;
|
|
72
|
+
details.push(`Masked ${emailCount}x email addresses`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const [pattern, replacement] of SENSITIVE_PATTERNS) {
|
|
76
|
+
const matches = clean.match(pattern);
|
|
77
|
+
if (matches) {
|
|
78
|
+
count += matches.length;
|
|
79
|
+
if (typeof replacement === "string") {
|
|
80
|
+
details.push(`Redacted ${matches.length}x ${pattern.source.slice(0, 30)}`);
|
|
81
|
+
} else {
|
|
82
|
+
details.push(`Masked ${matches.length}x email addresses`);
|
|
83
|
+
}
|
|
84
|
+
clean = clean.replace(pattern, replacement as string);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { clean, redacted: count > 0, count, details };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ═══════════════════════════════════════
|
|
92
|
+
Quick check — is filtering needed?
|
|
93
|
+
═══════════════════════════════════════ */
|
|
94
|
+
export function needsFiltering(text: string): boolean {
|
|
95
|
+
if (!text) return false;
|
|
96
|
+
// Quick scan with the most common patterns
|
|
97
|
+
if (/sk-[a-zA-Z0-9]{32,}/.test(text)) return true;
|
|
98
|
+
if (/api_key.*[:=]/.test(text)) return true;
|
|
99
|
+
if (/password.*[:=]/.test(text)) return true;
|
|
100
|
+
if (/-----BEGIN.*PRIVATE KEY-----/.test(text)) return true;
|
|
101
|
+
if (EMAIL_RE.test(text)) return true;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
package/src/core/graph.ts
CHANGED
|
@@ -1,156 +1,156 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 简易知识图谱 — entity-relation storage in SQLite.
|
|
3
|
-
*
|
|
4
|
-
* Lightweight triple store: (subject, predicate, object) with metadata.
|
|
5
|
-
* Used for: project info, tool preferences, dependency relationships.
|
|
6
|
-
*
|
|
7
|
-
* Schema:
|
|
8
|
-
* CREATE TABLE triples (subj, pred, obj, agent, ts, meta)
|
|
9
|
-
*
|
|
10
|
-
* Queries:
|
|
11
|
-
* - Find all relations for an entity
|
|
12
|
-
* - Find all entities matching a predicate
|
|
13
|
-
* - Transitive closure (2-hop max for performance)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import * as fs from "fs";
|
|
17
|
-
import * as path from "path";
|
|
18
|
-
import { USER_CONFIG_DIR } from "./config";
|
|
19
|
-
import { getLogger } from "./logger";
|
|
20
|
-
|
|
21
|
-
const log = getLogger("graph");
|
|
22
|
-
|
|
23
|
-
/* ═══════════════════════════════════════
|
|
24
|
-
Triple store — in-memory + optional persistence
|
|
25
|
-
═══════════════════════════════════════ */
|
|
26
|
-
interface Triple {
|
|
27
|
-
subj: string;
|
|
28
|
-
pred: string;
|
|
29
|
-
obj: string;
|
|
30
|
-
agent: string;
|
|
31
|
-
ts: string;
|
|
32
|
-
meta?: Record<string, string>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class KnowledgeGraph {
|
|
36
|
-
private triples: Triple[] = [];
|
|
37
|
-
private indexPath: string;
|
|
38
|
-
|
|
39
|
-
constructor(name: string = "default") {
|
|
40
|
-
this.indexPath = path.join(USER_CONFIG_DIR, `kg_${name}.json`);
|
|
41
|
-
this.load();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Add a fact: (subject, predicate, object). */
|
|
45
|
-
add(subj: string, pred: string, obj: string, agent: string = "system", meta?: Record<string, string>): void {
|
|
46
|
-
// Deduplicate
|
|
47
|
-
const exists = this.triples.find(t => t.subj === subj && t.pred === pred && t.obj === obj);
|
|
48
|
-
if (exists) { exists.ts = new Date().toISOString(); if (meta) exists.meta = { ...exists.meta, ...meta }; return; }
|
|
49
|
-
|
|
50
|
-
this.triples.push({ subj, pred, obj, agent, ts: new Date().toISOString(), meta });
|
|
51
|
-
if (this.triples.length > 5000) this.triples.splice(0, this.triples.length - 5000);
|
|
52
|
-
this.save();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Find all facts about an entity. */
|
|
56
|
-
about(entity: string, limit: number = 20): Triple[] {
|
|
57
|
-
return this.triples.filter(t => t.subj === entity || t.obj === entity).slice(-limit);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Find all subjects matching a predicate. */
|
|
61
|
-
byPredicate(pred: string): Triple[] {
|
|
62
|
-
return this.triples.filter(t => t.pred === pred);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Find all objects for a subject-predicate pair. */
|
|
66
|
-
find(subj: string, pred: string): Triple[] {
|
|
67
|
-
return this.triples.filter(t => t.subj === subj && t.pred === pred);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Transitive expansion: 2-hop from a starting entity. */
|
|
71
|
-
expand(entity: string, maxDepth: number = 2): Triple[] {
|
|
72
|
-
const seen = new Set<Triple>();
|
|
73
|
-
const queue = [entity];
|
|
74
|
-
for (let depth = 0; depth < maxDepth && queue.length > 0; depth++) {
|
|
75
|
-
const current = queue.shift()!;
|
|
76
|
-
const facts = this.about(current, 10);
|
|
77
|
-
for (const f of facts) {
|
|
78
|
-
if (seen.has(f)) continue;
|
|
79
|
-
seen.add(f);
|
|
80
|
-
if (f.subj === current && !queue.includes(f.obj)) queue.push(f.obj);
|
|
81
|
-
if (f.obj === current && !queue.includes(f.subj)) queue.push(f.subj);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return Array.from(seen);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Remove a fact. */
|
|
88
|
-
remove(subj: string, pred: string, obj: string): void {
|
|
89
|
-
this.triples = this.triples.filter(t => !(t.subj === subj && t.pred === pred && t.obj === obj));
|
|
90
|
-
this.save();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Search for entities or predicates containing a keyword. */
|
|
94
|
-
search(keyword: string, limit: number = 15): Triple[] {
|
|
95
|
-
const k = keyword.toLowerCase();
|
|
96
|
-
return this.triples.filter(t => t.subj.toLowerCase().includes(k) || t.pred.toLowerCase().includes(k) || t.obj.toLowerCase().includes(k)).slice(-limit);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Format facts as readable text. */
|
|
100
|
-
format(entity?: string): string {
|
|
101
|
-
const facts = entity ? this.about(entity) : this.triples.slice(-30);
|
|
102
|
-
if (facts.length === 0) return "(no facts)";
|
|
103
|
-
const bySubj = new Map<string, string[]>();
|
|
104
|
-
for (const f of facts) {
|
|
105
|
-
if (!bySubj.has(f.subj)) bySubj.set(f.subj, []);
|
|
106
|
-
bySubj.get(f.subj)!.push(`${f.pred} → ${f.obj}`);
|
|
107
|
-
}
|
|
108
|
-
const lines: string[] = [];
|
|
109
|
-
for (const [subj, preds] of bySubj) {
|
|
110
|
-
lines.push(`**${subj}**: ${preds.join(", ")}`);
|
|
111
|
-
}
|
|
112
|
-
return lines.join("\n");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
get size(): number { return this.triples.length; }
|
|
116
|
-
|
|
117
|
-
private save(): void {
|
|
118
|
-
try {
|
|
119
|
-
const dir = path.dirname(this.indexPath);
|
|
120
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
121
|
-
fs.writeFileSync(this.indexPath, JSON.stringify(this.triples.slice(-2000)), "utf-8");
|
|
122
|
-
} catch (e) { log.warn("kg_save_failed", { error: String(e) }); }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private load(): void {
|
|
126
|
-
try {
|
|
127
|
-
if (fs.existsSync(this.indexPath)) {
|
|
128
|
-
this.triples = JSON.parse(fs.readFileSync(this.indexPath, "utf-8"));
|
|
129
|
-
}
|
|
130
|
-
} catch { this.triples = []; }
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/* ── Auto-extract facts from conversation ── */
|
|
135
|
-
const RELATION_PATTERNS: Array<[RegExp, string]> = [
|
|
136
|
-
[/(\w+) (?:是|为|属于|使用|用|用到了|采用) (.+?)(?:[。,,.\n]|$)/g, "is"],
|
|
137
|
-
[/(\w+) (?:版本|version|v) (?:是|为)? ?(\d[\d.]*)/gi, "version"],
|
|
138
|
-
[/(\w+) (?:depends|依赖|需要|requires) (\w+)/gi, "depends_on"],
|
|
139
|
-
[/(\w+) (?:config|配置) (?:为|是)? (.+?)(?:[。,,.\n]|$)/gi, "config"],
|
|
140
|
-
[/(\w+) (?:file|path|文件|路径) (?:在|为|at) (.+?)(?:[。,,.\n]|$)/gi, "located_at"],
|
|
141
|
-
];
|
|
142
|
-
|
|
143
|
-
export function extractFacts(text: string, agent: string): Array<[string, string, string]> {
|
|
144
|
-
const facts: Array<[string, string, string]> = [];
|
|
145
|
-
for (const [pattern, pred] of RELATION_PATTERNS) {
|
|
146
|
-
let match;
|
|
147
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
148
|
-
const subj = match[1].trim().toLowerCase();
|
|
149
|
-
const obj = match[2].trim();
|
|
150
|
-
if (subj.length >= 2 && obj.length >= 2 && subj !== obj) {
|
|
151
|
-
facts.push([subj, pred, obj]);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return facts;
|
|
156
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 简易知识图谱 — entity-relation storage in SQLite.
|
|
3
|
+
*
|
|
4
|
+
* Lightweight triple store: (subject, predicate, object) with metadata.
|
|
5
|
+
* Used for: project info, tool preferences, dependency relationships.
|
|
6
|
+
*
|
|
7
|
+
* Schema:
|
|
8
|
+
* CREATE TABLE triples (subj, pred, obj, agent, ts, meta)
|
|
9
|
+
*
|
|
10
|
+
* Queries:
|
|
11
|
+
* - Find all relations for an entity
|
|
12
|
+
* - Find all entities matching a predicate
|
|
13
|
+
* - Transitive closure (2-hop max for performance)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from "fs";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
import { USER_CONFIG_DIR } from "./config";
|
|
19
|
+
import { getLogger } from "./logger";
|
|
20
|
+
|
|
21
|
+
const log = getLogger("graph");
|
|
22
|
+
|
|
23
|
+
/* ═══════════════════════════════════════
|
|
24
|
+
Triple store — in-memory + optional persistence
|
|
25
|
+
═══════════════════════════════════════ */
|
|
26
|
+
interface Triple {
|
|
27
|
+
subj: string;
|
|
28
|
+
pred: string;
|
|
29
|
+
obj: string;
|
|
30
|
+
agent: string;
|
|
31
|
+
ts: string;
|
|
32
|
+
meta?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class KnowledgeGraph {
|
|
36
|
+
private triples: Triple[] = [];
|
|
37
|
+
private indexPath: string;
|
|
38
|
+
|
|
39
|
+
constructor(name: string = "default") {
|
|
40
|
+
this.indexPath = path.join(USER_CONFIG_DIR, `kg_${name}.json`);
|
|
41
|
+
this.load();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Add a fact: (subject, predicate, object). */
|
|
45
|
+
add(subj: string, pred: string, obj: string, agent: string = "system", meta?: Record<string, string>): void {
|
|
46
|
+
// Deduplicate
|
|
47
|
+
const exists = this.triples.find(t => t.subj === subj && t.pred === pred && t.obj === obj);
|
|
48
|
+
if (exists) { exists.ts = new Date().toISOString(); if (meta) exists.meta = { ...exists.meta, ...meta }; return; }
|
|
49
|
+
|
|
50
|
+
this.triples.push({ subj, pred, obj, agent, ts: new Date().toISOString(), meta });
|
|
51
|
+
if (this.triples.length > 5000) this.triples.splice(0, this.triples.length - 5000);
|
|
52
|
+
this.save();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Find all facts about an entity. */
|
|
56
|
+
about(entity: string, limit: number = 20): Triple[] {
|
|
57
|
+
return this.triples.filter(t => t.subj === entity || t.obj === entity).slice(-limit);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Find all subjects matching a predicate. */
|
|
61
|
+
byPredicate(pred: string): Triple[] {
|
|
62
|
+
return this.triples.filter(t => t.pred === pred);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Find all objects for a subject-predicate pair. */
|
|
66
|
+
find(subj: string, pred: string): Triple[] {
|
|
67
|
+
return this.triples.filter(t => t.subj === subj && t.pred === pred);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Transitive expansion: 2-hop from a starting entity. */
|
|
71
|
+
expand(entity: string, maxDepth: number = 2): Triple[] {
|
|
72
|
+
const seen = new Set<Triple>();
|
|
73
|
+
const queue = [entity];
|
|
74
|
+
for (let depth = 0; depth < maxDepth && queue.length > 0; depth++) {
|
|
75
|
+
const current = queue.shift()!;
|
|
76
|
+
const facts = this.about(current, 10);
|
|
77
|
+
for (const f of facts) {
|
|
78
|
+
if (seen.has(f)) continue;
|
|
79
|
+
seen.add(f);
|
|
80
|
+
if (f.subj === current && !queue.includes(f.obj)) queue.push(f.obj);
|
|
81
|
+
if (f.obj === current && !queue.includes(f.subj)) queue.push(f.subj);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return Array.from(seen);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Remove a fact. */
|
|
88
|
+
remove(subj: string, pred: string, obj: string): void {
|
|
89
|
+
this.triples = this.triples.filter(t => !(t.subj === subj && t.pred === pred && t.obj === obj));
|
|
90
|
+
this.save();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Search for entities or predicates containing a keyword. */
|
|
94
|
+
search(keyword: string, limit: number = 15): Triple[] {
|
|
95
|
+
const k = keyword.toLowerCase();
|
|
96
|
+
return this.triples.filter(t => t.subj.toLowerCase().includes(k) || t.pred.toLowerCase().includes(k) || t.obj.toLowerCase().includes(k)).slice(-limit);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Format facts as readable text. */
|
|
100
|
+
format(entity?: string): string {
|
|
101
|
+
const facts = entity ? this.about(entity) : this.triples.slice(-30);
|
|
102
|
+
if (facts.length === 0) return "(no facts)";
|
|
103
|
+
const bySubj = new Map<string, string[]>();
|
|
104
|
+
for (const f of facts) {
|
|
105
|
+
if (!bySubj.has(f.subj)) bySubj.set(f.subj, []);
|
|
106
|
+
bySubj.get(f.subj)!.push(`${f.pred} → ${f.obj}`);
|
|
107
|
+
}
|
|
108
|
+
const lines: string[] = [];
|
|
109
|
+
for (const [subj, preds] of bySubj) {
|
|
110
|
+
lines.push(`**${subj}**: ${preds.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get size(): number { return this.triples.length; }
|
|
116
|
+
|
|
117
|
+
private save(): void {
|
|
118
|
+
try {
|
|
119
|
+
const dir = path.dirname(this.indexPath);
|
|
120
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
121
|
+
fs.writeFileSync(this.indexPath, JSON.stringify(this.triples.slice(-2000)), "utf-8");
|
|
122
|
+
} catch (e) { log.warn("kg_save_failed", { error: String(e) }); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private load(): void {
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(this.indexPath)) {
|
|
128
|
+
this.triples = JSON.parse(fs.readFileSync(this.indexPath, "utf-8"));
|
|
129
|
+
}
|
|
130
|
+
} catch { this.triples = []; }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ── Auto-extract facts from conversation ── */
|
|
135
|
+
const RELATION_PATTERNS: Array<[RegExp, string]> = [
|
|
136
|
+
[/(\w+) (?:是|为|属于|使用|用|用到了|采用) (.+?)(?:[。,,.\n]|$)/g, "is"],
|
|
137
|
+
[/(\w+) (?:版本|version|v) (?:是|为)? ?(\d[\d.]*)/gi, "version"],
|
|
138
|
+
[/(\w+) (?:depends|依赖|需要|requires) (\w+)/gi, "depends_on"],
|
|
139
|
+
[/(\w+) (?:config|配置) (?:为|是)? (.+?)(?:[。,,.\n]|$)/gi, "config"],
|
|
140
|
+
[/(\w+) (?:file|path|文件|路径) (?:在|为|at) (.+?)(?:[。,,.\n]|$)/gi, "located_at"],
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
export function extractFacts(text: string, agent: string): Array<[string, string, string]> {
|
|
144
|
+
const facts: Array<[string, string, string]> = [];
|
|
145
|
+
for (const [pattern, pred] of RELATION_PATTERNS) {
|
|
146
|
+
let match;
|
|
147
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
148
|
+
const subj = match[1].trim().toLowerCase();
|
|
149
|
+
const obj = match[2].trim();
|
|
150
|
+
if (subj.length >= 2 && obj.length >= 2 && subj !== obj) {
|
|
151
|
+
facts.push([subj, pred, obj]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return facts;
|
|
156
|
+
}
|