archgraph-argo 0.11.0 → 0.12.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.qea +0 -0
- package/argo/scripts/argo-mcp-server.js +56 -3
- package/argo/scripts/ea-qea-sync-lib.js +688 -0
- package/argo/scripts/ea-qea-sync.js +109 -0
- package/argo/scripts/systemarchitecture-mcp-server.js +73 -0
- package/install-argo.ps1 +23 -89
- package/package.json +3 -5
- package/scripts/ea-layout-store.js +0 -242
- package/scripts/ea-web-service.js +0 -1349
- package/web/app.js +0 -398
- package/web/index.html +0 -86
- package/web/style.css +0 -251
- package/web/vendor/g6.min.js +0 -68
|
@@ -1,1349 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ArchGraph 本地 Web 服务(EA 知识图谱导入/导出/查看/编辑)— MVP 实现
|
|
5
|
-
*
|
|
6
|
-
* 零第三方运行时依赖(仅 Node 内置 http/fs/path/crypto/readline/child_process)。
|
|
7
|
-
* 可被测试 require(不 require 即启动监听);默认绑定 127.0.0.1,端口可配。
|
|
8
|
-
*
|
|
9
|
-
* 写图红线:所有「编辑」操作(新增/修改/删除视图/元素/关系)统一通过 ARGO MCP
|
|
10
|
-
* 写图接口完成(进程内 callTool,fallback 为 stdio 子进程 MCP 客户端),
|
|
11
|
-
* 与 Agent 写图路径一致;禁止在编辑路径直接改写 SystemArchitecture.json。
|
|
12
|
-
* 「导入」为整体替换操作(受控例外,见设计 AD-a):属于文档级批量操作,
|
|
13
|
-
* 校验(结构/引用完整,与 ARGO MCP 同 schema)→ 备份 → 原子写(temp+rename),
|
|
14
|
-
* 写图原语与 ARGO MCP 内部一致;「编辑」类(元素/关系/视图级增删改)必须经 ARGO MCP。
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const http = require('node:http');
|
|
18
|
-
const fs = require('node:fs');
|
|
19
|
-
const path = require('node:path');
|
|
20
|
-
const crypto = require('node:crypto');
|
|
21
|
-
const readline = require('node:readline');
|
|
22
|
-
const { spawn } = require('node:child_process');
|
|
23
|
-
const { createLayoutStore } = require('./ea-layout-store.js');
|
|
24
|
-
|
|
25
|
-
const DEFAULT_HOST = '127.0.0.1';
|
|
26
|
-
const DEFAULT_PORT = 8787;
|
|
27
|
-
const GRAPH_MARKER = ['design', 'KG', 'SystemArchitecture.json'];
|
|
28
|
-
const MAX_IMPORT_BYTES = 20 * 1024 * 1024; // 20 MB
|
|
29
|
-
const MAX_BODY_BYTES = 1 * 1024 * 1024; // 1 MB(搜索/编辑等请求体)
|
|
30
|
-
const DEFAULT_UNDO_DEPTH = 50;
|
|
31
|
-
const MAX_DISCOVERY_DEPTH = 6;
|
|
32
|
-
|
|
33
|
-
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
34
|
-
|
|
35
|
-
const MIME = Object.freeze({
|
|
36
|
-
'.html': 'text/html; charset=utf-8',
|
|
37
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
38
|
-
'.css': 'text/css; charset=utf-8',
|
|
39
|
-
'.json': 'application/json; charset=utf-8',
|
|
40
|
-
'.svg': 'image/svg+xml',
|
|
41
|
-
'.png': 'image/png',
|
|
42
|
-
'.ico': 'image/x-icon',
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 前端编辑操作语义 ↔ ARGO MCP 写图接口 一一映射(设计文档 §3 映射表)。
|
|
47
|
-
*/
|
|
48
|
-
const EDIT_OP_TOOL_MAP = Object.freeze({
|
|
49
|
-
addElement: 'addArchitectureElement',
|
|
50
|
-
updateElement: 'updateArchitectureElement',
|
|
51
|
-
removeElement: 'removeArchitectureElement',
|
|
52
|
-
addView: 'addArchitectureView',
|
|
53
|
-
updateView: 'updateArchitectureView',
|
|
54
|
-
removeView: 'removeArchitectureView',
|
|
55
|
-
updateRelationship: 'updateArchitectureRelationship',
|
|
56
|
-
removeRelationship: 'removeArchitectureRelationship',
|
|
57
|
-
applyMutation: 'applySystemArchitectureMutation',
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const EDIT_OPS = Object.freeze(Object.keys(EDIT_OP_TOOL_MAP));
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// 纯函数:项目发现 / 状态 / 校验 / 搜索 / 图数据
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
function defaultSearchRoots(explicitRoot) {
|
|
67
|
-
if (explicitRoot) {
|
|
68
|
-
return [path.resolve(explicitRoot)];
|
|
69
|
-
}
|
|
70
|
-
const roots = [REPO_ROOT, path.dirname(REPO_ROOT), path.dirname(path.dirname(REPO_ROOT))];
|
|
71
|
-
return [...new Set(roots.map((root) => path.resolve(root)))];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function projectIdForGraphPath(graphPath) {
|
|
75
|
-
return crypto.createHash('sha1').update(path.resolve(graphPath)).digest('hex').slice(0, 12);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function readGraphDocument(graphPath) {
|
|
79
|
-
return JSON.parse(fs.readFileSync(graphPath, 'utf8'));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* 递归发现项目:以 design/KG/SystemArchitecture.json 为 marker。
|
|
84
|
-
* 返回 [{ id, name, root, graphPath }]。
|
|
85
|
-
*/
|
|
86
|
-
function discoverProjects(searchRoots) {
|
|
87
|
-
const seen = new Set();
|
|
88
|
-
const projects = [];
|
|
89
|
-
|
|
90
|
-
function addProject(projectRoot) {
|
|
91
|
-
const graphPath = path.join(projectRoot, ...GRAPH_MARKER);
|
|
92
|
-
if (seen.has(graphPath)) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
seen.add(graphPath);
|
|
96
|
-
projects.push({
|
|
97
|
-
id: projectIdForGraphPath(graphPath),
|
|
98
|
-
name: path.basename(projectRoot),
|
|
99
|
-
root: path.resolve(projectRoot),
|
|
100
|
-
graphPath,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function walk(dir, depth) {
|
|
105
|
-
if (depth > MAX_DISCOVERY_DEPTH) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
if (fs.existsSync(path.join(dir, ...GRAPH_MARKER))) {
|
|
109
|
-
addProject(dir);
|
|
110
|
-
return; // 项目原子:不再下钻
|
|
111
|
-
}
|
|
112
|
-
let entries;
|
|
113
|
-
try {
|
|
114
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
115
|
-
} catch {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
for (const entry of entries) {
|
|
119
|
-
if (!entry.isDirectory()) {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const name = entry.name;
|
|
123
|
-
if (name === 'node_modules' || name === '.git' || name === 'vendor' || name === '.argo' || name.startsWith('.')) {
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
walk(path.join(dir, name), depth + 1);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
for (const root of searchRoots) {
|
|
131
|
-
if (fs.existsSync(root)) {
|
|
132
|
-
walk(path.resolve(root), 0);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function computeStatus(project) {
|
|
140
|
-
const base = {
|
|
141
|
-
id: project.id,
|
|
142
|
-
name: project.name,
|
|
143
|
-
graphPath: project.graphPath,
|
|
144
|
-
root: project.root,
|
|
145
|
-
};
|
|
146
|
-
let stat;
|
|
147
|
-
try {
|
|
148
|
-
stat = fs.statSync(project.graphPath);
|
|
149
|
-
} catch (error) {
|
|
150
|
-
return { ...base, valid: false, error: String(error.message), mtime: null, elements: 0, relationships: 0, views: 0 };
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
const doc = readGraphDocument(project.graphPath);
|
|
154
|
-
const errors = validateGraphDocument(doc);
|
|
155
|
-
return {
|
|
156
|
-
...base,
|
|
157
|
-
valid: errors.length === 0,
|
|
158
|
-
elements: Array.isArray(doc.elements) ? doc.elements.length : 0,
|
|
159
|
-
relationships: Array.isArray(doc.relationships) ? doc.relationships.length : 0,
|
|
160
|
-
views: Array.isArray(doc.views) ? doc.views.length : 0,
|
|
161
|
-
mtime: stat.mtime.toISOString(),
|
|
162
|
-
errors: errors.slice(0, 5),
|
|
163
|
-
};
|
|
164
|
-
} catch (error) {
|
|
165
|
-
return {
|
|
166
|
-
...base,
|
|
167
|
-
valid: false,
|
|
168
|
-
error: String(error.message),
|
|
169
|
-
elements: 0,
|
|
170
|
-
relationships: 0,
|
|
171
|
-
views: 0,
|
|
172
|
-
mtime: stat.mtime.toISOString(),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* 结构校验(需求 FR-4 / AC-4 / AC-5):
|
|
179
|
-
* 根字段、id 唯一、parent/source_id/target_id/included_* 引用完整。
|
|
180
|
-
* 返回可读错误数组(空数组 = 通过)。
|
|
181
|
-
*/
|
|
182
|
-
function validateGraphDocument(doc) {
|
|
183
|
-
const errors = [];
|
|
184
|
-
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
185
|
-
return ['根节点必须是 JSON 对象'];
|
|
186
|
-
}
|
|
187
|
-
for (const key of ['name', 'description', 'elements', 'relationships', 'views']) {
|
|
188
|
-
if (typeof doc[key] === 'undefined') {
|
|
189
|
-
errors.push(`缺少根字段 '${key}'`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (!Array.isArray(doc.elements)) {
|
|
193
|
-
errors.push("'elements' 必须是数组");
|
|
194
|
-
}
|
|
195
|
-
if (!Array.isArray(doc.relationships)) {
|
|
196
|
-
errors.push("'relationships' 必须是数组");
|
|
197
|
-
}
|
|
198
|
-
if (!Array.isArray(doc.views)) {
|
|
199
|
-
errors.push("'views' 必须是数组");
|
|
200
|
-
}
|
|
201
|
-
if (errors.length > 0) {
|
|
202
|
-
return errors;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const elementIds = new Set();
|
|
206
|
-
for (const element of doc.elements) {
|
|
207
|
-
if (!element || typeof element.id !== 'string' || element.id === '') {
|
|
208
|
-
errors.push('存在缺少 id 的元素');
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (elementIds.has(element.id)) {
|
|
212
|
-
errors.push(`元素 id 重复:'${element.id}'`);
|
|
213
|
-
}
|
|
214
|
-
elementIds.add(element.id);
|
|
215
|
-
}
|
|
216
|
-
const relationshipIds = new Set();
|
|
217
|
-
for (const relationship of doc.relationships) {
|
|
218
|
-
if (!relationship || typeof relationship.id !== 'string' || relationship.id === '') {
|
|
219
|
-
errors.push('存在缺少 id 的关系');
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
if (relationshipIds.has(relationship.id)) {
|
|
223
|
-
errors.push(`关系 id 重复:'${relationship.id}'`);
|
|
224
|
-
}
|
|
225
|
-
relationshipIds.add(relationship.id);
|
|
226
|
-
}
|
|
227
|
-
const viewIds = new Set();
|
|
228
|
-
for (const view of doc.views) {
|
|
229
|
-
if (!view || typeof view.view_id !== 'string' || view.view_id === '') {
|
|
230
|
-
errors.push('存在缺少 view_id 的视图');
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
if (viewIds.has(view.view_id)) {
|
|
234
|
-
errors.push(`视图 view_id 重复:'${view.view_id}'`);
|
|
235
|
-
}
|
|
236
|
-
viewIds.add(view.view_id);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
for (const element of doc.elements) {
|
|
240
|
-
if (element.parent && !elementIds.has(element.parent)) {
|
|
241
|
-
errors.push(`元素 '${element.id}' 的 parent '${element.parent}' 不存在`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
for (const relationship of doc.relationships) {
|
|
245
|
-
if (!elementIds.has(relationship.source_id)) {
|
|
246
|
-
errors.push(`关系 '${relationship.id}' 的 source_id '${relationship.source_id}' 不存在`);
|
|
247
|
-
}
|
|
248
|
-
if (!elementIds.has(relationship.target_id)) {
|
|
249
|
-
errors.push(`关系 '${relationship.id}' 的 target_id '${relationship.target_id}' 不存在`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
for (const view of doc.views) {
|
|
253
|
-
for (const id of view.included_elements || []) {
|
|
254
|
-
if (!elementIds.has(id)) {
|
|
255
|
-
errors.push(`视图 '${view.view_id}' 引用的元素 '${id}' 不存在`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
for (const id of view.included_relationships || []) {
|
|
259
|
-
if (!relationshipIds.has(id)) {
|
|
260
|
-
errors.push(`视图 '${view.view_id}' 引用的关系 '${id}' 不存在`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (view.parent_element_id && !elementIds.has(view.parent_element_id)) {
|
|
264
|
-
errors.push(`视图 '${view.view_id}' 的 parent_element_id '${view.parent_element_id}' 不存在`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (doc.attributes !== undefined && !Array.isArray(doc.attributes)) {
|
|
268
|
-
errors.push("'attributes' 必须是数组");
|
|
269
|
-
}
|
|
270
|
-
return errors;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function layerOf(type) {
|
|
274
|
-
const t = String(type || '');
|
|
275
|
-
if (/^(Business|Stakeholder|Driver|Assessment|Goal|Outcome|Principle|Requirement|Constraint|Meaning|Value|Product|Contract|Representation)/.test(t)) {
|
|
276
|
-
return 'Business';
|
|
277
|
-
}
|
|
278
|
-
if (/^(Application|Data Object)/.test(t)) {
|
|
279
|
-
return 'Application';
|
|
280
|
-
}
|
|
281
|
-
if (/^(Technology|Node$|Device$|System Software|Artifact|Equipment|Facility|Distribution Network|Material|Path$|Communication Network)/.test(t)) {
|
|
282
|
-
return 'Technology';
|
|
283
|
-
}
|
|
284
|
-
if (/^(Work Package|Deliverable|Implementation Event|Plateau|Gap$)/.test(t)) {
|
|
285
|
-
return 'Implementation';
|
|
286
|
-
}
|
|
287
|
-
return 'Other';
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* 视图图数据(供前端渲染):nodes / edges,带简单圆形初始布局。
|
|
292
|
-
*/
|
|
293
|
-
function buildViewGraph(doc, viewId) {
|
|
294
|
-
const view = doc.views.find((entry) => entry.view_id === viewId);
|
|
295
|
-
if (!view) {
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
const included = view.included_elements || [];
|
|
299
|
-
const count = included.length;
|
|
300
|
-
const nodes = included
|
|
301
|
-
.map((id, index) => {
|
|
302
|
-
const element = doc.elements.find((entry) => entry.id === id);
|
|
303
|
-
if (!element) {
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
const angle = (2 * Math.PI * index) / Math.max(count, 1);
|
|
307
|
-
return {
|
|
308
|
-
id: element.id,
|
|
309
|
-
label: element.name,
|
|
310
|
-
type: element.type,
|
|
311
|
-
layer: layerOf(element.type),
|
|
312
|
-
x: Math.round(80 + 160 * Math.cos(angle)),
|
|
313
|
-
y: Math.round(80 + 160 * Math.sin(angle)),
|
|
314
|
-
fx: null,
|
|
315
|
-
fy: null,
|
|
316
|
-
data: {
|
|
317
|
-
description: element.description || '',
|
|
318
|
-
parent: element.parent || null,
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
})
|
|
322
|
-
.filter(Boolean);
|
|
323
|
-
|
|
324
|
-
const edges = (view.included_relationships || [])
|
|
325
|
-
.map((id) => {
|
|
326
|
-
const relationship = doc.relationships.find((entry) => entry.id === id);
|
|
327
|
-
if (!relationship) {
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
return {
|
|
331
|
-
id: relationship.id,
|
|
332
|
-
source: relationship.source_id,
|
|
333
|
-
target: relationship.target_id,
|
|
334
|
-
label: relationship.type,
|
|
335
|
-
type: relationship.type,
|
|
336
|
-
};
|
|
337
|
-
})
|
|
338
|
-
.filter(Boolean);
|
|
339
|
-
|
|
340
|
-
return {
|
|
341
|
-
view: { view_id: view.view_id, view_name: view.view_name },
|
|
342
|
-
nodes,
|
|
343
|
-
edges,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function scoreText(field, query) {
|
|
348
|
-
if (typeof field !== 'string') {
|
|
349
|
-
return 0;
|
|
350
|
-
}
|
|
351
|
-
const lower = field.toLowerCase();
|
|
352
|
-
if (lower === query) {
|
|
353
|
-
return 100;
|
|
354
|
-
}
|
|
355
|
-
if (lower.startsWith(query)) {
|
|
356
|
-
return 50;
|
|
357
|
-
}
|
|
358
|
-
if (lower.includes(query)) {
|
|
359
|
-
return 20;
|
|
360
|
-
}
|
|
361
|
-
return 0;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* local 子串检索(始终可用)。返回 { mode: 'local', hits: [...] }。
|
|
366
|
-
*/
|
|
367
|
-
function searchLocal(doc, rawQuery) {
|
|
368
|
-
const query = String(rawQuery || '').trim().toLowerCase();
|
|
369
|
-
if (!query) {
|
|
370
|
-
return { mode: 'local', query: rawQuery || '', hits: [] };
|
|
371
|
-
}
|
|
372
|
-
const hits = [];
|
|
373
|
-
for (const element of doc.elements || []) {
|
|
374
|
-
const score = Math.max(
|
|
375
|
-
scoreText(element.name, query),
|
|
376
|
-
scoreText(element.id, query),
|
|
377
|
-
scoreText(element.type, query) / 2,
|
|
378
|
-
scoreText(element.description, query) / 2,
|
|
379
|
-
);
|
|
380
|
-
if (score > 0) {
|
|
381
|
-
hits.push({ kind: 'element', id: element.id, name: element.name, type: element.type, description: element.description || '', score });
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
for (const relationship of doc.relationships || []) {
|
|
385
|
-
const score = Math.max(
|
|
386
|
-
scoreText(relationship.name, query),
|
|
387
|
-
scoreText(relationship.type, query) / 2,
|
|
388
|
-
scoreText(relationship.statement, query) / 2,
|
|
389
|
-
);
|
|
390
|
-
if (score > 0) {
|
|
391
|
-
hits.push({ kind: 'relationship', id: relationship.id, name: relationship.name, type: relationship.type, statement: relationship.statement || '', score });
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
for (const view of doc.views || []) {
|
|
395
|
-
const score = Math.max(scoreText(view.view_name, query), scoreText(view.view_id, query));
|
|
396
|
-
if (score > 0) {
|
|
397
|
-
hits.push({ kind: 'view', id: view.view_id, name: view.view_name, score });
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
hits.sort((a, b) => b.score - a.score);
|
|
401
|
-
return { mode: 'local', query: rawQuery || '', hits: hits.slice(0, 50) };
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// ---------------------------------------------------------------------------
|
|
405
|
-
// 编辑 op → MCP 参数与逆操作(Command 模式,AD-h)
|
|
406
|
-
// ---------------------------------------------------------------------------
|
|
407
|
-
|
|
408
|
-
function findById(entries, id) {
|
|
409
|
-
return (entries || []).find((entry) => entry.id === id);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function buildEditArgs(op, payload) {
|
|
413
|
-
const p = payload || {};
|
|
414
|
-
switch (op) {
|
|
415
|
-
case 'addElement':
|
|
416
|
-
return { element: p.element, view_ids: p.view_ids };
|
|
417
|
-
case 'updateElement':
|
|
418
|
-
return { id: p.id, patch: p.patch };
|
|
419
|
-
case 'removeElement':
|
|
420
|
-
return p.view_ids ? { id: p.id, view_ids: p.view_ids } : { id: p.id };
|
|
421
|
-
case 'addView':
|
|
422
|
-
return { view: p.view };
|
|
423
|
-
case 'updateView':
|
|
424
|
-
return { view_id: p.view_id, patch: p.patch };
|
|
425
|
-
case 'removeView':
|
|
426
|
-
return { view_id: p.view_id };
|
|
427
|
-
case 'updateRelationship':
|
|
428
|
-
return { id: p.id, patch: p.patch };
|
|
429
|
-
case 'removeRelationship':
|
|
430
|
-
return p.view_ids ? { id: p.id, view_ids: p.view_ids } : { id: p.id };
|
|
431
|
-
case 'applyMutation':
|
|
432
|
-
return { mutations: p.mutations };
|
|
433
|
-
default:
|
|
434
|
-
throw new Error(`Unsupported edit op: ${op}`);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function viewsContainingElement(doc, elementId) {
|
|
439
|
-
return (doc.views || [])
|
|
440
|
-
.filter((view) => (view.included_elements || []).includes(elementId))
|
|
441
|
-
.map((view) => view.view_id);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function viewsContainingRelationship(doc, relationshipId) {
|
|
445
|
-
return (doc.views || [])
|
|
446
|
-
.filter((view) => (view.included_relationships || []).includes(relationshipId))
|
|
447
|
-
.map((view) => view.view_id);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function oldFieldsForPatch(entry, patch) {
|
|
451
|
-
const oldFields = {};
|
|
452
|
-
for (const key of Object.keys(patch || {})) {
|
|
453
|
-
if (key === 'id' || key === 'type' || key === 'view_id') {
|
|
454
|
-
continue; // 不可变字段
|
|
455
|
-
}
|
|
456
|
-
oldFields[key] = entry ? entry[key] : undefined;
|
|
457
|
-
}
|
|
458
|
-
return oldFields;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* 依据编辑前文档推导逆操作(ARGO MCP 逆调用),供撤销使用。
|
|
463
|
-
*/
|
|
464
|
-
function deriveInverseCommand(op, args, beforeDoc) {
|
|
465
|
-
switch (op) {
|
|
466
|
-
case 'addElement':
|
|
467
|
-
return { tool: 'removeArchitectureElement', args: { id: args.element.id } };
|
|
468
|
-
case 'addView':
|
|
469
|
-
return { tool: 'removeArchitectureView', args: { view_id: args.view.view_id } };
|
|
470
|
-
case 'removeElement': {
|
|
471
|
-
const element = findById(beforeDoc.elements, args.id);
|
|
472
|
-
const viewIds = viewsContainingElement(beforeDoc, args.id);
|
|
473
|
-
return {
|
|
474
|
-
tool: 'addArchitectureElement',
|
|
475
|
-
args: { element, view_ids: viewIds.length > 0 ? viewIds : undefined },
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
case 'removeView': {
|
|
479
|
-
const view = beforeDoc.views.find((entry) => entry.view_id === args.view_id);
|
|
480
|
-
return { tool: 'addArchitectureView', args: { view } };
|
|
481
|
-
}
|
|
482
|
-
case 'removeRelationship': {
|
|
483
|
-
const relationship = findById(beforeDoc.relationships, args.id);
|
|
484
|
-
const viewIds = viewsContainingRelationship(beforeDoc, args.id);
|
|
485
|
-
return {
|
|
486
|
-
tool: 'addArchitectureRelationship',
|
|
487
|
-
args: { relationship, view_ids: viewIds.length > 0 ? viewIds : undefined },
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
case 'updateElement': {
|
|
491
|
-
const element = findById(beforeDoc.elements, args.id);
|
|
492
|
-
return { tool: 'updateArchitectureElement', args: { id: args.id, patch: oldFieldsForPatch(element, args.patch) } };
|
|
493
|
-
}
|
|
494
|
-
case 'updateView': {
|
|
495
|
-
const view = beforeDoc.views.find((entry) => entry.view_id === args.view_id);
|
|
496
|
-
return { tool: 'updateArchitectureView', args: { view_id: args.view_id, patch: oldFieldsForPatch(view, args.patch) } };
|
|
497
|
-
}
|
|
498
|
-
case 'updateRelationship': {
|
|
499
|
-
const relationship = findById(beforeDoc.relationships, args.id);
|
|
500
|
-
return { tool: 'updateArchitectureRelationship', args: { id: args.id, patch: oldFieldsForPatch(relationship, args.patch) } };
|
|
501
|
-
}
|
|
502
|
-
case 'applyMutation':
|
|
503
|
-
// 批量/复合变更:MVP 记录为快照回退(见 createService 的 applyMutation 处理)。
|
|
504
|
-
return null;
|
|
505
|
-
default:
|
|
506
|
-
return null;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// ---------------------------------------------------------------------------
|
|
511
|
-
// ARGO MCP 适配层(唯一写图入口;读检索亦复用)
|
|
512
|
-
// ---------------------------------------------------------------------------
|
|
513
|
-
|
|
514
|
-
function normalizeMcpResult(result) {
|
|
515
|
-
let payload = result;
|
|
516
|
-
let rawText = '';
|
|
517
|
-
if (result && Array.isArray(result.content)) {
|
|
518
|
-
rawText = result.content.map((entry) => (entry && entry.text) || '').join('\n');
|
|
519
|
-
try {
|
|
520
|
-
payload = JSON.parse(rawText);
|
|
521
|
-
} catch {
|
|
522
|
-
payload = { raw: rawText };
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
const failed = (result && result.isError === true) || (payload && payload.status === 'failed');
|
|
526
|
-
return {
|
|
527
|
-
ok: !failed,
|
|
528
|
-
payload,
|
|
529
|
-
rawText,
|
|
530
|
-
error: payload && payload.error ? payload.error : failed ? { message: 'ARGO MCP tool failed' } : null,
|
|
531
|
-
raw: result,
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function createMcpAdapter(options = {}) {
|
|
536
|
-
const mode = options.mode || 'in-process';
|
|
537
|
-
let argoMcp = null;
|
|
538
|
-
let loadError = null;
|
|
539
|
-
if (mode === 'in-process') {
|
|
540
|
-
try {
|
|
541
|
-
// eslint-disable-next-line global-require
|
|
542
|
-
argoMcp = require('../argo/scripts/argo-mcp-server.js');
|
|
543
|
-
} catch (error) {
|
|
544
|
-
loadError = error;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async function callStdio(toolName, args, projectRoot) {
|
|
549
|
-
const script = path.join(REPO_ROOT, 'argo', 'scripts', 'argo-mcp-server.js');
|
|
550
|
-
return new Promise((resolve, reject) => {
|
|
551
|
-
const child = spawn(process.execPath, [script], {
|
|
552
|
-
cwd: projectRoot || process.cwd(),
|
|
553
|
-
env: { ...process.env, ...(projectRoot ? { ARGO_REPO_ROOT: projectRoot } : {}) },
|
|
554
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
555
|
-
});
|
|
556
|
-
const rl = readline.createInterface({ input: child.stdout });
|
|
557
|
-
let seq = 0;
|
|
558
|
-
const pending = new Map();
|
|
559
|
-
rl.on('line', (line) => {
|
|
560
|
-
if (!line.trim()) {
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
let msg;
|
|
564
|
-
try {
|
|
565
|
-
msg = JSON.parse(line);
|
|
566
|
-
} catch {
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
if (msg.id !== undefined && pending.has(msg.id)) {
|
|
570
|
-
const handler = pending.get(msg.id);
|
|
571
|
-
pending.delete(msg.id);
|
|
572
|
-
if (msg.error) {
|
|
573
|
-
handler.reject(new Error(msg.error.message || 'MCP error'));
|
|
574
|
-
} else {
|
|
575
|
-
handler.resolve(msg.result);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
child.stderr.on('data', () => {});
|
|
580
|
-
child.on('error', reject);
|
|
581
|
-
function send(method, params) {
|
|
582
|
-
const id = ++seq;
|
|
583
|
-
return new Promise((res, rej) => {
|
|
584
|
-
pending.set(id, { resolve: res, reject: rej });
|
|
585
|
-
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
(async () => {
|
|
589
|
-
try {
|
|
590
|
-
await send('initialize', {
|
|
591
|
-
protocolVersion: '2024-11-05',
|
|
592
|
-
capabilities: {},
|
|
593
|
-
clientInfo: { name: 'ea-web-service', version: '1.0.0' },
|
|
594
|
-
});
|
|
595
|
-
const list = await send('tools/list', {});
|
|
596
|
-
const tool = (list.tools || []).find((entry) => entry.name === toolName);
|
|
597
|
-
if (!tool) {
|
|
598
|
-
throw new Error(`Tool not found: ${toolName}`);
|
|
599
|
-
}
|
|
600
|
-
const result = await send('tools/call', { name: toolName, arguments: args || {} });
|
|
601
|
-
resolve(normalizeMcpResult(result));
|
|
602
|
-
} catch (error) {
|
|
603
|
-
reject(error);
|
|
604
|
-
} finally {
|
|
605
|
-
child.stdin.end();
|
|
606
|
-
}
|
|
607
|
-
})();
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
async function callTool(toolName, args, projectRoot) {
|
|
612
|
-
if (mode === 'stdio') {
|
|
613
|
-
return callStdio(toolName, args, projectRoot);
|
|
614
|
-
}
|
|
615
|
-
if (!argoMcp) {
|
|
616
|
-
throw new Error(`ARGO MCP in-process backend unavailable: ${loadError ? loadError.message : 'not loaded'}`);
|
|
617
|
-
}
|
|
618
|
-
const previous = process.env.ARGO_REPO_ROOT;
|
|
619
|
-
if (projectRoot) {
|
|
620
|
-
process.env.ARGO_REPO_ROOT = projectRoot;
|
|
621
|
-
}
|
|
622
|
-
try {
|
|
623
|
-
const result = await argoMcp.callTool(toolName, args || {}, null, undefined);
|
|
624
|
-
return normalizeMcpResult(result);
|
|
625
|
-
} finally {
|
|
626
|
-
if (previous === undefined) {
|
|
627
|
-
delete process.env.ARGO_REPO_ROOT;
|
|
628
|
-
} else {
|
|
629
|
-
process.env.ARGO_REPO_ROOT = previous;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return {
|
|
635
|
-
mode,
|
|
636
|
-
available: mode === 'stdio' ? true : !!argoMcp,
|
|
637
|
-
callTool,
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// ---------------------------------------------------------------------------
|
|
642
|
-
// 搜索(local 恒可用;semantic/context 复用 ARGO MCP,不可用则明确降级)
|
|
643
|
-
// ---------------------------------------------------------------------------
|
|
644
|
-
|
|
645
|
-
function withTimeout(promise, ms, fallbackError) {
|
|
646
|
-
let timer;
|
|
647
|
-
const timeout = new Promise((resolve) => {
|
|
648
|
-
timer = setTimeout(() => resolve({ timedOut: true }), ms);
|
|
649
|
-
});
|
|
650
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// 把 ARGO MCP 语义/上下文检索结果(document/result 形态)归一化为前端可渲染的 hits。
|
|
654
|
-
function hitsFromPayload(payload) {
|
|
655
|
-
const source = (payload && payload.document) || (payload && payload.result) || {};
|
|
656
|
-
const elements = Array.isArray(source.elements) ? source.elements : [];
|
|
657
|
-
const relationships = Array.isArray(source.relationships) ? source.relationships : [];
|
|
658
|
-
const views = Array.isArray(source.views) ? source.views : [];
|
|
659
|
-
const hits = [
|
|
660
|
-
...elements.map((e) => ({ kind: 'element', id: e.id, name: e.name, type: e.type })),
|
|
661
|
-
...relationships.map((r) => ({ kind: 'relationship', id: r.id, name: r.name || r.type, type: r.type })),
|
|
662
|
-
...views.map((v) => ({ kind: 'view', id: v.view_id || v.id, name: v.view_name || v.name || v.view_id || v.id, type: 'View' })),
|
|
663
|
-
];
|
|
664
|
-
return hits.slice(0, 50);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
async function searchSemantic(adapter, project, query) {
|
|
668
|
-
// 首次语义检索需初始化语义旅程/嵌入生命周期,可能耗时数秒;给足超时,后续调用会更快。
|
|
669
|
-
const timeoutMs = 15000;
|
|
670
|
-
const args = {
|
|
671
|
-
architecturePath: GRAPH_MARKER.join('/'),
|
|
672
|
-
query: { purpose: 'general', intent: query },
|
|
673
|
-
};
|
|
674
|
-
const attempt = adapter.callTool('getSystemArchitecture', args, project.root);
|
|
675
|
-
const result = await withTimeout(attempt, timeoutMs, { timedOut: true });
|
|
676
|
-
if (result && result.timedOut) {
|
|
677
|
-
return {
|
|
678
|
-
mode: 'semantic',
|
|
679
|
-
supported: false,
|
|
680
|
-
message: '语义检索超时,已回退本地检索',
|
|
681
|
-
fallback: searchLocal(readGraphDocument(project.graphPath), query),
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
if (!result.ok) {
|
|
685
|
-
return {
|
|
686
|
-
mode: 'semantic',
|
|
687
|
-
supported: false,
|
|
688
|
-
message: '语义检索失败,已回退本地检索',
|
|
689
|
-
detail: (result.error && result.error.message) || 'getSystemArchitecture failed',
|
|
690
|
-
fallback: searchLocal(readGraphDocument(project.graphPath), query),
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
return { mode: 'semantic', supported: true, hits: hitsFromPayload(result.payload) };
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
async function searchContext(adapter, project, query, elementId) {
|
|
697
|
-
const doc = readGraphDocument(project.graphPath);
|
|
698
|
-
const local = searchLocal(doc, elementId || query);
|
|
699
|
-
const elementHit = local.hits.find((hit) => hit.kind === 'element');
|
|
700
|
-
if (!elementHit) {
|
|
701
|
-
return {
|
|
702
|
-
mode: 'context',
|
|
703
|
-
supported: false,
|
|
704
|
-
message: elementId ? `未找到元素 '${elementId}'` : '未找到可做上下文检索的元素(TODO)',
|
|
705
|
-
hits: local.hits,
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
const args = { elementId: elementHit.id };
|
|
709
|
-
const result = await adapter.callTool('getIntentElementContext', args, project.root);
|
|
710
|
-
if (!result.ok) {
|
|
711
|
-
return {
|
|
712
|
-
mode: 'context',
|
|
713
|
-
supported: false,
|
|
714
|
-
message: '上下文检索失败,已回退本地检索',
|
|
715
|
-
detail: (result.error && result.error.message) || 'getIntentElementContext failed',
|
|
716
|
-
hits: local.hits,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
return { mode: 'context', supported: true, elementId: elementHit.id, hits: hitsFromPayload(result.payload) };
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
async function searchProject(adapter, project, body) {
|
|
723
|
-
const { query, mode, elementId } = body || {};
|
|
724
|
-
const q = String(query || '').trim();
|
|
725
|
-
if (!q && !elementId) {
|
|
726
|
-
return { mode: mode || 'local', hits: [] };
|
|
727
|
-
}
|
|
728
|
-
if (mode === 'semantic') {
|
|
729
|
-
return searchSemantic(adapter, project, q);
|
|
730
|
-
}
|
|
731
|
-
if (mode === 'context') {
|
|
732
|
-
return searchContext(adapter, project, q, elementId);
|
|
733
|
-
}
|
|
734
|
-
const doc = readGraphDocument(project.graphPath);
|
|
735
|
-
return searchLocal(doc, q);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// ---------------------------------------------------------------------------
|
|
739
|
-
// 文件写入工具(备份 + 原子写)
|
|
740
|
-
// ---------------------------------------------------------------------------
|
|
741
|
-
|
|
742
|
-
function backupGraph(project, keep = 10) {
|
|
743
|
-
const backupDir = path.join(project.root, '.argo', 'backups', project.name || project.id);
|
|
744
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
745
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
746
|
-
fs.copyFileSync(project.graphPath, path.join(backupDir, `${stamp}.json`));
|
|
747
|
-
const files = fs.readdirSync(backupDir)
|
|
748
|
-
.filter((name) => name.endsWith('.json'))
|
|
749
|
-
.sort();
|
|
750
|
-
while (files.length > keep) {
|
|
751
|
-
const oldest = files.shift();
|
|
752
|
-
try {
|
|
753
|
-
fs.unlinkSync(path.join(backupDir, oldest));
|
|
754
|
-
} catch {
|
|
755
|
-
/* ignore */
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function atomicWriteFile(filePath, text) {
|
|
761
|
-
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
762
|
-
fs.writeFileSync(tempPath, text, 'utf8');
|
|
763
|
-
fs.renameSync(tempPath, filePath);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// 跨进程文件锁(AD-i):锁文件 + 原子创建('wx');占用时短暂重试后拒绝(409)。
|
|
767
|
-
const LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
768
|
-
function sleepSync(ms) {
|
|
769
|
-
Atomics.wait(LOCK_SLEEP, 0, 0, ms);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function acquireFileLock(graphPath, { timeoutMs = 3000, retryMs = 40 } = {}) {
|
|
773
|
-
const lockPath = `${graphPath}.lock`;
|
|
774
|
-
const start = Date.now();
|
|
775
|
-
for (;;) {
|
|
776
|
-
let fd;
|
|
777
|
-
try {
|
|
778
|
-
fd = fs.openSync(lockPath, 'wx');
|
|
779
|
-
try {
|
|
780
|
-
fs.writeSync(fd, String(process.pid));
|
|
781
|
-
} catch {
|
|
782
|
-
/* ignore */
|
|
783
|
-
}
|
|
784
|
-
return () => {
|
|
785
|
-
try {
|
|
786
|
-
fs.closeSync(fd);
|
|
787
|
-
} catch {
|
|
788
|
-
/* ignore */
|
|
789
|
-
}
|
|
790
|
-
try {
|
|
791
|
-
fs.unlinkSync(lockPath);
|
|
792
|
-
} catch {
|
|
793
|
-
/* ignore */
|
|
794
|
-
}
|
|
795
|
-
};
|
|
796
|
-
} catch (error) {
|
|
797
|
-
if (error.code !== 'EEXIST') {
|
|
798
|
-
throw error;
|
|
799
|
-
}
|
|
800
|
-
if (Date.now() - start > timeoutMs) {
|
|
801
|
-
throw new HttpError(409, '图谱正被其他进程写入,请稍后重试');
|
|
802
|
-
}
|
|
803
|
-
sleepSync(retryMs);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// ---------------------------------------------------------------------------
|
|
809
|
-
// 服务
|
|
810
|
-
// ---------------------------------------------------------------------------
|
|
811
|
-
|
|
812
|
-
class HttpError extends Error {
|
|
813
|
-
constructor(status, message) {
|
|
814
|
-
super(message);
|
|
815
|
-
this.status = status;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function createService(options = {}) {
|
|
820
|
-
const host = options.host || process.env.EA_WEB_HOST || DEFAULT_HOST;
|
|
821
|
-
const port = options.port !== undefined ? options.port
|
|
822
|
-
: process.env.EA_WEB_PORT ? Number(process.env.EA_WEB_PORT) : DEFAULT_PORT;
|
|
823
|
-
const searchRoots = options.searchRoots || defaultSearchRoots(options.root);
|
|
824
|
-
const staticDir = options.staticDir || path.join(REPO_ROOT, 'web');
|
|
825
|
-
const undoDepth = options.undoDepth || DEFAULT_UNDO_DEPTH;
|
|
826
|
-
const mcpAdapter = options.mcpAdapter || createMcpAdapter(options.mcp || {});
|
|
827
|
-
// 布局侧车(M1-S2,2026-09-03 修订:默认按项目隔离):坐标独立持久化,与图谱 JSON 物理隔离。
|
|
828
|
-
// 默认落在各项目自己的 <projectRoot>/design/KG/ea-layouts/<view_id>.json;
|
|
829
|
-
// layoutRoot 选项 / EA_LAYOUT_ROOT 环境变量可显式覆盖为集中存储根。
|
|
830
|
-
const layoutStore = options.layoutStore || createLayoutStore({ layoutRoot: options.layoutRoot });
|
|
831
|
-
|
|
832
|
-
const state = {
|
|
833
|
-
projects: new Map(),
|
|
834
|
-
undoStacks: new Map(),
|
|
835
|
-
redoStacks: new Map(),
|
|
836
|
-
writeQueues: new Map(),
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
function refreshProjects() {
|
|
840
|
-
const projects = discoverProjects(searchRoots);
|
|
841
|
-
state.projects = new Map(projects.map((project) => [project.id, project]));
|
|
842
|
-
return projects;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function getProject(id) {
|
|
846
|
-
const project = state.projects.get(id);
|
|
847
|
-
if (!project) {
|
|
848
|
-
throw new HttpError(404, `project not found: ${id}`);
|
|
849
|
-
}
|
|
850
|
-
return project;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function withProjectWriteLock(projectId, fn) {
|
|
854
|
-
const previous = state.writeQueues.get(projectId) || Promise.resolve();
|
|
855
|
-
const next = previous.then(fn, fn);
|
|
856
|
-
state.writeQueues.set(projectId, next.catch(() => {}));
|
|
857
|
-
return next;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function pushUndo(projectId, command) {
|
|
861
|
-
let stack = state.undoStacks.get(projectId);
|
|
862
|
-
if (!stack) {
|
|
863
|
-
stack = [];
|
|
864
|
-
state.undoStacks.set(projectId, stack);
|
|
865
|
-
}
|
|
866
|
-
stack.push(command);
|
|
867
|
-
while (stack.length > undoDepth) {
|
|
868
|
-
stack.shift();
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function popUndo(projectId) {
|
|
873
|
-
const stack = state.undoStacks.get(projectId);
|
|
874
|
-
return stack && stack.length ? stack.pop() : null;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
function pushRedo(projectId, command) {
|
|
878
|
-
let stack = state.redoStacks.get(projectId);
|
|
879
|
-
if (!stack) {
|
|
880
|
-
stack = [];
|
|
881
|
-
state.redoStacks.set(projectId, stack);
|
|
882
|
-
}
|
|
883
|
-
stack.push(command);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function popRedo(projectId) {
|
|
887
|
-
const stack = state.redoStacks.get(projectId);
|
|
888
|
-
return stack && stack.length ? stack.pop() : null;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
async function editProject(projectId, op, payload) {
|
|
892
|
-
const project = getProject(projectId);
|
|
893
|
-
const toolName = EDIT_OP_TOOL_MAP[op];
|
|
894
|
-
if (!toolName) {
|
|
895
|
-
throw new HttpError(400, `unsupported op: ${op} (可用: ${EDIT_OPS.join(', ')})`);
|
|
896
|
-
}
|
|
897
|
-
const args = buildEditArgs(op, payload);
|
|
898
|
-
return withProjectWriteLock(projectId, async () => {
|
|
899
|
-
const release = acquireFileLock(project.graphPath);
|
|
900
|
-
try {
|
|
901
|
-
const beforeDoc = readGraphDocument(project.graphPath);
|
|
902
|
-
backupGraph(project);
|
|
903
|
-
const result = await mcpAdapter.callTool(toolName, args, project.root);
|
|
904
|
-
if (!result.ok) {
|
|
905
|
-
throw new HttpError(400, `编辑失败:${JSON.stringify(result.error || result.payload)}`);
|
|
906
|
-
}
|
|
907
|
-
let command;
|
|
908
|
-
if (op === 'applyMutation') {
|
|
909
|
-
const afterDoc = readGraphDocument(project.graphPath);
|
|
910
|
-
command = { op, kind: 'snapshot', before: beforeDoc, after: afterDoc };
|
|
911
|
-
} else {
|
|
912
|
-
command = {
|
|
913
|
-
op,
|
|
914
|
-
tool: toolName,
|
|
915
|
-
args,
|
|
916
|
-
inverse: deriveInverseCommand(op, args, beforeDoc),
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
pushUndo(projectId, command);
|
|
920
|
-
state.redoStacks.delete(projectId);
|
|
921
|
-
return { ok: true, op, tool: toolName, result: result.payload };
|
|
922
|
-
} finally {
|
|
923
|
-
release();
|
|
924
|
-
}
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
async function undoProject(projectId) {
|
|
929
|
-
const project = getProject(projectId);
|
|
930
|
-
const command = popUndo(projectId);
|
|
931
|
-
if (!command || (!command.inverse && command.kind !== 'snapshot')) {
|
|
932
|
-
throw new HttpError(400, '无可撤销的操作');
|
|
933
|
-
}
|
|
934
|
-
return withProjectWriteLock(projectId, async () => {
|
|
935
|
-
const release = acquireFileLock(project.graphPath);
|
|
936
|
-
try {
|
|
937
|
-
if (command.kind === 'snapshot') {
|
|
938
|
-
atomicWriteFile(project.graphPath, JSON.stringify(command.before, null, 2));
|
|
939
|
-
pushRedo(projectId, command);
|
|
940
|
-
return { ok: true, undone: command.op };
|
|
941
|
-
}
|
|
942
|
-
const result = await mcpAdapter.callTool(command.inverse.tool, command.inverse.args, project.root);
|
|
943
|
-
if (!result.ok) {
|
|
944
|
-
pushUndo(projectId, command);
|
|
945
|
-
throw new HttpError(400, `撤销失败:${JSON.stringify(result.error || result.payload)}`);
|
|
946
|
-
}
|
|
947
|
-
pushRedo(projectId, command);
|
|
948
|
-
return { ok: true, undone: command.op, result: result.payload };
|
|
949
|
-
} finally {
|
|
950
|
-
release();
|
|
951
|
-
}
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
async function redoProject(projectId) {
|
|
956
|
-
const project = getProject(projectId);
|
|
957
|
-
const command = popRedo(projectId);
|
|
958
|
-
if (!command) {
|
|
959
|
-
throw new HttpError(400, '无可重做的操作');
|
|
960
|
-
}
|
|
961
|
-
return withProjectWriteLock(projectId, async () => {
|
|
962
|
-
const release = acquireFileLock(project.graphPath);
|
|
963
|
-
try {
|
|
964
|
-
if (command.kind === 'snapshot') {
|
|
965
|
-
atomicWriteFile(project.graphPath, JSON.stringify(command.after, null, 2));
|
|
966
|
-
pushUndo(projectId, command);
|
|
967
|
-
return { ok: true, redone: command.op };
|
|
968
|
-
}
|
|
969
|
-
const result = await mcpAdapter.callTool(command.tool, command.args, project.root);
|
|
970
|
-
if (!result.ok) {
|
|
971
|
-
pushRedo(projectId, command);
|
|
972
|
-
throw new HttpError(400, `重做失败:${JSON.stringify(result.error || result.payload)}`);
|
|
973
|
-
}
|
|
974
|
-
pushUndo(projectId, command);
|
|
975
|
-
return { ok: true, redone: command.op, result: result.payload };
|
|
976
|
-
} finally {
|
|
977
|
-
release();
|
|
978
|
-
}
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
async function importProject(projectId, text) {
|
|
983
|
-
const project = getProject(projectId);
|
|
984
|
-
if (Buffer.byteLength(text, 'utf8') > MAX_IMPORT_BYTES) {
|
|
985
|
-
throw new HttpError(413, `文件过大:超过 ${MAX_IMPORT_BYTES / 1024 / 1024} MB 上限`);
|
|
986
|
-
}
|
|
987
|
-
let doc;
|
|
988
|
-
try {
|
|
989
|
-
doc = JSON.parse(text);
|
|
990
|
-
} catch (error) {
|
|
991
|
-
throw new HttpError(400, `JSON 解析失败:${error.message}`);
|
|
992
|
-
}
|
|
993
|
-
const errors = validateGraphDocument(doc);
|
|
994
|
-
if (errors.length > 0) {
|
|
995
|
-
throw new HttpError(400, `校验失败:${errors.join('; ')}`);
|
|
996
|
-
}
|
|
997
|
-
return withProjectWriteLock(projectId, async () => {
|
|
998
|
-
const release = acquireFileLock(project.graphPath);
|
|
999
|
-
try {
|
|
1000
|
-
backupGraph(project);
|
|
1001
|
-
atomicWriteFile(project.graphPath, text);
|
|
1002
|
-
state.undoStacks.delete(projectId);
|
|
1003
|
-
state.redoStacks.delete(projectId);
|
|
1004
|
-
return { ok: true, elements: doc.elements.length, relationships: doc.relationships.length, views: doc.views.length };
|
|
1005
|
-
} finally {
|
|
1006
|
-
release();
|
|
1007
|
-
}
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
async function handle(req, res) {
|
|
1012
|
-
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
1013
|
-
let pathname;
|
|
1014
|
-
try {
|
|
1015
|
-
pathname = decodeURIComponent(url.pathname);
|
|
1016
|
-
} catch {
|
|
1017
|
-
return sendJson(res, 400, { error: '非法的 URL 编码' });
|
|
1018
|
-
}
|
|
1019
|
-
try {
|
|
1020
|
-
if (req.method === 'GET' && (pathname === '/' || pathname === '/index.html')) {
|
|
1021
|
-
return serveStatic(res, path.join(staticDir, 'index.html'), staticDir);
|
|
1022
|
-
}
|
|
1023
|
-
if (req.method === 'GET' && (pathname === '/app.js' || pathname === '/style.css')) {
|
|
1024
|
-
return serveStatic(res, path.join(staticDir, pathname.slice(1)), staticDir);
|
|
1025
|
-
}
|
|
1026
|
-
// 本地 vendor 静态资源(如 AntV G6 v5:/vendor/g6.min.js);路径穿越由 serveStatic 防护。
|
|
1027
|
-
if (req.method === 'GET' && pathname.startsWith('/vendor/')) {
|
|
1028
|
-
return serveStatic(res, path.join(staticDir, pathname.slice(1)), staticDir);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
if (req.method === 'GET' && pathname === '/api/projects') {
|
|
1032
|
-
refreshProjects();
|
|
1033
|
-
const projects = [...state.projects.values()].map(computeStatus);
|
|
1034
|
-
return sendJson(res, 200, { projects });
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
const m = pathname.match(/^\/api\/projects\/([A-Za-z0-9]+)(\/.*)?$/);
|
|
1038
|
-
if (m) {
|
|
1039
|
-
const projectId = m[1];
|
|
1040
|
-
const rest = m[2] || '';
|
|
1041
|
-
// await 使异步路由内抛出的 HttpError 能被上方 catch 捕获并转为 JSON 错误响应,
|
|
1042
|
-
// 而非未处理的 Promise 拒绝(进程崩溃)。
|
|
1043
|
-
return await handleProjectRoute(req, res, projectId, rest, url);
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
return sendJson(res, 404, { error: `not found: ${pathname}` });
|
|
1047
|
-
} catch (error) {
|
|
1048
|
-
const status = error instanceof HttpError ? error.status : 500;
|
|
1049
|
-
if (status === 500) {
|
|
1050
|
-
console.error('[ea-web-service]', error);
|
|
1051
|
-
}
|
|
1052
|
-
return sendJson(res, status, { error: error.message || String(error) });
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
async function handleProjectRoute(req, res, projectId, rest) {
|
|
1057
|
-
if (rest === '' || rest === '/') {
|
|
1058
|
-
return sendJson(res, 200, { project: computeStatus(getProject(projectId)) });
|
|
1059
|
-
}
|
|
1060
|
-
if (req.method === 'POST' && rest === '/select') {
|
|
1061
|
-
return sendJson(res, 200, { project: computeStatus(getProject(projectId)) });
|
|
1062
|
-
}
|
|
1063
|
-
const contextMatch = rest.match(/^\/context\/([A-Za-z0-9._-]+)$/);
|
|
1064
|
-
if (req.method === 'GET' && contextMatch) {
|
|
1065
|
-
const project = getProject(projectId);
|
|
1066
|
-
const result = await mcpAdapter.callTool('getIntentElementContext', { elementId: contextMatch[1] }, project.root);
|
|
1067
|
-
if (result.ok) {
|
|
1068
|
-
return sendJson(res, 200, { elementId: contextMatch[1], context: result.payload });
|
|
1069
|
-
}
|
|
1070
|
-
return sendJson(res, 502, { error: (result.error && result.error.message) || 'getIntentElementContext failed' });
|
|
1071
|
-
}
|
|
1072
|
-
if (req.method === 'GET' && rest === '/status') {
|
|
1073
|
-
return sendJson(res, 200, computeStatus(getProject(projectId)));
|
|
1074
|
-
}
|
|
1075
|
-
if (req.method === 'GET' && rest === '/views') {
|
|
1076
|
-
const project = getProject(projectId);
|
|
1077
|
-
const doc = readGraphDocument(project.graphPath);
|
|
1078
|
-
const views = (doc.views || []).map((view) => ({
|
|
1079
|
-
view_id: view.view_id,
|
|
1080
|
-
view_name: view.view_name,
|
|
1081
|
-
parent_element_id: view.parent_element_id || null,
|
|
1082
|
-
element_count: (view.included_elements || []).length,
|
|
1083
|
-
relationship_count: (view.included_relationships || []).length,
|
|
1084
|
-
}));
|
|
1085
|
-
return sendJson(res, 200, { project: project.id, views });
|
|
1086
|
-
}
|
|
1087
|
-
const viewMatch = rest.match(/^\/views\/([A-Za-z0-9._-]+)\/graph$/);
|
|
1088
|
-
if (req.method === 'GET' && viewMatch) {
|
|
1089
|
-
const project = getProject(projectId);
|
|
1090
|
-
const doc = readGraphDocument(project.graphPath);
|
|
1091
|
-
const graph = buildViewGraph(doc, viewMatch[1]);
|
|
1092
|
-
if (!graph) {
|
|
1093
|
-
throw new HttpError(404, `view not found: ${viewMatch[1]}`);
|
|
1094
|
-
}
|
|
1095
|
-
return sendJson(res, 200, { project: { id: project.id, name: project.name }, ...graph });
|
|
1096
|
-
}
|
|
1097
|
-
// 布局侧车(M1-S2,加法端点):GET 返回按当前成员合并后的坐标全集,
|
|
1098
|
-
// PUT 按当前文档计算成员身份签名后原子写入侧车;坐标永不进入图谱 JSON。
|
|
1099
|
-
const layoutMatch = rest.match(/^\/views\/([A-Za-z0-9._-]+)\/layout$/);
|
|
1100
|
-
if (layoutMatch) {
|
|
1101
|
-
const project = getProject(projectId);
|
|
1102
|
-
const doc = readGraphDocument(project.graphPath);
|
|
1103
|
-
const view = (doc.views || []).find((entry) => entry.view_id === layoutMatch[1]);
|
|
1104
|
-
if (!view) {
|
|
1105
|
-
throw new HttpError(404, `view not found: ${layoutMatch[1]}`);
|
|
1106
|
-
}
|
|
1107
|
-
if (req.method === 'GET') {
|
|
1108
|
-
const layout = layoutStore.mergeLayout({ project, view });
|
|
1109
|
-
return sendJson(res, 200, { project: project.id, view_id: view.view_id, ...layout });
|
|
1110
|
-
}
|
|
1111
|
-
if (req.method === 'PUT') {
|
|
1112
|
-
const body = await readJsonBody(req, MAX_BODY_BYTES);
|
|
1113
|
-
try {
|
|
1114
|
-
const result = layoutStore.putLayout({
|
|
1115
|
-
project,
|
|
1116
|
-
view,
|
|
1117
|
-
elements: body && body.elements,
|
|
1118
|
-
});
|
|
1119
|
-
return sendJson(res, 200, { ok: true, project: project.id, ...result });
|
|
1120
|
-
} catch (error) {
|
|
1121
|
-
throw new HttpError(400, error.message);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
throw new HttpError(405, `method not allowed: ${req.method}`);
|
|
1125
|
-
}
|
|
1126
|
-
if (req.method === 'GET' && rest === '/export') {
|
|
1127
|
-
const project = getProject(projectId);
|
|
1128
|
-
const text = fs.readFileSync(project.graphPath, 'utf8');
|
|
1129
|
-
res.writeHead(200, {
|
|
1130
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
1131
|
-
'Content-Disposition': `attachment; filename="${project.name || 'SystemArchitecture'}.json"`,
|
|
1132
|
-
});
|
|
1133
|
-
return res.end(text);
|
|
1134
|
-
}
|
|
1135
|
-
if (req.method === 'POST' && rest === '/import') {
|
|
1136
|
-
const text = await readBody(req, MAX_IMPORT_BYTES);
|
|
1137
|
-
return sendJson(res, 200, await importProject(projectId, text));
|
|
1138
|
-
}
|
|
1139
|
-
if (req.method === 'POST' && rest === '/search') {
|
|
1140
|
-
const body = await readJsonBody(req, MAX_BODY_BYTES);
|
|
1141
|
-
const project = getProject(projectId);
|
|
1142
|
-
return sendJson(res, 200, await searchProject(mcpAdapter, project, body));
|
|
1143
|
-
}
|
|
1144
|
-
if (req.method === 'POST' && rest === '/edit') {
|
|
1145
|
-
const body = await readJsonBody(req, MAX_BODY_BYTES);
|
|
1146
|
-
const { op, payload } = body || {};
|
|
1147
|
-
return sendJson(res, 200, await editProject(projectId, op, payload));
|
|
1148
|
-
}
|
|
1149
|
-
if (req.method === 'POST' && rest === '/undo') {
|
|
1150
|
-
return sendJson(res, 200, await undoProject(projectId));
|
|
1151
|
-
}
|
|
1152
|
-
if (req.method === 'POST' && rest === '/redo') {
|
|
1153
|
-
return sendJson(res, 200, await redoProject(projectId));
|
|
1154
|
-
}
|
|
1155
|
-
throw new HttpError(404, `not found: ${rest}`);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
let refreshTimer = null;
|
|
1159
|
-
const watchers = [];
|
|
1160
|
-
const refreshIntervalMs = options.refreshIntervalMs !== undefined ? options.refreshIntervalMs : 5000;
|
|
1161
|
-
|
|
1162
|
-
function debounce(fn, ms) {
|
|
1163
|
-
let timer = null;
|
|
1164
|
-
return () => {
|
|
1165
|
-
clearTimeout(timer);
|
|
1166
|
-
timer = setTimeout(fn, ms);
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
const debouncedRefresh = debounce(() => { try { refreshProjects(); } catch { /* ignore */ } }, 1000);
|
|
1171
|
-
|
|
1172
|
-
function startRefreshing() {
|
|
1173
|
-
refreshProjects();
|
|
1174
|
-
if (refreshTimer) clearInterval(refreshTimer);
|
|
1175
|
-
refreshTimer = setInterval(() => { try { refreshProjects(); } catch { /* ignore */ } }, refreshIntervalMs);
|
|
1176
|
-
for (const root of searchRoots) {
|
|
1177
|
-
try {
|
|
1178
|
-
watchers.push(fs.watch(root, { recursive: true }, debouncedRefresh));
|
|
1179
|
-
} catch {
|
|
1180
|
-
/* watch 不支持则靠轮询兜底 */
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
function stopRefreshing() {
|
|
1186
|
-
if (refreshTimer) {
|
|
1187
|
-
clearInterval(refreshTimer);
|
|
1188
|
-
refreshTimer = null;
|
|
1189
|
-
}
|
|
1190
|
-
for (const watcher of watchers) {
|
|
1191
|
-
try {
|
|
1192
|
-
watcher.close();
|
|
1193
|
-
} catch {
|
|
1194
|
-
/* ignore */
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
watchers.length = 0;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const server = http.createServer(handle);
|
|
1201
|
-
|
|
1202
|
-
function start() {
|
|
1203
|
-
startRefreshing();
|
|
1204
|
-
return new Promise((resolve, reject) => {
|
|
1205
|
-
server.once('error', reject);
|
|
1206
|
-
server.listen(port, host, () => {
|
|
1207
|
-
const address = server.address();
|
|
1208
|
-
resolve({ host, port: address && address.port ? address.port : port });
|
|
1209
|
-
});
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
function stop() {
|
|
1214
|
-
return new Promise((resolve) => {
|
|
1215
|
-
stopRefreshing();
|
|
1216
|
-
server.close(() => resolve());
|
|
1217
|
-
// 主动关闭挂起的连接(含未消费响应体的 keep-alive 连接),保证 stop 必然完成。
|
|
1218
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
1219
|
-
server.closeAllConnections();
|
|
1220
|
-
}
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
return {
|
|
1225
|
-
server,
|
|
1226
|
-
start,
|
|
1227
|
-
stop,
|
|
1228
|
-
refreshProjects,
|
|
1229
|
-
handle,
|
|
1230
|
-
editProject,
|
|
1231
|
-
undoProject,
|
|
1232
|
-
redoProject,
|
|
1233
|
-
importProject,
|
|
1234
|
-
mcpAdapter,
|
|
1235
|
-
layoutStore,
|
|
1236
|
-
state,
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
// ---------------------------------------------------------------------------
|
|
1241
|
-
// HTTP 辅助
|
|
1242
|
-
// ---------------------------------------------------------------------------
|
|
1243
|
-
|
|
1244
|
-
function sendJson(res, status, payload) {
|
|
1245
|
-
const text = JSON.stringify(payload);
|
|
1246
|
-
res.writeHead(status, {
|
|
1247
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
1248
|
-
'Content-Length': Buffer.byteLength(text, 'utf8'),
|
|
1249
|
-
});
|
|
1250
|
-
res.end(text);
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
function serveStatic(res, filePath, baseDir) {
|
|
1254
|
-
const safe = path.resolve(filePath);
|
|
1255
|
-
const base = path.resolve(baseDir || path.dirname(filePath));
|
|
1256
|
-
if (safe !== base && !safe.startsWith(base + path.sep)) {
|
|
1257
|
-
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1258
|
-
return res.end('forbidden');
|
|
1259
|
-
}
|
|
1260
|
-
if (!fs.existsSync(safe)) {
|
|
1261
|
-
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1262
|
-
return res.end('not found');
|
|
1263
|
-
}
|
|
1264
|
-
const ext = path.extname(safe).toLowerCase();
|
|
1265
|
-
const text = fs.readFileSync(safe);
|
|
1266
|
-
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
|
|
1267
|
-
res.end(text);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
function readBody(req, limit) {
|
|
1271
|
-
return new Promise((resolve, reject) => {
|
|
1272
|
-
const chunks = [];
|
|
1273
|
-
let size = 0;
|
|
1274
|
-
req.on('data', (chunk) => {
|
|
1275
|
-
size += chunk.length;
|
|
1276
|
-
if (size > limit) {
|
|
1277
|
-
reject(new HttpError(413, `请求体过大(上限 ${limit} 字节)`));
|
|
1278
|
-
req.destroy();
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
chunks.push(chunk);
|
|
1282
|
-
});
|
|
1283
|
-
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
1284
|
-
req.on('error', reject);
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
async function readJsonBody(req, limit) {
|
|
1289
|
-
const text = await readBody(req, limit);
|
|
1290
|
-
if (!text.trim()) {
|
|
1291
|
-
return {};
|
|
1292
|
-
}
|
|
1293
|
-
try {
|
|
1294
|
-
return JSON.parse(text);
|
|
1295
|
-
} catch (error) {
|
|
1296
|
-
throw new HttpError(400, `JSON 解析失败:${error.message}`);
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
// ---------------------------------------------------------------------------
|
|
1301
|
-
// 入口
|
|
1302
|
-
// ---------------------------------------------------------------------------
|
|
1303
|
-
|
|
1304
|
-
function main() {
|
|
1305
|
-
const argv = process.argv.slice(2);
|
|
1306
|
-
const rootIndex = argv.indexOf('--root');
|
|
1307
|
-
const root = rootIndex >= 0 ? argv[rootIndex + 1] : undefined;
|
|
1308
|
-
const portIndex = argv.indexOf('--port');
|
|
1309
|
-
const port = portIndex >= 0 ? Number(argv[portIndex + 1]) : undefined;
|
|
1310
|
-
const service = createService({ root, port });
|
|
1311
|
-
service.start().then(({ host, port: actualPort }) => {
|
|
1312
|
-
console.log(`ArchGraph 本地 Web 服务已启动:http://${host}:${actualPort}`);
|
|
1313
|
-
}).catch((error) => {
|
|
1314
|
-
console.error('启动失败:', error);
|
|
1315
|
-
process.exit(1);
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
module.exports = {
|
|
1320
|
-
DEFAULT_HOST,
|
|
1321
|
-
DEFAULT_PORT,
|
|
1322
|
-
GRAPH_MARKER,
|
|
1323
|
-
MAX_IMPORT_BYTES,
|
|
1324
|
-
EDIT_OP_TOOL_MAP,
|
|
1325
|
-
EDIT_OPS,
|
|
1326
|
-
REPO_ROOT,
|
|
1327
|
-
HttpError,
|
|
1328
|
-
defaultSearchRoots,
|
|
1329
|
-
discoverProjects,
|
|
1330
|
-
computeStatus,
|
|
1331
|
-
validateGraphDocument,
|
|
1332
|
-
buildViewGraph,
|
|
1333
|
-
searchLocal,
|
|
1334
|
-
searchSemantic,
|
|
1335
|
-
searchContext,
|
|
1336
|
-
hitsFromPayload,
|
|
1337
|
-
buildEditArgs,
|
|
1338
|
-
deriveInverseCommand,
|
|
1339
|
-
createMcpAdapter,
|
|
1340
|
-
normalizeMcpResult,
|
|
1341
|
-
acquireFileLock,
|
|
1342
|
-
createService,
|
|
1343
|
-
readGraphDocument,
|
|
1344
|
-
layerOf,
|
|
1345
|
-
};
|
|
1346
|
-
|
|
1347
|
-
if (require.main === module) {
|
|
1348
|
-
main();
|
|
1349
|
-
}
|