zen-gitsync 2.17.17 → 2.17.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/paths.js +10 -0
- package/src/ui/public/assets/{AgentView-CXZUWLPL.css → AgentView-AMFKSDeH.css} +1 -1
- package/src/ui/public/assets/AgentView-DQAB0ypS.js +13 -0
- package/src/ui/public/assets/{AppVersionBadge-D70AjIBo.js → AppVersionBadge-B5VG1_Lu.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-t3fnGS_5.js → BranchSelector-Cj6athRO.js} +1 -1
- package/src/ui/public/assets/{CommitForm-DmlTnTwc.js → CommitForm-a2tMsfF9.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-CecgvEzd.js → CommonDialog-I-GCqRMC.js} +1 -1
- package/src/ui/public/assets/EditorView-wTi53Nxc.js +16 -0
- package/src/ui/public/assets/{FileDiffViewer-DH8Y2lUO.js → FileDiffViewer-B7i4BmbH.js} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-Cie2_TG4.js → FlowExecutionViewer-xZjrf8e1.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-Bbf8X1KP.js → FlowOrchestrationWorkspace-R0QKmUpG.js} +1 -1
- package/src/ui/public/assets/{IconButton-BEiZI4F6.js → IconButton-B6eqvssJ.js} +1 -1
- package/src/ui/public/assets/{LogList-DvuC03Tx.js → LogList-BPqurE9g.js} +1 -1
- package/src/ui/public/assets/{MindmapView-B6rITyNs.js → MindmapView-NdFNlkTF.js} +1 -1
- package/src/ui/public/assets/{MonitorView-q92EZvay.js → MonitorView-DuSZa8a3.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-RiIi8TwW.js → ProjectStartupButton-BrfKVP-w.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-qtBotYAQ.js → RemoteManagerDialog-BI7fRsOQ.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-DvtzfwHV.js → RemoteRepoCard-B4E8Ud3F.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-EEWAmy4U.js → SourceMapView-B0OzHjxP.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-Bkc_oGUI.js → SvgIcon-B2h7B2yF.js} +1 -1
- package/src/ui/public/assets/{TemplateManager-9e4amMlI.js → TemplateManager-mIWE3Qf8.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-4FoJblJi.js → UserInputNode-BafC-B2U.js} +1 -1
- package/src/ui/public/assets/{WorkbenchView-BFaTinH8.js → WorkbenchView-BseGz8hJ.js} +5 -5
- package/src/ui/public/assets/{_plugin-vue_export-helper-BLGowG5t.js → _plugin-vue_export-helper-Dvuzpv2Y.js} +4 -4
- package/src/ui/public/assets/agentConversations-BEKqkGcm.js +5 -0
- package/src/ui/public/assets/{configStore-BUXv5i5j.js → configStore-CX0FkQ94.js} +1 -1
- package/src/ui/public/assets/{gitStore-BGbEYd0U.js → gitStore-CMuWWr2d.js} +1 -1
- package/src/ui/public/assets/{index-Di5cimoI.js → index-CB2cIiHl.js} +12 -12
- package/src/ui/public/assets/{previewSandbox-DEQpyVNu.js → previewSandbox-DxR4iN9P.js} +0 -0
- package/src/ui/public/index.html +10 -10
- package/src/ui/server/index.js +28 -2
- package/src/ui/server/routes/aiContext/collectors.js +360 -0
- package/src/ui/server/routes/aiContext/collectors.test.js +297 -0
- package/src/ui/server/routes/aiContext/index.js +444 -0
- package/src/ui/server/routes/aiContext/index.test.js +433 -0
- package/src/ui/server/routes/aiContext/jobRefresh.js +110 -0
- package/src/ui/server/routes/aiContext/jobRefresh.test.js +191 -0
- package/src/ui/server/routes/aiContext/render.js +366 -0
- package/src/ui/server/routes/aiContext/render.test.js +255 -0
- package/src/ui/server/routes/aiContext/wiring.js +80 -0
- package/src/ui/server/routes/fs.js +43 -0
- package/src/ui/server/routes/mindmap.js +36 -24
- package/src/ui/server/routes/monitor.js +44 -35
- package/src/ui/server/routes/remoteRepos.js +57 -48
- package/src/ui/server/routes/workbench/agentChat.js +35 -7
- package/src/ui/server/routes/workbench/agentChatContext.test.js +66 -1
- package/src/ui/server/routes/workbench/agentEngines.js +77 -0
- package/src/ui/server/routes/workbench/agentEngines.test.js +65 -0
- package/src/ui/server/routes/workbench/agentRoutes.js +169 -44
- package/src/ui/server/routes/workbench/agentRoutes.test.js +66 -4
- package/src/ui/server/routes/workbench/agentSessionStore.js +3 -0
- package/src/ui/server/routes/workbench/envContext.js +61 -64
- package/src/ui/server/routes/workbench/envContext.test.js +133 -63
- package/src/ui/server/routes/workbench/index.js +78 -32
- package/src/ui/server/routes/workbench/runExternalTurn.js +279 -0
- package/src/ui/server/routes/workbench/runExternalTurn.test.js +275 -0
- package/src/ui/server/routes/workbench/shared.js +17 -0
- package/src/ui/public/assets/AgentView-Dhs3eKFY.js +0 -7
- package/src/ui/public/assets/EditorView-DVRemzZU.js +0 -16
- package/src/ui/public/assets/agentConversations-BQCIv4u1.js +0 -5
|
Binary file
|
package/src/ui/public/index.html
CHANGED
|
@@ -10,24 +10,24 @@
|
|
|
10
10
|
<meta name="theme-color" content="#141820" media="(prefers-color-scheme: dark)" />
|
|
11
11
|
<title>Zen GitSync</title>
|
|
12
12
|
<!-- 字体改为自托管(@font-face 见 src/main.css),不再依赖 Google Fonts 外链 -->
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-CB2cIiHl.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/element-plus-D7LDHcAn.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/monaco-BScz9v63.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/vendor-CLmPUj3K.js">
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/IconButton-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/assets/configStore-
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-Dvuzpv2Y.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-B2h7B2yF.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/IconButton-B6eqvssJ.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/assets/configStore-CX0FkQ94.js">
|
|
22
22
|
<link rel="modulepreload" crossorigin href="/assets/socket-io-BFqQXqAp.js">
|
|
23
|
-
<link rel="modulepreload" crossorigin href="/assets/gitStore-
|
|
23
|
+
<link rel="modulepreload" crossorigin href="/assets/gitStore-CMuWWr2d.js">
|
|
24
24
|
<link rel="modulepreload" crossorigin href="/assets/useThemeObserver-QpveGpnn.js">
|
|
25
25
|
<link rel="modulepreload" crossorigin href="/assets/flow-mindmap-zXXF3qDh.js">
|
|
26
|
-
<link rel="modulepreload" crossorigin href="/assets/previewSandbox-
|
|
27
|
-
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-
|
|
28
|
-
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-
|
|
26
|
+
<link rel="modulepreload" crossorigin href="/assets/previewSandbox-DxR4iN9P.js">
|
|
27
|
+
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-I-GCqRMC.js">
|
|
28
|
+
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-B7i4BmbH.js">
|
|
29
29
|
<link rel="modulepreload" crossorigin href="/assets/toolsStore-DMCSd287.js">
|
|
30
|
-
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-
|
|
30
|
+
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-mIWE3Qf8.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vue-flow-CxnoC7Uq.js">
|
|
32
32
|
<link rel="stylesheet" crossorigin href="/assets/element-plus-C1xzgNSR.css">
|
|
33
33
|
<link rel="stylesheet" crossorigin href="/assets/monaco-B2lPvkrc.css">
|
package/src/ui/server/index.js
CHANGED
|
@@ -53,6 +53,7 @@ import { registerInstancesRoutes } from './routes/instances.js';
|
|
|
53
53
|
import { registerMonitorRoutes } from './routes/monitor.js';
|
|
54
54
|
import { registerMindmapRoutes } from './routes/mindmap.js';
|
|
55
55
|
import { registerAgentRoutes } from './routes/workbench/agentRoutes.js';
|
|
56
|
+
import { createWorkspaceSnapshotter } from './routes/aiContext/wiring.js';
|
|
56
57
|
import { createInstanceRegistry, getRegistryPath } from './utils/instanceRegistry.js';
|
|
57
58
|
import { createSavePortToFile } from './utils/createSavePortToFile.js';
|
|
58
59
|
import { createOriginGuard, createOriginCheckerFromEnv } from './middleware/originGuard.js';
|
|
@@ -424,12 +425,23 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
424
425
|
// 资源管理器右键菜单(设置 → 通用设置 → 系统集成 的按钮,写 HKCU 注册表)
|
|
425
426
|
registerExplorerContextMenuRoutes({ app });
|
|
426
427
|
|
|
428
|
+
// 工作区状态快照生成器:**整个进程只建这一个实例**,往下注入给两条链路 ——
|
|
429
|
+
// · registerWorkbenchRoutes:多项目编排台派发任务时的「运行环境上下文」(读 tasks 板块)
|
|
430
|
+
// · registerAgentRoutes :智能体页对话的注入块(读全部七个板块)
|
|
431
|
+
// 建两个实例就会各自缓存一份:TTL 不同步、刷新翻倍,还会出现"编排台看到的看板"
|
|
432
|
+
// 与"g ai 看到的看板"新旧不一致。所以先在这里建好,再注入下去。
|
|
433
|
+
const aiContextSnapshotter = createWorkspaceSnapshotter({
|
|
434
|
+
configManager,
|
|
435
|
+
getCurrentProjectPath: () => currentProjectPath,
|
|
436
|
+
});
|
|
437
|
+
|
|
427
438
|
registerWorkbenchRoutes({
|
|
428
439
|
app,
|
|
429
440
|
getCurrentProjectPath: () => currentProjectPath,
|
|
430
441
|
getProjectRoomId: () => projectRoomId,
|
|
431
442
|
io,
|
|
432
|
-
configManager
|
|
443
|
+
configManager,
|
|
444
|
+
getAiContextSnapshotter: () => aiContextSnapshotter
|
|
433
445
|
});
|
|
434
446
|
|
|
435
447
|
// local-file-picker 中间件,提供 /api/fs/* 文件浏览路由
|
|
@@ -467,10 +479,12 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
467
479
|
registerMindmapRoutes({ app });
|
|
468
480
|
|
|
469
481
|
// 智能体:Web 端 AI 编码助手(含工具调用 + 会话持久化)
|
|
482
|
+
// 快照生成器在上面已经建好(要走同一个实例),这里注入进去复用。
|
|
470
483
|
registerAgentRoutes({
|
|
471
484
|
app,
|
|
472
485
|
getCurrentProjectPath: () => currentProjectPath,
|
|
473
|
-
configManager
|
|
486
|
+
configManager,
|
|
487
|
+
snapshotter: aiContextSnapshotter
|
|
474
488
|
});
|
|
475
489
|
|
|
476
490
|
perfMark('全部 API 路由注册完成')
|
|
@@ -586,6 +600,18 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
586
600
|
registerCurrentInstance().catch((e) => {
|
|
587
601
|
console.warn(chalk.yellow(`[instanceRegistry] 启动注册流程失败: ${e?.message || e}`));
|
|
588
602
|
});
|
|
603
|
+
|
|
604
|
+
// 预热「工作区状态快照」(g ai 的上下文)—— 这就是"g ui 刚启动"那个时机。
|
|
605
|
+
//
|
|
606
|
+
// 三个刻意的选择:
|
|
607
|
+
// · 放在 listening 之后而不是注册路由时:那会儿服务还没真的可用,而且启动路径
|
|
608
|
+
// 上多几次 netstat / gh 子进程会拖慢开浏览器。
|
|
609
|
+
// · 再延 2 秒:启动瞬间前端要并发拉一堆接口,别跟它们抢 IO 与网络。
|
|
610
|
+
// · 不 await、失败只记日志:warm() 内部已经吞掉异常,预热失败不该影响服务可用性,
|
|
611
|
+
// 大不了用户第一次打开智能体页时快照是空的(那一轮注入块里仍有实时查询方法)。
|
|
612
|
+
setTimeout(() => {
|
|
613
|
+
aiContextSnapshotter?.warm?.();
|
|
614
|
+
}, 2000).unref?.();
|
|
589
615
|
});
|
|
590
616
|
|
|
591
617
|
// 尝试在可用端口上启动服务器(不等待;listen 事件会驱动后续逻辑)
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// 各板块的取数 + 归纳。
|
|
16
|
+
//
|
|
17
|
+
// 分两层,刻意的:
|
|
18
|
+
// · **IO 层**(probe / listDir / loadRemoteReposState)全部走参数注入,生产传真实实现,
|
|
19
|
+
// 单测传假实现 —— 不联网、不 spawn、不碰真实用户数据(与 remoteRepos 的 *Impl 同一套做法)。
|
|
20
|
+
// · **归纳层**是纯函数:把数据压成「一行摘要 + 若干正文行」。口径在这里唯一实现。
|
|
21
|
+
//
|
|
22
|
+
// 每个 collector 都回同一个形状:
|
|
23
|
+
// { id, title, file, ok, summary, lines, error }
|
|
24
|
+
// 失败时 ok=false 且 error 有值 —— **绝不静默返回空**,因为空会被模型读成"没有"。
|
|
25
|
+
// (agentChat 的系统提示里为 gh/gitee 写了三段就是在防这个误读。)
|
|
26
|
+
|
|
27
|
+
import { AI_CONTEXT_SECTIONS, sectionMeta, oneLine, truncate } from './render.js';
|
|
28
|
+
import { summarizeProjectTasks, buildProjectEntries, TASK_COLUMNS } from '../workbench/projectRegistry.js';
|
|
29
|
+
|
|
30
|
+
const COLUMN_LABELS = { todo: '待处理', doing: '进行中', done: '已完成' };
|
|
31
|
+
|
|
32
|
+
/** 任务清单在文件里最多列几条(超了只给计数,细节让模型去看 tasks.json) */
|
|
33
|
+
export const TASKS_FILE_LIMIT = 60;
|
|
34
|
+
/** 项目清单在文件里最多列几行 */
|
|
35
|
+
export const PROJECTS_FILE_LIMIT = 30;
|
|
36
|
+
/** 端口清单最多列几行 */
|
|
37
|
+
export const PORTS_FILE_LIMIT = 40;
|
|
38
|
+
/** 远程仓库清单最多列几行 */
|
|
39
|
+
export const REPOS_FILE_LIMIT = 80;
|
|
40
|
+
/**
|
|
41
|
+
* 摘要里最多带几个仓库名当"预览"。
|
|
42
|
+
*
|
|
43
|
+
* 为什么摘要不能只写"N 个仓库":用户问的就是"我的 Gitee 有哪些项目"。只给数量,
|
|
44
|
+
* 模型得先想到去读文件才能回答;带上前几个名字,它一眼就知道**答案在手边**,
|
|
45
|
+
* 并且有个由头去读全量清单。名字很短、几乎不变,这点常驻开销换的是"不用猜"。
|
|
46
|
+
*/
|
|
47
|
+
export const REPOS_SUMMARY_PREVIEW = 5;
|
|
48
|
+
|
|
49
|
+
function fail(id, message) {
|
|
50
|
+
const meta = sectionMeta(id);
|
|
51
|
+
return { id, title: meta?.title || id, file: meta?.file || `${id}.md`, ok: false, summary: '', lines: [], error: oneLine(message) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ok(id, summary, lines = [], data = null) {
|
|
55
|
+
const meta = sectionMeta(id);
|
|
56
|
+
const shape = { id, title: meta?.title || id, file: meta?.file || `${id}.md`, ok: true, summary: oneLine(summary), lines, error: '' };
|
|
57
|
+
// `data` 是给**另一个视图**用的结构化数据(目前只有 tasks 板块用):
|
|
58
|
+
// 渲染成文件/注入块的摘要归本模块,但"项目清单 + 各列计数 + 合计"这类数字
|
|
59
|
+
// 还要被 envContext(多项目编排台视角)复用。带上它,两条链路就只有一个口径。
|
|
60
|
+
// 刻意用**纯对象**(Map 转掉)—— 它会跟着结果对象一路传到快照缓存里,
|
|
61
|
+
// 万一哪天被 JSON.stringify 序列化,Map 会静默变成 {}。
|
|
62
|
+
if (data && typeof data === 'object') shape.data = data;
|
|
63
|
+
return shape;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function gb(bytes) {
|
|
67
|
+
const n = Number(bytes);
|
|
68
|
+
if (!Number.isFinite(n) || n <= 0) return '0GB';
|
|
69
|
+
return `${(n / 1024 / 1024 / 1024).toFixed(1)}GB`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── ① 当前项目 Git 状态 ────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* 复用 directoryGitState 的 probeDirectoryGitState(自带 15s TTL 缓存),
|
|
75
|
+
* 不再多 spawn 一次 `git status`:分支/改动数它就够了,文件级清单留给模型自己跑。
|
|
76
|
+
*/
|
|
77
|
+
export async function collectGit({ repoPath = '', probe } = {}) {
|
|
78
|
+
if (!repoPath) return fail('git', '未拿到当前项目路径');
|
|
79
|
+
if (typeof probe !== 'function') return fail('git', '内部错误:缺少 git 状态探测实现');
|
|
80
|
+
|
|
81
|
+
let state;
|
|
82
|
+
try {
|
|
83
|
+
state = await probe(repoPath);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return fail('git', `探测失败: ${err?.message || err}`);
|
|
86
|
+
}
|
|
87
|
+
if (!state) return fail('git', '探测未返回结果');
|
|
88
|
+
if (state.exists === false) return fail('git', `目录不存在: ${repoPath}`);
|
|
89
|
+
if (!state.isGitRepo) {
|
|
90
|
+
return ok('git', `${repoPath} 不是 git 仓库`, [`- 路径: ${repoPath}`, '- 状态: 不是 git 仓库']);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const branch = state.detached ? '(分离 HEAD)' : (state.branch || '(未知分支)');
|
|
94
|
+
const track = state.hasUpstream
|
|
95
|
+
? `领先 ${state.ahead || 0} / 落后 ${state.behind || 0}`
|
|
96
|
+
: '无上游分支';
|
|
97
|
+
const dirty = state.changed || 0;
|
|
98
|
+
const summary = dirty > 0
|
|
99
|
+
? `分支 ${branch},${track},未提交 ${dirty} 个文件`
|
|
100
|
+
: `分支 ${branch},${track},工作区干净`;
|
|
101
|
+
|
|
102
|
+
const lines = [
|
|
103
|
+
`- 路径: ${repoPath}`,
|
|
104
|
+
`- 分支: ${branch}${state.upstream ? ` → ${state.upstream}` : ''}`,
|
|
105
|
+
`- 领先/落后(本地快照,未联网 fetch): 领先 ${state.ahead || 0} / 落后 ${state.behind || 0}`,
|
|
106
|
+
`- 未提交改动: ${dirty} 个文件(已暂存 ${state.staged || 0} / 未暂存 ${state.unstaged || 0} / 未跟踪 ${state.untracked || 0})`,
|
|
107
|
+
'',
|
|
108
|
+
'要文件级清单(具体改了哪些文件)请自己跑 `git status --porcelain`。',
|
|
109
|
+
];
|
|
110
|
+
return ok('git', summary, lines);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── ②③ 远程仓库(GitHub / Gitee) ────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* 纯函数:把 loadRemoteReposState 的结果归纳成板块。
|
|
116
|
+
*
|
|
117
|
+
* 这里严防的三种误读:
|
|
118
|
+
* · 没装 CLI → 必须写成"未检测到 gh",不能是空清单(否则模型答"你没有仓库")
|
|
119
|
+
* · 装了没登录 → 必须点明"未登录",并给出登录命令
|
|
120
|
+
* · 拉取报错 → 必须把原因带上,且说明"清单可能不完整"
|
|
121
|
+
*/
|
|
122
|
+
export function collectRemote({ provider, state } = {}) {
|
|
123
|
+
const id = provider === 'gitee' ? 'gitee' : 'github';
|
|
124
|
+
const label = id === 'gitee' ? 'Gitee' : 'GitHub';
|
|
125
|
+
const cli = id === 'gitee' ? 'gitee' : 'gh';
|
|
126
|
+
if (!state) return fail(id, '未拿到检测结果');
|
|
127
|
+
|
|
128
|
+
if (!state.installed) {
|
|
129
|
+
return ok(id, `未检测到 ${cli} CLI,取不到仓库列表`, [
|
|
130
|
+
`- ${cli} CLI: 未安装(或不在服务端进程的 PATH 里)`,
|
|
131
|
+
`- 前端「远程仓库」页可一键安装/登录;装好后可能需重启服务才能被服务端看见`,
|
|
132
|
+
'',
|
|
133
|
+
'⚠️ 这不是"用户没有仓库",只是取不到。不要据此回答"你没有仓库"。',
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!state.authenticated) {
|
|
138
|
+
return ok(id, `已装 ${cli} 但未登录,取不到仓库列表`, [
|
|
139
|
+
`- ${cli} CLI: 已安装${state.version ? `(${state.version})` : ''}`,
|
|
140
|
+
`- 登录态: 未登录`,
|
|
141
|
+
`- 登录命令: ${cli} auth login`,
|
|
142
|
+
'',
|
|
143
|
+
'⚠️ 这不是"用户没有仓库",只是没登录。引导用户去登录,不要索要 token。',
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const repos = Array.isArray(state.repos) ? state.repos : [];
|
|
148
|
+
const user = state.user || '';
|
|
149
|
+
const head = [
|
|
150
|
+
`- ${cli} CLI: 已安装${state.version ? `(${state.version})` : ''}`,
|
|
151
|
+
`- 登录账号: ${user || '(未能解析出账号名)'}`,
|
|
152
|
+
`- 仓库数: ${repos.length}${state.truncated ? '(已截断,可能更多)' : ''}`,
|
|
153
|
+
];
|
|
154
|
+
if (state.error) head.push(`- ⚠️ 上次拉取报错: ${oneLine(state.error)}(清单可能不完整)`);
|
|
155
|
+
head.push('');
|
|
156
|
+
|
|
157
|
+
const shown = repos.slice(0, REPOS_FILE_LIMIT);
|
|
158
|
+
head.push(`## 仓库清单${repos.length > shown.length ? `(只列前 ${shown.length} 个)` : ''}`, '');
|
|
159
|
+
for (const r of shown) {
|
|
160
|
+
const parts = [r.name || r.fullName || '(无名)'];
|
|
161
|
+
if (r.visibility) parts.push(r.visibility);
|
|
162
|
+
if (r.language) parts.push(r.language);
|
|
163
|
+
if (r.updatedAt) parts.push(`更新于 ${String(r.updatedAt).slice(0, 10)}`);
|
|
164
|
+
if (r.description) parts.push(truncate(r.description, 60));
|
|
165
|
+
head.push(`- ${parts.join(' | ')}`);
|
|
166
|
+
}
|
|
167
|
+
if (shown.length === 0) head.push('(该账号下没有可见仓库)');
|
|
168
|
+
|
|
169
|
+
// 摘要带上前几个仓库名:用户问"我有哪些项目"时,模型不必先读文件才知道自己有没有能力回答
|
|
170
|
+
const preview = repos
|
|
171
|
+
.slice(0, REPOS_SUMMARY_PREVIEW)
|
|
172
|
+
.map(r => r?.name || r?.fullName)
|
|
173
|
+
.filter(Boolean);
|
|
174
|
+
const previewText = preview.length > 0
|
|
175
|
+
? `(如 ${preview.join('、')}${repos.length > preview.length ? ' 等' : ''})`
|
|
176
|
+
: '';
|
|
177
|
+
const summary = `${repos.length} 个仓库${state.truncated ? '(截断)' : ''}`
|
|
178
|
+
+ (user ? `,账号 ${user}` : '')
|
|
179
|
+
+ previewText;
|
|
180
|
+
return ok(id, summary, head);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── ④ 自定义命令 ─────────────────────────────────────────────
|
|
184
|
+
/** 纯函数:读 config.customCommands(src/config.js:117 的唯一落点) */
|
|
185
|
+
export function collectCustomCommands({ config } = {}) {
|
|
186
|
+
const cmds = Array.isArray(config?.customCommands) ? config.customCommands : [];
|
|
187
|
+
const lines = [];
|
|
188
|
+
for (const c of cmds) {
|
|
189
|
+
if (!c) continue;
|
|
190
|
+
const name = c.name || c.command || '(未命名)';
|
|
191
|
+
lines.push(`- ${name}${c.command ? ` → \`${oneLine(c.command)}\`` : ''}${c.description ? ` — ${truncate(c.description, 80)}` : ''}`);
|
|
192
|
+
}
|
|
193
|
+
if (lines.length === 0) lines.push('(没有配置自定义命令)');
|
|
194
|
+
return ok('commands', `共 ${cmds.length} 条`, lines);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── ⑤ 工作台任务 ─────────────────────────────────────────────
|
|
198
|
+
/**
|
|
199
|
+
* 纯函数。统计口径**复用 projectRegistry**:项目条目走 buildProjectEntries、
|
|
200
|
+
* 任务分列走 summarizeProjectTasks —— 看板怎么分列,这里就怎么统计,不另写一份。
|
|
201
|
+
*/
|
|
202
|
+
export function collectWorkbenchTasks({ tasks = [], jobs = [], recentDirs = [] } = {}) {
|
|
203
|
+
const allTasks = Array.isArray(tasks) ? tasks.filter(Boolean) : [];
|
|
204
|
+
const allJobs = Array.isArray(jobs) ? jobs.filter(Boolean) : [];
|
|
205
|
+
const projects = buildProjectEntries({ recentDirs, tasks: allTasks });
|
|
206
|
+
const stats = summarizeProjectTasks(allTasks, allJobs);
|
|
207
|
+
|
|
208
|
+
const total = { total: 0 };
|
|
209
|
+
for (const k of TASK_COLUMNS) total[k] = 0;
|
|
210
|
+
for (const s of stats.values()) {
|
|
211
|
+
total.total += s.total;
|
|
212
|
+
for (const k of TASK_COLUMNS) total[k] += s[k] || 0;
|
|
213
|
+
}
|
|
214
|
+
const runningJobs = allJobs.filter(j => j && (j.status === 'running' || j.status === 'pending')).length;
|
|
215
|
+
|
|
216
|
+
const lines = [];
|
|
217
|
+
lines.push(`## 项目清单(共 ${projects.length} 个)`, '');
|
|
218
|
+
lines.push(`格式: 名称 | 路径 | ${TASK_COLUMNS.map(k => COLUMN_LABELS[k]).join('/')}`);
|
|
219
|
+
for (const p of projects.slice(0, PROJECTS_FILE_LIMIT)) {
|
|
220
|
+
const s = stats.get(p.key) || {};
|
|
221
|
+
lines.push(`- ${p.name || p.key} | ${p.path} | ${TASK_COLUMNS.map(k => s[k] || 0).join('/')}`);
|
|
222
|
+
}
|
|
223
|
+
if (projects.length > PROJECTS_FILE_LIMIT) {
|
|
224
|
+
lines.push(`- …还有 ${projects.length - PROJECTS_FILE_LIMIT} 个未列出`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 任务本体:状态 + 内容。列在这里是为了让模型不用先读 tasks.json 就能答"有哪些进行中的"。
|
|
228
|
+
lines.push('', `## 任务明细(共 ${allTasks.length} 条)`, '');
|
|
229
|
+
const jobsByTask = new Map();
|
|
230
|
+
for (const j of allJobs) {
|
|
231
|
+
if (!j || !j.taskId) continue;
|
|
232
|
+
if (!jobsByTask.has(j.taskId)) jobsByTask.set(j.taskId, []);
|
|
233
|
+
jobsByTask.get(j.taskId).push(j);
|
|
234
|
+
}
|
|
235
|
+
const sorted = [...allTasks].sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
|
|
236
|
+
const shown = sorted.slice(0, TASKS_FILE_LIMIT);
|
|
237
|
+
for (const t of shown) {
|
|
238
|
+
const js = jobsByTask.get(t.id) || [];
|
|
239
|
+
const lastJob = js.length > 0
|
|
240
|
+
? [...js].sort((a, b) => String(b.endedAt || b.startedAt || '').localeCompare(String(a.endedAt || a.startedAt || '')))[0]
|
|
241
|
+
: null;
|
|
242
|
+
const meta = [];
|
|
243
|
+
if (t.projectPath) meta.push(t.projectPath);
|
|
244
|
+
if (lastJob?.status) meta.push(`最近执行 ${lastJob.status}`);
|
|
245
|
+
if (t.updatedAt) meta.push(`更新于 ${String(t.updatedAt).slice(0, 16).replace('T', ' ')}`);
|
|
246
|
+
const desc = t.description || t.desc || '';
|
|
247
|
+
lines.push(`- [${t.title || t.id}]${meta.length ? ` (${meta.join(' · ')})` : ''}${desc ? ` — ${truncate(desc, 160)}` : ''}`);
|
|
248
|
+
}
|
|
249
|
+
if (sorted.length > shown.length) lines.push(`- …还有 ${sorted.length - shown.length} 条未列出(读下面的 tasks.json 看全)`);
|
|
250
|
+
|
|
251
|
+
const summary =
|
|
252
|
+
`全部项目合计 ${total.total} 条 —— ` +
|
|
253
|
+
TASK_COLUMNS.map(k => `${COLUMN_LABELS[k]} ${total[k] || 0}`).join(' / ') +
|
|
254
|
+
(runningJobs > 0 ? `,正在执行 ${runningJobs} 个` : ',当前无执行中的任务');
|
|
255
|
+
|
|
256
|
+
// 结构化部分原样交出去:envContext(编排台视角)要拿它渲染"当前项目标记 + 项目清单",
|
|
257
|
+
// 而不是自己再读一遍 tasks.json / jobs.json。同一份数据、两个视图,口径不可能再分叉。
|
|
258
|
+
return ok('tasks', summary, lines, {
|
|
259
|
+
projects,
|
|
260
|
+
statsByKey: Object.fromEntries(stats),
|
|
261
|
+
total,
|
|
262
|
+
runningJobs,
|
|
263
|
+
taskCount: allTasks.length,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── ⑥ 系统状态 ───────────────────────────────────────────────
|
|
268
|
+
/** 纯函数:吃 monitor 路由的 overview + ports 结果 */
|
|
269
|
+
export function collectSystem({ overview, ports } = {}) {
|
|
270
|
+
if (!overview) return fail('system', '未拿到系统概览');
|
|
271
|
+
const mem = overview.memory || {};
|
|
272
|
+
const cpu = overview.cpu || {};
|
|
273
|
+
const sys = overview.system || {};
|
|
274
|
+
const lines = [
|
|
275
|
+
`- 主机: ${sys.hostname || '(未知)'} · ${sys.platform || ''} ${sys.arch || ''}`,
|
|
276
|
+
`- CPU 占用: ${(Number(cpu.usage) || 0).toFixed(1)}%${cpu.cores ? `(${cpu.cores} 核` : ''}${cpu.model ? ` · ${cpu.model}` : ''}${cpu.cores ? ')' : ''}`,
|
|
277
|
+
`- 内存: ${(Number(mem.usagePercent) || 0).toFixed(1)}%(已用 ${gb(mem.used)} / 共 ${gb(mem.total)})`,
|
|
278
|
+
`- Node: ${sys.nodeVersion || ''}`,
|
|
279
|
+
];
|
|
280
|
+
// getSystemOverview 给的 disks 是 `{ total, free, used, usagePercent, drives: [...] }`
|
|
281
|
+
// (不是数组);这里两种形状都认,免得以后有人改成数组就静默少一段。
|
|
282
|
+
const disks = Array.isArray(overview.disks)
|
|
283
|
+
? overview.disks
|
|
284
|
+
: (Array.isArray(overview.disks?.drives) ? overview.disks.drives : []);
|
|
285
|
+
if (disks.length > 0) {
|
|
286
|
+
lines.push('- 磁盘:');
|
|
287
|
+
for (const d of disks) {
|
|
288
|
+
const pct = Number(d.usagePercent ?? 0);
|
|
289
|
+
lines.push(` - ${d.mount || d.drive || d.path || '?'} 已用 ${gb(d.used)} / 共 ${gb(d.total)}(${Number.isFinite(pct) ? pct.toFixed(1) : '0'}%)`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const list = Array.isArray(ports) ? ports : [];
|
|
294
|
+
lines.push('', `## 监听端口(${list.length} 个)`, '');
|
|
295
|
+
for (const p of list.slice(0, PORTS_FILE_LIMIT)) {
|
|
296
|
+
const pid = p.pid ?? p.PID ?? '';
|
|
297
|
+
const proc = p.process || p.name || '';
|
|
298
|
+
lines.push(`- ${p.protocol || ''} ${p.localAddress || p.address || ''}:${p.port ?? ''} ${pid ? `PID ${pid}` : ''} ${proc}`.trimEnd());
|
|
299
|
+
}
|
|
300
|
+
if (list.length === 0) lines.push('(没有查到监听端口)');
|
|
301
|
+
if (list.length > PORTS_FILE_LIMIT) lines.push(`- …还有 ${list.length - PORTS_FILE_LIMIT} 个未列出`);
|
|
302
|
+
|
|
303
|
+
const summary =
|
|
304
|
+
`CPU ${(Number(cpu.usage) || 0).toFixed(1)}% · 内存 ${(Number(mem.usagePercent) || 0).toFixed(1)}%` +
|
|
305
|
+
(disks.length > 0 ? ` · ${disks.length} 块磁盘` : '') +
|
|
306
|
+
` · ${list.length} 个监听端口`;
|
|
307
|
+
return ok('system', summary, lines);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── ⑦ 思维导图 ───────────────────────────────────────────────
|
|
311
|
+
/**
|
|
312
|
+
* 目录来自 config.ui.mindmapDirs(前端 mindmapStore 的落点)。
|
|
313
|
+
* listDir 走注入:生产用路由里同一份 readdir 实现(见 routes/mindmap.js 的 listMindmapFiles),
|
|
314
|
+
* 保证"快照里数出来的文件"和界面看到的是同一批。
|
|
315
|
+
*/
|
|
316
|
+
export async function collectMindmap({ dirs = [], listDir } = {}) {
|
|
317
|
+
const roots = (Array.isArray(dirs) ? dirs : []).filter(d => typeof d === 'string' && d.trim());
|
|
318
|
+
if (roots.length === 0) {
|
|
319
|
+
return ok('mindmap', '未配置思维导图目录', ['(设置里还没添加思维导图工作区目录)']);
|
|
320
|
+
}
|
|
321
|
+
if (typeof listDir !== 'function') return fail('mindmap', '内部错误:缺少目录列举实现');
|
|
322
|
+
|
|
323
|
+
const lines = [];
|
|
324
|
+
let totalFiles = 0;
|
|
325
|
+
const errors = [];
|
|
326
|
+
for (const dir of roots) {
|
|
327
|
+
let files = [];
|
|
328
|
+
try {
|
|
329
|
+
files = (await listDir(dir)) || [];
|
|
330
|
+
} catch (err) {
|
|
331
|
+
errors.push(`${dir}: ${err?.message || err}`);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
totalFiles += files.length;
|
|
335
|
+
lines.push(`## ${dir}(${files.length} 个)`, '');
|
|
336
|
+
if (files.length === 0) lines.push('(这个目录下没有 .mindmap.json)');
|
|
337
|
+
for (const f of files) {
|
|
338
|
+
const ts = f.mtime ? new Date(f.mtime).toISOString().slice(0, 16).replace('T', ' ') : '';
|
|
339
|
+
lines.push(`- ${f.title || f.name}${ts ? ` | ${ts}` : ''}${f.size ? ` | ${Math.round(f.size / 1024)}KB` : ''}`);
|
|
340
|
+
}
|
|
341
|
+
lines.push('');
|
|
342
|
+
}
|
|
343
|
+
if (errors.length > 0) {
|
|
344
|
+
lines.push('取数失败:', ...errors.map(e => `- ${e}`), '');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 全部目录都读不到 → 整块判失败,不能报成"0 个导图"。
|
|
348
|
+
// 零个导图和"目录都打不开"在摘要里长得一模一样,而后者会被模型读成
|
|
349
|
+
// "用户没有思维导图"——这正是渲染层 renderSectionFile 反复在防的那种误读。
|
|
350
|
+
if (errors.length === roots.length) {
|
|
351
|
+
const failed = fail('mindmap', `全部 ${roots.length} 个目录都读取失败: ${errors[0]}`);
|
|
352
|
+
failed.lines = lines;
|
|
353
|
+
return failed;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const summary = `${roots.length} 个目录 · ${totalFiles} 个导图` + (errors.length > 0 ? `(${errors.length} 个目录读取失败)` : '');
|
|
357
|
+
return ok('mindmap', summary, lines);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export { AI_CONTEXT_SECTIONS };
|