zen-gitsync 2.17.16 → 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 +13 -0
- package/src/ui/public/assets/AgentView-AMFKSDeH.css +1 -0
- package/src/ui/public/assets/AgentView-DQAB0ypS.js +13 -0
- package/src/ui/public/assets/AppVersionBadge-B5VG1_Lu.js +2 -0
- package/src/ui/public/assets/AppVersionBadge-DDYQqoy7.css +1 -0
- package/src/ui/public/assets/BranchSelector-Cj6athRO.js +1 -0
- package/src/ui/public/assets/BranchSelector-F4ibldMN.css +1 -0
- package/src/ui/public/assets/CommitForm-4kI2csgs.css +1 -0
- package/src/ui/public/assets/CommitForm-a2tMsfF9.js +6 -0
- package/src/ui/public/assets/CommonDialog-I-GCqRMC.js +1 -0
- package/src/ui/public/assets/CommonDialog-w6EmBEXv.css +1 -0
- package/src/ui/public/assets/EditorView-DBqhjm84.css +1 -0
- package/src/ui/public/assets/EditorView-wTi53Nxc.js +16 -0
- package/src/ui/public/assets/FileDiffViewer-B7i4BmbH.js +15 -0
- package/src/ui/public/assets/FileDiffViewer-n5ea9qMe.css +1 -0
- package/src/ui/public/assets/{FlowExecutionViewer-Drw4hlJG.js → FlowExecutionViewer-xZjrf8e1.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-DwuSSxJ1.css +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-R0QKmUpG.js +21 -0
- package/src/ui/public/assets/IconButton-B6eqvssJ.js +1 -0
- package/src/ui/public/assets/IconButton-RlXqKOeu.css +1 -0
- package/src/ui/public/assets/LogList-BPqurE9g.js +7 -0
- package/src/ui/public/assets/LogList-XspUhx-C.css +1 -0
- package/src/ui/public/assets/MindmapView-Diq2Cu4O.css +1 -0
- package/src/ui/public/assets/MindmapView-NdFNlkTF.js +2 -0
- package/src/ui/public/assets/MonitorView-DuSZa8a3.js +1 -0
- package/src/ui/public/assets/MonitorView-DwUwJXnY.css +1 -0
- package/src/ui/public/assets/ProjectStartupButton-BrfKVP-w.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-CB8IOg1E.css +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-BI7fRsOQ.js +4 -0
- package/src/ui/public/assets/RemoteManagerDialog-D4WphMtO.css +1 -0
- package/src/ui/public/assets/RemoteRepoCard-B4E8Ud3F.js +7 -0
- package/src/ui/public/assets/RemoteRepoCard-o01IrbVz.css +1 -0
- package/src/ui/public/assets/SourceMapView-B0OzHjxP.js +3 -0
- package/src/ui/public/assets/SourceMapView-CfDydtsu.css +1 -0
- package/src/ui/public/assets/SvgIcon-B2h7B2yF.js +1 -0
- package/src/ui/public/assets/TemplateManager-DO7EPfj8.css +1 -0
- package/src/ui/public/assets/TemplateManager-mIWE3Qf8.js +1 -0
- package/src/ui/public/assets/UserInputNode-BafC-B2U.js +1 -0
- package/src/ui/public/assets/UserInputNode-DDNSarag.css +1 -0
- package/src/ui/public/assets/WorkbenchView-BU2kJCAA.css +1 -0
- package/src/ui/public/assets/WorkbenchView-BseGz8hJ.js +56 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-dg2VUnSQ.js → _plugin-vue_export-helper-Dvuzpv2Y.js} +7 -7
- package/src/ui/public/assets/agentConversations-BEKqkGcm.js +5 -0
- package/src/ui/public/assets/{configStore-DtjAc3w9.js → configStore-CX0FkQ94.js} +1 -1
- package/src/ui/public/assets/element-plus-C1xzgNSR.css +1 -0
- package/src/ui/public/assets/{element-plus-DxBPQwL7.js → element-plus-D7LDHcAn.js} +3 -3
- package/src/ui/public/assets/flow-mindmap-zXXF3qDh.js +139 -0
- package/src/ui/public/assets/gitStore-CMuWWr2d.js +6 -0
- package/src/ui/public/assets/index-CB2cIiHl.js +115 -0
- package/src/ui/public/assets/index-DIU5200G.css +1 -0
- package/src/ui/public/assets/{office-docx-CZXLkCf5.js → office-docx-C8sYW8O1.js} +5 -5
- package/src/ui/public/assets/{office-excel-CBKszjVA.js → office-excel-DpZn5My2.js} +1 -1
- package/src/ui/public/assets/{office-pptx-NeUSnN3k.js → office-pptx-BnYBTwT7.js} +1 -1
- package/src/ui/public/assets/previewSandbox-CxLaNg4X.css +1 -0
- package/src/ui/public/assets/previewSandbox-DxR4iN9P.js +0 -0
- package/src/ui/public/assets/toolsStore-DMCSd287.js +1 -0
- package/src/ui/public/assets/{useThemeObserver-E7Hk50l3.js → useThemeObserver-QpveGpnn.js} +1 -1
- package/src/ui/public/assets/{vendor-DnB9EJa3.css → vendor-C-GCxMMu.css} +1 -1
- package/src/ui/public/assets/{vendor-bdHhLiED.js → vendor-CLmPUj3K.js} +8 -8
- package/src/ui/public/assets/vue-flow-CxnoC7Uq.js +39 -0
- package/src/ui/public/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/src/ui/public/fonts/plus-jakarta-sans-latin-ext.woff2 +0 -0
- package/src/ui/public/fonts/plus-jakarta-sans-latin-italic.woff2 +0 -0
- package/src/ui/public/fonts/plus-jakarta-sans-latin.woff2 +0 -0
- package/src/ui/public/index.html +28 -27
- 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 +94 -10
- package/src/ui/server/routes/workbench/agentChatContext.test.js +171 -0
- package/src/ui/server/routes/workbench/agentChatShared.test.js +33 -3
- 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 +208 -48
- package/src/ui/server/routes/workbench/agentRoutes.test.js +160 -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/server/utils/agentAttachments.js +169 -0
- package/src/ui/server/utils/agentAttachments.test.js +140 -0
- package/src/ui/public/assets/AgentView-DOyN97Ve.js +0 -5
- package/src/ui/public/assets/AgentView-DkM2KXuc.css +0 -1
- package/src/ui/public/assets/AppVersionBadge-CJrDr7GV.css +0 -1
- package/src/ui/public/assets/AppVersionBadge-oON3sAj4.js +0 -2
- package/src/ui/public/assets/BranchSelector-B0eLkAdv.css +0 -1
- package/src/ui/public/assets/BranchSelector-Cpj7eGqY.js +0 -1
- package/src/ui/public/assets/CommitForm-CQ0K8Yy2.css +0 -1
- package/src/ui/public/assets/CommitForm-D_ovYhVw.js +0 -6
- package/src/ui/public/assets/CommonDialog-ChZbqfRF.js +0 -1
- package/src/ui/public/assets/CommonDialog-jP0E9mco.css +0 -1
- package/src/ui/public/assets/EditorView-BPzpI0GV.js +0 -1
- package/src/ui/public/assets/EditorView-DZMLvQds.css +0 -1
- package/src/ui/public/assets/FileDiffViewer-B9EkuKdF.css +0 -1
- package/src/ui/public/assets/FileDiffViewer-B_qEKA9L.js +0 -15
- package/src/ui/public/assets/FlowOrchestrationWorkspace-C7yTD1gj.js +0 -21
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CYGp-PqS.css +0 -1
- package/src/ui/public/assets/IconButton-DEARmMkZ.js +0 -1
- package/src/ui/public/assets/IconButton-tjkOjzkC.css +0 -1
- package/src/ui/public/assets/LogList-BA_gowxE.js +0 -7
- package/src/ui/public/assets/LogList-BlJplIi1.css +0 -1
- package/src/ui/public/assets/MindmapView-8X6z245q.css +0 -1
- package/src/ui/public/assets/MindmapView-K-XcLLXr.js +0 -2
- package/src/ui/public/assets/MonitorView-C0RL5K9W.js +0 -1
- package/src/ui/public/assets/MonitorView-DetlFFXq.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-DC36_vvU.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-QLPltCK5.css +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-BauAeH2w.css +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-Bkqja660.js +0 -4
- package/src/ui/public/assets/RemoteRepoCard-Bsm7cj_H.js +0 -7
- package/src/ui/public/assets/RemoteRepoCard-CG-jARTe.css +0 -1
- package/src/ui/public/assets/SourceMapView-CN4Hkyp4.css +0 -1
- package/src/ui/public/assets/SourceMapView-Y5NKdIk6.js +0 -3
- package/src/ui/public/assets/SvgIcon-D_mFHXP4.js +0 -1
- package/src/ui/public/assets/TemplateManager-BzKVXZhy.js +0 -1
- package/src/ui/public/assets/TemplateManager-DMuG4i3G.css +0 -1
- package/src/ui/public/assets/UserInputNode-BPLK4aYx.js +0 -1
- package/src/ui/public/assets/UserInputNode-BVgQkB2l.css +0 -1
- package/src/ui/public/assets/WorkbenchView-B1BJhWHi.css +0 -1
- package/src/ui/public/assets/WorkbenchView-xbGdiVMQ.js +0 -56
- package/src/ui/public/assets/element-plus-Cwwq-Gje.css +0 -1
- package/src/ui/public/assets/flow-mindmap-BqFb1WO5.js +0 -139
- package/src/ui/public/assets/gitStore-BZo9_zyy.js +0 -6
- package/src/ui/public/assets/index-CHuCYLWm.js +0 -111
- package/src/ui/public/assets/index-CmbatbSR.css +0 -1
- package/src/ui/public/assets/previewSandbox-BTTTXooN.js +0 -0
- package/src/ui/public/assets/previewSandbox-CgdwPcuW.css +0 -1
- package/src/ui/public/assets/vue-flow-fLUQ0_kE.js +0 -39
|
@@ -0,0 +1,444 @@
|
|
|
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
|
+
// 解决什么问题:g ai 过去只知道自己 cwd 那点事。问"我 GitHub 上有些什么仓库""工作台还有
|
|
18
|
+
// 哪些没做完""思维导图都放在哪",它要么答"我看不到",要么现场一条条命令去刨。
|
|
19
|
+
// 数据其实全在本机,只是没人告诉它。
|
|
20
|
+
//
|
|
21
|
+
// 与 envContext.js 的分工(两者**互补、不重叠**):
|
|
22
|
+
// · envContext 服务于**多项目编排台派发的任务**,注入后立刻执行,只覆盖项目与看板;
|
|
23
|
+
// · 这里服务于**智能体页的日常对话**,覆盖七个板块,落盘成文件供按需读取。
|
|
24
|
+
// 共用同一条"摘要内联 + 路径外挂"的口径,细节见 render.js 头注释。
|
|
25
|
+
//
|
|
26
|
+
// ⚠️ 五个设计约束,改动前先读:
|
|
27
|
+
//
|
|
28
|
+
// 1) **跑在服务端,与界面挂载状态无关。**
|
|
29
|
+
// 不是从 pinia store 里抓状态 —— 那要求用户先把七个 tab 都点一遍(见 App.vue:995-1034
|
|
30
|
+
// 的 v-if 懒挂载),没点过的板块永远是空文件;两个 g ui 实例并行还会互相覆盖。
|
|
31
|
+
// 服务端这边 config / tasks / git CLI / os 全在手边,一次 HTTP 都不用。
|
|
32
|
+
//
|
|
33
|
+
// 2) **按板块独立刷新,不做"定时全量重写"。**
|
|
34
|
+
// 七个板块的取数成本差两个数量级:git 状态是本地毫秒级,`gh` / `gitee` 拉列表要联网、
|
|
35
|
+
// 单条命令 25s 超时。统一按一个周期刷,要么本地板块不够新、要么联网板块把机器刷爆。
|
|
36
|
+
// 所以每个板块有**自己的** ttlMs,并提供 refreshSections(ids) 只刷指定的几块。
|
|
37
|
+
// 这一条也顺带解决了"刚刚改过配置"的写放大老问题(见 paths.js 注释)。
|
|
38
|
+
//
|
|
39
|
+
// 3) **绝不抛异常给调用方。**
|
|
40
|
+
// 它挂在对话链路上。某一板块取不到(CLI 没装、目录没了、磁盘忙)只该让那一块降级,
|
|
41
|
+
// 不该让用户这条消息发不出去。每个板块单独 try/catch,写盘失败也只记日志。
|
|
42
|
+
//
|
|
43
|
+
// 4) **绝不阻塞"用户按下发送 → 模型开始吐字"这段。**
|
|
44
|
+
// 读路径(getSnapshot / getBlock)一律不等生成:有就回、过期也**先回旧的**再后台定向刷。
|
|
45
|
+
// 生成只发生在 refreshAll / refreshSections / warm 里。这一条是踩出来的 ——
|
|
46
|
+
// 最初的实现 await 生成,直接挂掉了一个既有回归测试。
|
|
47
|
+
// "实时性"不靠读路径等待来保证,靠两件事:① 各种时机主动刷新(见 agentRoutes 的
|
|
48
|
+
// 刷新端点 + 服务端启动预热),② 注入块里那句"要实时值就跑什么命令"。
|
|
49
|
+
//
|
|
50
|
+
// 5) **IO 全部走注入。**
|
|
51
|
+
// 本模块不 import 任何 workbench 内部模块 —— jobStore.js 在**模块加载时**就会
|
|
52
|
+
// hydrateJobs() 并回写 ~/.zen-gitsync/jobs.json(见 promptParts.js 头注释),
|
|
53
|
+
// import 进来会让单测碰到真实用户数据。调用方(wiring.js)本来就有这些依赖,传进来即可。
|
|
54
|
+
|
|
55
|
+
import path from 'node:path';
|
|
56
|
+
import fs from 'node:fs/promises';
|
|
57
|
+
|
|
58
|
+
import { AI_CONTEXT_DIR } from '../../../../paths.js';
|
|
59
|
+
import { atomicWriteText, ensureDir } from '../../../../fsAtomic.js';
|
|
60
|
+
import logger from '../../utils/logger.js';
|
|
61
|
+
import {
|
|
62
|
+
AI_CONTEXT_SECTIONS,
|
|
63
|
+
INDEX_FILE,
|
|
64
|
+
buildContextBlock,
|
|
65
|
+
formatStamp,
|
|
66
|
+
renderIndexFile,
|
|
67
|
+
renderSectionFile,
|
|
68
|
+
sectionForceTtl,
|
|
69
|
+
sectionMeta,
|
|
70
|
+
sectionTtl,
|
|
71
|
+
} from './render.js';
|
|
72
|
+
import {
|
|
73
|
+
collectCustomCommands,
|
|
74
|
+
collectGit,
|
|
75
|
+
collectMindmap,
|
|
76
|
+
collectRemote,
|
|
77
|
+
collectSystem,
|
|
78
|
+
collectWorkbenchTasks,
|
|
79
|
+
} from './collectors.js';
|
|
80
|
+
|
|
81
|
+
/** 板块 id → 缺依赖时的失败结果(形状与 collector 的成功结果一致) */
|
|
82
|
+
function failedSection(id, message) {
|
|
83
|
+
const meta = sectionMeta(id);
|
|
84
|
+
return {
|
|
85
|
+
id,
|
|
86
|
+
title: meta?.title || id,
|
|
87
|
+
file: meta?.file || `${id}.md`,
|
|
88
|
+
ok: false,
|
|
89
|
+
summary: '',
|
|
90
|
+
lines: [],
|
|
91
|
+
error: String(message ?? '未知错误'),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 建一个快照生成器。
|
|
97
|
+
*
|
|
98
|
+
* @param {object} deps
|
|
99
|
+
* @param {object} deps.configManager 需有 loadConfig / getRecentDirectories
|
|
100
|
+
* @param {Function} deps.getCurrentProjectPath () => string
|
|
101
|
+
* @param {Function} deps.readTasks () => Promise<object[]>
|
|
102
|
+
* @param {Function} deps.readJobs () => Promise<object[]>
|
|
103
|
+
* @param {Function} deps.probeGit (dir) => Promise<state>
|
|
104
|
+
* @param {Function} deps.loadRemoteReposState (provider) => Promise<state>
|
|
105
|
+
* @param {Function} deps.getMonitorOverview () => Promise<overview>
|
|
106
|
+
* @param {Function} deps.listPorts () => Promise<port[]>
|
|
107
|
+
* @param {Function} deps.listMindmapFiles (dir) => Promise<file[]>
|
|
108
|
+
* @param {object} [deps.truthFiles] { tasksFile, jobsFile, configFile } 注入给模型看
|
|
109
|
+
* @param {Function} [deps.now] 便于测试注入时钟
|
|
110
|
+
* @param {string} [deps.dir] 快照目录(默认 ~/.zen-gitsync/ai-context)
|
|
111
|
+
*/
|
|
112
|
+
export function createAiContextSnapshotter({
|
|
113
|
+
configManager,
|
|
114
|
+
getCurrentProjectPath,
|
|
115
|
+
readTasks,
|
|
116
|
+
readJobs,
|
|
117
|
+
probeGit,
|
|
118
|
+
loadRemoteReposState,
|
|
119
|
+
getMonitorOverview,
|
|
120
|
+
listPorts,
|
|
121
|
+
listMindmapFiles,
|
|
122
|
+
truthFiles = {},
|
|
123
|
+
now = () => Date.now(),
|
|
124
|
+
dir = AI_CONTEXT_DIR,
|
|
125
|
+
} = {}) {
|
|
126
|
+
/**
|
|
127
|
+
* 每个板块存**自己的**采集时刻 —— 这正是"按板块刷新"的落点。
|
|
128
|
+
* 共用一个 at 会让"刚刷过的 git"和"十分钟前刷的仓库列表"看起来一样新。
|
|
129
|
+
* @type {Map<string, { result: object, at: number }>|null}
|
|
130
|
+
*/
|
|
131
|
+
let sections = null;
|
|
132
|
+
/** 最近一次落盘时刻(只用于 INDEX 的"本次汇总时间") */
|
|
133
|
+
let persistedAt = 0;
|
|
134
|
+
/** 同一板块的并发生成共享同一个 Promise,避免几个时机同时触发时各拉一次网络 */
|
|
135
|
+
const inflightById = new Map();
|
|
136
|
+
|
|
137
|
+
function logWarn(msg) {
|
|
138
|
+
try { logger.warn(`[ai-context] ${msg}`); } catch { /* 日志失败不影响主链路 */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ensureCache() {
|
|
142
|
+
if (!sections) sections = new Map();
|
|
143
|
+
return sections;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stateOf(id) {
|
|
147
|
+
return sections?.get(id) || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isSectionFresh(id) {
|
|
151
|
+
const st = stateOf(id);
|
|
152
|
+
if (!st) return false;
|
|
153
|
+
return now() - st.at < sectionTtl(id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 强制刷新时的地板:防止"切个面板就刷一次"把联网板块刷爆 */
|
|
157
|
+
function isForceAllowed(id) {
|
|
158
|
+
const st = stateOf(id);
|
|
159
|
+
if (!st) return true;
|
|
160
|
+
const floor = sectionForceTtl(id);
|
|
161
|
+
return floor === 0 || now() - st.at >= floor;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 读配置/最近目录/当前项目 —— 每次刷新读一遍,它们本身很便宜 */
|
|
165
|
+
async function loadCtx() {
|
|
166
|
+
let config = null;
|
|
167
|
+
try {
|
|
168
|
+
config = await configManager?.loadConfig?.();
|
|
169
|
+
} catch (err) {
|
|
170
|
+
logWarn(`读取配置失败,自定义命令与思维导图目录这两块会降级: ${err?.message || err}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let recentDirs = [];
|
|
174
|
+
try {
|
|
175
|
+
recentDirs = (await configManager?.getRecentDirectories?.()) || [];
|
|
176
|
+
} catch (err) {
|
|
177
|
+
logWarn(`读取最近目录失败,项目清单可能不完整: ${err?.message || err}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const repoPath = typeof getCurrentProjectPath === 'function' ? (getCurrentProjectPath() || '') : '';
|
|
181
|
+
const mindmapDirs = Array.isArray(config?.ui?.mindmapDirs) ? config.ui.mindmapDirs : [];
|
|
182
|
+
return { config, recentDirs, repoPath, mindmapDirs };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 板块 id → 取数函数。每个都自己兜异常,返回形状统一的结果。 */
|
|
186
|
+
async function collectOne(id, ctx) {
|
|
187
|
+
try {
|
|
188
|
+
switch (id) {
|
|
189
|
+
case 'git':
|
|
190
|
+
return await collectGit({ repoPath: ctx.repoPath, probe: probeGit });
|
|
191
|
+
case 'github':
|
|
192
|
+
return collectRemote({ provider: 'github', state: await loadRemoteReposState('github') });
|
|
193
|
+
case 'gitee':
|
|
194
|
+
return collectRemote({ provider: 'gitee', state: await loadRemoteReposState('gitee') });
|
|
195
|
+
case 'commands':
|
|
196
|
+
return collectCustomCommands({ config: ctx.config });
|
|
197
|
+
case 'tasks':
|
|
198
|
+
return collectWorkbenchTasks({
|
|
199
|
+
tasks: await readTasks(),
|
|
200
|
+
jobs: await readJobs(),
|
|
201
|
+
recentDirs: ctx.recentDirs,
|
|
202
|
+
});
|
|
203
|
+
case 'system':
|
|
204
|
+
return collectSystem({ overview: await getMonitorOverview(), ports: await listPorts() });
|
|
205
|
+
case 'mindmap':
|
|
206
|
+
return collectMindmap({ dirs: ctx.mindmapDirs, listDir: listMindmapFiles });
|
|
207
|
+
default:
|
|
208
|
+
return failedSection(id, '未知板块');
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
logWarn(`板块 ${id} 取数失败: ${err?.message || err}`);
|
|
212
|
+
return failedSection(id, err?.message || err);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 取一个板块并按 id 去重并发 */
|
|
217
|
+
function runSectionOnce(id, ctx) {
|
|
218
|
+
const existing = inflightById.get(id);
|
|
219
|
+
if (existing) return existing;
|
|
220
|
+
const p = collectOne(id, ctx)
|
|
221
|
+
.then((result) => {
|
|
222
|
+
ensureCache().set(id, { result, at: now() });
|
|
223
|
+
return result;
|
|
224
|
+
})
|
|
225
|
+
.finally(() => { inflightById.delete(id); });
|
|
226
|
+
inflightById.set(id, p);
|
|
227
|
+
return p;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 落盘:只重写**本次刷新的那几个**板块文件,加上 INDEX(INDEX 必须全量重渲染 ——
|
|
232
|
+
* 它是总览,任何一块更新了它都得跟着变)。
|
|
233
|
+
*
|
|
234
|
+
* 写失败**只记日志**:文件可能还是上一轮的内容(头部有时间戳,模型能看出来),
|
|
235
|
+
* 但绝不因为磁盘忙就让用户这条消息发不出去。
|
|
236
|
+
*/
|
|
237
|
+
async function persist({ ids, ctx }) {
|
|
238
|
+
try {
|
|
239
|
+
await ensureDir(dir);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
logWarn(`创建快照目录失败: ${err?.message || err}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const scope = ctx?.repoPath ? `当前项目 ${ctx.repoPath}` : '';
|
|
246
|
+
|
|
247
|
+
const writes = ids.map((id) => {
|
|
248
|
+
const st = stateOf(id);
|
|
249
|
+
if (!st) return Promise.resolve();
|
|
250
|
+
const text = renderSectionFile({
|
|
251
|
+
id,
|
|
252
|
+
title: st.result.title,
|
|
253
|
+
generatedAt: formatStamp(new Date(st.at)),
|
|
254
|
+
// 「覆盖范围」只给 git 板块:其余六块(远程仓库 / 自定义命令 / 任务 / 系统 / 思维导图)
|
|
255
|
+
// 讲的是整机与全账号的事,顶上写一句"覆盖范围: 当前项目 X"是错的 ——
|
|
256
|
+
// 模型会以为那份 65 个仓库的清单只属于当前项目。
|
|
257
|
+
scope: id === 'git' ? scope : '',
|
|
258
|
+
lines: st.result.lines,
|
|
259
|
+
error: st.result.ok ? '' : st.result.error,
|
|
260
|
+
});
|
|
261
|
+
return atomicWriteText(path.join(dir, st.result.file), text).catch(err => {
|
|
262
|
+
logWarn(`写入 ${st.result.file} 失败: ${err?.message || err}`);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
persistedAt = now();
|
|
267
|
+
|
|
268
|
+
writes.push(
|
|
269
|
+
atomicWriteText(
|
|
270
|
+
path.join(dir, INDEX_FILE),
|
|
271
|
+
// INDEX 不写「覆盖范围」:它汇总的是七个不同范围的板块,没有统一的覆盖范围。
|
|
272
|
+
renderIndexFile({
|
|
273
|
+
generatedAt: formatStamp(new Date(persistedAt)),
|
|
274
|
+
results: orderedResults(),
|
|
275
|
+
}),
|
|
276
|
+
).catch(err => logWarn(`写入 ${INDEX_FILE} 失败: ${err?.message || err}`)),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
await Promise.all(writes);
|
|
280
|
+
await pruneUnknownFiles(new Set([INDEX_FILE, ...AI_CONTEXT_SECTIONS.map(s => s.file)]));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 清掉目录里不属于任何已知板块的 .md。
|
|
285
|
+
* 只删 .md —— 目录是我们自己生成的,但万一用户往里放了别的东西,不碰。
|
|
286
|
+
* 典型场景:某板块以后被下线,它的旧文件不该继续被模型读到。
|
|
287
|
+
*/
|
|
288
|
+
async function pruneUnknownFiles(keep) {
|
|
289
|
+
try {
|
|
290
|
+
const entries = await fs.readdir(dir).catch(() => []);
|
|
291
|
+
await Promise.all(entries.map(async (name) => {
|
|
292
|
+
if (!name.toLowerCase().endsWith('.md')) return;
|
|
293
|
+
if (keep.has(name)) return;
|
|
294
|
+
await fs.unlink(path.join(dir, name)).catch(() => {});
|
|
295
|
+
}));
|
|
296
|
+
} catch (err) {
|
|
297
|
+
logWarn(`清理陈旧快照文件失败: ${err?.message || err}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 按固定顺序取出已缓存的板块结果(带各自的 collectedAt,给注入块与 INDEX 用) */
|
|
302
|
+
function orderedResults() {
|
|
303
|
+
const out = [];
|
|
304
|
+
for (const s of AI_CONTEXT_SECTIONS) {
|
|
305
|
+
const st = stateOf(s.id);
|
|
306
|
+
if (!st) continue;
|
|
307
|
+
out.push({ ...st.result, collectedAt: formatStamp(new Date(st.at)), at: st.at });
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 定向刷新。
|
|
314
|
+
*
|
|
315
|
+
* @param {string[]} [ids] 要刷的板块 id;不传 = 全部
|
|
316
|
+
* @param {object} [options]
|
|
317
|
+
* @param {boolean} [options.force] true 时忽略软 TTL;但仍受各自 forceTtlMs 地板限制
|
|
318
|
+
* (联网板块最多每 60s 拉一次,不受前端点击频率摆布)
|
|
319
|
+
* @returns {Promise<object>} getState() 的结果
|
|
320
|
+
*/
|
|
321
|
+
async function refreshSections(ids, { force = false } = {}) {
|
|
322
|
+
const known = AI_CONTEXT_SECTIONS.map(s => s.id);
|
|
323
|
+
const wanted = (Array.isArray(ids) && ids.length > 0 ? ids : known).filter(id => known.includes(id));
|
|
324
|
+
const target = wanted.filter(id => (force ? isForceAllowed(id) : !isSectionFresh(id)));
|
|
325
|
+
if (target.length === 0) return getState();
|
|
326
|
+
|
|
327
|
+
const ctx = await loadCtx();
|
|
328
|
+
await Promise.all(target.map(id => runSectionOnce(id, ctx)));
|
|
329
|
+
await persist({ ids: target, ctx });
|
|
330
|
+
return getState();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** 全量刷新(仍按各板块 TTL 判定是否需要真跑) */
|
|
334
|
+
function refreshAll({ force = false } = {}) {
|
|
335
|
+
return refreshSections(AI_CONTEXT_SECTIONS.map(s => s.id), { force });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 取快照 —— **永不阻塞**。
|
|
340
|
+
*
|
|
341
|
+
* 为什么不在这里等生成:生成要跑 git 探测 + `gh` / `gitee` 拉列表(单条 25s 超时) +
|
|
342
|
+
* PowerShell 查磁盘。串在"用户按下发送"和"模型开始吐字"之间,首字节就被拖十几秒。
|
|
343
|
+
*
|
|
344
|
+
* 口径:有就回(不管新旧),同时把**过期的板块**推到后台去定向刷。
|
|
345
|
+
* 冷启动(一份都没有)时回 null —— 调用方 getBlock 会退化成"只给实时查询方法"。
|
|
346
|
+
*/
|
|
347
|
+
function getSnapshot() {
|
|
348
|
+
const stale = AI_CONTEXT_SECTIONS.map(s => s.id).filter(id => !isSectionFresh(id));
|
|
349
|
+
if (stale.length > 0 && sections && sections.size > 0) {
|
|
350
|
+
// 已经有内容了 → 后台补刷过期的,本轮先用旧的
|
|
351
|
+
refreshSections(stale).catch(() => {});
|
|
352
|
+
}
|
|
353
|
+
const results = orderedResults();
|
|
354
|
+
if (results.length === 0) return Promise.resolve(null);
|
|
355
|
+
return Promise.resolve({
|
|
356
|
+
generatedAt: persistedAt ? formatStamp(new Date(persistedAt)) : '',
|
|
357
|
+
results,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 主动预热/刷新。给"服务端刚起来""用户切到了某个面板"这类时机调用。
|
|
363
|
+
* 不关心返回值,失败只记日志(refreshSections 内部已兜)。
|
|
364
|
+
*/
|
|
365
|
+
function warm({ sections: ids, force = false } = {}) {
|
|
366
|
+
return refreshSections(ids, { force }).catch((err) => {
|
|
367
|
+
logWarn(`预热失败: ${err?.message || err}`);
|
|
368
|
+
return getState();
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 取注入给模型的上下文块。
|
|
374
|
+
*
|
|
375
|
+
* **永远返回非空**:即使一份快照都还没生成,块里也有"七个板块各自怎么查实时值"+
|
|
376
|
+
* 快照目录 + 真相源路径 —— 这几样不需要取数,却是真正扛实时性的部分。
|
|
377
|
+
* 所以这个函数不会抛、也不会回空串,调用方可以无条件注入。
|
|
378
|
+
*
|
|
379
|
+
* @param {{ locale?: string }} [options]
|
|
380
|
+
* @returns {Promise<string>}
|
|
381
|
+
*/
|
|
382
|
+
async function getBlock({ locale = 'zh' } = {}) {
|
|
383
|
+
const snap = await getSnapshot();
|
|
384
|
+
return buildContextBlock({
|
|
385
|
+
generatedAt: snap?.generatedAt || '',
|
|
386
|
+
results: snap?.results || [],
|
|
387
|
+
dirPath: dir,
|
|
388
|
+
truthFiles,
|
|
389
|
+
locale,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** 当前各板块的状态(给刷新端点回执 / 排查用) */
|
|
394
|
+
function getState() {
|
|
395
|
+
return {
|
|
396
|
+
dir,
|
|
397
|
+
persistedAt: persistedAt ? formatStamp(new Date(persistedAt)) : null,
|
|
398
|
+
sections: AI_CONTEXT_SECTIONS.map((s) => {
|
|
399
|
+
const st = stateOf(s.id);
|
|
400
|
+
return {
|
|
401
|
+
id: s.id,
|
|
402
|
+
title: s.title,
|
|
403
|
+
file: s.file,
|
|
404
|
+
ttlMs: s.ttlMs,
|
|
405
|
+
forceTtlMs: s.forceTtlMs,
|
|
406
|
+
collectedAt: st ? formatStamp(new Date(st.at)) : null,
|
|
407
|
+
ageMs: st ? now() - st.at : null,
|
|
408
|
+
fresh: isSectionFresh(s.id),
|
|
409
|
+
ok: st ? st.result.ok : null,
|
|
410
|
+
summary: st ? st.result.summary : '',
|
|
411
|
+
};
|
|
412
|
+
}),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** 让指定板块(默认全部)的缓存立即失效 */
|
|
417
|
+
function invalidate(ids) {
|
|
418
|
+
if (!sections) return;
|
|
419
|
+
if (Array.isArray(ids) && ids.length > 0) {
|
|
420
|
+
for (const id of ids) sections.delete(id);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
sections.clear();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* 原样取某个板块的缓存结果(**含结构化 data**),不做任何刷新。
|
|
428
|
+
*
|
|
429
|
+
* 为什么不用 getSnapshot():那个函数会顺手把**其余过期的板块**推到后台去刷 ——
|
|
430
|
+
* 对"打开智能体页聊天"是对的(用户接下来可能问任何一块),但 envContext 只在
|
|
431
|
+
* 派发任务时取一次 tasks 板块,没理由因此去拉 gh/gitee。取不到就回 null,
|
|
432
|
+
* 调用方自己降级。
|
|
433
|
+
*
|
|
434
|
+
* @param {string} id 板块 id
|
|
435
|
+
* @returns {object|null}
|
|
436
|
+
*/
|
|
437
|
+
function getSectionResult(id) {
|
|
438
|
+
return stateOf(id)?.result || null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return { getSnapshot, getBlock, refreshAll, refreshSections, warm, getState, invalidate, getSectionResult };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export { AI_CONTEXT_SECTIONS, INDEX_FILE };
|