archgraph-argo 0.10.52 → 0.11.0
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/argo/defaults/EA-model-template.feap +0 -0
- package/install-argo.ps1 +89 -23
- package/package.json +4 -1
- package/scripts/ea-layout-store.js +242 -0
- package/scripts/ea-web-service.js +1349 -0
- package/web/app.js +398 -0
- package/web/index.html +86 -0
- package/web/style.css +251 -0
- package/web/vendor/g6.min.js +68 -0
|
Binary file
|
package/install-argo.ps1
CHANGED
|
@@ -26,7 +26,10 @@ param(
|
|
|
26
26
|
[string]$OpenClawRepoRoot = '',
|
|
27
27
|
[switch]$SkipOpenClaw,
|
|
28
28
|
[string]$McpPath,
|
|
29
|
-
[string]$GraphMcpUrl = 'https://argo.derekworkspacev5.com/mcp'
|
|
29
|
+
[string]$GraphMcpUrl = 'https://argo.derekworkspacev5.com/mcp',
|
|
30
|
+
[switch]$SkipEaWeb,
|
|
31
|
+
[int]$EaWebPort = 8787,
|
|
32
|
+
[string]$EaWebRoot = ''
|
|
30
33
|
)
|
|
31
34
|
|
|
32
35
|
$ErrorActionPreference = 'Stop'
|
|
@@ -737,37 +740,37 @@ Write-Host '==> Deploying Argo toolchain'
|
|
|
737
740
|
|
|
738
741
|
$schemaSrc = Join-Path $argoDir 'schema'
|
|
739
742
|
$schemaDest = Join-Path $ArgoRoot 'schema'
|
|
740
|
-
Write-Host "[1/
|
|
743
|
+
Write-Host "[1/23] argo\schema -> $schemaDest"
|
|
741
744
|
Copy-Tree -Source $schemaSrc -Destination $schemaDest
|
|
742
745
|
|
|
743
746
|
$scriptsSrc = Join-Path $argoDir 'scripts'
|
|
744
747
|
$scriptsDest = Join-Path $ArgoRoot 'scripts'
|
|
745
|
-
Write-Host "[2/
|
|
748
|
+
Write-Host "[2/23] argo\scripts -> $scriptsDest"
|
|
746
749
|
Copy-Tree -Source $scriptsSrc -Destination $scriptsDest
|
|
747
750
|
|
|
748
751
|
$defaultsSrc = Join-Path $argoDir 'defaults'
|
|
749
752
|
$defaultsDest = Join-Path $ArgoRoot 'defaults'
|
|
750
|
-
Write-Host "[3/
|
|
753
|
+
Write-Host "[3/23] argo\defaults -> $defaultsDest"
|
|
751
754
|
Copy-Tree -Source $defaultsSrc -Destination $defaultsDest
|
|
752
755
|
|
|
753
756
|
$skillSrc = Join-Path (Join-Path $argoDir 'skills') 'argo-init'
|
|
754
757
|
$skillDest = Join-Path $SkillsRoot 'argo-init'
|
|
755
|
-
Write-Host "[4/
|
|
758
|
+
Write-Host "[4/23] argo\skills\argo-init -> $skillDest"
|
|
756
759
|
Copy-Tree -Source $skillSrc -Destination $skillDest
|
|
757
760
|
|
|
758
761
|
$ruleSrc = Join-Path (Join-Path $argoDir 'rules') 'archgraph.instructions.md'
|
|
759
762
|
$ruleDest = Join-Path $PromptsRoot 'archgraph.instructions.md'
|
|
760
|
-
Write-Host "[5/
|
|
763
|
+
Write-Host "[5/23] argo\rules\archgraph.instructions.md -> $ruleDest"
|
|
761
764
|
New-Item -ItemType Directory -Force -Path $PromptsRoot | Out-Null
|
|
762
765
|
Copy-Item -Force -Path $ruleSrc -Destination $ruleDest
|
|
763
766
|
|
|
764
767
|
$depsSrc = Join-Path $argoDir 'package.json'
|
|
765
768
|
$depsDest = Join-Path $ArgoRoot 'package.json'
|
|
766
|
-
Write-Host "[6/
|
|
769
|
+
Write-Host "[6/23] argo\package.json -> $depsDest"
|
|
767
770
|
Copy-Item -Force -Path $depsSrc -Destination $depsDest
|
|
768
771
|
|
|
769
772
|
$cursorSkillDest = Join-Path $CursorSkillsRoot 'argo-init'
|
|
770
|
-
Write-Host "[7/
|
|
773
|
+
Write-Host "[7/23] argo\skills\argo-init -> $cursorSkillDest (Cursor)"
|
|
771
774
|
Copy-Tree -Source $skillSrc -Destination $cursorSkillDest
|
|
772
775
|
|
|
773
776
|
$mcpBridgeSrc = Join-Path $argoDir 'mcp-bridges'
|
|
@@ -776,29 +779,29 @@ Write-Host " argo\mcp-bridges -> $mcpBridgeDest (Cursor graph-mcp stdio bridge)
|
|
|
776
779
|
Copy-Tree -Source $mcpBridgeSrc -Destination $mcpBridgeDest
|
|
777
780
|
|
|
778
781
|
$openCodeSkillDest = Join-Path $OpenCodeSkillsRoot 'argo-init'
|
|
779
|
-
Write-Host "[8/
|
|
782
|
+
Write-Host "[8/23] argo\skills\argo-init -> $openCodeSkillDest (OpenCode)"
|
|
780
783
|
Copy-Tree -Source $skillSrc -Destination $openCodeSkillDest
|
|
781
784
|
|
|
782
|
-
Write-Host "[9/
|
|
785
|
+
Write-Host "[9/23] argo\rules\archgraph.instructions.md -> $OpenCodeAgentsPath (OpenCode global AGENTS.md)"
|
|
783
786
|
Add-AgentsRule -AgentsPath $OpenCodeAgentsPath -RulePath $ruleSrc
|
|
784
787
|
|
|
785
788
|
$agentsSrc = Join-Path $argoDir 'agents'
|
|
786
|
-
Write-Host "[10/
|
|
789
|
+
Write-Host "[10/23] argo\agents -> $CopilotAgentsRoot (Copilot user-level)"
|
|
787
790
|
Copy-Agents -Source $agentsSrc -Destination $CopilotAgentsRoot
|
|
788
791
|
|
|
789
|
-
Write-Host "[11/
|
|
792
|
+
Write-Host "[11/23] argo\agents -> $CursorAgentsRoot (Cursor user-level, converted to .md)"
|
|
790
793
|
Copy-Agents -Source $agentsSrc -Destination $CursorAgentsRoot -Target cursor
|
|
791
794
|
|
|
792
|
-
Write-Host "[12/
|
|
795
|
+
Write-Host "[12/23] argo\agents -> $OpenCodeAgentsRoot (OpenCode user-level, converted to .md)"
|
|
793
796
|
Copy-Agents -Source $agentsSrc -Destination $OpenCodeAgentsRoot -Target opencode
|
|
794
797
|
|
|
795
798
|
$pluginsSrc = Join-Path $argoDir 'plugins'
|
|
796
|
-
Write-Host "[13/
|
|
799
|
+
Write-Host "[13/23] argo\plugins -> $PluginsRoot (Argo opencode plugins)"
|
|
797
800
|
Copy-Tree -Source $pluginsSrc -Destination $PluginsRoot
|
|
798
801
|
|
|
799
802
|
$cursorRuleSrc = Join-Path (Join-Path $argoDir 'rules') 'archgraph.instructions.md'
|
|
800
803
|
$cursorRuleDest = Join-Path $CursorRulesRoot 'archgraph.mdc'
|
|
801
|
-
Write-Host "[14/
|
|
804
|
+
Write-Host "[14/23] argo\rules\archgraph.instructions.md -> $cursorRuleDest (Cursor global rule, alwaysApply)"
|
|
802
805
|
New-Item -ItemType Directory -Force -Path $CursorRulesRoot | Out-Null
|
|
803
806
|
Convert-RuleFile -SourceFile $cursorRuleSrc -DestinationFile $cursorRuleDest
|
|
804
807
|
|
|
@@ -810,16 +813,16 @@ if ($SkipDsh) {
|
|
|
810
813
|
$patchPath = Join-Path $DshHome 'cordis.patch.yml'
|
|
811
814
|
$argoServer = (Join-Path $ArgoRoot 'scripts\argo-mcp-server.js').Replace('\', '/')
|
|
812
815
|
|
|
813
|
-
Write-Host "[15/
|
|
816
|
+
Write-Host "[15/23] argo\rules\archgraph.instructions.md -> $DshHome\AGENTS.md (DeepSeek Harness user-global rule, frontmatter stripped)"
|
|
814
817
|
Write-DshAgentRule -DshHome $DshHome -RuleText $ruleSrcContent
|
|
815
818
|
|
|
816
|
-
Write-Host "[16/
|
|
819
|
+
Write-Host "[16/23] argo\skills\argo-init -> $dshSkillDest (DeepSeek Harness skill)"
|
|
817
820
|
Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $dshSkillDest
|
|
818
821
|
|
|
819
|
-
Write-Host "[17/
|
|
822
|
+
Write-Host "[17/23] argo\rules\<WakeupGuideline> -> $DshHome\plugins\dsh-argo-wakeup\index.js (DeepSeek Harness wakeup plugin)"
|
|
820
823
|
$wakeupDshPath = New-DshWakeupPlugin -DshHome $DshHome -RuleText $ruleSrcContent
|
|
821
824
|
|
|
822
|
-
Write-Host "[18/
|
|
825
|
+
Write-Host "[18/23] argo-workspace + argo-wakeup rows -> $patchPath (DeepSeek Harness MCP bridge + wakeup plugin)"
|
|
823
826
|
# The generated dsh-argo-workspace bridge connects directly to the argo
|
|
824
827
|
# server (no dsh-mcp-client row), registers every tool as mcp__argo__* and
|
|
825
828
|
# injects the current session's workspace (SessionHeader.cwd) as the
|
|
@@ -850,7 +853,7 @@ if ($SkipDsh) {
|
|
|
850
853
|
Write-DshManagedBlock -Path $patchPath -Block $block -MarkerStart '# BEGIN ArchGraph ARGO deployment' -MarkerEnd '# END ArchGraph ARGO deployment'
|
|
851
854
|
}
|
|
852
855
|
|
|
853
|
-
Write-Host "[19/
|
|
856
|
+
Write-Host "[19/23] argo\agents -> $DshHome\.agent-presets\<id> (DeepSeek Harness agent presets)"
|
|
854
857
|
New-DshAgentPresets -DshHome $DshHome -AgentsSrc (Join-Path $argoDir 'agents')
|
|
855
858
|
|
|
856
859
|
Write-Host " note: graph-mcp remote ($GraphMcpUrl) is NOT registered for DeepSeek Harness -"
|
|
@@ -871,10 +874,10 @@ if ($SkipOpenClaw) {
|
|
|
871
874
|
$openClawAgentsDest = Join-Path $OpenClawWorkspace 'AGENTS.md'
|
|
872
875
|
|
|
873
876
|
Write-Host '==> Deploying OpenClaw integration'
|
|
874
|
-
Write-Host "[20/
|
|
877
|
+
Write-Host "[20/23] argo\rules\archgraph.instructions.md -> $openClawAgentsDest (OpenClaw workspace AGENTS.md, frontmatter stripped)"
|
|
875
878
|
Write-OpenClawAgentRule -OpenClawWorkspace $OpenClawWorkspace -RuleText $ruleSrcContent
|
|
876
879
|
|
|
877
|
-
Write-Host "[21/
|
|
880
|
+
Write-Host "[21/23] argo\skills\argo-init -> $openClawSkillDest (OpenClaw managed skill, all agents)"
|
|
878
881
|
Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $openClawSkillDest
|
|
879
882
|
|
|
880
883
|
Write-Host ' OpenClaw injects AGENTS.md into Project Context on every session, so the wakeup'
|
|
@@ -1043,7 +1046,7 @@ if ($SkipMcp) {
|
|
|
1043
1046
|
# location; -OpenClawRepoRoot overrides it (e.g. for tests or when the
|
|
1044
1047
|
# installer runs from a non-workspace npm-global package dir).
|
|
1045
1048
|
$openClawRepoRoot = if ($OpenClawRepoRoot) { $OpenClawRepoRoot } else { $repoRoot }
|
|
1046
|
-
Write-Host "[22/
|
|
1049
|
+
Write-Host "[22/23] argo MCP server -> $(Join-Path $OpenClawHome 'openclaw.json') (OpenClaw mcp.servers.argo, env.ARGO_REPO_ROOT pinned)"
|
|
1047
1050
|
Write-OpenClawMcpConfig -OpenClawHome $OpenClawHome -RepoRoot $openClawRepoRoot -ArgoServer $argoServer -GraphMcpServer $openClawGraphMcpServer
|
|
1048
1051
|
}
|
|
1049
1052
|
}
|
|
@@ -1055,5 +1058,68 @@ if (Test-Path $wakeupPluginPath) {
|
|
|
1055
1058
|
Write-Host "argo-wakeup plugin registered -> $OpenCodeConfigPath"
|
|
1056
1059
|
}
|
|
1057
1060
|
|
|
1061
|
+
# EA local web service (WP2786): deploy the two service scripts next to the
|
|
1062
|
+
# harness scripts (REPO_ROOT = $ArgoRoot keeps the relative layout intact) plus
|
|
1063
|
+
# the web/ statics, then start the service in the background. Re-running the
|
|
1064
|
+
# installer is idempotent: an occupied port skips the startup.
|
|
1065
|
+
$eaServiceSrc = Join-Path $repoRoot 'scripts\ea-web-service.js'
|
|
1066
|
+
$eaLayoutStoreSrc = Join-Path $repoRoot 'scripts\ea-layout-store.js'
|
|
1067
|
+
$eaWebSrc = Join-Path $repoRoot 'web'
|
|
1068
|
+
Write-Host "[23/23] scripts\ea-web-service.js + scripts\ea-layout-store.js + web -> $ArgoRoot (EA local web service)"
|
|
1069
|
+
if ((Test-Path $eaServiceSrc) -and (Test-Path $eaLayoutStoreSrc) -and (Test-Path $eaWebSrc)) {
|
|
1070
|
+
Copy-Item -Force -Path $eaServiceSrc -Destination (Join-Path $scriptsDest 'ea-web-service.js')
|
|
1071
|
+
Copy-Item -Force -Path $eaLayoutStoreSrc -Destination (Join-Path $scriptsDest 'ea-layout-store.js')
|
|
1072
|
+
Copy-Tree -Source $eaWebSrc -Destination (Join-Path $ArgoRoot 'web')
|
|
1073
|
+
Write-Host " service scripts + web statics deployed -> $scriptsDest / $(Join-Path $ArgoRoot 'web')"
|
|
1074
|
+
if ($SkipEaWeb) {
|
|
1075
|
+
Write-Host ' EA web service startup skipped (-SkipEaWeb)'
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
$eaWebRoot = if ($EaWebRoot) { $EaWebRoot } else { $PWD.Path }
|
|
1079
|
+
$eaPortInUse = $false
|
|
1080
|
+
try {
|
|
1081
|
+
$eaProbe = New-Object System.Net.Sockets.TcpClient
|
|
1082
|
+
$eaProbe.Connect('127.0.0.1', $EaWebPort)
|
|
1083
|
+
$eaPortInUse = $true
|
|
1084
|
+
$eaProbe.Close()
|
|
1085
|
+
}
|
|
1086
|
+
catch {
|
|
1087
|
+
$eaPortInUse = $false
|
|
1088
|
+
}
|
|
1089
|
+
if ($eaPortInUse) {
|
|
1090
|
+
Write-Host " EA web service already listening on 127.0.0.1:$EaWebPort; startup skipped (idempotent re-deploy)"
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
|
|
1094
|
+
if (-not $nodeCmd) {
|
|
1095
|
+
throw 'EA web service startup failed: node not found on PATH'
|
|
1096
|
+
}
|
|
1097
|
+
$eaServiceDest = Join-Path $scriptsDest 'ea-web-service.js'
|
|
1098
|
+
$eaLogOut = Join-Path $ArgoRoot 'ea-web-service.log'
|
|
1099
|
+
$eaLogErr = Join-Path $ArgoRoot 'ea-web-service.err.log'
|
|
1100
|
+
# Start the service through WMI (Win32_Process.Create) instead of
|
|
1101
|
+
# Start-Process: the process is then created by the WMI service and
|
|
1102
|
+
# inherits NONE of this installer's stdio pipe handles. Inherited
|
|
1103
|
+
# pipes would keep spawnSync-based launchers (argo-deploy bin, tests)
|
|
1104
|
+
# blocked until the service exits. cmd.exe is used only to redirect
|
|
1105
|
+
# the service stdout/stderr into log files.
|
|
1106
|
+
$q = [char]34
|
|
1107
|
+
$eaCmdLine = "cmd.exe /S /c $q$q$($nodeCmd.Source)$q $q$eaServiceDest$q --root $q$eaWebRoot$q --port $EaWebPort > $q$eaLogOut$q 2> $q$eaLogErr$q$q"
|
|
1108
|
+
$eaCreate = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
|
|
1109
|
+
CommandLine = $eaCmdLine
|
|
1110
|
+
CurrentDirectory = $ArgoRoot
|
|
1111
|
+
}
|
|
1112
|
+
if ($eaCreate.ReturnValue -ne 0) {
|
|
1113
|
+
throw "EA web service startup failed: Win32_Process.Create ReturnValue $($eaCreate.ReturnValue)"
|
|
1114
|
+
}
|
|
1115
|
+
Write-Host " EA web service started in background (pid $($eaCreate.ProcessId)): http://127.0.0.1:$EaWebPort (root: $eaWebRoot)"
|
|
1116
|
+
Write-Host " logs: $eaLogOut / $eaLogErr"
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
Write-Host ' EA web service sources not present in this package; step skipped'
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1058
1124
|
Write-Host ''
|
|
1059
1125
|
Write-Host 'Argo deployment complete.'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archgraph-argo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"argo/skills/argo-init",
|
|
17
17
|
"argo/rules",
|
|
18
18
|
"argo/package.json",
|
|
19
|
+
"scripts/ea-web-service.js",
|
|
20
|
+
"scripts/ea-layout-store.js",
|
|
21
|
+
"web",
|
|
19
22
|
"vendor",
|
|
20
23
|
"install-argo.ps1",
|
|
21
24
|
"bin",
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 布局侧车(Layout Sidecar,WP2785 M1-S2,2026-09-03 修订:默认按项目隔离,AT-2785-L5)
|
|
5
|
+
* — 图谱画布坐标独立持久化。
|
|
6
|
+
*
|
|
7
|
+
* 零第三方依赖(仅 Node 内置 path/fs/crypto)。与 design/KG/SystemArchitecture.json
|
|
8
|
+
* 完全物理隔离:坐标不进图谱 JSON、不进 schema;本模块永远不读写图谱文件本身。
|
|
9
|
+
*
|
|
10
|
+
* 存储根(按优先级):
|
|
11
|
+
* 1. 显式覆盖:createLayoutStore({ layoutRoot }) 选项或 EA_LAYOUT_ROOT 环境变量
|
|
12
|
+
* → <layoutRoot>/<projectId>/<view_id>.json(测试 / 自定义集中存储根)。
|
|
13
|
+
* 2. 默认(无任何覆盖):按项目隔离
|
|
14
|
+
* → <projectRoot>/design/KG/ea-layouts/<view_id>.json(仍是独立文件,
|
|
15
|
+
* 只落在 ea-layouts/ 子目录,绝不触碰 SystemArchitecture.json 本身)。
|
|
16
|
+
*
|
|
17
|
+
* 文件内容:{ version, graphKey, view_id, signature, updatedAt, elements: { <elementId>: {x,y} } }
|
|
18
|
+
*
|
|
19
|
+
* 失效键 = 视图成员身份签名(成员身份集合的 sha256,只含身份 id,不含任何内容字段):
|
|
20
|
+
* signature = sha256(sorted(included_elements) + sorted(included_relationships))。
|
|
21
|
+
* 图谱内容级修改(元素 name/description/attributes、关系描述等)不改变成员集合
|
|
22
|
+
* → signature 不变 → 坐标完全不动;仅成员结构变化触发新成员补位/移除成员清理。
|
|
23
|
+
*
|
|
24
|
+
* 合并语义(读取时):以当前图谱视图成员为准——仍有坐标的现存成员原样保留;
|
|
25
|
+
* 新成员(无坐标)按确定性算法补位(沿用 buildViewGraph 的圆形布局公式)并写回侧车;
|
|
26
|
+
* 已不在成员中的坐标清理。
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const crypto = require('node:crypto');
|
|
32
|
+
|
|
33
|
+
const LAYOUT_VERSION = 1;
|
|
34
|
+
// 默认按项目隔离时,侧车相对项目根的存放目录。
|
|
35
|
+
const PROJECT_LAYOUT_DIR = path.join('design', 'KG', 'ea-layouts');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 解析显式存储根覆盖:选项 > EA_LAYOUT_ROOT 环境变量。
|
|
39
|
+
* 返回 null 表示无覆盖 → 使用默认的按项目隔离存储。
|
|
40
|
+
*/
|
|
41
|
+
function resolveLayoutRoot(explicitRoot) {
|
|
42
|
+
if (explicitRoot) {
|
|
43
|
+
return path.resolve(explicitRoot);
|
|
44
|
+
}
|
|
45
|
+
if (process.env.EA_LAYOUT_ROOT) {
|
|
46
|
+
return path.resolve(process.env.EA_LAYOUT_ROOT);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 视图成员身份签名:仅由 included_elements / included_relationships 的身份 id 集合决定,
|
|
53
|
+
* 与元素/关系的任何内容字段无关。
|
|
54
|
+
*/
|
|
55
|
+
function computeViewSignature(view) {
|
|
56
|
+
const elements = Array.from(new Set((view && view.included_elements) || [])).sort();
|
|
57
|
+
const relationships = Array.from(new Set((view && view.included_relationships) || [])).sort();
|
|
58
|
+
const payload = `${elements.join('\u0001')}\u0000${relationships.join('\u0001')}`;
|
|
59
|
+
return crypto.createHash('sha256').update(payload).digest('hex');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 文件名安全化(服务端路由已限制 [A-Za-z0-9._-],此处为独立模块的防御性兜底)。
|
|
63
|
+
function safeFileName(segment) {
|
|
64
|
+
return String(segment).replace(/[^A-Za-z0-9._-]/g, '_');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 确定性补位:沿用 buildViewGraph 的圆形布局公式(成员索引 → 圆周位置)。
|
|
69
|
+
*/
|
|
70
|
+
function defaultPosition(index, count) {
|
|
71
|
+
const angle = (2 * Math.PI * index) / Math.max(count, 1);
|
|
72
|
+
return {
|
|
73
|
+
x: Math.round(80 + 160 * Math.cos(angle)),
|
|
74
|
+
y: Math.round(80 + 160 * Math.sin(angle)),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isValidPosition(pos) {
|
|
79
|
+
return !!pos
|
|
80
|
+
&& typeof pos === 'object'
|
|
81
|
+
&& !Array.isArray(pos)
|
|
82
|
+
&& Number.isFinite(Number(pos.x))
|
|
83
|
+
&& Number.isFinite(Number(pos.y));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createLayoutStore(options = {}) {
|
|
87
|
+
// null → 默认按项目隔离存储;否则为显式集中存储根。
|
|
88
|
+
const explicitRoot = resolveLayoutRoot(options.layoutRoot);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 目标解析:target = { project?: {id, root, graphPath}, projectId?, graphKey? }。
|
|
92
|
+
* 显式根模式用 projectId 分桶;默认模式用 project.root 定位项目内 ea-layouts/ 目录。
|
|
93
|
+
*/
|
|
94
|
+
function resolveTarget(target, viewId) {
|
|
95
|
+
const t = target || {};
|
|
96
|
+
const project = t.project || {};
|
|
97
|
+
const projectId = t.projectId || project.id || null;
|
|
98
|
+
const graphKey = t.graphKey || project.graphPath || null;
|
|
99
|
+
if (explicitRoot) {
|
|
100
|
+
if (!projectId) {
|
|
101
|
+
throw new Error('layout store: 显式存储根模式下必须提供 projectId(或 project.id)');
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
projectId,
|
|
105
|
+
graphKey,
|
|
106
|
+
filePath: path.join(explicitRoot, safeFileName(projectId), `${safeFileName(viewId)}.json`),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const projectRoot = project.root;
|
|
110
|
+
if (!projectRoot) {
|
|
111
|
+
throw new Error('layout store: 默认按项目隔离存储必须提供 project.root');
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
projectId,
|
|
115
|
+
graphKey,
|
|
116
|
+
filePath: path.join(path.resolve(projectRoot), PROJECT_LAYOUT_DIR, `${safeFileName(viewId)}.json`),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function filePathFor(target, viewId) {
|
|
121
|
+
return resolveTarget(target, viewId).filePath;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readRecord(target, viewId) {
|
|
125
|
+
let text;
|
|
126
|
+
try {
|
|
127
|
+
text = fs.readFileSync(filePathFor(target, viewId), 'utf8');
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const record = JSON.parse(text);
|
|
133
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (!record.elements || typeof record.elements !== 'object' || Array.isArray(record.elements)) {
|
|
137
|
+
record.elements = {};
|
|
138
|
+
}
|
|
139
|
+
return record;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function writeRecord(target, viewId, record) {
|
|
146
|
+
const filePath = filePathFor(target, viewId);
|
|
147
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
148
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
149
|
+
fs.writeFileSync(tempPath, JSON.stringify(record, null, 2), 'utf8');
|
|
150
|
+
fs.renameSync(tempPath, filePath);
|
|
151
|
+
return filePath;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 读取合并后的布局(以当前图谱视图成员为准)。
|
|
156
|
+
* 有补位/清理/签名变化时写回侧车;纯内容级修改(签名不变)不触发写入。
|
|
157
|
+
* 入参:{ project?: {id, root, graphPath}, projectId?, graphKey?, view }。
|
|
158
|
+
* 返回 { signature, elements: { <elementId>: {x,y} } }。
|
|
159
|
+
*/
|
|
160
|
+
function mergeLayout({ project, projectId, graphKey, view }) {
|
|
161
|
+
if (!view || typeof view.view_id !== 'string') {
|
|
162
|
+
throw new Error('mergeLayout: view with view_id is required');
|
|
163
|
+
}
|
|
164
|
+
const target = { project, projectId, graphKey };
|
|
165
|
+
const viewId = view.view_id;
|
|
166
|
+
const signature = computeViewSignature(view);
|
|
167
|
+
const record = readRecord(target, viewId);
|
|
168
|
+
const saved = record ? record.elements : {};
|
|
169
|
+
const members = Array.isArray(view.included_elements) ? view.included_elements : [];
|
|
170
|
+
const elements = {};
|
|
171
|
+
let dirty = !record || record.signature !== signature;
|
|
172
|
+
|
|
173
|
+
members.forEach((id, index) => {
|
|
174
|
+
const pos = saved[id];
|
|
175
|
+
if (isValidPosition(pos)) {
|
|
176
|
+
elements[id] = { x: Number(pos.x), y: Number(pos.y) };
|
|
177
|
+
} else {
|
|
178
|
+
elements[id] = defaultPosition(index, members.length);
|
|
179
|
+
dirty = true;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
for (const id of Object.keys(saved)) {
|
|
183
|
+
if (!members.includes(id)) {
|
|
184
|
+
dirty = true; // 已不在成员中的坐标:清理
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (dirty) {
|
|
188
|
+
writeRecord(target, viewId, {
|
|
189
|
+
version: LAYOUT_VERSION,
|
|
190
|
+
graphKey: resolveTarget(target, viewId).graphKey,
|
|
191
|
+
view_id: viewId,
|
|
192
|
+
signature,
|
|
193
|
+
updatedAt: new Date().toISOString(),
|
|
194
|
+
elements,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return { signature, elements };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 全量写入布局(按当前文档计算签名后原子写入侧车文件)。
|
|
202
|
+
* 入参:{ project?: {id, root, graphPath}, projectId?, graphKey?, view, elements };
|
|
203
|
+
* elements: { <elementId>: {x,y} };非法坐标抛错(由服务端映射为 400)。
|
|
204
|
+
*/
|
|
205
|
+
function putLayout({ project, projectId, graphKey, view, elements }) {
|
|
206
|
+
if (!view || typeof view.view_id !== 'string') {
|
|
207
|
+
throw new Error('putLayout: view with view_id is required');
|
|
208
|
+
}
|
|
209
|
+
if (!elements || typeof elements !== 'object' || Array.isArray(elements)) {
|
|
210
|
+
throw new Error('body.elements 必须是 { <elementId>: {x,y} } 对象');
|
|
211
|
+
}
|
|
212
|
+
const normalized = {};
|
|
213
|
+
for (const [id, pos] of Object.entries(elements)) {
|
|
214
|
+
if (!isValidPosition(pos)) {
|
|
215
|
+
throw new Error(`elements['${id}'] 的坐标必须是有限的 {x,y} 数字`);
|
|
216
|
+
}
|
|
217
|
+
normalized[id] = { x: Number(pos.x), y: Number(pos.y) };
|
|
218
|
+
}
|
|
219
|
+
const target = { project, projectId, graphKey };
|
|
220
|
+
const signature = computeViewSignature(view);
|
|
221
|
+
writeRecord(target, view.view_id, {
|
|
222
|
+
version: LAYOUT_VERSION,
|
|
223
|
+
graphKey: resolveTarget(target, view.view_id).graphKey,
|
|
224
|
+
view_id: view.view_id,
|
|
225
|
+
signature,
|
|
226
|
+
updatedAt: new Date().toISOString(),
|
|
227
|
+
elements: normalized,
|
|
228
|
+
});
|
|
229
|
+
return { signature, view_id: view.view_id };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { root: explicitRoot, filePathFor, readRecord, writeRecord, mergeLayout, putLayout };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
LAYOUT_VERSION,
|
|
237
|
+
PROJECT_LAYOUT_DIR,
|
|
238
|
+
resolveLayoutRoot,
|
|
239
|
+
computeViewSignature,
|
|
240
|
+
defaultPosition,
|
|
241
|
+
createLayoutStore,
|
|
242
|
+
};
|