zen-gitsync 2.16.36 → 2.16.38
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/package.json +1 -1
- package/src/ui/public/assets/AgentView-DWZx4tNO.js +4 -0
- package/src/ui/public/assets/AppVersionBadge-CE0OhuMK.js +2 -0
- package/src/ui/public/assets/BranchSelector-DnNH_zRm.js +1 -0
- package/src/ui/public/assets/CommitForm-Da9hF4Ow.js +6 -0
- package/src/ui/public/assets/CommonDialog-ByvhJIxb.js +1 -0
- package/src/ui/public/assets/CommonDialog-Cyh3xu0p.css +1 -0
- package/src/ui/public/assets/EditorView-0UgFqhAH.js +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-CiK6fWX0.js +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BiTuDHu6.js +21 -0
- package/src/ui/public/assets/LogList-BJey2eS5.js +7 -0
- package/src/ui/public/assets/MindmapView-DGbJgFMZ.js +1 -0
- package/src/ui/public/assets/MonitorView-CXrl7ym6.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-Dr8VCQSL.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-pjxWmG7e.js +1 -0
- package/src/ui/public/assets/SourceMapView-DVcv9WM2.js +3 -0
- package/src/ui/public/assets/SvgIcon-BQ6bR7l0.css +1 -0
- package/src/ui/public/assets/SvgIcon-ryKBwC8P.js +1 -0
- package/src/ui/public/assets/UserInputNode-8v5LCzR9.js +1 -0
- package/src/ui/public/assets/WorkbenchView-BN_6Pb8H.js +12 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-C5Vpcpi0.js +17 -0
- package/src/ui/public/assets/configStore-DdYe2Jg3.js +1 -0
- package/src/ui/public/assets/element-plus-Cchqf1s7.js +19 -0
- package/src/ui/public/assets/element-plus-D3Dn7nX0.css +1 -0
- package/src/ui/public/assets/{flow-mindmap-CatiJi_w.js → flow-mindmap-D4bc307Q.js} +1 -1
- package/src/ui/public/assets/index-D-n9_1ON.js +46 -0
- package/src/ui/public/assets/index-DDmVbc4D.css +1 -0
- package/src/ui/public/assets/{monaco-DPQ3OaiL.js → monaco-43aputPh.js} +2 -2
- package/src/ui/public/assets/office-docx-BHL8ws1l.js +31 -0
- package/src/ui/public/assets/{office-excel-BGq5miA9.js → office-excel-CgdzbtOn.js} +2 -2
- package/src/ui/public/assets/{office-pptx-CNefnIUm.js → office-pptx-DyxMcWQh.js} +1 -1
- package/src/ui/public/assets/socket-io-BZIzo1US.js +1 -0
- package/src/ui/public/assets/toolsStore-C0VjU2dD.js +1 -0
- package/src/ui/public/assets/{ts.worker-METxwbDZ.js → ts.worker-Dth06zuC.js} +16 -1
- package/src/ui/public/assets/{vendor-C_cx2RSh.js → vendor-FHXYVNcm.js} +36 -36
- package/src/ui/public/assets/{vue-flow-CBbapQM_.js → vue-flow-3jJr-V2W.js} +2 -2
- package/src/ui/public/index.html +16 -9
- package/src/ui/server/index.js +11 -3
- package/src/ui/server/routes/workbench/agentSessionStore.js +173 -173
- package/src/ui/server/utils/instanceRegistry.js +234 -60
- package/src/ui/server/utils/instanceRegistry.test.js +396 -0
- package/src/ui/public/assets/AgentView-CFP7GbUl.js +0 -4
- package/src/ui/public/assets/AppVersionBadge-CBSY_oq9.js +0 -2
- package/src/ui/public/assets/BranchSelector-Fi2LA9XU.js +0 -1
- package/src/ui/public/assets/CommitForm-CLIleDCk.js +0 -6
- package/src/ui/public/assets/EditorView-CBVzdZmP.js +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-BBkNLVm3.js +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BZqZPDN5.js +0 -21
- package/src/ui/public/assets/LogList-CJnjZAeU.js +0 -7
- package/src/ui/public/assets/MindmapView-BAGMeCsu.js +0 -1
- package/src/ui/public/assets/MonitorView-CsAGCNaD.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-MPOop7ax.js +0 -1
- package/src/ui/public/assets/RemoteRepoCard-B2wpKIp1.js +0 -1
- package/src/ui/public/assets/SourceMapView-FE6FVmmX.js +0 -3
- package/src/ui/public/assets/UserInputNode-BicQgmt7.js +0 -1
- package/src/ui/public/assets/WorkbenchView-DdipCfIx.js +0 -12
- package/src/ui/public/assets/element-plus-CXfbWXaJ.css +0 -1
- package/src/ui/public/assets/element-plus-utMlO-GC.js +0 -27
- package/src/ui/public/assets/index-2NOwDrtr.js +0 -62
- package/src/ui/public/assets/index-BSEyfbfQ.css +0 -1
- package/src/ui/public/assets/office-docx-CUC3NjrQ.js +0 -31
- package/src/ui/public/assets/socket-io-ObqgZMoP.js +0 -1
- /package/src/ui/public/assets/{dagre-BCXBo6I9.js → dagre-97WoLjFt.js} +0 -0
package/src/ui/public/index.html
CHANGED
|
@@ -10,20 +10,27 @@
|
|
|
10
10
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
11
11
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
12
12
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-D-n9_1ON.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-BM3Ffeng.js">
|
|
15
|
-
<link rel="modulepreload" crossorigin href="/assets/monaco-
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/element-plus-
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/flow-
|
|
15
|
+
<link rel="modulepreload" crossorigin href="/assets/monaco-43aputPh.js">
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/element-plus-Cchqf1s7.js">
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-FHXYVNcm.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-C5Vpcpi0.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/socket-io-BZIzo1US.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/vue-flow-3jJr-V2W.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/assets/flow-mindmap-D4bc307Q.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-ryKBwC8P.js">
|
|
23
|
+
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-ByvhJIxb.js">
|
|
24
|
+
<link rel="modulepreload" crossorigin href="/assets/configStore-DdYe2Jg3.js">
|
|
25
|
+
<link rel="modulepreload" crossorigin href="/assets/toolsStore-C0VjU2dD.js">
|
|
21
26
|
<link rel="stylesheet" crossorigin href="/assets/monaco-B2lPvkrc.css">
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/element-plus-
|
|
27
|
+
<link rel="stylesheet" crossorigin href="/assets/element-plus-D3Dn7nX0.css">
|
|
23
28
|
<link rel="stylesheet" crossorigin href="/assets/vendor-BkgN6wPK.css">
|
|
24
29
|
<link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
|
|
25
30
|
<link rel="stylesheet" crossorigin href="/assets/flow-mindmap-DnzM9SlK.css">
|
|
26
|
-
<link rel="stylesheet" crossorigin href="/assets/
|
|
31
|
+
<link rel="stylesheet" crossorigin href="/assets/SvgIcon-BQ6bR7l0.css">
|
|
32
|
+
<link rel="stylesheet" crossorigin href="/assets/CommonDialog-Cyh3xu0p.css">
|
|
33
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DDmVbc4D.css">
|
|
27
34
|
</head>
|
|
28
35
|
<body>
|
|
29
36
|
<div id="app"></div>
|
package/src/ui/server/index.js
CHANGED
|
@@ -580,8 +580,9 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
580
580
|
return;
|
|
581
581
|
}
|
|
582
582
|
|
|
583
|
+
let projectName = '';
|
|
583
584
|
try {
|
|
584
|
-
|
|
585
|
+
projectName = await instanceRegistry._resolveProjectName(currentProjectPath);
|
|
585
586
|
await instanceRegistry.register({
|
|
586
587
|
pid: process.pid,
|
|
587
588
|
port: currentPort,
|
|
@@ -595,9 +596,16 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
595
596
|
return;
|
|
596
597
|
}
|
|
597
598
|
|
|
598
|
-
// 5
|
|
599
|
+
// 5 秒心跳。必须带上完整注册信息:多实例同时启动时 register 的
|
|
600
|
+
// read-modify-write 存在跨进程竞态,本实例的条目可能被后来者整表覆盖掉。
|
|
601
|
+
// 带全信息后,心跳发现条目缺失会把自己补回注册表(自愈,见 instanceRegistry.heartbeat)。
|
|
599
602
|
const heartbeatTimer = setInterval(() => {
|
|
600
|
-
instanceRegistry.heartbeat(process.pid, {
|
|
603
|
+
instanceRegistry.heartbeat(process.pid, {
|
|
604
|
+
port: currentPort,
|
|
605
|
+
projectPath: currentProjectPath,
|
|
606
|
+
projectName,
|
|
607
|
+
hostname: os.hostname()
|
|
608
|
+
})
|
|
601
609
|
.catch((e) => console.warn(chalk.yellow(`[instanceRegistry] 心跳失败: ${e?.message || e}`)));
|
|
602
610
|
}, 5000);
|
|
603
611
|
|
|
@@ -1,173 +1,173 @@
|
|
|
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
|
-
// 智能体对话 session 持久化层。
|
|
16
|
-
//
|
|
17
|
-
// 与 ai-split-sessions 不同,这里存储的是完整 agent 对话(含工具调用)。
|
|
18
|
-
// 消息格式兼容 OpenAI chat completions:
|
|
19
|
-
// - { role: 'system', content: string }
|
|
20
|
-
// - { role: 'user', content: string | content_parts[] }
|
|
21
|
-
// - { role: 'assistant', content: string|null, tool_calls?: [{id,type,function:{name,arguments}}] }
|
|
22
|
-
// - { role: 'tool', tool_call_id, name, content: string }
|
|
23
|
-
//
|
|
24
|
-
// 文件命名: {sessionId}.json,原子写(tmp + rename)。
|
|
25
|
-
// 数据目录: ~/.zen-gitsync/agent-sessions/
|
|
26
|
-
|
|
27
|
-
import fsp from 'fs/promises';
|
|
28
|
-
import path from 'path';
|
|
29
|
-
import { logger } from './shared.js';
|
|
30
|
-
|
|
31
|
-
// 数据目录:与 ai-split-sessions 平级
|
|
32
|
-
export const AGENT_SESSIONS_DIR = path.join(
|
|
33
|
-
path.dirname(path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname.replace(/^\//, ''))))),
|
|
34
|
-
'.zen-gitsync',
|
|
35
|
-
'agent-sessions'
|
|
36
|
-
);
|
|
37
|
-
// 上面路径在 Windows 下不可靠,改用 os.homedir
|
|
38
|
-
import os from 'os';
|
|
39
|
-
export const AGENT_SESSIONS_DIR_FIXED = path.join(os.homedir(), '.zen-gitsync', 'agent-sessions');
|
|
40
|
-
|
|
41
|
-
export const MAX_AGENT_SESSIONS = 200;
|
|
42
|
-
export const AGENT_SESSIONS_KEEP = 100;
|
|
43
|
-
|
|
44
|
-
// 16 字符: 时间戳后 8 位 + 随机 8 位 base36
|
|
45
|
-
export function genAgentSessionId() {
|
|
46
|
-
return `ag-${Date.now().toString(36).slice(-8)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 从第一条 user 消息自动生成标题
|
|
50
|
-
export function autoTitle(messages) {
|
|
51
|
-
for (const m of messages) {
|
|
52
|
-
if (m.role === 'user') {
|
|
53
|
-
let text = '';
|
|
54
|
-
if (typeof m.content === 'string') {
|
|
55
|
-
text = m.content;
|
|
56
|
-
} else if (Array.isArray(m.content)) {
|
|
57
|
-
text = m.content.filter(p => p?.type === 'text').map(p => p.text).join(' ');
|
|
58
|
-
}
|
|
59
|
-
text = text.trim();
|
|
60
|
-
if (text) {
|
|
61
|
-
// 取第一行,截断到 40 字符
|
|
62
|
-
const firstLine = text.split('\n')[0].trim();
|
|
63
|
-
return firstLine.length > 40 ? firstLine.slice(0, 40) + '…' : firstLine;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return '(新会话)';
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function readAgentSessionFile(sessionId) {
|
|
71
|
-
if (!/^ag-[a-z0-9-]{4,32}$/i.test(sessionId)) {
|
|
72
|
-
const err = new Error('非法 sessionId'); err.statusCode = 400; throw err;
|
|
73
|
-
}
|
|
74
|
-
const file = path.join(AGENT_SESSIONS_DIR_FIXED, `${sessionId}.json`);
|
|
75
|
-
try {
|
|
76
|
-
return JSON.parse(await fsp.readFile(file, 'utf-8'));
|
|
77
|
-
} catch (err) {
|
|
78
|
-
if (err.code === 'ENOENT') {
|
|
79
|
-
const e = new Error('会话不存在'); e.statusCode = 404; throw e;
|
|
80
|
-
}
|
|
81
|
-
throw err;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function writeAgentSessionFile(sessionId, data) {
|
|
86
|
-
await fsp.mkdir(AGENT_SESSIONS_DIR_FIXED, { recursive: true });
|
|
87
|
-
const file = path.join(AGENT_SESSIONS_DIR_FIXED, `${sessionId}.json`);
|
|
88
|
-
const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
|
|
89
|
-
await fsp.writeFile(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
90
|
-
await fsp.rename(tmp, file);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function deleteAgentSessionFile(sessionId) {
|
|
94
|
-
if (!/^ag-[a-z0-9-]{4,32}$/i.test(sessionId)) {
|
|
95
|
-
const err = new Error('非法 sessionId'); err.statusCode = 400; throw err;
|
|
96
|
-
}
|
|
97
|
-
const file = path.join(AGENT_SESSIONS_DIR_FIXED, `${sessionId}.json`);
|
|
98
|
-
try {
|
|
99
|
-
await fsp.unlink(file);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
if (err.code === 'ENOENT') {
|
|
102
|
-
const e = new Error('会话不存在'); e.statusCode = 404; throw e;
|
|
103
|
-
}
|
|
104
|
-
throw err;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 列表只读 metadata
|
|
109
|
-
async function listAgentSessionsMeta() {
|
|
110
|
-
await fsp.mkdir(AGENT_SESSIONS_DIR_FIXED, { recursive: true });
|
|
111
|
-
const files = await fsp.readdir(AGENT_SESSIONS_DIR_FIXED);
|
|
112
|
-
const list = [];
|
|
113
|
-
for (const f of files) {
|
|
114
|
-
if (!f.endsWith('.json')) continue;
|
|
115
|
-
const sid = f.slice(0, -5);
|
|
116
|
-
try {
|
|
117
|
-
const file = path.join(AGENT_SESSIONS_DIR_FIXED, f);
|
|
118
|
-
const stat = await fsp.stat(file);
|
|
119
|
-
const data = JSON.parse(await fsp.readFile(file, 'utf-8'));
|
|
120
|
-
list.push({
|
|
121
|
-
sessionId: data.sessionId || sid,
|
|
122
|
-
title: data.title || '(无标题)',
|
|
123
|
-
source: data.source || 'web', // 'web' | 'cli'
|
|
124
|
-
cwd: data.cwd || '',
|
|
125
|
-
model: data.model || '',
|
|
126
|
-
createdAt: data.createdAt || '',
|
|
127
|
-
updatedAt: data.updatedAt || '',
|
|
128
|
-
messageCount: Array.isArray(data.messages) ? data.messages.length : 0,
|
|
129
|
-
size: stat.size
|
|
130
|
-
});
|
|
131
|
-
} catch { /* 跳过坏文件 */ }
|
|
132
|
-
}
|
|
133
|
-
list.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
|
|
134
|
-
return list;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function enforceAgentSessionsRetention() {
|
|
138
|
-
const files = await fsp.readdir(AGENT_SESSIONS_DIR_FIXED).catch(() => []);
|
|
139
|
-
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
140
|
-
if (jsonFiles.length < MAX_AGENT_SESSIONS) return;
|
|
141
|
-
const stats = await Promise.all(jsonFiles.map(async f => {
|
|
142
|
-
const full = path.join(AGENT_SESSIONS_DIR_FIXED, f);
|
|
143
|
-
const s = await fsp.stat(full);
|
|
144
|
-
return { file: full, mtime: s.mtimeMs };
|
|
145
|
-
}));
|
|
146
|
-
stats.sort((a, b) => b.mtime - a.mtime);
|
|
147
|
-
const toDelete = stats.slice(AGENT_SESSIONS_KEEP);
|
|
148
|
-
await Promise.all(toDelete.map(s => fsp.unlink(s.file).catch(() => {})));
|
|
149
|
-
if (toDelete.length > 0) {
|
|
150
|
-
logger.info(`[agent] sessions: cleaned up ${toDelete.length} old files`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// 更新会话标题
|
|
155
|
-
async function renameAgentSession(sessionId, title) {
|
|
156
|
-
const data = await readAgentSessionFile(sessionId);
|
|
157
|
-
data.title = String(title || '').trim() || '(无标题)';
|
|
158
|
-
data.updatedAt = new Date().toISOString();
|
|
159
|
-
await writeAgentSessionFile(sessionId, data);
|
|
160
|
-
return data;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export const agentSessionStore = {
|
|
164
|
-
genSessionId: genAgentSessionId,
|
|
165
|
-
autoTitle,
|
|
166
|
-
read: readAgentSessionFile,
|
|
167
|
-
write: writeAgentSessionFile,
|
|
168
|
-
delete: deleteAgentSessionFile,
|
|
169
|
-
listMeta: listAgentSessionsMeta,
|
|
170
|
-
enforceRetention: enforceAgentSessionsRetention,
|
|
171
|
-
rename: renameAgentSession,
|
|
172
|
-
DIR: AGENT_SESSIONS_DIR_FIXED,
|
|
173
|
-
};
|
|
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
|
+
// 智能体对话 session 持久化层。
|
|
16
|
+
//
|
|
17
|
+
// 与 ai-split-sessions 不同,这里存储的是完整 agent 对话(含工具调用)。
|
|
18
|
+
// 消息格式兼容 OpenAI chat completions:
|
|
19
|
+
// - { role: 'system', content: string }
|
|
20
|
+
// - { role: 'user', content: string | content_parts[] }
|
|
21
|
+
// - { role: 'assistant', content: string|null, tool_calls?: [{id,type,function:{name,arguments}}] }
|
|
22
|
+
// - { role: 'tool', tool_call_id, name, content: string }
|
|
23
|
+
//
|
|
24
|
+
// 文件命名: {sessionId}.json,原子写(tmp + rename)。
|
|
25
|
+
// 数据目录: ~/.zen-gitsync/agent-sessions/
|
|
26
|
+
|
|
27
|
+
import fsp from 'fs/promises';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
import { logger } from './shared.js';
|
|
30
|
+
|
|
31
|
+
// 数据目录:与 ai-split-sessions 平级
|
|
32
|
+
export const AGENT_SESSIONS_DIR = path.join(
|
|
33
|
+
path.dirname(path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname.replace(/^\//, ''))))),
|
|
34
|
+
'.zen-gitsync',
|
|
35
|
+
'agent-sessions'
|
|
36
|
+
);
|
|
37
|
+
// 上面路径在 Windows 下不可靠,改用 os.homedir
|
|
38
|
+
import os from 'os';
|
|
39
|
+
export const AGENT_SESSIONS_DIR_FIXED = path.join(os.homedir(), '.zen-gitsync', 'agent-sessions');
|
|
40
|
+
|
|
41
|
+
export const MAX_AGENT_SESSIONS = 200;
|
|
42
|
+
export const AGENT_SESSIONS_KEEP = 100;
|
|
43
|
+
|
|
44
|
+
// 16 字符: 时间戳后 8 位 + 随机 8 位 base36
|
|
45
|
+
export function genAgentSessionId() {
|
|
46
|
+
return `ag-${Date.now().toString(36).slice(-8)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 从第一条 user 消息自动生成标题
|
|
50
|
+
export function autoTitle(messages) {
|
|
51
|
+
for (const m of messages) {
|
|
52
|
+
if (m.role === 'user') {
|
|
53
|
+
let text = '';
|
|
54
|
+
if (typeof m.content === 'string') {
|
|
55
|
+
text = m.content;
|
|
56
|
+
} else if (Array.isArray(m.content)) {
|
|
57
|
+
text = m.content.filter(p => p?.type === 'text').map(p => p.text).join(' ');
|
|
58
|
+
}
|
|
59
|
+
text = text.trim();
|
|
60
|
+
if (text) {
|
|
61
|
+
// 取第一行,截断到 40 字符
|
|
62
|
+
const firstLine = text.split('\n')[0].trim();
|
|
63
|
+
return firstLine.length > 40 ? firstLine.slice(0, 40) + '…' : firstLine;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return '(新会话)';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readAgentSessionFile(sessionId) {
|
|
71
|
+
if (!/^ag-[a-z0-9-]{4,32}$/i.test(sessionId)) {
|
|
72
|
+
const err = new Error('非法 sessionId'); err.statusCode = 400; throw err;
|
|
73
|
+
}
|
|
74
|
+
const file = path.join(AGENT_SESSIONS_DIR_FIXED, `${sessionId}.json`);
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(await fsp.readFile(file, 'utf-8'));
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.code === 'ENOENT') {
|
|
79
|
+
const e = new Error('会话不存在'); e.statusCode = 404; throw e;
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function writeAgentSessionFile(sessionId, data) {
|
|
86
|
+
await fsp.mkdir(AGENT_SESSIONS_DIR_FIXED, { recursive: true });
|
|
87
|
+
const file = path.join(AGENT_SESSIONS_DIR_FIXED, `${sessionId}.json`);
|
|
88
|
+
const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
|
|
89
|
+
await fsp.writeFile(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
90
|
+
await fsp.rename(tmp, file);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function deleteAgentSessionFile(sessionId) {
|
|
94
|
+
if (!/^ag-[a-z0-9-]{4,32}$/i.test(sessionId)) {
|
|
95
|
+
const err = new Error('非法 sessionId'); err.statusCode = 400; throw err;
|
|
96
|
+
}
|
|
97
|
+
const file = path.join(AGENT_SESSIONS_DIR_FIXED, `${sessionId}.json`);
|
|
98
|
+
try {
|
|
99
|
+
await fsp.unlink(file);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err.code === 'ENOENT') {
|
|
102
|
+
const e = new Error('会话不存在'); e.statusCode = 404; throw e;
|
|
103
|
+
}
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 列表只读 metadata
|
|
109
|
+
async function listAgentSessionsMeta() {
|
|
110
|
+
await fsp.mkdir(AGENT_SESSIONS_DIR_FIXED, { recursive: true });
|
|
111
|
+
const files = await fsp.readdir(AGENT_SESSIONS_DIR_FIXED);
|
|
112
|
+
const list = [];
|
|
113
|
+
for (const f of files) {
|
|
114
|
+
if (!f.endsWith('.json')) continue;
|
|
115
|
+
const sid = f.slice(0, -5);
|
|
116
|
+
try {
|
|
117
|
+
const file = path.join(AGENT_SESSIONS_DIR_FIXED, f);
|
|
118
|
+
const stat = await fsp.stat(file);
|
|
119
|
+
const data = JSON.parse(await fsp.readFile(file, 'utf-8'));
|
|
120
|
+
list.push({
|
|
121
|
+
sessionId: data.sessionId || sid,
|
|
122
|
+
title: data.title || '(无标题)',
|
|
123
|
+
source: data.source || 'web', // 'web' | 'cli'
|
|
124
|
+
cwd: data.cwd || '',
|
|
125
|
+
model: data.model || '',
|
|
126
|
+
createdAt: data.createdAt || '',
|
|
127
|
+
updatedAt: data.updatedAt || '',
|
|
128
|
+
messageCount: Array.isArray(data.messages) ? data.messages.length : 0,
|
|
129
|
+
size: stat.size
|
|
130
|
+
});
|
|
131
|
+
} catch { /* 跳过坏文件 */ }
|
|
132
|
+
}
|
|
133
|
+
list.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
|
|
134
|
+
return list;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function enforceAgentSessionsRetention() {
|
|
138
|
+
const files = await fsp.readdir(AGENT_SESSIONS_DIR_FIXED).catch(() => []);
|
|
139
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
140
|
+
if (jsonFiles.length < MAX_AGENT_SESSIONS) return;
|
|
141
|
+
const stats = await Promise.all(jsonFiles.map(async f => {
|
|
142
|
+
const full = path.join(AGENT_SESSIONS_DIR_FIXED, f);
|
|
143
|
+
const s = await fsp.stat(full);
|
|
144
|
+
return { file: full, mtime: s.mtimeMs };
|
|
145
|
+
}));
|
|
146
|
+
stats.sort((a, b) => b.mtime - a.mtime);
|
|
147
|
+
const toDelete = stats.slice(AGENT_SESSIONS_KEEP);
|
|
148
|
+
await Promise.all(toDelete.map(s => fsp.unlink(s.file).catch(() => {})));
|
|
149
|
+
if (toDelete.length > 0) {
|
|
150
|
+
logger.info(`[agent] sessions: cleaned up ${toDelete.length} old files`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 更新会话标题
|
|
155
|
+
async function renameAgentSession(sessionId, title) {
|
|
156
|
+
const data = await readAgentSessionFile(sessionId);
|
|
157
|
+
data.title = String(title || '').trim() || '(无标题)';
|
|
158
|
+
data.updatedAt = new Date().toISOString();
|
|
159
|
+
await writeAgentSessionFile(sessionId, data);
|
|
160
|
+
return data;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const agentSessionStore = {
|
|
164
|
+
genSessionId: genAgentSessionId,
|
|
165
|
+
autoTitle,
|
|
166
|
+
read: readAgentSessionFile,
|
|
167
|
+
write: writeAgentSessionFile,
|
|
168
|
+
delete: deleteAgentSessionFile,
|
|
169
|
+
listMeta: listAgentSessionsMeta,
|
|
170
|
+
enforceRetention: enforceAgentSessionsRetention,
|
|
171
|
+
rename: renameAgentSession,
|
|
172
|
+
DIR: AGENT_SESSIONS_DIR_FIXED,
|
|
173
|
+
};
|