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.
Files changed (207) hide show
  1. package/README.en.md +5 -0
  2. package/README.md +411 -173
  3. package/README.zh-CN.md +448 -0
  4. package/compose.graphiti.yml +6 -6
  5. package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
  6. package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
  7. package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
  8. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
  9. package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
  10. package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
  11. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
  12. package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
  13. package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
  14. package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
  15. package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
  16. package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
  17. package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
  18. package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
  19. package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
  20. package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
  21. package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
  22. package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
  23. package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
  24. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  25. package/dist/web/assets/console-CbDsKrsO.js +1 -0
  26. package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
  27. package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
  28. package/dist/web/assets/i18n-BQcns1PH.js +5 -0
  29. package/dist/web/assets/identity-B8K4VYg6.js +1 -0
  30. package/dist/web/assets/index-CB8Xmsc2.js +2 -0
  31. package/dist/web/assets/index-CZeuPh-U.css +1 -0
  32. package/dist/web/assets/model-eF52GgPF.js +1 -0
  33. package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
  34. package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
  35. package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
  36. package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
  37. package/dist/web/index.html +9 -6
  38. package/docs/external-knowledge-architecture.md +279 -0
  39. package/examples/external-knowledge/markdown-folder.mjs +235 -0
  40. package/graph-provider/fuli_graph/app.py +57 -1
  41. package/graph-provider/fuli_graph/graph_models.py +3 -0
  42. package/graph-provider/fuli_graph/graph_query.py +62 -16
  43. package/graph-provider/fuli_graph/knowledge_review.py +612 -0
  44. package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
  45. package/graph-provider/fuli_graph/knowledge_search.py +51 -4
  46. package/graph-provider/fuli_graph/models.py +19 -0
  47. package/graph-provider/fuli_graph/runtime.py +15 -0
  48. package/graph-provider/fuli_graph/store_knowledge.py +2 -1
  49. package/graph-provider/pyproject.toml +1 -1
  50. package/npm-shrinkwrap.json +5 -407
  51. package/package.json +7 -4
  52. package/skills/capturing-session-knowledge/SKILL.md +24 -4
  53. package/skills/flreview/SKILL.md +73 -0
  54. package/skills/flreview/agents/openai.yaml +6 -0
  55. package/src/agent-tools/graph-definitions.js +79 -4
  56. package/src/agent-tools/graph-handlers.js +12 -0
  57. package/src/cli/command-registry.js +3 -3
  58. package/src/cli/local-runtime-command.js +60 -23
  59. package/src/cli/local-runtime-options.js +16 -6
  60. package/src/cli/setup-agent-selection.js +12 -12
  61. package/src/cli/setup-command.js +41 -18
  62. package/src/cli/shell-argument.js +10 -0
  63. package/src/cli/uninstall-command.js +17 -15
  64. package/src/cli/update-command.js +38 -30
  65. package/src/external-knowledge/catalog.js +61 -0
  66. package/src/external-knowledge/conflict-policy.js +58 -0
  67. package/src/external-knowledge/connected-search.js +166 -0
  68. package/src/external-knowledge/connectors/custom.js +133 -0
  69. package/src/external-knowledge/connectors/feishu.js +461 -0
  70. package/src/external-knowledge/connectors/mcp.js +438 -0
  71. package/src/external-knowledge/connectors/notion.js +307 -0
  72. package/src/external-knowledge/connectors/retrieval-api.js +222 -0
  73. package/src/external-knowledge/document-mapping.js +151 -0
  74. package/src/external-knowledge/graph-projection.js +17 -0
  75. package/src/external-knowledge/index.js +8 -0
  76. package/src/external-knowledge/registry.js +91 -0
  77. package/src/external-knowledge/runtime.js +68 -0
  78. package/src/external-knowledge/safe-diagnostic.js +14 -0
  79. package/src/external-knowledge/service.js +811 -0
  80. package/src/graphiti/agent-knowledge-workflows.js +7 -2
  81. package/src/graphiti/federated-application.js +66 -41
  82. package/src/graphiti/knowledge-item-role.js +21 -0
  83. package/src/graphiti/knowledge-review.js +43 -0
  84. package/src/graphiti/project-profile-mapping.js +32 -0
  85. package/src/graphiti/provider-client.js +22 -1
  86. package/src/graphiti/source-marker.js +54 -3
  87. package/src/graphiti/task-knowledge-recall.js +250 -0
  88. package/src/http/api-router.js +23 -108
  89. package/src/http/external-knowledge-api-router.js +104 -0
  90. package/src/http/graph-api-router.js +12 -1
  91. package/src/http/request-policy.js +38 -3
  92. package/src/local-runtime/lifecycle.js +37 -16
  93. package/src/mcp/create-mcp-server.js +1 -0
  94. package/src/mcp/instructions.js +7 -7
  95. package/src/mcp/tool-annotations.js +6 -0
  96. package/src/mcp/tool-result.js +1 -0
  97. package/src/server/application-lifecycle.js +2 -11
  98. package/src/server/blocked-ports.js +11 -0
  99. package/src/server/lan-access.js +52 -0
  100. package/src/server.js +103 -25
  101. package/src/setup/agent-installation-status.js +2 -1
  102. package/src/setup/agents.js +2 -1
  103. package/src/setup/claude-code-config.js +2 -1
  104. package/src/setup/codex-bootstrap.js +12 -7
  105. package/src/setup/container-runtime.js +25 -21
  106. package/src/setup/graph-runtime.js +219 -58
  107. package/src/setup/node-runtime.js +3 -3
  108. package/src/setup/options.js +2 -4
  109. package/src/setup/paths.js +18 -3
  110. package/src/setup/review-skill-adapters.js +49 -0
  111. package/src/setup/setup.js +26 -2
  112. package/src/setup/uninstall.js +4 -2
  113. package/src/system/resource-monitor.js +317 -0
  114. package/src/system/runtime-settings.js +131 -0
  115. package/src/system/system-service.js +119 -0
  116. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  118. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-CJCNI2Wd.css +0 -1
  120. package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.js +0 -1
  121. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  122. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  123. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  124. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  125. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  126. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  127. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  128. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  129. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  130. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  131. package/dist/web/assets/index-BQRkOwPx.js +0 -2
  132. package/dist/web/assets/index-CpLDx3of.css +0 -1
  133. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  134. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  135. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  136. package/src/agent/candidate-read-service.js +0 -53
  137. package/src/agent/context-pack-service.js +0 -83
  138. package/src/agent/public-read-policy.js +0 -39
  139. package/src/agent/public-read-service.js +0 -69
  140. package/src/agent/read-projection.js +0 -107
  141. package/src/agent/search-service.js +0 -61
  142. package/src/agent/space-boundary.js +0 -33
  143. package/src/app/bootstrap.js +0 -8
  144. package/src/app/create-application.js +0 -143
  145. package/src/app/state-service.js +0 -38
  146. package/src/candidates.js +0 -78
  147. package/src/classifier.js +0 -63
  148. package/src/cli/command-arguments.js +0 -14
  149. package/src/context-pack.js +0 -206
  150. package/src/extractor.js +0 -77
  151. package/src/fact-history.js +0 -39
  152. package/src/facts/fact-service.js +0 -89
  153. package/src/http/bounded-json-array.js +0 -19
  154. package/src/http/lens-route.js +0 -50
  155. package/src/ingestion.js +0 -161
  156. package/src/lens/lens-correction-service.js +0 -81
  157. package/src/lens/lens-fact-writer.js +0 -85
  158. package/src/lens/lens-input.js +0 -62
  159. package/src/lens/lens-query-projection.js +0 -87
  160. package/src/lens/lens-query-ranking.js +0 -62
  161. package/src/lens/lens-query.js +0 -143
  162. package/src/lens/lens-resource-projection.js +0 -93
  163. package/src/lens/lens-resource-selection.js +0 -45
  164. package/src/lens/lens-resource-service.js +0 -103
  165. package/src/lens/lens-search-query.js +0 -122
  166. package/src/lens/lens-service.js +0 -183
  167. package/src/models.js +0 -44
  168. package/src/observer.js +0 -83
  169. package/src/project-rules.js +0 -33
  170. package/src/publication/outbox-service.js +0 -46
  171. package/src/publication/publication-envelope-schema.js +0 -75
  172. package/src/publication/publication-envelope.js +0 -37
  173. package/src/publication/publication-policy.js +0 -82
  174. package/src/publication/publication-service.js +0 -48
  175. package/src/router.js +0 -58
  176. package/src/runtime-options.js +0 -138
  177. package/src/security/episode-visibility.js +0 -14
  178. package/src/security/fact-visibility.js +0 -15
  179. package/src/security/source-metadata-policy.js +0 -8
  180. package/src/storage/episode-evidence-preview.js +0 -100
  181. package/src/storage/fact-search-order.js +0 -18
  182. package/src/storage/file/episode-evidence.js +0 -37
  183. package/src/storage/file/episode-index.js +0 -59
  184. package/src/storage/file/fact-search.js +0 -60
  185. package/src/storage/file-store.js +0 -437
  186. package/src/storage/migrate.js +0 -128
  187. package/src/storage/migrations/001-initial.sql +0 -83
  188. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  189. package/src/storage/record-validation.js +0 -46
  190. package/src/storage/snapshot-defaults.js +0 -54
  191. package/src/storage/sqlite/candidates-repository.js +0 -84
  192. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  193. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  194. package/src/storage/sqlite/episodes-repository.js +0 -52
  195. package/src/storage/sqlite/fact-search-repository.js +0 -162
  196. package/src/storage/sqlite/facts-repository.js +0 -146
  197. package/src/storage/sqlite/imports-repository.js +0 -39
  198. package/src/storage/sqlite/mapper.js +0 -84
  199. package/src/storage/sqlite/open-database.js +0 -15
  200. package/src/storage/sqlite/outbox-repository.js +0 -103
  201. package/src/storage/sqlite/snapshot-repository.js +0 -69
  202. package/src/storage/sqlite/spaces-repository.js +0 -52
  203. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  204. package/src/storage/sqlite-store.js +0 -159
  205. package/src/storage/stable-json.js +0 -24
  206. package/src/storage/store-port.js +0 -20
  207. package/src/store.js +0 -2
@@ -0,0 +1,307 @@
1
+ const NOTION_API_ROOT = 'https://api.notion.com';
2
+ const NOTION_API_VERSION = '2026-03-11';
3
+
4
+ export function createNotionConnector({ fetchImpl = globalThis.fetch } = {}) {
5
+ const request = notionRequester(fetchImpl);
6
+ return {
7
+ type: 'notion',
8
+ name: 'Notion',
9
+ capabilities: ['discover', 'sync', 'retrieve'],
10
+
11
+ async check(context) {
12
+ notionSources(context.source);
13
+ const user = await request(context, 'GET', '/v1/users/me');
14
+ return {
15
+ status: 'ready',
16
+ capabilities: ['discover', 'sync', 'retrieve'],
17
+ account: { id: user.id ?? null, name: user.name ?? null },
18
+ apiVersion: NOTION_API_VERSION
19
+ };
20
+ },
21
+
22
+ async discover(context) {
23
+ const body = {
24
+ page_size: pageSize(context.limit),
25
+ filter: { property: 'object', value: 'page' },
26
+ ...(context.cursor ? { start_cursor: context.cursor } : {}),
27
+ ...(context.query ? { query: context.query } : {})
28
+ };
29
+ const result = await request(context, 'POST', '/v1/search', body);
30
+ return {
31
+ items: (result.results ?? []).map(pageSummary),
32
+ nextCursor: result.next_cursor ?? null,
33
+ hasMore: result.has_more === true
34
+ };
35
+ },
36
+
37
+ async sync(context) {
38
+ const sources = notionSources(context.source);
39
+ const state = decodeCursor(context.cursor);
40
+ if (state.sourceIndex >= sources.length) return emptySyncPage();
41
+ const current = sources[state.sourceIndex];
42
+ if (current.kind === 'page') {
43
+ const page = await request(
44
+ context,
45
+ 'GET',
46
+ `/v1/pages/${encodeURIComponent(current.id)}`
47
+ );
48
+ const nextCursor = nextSourceCursor(state.sourceIndex, sources.length);
49
+ if (page.in_trash === true) {
50
+ return {
51
+ items: [],
52
+ deleted: [page.id ?? current.id],
53
+ nextCursor,
54
+ hasMore: nextCursor !== null
55
+ };
56
+ }
57
+ return {
58
+ items: [await notionPageDocument(context, request, page)],
59
+ deleted: [],
60
+ nextCursor,
61
+ hasMore: nextCursor !== null
62
+ };
63
+ }
64
+ const query = await request(
65
+ context,
66
+ 'POST',
67
+ `/v1/data_sources/${encodeURIComponent(current.id)}/query`,
68
+ {
69
+ page_size: pageSize(context.limit),
70
+ ...(state.startCursor ? { start_cursor: state.startCursor } : {})
71
+ }
72
+ );
73
+ const activePages = (query.results ?? []).filter((page) => page.in_trash !== true);
74
+ const items = [];
75
+ for (const page of activePages) {
76
+ items.push(await notionPageDocument(context, request, page));
77
+ }
78
+ const nextCursor = query.has_more === true
79
+ ? encodeCursor({ sourceIndex: state.sourceIndex, startCursor: query.next_cursor })
80
+ : nextSourceCursor(state.sourceIndex, sources.length);
81
+ return {
82
+ items,
83
+ deleted: (query.results ?? [])
84
+ .filter((page) => page.in_trash === true)
85
+ .map(({ id }) => id),
86
+ nextCursor,
87
+ hasMore: nextCursor !== null
88
+ };
89
+ },
90
+
91
+ async retrieve(context) {
92
+ const sources = notionSources(context.source);
93
+ const result = await request(context, 'POST', '/v1/search', {
94
+ query: requiredString(context.query, 'Notion query'),
95
+ page_size: pageSize(context.limit),
96
+ filter: { property: 'object', value: 'page' }
97
+ });
98
+ const allowed = (result.results ?? [])
99
+ .filter((page) => page.in_trash !== true && notionPageIsBound(page, sources))
100
+ .slice(0, pageSize(context.limit));
101
+ const items = [];
102
+ for (const page of allowed) {
103
+ items.push(await notionPageDocument(context, request, page));
104
+ }
105
+ return { items };
106
+ }
107
+ };
108
+ }
109
+
110
+ function notionRequester(fetchImpl) {
111
+ if (typeof fetchImpl !== 'function') throw new TypeError('Notion fetch is unavailable');
112
+ return async (context, method, path, body = undefined) => {
113
+ if (!['GET', 'POST'].includes(method)) {
114
+ throw new TypeError('Notion connector only permits read operations');
115
+ }
116
+ const token = credentialFromEnvironment(context, 'tokenEnv', 'Notion token');
117
+ const response = await fetchImpl(`${NOTION_API_ROOT}${path}`, {
118
+ method,
119
+ headers: {
120
+ Authorization: `Bearer ${token}`,
121
+ 'Notion-Version': NOTION_API_VERSION,
122
+ Accept: 'application/json',
123
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' })
124
+ },
125
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
126
+ signal: AbortSignal.timeout(timeoutMilliseconds(context.config?.timeoutMs))
127
+ });
128
+ const payload = await response.json().catch(() => ({}));
129
+ if (!response.ok) {
130
+ const error = new Error(
131
+ payload.message ?? `Notion API request failed with status ${response.status}`
132
+ );
133
+ error.status = response.status;
134
+ throw error;
135
+ }
136
+ return payload;
137
+ };
138
+ }
139
+
140
+ async function notionPageDocument(context, request, page) {
141
+ const id = requiredString(page.id, 'Notion page id');
142
+ const markdown = await request(
143
+ context,
144
+ 'GET',
145
+ `/v1/pages/${encodeURIComponent(id)}/markdown`
146
+ );
147
+ let content = typeof markdown.markdown === 'string' ? markdown.markdown : '';
148
+ const unresolved = [];
149
+ for (const blockId of (markdown.unknown_block_ids ?? []).slice(0, 100)) {
150
+ try {
151
+ const block = await request(
152
+ context,
153
+ 'GET',
154
+ `/v1/pages/${encodeURIComponent(blockId)}/markdown`
155
+ );
156
+ if (typeof block.markdown === 'string' && block.markdown.trim()) {
157
+ content += `\n\n${block.markdown}`;
158
+ }
159
+ } catch (error) {
160
+ if (error.status !== 404) throw error;
161
+ unresolved.push(blockId);
162
+ }
163
+ }
164
+ if (!content.trim()) content = `# ${notionTitle(page)}`;
165
+ return {
166
+ id,
167
+ title: notionTitle(page),
168
+ content,
169
+ url: onlineUrl(page.url),
170
+ updatedAt: dateTimeOrNull(page.last_edited_time),
171
+ metadata: {
172
+ notionObject: page.object ?? 'page',
173
+ parent: jsonObject(page.parent),
174
+ markdownTruncated: markdown.truncated === true,
175
+ unresolvedBlockIds: unresolved
176
+ }
177
+ };
178
+ }
179
+
180
+ function notionSources(source, { allowEmpty = false } = {}) {
181
+ const pageIds = idArray(source?.pageIds, 'Notion pageIds');
182
+ const dataSourceIds = idArray(source?.dataSourceIds, 'Notion dataSourceIds');
183
+ const sources = [
184
+ ...pageIds.map((id) => ({ kind: 'page', id })),
185
+ ...dataSourceIds.map((id) => ({ kind: 'data_source', id }))
186
+ ];
187
+ if (!allowEmpty && sources.length === 0) {
188
+ throw new TypeError('Notion source requires pageIds or dataSourceIds');
189
+ }
190
+ return sources;
191
+ }
192
+
193
+ function notionPageIsBound(page, sources) {
194
+ if (sources.some(({ kind, id }) => kind === 'page' && id === page.id)) return true;
195
+ const parentId = page.parent?.data_source_id ?? page.parent?.database_id;
196
+ return sources.some(({ kind, id }) => kind === 'data_source' && id === parentId);
197
+ }
198
+
199
+ function pageSummary(page) {
200
+ return {
201
+ id: page.id,
202
+ title: notionTitle(page),
203
+ type: 'page',
204
+ url: onlineUrl(page.url),
205
+ updatedAt: dateTimeOrNull(page.last_edited_time),
206
+ metadata: { parent: jsonObject(page.parent) }
207
+ };
208
+ }
209
+
210
+ function notionTitle(page) {
211
+ for (const property of Object.values(page.properties ?? {})) {
212
+ if (property?.type !== 'title' || !Array.isArray(property.title)) continue;
213
+ const title = property.title.map((item) => item?.plain_text ?? '').join('').trim();
214
+ if (title) return title;
215
+ }
216
+ return page.title?.map?.((item) => item?.plain_text ?? '').join('').trim()
217
+ || page.id
218
+ || 'Untitled Notion page';
219
+ }
220
+
221
+ function nextSourceCursor(index, length) {
222
+ return index + 1 < length
223
+ ? encodeCursor({ sourceIndex: index + 1, startCursor: null })
224
+ : null;
225
+ }
226
+
227
+ function encodeCursor(value) {
228
+ return Buffer.from(JSON.stringify(value)).toString('base64url');
229
+ }
230
+
231
+ function decodeCursor(value) {
232
+ if (!value) return { sourceIndex: 0, startCursor: null };
233
+ try {
234
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
235
+ if (!Number.isInteger(parsed.sourceIndex) || parsed.sourceIndex < 0) throw new Error();
236
+ if (parsed.startCursor !== null && typeof parsed.startCursor !== 'string') throw new Error();
237
+ return parsed;
238
+ } catch {
239
+ throw new TypeError('Invalid Notion synchronization cursor');
240
+ }
241
+ }
242
+
243
+ function emptySyncPage() {
244
+ return { items: [], deleted: [], nextCursor: null, hasMore: false };
245
+ }
246
+
247
+ function credentialFromEnvironment(context, key, label) {
248
+ const name = context.config?.[key];
249
+ if (typeof name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) {
250
+ throw new TypeError(`${label} environment variable name is required`);
251
+ }
252
+ const value = context.env?.[name];
253
+ if (typeof value !== 'string' || !value) {
254
+ throw new TypeError(`${label} environment variable is unavailable: ${name}`);
255
+ }
256
+ return value;
257
+ }
258
+
259
+ function idArray(value, label) {
260
+ if (value === undefined || value === null) return [];
261
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || !item.trim())) {
262
+ throw new TypeError(`${label} must be an array of nonempty strings`);
263
+ }
264
+ return [...new Set(value.map((item) => item.trim()))];
265
+ }
266
+
267
+ function pageSize(value) {
268
+ if (value === undefined || value === null) return 100;
269
+ if (!Number.isInteger(value) || value <= 0) throw new TypeError('Notion page size is invalid');
270
+ return Math.min(value, 100);
271
+ }
272
+
273
+ function timeoutMilliseconds(value) {
274
+ if (value === undefined || value === null) return 30_000;
275
+ if (!Number.isInteger(value) || value < 1_000 || value > 120_000) {
276
+ throw new TypeError('Notion timeoutMs must be between 1000 and 120000');
277
+ }
278
+ return value;
279
+ }
280
+
281
+ function requiredString(value, label) {
282
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${label} is required`);
283
+ return value.trim();
284
+ }
285
+
286
+ function onlineUrl(value) {
287
+ if (typeof value !== 'string') return null;
288
+ try {
289
+ const url = new URL(value);
290
+ return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password
291
+ ? url.toString()
292
+ : null;
293
+ } catch {
294
+ return null;
295
+ }
296
+ }
297
+
298
+ function dateTimeOrNull(value) {
299
+ if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) return null;
300
+ return new Date(value).toISOString();
301
+ }
302
+
303
+ function jsonObject(value) {
304
+ return value && typeof value === 'object' && !Array.isArray(value)
305
+ ? structuredClone(value)
306
+ : {};
307
+ }
@@ -0,0 +1,222 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const DEFAULT_SCORE_THRESHOLD = 0.5;
4
+ const MAX_KNOWLEDGE_BASES = 32;
5
+
6
+ export function createRetrievalApiConnector({ fetchImpl = globalThis.fetch } = {}) {
7
+ if (typeof fetchImpl !== 'function') {
8
+ throw new TypeError('Retrieval API fetch is unavailable');
9
+ }
10
+
11
+ async function retrieve(context, { checking = false } = {}) {
12
+ const endpoint = retrievalEndpoint(context.config?.url);
13
+ const knowledgeIds = idArray(context.source?.knowledgeIds, 'knowledgeIds');
14
+ const limit = checking ? 1 : positiveInteger(context.limit ?? 12, 'limit', 100);
15
+ const query = checking
16
+ ? 'FULI connection check'
17
+ : requiredString(context.query, 'Retrieval query', 2_000);
18
+ const results = await Promise.all(knowledgeIds.map(async (knowledgeId) => {
19
+ const payload = await request(fetchImpl, endpoint, context, {
20
+ knowledge_id: knowledgeId,
21
+ query,
22
+ retrieval_setting: {
23
+ top_k: limit,
24
+ score_threshold: scoreThreshold(context.config?.scoreThreshold)
25
+ }
26
+ });
27
+ if (!Array.isArray(payload.records)) {
28
+ throw new TypeError('Retrieval API response records must be an array');
29
+ }
30
+ return payload.records.slice(0, limit).map((record, index) =>
31
+ retrievalDocument(record, { knowledgeId, index })
32
+ );
33
+ }));
34
+ return {
35
+ items: results.flat()
36
+ .sort((left, right) => (right.metadata.score ?? 0) - (left.metadata.score ?? 0))
37
+ .slice(0, limit)
38
+ };
39
+ }
40
+
41
+ return {
42
+ type: 'retrieval_api',
43
+ name: 'RAG Retrieval API',
44
+ capabilities: ['retrieve'],
45
+
46
+ async check(context) {
47
+ await retrieve(context, { checking: true });
48
+ return {
49
+ status: 'ready',
50
+ capabilities: ['retrieve'],
51
+ protocol: 'dify_external_knowledge_v1'
52
+ };
53
+ },
54
+
55
+ retrieve
56
+ };
57
+ }
58
+
59
+ async function request(fetchImpl, endpoint, context, body) {
60
+ const headers = {
61
+ Accept: 'application/json',
62
+ 'Content-Type': 'application/json'
63
+ };
64
+ const tokenName = optionalEnvironmentName(context.config?.tokenEnv, 'tokenEnv');
65
+ if (tokenName) {
66
+ const token = context.env?.[tokenName];
67
+ if (typeof token !== 'string' || !token) {
68
+ throw new TypeError(`Retrieval API token environment variable is unavailable: ${tokenName}`);
69
+ }
70
+ headers.Authorization = `Bearer ${token}`;
71
+ }
72
+ const response = await fetchImpl(endpoint, {
73
+ method: 'POST',
74
+ headers,
75
+ body: JSON.stringify(body),
76
+ signal: AbortSignal.timeout(timeoutMilliseconds(context.config?.timeoutMs))
77
+ });
78
+ const payload = await response.json().catch(() => ({}));
79
+ if (!response.ok) {
80
+ throw new Error(
81
+ payload?.message ?? payload?.error ??
82
+ `Retrieval API request failed with status ${response.status}`
83
+ );
84
+ }
85
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
86
+ throw new TypeError('Retrieval API response must be an object');
87
+ }
88
+ return payload;
89
+ }
90
+
91
+ function retrievalDocument(record, { knowledgeId, index }) {
92
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
93
+ throw new TypeError('Retrieval API records must be objects');
94
+ }
95
+ const metadata = jsonObject(record.metadata);
96
+ const content = requiredString(record.content, 'Retrieval record content', 1_000_000);
97
+ const title = firstString(
98
+ record.title,
99
+ metadata.title,
100
+ metadata.document_name,
101
+ metadata.filename,
102
+ `Knowledge result ${index + 1}`
103
+ );
104
+ const sourceId = firstString(
105
+ metadata.segment_id,
106
+ metadata.chunk_id,
107
+ metadata.document_id,
108
+ metadata.id,
109
+ metadata.path,
110
+ metadata.url,
111
+ metadata.source_url
112
+ ) ?? createHash('sha256')
113
+ .update(`${knowledgeId}\0${title}\0${content}`)
114
+ .digest('hex');
115
+ const combinedId = `${knowledgeId}:${sourceId}`;
116
+ return {
117
+ id: combinedId.length <= 512
118
+ ? combinedId
119
+ : `retrieval:${createHash('sha256').update(combinedId).digest('hex')}`,
120
+ title,
121
+ content,
122
+ url: onlineUrl(firstString(metadata.url, metadata.source_url)),
123
+ updatedAt: dateTime(firstString(metadata.updated_at, metadata.updatedAt)),
124
+ metadata: {
125
+ ...metadata,
126
+ knowledgeId,
127
+ score: finiteNumber(record.score)
128
+ }
129
+ };
130
+ }
131
+
132
+ function retrievalEndpoint(value) {
133
+ const raw = requiredString(value, 'Retrieval API URL', 2_048);
134
+ const url = new URL(raw);
135
+ const loopback = url.hostname === 'localhost' ||
136
+ url.hostname === '127.0.0.1' ||
137
+ url.hostname === '[::1]';
138
+ if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) ||
139
+ url.username || url.password) {
140
+ throw new TypeError('Retrieval API URL must use HTTPS or loopback HTTP without credentials');
141
+ }
142
+ return url.toString();
143
+ }
144
+
145
+ function idArray(value, label) {
146
+ if (!Array.isArray(value) || value.length === 0 || value.length > MAX_KNOWLEDGE_BASES) {
147
+ throw new TypeError(`${label} must contain 1 to ${MAX_KNOWLEDGE_BASES} IDs`);
148
+ }
149
+ const ids = value.map((item) => requiredString(item, `${label} item`, 512));
150
+ if (new Set(ids).size !== ids.length) throw new TypeError(`${label} must be unique`);
151
+ return ids;
152
+ }
153
+
154
+ function scoreThreshold(value) {
155
+ if (value === undefined || value === null || value === '') return DEFAULT_SCORE_THRESHOLD;
156
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1) {
157
+ throw new TypeError('scoreThreshold must be a number from 0 to 1');
158
+ }
159
+ return value;
160
+ }
161
+
162
+ function timeoutMilliseconds(value) {
163
+ if (value === undefined || value === null) return 20_000;
164
+ return positiveInteger(value, 'timeoutMs', 120_000);
165
+ }
166
+
167
+ function optionalEnvironmentName(value, label) {
168
+ if (value === undefined || value === null || value === '') return null;
169
+ if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value)) {
170
+ throw new TypeError(`${label} must be an environment variable name`);
171
+ }
172
+ return value;
173
+ }
174
+
175
+ function jsonObject(value) {
176
+ return value && typeof value === 'object' && !Array.isArray(value)
177
+ ? structuredClone(value)
178
+ : {};
179
+ }
180
+
181
+ function onlineUrl(value) {
182
+ if (!value) return null;
183
+ try {
184
+ const url = new URL(value);
185
+ return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password
186
+ ? url.toString()
187
+ : null;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ function dateTime(value) {
194
+ return value && !Number.isNaN(Date.parse(value))
195
+ ? new Date(value).toISOString()
196
+ : null;
197
+ }
198
+
199
+ function finiteNumber(value) {
200
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
201
+ }
202
+
203
+ function firstString(...values) {
204
+ for (const value of values) {
205
+ if (typeof value === 'string' && value.trim()) return value.trim();
206
+ }
207
+ return null;
208
+ }
209
+
210
+ function requiredString(value, label, maximum) {
211
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${label} is required`);
212
+ const normalized = value.trim();
213
+ if (normalized.length > maximum) throw new TypeError(`${label} is too long`);
214
+ return normalized;
215
+ }
216
+
217
+ function positiveInteger(value, label, maximum) {
218
+ if (!Number.isInteger(value) || value <= 0 || value > maximum) {
219
+ throw new TypeError(`${label} must be a positive integer no greater than ${maximum}`);
220
+ }
221
+ return value;
222
+ }
@@ -0,0 +1,151 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { detectSensitiveContent } from '../security/sensitive-content.js';
4
+
5
+ const MAX_CHUNK_LENGTH = 6_000;
6
+ const MAX_EXCERPT_LENGTH = 2_048;
7
+
8
+ export function normalizeExternalDocument(value) {
9
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
10
+ throw new TypeError('Connector items must be objects');
11
+ }
12
+ const id = requiredString(value.id, 'Connector item id', 512);
13
+ const title = requiredString(value.title ?? value.name ?? id, 'Connector item title', 512);
14
+ const content = requiredString(value.content, 'Connector item content', 1_000_000);
15
+ const url = optionalOnlineUrl(value.url);
16
+ const updatedAt = optionalDateTime(value.updatedAt, 'Connector item updatedAt');
17
+ const metadata = plainJsonObject(value.metadata ?? {}, 'Connector item metadata');
18
+ if (detectSensitiveContent(JSON.stringify({ title, content, metadata })).restricted) {
19
+ throw new TypeError('Connector item contains credentials and cannot be retrieved or stored');
20
+ }
21
+ return { id, title, content, url, updatedAt, metadata };
22
+ }
23
+
24
+ export function externalDocumentHash(document) {
25
+ return createHash('sha256').update(JSON.stringify({
26
+ title: document.title,
27
+ content: document.content,
28
+ url: document.url,
29
+ updatedAt: document.updatedAt,
30
+ metadata: document.metadata
31
+ })).digest('hex');
32
+ }
33
+
34
+ export function externalDocumentStateKey(id) {
35
+ return createHash('sha256').update(`external-document:${id}`).digest('hex');
36
+ }
37
+
38
+ export function externalDocumentEpisode({ binding, document, now }) {
39
+ const chunks = chunkText(document.content);
40
+ const itemDigest = shortHash(document.id);
41
+ const targetScopeId = binding.targetId ?? binding.id;
42
+ const referenceTime = document.updatedAt ?? now.toISOString();
43
+ return {
44
+ targetKind: 'personal',
45
+ spaceId: binding.target.personalSpaceId,
46
+ personalProjectId: binding.target.personalProjectId,
47
+ idempotencyKey: `external:${targetScopeId}:${itemDigest}:${externalDocumentHash(document)}`,
48
+ sessionId: `external:${targetScopeId}`,
49
+ name: document.title,
50
+ sourceKind: 'document',
51
+ sourceDescription: `Read-only ${binding.connectorType} knowledge source`,
52
+ sourceUri: document.url,
53
+ // The graph contract keeps source_application to a stable agent enum. The
54
+ // connector identity stays in sourceDescription and entity attributes.
55
+ sourceApplication: 'other',
56
+ sourceTurnId: document.id,
57
+ sourceExcerpt: document.content.slice(0, MAX_EXCERPT_LENGTH),
58
+ referenceTime,
59
+ summary: `Imported from the read-only binding “${binding.name}”.`,
60
+ sensitivity: 'restricted',
61
+ entities: chunks.map((content, index) => ({
62
+ key: `external:${targetScopeId}:${itemDigest}:${index + 1}`,
63
+ name: chunks.length === 1 ? document.title : `${document.title} (${index + 1}/${chunks.length})`,
64
+ type: 'ExternalKnowledgeDocument',
65
+ summary: content,
66
+ originQuadrant: 'known_known',
67
+ currentQuadrant: 'known_known',
68
+ epistemicStatus: 'observed',
69
+ confirmationStatus: 'pending',
70
+ confirmationBasis: {
71
+ existenceReason: 'A configured read-only connector returned this source document.',
72
+ quadrantReason: 'The source text is explicit, but FULI has not independently verified it.',
73
+ proposedBy: { kind: 'import', label: binding.connectorType }
74
+ },
75
+ inheritanceMode: 'local_only',
76
+ attributes: {
77
+ externalBindingId: binding.id,
78
+ externalBindingTargetId: targetScopeId,
79
+ externalConnectorType: binding.connectorType,
80
+ externalItemId: document.id,
81
+ externalItemUrl: document.url,
82
+ externalUpdatedAt: document.updatedAt,
83
+ externalChunk: index + 1,
84
+ externalChunkCount: chunks.length,
85
+ externalMetadata: document.metadata
86
+ }
87
+ })),
88
+ relationships: []
89
+ };
90
+ }
91
+
92
+ export function chunkText(content, maximum = MAX_CHUNK_LENGTH) {
93
+ if (content.length <= maximum) return [content];
94
+ const chunks = [];
95
+ let offset = 0;
96
+ while (offset < content.length) {
97
+ const end = Math.min(offset + maximum, content.length);
98
+ let split = end;
99
+ if (end < content.length) {
100
+ const candidate = Math.max(
101
+ content.lastIndexOf('\n\n', end),
102
+ content.lastIndexOf('\n', end),
103
+ content.lastIndexOf(' ', end)
104
+ );
105
+ if (candidate > offset + Math.floor(maximum / 2)) split = candidate;
106
+ }
107
+ chunks.push(content.slice(offset, split).trim());
108
+ offset = split;
109
+ while (/\s/u.test(content[offset] ?? '')) offset += 1;
110
+ }
111
+ return chunks.filter(Boolean);
112
+ }
113
+
114
+ function shortHash(value) {
115
+ return createHash('sha256').update(value).digest('hex').slice(0, 24);
116
+ }
117
+
118
+ function requiredString(value, label, maximum) {
119
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${label} is required`);
120
+ const normalized = value.trim();
121
+ if (normalized.length > maximum) throw new TypeError(`${label} is too long`);
122
+ return normalized;
123
+ }
124
+
125
+ function optionalOnlineUrl(value) {
126
+ if (value === undefined || value === null || value === '') return null;
127
+ const url = new URL(value);
128
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
129
+ throw new TypeError('Connector item URL must be an HTTP(S) URL without credentials');
130
+ }
131
+ return url.toString();
132
+ }
133
+
134
+ function optionalDateTime(value, label) {
135
+ if (value === undefined || value === null || value === '') return null;
136
+ if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) {
137
+ throw new TypeError(`${label} must be an ISO date-time`);
138
+ }
139
+ return new Date(value).toISOString();
140
+ }
141
+
142
+ function plainJsonObject(value, label) {
143
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
144
+ throw new TypeError(`${label} must be an object`);
145
+ }
146
+ try {
147
+ return structuredClone(value);
148
+ } catch {
149
+ throw new TypeError(`${label} must contain JSON-compatible values`);
150
+ }
151
+ }
@@ -0,0 +1,17 @@
1
+ export function mergeExternalKnowledgeProjection(
2
+ externalKnowledge,
3
+ graph,
4
+ personalSpaceId,
5
+ personalProjectId
6
+ ) {
7
+ const projection = externalKnowledge?.projectGraphProjection?.({
8
+ personalSpaceId,
9
+ personalProjectId,
10
+ graph
11
+ }) ?? { nodes: [], edges: [] };
12
+ return {
13
+ ...graph,
14
+ nodes: [...graph.nodes, ...projection.nodes],
15
+ edges: [...graph.edges, ...projection.edges]
16
+ };
17
+ }