fuli-context 0.5.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 (206) hide show
  1. package/README.en.md +2 -333
  2. package/README.md +370 -202
  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 -5
  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 +51 -28
  59. package/src/cli/local-runtime-options.js +14 -8
  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 +1 -1
  92. package/src/local-runtime/lifecycle.js +26 -15
  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.js +36 -17
  100. package/src/setup/agent-installation-status.js +2 -1
  101. package/src/setup/agents.js +2 -1
  102. package/src/setup/claude-code-config.js +2 -1
  103. package/src/setup/codex-bootstrap.js +12 -7
  104. package/src/setup/container-runtime.js +25 -21
  105. package/src/setup/graph-runtime.js +148 -56
  106. package/src/setup/node-runtime.js +3 -3
  107. package/src/setup/options.js +2 -4
  108. package/src/setup/paths.js +18 -3
  109. package/src/setup/review-skill-adapters.js +49 -0
  110. package/src/setup/setup.js +26 -2
  111. package/src/setup/uninstall.js +4 -2
  112. package/src/system/resource-monitor.js +317 -0
  113. package/src/system/runtime-settings.js +131 -0
  114. package/src/system/system-service.js +119 -0
  115. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  116. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +0 -1
  120. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  121. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  122. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  123. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  124. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  125. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  126. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  127. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  128. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  129. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  130. package/dist/web/assets/index-CpLDx3of.css +0 -1
  131. package/dist/web/assets/index-DAzeFJSc.js +0 -2
  132. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  133. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  134. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  135. package/src/agent/candidate-read-service.js +0 -53
  136. package/src/agent/context-pack-service.js +0 -83
  137. package/src/agent/public-read-policy.js +0 -39
  138. package/src/agent/public-read-service.js +0 -69
  139. package/src/agent/read-projection.js +0 -107
  140. package/src/agent/search-service.js +0 -61
  141. package/src/agent/space-boundary.js +0 -33
  142. package/src/app/bootstrap.js +0 -8
  143. package/src/app/create-application.js +0 -143
  144. package/src/app/state-service.js +0 -38
  145. package/src/candidates.js +0 -78
  146. package/src/classifier.js +0 -63
  147. package/src/cli/command-arguments.js +0 -14
  148. package/src/context-pack.js +0 -206
  149. package/src/extractor.js +0 -77
  150. package/src/fact-history.js +0 -39
  151. package/src/facts/fact-service.js +0 -89
  152. package/src/http/bounded-json-array.js +0 -19
  153. package/src/http/lens-route.js +0 -50
  154. package/src/ingestion.js +0 -161
  155. package/src/lens/lens-correction-service.js +0 -81
  156. package/src/lens/lens-fact-writer.js +0 -85
  157. package/src/lens/lens-input.js +0 -62
  158. package/src/lens/lens-query-projection.js +0 -87
  159. package/src/lens/lens-query-ranking.js +0 -62
  160. package/src/lens/lens-query.js +0 -143
  161. package/src/lens/lens-resource-projection.js +0 -93
  162. package/src/lens/lens-resource-selection.js +0 -45
  163. package/src/lens/lens-resource-service.js +0 -103
  164. package/src/lens/lens-search-query.js +0 -122
  165. package/src/lens/lens-service.js +0 -183
  166. package/src/models.js +0 -44
  167. package/src/observer.js +0 -83
  168. package/src/project-rules.js +0 -33
  169. package/src/publication/outbox-service.js +0 -46
  170. package/src/publication/publication-envelope-schema.js +0 -75
  171. package/src/publication/publication-envelope.js +0 -37
  172. package/src/publication/publication-policy.js +0 -82
  173. package/src/publication/publication-service.js +0 -48
  174. package/src/router.js +0 -58
  175. package/src/runtime-options.js +0 -138
  176. package/src/security/episode-visibility.js +0 -14
  177. package/src/security/fact-visibility.js +0 -15
  178. package/src/security/source-metadata-policy.js +0 -8
  179. package/src/storage/episode-evidence-preview.js +0 -100
  180. package/src/storage/fact-search-order.js +0 -18
  181. package/src/storage/file/episode-evidence.js +0 -37
  182. package/src/storage/file/episode-index.js +0 -59
  183. package/src/storage/file/fact-search.js +0 -60
  184. package/src/storage/file-store.js +0 -437
  185. package/src/storage/migrate.js +0 -128
  186. package/src/storage/migrations/001-initial.sql +0 -83
  187. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  188. package/src/storage/record-validation.js +0 -46
  189. package/src/storage/snapshot-defaults.js +0 -54
  190. package/src/storage/sqlite/candidates-repository.js +0 -84
  191. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  192. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  193. package/src/storage/sqlite/episodes-repository.js +0 -52
  194. package/src/storage/sqlite/fact-search-repository.js +0 -162
  195. package/src/storage/sqlite/facts-repository.js +0 -146
  196. package/src/storage/sqlite/imports-repository.js +0 -39
  197. package/src/storage/sqlite/mapper.js +0 -84
  198. package/src/storage/sqlite/open-database.js +0 -15
  199. package/src/storage/sqlite/outbox-repository.js +0 -103
  200. package/src/storage/sqlite/snapshot-repository.js +0 -69
  201. package/src/storage/sqlite/spaces-repository.js +0 -52
  202. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  203. package/src/storage/sqlite-store.js +0 -159
  204. package/src/storage/stable-json.js +0 -24
  205. package/src/storage/store-port.js +0 -20
  206. package/src/store.js +0 -2
@@ -7,6 +7,10 @@ import {
7
7
  } from './setup-agent-selection.js';
8
8
  import { parseSetupOptions } from '../setup/options.js';
9
9
  import { applyLocalSetup, planLocalSetup } from '../setup/setup.js';
10
+ import {
11
+ DEFAULT_RUNTIME_SETTINGS,
12
+ runtimeSettingsWithOverrides
13
+ } from '../system/runtime-settings.js';
10
14
 
11
15
  export async function runSetupCommand(args, dependencies = {}) {
12
16
  const options = parseSetupOptions(args);
@@ -23,7 +27,7 @@ export async function runSetupCommand(args, dependencies = {}) {
23
27
  write(formatSetupPreview(plan, options));
24
28
 
25
29
  if (!options.yes && !await confirm()) {
26
- write('已取消,没有修改任何内容。');
30
+ write('Cancelled. No changes were made.');
27
31
  return { status: 'cancelled' };
28
32
  }
29
33
 
@@ -39,33 +43,50 @@ export function formatSetupPreview(plan, options) {
39
43
  const available = plan.agents.filter(({ available }) => available);
40
44
  const selected = available.filter(({ selected }) => selected !== false);
41
45
  const agentSummary = options.skipAgents || (available.length > 0 && selected.length === 0)
42
- ? '跳过接入'
43
- : selected.map(({ label }) => label).join('、') || '未检测到';
46
+ ? 'skipped'
47
+ : selected.map(({ label }) => label).join(', ') || 'none detected';
48
+ const runtimeSettings = plan.runtimeSettings ?? runtimeSettingsWithOverrides(
49
+ DEFAULT_RUNTIME_SETTINGS,
50
+ { consolePort: options.port }
51
+ );
44
52
  return [
45
- '准备设置复利',
53
+ 'Ready to set up Fuli',
46
54
  options.personalOnly
47
- ? '存储:仅个人:本机 Graphiti / Neo4j'
48
- : '存储:本机 Graphiti / Neo4j + 团队共享项目 Provider',
49
- '容器运行时:自动检测;已安装但未运行时会自动启动',
50
- `公共服务:${options.personalOnly ? '暂不连接' : '连接本机开发 Provider'}`,
51
- `个人空间:${options.personalSpaceName}`,
52
- `本地控制台:${options.noStart ? '暂不启动' : `http://127.0.0.1:${options.port}`}`,
53
- `Agent:${agentSummary}`
55
+ ? 'Storage: personal only, using local Graphiti / Neo4j'
56
+ : 'Storage: local Graphiti / Neo4j plus a development shared-project Provider',
57
+ 'Container runtime: detected automatically; an installed desktop runtime starts if needed',
58
+ `Shared services: ${options.personalOnly ? 'not connected' : 'local development Provider'}`,
59
+ `Personal space: ${options.personalSpaceName}`,
60
+ `Management UI: ${options.noStart
61
+ ? 'will not start'
62
+ : `http://127.0.0.1:${runtimeSettings.ports.console}`}`,
63
+ `Agents: ${agentSummary}`
54
64
  ].join('\n');
55
65
  }
56
66
 
57
67
  export function formatSetupResult(result, plan) {
58
- const lines = [result.status === 'ready' ? '复利已准备好。' : '复利已启动,但有 Agent 未连接。'];
59
- if (result.runtime.url) lines.push(`打开:${result.runtime.url}`);
60
- lines.push('知识存储:Graphiti / Neo4j');
68
+ const lines = [result.status === 'ready'
69
+ ? 'Fuli is ready.'
70
+ : 'Fuli started, but one or more Agents are not connected.'];
71
+ if (result.runtime.url) lines.push(`Open: ${result.runtime.url}`);
72
+ if (result.runtime.lan === true) {
73
+ lines.push(
74
+ 'LAN URLs:',
75
+ ...result.runtime.lanUrls.map((url) => ` ${url}`),
76
+ `Username: ${result.runtime.lanAccess.username}`,
77
+ `Temporary access code: ${result.runtime.lanAccess.accessCode}`,
78
+ 'Use only on trusted Wi-Fi. Restarting LAN mode rotates the access code.'
79
+ );
80
+ }
81
+ lines.push('Knowledge storage: Graphiti / Neo4j');
61
82
  for (const agent of result.agents) {
62
83
  if (agent.status !== 'connected') {
63
- lines.push(`${agent.label}:连接失败,请稍后重试 setup`);
84
+ lines.push(`${agent.label}: connection failed; retry setup later`);
64
85
  continue;
65
86
  }
66
87
  lines.push(agent.newTaskRequired
67
- ? `${agent.label}:已连接;请新建或重新打开一个任务以加载新配置`
68
- : `${agent.label}:已连接`);
88
+ ? `${agent.label}: connected; create or reopen a task to load the new configuration`
89
+ : `${agent.label}: connected`);
69
90
  }
70
91
  return lines.join('\n');
71
92
  }
@@ -73,7 +94,9 @@ export function formatSetupResult(result, plan) {
73
94
  async function confirmInTerminal() {
74
95
  const input = createInterface({ input: process.stdin, output: process.stdout });
75
96
  try {
76
- const answer = (await input.question('只会修改以上内容,继续?[Y/n] ')).trim().toLowerCase();
97
+ const answer = (await input.question('Only the items above will change. Continue? [Y/n] '))
98
+ .trim()
99
+ .toLowerCase();
77
100
  return answer === '' || answer === 'y' || answer === 'yes';
78
101
  } finally {
79
102
  input.close();
@@ -0,0 +1,10 @@
1
+ export function quoteShellArgument(value, platform = process.platform) {
2
+ const argument = String(value);
3
+ if (platform === 'win32') {
4
+ if (argument.includes('"')) {
5
+ throw new TypeError('Windows paths cannot contain a double quote');
6
+ }
7
+ return `"${argument}"`;
8
+ }
9
+ return `'${argument.replaceAll("'", "'\\''")}'`;
10
+ }
@@ -14,7 +14,7 @@ export async function runUninstallCommand(args, dependencies = {}) {
14
14
  write(formatUninstallPreview(plan));
15
15
 
16
16
  if (!options.yes && !await confirm()) {
17
- write('已取消,没有修改任何内容。');
17
+ write('Cancelled. No changes were made.');
18
18
  return { status: 'cancelled' };
19
19
  }
20
20
 
@@ -24,30 +24,32 @@ export async function runUninstallCommand(args, dependencies = {}) {
24
24
  }
25
25
 
26
26
  export function formatUninstallPreview(plan) {
27
- const labels = plan.agents.map(({ label }) => label).join('、') || '无';
27
+ const labels = plan.agents.map(({ label }) => label).join(', ') || 'none';
28
28
  return [
29
- '准备清理复利的本机接入',
30
- '本地服务:停止',
31
- `Agent 接入:${labels}`,
32
- 'Skills:仅删除与当前安装包完全一致的副本;有本地修改的会保留',
33
- `数据:保留 ${plan.paths.dataDir}`,
34
- 'Neo4j 数据卷:保留'
29
+ 'Ready to remove local Fuli integrations',
30
+ 'Local services: stop',
31
+ `Agent integrations: ${labels}`,
32
+ 'Skills: remove only copies identical to this package; preserve locally modified copies',
33
+ `Data: preserve ${plan.paths.dataDir}`,
34
+ 'Neo4j volumes: preserve'
35
35
  ].join('\n');
36
36
  }
37
37
 
38
38
  export function formatUninstallResult(result) {
39
39
  const lines = [
40
- result.status === 'ready' ? '复利本机接入已清理。' : '复利本机接入未完全清理。',
41
- `数据已保留:${result.data.path}`,
42
- `最后移除全局命令:npm uninstall --global ${FULI_PACKAGE_NAME}`
40
+ result.status === 'ready'
41
+ ? 'Local Fuli integrations were removed.'
42
+ : 'Some local Fuli integrations could not be removed.',
43
+ `Data preserved: ${result.data.path}`,
44
+ `Finally, remove the global command: npm uninstall --global ${FULI_PACKAGE_NAME}`
43
45
  ];
44
46
  if (result.runtime.status !== 'stopped') {
45
- lines.push(`本地服务:${result.runtime.message ?? '未能安全确认已停止'}`);
47
+ lines.push(`Local services: ${result.runtime.message ?? 'could not safely confirm they stopped'}`);
46
48
  }
47
49
  for (const agent of result.agents) {
48
50
  const modified = agent.skills.some(({ status }) => status === 'preserved_modified');
49
- if (modified) lines.push(`${agent.label}:保留了有本地修改的 Skill`);
50
- if (agent.errors?.length) lines.push(`${agent.label}:${agent.errors.join(';')}`);
51
+ if (modified) lines.push(`${agent.label}: preserved a locally modified Skill`);
52
+ if (agent.errors?.length) lines.push(`${agent.label}: ${agent.errors.join('; ')}`);
51
53
  }
52
54
  return lines.join('\n');
53
55
  }
@@ -55,7 +57,7 @@ export function formatUninstallResult(result) {
55
57
  async function confirmInTerminal() {
56
58
  const input = createInterface({ input: process.stdin, output: process.stdout });
57
59
  try {
58
- const answer = (await input.question('继续清理本机接入?[y/N] ')).trim().toLowerCase();
60
+ const answer = (await input.question('Remove local integrations? [y/N] ')).trim().toLowerCase();
59
61
  return answer === 'y' || answer === 'yes';
60
62
  } finally {
61
63
  input.close();
@@ -7,7 +7,7 @@ import {
7
7
  FULI_PACKAGE_NAME,
8
8
  FULI_VERSION
9
9
  } from '../package-metadata.js';
10
- import { quoteShellArgument } from '../runtime-options.js';
10
+ import { quoteShellArgument } from './shell-argument.js';
11
11
  import { parseUpdateOptions } from '../setup/options.js';
12
12
  import { runLocalRuntimeCommand } from './local-runtime-command.js';
13
13
 
@@ -31,7 +31,7 @@ export async function runUpdateCommand(args = [], dependencies = {}) {
31
31
 
32
32
  write(formatUpdatePreview(options));
33
33
  if (!options.yes && !await confirm()) {
34
- write('已取消,没有修改任何内容。');
34
+ write('Cancelled. No changes were made.');
35
35
  return { status: 'cancelled' };
36
36
  }
37
37
 
@@ -51,13 +51,13 @@ export async function runUpdateCommand(args = [], dependencies = {}) {
51
51
  const stopped = await stopRuntime('stop', stopArgs, { env, write });
52
52
  if (stopped.status === 'partial') {
53
53
  throw new Error([
54
- '无法安全确认旧版界面进程已停止,更新尚未开始。',
55
- '请先运行 fuli status 检查本地状态,再重试 fuli update。'
54
+ 'Could not safely confirm that the previous UI process stopped. The update did not start.',
55
+ 'Run fuli status to inspect the local state, then retry fuli update.'
56
56
  ].join('\n'));
57
57
  }
58
58
 
59
59
  const selectedPackage = `${FULI_PACKAGE_NAME}@${latestVersion}`;
60
- write(`正在安装 ${selectedPackage}(npm latest)…`);
60
+ write(`Installing ${selectedPackage} (npm latest)...`);
61
61
  const installed = runInherited(npmCommand, [
62
62
  'install',
63
63
  '--global',
@@ -67,15 +67,16 @@ export async function runUpdateCommand(args = [], dependencies = {}) {
67
67
  ], { env });
68
68
  if (!processSucceeded(installed)) {
69
69
  throw new Error([
70
- `全局包安装失败(${describeProcessFailure(installed)})。知识数据未删除。`,
71
- `请先运行 npm install --global ${LATEST_PACKAGE},再运行 fuli setup --yes。`
70
+ `Global package installation failed (${describeProcessFailure(installed)}). ` +
71
+ 'Knowledge data was not deleted.',
72
+ `Run npm install --global ${LATEST_PACKAGE}, then run fuli setup --yes.`
72
73
  ].join('\n'));
73
74
  }
74
75
 
75
76
  const globalRootResult = runCaptured(npmCommand, ['root', '--global'], { env });
76
77
  if (!processSucceeded(globalRootResult)) {
77
78
  throw postInstallError(
78
- `无法定位 npm 全局安装目录(${describeProcessFailure(globalRootResult)})`,
79
+ `Could not locate the global npm directory (${describeProcessFailure(globalRootResult)})`,
79
80
  args,
80
81
  platform
81
82
  );
@@ -83,33 +84,35 @@ export async function runUpdateCommand(args = [], dependencies = {}) {
83
84
  const globalRoot = String(globalRootResult.stdout ?? '').trim();
84
85
  const cliPath = resolveGlobalCliPath(globalRoot, platform);
85
86
  if (!cliPath || !fileExists(cliPath)) {
86
- throw postInstallError('新版 CLI 文件不存在,请重新安装全局包', args, platform);
87
+ throw postInstallError('The new CLI file is missing; reinstall the global package', args, platform);
87
88
  }
88
89
 
89
90
  const versionResult = runCaptured(nodePath, [cliPath, '--version'], { env });
90
91
  if (!processSucceeded(versionResult)) {
91
92
  throw postInstallError(
92
- `新版 CLI 无法启动(${describeProcessFailure(versionResult)})`,
93
+ `The new CLI could not start (${describeProcessFailure(versionResult)})`,
93
94
  args,
94
95
  platform
95
96
  );
96
97
  }
97
98
  const version = String(versionResult.stdout ?? '').trim();
98
- if (!version) throw postInstallError('新版 CLI 未返回版本号', args, platform);
99
+ if (!version) throw postInstallError('The new CLI returned no version', args, platform);
99
100
  if (compareSemanticVersions(version, latestVersion) !== 0) {
100
101
  throw postInstallError(
101
- `新版 CLI 版本 ${version} 与更新前确认的 npm latest ${latestVersion} 不一致`,
102
+ `The new CLI version ${version} does not match the npm latest version ${latestVersion} ` +
103
+ 'confirmed before the update',
102
104
  args,
103
105
  platform
104
106
  );
105
107
  }
106
108
 
107
- write('正在使用新版 CLI 刷新本机服务、Agent 接入和 Skills…');
109
+ write('Refreshing local services, Agent integrations, and Skills with the new CLI...');
108
110
  const setupArgs = ['setup', '--yes', ...args.filter((arg) => arg !== '--yes')];
109
111
  const setupResult = runInherited(nodePath, [cliPath, ...setupArgs], { env });
110
112
  if (!processSucceeded(setupResult)) {
111
113
  throw postInstallError(
112
- `新版已安装,但 setup 未完成(${describeProcessFailure(setupResult)})`,
114
+ `The new version was installed, but setup did not complete ` +
115
+ `(${describeProcessFailure(setupResult)})`,
113
116
  args,
114
117
  platform
115
118
  );
@@ -126,27 +129,32 @@ export async function runUpdateCommand(args = [], dependencies = {}) {
126
129
 
127
130
  export function formatUpdatePreview(options) {
128
131
  return [
129
- '准备更新复利',
130
- `npm 包:${LATEST_PACKAGE}`,
132
+ 'Ready to update Fuli',
133
+ `npm package: ${LATEST_PACKAGE}`,
131
134
  options.noStart
132
- ? '本地界面:安全停止;更新后保持关闭'
133
- : '本地服务:安全停止;更新后由新版重新启动',
134
- options.dataDir ? `数据:保留 ${options.dataDir}` : '数据:保留当前默认数据目录',
135
- `Agent 接入与 Skills:${options.skipAgents ? '按参数跳过刷新' : '由新版 setup 刷新'}`
135
+ ? 'Local UI: stop safely and keep stopped after the update'
136
+ : 'Local services: stop safely and restart with the new version',
137
+ options.dataDir
138
+ ? `Data: preserve ${options.dataDir}`
139
+ : 'Data: preserve the current default data directory',
140
+ `Agent integrations and Skills: ${options.skipAgents
141
+ ? 'skip refresh as requested'
142
+ : 'refresh through setup in the new version'}`
136
143
  ].join('\n');
137
144
  }
138
145
 
139
146
  export function formatUpdateResult(result) {
140
147
  if (result.status === 'ahead') {
141
148
  return [
142
- `当前 CLI 版本 ${result.version} 高于 npm latest ${result.latestVersion};`,
143
- '为避免降级,未停止服务、未安装包,也未修改 Agent 接入。'
149
+ `The current CLI version ${result.version} is newer than npm latest ${result.latestVersion}. `,
150
+ 'To avoid a downgrade, services were not stopped, no package was installed, and Agent ' +
151
+ 'integrations were not changed.'
144
152
  ].join('');
145
153
  }
146
154
  if (result.status === 'current') {
147
- return `复利已是最新版本 ${result.version};本机接入已刷新。`;
155
+ return `Fuli is already at the latest version (${result.version}); local integrations refreshed.`;
148
156
  }
149
- return `复利已从 ${result.previousVersion} 更新到 ${result.version}。`;
157
+ return `Fuli updated from ${result.previousVersion} to ${result.version}.`;
150
158
  }
151
159
 
152
160
  export function compareSemanticVersions(left, right) {
@@ -185,8 +193,8 @@ export function resolveGlobalCliPath(globalRoot, platform = process.platform) {
185
193
 
186
194
  function postInstallError(message, args, platform) {
187
195
  return new Error([
188
- `${message}。知识数据未删除。`,
189
- `请重新运行:${formatSetupRecoveryCommand(args, platform)}`
196
+ `${message}. Knowledge data was not deleted.`,
197
+ `Run again: ${formatSetupRecoveryCommand(args, platform)}`
190
198
  ].join('\n'));
191
199
  }
192
200
 
@@ -198,8 +206,8 @@ function queryLatestVersion(npmCommand, runCaptured, env) {
198
206
  );
199
207
  if (!processSucceeded(result)) {
200
208
  throw new Error([
201
- `无法检查 ${LATEST_PACKAGE}(${describeProcessFailure(result)})。`,
202
- '本地服务尚未停止,也没有安装或修改任何包。'
209
+ `Could not check ${LATEST_PACKAGE} (${describeProcessFailure(result)}).`,
210
+ 'Local services were not stopped, and no package was installed or changed.'
203
211
  ].join('\n'));
204
212
  }
205
213
  const raw = String(result.stdout ?? '').trim();
@@ -244,7 +252,7 @@ function formatCommandArgument(value, platform) {
244
252
  try {
245
253
  return quoteShellArgument(value, platform);
246
254
  } catch {
247
- return '<复用原参数>';
255
+ return '<reuse-original-argument>';
248
256
  }
249
257
  }
250
258
 
@@ -279,7 +287,7 @@ function spawnCaptured(command, args, { env }) {
279
287
  async function confirmInTerminal() {
280
288
  const input = createInterface({ input: process.stdin, output: process.stdout });
281
289
  try {
282
- const answer = (await input.question('将修改全局 npm 安装,继续?[Y/n] '))
290
+ const answer = (await input.question('This will modify the global npm installation. Continue? [Y/n] '))
283
291
  .trim()
284
292
  .toLowerCase();
285
293
  return answer === '' || answer === 'y' || answer === 'yes';
@@ -0,0 +1,61 @@
1
+ import { createCustomConnector } from './connectors/custom.js';
2
+ import { createFeishuConnector } from './connectors/feishu.js';
3
+ import { createMcpConnector } from './connectors/mcp.js';
4
+ import { createNotionConnector } from './connectors/notion.js';
5
+ import { createRetrievalApiConnector } from './connectors/retrieval-api.js';
6
+
7
+ export function createConnectorCatalog({
8
+ customConnectorDirectory,
9
+ fetchImpl = globalThis.fetch,
10
+ mcpOpenSession,
11
+ sleep
12
+ }) {
13
+ const connectors = [
14
+ withMetadata(createMcpConnector({ openSession: mcpOpenSession }), {
15
+ status: 'supported',
16
+ trust: 'remote_protocol',
17
+ description: 'MCP resources and explicitly configured read tools.'
18
+ }),
19
+ withMetadata(createNotionConnector({ fetchImpl }), {
20
+ status: 'supported',
21
+ trust: 'official_read_api',
22
+ description: 'Notion pages and data sources through the current markdown API.'
23
+ }),
24
+ withMetadata(createFeishuConnector({ fetchImpl, sleep }), {
25
+ status: 'supported',
26
+ trust: 'official_read_api',
27
+ description: 'Feishu/Lark wiki discovery and read-only docx content.',
28
+ limitations: ['docx_content_only', 'wiki_search_requires_user_access_token']
29
+ }),
30
+ withMetadata(createRetrievalApiConnector({ fetchImpl }), {
31
+ status: 'supported',
32
+ trust: 'compatible_read_api',
33
+ description: 'Dify-compatible retrieval API for external RAG knowledge bases.',
34
+ limitations: ['live_retrieval_only', 'requires_stable_document_metadata_for_best_provenance']
35
+ }),
36
+ withMetadata(createCustomConnector({ directory: customConnectorDirectory }), {
37
+ status: 'advanced',
38
+ trust: 'trusted_local_code',
39
+ description: 'A local ESM connector module explicitly installed by the user.'
40
+ })
41
+ ];
42
+ const byType = new Map(connectors.map((connector) => [connector.type, connector]));
43
+ return {
44
+ get(type) {
45
+ return byType.get(type) ?? null;
46
+ },
47
+ list() {
48
+ return connectors.map(({ metadata }) => structuredClone(metadata));
49
+ }
50
+ };
51
+ }
52
+
53
+ function withMetadata(connector, metadata) {
54
+ const descriptor = Object.freeze({
55
+ type: connector.type,
56
+ name: connector.name,
57
+ capabilities: [...connector.capabilities],
58
+ ...metadata
59
+ });
60
+ return Object.assign(connector, { metadata: descriptor });
61
+ }
@@ -0,0 +1,58 @@
1
+ import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
2
+
3
+ const MODES = new Set(['ask_human', 'agent_decide']);
4
+ const EMPTY_POLICIES = Object.freeze({ version: 1, projects: {} });
5
+
6
+ export class KnowledgeConflictPolicyStore {
7
+ constructor(filePath, { now = () => new Date() } = {}) {
8
+ if (typeof filePath !== 'string' || !filePath.trim()) {
9
+ throw new TypeError('Knowledge conflict policy path is required');
10
+ }
11
+ this.filePath = filePath;
12
+ this.now = now;
13
+ }
14
+
15
+ get(personalProjectId) {
16
+ const id = requiredString(personalProjectId, 'personalProjectId');
17
+ const projects = this.#read().projects;
18
+ const stored = projects[policyKey(id)] ??
19
+ (Object.hasOwn(projects, id) ? projects[id] : null);
20
+ return stored
21
+ ? structuredClone(stored)
22
+ : { personalProjectId: id, mode: 'ask_human', updatedAt: null };
23
+ }
24
+
25
+ set(personalProjectId, mode) {
26
+ const id = requiredString(personalProjectId, 'personalProjectId');
27
+ if (!MODES.has(mode)) {
28
+ throw new TypeError('Conflict policy mode must be ask_human or agent_decide');
29
+ }
30
+ const document = this.#read();
31
+ const policy = {
32
+ personalProjectId: id,
33
+ mode,
34
+ updatedAt: this.now().toISOString()
35
+ };
36
+ document.projects[policyKey(id)] = policy;
37
+ writeJsonFileAtomic(this.filePath, document);
38
+ return structuredClone(policy);
39
+ }
40
+
41
+ #read() {
42
+ const document = readJsonFile(this.filePath, EMPTY_POLICIES);
43
+ if (document?.version !== 1 || !document.projects ||
44
+ typeof document.projects !== 'object' || Array.isArray(document.projects)) {
45
+ throw new TypeError('Unsupported knowledge conflict policy format');
46
+ }
47
+ return structuredClone(document);
48
+ }
49
+ }
50
+
51
+ function policyKey(personalProjectId) {
52
+ return `project:${personalProjectId}`;
53
+ }
54
+
55
+ function requiredString(value, label) {
56
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${label} is required`);
57
+ return value.trim();
58
+ }
@@ -0,0 +1,166 @@
1
+ import { safeExternalSourceDiagnostic } from './safe-diagnostic.js';
2
+
3
+ export class ConnectedKnowledgeSearch {
4
+ constructor({ app, externalKnowledge, policies }) {
5
+ if (!app || !externalKnowledge || !policies) {
6
+ throw new TypeError('Connected knowledge search requires app, externalKnowledge, and policies');
7
+ }
8
+ this.app = app;
9
+ this.externalKnowledge = externalKnowledge;
10
+ this.policies = policies;
11
+ }
12
+
13
+ async query(input) {
14
+ const personalSpaceId = requiredString(input?.personalSpaceId, 'personalSpaceId');
15
+ const personalProjectId = requiredString(input?.personalProjectId, 'personalProjectId');
16
+ const query = requiredString(input?.query, 'query');
17
+ const limit = positiveInteger(input?.limit ?? 12, 'limit', 100);
18
+ const projectIds = stringArray(input?.projectIds, 'projectIds', 32);
19
+ const contextPersonalProjectIds = stringArray(
20
+ input?.contextPersonalProjectIds,
21
+ 'contextPersonalProjectIds',
22
+ 15
23
+ );
24
+ const [graph, bindings] = await Promise.all([
25
+ this.app.searchKnowledge({
26
+ personalSpaceId,
27
+ personalProjectId,
28
+ projectIds,
29
+ contextPersonalProjectIds,
30
+ personalProjectScope: input.personalProjectScope ?? 'bounded',
31
+ query,
32
+ limit,
33
+ includeHistorical: input.includeHistorical === true,
34
+ includePending: input.includePending !== false,
35
+ agentInvocation: true,
36
+ agentToolName: 'search_connected_knowledge'
37
+ }),
38
+ this.externalKnowledge.listBindings()
39
+ ]);
40
+ const selected = bindings.flatMap((binding) =>
41
+ binding.status === 'ready'
42
+ ? bindingTargets(binding)
43
+ .filter((target) =>
44
+ target.status !== 'error' &&
45
+ target.mode !== 'mirror' &&
46
+ target.personalSpaceId === personalSpaceId &&
47
+ target.personalProjectId === personalProjectId
48
+ )
49
+ .map((target) => ({ binding, target }))
50
+ : []
51
+ );
52
+ const retrieved = await Promise.allSettled(selected.map(({ binding, target }) =>
53
+ this.externalKnowledge.retrieveBinding(binding.id, {
54
+ query,
55
+ limit,
56
+ personalSpaceId: target.personalSpaceId,
57
+ personalProjectId: target.personalProjectId
58
+ })
59
+ ));
60
+ const external = [];
61
+ const sourceErrors = [];
62
+ for (const [index, outcome] of retrieved.entries()) {
63
+ const { binding, target } = selected[index];
64
+ const identity = {
65
+ id: binding.id,
66
+ name: binding.name,
67
+ connectorType: binding.connectorType,
68
+ targetId: target.id,
69
+ mode: target.mode
70
+ };
71
+ if (outcome.status === 'fulfilled') {
72
+ external.push({
73
+ binding: identity,
74
+ items: outcome.value.items ?? [],
75
+ skippedCredentials: outcome.value.skippedCredentials ?? 0
76
+ });
77
+ } else {
78
+ sourceErrors.push({
79
+ binding: identity,
80
+ error: safeExternalSourceDiagnostic(outcome.reason)
81
+ });
82
+ }
83
+ }
84
+ const conflictPolicy = this.policies.get(personalProjectId);
85
+ return {
86
+ query,
87
+ graph,
88
+ external,
89
+ sourceErrors,
90
+ sourceScopes: {
91
+ personalProjectId,
92
+ publicProjectIds: projectIds,
93
+ publicSpaceStatus: projectIds.length ? 'beta' : 'not_requested'
94
+ },
95
+ conflictPolicy,
96
+ conflictAssessmentRequired: requiresConflictAssessment({
97
+ graph,
98
+ external,
99
+ publicProjectIds: projectIds
100
+ }),
101
+ conflictGuidance: guidanceFor(conflictPolicy.mode)
102
+ };
103
+ }
104
+
105
+ getConflictPolicy({ personalProjectId }) {
106
+ return this.policies.get(personalProjectId);
107
+ }
108
+
109
+ async updateConflictPolicy({ personalSpaceId, personalProjectId, mode }) {
110
+ const projects = await this.app.listPersonalProjects({ personalSpaceId });
111
+ if (!projects.some(({ project_id: id, id: fallback }) =>
112
+ (id ?? fallback) === personalProjectId
113
+ )) {
114
+ throw new TypeError('Conflict policy target must be an existing personal project');
115
+ }
116
+ return this.policies.set(personalProjectId, mode);
117
+ }
118
+ }
119
+
120
+ function bindingTargets(binding) {
121
+ if (Array.isArray(binding.targets)) return binding.targets;
122
+ return binding.target ? [{
123
+ id: binding.id,
124
+ ...binding.target,
125
+ mode: binding.mode,
126
+ status: binding.status
127
+ }] : [];
128
+ }
129
+
130
+ function guidanceFor(mode) {
131
+ return mode === 'agent_decide'
132
+ ? 'When sources materially conflict, the Agent may decide for this response only. Explain the basis and source provenance; never rewrite, confirm, or invalidate underlying knowledge automatically.'
133
+ : 'When sources materially conflict, surface the conflict in the Agent conversation and ask the user before selecting durable truth. Do not rewrite any source.';
134
+ }
135
+
136
+ function hasGraphEvidence(graph) {
137
+ return (graph?.facts?.length ?? 0) > 0 || (graph?.entities?.length ?? 0) > 0;
138
+ }
139
+
140
+ function requiresConflictAssessment({ graph, external, publicProjectIds }) {
141
+ const graphHasEvidence = hasGraphEvidence(graph);
142
+ const liveSourceCount = external.filter(({ items }) => items.length > 0).length;
143
+ return (graphHasEvidence ? 1 : 0) + liveSourceCount > 1 ||
144
+ (graphHasEvidence && publicProjectIds.length > 0);
145
+ }
146
+
147
+ function stringArray(value, label, maximum) {
148
+ if (value === undefined || value === null) return [];
149
+ if (!Array.isArray(value) || value.length > maximum ||
150
+ value.some((item) => typeof item !== 'string' || !item.trim())) {
151
+ throw new TypeError(`${label} must be an array of at most ${maximum} nonempty strings`);
152
+ }
153
+ return [...new Set(value.map((item) => item.trim()))];
154
+ }
155
+
156
+ function positiveInteger(value, label, maximum) {
157
+ if (!Number.isInteger(value) || value <= 0 || value > maximum) {
158
+ throw new TypeError(`${label} must be a positive integer no greater than ${maximum}`);
159
+ }
160
+ return value;
161
+ }
162
+
163
+ function requiredString(value, label) {
164
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${label} is required`);
165
+ return value.trim();
166
+ }