fuli-context 0.5.0 → 0.7.3
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.md +370 -202
- 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-qeO_vm0T.js +1 -0
- package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
- package/dist/web/assets/ConnectionsPage-DO0GR9Xg.js +1 -0
- package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
- package/dist/web/assets/KnowledgeInspector-DEW5lA1m.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BkM568Ld.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
- package/dist/web/assets/KnowledgePage-BAT4aZIm.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-DyGtBz--.js +4 -0
- package/dist/web/assets/OverviewPage-CmCcDCAs.js +1 -0
- package/dist/web/assets/PersonalProfilePage-C42mwKN8.js +1 -0
- package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
- package/dist/web/assets/PersonalProjectsPage-BwaXR62K.js +1 -0
- package/dist/web/assets/PublicProjectsPage-egKBulOq.js +1 -0
- package/dist/web/assets/ReviewPage-B3vnZ6gn.js +1 -0
- package/dist/web/assets/SearchableSelect-ih69qbcS.js +1 -0
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
- package/dist/web/assets/SettingsPage-oLE8mNs5.js +1 -0
- package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
- package/dist/web/assets/console-DE40hXGC.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-CDxBs1sz.js +5 -0
- package/dist/web/assets/identity-BCxb1mcy.js +1 -0
- package/dist/web/assets/index-BzhNfzU_.js +2 -0
- package/dist/web/assets/index-CCOGfZzE.css +1 -0
- package/dist/web/assets/model-CCMdt10Q.js +1 -0
- package/dist/web/assets/paths-DwHeiGMT.js +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-HdI7XAmu.js +1 -0
- package/dist/web/assets/vue-router-Drfl1F8i.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 +657 -0
- package/graph-provider/fuli_graph/knowledge_review_models.py +133 -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 -5
- package/skills/capturing-session-knowledge/SKILL.md +24 -4
- package/skills/flreview/SKILL.md +100 -0
- package/skills/flreview/agents/openai.yaml +6 -0
- package/src/agent-tools/graph-definitions.js +81 -6
- package/src/agent-tools/graph-handlers.js +12 -0
- package/src/cli/command-registry.js +3 -3
- package/src/cli/local-runtime-command.js +51 -28
- package/src/cli/local-runtime-options.js +14 -8
- 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 +68 -42
- 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 +1 -1
- package/src/local-runtime/lifecycle.js +26 -15
- package/src/mcp/create-mcp-server.js +4 -1
- 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.js +36 -17
- 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 +148 -56
- 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/README.en.md +0 -336
- 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-CZAF_OlN.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.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-CpLDx3of.css +0 -1
- package/dist/web/assets/index-DAzeFJSc.js +0 -2
- 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,133 @@
|
|
|
1
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export function createCustomConnector({ directory }) {
|
|
6
|
+
const root = realpathSync(requiredString(directory, 'Custom connector directory'));
|
|
7
|
+
|
|
8
|
+
async function load(context) {
|
|
9
|
+
const modulePath = connectorModulePath(root, context.config?.module);
|
|
10
|
+
const modified = statSync(modulePath).mtimeMs;
|
|
11
|
+
const namespace = await import(`${pathToFileURL(modulePath).href}?mtime=${modified}`);
|
|
12
|
+
const implementation = namespace.default ?? namespace.connector;
|
|
13
|
+
if (!implementation || typeof implementation !== 'object') {
|
|
14
|
+
throw new TypeError('Custom connector module must export a connector object');
|
|
15
|
+
}
|
|
16
|
+
if (typeof implementation.sync !== 'function' &&
|
|
17
|
+
typeof implementation.retrieve !== 'function') {
|
|
18
|
+
throw new TypeError('Custom connector must implement sync or retrieve');
|
|
19
|
+
}
|
|
20
|
+
return implementation;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function invoke(method, context, { optional = false } = {}) {
|
|
24
|
+
const implementation = await load(context);
|
|
25
|
+
if (typeof implementation[method] !== 'function') {
|
|
26
|
+
if (optional) return null;
|
|
27
|
+
throw new TypeError(`Custom connector does not implement ${method}`);
|
|
28
|
+
}
|
|
29
|
+
return implementation[method](customContext(context));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
type: 'custom',
|
|
34
|
+
name: '自定义代码',
|
|
35
|
+
capabilities: ['discover', 'sync', 'retrieve'],
|
|
36
|
+
|
|
37
|
+
async check(context) {
|
|
38
|
+
const implementation = await load(context);
|
|
39
|
+
const capabilities = ['discover', 'sync', 'retrieve']
|
|
40
|
+
.filter((name) => typeof implementation[name] === 'function');
|
|
41
|
+
const result = typeof implementation.check === 'function'
|
|
42
|
+
? await implementation.check(customContext(context))
|
|
43
|
+
: { status: 'ready' };
|
|
44
|
+
return { ...result, capabilities };
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async discover(context) {
|
|
48
|
+
return await invoke('discover', context, { optional: true }) ?? {
|
|
49
|
+
items: [],
|
|
50
|
+
nextCursor: null,
|
|
51
|
+
hasMore: false
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async sync(context) {
|
|
56
|
+
return invoke('sync', context);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async retrieve(context) {
|
|
60
|
+
return invoke('retrieve', context);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function connectorModulePath(root, value) {
|
|
66
|
+
const name = requiredString(value, 'Custom connector module');
|
|
67
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]*\.m?js$/u.test(name) || name.includes('//')) {
|
|
68
|
+
throw new TypeError('Custom connector module must stay inside the custom connector directory');
|
|
69
|
+
}
|
|
70
|
+
const candidate = resolve(root, name);
|
|
71
|
+
const pathFromRoot = relative(root, candidate);
|
|
72
|
+
if (!pathFromRoot || pathFromRoot.startsWith('..') || isAbsolute(pathFromRoot)) {
|
|
73
|
+
throw new TypeError('Custom connector module must stay inside the custom connector directory');
|
|
74
|
+
}
|
|
75
|
+
let actual;
|
|
76
|
+
try {
|
|
77
|
+
actual = realpathSync(candidate);
|
|
78
|
+
} catch {
|
|
79
|
+
throw new TypeError(`Custom connector module was not found: ${name}`);
|
|
80
|
+
}
|
|
81
|
+
const actualFromRoot = relative(root, actual);
|
|
82
|
+
if (!actualFromRoot || actualFromRoot.startsWith('..') || isAbsolute(actualFromRoot)) {
|
|
83
|
+
throw new TypeError('Custom connector module must stay inside the custom connector directory');
|
|
84
|
+
}
|
|
85
|
+
return actual;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function customContext(context) {
|
|
89
|
+
const environmentNames = stringArray(context.config?.environmentNames);
|
|
90
|
+
const exposedEnvironment = Object.create(null);
|
|
91
|
+
for (const name of environmentNames) {
|
|
92
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) {
|
|
93
|
+
throw new TypeError(`Invalid custom connector environment name: ${name}`);
|
|
94
|
+
}
|
|
95
|
+
if (typeof context.env?.[name] !== 'string') {
|
|
96
|
+
throw new TypeError(`Custom connector environment variable is unavailable: ${name}`);
|
|
97
|
+
}
|
|
98
|
+
exposedEnvironment[name] = context.env[name];
|
|
99
|
+
}
|
|
100
|
+
const config = { ...(context.config ?? {}) };
|
|
101
|
+
delete config.module;
|
|
102
|
+
delete config.environmentNames;
|
|
103
|
+
return deepFreeze(structuredClone({
|
|
104
|
+
config,
|
|
105
|
+
source: context.source ?? {},
|
|
106
|
+
cursor: context.cursor ?? null,
|
|
107
|
+
query: context.query ?? null,
|
|
108
|
+
limit: context.limit ?? null,
|
|
109
|
+
bindingId: context.bindingId ?? null,
|
|
110
|
+
mode: context.mode ?? null,
|
|
111
|
+
env: exposedEnvironment
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function deepFreeze(value) {
|
|
116
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
117
|
+
Object.freeze(value);
|
|
118
|
+
for (const item of Object.values(value)) deepFreeze(item);
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function stringArray(value) {
|
|
123
|
+
if (value === undefined || value === null) return [];
|
|
124
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
125
|
+
throw new TypeError('Custom connector environmentNames must be an array of strings');
|
|
126
|
+
}
|
|
127
|
+
return [...new Set(value)];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function requiredString(value, label) {
|
|
131
|
+
if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${label} is required`);
|
|
132
|
+
return value.trim();
|
|
133
|
+
}
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
const API_ROOTS = Object.freeze({
|
|
2
|
+
cn: 'https://open.feishu.cn',
|
|
3
|
+
global: 'https://open.larksuite.com'
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
export function createFeishuConnector({
|
|
7
|
+
fetchImpl = globalThis.fetch,
|
|
8
|
+
sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
|
|
9
|
+
} = {}) {
|
|
10
|
+
const tokenCache = new Map();
|
|
11
|
+
let nextRawRequestAt = 0;
|
|
12
|
+
const request = feishuRequester(fetchImpl, tokenCache);
|
|
13
|
+
|
|
14
|
+
async function readDocument(context, node) {
|
|
15
|
+
if (normalizedObjectType(node.obj_type) !== 'docx') return null;
|
|
16
|
+
const interval = rawContentInterval(context.config?.rawContentIntervalMs);
|
|
17
|
+
const wait = nextRawRequestAt - Date.now();
|
|
18
|
+
if (wait > 0) await sleep(wait);
|
|
19
|
+
nextRawRequestAt = Date.now() + interval;
|
|
20
|
+
const result = await request(
|
|
21
|
+
context,
|
|
22
|
+
'GET',
|
|
23
|
+
`/open-apis/docx/v1/documents/${encodeURIComponent(node.obj_token)}/raw_content`,
|
|
24
|
+
undefined,
|
|
25
|
+
{ query: { lang: '0' } }
|
|
26
|
+
);
|
|
27
|
+
const content = result.content;
|
|
28
|
+
if (typeof content !== 'string' || !content.trim()) return null;
|
|
29
|
+
return {
|
|
30
|
+
id: node.node_token ?? node.node_id,
|
|
31
|
+
title: node.title || node.node_token || node.node_id,
|
|
32
|
+
content,
|
|
33
|
+
url: nodeUrl(node, context.source),
|
|
34
|
+
updatedAt: epochDateTime(node.obj_edit_time),
|
|
35
|
+
metadata: {
|
|
36
|
+
feishuSpaceId: node.space_id ?? context.source?.spaceId ?? null,
|
|
37
|
+
feishuNodeToken: node.node_token ?? node.node_id,
|
|
38
|
+
feishuObjectToken: node.obj_token,
|
|
39
|
+
feishuObjectType: normalizedObjectType(node.obj_type)
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function resolveNode(context, token) {
|
|
45
|
+
const result = await request(
|
|
46
|
+
context,
|
|
47
|
+
'GET',
|
|
48
|
+
'/open-apis/wiki/v2/spaces/get_node',
|
|
49
|
+
undefined,
|
|
50
|
+
{ query: { token } }
|
|
51
|
+
);
|
|
52
|
+
return result.node;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
type: 'feishu',
|
|
57
|
+
name: '飞书 / Lark',
|
|
58
|
+
capabilities: ['discover', 'sync', 'retrieve'],
|
|
59
|
+
|
|
60
|
+
async check(context) {
|
|
61
|
+
const source = feishuSource(context.source);
|
|
62
|
+
if (source.nodeTokens.length) {
|
|
63
|
+
await resolveNode(context, source.nodeTokens[0]);
|
|
64
|
+
} else if (source.spaceId) {
|
|
65
|
+
await request(
|
|
66
|
+
context,
|
|
67
|
+
'GET',
|
|
68
|
+
`/open-apis/wiki/v2/spaces/${encodeURIComponent(source.spaceId)}`
|
|
69
|
+
);
|
|
70
|
+
} else {
|
|
71
|
+
await accessToken(context, fetchImpl, tokenCache);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
status: 'ready',
|
|
75
|
+
capabilities: ['discover', 'sync', 'retrieve'],
|
|
76
|
+
region: region(context.config?.region)
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async discover(context) {
|
|
81
|
+
const source = feishuSource(context.source, { allowEmpty: true });
|
|
82
|
+
if (context.query) {
|
|
83
|
+
const result = await searchWiki(context, request, source, context.query, context.cursor);
|
|
84
|
+
return {
|
|
85
|
+
items: (result.items ?? []).map(nodeSummary),
|
|
86
|
+
nextCursor: result.page_token ?? null,
|
|
87
|
+
hasMore: result.has_more === true
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (!source.spaceId) {
|
|
91
|
+
throw new TypeError('Feishu discovery without a query requires spaceId');
|
|
92
|
+
}
|
|
93
|
+
const listed = await listChildren(context, request, source, {
|
|
94
|
+
parent: source.rootNodeToken,
|
|
95
|
+
pageToken: context.cursor
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
items: (listed.items ?? []).map(nodeSummary),
|
|
99
|
+
nextCursor: listed.page_token ?? null,
|
|
100
|
+
hasMore: listed.has_more === true
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async sync(context) {
|
|
105
|
+
const source = feishuSource(context.source);
|
|
106
|
+
if (source.nodeTokens.length) {
|
|
107
|
+
const state = decodeCursor(context.cursor, 'nodes', {
|
|
108
|
+
mode: 'nodes',
|
|
109
|
+
index: 0
|
|
110
|
+
});
|
|
111
|
+
if (state.index >= source.nodeTokens.length) return emptyPage();
|
|
112
|
+
const node = await resolveNode(context, source.nodeTokens[state.index]);
|
|
113
|
+
const document = await readDocument(context, node);
|
|
114
|
+
const nextCursor = state.index + 1 < source.nodeTokens.length
|
|
115
|
+
? encodeCursor({ mode: 'nodes', index: state.index + 1 })
|
|
116
|
+
: null;
|
|
117
|
+
return {
|
|
118
|
+
items: document ? [document] : [],
|
|
119
|
+
deleted: [],
|
|
120
|
+
nextCursor,
|
|
121
|
+
hasMore: nextCursor !== null,
|
|
122
|
+
unsupported: document ? [] : [node.obj_type]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const initial = {
|
|
126
|
+
mode: 'tree',
|
|
127
|
+
rootPending: Boolean(source.rootNodeToken),
|
|
128
|
+
queue: [source.rootNodeToken ?? null],
|
|
129
|
+
pageToken: null,
|
|
130
|
+
discoveredChildren: []
|
|
131
|
+
};
|
|
132
|
+
const state = decodeCursor(context.cursor, 'tree', initial);
|
|
133
|
+
if (state.rootPending) {
|
|
134
|
+
const node = await resolveNode(context, source.rootNodeToken);
|
|
135
|
+
const document = await readDocument(context, node);
|
|
136
|
+
const nextCursor = encodeCursor({ ...state, rootPending: false });
|
|
137
|
+
return {
|
|
138
|
+
items: document ? [document] : [],
|
|
139
|
+
deleted: [],
|
|
140
|
+
nextCursor,
|
|
141
|
+
hasMore: true,
|
|
142
|
+
unsupported: document ? [] : [node.obj_type]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (state.queue.length === 0) return emptyPage();
|
|
146
|
+
const parent = state.queue[0];
|
|
147
|
+
const listed = await listChildren(context, request, source, {
|
|
148
|
+
parent,
|
|
149
|
+
pageToken: state.pageToken,
|
|
150
|
+
limit: context.limit
|
|
151
|
+
});
|
|
152
|
+
const nodes = listed.items ?? [];
|
|
153
|
+
const items = [];
|
|
154
|
+
const unsupported = [];
|
|
155
|
+
for (const node of nodes) {
|
|
156
|
+
const document = await readDocument(context, node);
|
|
157
|
+
if (document) items.push(document);
|
|
158
|
+
else unsupported.push(node.obj_type);
|
|
159
|
+
}
|
|
160
|
+
const children = [
|
|
161
|
+
...state.discoveredChildren,
|
|
162
|
+
...nodes.filter(({ has_child: hasChild }) => hasChild === true)
|
|
163
|
+
.map(({ node_token: token }) => token)
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
];
|
|
166
|
+
const hasMoreForParent = listed.has_more === true;
|
|
167
|
+
const queue = hasMoreForParent
|
|
168
|
+
? state.queue
|
|
169
|
+
: [...state.queue.slice(1), ...children];
|
|
170
|
+
const nextState = {
|
|
171
|
+
mode: 'tree',
|
|
172
|
+
rootPending: false,
|
|
173
|
+
queue,
|
|
174
|
+
pageToken: hasMoreForParent ? listed.page_token : null,
|
|
175
|
+
discoveredChildren: hasMoreForParent ? children : []
|
|
176
|
+
};
|
|
177
|
+
const nextCursor = queue.length ? encodeCursor(nextState) : null;
|
|
178
|
+
return {
|
|
179
|
+
items,
|
|
180
|
+
deleted: [],
|
|
181
|
+
nextCursor,
|
|
182
|
+
hasMore: nextCursor !== null,
|
|
183
|
+
unsupported
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
async retrieve(context) {
|
|
188
|
+
const source = feishuSource(context.source);
|
|
189
|
+
const result = await searchWiki(context, request, source, context.query, null, context.limit);
|
|
190
|
+
const items = [];
|
|
191
|
+
for (const node of (result.items ?? []).slice(0, pageSize(context.limit))) {
|
|
192
|
+
const document = await readDocument(context, node);
|
|
193
|
+
if (document) items.push(document);
|
|
194
|
+
}
|
|
195
|
+
return { items };
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function feishuRequester(fetchImpl, tokenCache) {
|
|
201
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('Feishu fetch is unavailable');
|
|
202
|
+
return async (context, method, path, body = undefined, { query = {} } = {}) => {
|
|
203
|
+
const allowedPost = path === '/open-apis/wiki/v2/nodes/search';
|
|
204
|
+
if (method !== 'GET' && !(method === 'POST' && allowedPost)) {
|
|
205
|
+
throw new TypeError('Feishu connector only permits knowledge read operations');
|
|
206
|
+
}
|
|
207
|
+
const url = new URL(`${apiRoot(context.config?.region)}${path}`);
|
|
208
|
+
for (const [key, value] of Object.entries(query)) {
|
|
209
|
+
if (value !== undefined && value !== null && value !== '') url.searchParams.set(key, value);
|
|
210
|
+
}
|
|
211
|
+
const token = await accessToken(context, fetchImpl, tokenCache);
|
|
212
|
+
const response = await fetchImpl(url, {
|
|
213
|
+
method,
|
|
214
|
+
headers: {
|
|
215
|
+
Authorization: `Bearer ${token}`,
|
|
216
|
+
Accept: 'application/json',
|
|
217
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
218
|
+
},
|
|
219
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
220
|
+
signal: AbortSignal.timeout(timeoutMilliseconds(context.config?.timeoutMs))
|
|
221
|
+
});
|
|
222
|
+
const payload = await response.json().catch(() => ({}));
|
|
223
|
+
if (!response.ok || payload.code !== 0) {
|
|
224
|
+
throw new Error(payload.msg ?? `Feishu API request failed with status ${response.status}`);
|
|
225
|
+
}
|
|
226
|
+
return payload.data ?? {};
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function accessToken(context, fetchImpl, tokenCache) {
|
|
231
|
+
const directName = context.config?.accessTokenEnv;
|
|
232
|
+
if (directName) return environmentValue(context.env, directName, 'Feishu access token');
|
|
233
|
+
const appIdName = context.config?.appIdEnv;
|
|
234
|
+
const appSecretName = context.config?.appSecretEnv;
|
|
235
|
+
const appId = environmentValue(context.env, appIdName, 'Feishu app ID');
|
|
236
|
+
const appSecret = environmentValue(context.env, appSecretName, 'Feishu app secret');
|
|
237
|
+
const cacheKey = `${region(context.config?.region)}:${appIdName}:${appSecretName}`;
|
|
238
|
+
const cached = tokenCache.get(cacheKey);
|
|
239
|
+
if (cached && cached.expiresAt > Date.now() + 60_000) return cached.value;
|
|
240
|
+
const response = await fetchImpl(
|
|
241
|
+
`${apiRoot(context.config?.region)}/open-apis/auth/v3/tenant_access_token/internal`,
|
|
242
|
+
{
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
245
|
+
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
|
246
|
+
signal: AbortSignal.timeout(timeoutMilliseconds(context.config?.timeoutMs))
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
const payload = await response.json().catch(() => ({}));
|
|
250
|
+
if (!response.ok || payload.code !== 0 || !payload.tenant_access_token) {
|
|
251
|
+
throw new Error(payload.msg ?? 'Feishu tenant access token request failed');
|
|
252
|
+
}
|
|
253
|
+
tokenCache.set(cacheKey, {
|
|
254
|
+
value: payload.tenant_access_token,
|
|
255
|
+
expiresAt: Date.now() + Math.max(60, Number(payload.expire ?? 7200)) * 1_000
|
|
256
|
+
});
|
|
257
|
+
return payload.tenant_access_token;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function listChildren(context, request, source, {
|
|
261
|
+
parent = null,
|
|
262
|
+
pageToken = null,
|
|
263
|
+
limit = 50
|
|
264
|
+
}) {
|
|
265
|
+
return request(
|
|
266
|
+
context,
|
|
267
|
+
'GET',
|
|
268
|
+
`/open-apis/wiki/v2/spaces/${encodeURIComponent(source.spaceId)}/nodes`,
|
|
269
|
+
undefined,
|
|
270
|
+
{
|
|
271
|
+
query: {
|
|
272
|
+
page_size: String(pageSize(limit)),
|
|
273
|
+
...(pageToken ? { page_token: pageToken } : {}),
|
|
274
|
+
...(parent ? { parent_node_token: parent } : {})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function searchWiki(context, request, source, query, pageToken, limit = 50) {
|
|
281
|
+
const normalizedQuery = requiredString(query, 'Feishu query').slice(0, 50);
|
|
282
|
+
return request(
|
|
283
|
+
context,
|
|
284
|
+
'POST',
|
|
285
|
+
'/open-apis/wiki/v2/nodes/search',
|
|
286
|
+
{
|
|
287
|
+
query: normalizedQuery,
|
|
288
|
+
...(source.spaceId ? { space_id: source.spaceId } : {}),
|
|
289
|
+
...(source.rootNodeToken ? { node_id: source.rootNodeToken } : {})
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
query: {
|
|
293
|
+
page_size: String(pageSize(limit)),
|
|
294
|
+
...(pageToken ? { page_token: pageToken } : {})
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function feishuSource(source, { allowEmpty = false } = {}) {
|
|
301
|
+
const nodeTokens = idArray(source?.nodeTokens, 'Feishu nodeTokens');
|
|
302
|
+
const spaceId = optionalString(source?.spaceId);
|
|
303
|
+
const rootNodeToken = optionalString(source?.rootNodeToken);
|
|
304
|
+
if (!allowEmpty && !nodeTokens.length && !spaceId) {
|
|
305
|
+
throw new TypeError('Feishu source requires nodeTokens or spaceId');
|
|
306
|
+
}
|
|
307
|
+
if (rootNodeToken && !spaceId) {
|
|
308
|
+
throw new TypeError('Feishu rootNodeToken requires spaceId');
|
|
309
|
+
}
|
|
310
|
+
return { nodeTokens, spaceId, rootNodeToken };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function nodeSummary(node) {
|
|
314
|
+
return {
|
|
315
|
+
id: node.node_token ?? node.node_id,
|
|
316
|
+
title: node.title ?? node.node_token ?? node.node_id,
|
|
317
|
+
type: normalizedObjectType(node.obj_type),
|
|
318
|
+
url: onlineUrl(node.url),
|
|
319
|
+
updatedAt: epochDateTime(node.obj_edit_time),
|
|
320
|
+
metadata: {
|
|
321
|
+
spaceId: node.space_id ?? null,
|
|
322
|
+
objectToken: node.obj_token ?? null,
|
|
323
|
+
hasChild: node.has_child === true
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function nodeUrl(node, source) {
|
|
329
|
+
const direct = onlineUrl(node.url);
|
|
330
|
+
if (direct) return direct;
|
|
331
|
+
const base = onlineUrl(source?.webBaseUrl);
|
|
332
|
+
const token = node.node_token ?? node.node_id;
|
|
333
|
+
return base && token ? new URL(`/wiki/${encodeURIComponent(token)}`, base).toString() : null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function normalizedObjectType(value) {
|
|
337
|
+
const numeric = {
|
|
338
|
+
1: 'doc',
|
|
339
|
+
2: 'sheet',
|
|
340
|
+
3: 'bitable',
|
|
341
|
+
4: 'mindnote',
|
|
342
|
+
5: 'file',
|
|
343
|
+
7: 'wiki',
|
|
344
|
+
8: 'docx',
|
|
345
|
+
9: 'folder',
|
|
346
|
+
10: 'catalog',
|
|
347
|
+
11: 'slides'
|
|
348
|
+
};
|
|
349
|
+
return numeric[value] ?? String(value ?? 'unknown').toLowerCase();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function encodeCursor(value) {
|
|
353
|
+
const encoded = Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
354
|
+
if (encoded.length > 64_000) {
|
|
355
|
+
throw new Error('Feishu traversal cursor is too large; bind narrower root nodes');
|
|
356
|
+
}
|
|
357
|
+
return encoded;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function decodeCursor(value, mode, fallback) {
|
|
361
|
+
if (!value) return structuredClone(fallback);
|
|
362
|
+
try {
|
|
363
|
+
const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
364
|
+
if (parsed.mode !== mode) throw new Error();
|
|
365
|
+
if (mode === 'nodes' && (!Number.isInteger(parsed.index) || parsed.index < 0)) throw new Error();
|
|
366
|
+
if (mode === 'tree' && !Array.isArray(parsed.queue)) throw new Error();
|
|
367
|
+
return parsed;
|
|
368
|
+
} catch {
|
|
369
|
+
throw new TypeError('Invalid Feishu synchronization cursor');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function emptyPage() {
|
|
374
|
+
return {
|
|
375
|
+
items: [],
|
|
376
|
+
deleted: [],
|
|
377
|
+
nextCursor: null,
|
|
378
|
+
hasMore: false,
|
|
379
|
+
unsupported: []
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function region(value) {
|
|
384
|
+
const normalized = value ?? 'cn';
|
|
385
|
+
if (!Object.hasOwn(API_ROOTS, normalized)) {
|
|
386
|
+
throw new TypeError('Feishu region must be cn or global');
|
|
387
|
+
}
|
|
388
|
+
return normalized;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function apiRoot(value) {
|
|
392
|
+
return API_ROOTS[region(value)];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function environmentValue(env, name, label) {
|
|
396
|
+
if (typeof name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) {
|
|
397
|
+
throw new TypeError(`${label} environment variable name is required`);
|
|
398
|
+
}
|
|
399
|
+
const value = env?.[name];
|
|
400
|
+
if (typeof value !== 'string' || !value) {
|
|
401
|
+
throw new TypeError(`${label} environment variable is unavailable: ${name}`);
|
|
402
|
+
}
|
|
403
|
+
return value;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function idArray(value, label) {
|
|
407
|
+
if (value === undefined || value === null) return [];
|
|
408
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || !item.trim())) {
|
|
409
|
+
throw new TypeError(`${label} must be an array of nonempty strings`);
|
|
410
|
+
}
|
|
411
|
+
return [...new Set(value.map((item) => item.trim()))];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function rawContentInterval(value) {
|
|
415
|
+
if (value === undefined || value === null) return 210;
|
|
416
|
+
if (!Number.isInteger(value) || value < 0 || value > 10_000) {
|
|
417
|
+
throw new TypeError('Feishu rawContentIntervalMs must be between 0 and 10000');
|
|
418
|
+
}
|
|
419
|
+
return value;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function timeoutMilliseconds(value) {
|
|
423
|
+
if (value === undefined || value === null) return 30_000;
|
|
424
|
+
if (!Number.isInteger(value) || value < 1_000 || value > 120_000) {
|
|
425
|
+
throw new TypeError('Feishu timeoutMs must be between 1000 and 120000');
|
|
426
|
+
}
|
|
427
|
+
return value;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function pageSize(value) {
|
|
431
|
+
if (value === undefined || value === null) return 50;
|
|
432
|
+
if (!Number.isInteger(value) || value <= 0) throw new TypeError('Feishu page size is invalid');
|
|
433
|
+
return Math.min(value, 50);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function epochDateTime(value) {
|
|
437
|
+
const seconds = Number(value);
|
|
438
|
+
return Number.isFinite(seconds) && seconds > 0
|
|
439
|
+
? new Date(seconds * 1_000).toISOString()
|
|
440
|
+
: null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function onlineUrl(value) {
|
|
444
|
+
if (typeof value !== 'string') return null;
|
|
445
|
+
try {
|
|
446
|
+
const url = new URL(value);
|
|
447
|
+
return url.protocol === 'https:' && !url.username && !url.password ? url.toString() : null;
|
|
448
|
+
} catch {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function optionalString(value) {
|
|
454
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function requiredString(value, label) {
|
|
458
|
+
const normalized = optionalString(value);
|
|
459
|
+
if (!normalized) throw new TypeError(`${label} is required`);
|
|
460
|
+
return normalized;
|
|
461
|
+
}
|