fuli-context 0.4.0 → 0.7.2
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/README.en.md +5 -0
- package/README.md +411 -173
- package/README.zh-CN.md +448 -0
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
- package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
- package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
- package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
- package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
- package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
- package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
- package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
- package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
- package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
- package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
- package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
- package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
- package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
- package/dist/web/assets/console-CbDsKrsO.js +1 -0
- package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
- package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
- package/dist/web/assets/i18n-BQcns1PH.js +5 -0
- package/dist/web/assets/identity-B8K4VYg6.js +1 -0
- package/dist/web/assets/index-CB8Xmsc2.js +2 -0
- package/dist/web/assets/index-CZeuPh-U.css +1 -0
- package/dist/web/assets/model-eF52GgPF.js +1 -0
- package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
- package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
- package/dist/web/index.html +9 -6
- package/docs/external-knowledge-architecture.md +279 -0
- package/examples/external-knowledge/markdown-folder.mjs +235 -0
- package/graph-provider/fuli_graph/app.py +57 -1
- package/graph-provider/fuli_graph/graph_models.py +3 -0
- package/graph-provider/fuli_graph/graph_query.py +62 -16
- package/graph-provider/fuli_graph/knowledge_review.py +612 -0
- package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
- package/graph-provider/fuli_graph/knowledge_search.py +51 -4
- package/graph-provider/fuli_graph/models.py +19 -0
- package/graph-provider/fuli_graph/runtime.py +15 -0
- package/graph-provider/fuli_graph/store_knowledge.py +2 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +5 -407
- package/package.json +7 -4
- package/skills/capturing-session-knowledge/SKILL.md +24 -4
- package/skills/flreview/SKILL.md +73 -0
- package/skills/flreview/agents/openai.yaml +6 -0
- package/src/agent-tools/graph-definitions.js +79 -4
- package/src/agent-tools/graph-handlers.js +12 -0
- package/src/cli/command-registry.js +3 -3
- package/src/cli/local-runtime-command.js +60 -23
- package/src/cli/local-runtime-options.js +16 -6
- package/src/cli/setup-agent-selection.js +12 -12
- package/src/cli/setup-command.js +41 -18
- package/src/cli/shell-argument.js +10 -0
- package/src/cli/uninstall-command.js +17 -15
- package/src/cli/update-command.js +38 -30
- package/src/external-knowledge/catalog.js +61 -0
- package/src/external-knowledge/conflict-policy.js +58 -0
- package/src/external-knowledge/connected-search.js +166 -0
- package/src/external-knowledge/connectors/custom.js +133 -0
- package/src/external-knowledge/connectors/feishu.js +461 -0
- package/src/external-knowledge/connectors/mcp.js +438 -0
- package/src/external-knowledge/connectors/notion.js +307 -0
- package/src/external-knowledge/connectors/retrieval-api.js +222 -0
- package/src/external-knowledge/document-mapping.js +151 -0
- package/src/external-knowledge/graph-projection.js +17 -0
- package/src/external-knowledge/index.js +8 -0
- package/src/external-knowledge/registry.js +91 -0
- package/src/external-knowledge/runtime.js +68 -0
- package/src/external-knowledge/safe-diagnostic.js +14 -0
- package/src/external-knowledge/service.js +811 -0
- package/src/graphiti/agent-knowledge-workflows.js +7 -2
- package/src/graphiti/federated-application.js +66 -41
- package/src/graphiti/knowledge-item-role.js +21 -0
- package/src/graphiti/knowledge-review.js +43 -0
- package/src/graphiti/project-profile-mapping.js +32 -0
- package/src/graphiti/provider-client.js +22 -1
- package/src/graphiti/source-marker.js +54 -3
- package/src/graphiti/task-knowledge-recall.js +250 -0
- package/src/http/api-router.js +23 -108
- package/src/http/external-knowledge-api-router.js +104 -0
- package/src/http/graph-api-router.js +12 -1
- package/src/http/request-policy.js +38 -3
- package/src/local-runtime/lifecycle.js +37 -16
- package/src/mcp/create-mcp-server.js +1 -0
- package/src/mcp/instructions.js +7 -7
- package/src/mcp/tool-annotations.js +6 -0
- package/src/mcp/tool-result.js +1 -0
- package/src/server/application-lifecycle.js +2 -11
- package/src/server/blocked-ports.js +11 -0
- package/src/server/lan-access.js +52 -0
- package/src/server.js +103 -25
- package/src/setup/agent-installation-status.js +2 -1
- package/src/setup/agents.js +2 -1
- package/src/setup/claude-code-config.js +2 -1
- package/src/setup/codex-bootstrap.js +12 -7
- package/src/setup/container-runtime.js +25 -21
- package/src/setup/graph-runtime.js +219 -58
- package/src/setup/node-runtime.js +3 -3
- package/src/setup/options.js +2 -4
- package/src/setup/paths.js +18 -3
- package/src/setup/review-skill-adapters.js +49 -0
- package/src/setup/setup.js +26 -2
- package/src/setup/uninstall.js +4 -2
- package/src/system/resource-monitor.js +317 -0
- package/src/system/runtime-settings.js +131 -0
- package/src/system/system-service.js +119 -0
- package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
- package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-CJCNI2Wd.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.js +0 -1
- package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
- package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
- package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
- package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
- package/dist/web/assets/console-B_Ena2DS.js +0 -5
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-CjBwYSMX.js +0 -1
- package/dist/web/assets/index-BQRkOwPx.js +0 -2
- package/dist/web/assets/index-CpLDx3of.css +0 -1
- package/dist/web/assets/model-C-5RVIJ2.js +0 -1
- package/dist/web/assets/paths-BkULqmGI.js +0 -1
- package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
- package/src/agent/candidate-read-service.js +0 -53
- package/src/agent/context-pack-service.js +0 -83
- package/src/agent/public-read-policy.js +0 -39
- package/src/agent/public-read-service.js +0 -69
- package/src/agent/read-projection.js +0 -107
- package/src/agent/search-service.js +0 -61
- package/src/agent/space-boundary.js +0 -33
- package/src/app/bootstrap.js +0 -8
- package/src/app/create-application.js +0 -143
- package/src/app/state-service.js +0 -38
- package/src/candidates.js +0 -78
- package/src/classifier.js +0 -63
- package/src/cli/command-arguments.js +0 -14
- package/src/context-pack.js +0 -206
- package/src/extractor.js +0 -77
- package/src/fact-history.js +0 -39
- package/src/facts/fact-service.js +0 -89
- package/src/http/bounded-json-array.js +0 -19
- package/src/http/lens-route.js +0 -50
- package/src/ingestion.js +0 -161
- package/src/lens/lens-correction-service.js +0 -81
- package/src/lens/lens-fact-writer.js +0 -85
- package/src/lens/lens-input.js +0 -62
- package/src/lens/lens-query-projection.js +0 -87
- package/src/lens/lens-query-ranking.js +0 -62
- package/src/lens/lens-query.js +0 -143
- package/src/lens/lens-resource-projection.js +0 -93
- package/src/lens/lens-resource-selection.js +0 -45
- package/src/lens/lens-resource-service.js +0 -103
- package/src/lens/lens-search-query.js +0 -122
- package/src/lens/lens-service.js +0 -183
- package/src/models.js +0 -44
- package/src/observer.js +0 -83
- package/src/project-rules.js +0 -33
- package/src/publication/outbox-service.js +0 -46
- package/src/publication/publication-envelope-schema.js +0 -75
- package/src/publication/publication-envelope.js +0 -37
- package/src/publication/publication-policy.js +0 -82
- package/src/publication/publication-service.js +0 -48
- package/src/router.js +0 -58
- package/src/runtime-options.js +0 -138
- package/src/security/episode-visibility.js +0 -14
- package/src/security/fact-visibility.js +0 -15
- package/src/security/source-metadata-policy.js +0 -8
- package/src/storage/episode-evidence-preview.js +0 -100
- package/src/storage/fact-search-order.js +0 -18
- package/src/storage/file/episode-evidence.js +0 -37
- package/src/storage/file/episode-index.js +0 -59
- package/src/storage/file/fact-search.js +0 -60
- package/src/storage/file-store.js +0 -437
- package/src/storage/migrate.js +0 -128
- package/src/storage/migrations/001-initial.sql +0 -83
- package/src/storage/migrations/002-storage-integrity.sql +0 -113
- package/src/storage/record-validation.js +0 -46
- package/src/storage/snapshot-defaults.js +0 -54
- package/src/storage/sqlite/candidates-repository.js +0 -84
- package/src/storage/sqlite/episode-evidence-repository.js +0 -154
- package/src/storage/sqlite/episode-privacy-sql.js +0 -33
- package/src/storage/sqlite/episodes-repository.js +0 -52
- package/src/storage/sqlite/fact-search-repository.js +0 -162
- package/src/storage/sqlite/facts-repository.js +0 -146
- package/src/storage/sqlite/imports-repository.js +0 -39
- package/src/storage/sqlite/mapper.js +0 -84
- package/src/storage/sqlite/open-database.js +0 -15
- package/src/storage/sqlite/outbox-repository.js +0 -103
- package/src/storage/sqlite/snapshot-repository.js +0 -69
- package/src/storage/sqlite/spaces-repository.js +0 -52
- package/src/storage/sqlite/subscriptions-repository.js +0 -54
- package/src/storage/sqlite-store.js +0 -159
- package/src/storage/stable-json.js +0 -24
- package/src/storage/store-port.js +0 -20
- package/src/store.js +0 -2
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createFuliSourceMarker } from './source-marker.js';
|
|
2
|
+
import { knowledgeItemRole } from './knowledge-item-role.js';
|
|
3
|
+
import { detectSensitiveContent } from '../security/sensitive-content.js';
|
|
4
|
+
|
|
5
|
+
const MAX_QUERIES = 4;
|
|
6
|
+
const MAX_ITEMS_PER_KIND = 3;
|
|
7
|
+
const MAX_ITEM_TEXT = 1000;
|
|
8
|
+
|
|
9
|
+
const RECALL_INTENTS = Object.freeze([
|
|
10
|
+
{
|
|
11
|
+
category: 'release_delivery',
|
|
12
|
+
pattern: /(?:发布|发版|上线|部署|推送|再推|release|publish|push|deploy|ship|rollout|commit|github|\bgit\b)/iu,
|
|
13
|
+
queries: [
|
|
14
|
+
'发布 发版 版本 release publish runbook',
|
|
15
|
+
'推送 提交 push commit GitHub Connector runbook'
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
category: 'access_authentication',
|
|
20
|
+
pattern: /(?:认证|登录|凭据|身份|令牌|密钥|credential|authentication|authorization|login|token|ssh)/iu,
|
|
21
|
+
queries: ['认证 登录 凭据 身份 credential authentication SSH token runbook']
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
category: 'artifact_location',
|
|
25
|
+
pattern: /(?:网址|地址|路由|端点|在哪里|在哪儿|哪里|(?:文档|文件).{0,12}(?:位置|路径|在哪)|(?:找|查).{0,12}(?:文档|文件)|\burl\b|route|endpoint|document location|file path|where)/iu,
|
|
26
|
+
queries: ['地址 URL 路由 route endpoint 部署 deployment 文档 document']
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
category: 'requirements_architecture_decision',
|
|
30
|
+
pattern: /(?:需求|约束|架构|设计决定|决策|原因|取舍|requirement|constraint|architecture|decision|rationale|tradeoff)/iu,
|
|
31
|
+
queries: ['需求 约束 架构 决策 原因 requirement constraint architecture decision rationale']
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
category: 'runbook_method',
|
|
35
|
+
pattern: /(?:方法|方式|流程|步骤|怎么做|如何做|runbook|workflow|procedure|how to)/iu,
|
|
36
|
+
queries: ['方法 方式 流程 步骤 runbook workflow procedure']
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
category: 'prior_context',
|
|
40
|
+
pattern: /(?:之前|以前|上次|已经存|保存过|记得|照旧|同样|还是用|不用再|先前|previous|earlier|remember|same as|again)/iu,
|
|
41
|
+
queries: ['先前 方法 规则 决策 runbook previous method decision']
|
|
42
|
+
}
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export const TASK_KNOWLEDGE_RETRIEVAL_GUIDANCE = Object.freeze({
|
|
46
|
+
before_reasking:
|
|
47
|
+
'Before asking the user to repeat a stable project fact or method, inspect task_knowledge_recall and search Fuli when needed.',
|
|
48
|
+
focused_queries:
|
|
49
|
+
'Use one to four focused action, artifact, target-system, or identifier queries; never use the full conversational request as the only query.',
|
|
50
|
+
scope:
|
|
51
|
+
'Search only the exact active personal project, its explicitly inheritable knowledge, and the bounded personal-global profile.'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export function planTaskKnowledgeRecall(taskPrompt) {
|
|
55
|
+
if (typeof taskPrompt !== 'string' || !taskPrompt.trim()) {
|
|
56
|
+
return { status: 'not_requested', trigger_categories: [], queries: [] };
|
|
57
|
+
}
|
|
58
|
+
const prompt = singleLine(taskPrompt).slice(0, 8192);
|
|
59
|
+
if (detectSensitiveContent(prompt).restricted) {
|
|
60
|
+
return {
|
|
61
|
+
status: 'suppressed_sensitive',
|
|
62
|
+
trigger_categories: [],
|
|
63
|
+
queries: []
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const matched = RECALL_INTENTS.filter(({ pattern }) => pattern.test(prompt));
|
|
67
|
+
if (!matched.length) {
|
|
68
|
+
return { status: 'not_needed', trigger_categories: [], queries: [] };
|
|
69
|
+
}
|
|
70
|
+
const queries = unique(matched.flatMap(({ queries: values }) => values));
|
|
71
|
+
const distinctive = distinctivePromptQuery(prompt);
|
|
72
|
+
if (distinctive) queries.push(distinctive);
|
|
73
|
+
return {
|
|
74
|
+
status: 'planned',
|
|
75
|
+
trigger_categories: matched.map(({ category }) => category),
|
|
76
|
+
queries: unique(queries).slice(0, MAX_QUERIES)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function recallTaskKnowledge(application, resolution, taskPrompt) {
|
|
81
|
+
const plan = planTaskKnowledgeRecall(taskPrompt);
|
|
82
|
+
if (plan.status !== 'planned') return recallWithoutSearch(plan.status, plan);
|
|
83
|
+
if (!resolution.personalProjectId) {
|
|
84
|
+
return {
|
|
85
|
+
status: 'project_unresolved',
|
|
86
|
+
trigger_categories: plan.trigger_categories,
|
|
87
|
+
query_count: plan.queries.length,
|
|
88
|
+
guidance: TASK_KNOWLEDGE_RETRIEVAL_GUIDANCE,
|
|
89
|
+
facts: [],
|
|
90
|
+
entities: []
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const settlements = await Promise.allSettled(plan.queries.map((query) =>
|
|
95
|
+
application.searchKnowledge({
|
|
96
|
+
personalSpaceId: application.config.personal.spaceId,
|
|
97
|
+
personalProjectId: resolution.personalProjectId,
|
|
98
|
+
query,
|
|
99
|
+
limit: 6,
|
|
100
|
+
includeHistorical: false,
|
|
101
|
+
includePending: false,
|
|
102
|
+
agentInvocation: true,
|
|
103
|
+
agentToolName: 'automatic_task_knowledge_recall'
|
|
104
|
+
})
|
|
105
|
+
));
|
|
106
|
+
const results = settlements
|
|
107
|
+
.filter(({ status }) => status === 'fulfilled')
|
|
108
|
+
.map(({ value }) => value);
|
|
109
|
+
const failedQueryCount = settlements.length - results.length;
|
|
110
|
+
const facts = rankedUnique(
|
|
111
|
+
results.flatMap(({ facts = [] }) => facts),
|
|
112
|
+
'relationship'
|
|
113
|
+
).slice(0, MAX_ITEMS_PER_KIND).map(compactFact);
|
|
114
|
+
const entities = rankedUnique(
|
|
115
|
+
results.flatMap(({ entities = [] }) => entities),
|
|
116
|
+
'entity'
|
|
117
|
+
).slice(0, MAX_ITEMS_PER_KIND).map(compactEntity);
|
|
118
|
+
const sourceMarker = createFuliSourceMarker({
|
|
119
|
+
consoleUrl: application.consoleUrl,
|
|
120
|
+
facts,
|
|
121
|
+
entities,
|
|
122
|
+
projectScopePriority: 'strict'
|
|
123
|
+
});
|
|
124
|
+
const searched = results.length > 0;
|
|
125
|
+
return {
|
|
126
|
+
status: facts.length || entities.length
|
|
127
|
+
? 'matched'
|
|
128
|
+
: failedQueryCount === settlements.length ? 'unavailable' : 'no_match',
|
|
129
|
+
partial: failedQueryCount > 0,
|
|
130
|
+
failed_query_count: failedQueryCount,
|
|
131
|
+
trigger_categories: plan.trigger_categories,
|
|
132
|
+
query_count: plan.queries.length,
|
|
133
|
+
guidance: TASK_KNOWLEDGE_RETRIEVAL_GUIDANCE,
|
|
134
|
+
...(searched ? {
|
|
135
|
+
sourceMarker,
|
|
136
|
+
noMatchSourceMarker: createFuliSourceMarker({
|
|
137
|
+
consoleUrl: application.consoleUrl
|
|
138
|
+
})
|
|
139
|
+
} : {}),
|
|
140
|
+
facts,
|
|
141
|
+
entities
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function recallWithoutSearch(status, plan) {
|
|
146
|
+
return {
|
|
147
|
+
status,
|
|
148
|
+
trigger_categories: plan.trigger_categories,
|
|
149
|
+
query_count: plan.queries.length,
|
|
150
|
+
guidance: TASK_KNOWLEDGE_RETRIEVAL_GUIDANCE,
|
|
151
|
+
facts: [],
|
|
152
|
+
entities: []
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function rankedUnique(items, itemKind) {
|
|
157
|
+
const selected = new Map();
|
|
158
|
+
for (const item of items) {
|
|
159
|
+
const key = `${itemKind}:${item.id}`;
|
|
160
|
+
const current = selected.get(key);
|
|
161
|
+
if (!current || compareRecallItems(item, current) < 0) selected.set(key, item);
|
|
162
|
+
}
|
|
163
|
+
return [...selected.values()].sort(compareRecallItems);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function compareRecallItems(left, right) {
|
|
167
|
+
const leftLocal = left.defined_project_id ? 1 : 0;
|
|
168
|
+
const rightLocal = right.defined_project_id ? 1 : 0;
|
|
169
|
+
if (leftLocal !== rightLocal) return rightLocal - leftLocal;
|
|
170
|
+
const leftDistance = Number(left.scope_distance ?? 0);
|
|
171
|
+
const rightDistance = Number(right.scope_distance ?? 0);
|
|
172
|
+
if (leftDistance !== rightDistance) return leftDistance - rightDistance;
|
|
173
|
+
const leftSupporting = knowledgeItemRole(left) === 'supporting' ? 1 : 0;
|
|
174
|
+
const rightSupporting = knowledgeItemRole(right) === 'supporting' ? 1 : 0;
|
|
175
|
+
if (leftSupporting !== rightSupporting) return leftSupporting - rightSupporting;
|
|
176
|
+
return Number(right.score ?? 0) - Number(left.score ?? 0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function compactFact(fact) {
|
|
180
|
+
return compactItem(fact, {
|
|
181
|
+
source_entity: fact.source_entity,
|
|
182
|
+
target_entity: fact.target_entity,
|
|
183
|
+
relationship: fact.relationship,
|
|
184
|
+
fact: boundedText(fact.fact)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function compactEntity(entity) {
|
|
189
|
+
return compactItem(entity, {
|
|
190
|
+
name: entity.name,
|
|
191
|
+
type: entity.type,
|
|
192
|
+
summary: boundedText(entity.summary)
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function compactItem(item, content) {
|
|
197
|
+
return {
|
|
198
|
+
id: item.id,
|
|
199
|
+
space_id: item.space_id ?? item.spaceId,
|
|
200
|
+
scope: item.scope,
|
|
201
|
+
key: item.key ?? null,
|
|
202
|
+
defined_project_id: item.defined_project_id ?? null,
|
|
203
|
+
inherited_from_project_id: item.inherited_from_project_id ?? null,
|
|
204
|
+
scope_distance: item.scope_distance ?? 0,
|
|
205
|
+
confirmation_status: item.confirmation_status ?? null,
|
|
206
|
+
source_uris: item.source_uris ?? [],
|
|
207
|
+
score: item.score ?? null,
|
|
208
|
+
...content
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function distinctivePromptQuery(prompt) {
|
|
213
|
+
const quoted = [...prompt.matchAll(/[“”"']([^“”"']{2,80})[“”"']/gu)]
|
|
214
|
+
.map((match) => match[1]);
|
|
215
|
+
const versions = prompt.match(
|
|
216
|
+
/\bv?\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?\b/gu
|
|
217
|
+
) ?? [];
|
|
218
|
+
const identifiers = prompt.match(
|
|
219
|
+
/\b[A-Za-z][A-Za-z0-9]{0,63}[_./-][A-Za-z0-9_.\/-]{1,127}\b/gu
|
|
220
|
+
) ?? [];
|
|
221
|
+
const urls = [...prompt.matchAll(/https?:\/\/[^\s<>()]+/gu)]
|
|
222
|
+
.map((match) => safeUrlSearchTerm(match[0]))
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
return unique([...quoted, ...versions, ...identifiers, ...urls])
|
|
225
|
+
.slice(0, 4)
|
|
226
|
+
.join(' ')
|
|
227
|
+
.slice(0, 180);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function safeUrlSearchTerm(value) {
|
|
231
|
+
try {
|
|
232
|
+
const url = new URL(value);
|
|
233
|
+
if (url.username || url.password) return null;
|
|
234
|
+
return `${url.hostname}${url.pathname}`.slice(0, 120);
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function unique(values) {
|
|
241
|
+
return [...new Set(values.filter(Boolean))];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function singleLine(value) {
|
|
245
|
+
return String(value).replace(/[\r\n\t]+/gu, ' ').replace(/\s+/gu, ' ').trim();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function boundedText(value) {
|
|
249
|
+
return singleLine(value ?? '').slice(0, MAX_ITEM_TEXT);
|
|
250
|
+
}
|
package/src/http/api-router.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { readJson, sendJson } from './response.js';
|
|
2
|
-
import {
|
|
2
|
+
import { handleExternalKnowledgeApiRequest } from './external-knowledge-api-router.js';
|
|
3
3
|
import { handleGraphApiRequest } from './graph-api-router.js';
|
|
4
4
|
|
|
5
|
-
export async function handleApiRequest({
|
|
5
|
+
export async function handleApiRequest({
|
|
6
|
+
request,
|
|
7
|
+
response,
|
|
8
|
+
app,
|
|
9
|
+
system = null,
|
|
10
|
+
externalKnowledge = null,
|
|
11
|
+
connectedKnowledge = null
|
|
12
|
+
}) {
|
|
6
13
|
const url = new URL(request.url, 'http://127.0.0.1');
|
|
7
14
|
|
|
8
15
|
if (url.pathname === '/api/health' && request.method === 'GET') {
|
|
@@ -14,120 +21,28 @@ export async function handleApiRequest({ request, response, app }) {
|
|
|
14
21
|
return true;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (handleLensRequest({ request, response, url, app })) return true;
|
|
22
|
-
|
|
23
|
-
if (url.pathname === '/api/state' && request.method === 'GET') {
|
|
24
|
-
sendJson(response, 200, app.state());
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (url.pathname === '/api/spaces' && request.method === 'POST') {
|
|
29
|
-
const body = await readJson(request);
|
|
30
|
-
const space = app.createSpace(body.name, body.kind, body.description ?? null);
|
|
31
|
-
sendJson(response, 200, { space });
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (url.pathname === '/api/bootstrap' && request.method === 'POST') {
|
|
36
|
-
sendJson(response, 200, app.bootstrap());
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (url.pathname === '/api/subscriptions' && request.method === 'POST') {
|
|
41
|
-
const body = await readJson(request);
|
|
42
|
-
const subscription = app.subscribe(
|
|
43
|
-
body.personalSpaceId,
|
|
44
|
-
body.spaceId,
|
|
45
|
-
body.mode ?? 'latest'
|
|
46
|
-
);
|
|
47
|
-
sendJson(response, 200, { subscription });
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (url.pathname === '/api/remember' && request.method === 'POST') {
|
|
52
|
-
const body = await readJson(request);
|
|
53
|
-
const result = app.remember({
|
|
54
|
-
personalSpaceId: app.requireSpaceId({
|
|
55
|
-
id: body.personalSpaceId,
|
|
56
|
-
name: body.personalSpaceName,
|
|
57
|
-
label: 'Personal space'
|
|
58
|
-
}),
|
|
59
|
-
targetSpaceId: optionalSpaceId(app, {
|
|
60
|
-
id: body.targetSpaceId,
|
|
61
|
-
name: body.targetSpaceName,
|
|
62
|
-
label: 'Target space'
|
|
63
|
-
}),
|
|
64
|
-
sourceKind: body.sourceKind ?? 'agent',
|
|
65
|
-
body: body.body,
|
|
66
|
-
sourceUri: body.sourceUri ?? null
|
|
67
|
-
});
|
|
68
|
-
sendJson(response, 200, result);
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (url.pathname === '/api/observe/git-diff' && request.method === 'POST') {
|
|
73
|
-
const body = await readJson(request);
|
|
74
|
-
const result = app.observe({
|
|
75
|
-
personalSpaceId: body.personalSpaceId,
|
|
76
|
-
targetSpaceId: body.targetSpaceId || null,
|
|
77
|
-
cwd: body.cwd ?? process.cwd()
|
|
78
|
-
});
|
|
79
|
-
sendJson(response, 200, result);
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const candidateDecisionMatch = url.pathname.match(/^\/api\/candidates\/([^/]+)\/decision$/);
|
|
84
|
-
if (candidateDecisionMatch && request.method === 'POST') {
|
|
85
|
-
const body = await readJson(request);
|
|
86
|
-
const candidate = app.decideCandidate(candidateDecisionMatch[1], body.decision);
|
|
87
|
-
sendJson(response, 200, { candidate });
|
|
24
|
+
if (system && url.pathname === '/api/system/settings' && request.method === 'GET') {
|
|
25
|
+
sendJson(response, 200, system.getSettings());
|
|
88
26
|
return true;
|
|
89
27
|
}
|
|
90
28
|
|
|
91
|
-
if (url.pathname === '/api/
|
|
92
|
-
|
|
93
|
-
personalSpaceId: requiredSpaceId(app, {
|
|
94
|
-
id: url.searchParams.get('personalSpaceId'),
|
|
95
|
-
name: url.searchParams.get('personalSpaceName'),
|
|
96
|
-
label: 'Personal space'
|
|
97
|
-
}),
|
|
98
|
-
query: url.searchParams.get('q') ?? '',
|
|
99
|
-
includeHistorical: url.searchParams.get('historical') === 'true'
|
|
100
|
-
});
|
|
101
|
-
sendJson(response, 200, result);
|
|
29
|
+
if (system && url.pathname === '/api/system/settings' && request.method === 'PUT') {
|
|
30
|
+
sendJson(response, 200, system.updateSettings(await readJson(request)));
|
|
102
31
|
return true;
|
|
103
32
|
}
|
|
104
33
|
|
|
105
|
-
if (url.pathname === '/api/
|
|
106
|
-
|
|
107
|
-
personalSpaceId: requiredSpaceId(app, {
|
|
108
|
-
id: url.searchParams.get('personalSpaceId'),
|
|
109
|
-
name: url.searchParams.get('personalSpaceName'),
|
|
110
|
-
label: 'Personal space'
|
|
111
|
-
}),
|
|
112
|
-
spaceId: requiredSpaceId(app, {
|
|
113
|
-
id: url.searchParams.get('spaceId'),
|
|
114
|
-
name: url.searchParams.get('spaceName'),
|
|
115
|
-
label: 'Space'
|
|
116
|
-
}),
|
|
117
|
-
query: url.searchParams.get('q') ?? ''
|
|
118
|
-
});
|
|
119
|
-
sendJson(response, 200, result);
|
|
34
|
+
if (system && url.pathname === '/api/system/resources' && request.method === 'GET') {
|
|
35
|
+
sendJson(response, 200, await system.resources());
|
|
120
36
|
return true;
|
|
121
37
|
}
|
|
122
38
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
39
|
+
if (await handleExternalKnowledgeApiRequest({
|
|
40
|
+
request,
|
|
41
|
+
response,
|
|
42
|
+
url,
|
|
43
|
+
externalKnowledge,
|
|
44
|
+
connectedKnowledge
|
|
45
|
+
})) return true;
|
|
129
46
|
|
|
130
|
-
|
|
131
|
-
if (!input.id && !input.name) return null;
|
|
132
|
-
return requiredSpaceId(app, input);
|
|
47
|
+
return handleGraphApiRequest({ request, response, url, app });
|
|
133
48
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readJson, sendJson } from './response.js';
|
|
2
|
+
|
|
3
|
+
export async function handleExternalKnowledgeApiRequest({
|
|
4
|
+
request,
|
|
5
|
+
response,
|
|
6
|
+
url,
|
|
7
|
+
externalKnowledge,
|
|
8
|
+
connectedKnowledge
|
|
9
|
+
}) {
|
|
10
|
+
if (!externalKnowledge && !connectedKnowledge) return false;
|
|
11
|
+
|
|
12
|
+
if (externalKnowledge &&
|
|
13
|
+
url.pathname === '/api/external-knowledge/connectors' &&
|
|
14
|
+
request.method === 'GET') {
|
|
15
|
+
sendJson(response, 200, externalKnowledge.listConnectorTypes());
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (externalKnowledge &&
|
|
19
|
+
url.pathname === '/api/external-knowledge/discover' &&
|
|
20
|
+
request.method === 'POST') {
|
|
21
|
+
sendJson(response, 200, await externalKnowledge.discover(await readJson(request)));
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (externalKnowledge &&
|
|
25
|
+
url.pathname === '/api/external-knowledge/bindings' &&
|
|
26
|
+
request.method === 'GET') {
|
|
27
|
+
sendJson(response, 200, await externalKnowledge.listBindings());
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (externalKnowledge &&
|
|
31
|
+
url.pathname === '/api/external-knowledge/bindings' &&
|
|
32
|
+
request.method === 'POST') {
|
|
33
|
+
sendJson(response, 201, await externalKnowledge.createBinding(await readJson(request)));
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const action = url.pathname.match(
|
|
38
|
+
/^\/api\/external-knowledge\/bindings\/([^/]+)\/(check|sync|retrieve)$/
|
|
39
|
+
);
|
|
40
|
+
if (externalKnowledge && action && request.method === 'POST') {
|
|
41
|
+
const id = decodeURIComponent(action[1]);
|
|
42
|
+
const body = await readJson(request);
|
|
43
|
+
const result = action[2] === 'check'
|
|
44
|
+
? await externalKnowledge.checkBinding(id)
|
|
45
|
+
: action[2] === 'sync'
|
|
46
|
+
? await externalKnowledge.syncBinding(id, body)
|
|
47
|
+
: await externalKnowledge.retrieveBinding(id, body);
|
|
48
|
+
sendJson(response, 200, result);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const targets = url.pathname.match(
|
|
53
|
+
/^\/api\/external-knowledge\/bindings\/([^/]+)\/targets$/
|
|
54
|
+
);
|
|
55
|
+
if (externalKnowledge && targets && request.method === 'PATCH') {
|
|
56
|
+
sendJson(
|
|
57
|
+
response,
|
|
58
|
+
200,
|
|
59
|
+
await externalKnowledge.updateBindingTargets(
|
|
60
|
+
decodeURIComponent(targets[1]),
|
|
61
|
+
await readJson(request)
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const binding = url.pathname.match(
|
|
68
|
+
/^\/api\/external-knowledge\/bindings\/([^/]+)$/
|
|
69
|
+
);
|
|
70
|
+
if (externalKnowledge && binding && request.method === 'DELETE') {
|
|
71
|
+
sendJson(
|
|
72
|
+
response,
|
|
73
|
+
200,
|
|
74
|
+
await externalKnowledge.deleteBinding(decodeURIComponent(binding[1]))
|
|
75
|
+
);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (connectedKnowledge &&
|
|
80
|
+
url.pathname === '/api/external-knowledge/conflict-policy' &&
|
|
81
|
+
request.method === 'GET') {
|
|
82
|
+
sendJson(response, 200, connectedKnowledge.getConflictPolicy({
|
|
83
|
+
personalProjectId: url.searchParams.get('personalProjectId')
|
|
84
|
+
}));
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (connectedKnowledge &&
|
|
88
|
+
url.pathname === '/api/external-knowledge/conflict-policy' &&
|
|
89
|
+
request.method === 'PATCH') {
|
|
90
|
+
sendJson(
|
|
91
|
+
response,
|
|
92
|
+
200,
|
|
93
|
+
await connectedKnowledge.updateConflictPolicy(await readJson(request))
|
|
94
|
+
);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (connectedKnowledge &&
|
|
98
|
+
url.pathname === '/api/connected-knowledge/search' &&
|
|
99
|
+
request.method === 'POST') {
|
|
100
|
+
sendJson(response, 200, await connectedKnowledge.query(await readJson(request)));
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
@@ -44,7 +44,8 @@ export async function handleGraphApiRequest({ request, response, url, app }) {
|
|
|
44
44
|
spaceId: url.searchParams.get('spaceId'),
|
|
45
45
|
providerUrl: url.searchParams.get('providerUrl'),
|
|
46
46
|
personalProjectId: url.searchParams.get('personalProjectId'),
|
|
47
|
-
limit: numberParam(url, 'limit', 500)
|
|
47
|
+
limit: numberParam(url, 'limit', 500),
|
|
48
|
+
offset: nonNegativeNumberParam(url, 'offset', null)
|
|
48
49
|
}));
|
|
49
50
|
return true;
|
|
50
51
|
}
|
|
@@ -255,3 +256,13 @@ function numberParam(url, name, fallback) {
|
|
|
255
256
|
}
|
|
256
257
|
return value;
|
|
257
258
|
}
|
|
259
|
+
|
|
260
|
+
function nonNegativeNumberParam(url, name, fallback) {
|
|
261
|
+
const raw = url.searchParams.get(name);
|
|
262
|
+
if (raw === null) return fallback;
|
|
263
|
+
const value = Number(raw);
|
|
264
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
265
|
+
throw new TypeError(`${name} must be a non-negative integer`);
|
|
266
|
+
}
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
|
|
1
3
|
import { sendJson } from './response.js';
|
|
2
4
|
|
|
3
5
|
export function localServerAuthority(address) {
|
|
@@ -9,18 +11,38 @@ export function localServerAuthority(address) {
|
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function rejectDisallowedRequest({ request, response, authority }) {
|
|
14
|
+
return rejectRequestOutsidePolicy({ request, response, authority });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function rejectRequestOutsidePolicy({
|
|
18
|
+
request,
|
|
19
|
+
response,
|
|
20
|
+
authority,
|
|
21
|
+
lanAuthorities = [],
|
|
22
|
+
lanAccessToken = null
|
|
23
|
+
}) {
|
|
12
24
|
const origin = request.headers.origin;
|
|
13
25
|
const fetchSite = request.headers['sec-fetch-site'];
|
|
26
|
+
const requestAuthority = request.headers.host;
|
|
27
|
+
const lanRequest = lanAuthorities.includes(requestAuthority);
|
|
28
|
+
const loopbackRequest = requestAuthority === authority;
|
|
14
29
|
if (
|
|
15
30
|
!authority ||
|
|
16
|
-
|
|
17
|
-
(
|
|
31
|
+
(requestAuthority !== authority && !lanRequest) ||
|
|
32
|
+
(lanAuthorities.length > 0 && loopbackRequest && !isLoopbackAddress(request.socket?.remoteAddress)) ||
|
|
33
|
+
(origin !== undefined && origin !== `http://${requestAuthority}`) ||
|
|
18
34
|
(typeof fetchSite === 'string' && fetchSite.toLowerCase() === 'cross-site')
|
|
19
35
|
) {
|
|
20
36
|
sendJson(response, 403, { error: 'Forbidden' });
|
|
21
37
|
return true;
|
|
22
38
|
}
|
|
23
39
|
|
|
40
|
+
if (lanRequest && !hasLanAccess(request.headers.authorization, lanAccessToken)) {
|
|
41
|
+
response.setHeader('www-authenticate', 'Basic realm="FULI LAN", charset="UTF-8"');
|
|
42
|
+
sendJson(response, 401, { error: 'LAN access code required' });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
24
46
|
if (requiresJson(request) && !isJson(request.headers['content-type'])) {
|
|
25
47
|
sendJson(response, 415, { error: 'Unsupported media type' });
|
|
26
48
|
return true;
|
|
@@ -29,8 +51,21 @@ export function rejectDisallowedRequest({ request, response, authority }) {
|
|
|
29
51
|
return false;
|
|
30
52
|
}
|
|
31
53
|
|
|
54
|
+
function hasLanAccess(authorization, accessToken) {
|
|
55
|
+
if (typeof accessToken !== 'string' || !accessToken) return false;
|
|
56
|
+
const expected = `Basic ${Buffer.from(`fuli:${accessToken}`).toString('base64')}`;
|
|
57
|
+
if (typeof authorization !== 'string' || authorization.length !== expected.length) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return timingSafeEqual(Buffer.from(authorization), Buffer.from(expected));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isLoopbackAddress(address) {
|
|
64
|
+
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
function requiresJson(request) {
|
|
33
|
-
if (
|
|
68
|
+
if (!['PATCH', 'POST', 'PUT'].includes(request.method)) return false;
|
|
34
69
|
const pathname = new URL(request.url, 'http://127.0.0.1').pathname;
|
|
35
70
|
return pathname.startsWith('/api/');
|
|
36
71
|
}
|