zen-gitsync 2.16.36 → 2.16.37
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-CcX2ckae.js +2 -0
- package/src/ui/public/assets/BranchSelector-CmWzK_lJ.js +1 -0
- package/src/ui/public/assets/CommitForm-xwusUSow.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-CJMwWilV.js +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-CiK6fWX0.js +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-Bvqc_KwR.js +21 -0
- package/src/ui/public/assets/LogList-C8POmSML.js +7 -0
- package/src/ui/public/assets/MindmapView-DGbJgFMZ.js +1 -0
- package/src/ui/public/assets/MonitorView-Dy7Htpwd.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-Dr8VCQSL.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-DyjNYb-a.js +1 -0
- package/src/ui/public/assets/SourceMapView-BcpRuySS.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-C-FcOxAl.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-CupPXceE.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 +28 -2
- package/src/ui/server/utils/instanceRegistry.test.js +109 -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-CupPXceE.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
|
+
};
|
|
@@ -160,21 +160,47 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// 心跳。updates 可携带完整注册信息(port/projectPath/projectName/hostname):
|
|
164
|
+
// 条目存在 → 刷新 lastHeartbeat
|
|
165
|
+
// 条目不存在 → 用完整信息自愈重建
|
|
166
|
+
// 为什么要自愈:register() 是 read-modify-write,而 enqueueWrite 只串行化
|
|
167
|
+
// 单进程内的写。多实例几乎同时启动时,两个进程都读到旧表再各自整表写回,
|
|
168
|
+
// 后写覆盖先写,先注册的实例被静默抹掉(temp+rename 只保证单次写原子,
|
|
169
|
+
// 读-改-写整体不原子)。被覆盖的实例不会自己恢复,这里借 5s 心跳兜底补回。
|
|
163
170
|
async function heartbeat(pid, updates = {}) {
|
|
164
171
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
165
172
|
await enqueueWrite(async () => {
|
|
166
173
|
const obj = await readAll();
|
|
167
174
|
const key = String(pid);
|
|
168
175
|
const existing = obj.instances[key];
|
|
176
|
+
|
|
169
177
|
if (!existing) {
|
|
170
|
-
//
|
|
171
|
-
|
|
178
|
+
// 信息不全就无法自愈,保持原行为(跳过)
|
|
179
|
+
const canRebuild =
|
|
180
|
+
Number.isInteger(updates.port) && updates.port > 0 && !!updates.projectPath;
|
|
181
|
+
if (!canRebuild) return;
|
|
182
|
+
|
|
183
|
+
obj.instances[key] = {
|
|
184
|
+
pid,
|
|
185
|
+
port: updates.port,
|
|
186
|
+
projectName: updates.projectName || '',
|
|
187
|
+
projectPath: updates.projectPath,
|
|
188
|
+
startedAt: Date.now(),
|
|
189
|
+
lastHeartbeat: Date.now(),
|
|
190
|
+
hostname: updates.hostname || osMod.hostname()
|
|
191
|
+
};
|
|
192
|
+
await writeAll(obj);
|
|
193
|
+
logger.info(
|
|
194
|
+
`[instanceRegistry] 心跳检测到本实例条目丢失,已自愈重建 pid=${pid} port=${updates.port}`
|
|
195
|
+
);
|
|
172
196
|
return;
|
|
173
197
|
}
|
|
198
|
+
|
|
174
199
|
obj.instances[key] = {
|
|
175
200
|
...existing,
|
|
176
201
|
...(updates.projectPath ? { projectPath: updates.projectPath } : {}),
|
|
177
202
|
...(updates.projectName ? { projectName: updates.projectName } : {}),
|
|
203
|
+
...(Number.isInteger(updates.port) && updates.port > 0 ? { port: updates.port } : {}),
|
|
178
204
|
lastHeartbeat: Date.now()
|
|
179
205
|
};
|
|
180
206
|
await writeAll(obj);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { createInstanceRegistry } from './instanceRegistry.js'
|
|
4
|
+
|
|
5
|
+
function makeFs(initial = '{}') {
|
|
6
|
+
const files = new Map()
|
|
7
|
+
files.set('registry', initial)
|
|
8
|
+
return {
|
|
9
|
+
async readFile(p, enc) {
|
|
10
|
+
if (!files.has('registry')) {
|
|
11
|
+
const err = new Error('ENOENT')
|
|
12
|
+
err.code = 'ENOENT'
|
|
13
|
+
throw err
|
|
14
|
+
}
|
|
15
|
+
return files.get('registry')
|
|
16
|
+
},
|
|
17
|
+
async writeFile(p, data, enc) {
|
|
18
|
+
files.set('registry.tmp', data)
|
|
19
|
+
},
|
|
20
|
+
async rename(from, to) {
|
|
21
|
+
files.set('registry', files.get('registry.tmp'))
|
|
22
|
+
files.delete('registry.tmp')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makePath() {
|
|
28
|
+
return {
|
|
29
|
+
join(...parts) { return parts.join('/') },
|
|
30
|
+
basename(p) { return p.split('/').pop() }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeOs(hostname = 'test-host') {
|
|
35
|
+
return { hostname: () => hostname }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setup({ initial = '{"version":1,"instances":{}}' } = {}) {
|
|
39
|
+
const fs = makeFs(initial)
|
|
40
|
+
const path = makePath()
|
|
41
|
+
const os = makeOs()
|
|
42
|
+
const registryPath = 'registry'
|
|
43
|
+
const registry = createInstanceRegistry({ fs, path, os, registryPath })
|
|
44
|
+
return { registry, fs }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readRegistry(fs) {
|
|
48
|
+
const raw = await fs.readFile('registry', 'utf-8')
|
|
49
|
+
return JSON.parse(raw)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test('heartbeat: 条目存在时只刷新 lastHeartbeat', async () => {
|
|
53
|
+
const oldTime = Date.now() - 10_000
|
|
54
|
+
const initial = JSON.stringify({
|
|
55
|
+
version: 1,
|
|
56
|
+
instances: {
|
|
57
|
+
'100': {
|
|
58
|
+
pid: 100,
|
|
59
|
+
port: 9876,
|
|
60
|
+
projectName: 'p1',
|
|
61
|
+
projectPath: '/path/1',
|
|
62
|
+
startedAt: oldTime,
|
|
63
|
+
lastHeartbeat: oldTime,
|
|
64
|
+
hostname: 'host1'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
const { registry, fs } = setup({ initial })
|
|
69
|
+
|
|
70
|
+
await registry.heartbeat(100, { projectPath: '/path/1' })
|
|
71
|
+
|
|
72
|
+
const obj = await readRegistry(fs)
|
|
73
|
+
const entry = obj.instances['100']
|
|
74
|
+
assert.equal(entry.pid, 100)
|
|
75
|
+
assert.equal(entry.port, 9876)
|
|
76
|
+
assert.equal(entry.projectName, 'p1')
|
|
77
|
+
assert.ok(entry.lastHeartbeat > oldTime, 'lastHeartbeat 应该被刷新')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('heartbeat: 条目不存在且信息完整时自愈重建', async () => {
|
|
81
|
+
const { registry, fs } = setup()
|
|
82
|
+
|
|
83
|
+
await registry.heartbeat(100, {
|
|
84
|
+
port: 9876,
|
|
85
|
+
projectPath: '/path/1',
|
|
86
|
+
projectName: 'p1',
|
|
87
|
+
hostname: 'host1'
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const obj = await readRegistry(fs)
|
|
91
|
+
const entry = obj.instances['100']
|
|
92
|
+
assert.ok(entry, '条目应该被重建')
|
|
93
|
+
assert.equal(entry.pid, 100)
|
|
94
|
+
assert.equal(entry.port, 9876)
|
|
95
|
+
assert.equal(entry.projectName, 'p1')
|
|
96
|
+
assert.equal(entry.projectPath, '/path/1')
|
|
97
|
+
assert.equal(entry.hostname, 'host1')
|
|
98
|
+
assert.ok(entry.startedAt > 0)
|
|
99
|
+
assert.ok(entry.lastHeartbeat > 0)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('heartbeat: 条目不存在且信息不完整时不重建', async () => {
|
|
103
|
+
const { registry, fs } = setup()
|
|
104
|
+
|
|
105
|
+
await registry.heartbeat(100, { projectPath: '/path/1' }) // 缺少 port
|
|
106
|
+
|
|
107
|
+
const obj = await readRegistry(fs)
|
|
108
|
+
assert.deepEqual(obj.instances, {}, '不应该写入任何条目')
|
|
109
|
+
})
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import"./rolldown-runtime-BM3Ffeng.js";import{Br as e,Bt as t,Dn as n,Et as r,G as i,Gn as a,In as o,Pt as s,Qn as c,Rr as l,Sn as u,Un as d,Ur as f,Y as p,_r as m,a as h,bn as g,dr as _,er as v,et as ee,ht as te,i as y,kn as ne,lr as b,lt as re,mn as ie,o as ae,pn as oe,rn as se,tr as x,un as ce,vn as S,xn as C,yn as w}from"./element-plus-utMlO-GC.js";import{a as le,r as T,t as E}from"./vendor-C_cx2RSh.js";import{D,N as O,j as k}from"./index-2NOwDrtr.js";var A=64*1024;function j(e){if(!e||!Array.isArray(e.messages))return[];let t=[],n=e.messages.length;for(let r=0;r<n;r++){let i=e.messages[r];if(i.role!==`system`){if(i.role===`user`){let e=``,n;if(typeof i.content==`string`)e=i.content;else if(Array.isArray(i.content)){e=i.content.filter(e=>e?.type===`text`).map(e=>e.text||``).join(` `);let t=i.content.filter(e=>e?.type===`image_url`&&typeof e.image_url?.url==`string`).map((e,t)=>({id:`att-${r}-${t}`,name:`image`,size:0,type:P(e.image_url?.url||``)||`image/*`,preview:e.image_url?.url}));t.length>0&&(n=t)}(e||n)&&t.push({id:`msg-${r}`,role:`user`,content:e,attachments:n,status:`done`,createdAt:Date.now()})}else if(i.role===`assistant`){let a=``;typeof i.content==`string`&&(a=i.content),a.length>A&&(a=`…(前文已截断)\n${a.slice(-A)}`);let o=[];if(i.tool_calls&&i.tool_calls.length>0)for(let e of i.tool_calls)o.push({id:e.id||e.function?.name||T(),name:e.function?.name||``,arguments:e.function?.arguments||``,argsPreview:M(e.function?.name||``,e.function?.arguments||``),status:`done`,result:``});let s=r+1;for(;s<n&&e.messages[s].role===`tool`;){let t=e.messages[s],n=t.tool_call_id||t.name||``,i=o.find(e=>e.id===n);if(i){let e=t.content;typeof e!=`string`&&(e=`(non-text result)`),e.length>A&&(e=`…(前文已截断)\n${e.slice(-A)}`),i.result=e}r=s,s++}let c=!!a,l=o.length>0;(c||l)&&t.push({id:`msg-${r}`,role:`assistant`,content:a||``,toolCalls:l?o:void 0,status:`done`,createdAt:Date.now()})}}}return t}function M(e,t){try{let n=JSON.parse(t);switch(e){case`run_command`:return String(n.command||``).slice(0,200);case`read_file`:case`write_file`:case`edit_file`:return String(n.path||``);case`list_files`:return String(n.path||`.`);case`search_text`:return String(n.pattern||``);default:return JSON.stringify(n).slice(0,200)}}catch{return t.slice(0,200)}}function N(e){return new Promise((t,n)=>{let r=new FileReader;r.onload=()=>t(String(r.result||``)),r.onerror=()=>n(r.error||Error(`FileReader failed`)),r.readAsDataURL(e)})}function P(e){let t=/^data:([^;,]+)/.exec(e);return t?t[1]:``}function ue(){let e=D(),t=_([]),n=_(!1),r=_(null),i=_([]),a=_(!1),o=_(!1),s=null,c=0,l=0;async function u(){let r=++l,i=e.currentDirectory||``;if(!i){t.value=[],n.value=!1;return}n.value=!0;try{let e=`/api/agent/sessions?cwd=${encodeURIComponent(i)}`,n=await fetch(e).then(e=>e.json());if(r!==l)return;if(!n.success){h.error(n.error||O(`@AGENT:加载会话列表失败`));return}t.value=Array.isArray(n.sessions)?n.sessions:[]}catch(e){if(r!==l)return;h.error(O(`@AGENT:加载会话列表失败`)+`: `+(e?.message||e))}finally{r===l&&(n.value=!1)}}async function d(e){o.value=!0,r.value=e;try{let t=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`).then(e=>e.json());if(!t.success){h.error(t.error||O(`@AGENT:加载会话失败`));return}i.value=j(t.session)}catch(e){h.error(O(`@AGENT:加载会话失败`)+`: `+(e?.message||e))}finally{o.value=!1}}async function f(e){try{await y.confirm(O(`@AGENT:确认删除该会话`),O(`@AGENT:删除后无法恢复`),{type:`warning`})}catch{return}try{let n=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`DELETE`}).then(e=>e.json());if(!n.success){h.error(n.error||O(`@AGENT:删除失败`));return}t.value=t.value.filter(t=>t.sessionId!==e),r.value===e&&(r.value=null,i.value=[]),h.success(O(`@AGENT:已删除`))}catch(e){h.error(O(`@AGENT:删除失败`)+`: `+(e?.message||e))}}async function p(e,n){try{let r=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({title:n})}).then(e=>e.json());if(!r.success){h.error(r.error||O(`@AGENT:重命名失败`));return}let i=t.value.find(t=>t.sessionId===e);i&&(i.title=n),h.success(O(`@AGENT:已重命名`))}catch(e){h.error(O(`@AGENT:重命名失败`)+`: `+(e?.message||e))}}function m(){r.value=null,i.value=[]}async function g(t,n=[]){let o=n.filter(e=>e?.file?.type?.startsWith(`image/`));if(o.length<n.length&&h.warning(O(`@AGENT:仅支持发送图片,非图片文件已忽略`)),!t.trim()&&o.length===0||a.value)return;let l=(await Promise.allSettled(o.map(e=>N(e.file)))).map(e=>e.status===`fulfilled`?e.value:``).filter(e=>e.startsWith(`data:image/`)),d=++c;s=new AbortController;let f=s,p={id:T(),role:`user`,content:t,status:`done`,createdAt:Date.now()};o.length>0&&(p.attachments=o.map(e=>({id:e.id,name:e.file.name||`image`,size:e.file.size||0,type:e.file.type||``,preview:e.preview}))),i.value.push(p);let m=b({id:T(),role:`assistant`,content:``,status:`pending`,createdAt:Date.now()});i.value.push(m),a.value=!0;let g=[];try{let n=await fetch(`/api/agent/chat`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`text/event-stream`},body:JSON.stringify({sessionId:r.value||``,userMessage:t,cwd:e.currentDirectory||``,...l.length>0?{images:l}:{}}),signal:f.signal});if(d!==c)return;if(!n.ok||!n.body){let e=await n.text().catch(()=>``);throw Error(e||`HTTP ${n.status}`)}let a=n.body.getReader(),o=new TextDecoder(`utf-8`),s=``,p=r.value;for(;;){let{value:e,done:t}=await a.read();if(d!==c)return;if(t)break;s+=o.decode(e,{stream:!0});let n=s.split(`
|
|
2
|
-
`);s=n.pop()??``;for(let e of n){let t=e.trim();if(!t.startsWith(`data:`))continue;let n=t.slice(5).trim();if(!n)continue;let i;try{i=JSON.parse(n)}catch{continue}switch(i.type){case`meta`:i.sessionId&&(p=i.sessionId,r.value=i.sessionId);break;case`thinking`:m.reasoning||(m.reasoning=``,m.reasoningStatus=`streaming`),m.reasoning+=String(i.delta||``),m.status=`streaming`;break;case`content`:m.status===`pending`&&(m.status=`streaming`);{let e=String(i.delta||``);m.content+=e;let t=E(m.content);t.reasoning&&(m.reasoning||(m.reasoning=``,m.reasoningStatus=`streaming`),m.reasoning=t.reasoning,m.content=t.content)}break;case`tool_call_start`:{let e=b({id:i.toolCallId||T(),name:i.name||``,argsPreview:i.argsPreview||``,arguments:i.argsPreview||``,status:`running`,result:``});g.push(e),m.toolCalls||=[],m.toolCalls.push(e),m.status===`pending`&&(m.status=`streaming`);break}case`tool_result`:{let e=g.find(e=>e.id===i.toolCallId);e&&(e.result=String(i.result||``),e.status=`done`);break}case`done`:{let e=E(i.content||m.content);e.reasoning&&!m.reasoning&&(m.reasoning=e.reasoning),m.content=e.content,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`);break}case`error`:m.status=`error`,m.error=String(i.error||O(`@AGENT:未知错误`)),m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`);break}}i.value=[...i.value]}if(d!==c)return;m.status===`pending`&&(m.status=`done`),p&&p!==r.value&&(r.value=p),u().catch(()=>{})}catch(e){if(d!==c)return;e?.name===`AbortError`||f.signal.aborted?(m.content=(m.content||``)+`
|
|
3
|
-
|
|
4
|
-
[`+O(`@AGENT:已停止`)+`]`,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`)):(m.status=`error`,m.error=e?.message||String(e),h.error(m.error||O(`@AGENT:对话失败`)))}finally{d===c&&(a.value=!1,s=null)}}function v(){s&&=(s.abort(),null)}return{sessions:t,sessionsLoading:n,currentSessionId:r,messages:i,isStreaming:a,sessionLoading:o,loadSessions:u,loadSession:d,deleteSession:f,renameSession:p,newSession:m,sendMessage:g,stop:v}}var de={class:`agent-view`},F={class:`sidebar-header`},I={class:`sidebar-search`},L=[`placeholder`],R={class:`session-list`},z={key:0,class:`empty-state`},B={class:`empty-text`},V={key:0,class:`empty-hint`},H=[`onClick`],fe={class:`session-item-main`},pe={class:`session-item-title`},me={class:`session-item-meta`},U={class:`meta-time`},he={class:`meta-count`},ge={key:0,class:`meta-source cli`},_e=[`onClick`],ve=[`onClick`],ye={class:`agent-chat-area`},be={key:0,class:`chat-loading`},xe={key:0,class:`stop-button-bar`},W=k(ne({__name:`AgentView`,setup(ne){let b=D(),{sessions:T,sessionsLoading:E,currentSessionId:k,messages:A,isStreaming:j,sessionLoading:M,loadSessions:N,loadSession:P,deleteSession:W,renameSession:Se,newSession:G,sendMessage:K,stop:q}=ue(),Ce=S(()=>{let e=b.theme;return e===`dark`||e===`auto`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}),J=_(``),Y=S(()=>{if(!J.value.trim())return T.value;let e=J.value.toLowerCase();return T.value.filter(t=>(t.title||``).toLowerCase().includes(e)||(t.model||``).toLowerCase().includes(e))}),we=S(()=>[{id:`p1`,label:O(`@AGENT:查看项目结构`),prompt:O(`@AGENT:prompt_p1`)},{id:`p2`,label:O(`@AGENT:分析代码质量`),prompt:O(`@AGENT:prompt_p2`)},{id:`p3`,label:O(`@AGENT:帮我写测试`),prompt:O(`@AGENT:prompt_p3`)},{id:`p4`,label:O(`@AGENT:Git 状态检查`),prompt:O(`@AGENT:prompt_p4`)}]);async function Te(e){await K(e.text,e.files),await o(),Z()}async function Ee(e){await K(e.prompt),await o(),Z()}let X=_(null);function Z(e=!0){X.value?.scrollToBottom(e)}c(()=>A.value.length,()=>{o(()=>Z(!1))});function De(e){if(j.value){h.warning(O(`@AGENT:请先停止当前生成`));return}P(e)}function Oe(){if(j.value){h.warning(O(`@AGENT:请先停止当前生成`));return}G()}function ke(e,t){if(t.stopPropagation(),j.value&&k.value===e){h.warning(O(`@AGENT:请先停止当前生成`));return}W(e)}async function Ae(e,t,n){n.stopPropagation();try{let{value:n}=await y.prompt(O(`@AGENT:请输入新的会话标题`),O(`@AGENT:重命名会话`),{inputValue:t,inputPattern:/.+/,inputErrorMessage:O(`@AGENT:标题不能为空`)});n&&n!==t&&await Se(e,n)}catch{}}function je(e){if(!e)return``;try{let t=new Date(e);if(Number.isNaN(t.getTime()))return e;let n=new Date,r=n.getTime()-t.getTime(),i=e=>String(e).padStart(2,`0`);if(t.toDateString()===n.toDateString())return`${i(t.getHours())}:${i(t.getMinutes())}`;if(r<10080*60*1e3){let e=Math.floor(r/(1440*60*1e3));return e===0?O(`@AGENT:今天`):`${e}${O(`@AGENT:天前`)}`}return`${t.getFullYear()}-${i(t.getMonth()+1)}-${i(t.getDate())}`}catch{return e}}let Q=_(280),$=_(!1);function Me(e){$.value=!0;let t=e.clientX,n=Q.value,r=e=>{let r=e.clientX-t;Q.value=Math.max(200,Math.min(500,n+r))},i=()=>{$.value=!1,document.removeEventListener(`mousemove`,r),document.removeEventListener(`mouseup`,i)};document.addEventListener(`mousemove`,r),document.addEventListener(`mouseup`,i)}return c(()=>[b.currentDirectory,j.value],async([e,t],n)=>{let r=!n||e!==n[0],i=!!n&&n[1]===!0&&!t;(r||i)&&await N(),k.value&&!j.value&&!T.value.some(e=>e.sessionId===k.value)&&G()},{immediate:!0}),(o,c)=>{let h=ae;return d(),u(`div`,de,[w(`aside`,{class:`agent-sidebar`,style:e({width:Q.value+`px`})},[w(`div`,F,[w(`button`,{class:`new-session-btn`,onClick:Oe},[n(m(p),null,{default:v(()=>[n(m(s))]),_:1}),w(`span`,null,f(m(O)(`@AGENT:新建会话`)),1)])]),w(`div`,I,[n(m(p),{class:`search-icon`},{default:v(()=>[n(m(t))]),_:1}),x(w(`input`,{"onUpdate:modelValue":c[0]||=e=>J.value=e,placeholder:m(O)(`@AGENT:搜索会话...`),class:`search-input`,type:`text`},null,8,L),[[ce,J.value]])]),x((d(),u(`div`,R,[Y.value.length===0&&!m(E)?(d(),u(`div`,z,[n(m(p),{size:28,color:`var(--text-tertiary)`},{default:v(()=>[n(m(ee))]),_:1}),w(`div`,B,f(J.value?m(O)(`@AGENT:未找到匹配的会话`):m(O)(`@AGENT:暂无会话`)),1),J.value?C(``,!0):(d(),u(`div`,V,f(m(O)(`@AGENT:点击上方按钮开始对话`)),1))])):C(``,!0),(d(!0),u(ie,null,a(Y.value,e=>(d(),u(`div`,{key:e.sessionId,class:l([`session-item`,{active:m(k)===e.sessionId}]),onClick:t=>De(e.sessionId)},[w(`div`,fe,[w(`div`,pe,f(e.title||m(O)(`@AGENT:无标题`)),1),w(`div`,me,[w(`span`,U,f(je(e.updatedAt)),1),c[3]||=w(`span`,{class:`meta-dot`},`·`,-1),w(`span`,he,f(e.messageCount)+` `+f(m(O)(`@AGENT:条`)),1),e.source===`cli`?(d(),u(`span`,ge,`CLI`)):C(``,!0)])]),w(`div`,{class:`session-item-actions`,onClick:c[1]||=oe(()=>{},[`stop`])},[n(m(i),{content:m(O)(`@AGENT:重命名`),placement:`top`,"show-after":500},{default:v(()=>[w(`button`,{class:`item-action-btn`,onClick:t=>Ae(e.sessionId,e.title,t)},[n(m(p),null,{default:v(()=>[n(m(te))]),_:1})],8,_e)]),_:2},1032,[`content`]),n(m(i),{content:m(O)(`@AGENT:删除`),placement:`top`,"show-after":500},{default:v(()=>[w(`button`,{class:`item-action-btn danger`,onClick:t=>ke(e.sessionId,t)},[n(m(p),null,{default:v(()=>[n(m(re))]),_:1})],8,ve)]),_:2},1032,[`content`])])],10,H))),128))])),[[h,m(E)]])],4),w(`div`,{class:l([`sidebar-resizer`,{active:$.value}]),onMousedown:Me},null,34),w(`main`,ye,[m(M)?(d(),u(`div`,be,[n(m(p),{class:`is-loading`,size:32},{default:v(()=>[n(m(r))]),_:1}),w(`span`,null,f(m(O)(`@AGENT:加载会话中...`)),1)])):(d(),g(m(le),{key:1,ref_key:`chatContainerRef`,ref:X,messages:m(A),"preset-questions":we.value,"welcome-title":m(O)(`@AGENT:智能体助手`),"welcome-description":m(O)(`@AGENT:我可以帮你阅读代码、执行命令、修改文件。选择下方话题或直接输入你的问题。`),"assistant-name":`g ai`,theme:Ce.value,disabled:m(j),"upload-config":{accept:`image/*`},placeholder:m(j)?m(O)(`@AGENT:正在生成中...`):m(O)(`@AGENT:输入消息,Enter 发送`),onSend:Te,onSelect:Ee},null,8,[`messages`,`preset-questions`,`welcome-title`,`welcome-description`,`theme`,`disabled`,`placeholder`])),n(se,{name:`fade`},{default:v(()=>[m(j)?(d(),u(`div`,xe,[w(`button`,{class:`stop-button`,onClick:c[2]||=(...e)=>m(q)&&m(q)(...e)},[n(m(p),null,{default:v(()=>[n(m(r),{class:`is-loading`})]),_:1}),w(`span`,null,f(m(O)(`@AGENT:停止生成`)),1)])])):C(``,!0)]),_:1})])])}}}),[[`__scopeId`,`data-v-773baa74`]]);export{W as default};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import"./rolldown-runtime-BM3Ffeng.js";import{B as e,D as t,Dn as n,En as r,Et as i,G as a,In as o,Qn as s,Rr as c,Sn as l,Un as u,Ur as d,Vn as f,Y as p,_r as m,a as h,bn as g,dr as _,er as v,i as y,kn as b,nt as x,rt as S,vn as C,xn as w,yn as T,zn as E}from"./element-plus-utMlO-GC.js";import{N as D,j as O}from"./index-2NOwDrtr.js";async function k(e,t){let n=e.body.getReader(),r=new TextDecoder(`utf-8`),i=``;for(;;){let{done:e,value:a}=await n.read();if(e)break;i+=r.decode(a,{stream:!0});let o=i.split(`
|
|
2
|
-
`);i=o.pop()??``;for(let e of o)e.trim()&&t(e)}i.trim()&&t(i)}async function A(){let e=await fetch(`/api/app-version`);return e.ok?e.json():{success:!1,current:``,latest:null,hasUpdate:!1,error:`HTTP ${e.status}`}}async function j(e){let t=await fetch(`/api/app-upgrade`,{method:`POST`});if(!t.ok){let n=`HTTP ${t.status}`;try{let e=await t.json();e?.error&&(n=e.error)}catch{}e({type:`error`,message:n}),e({type:`done`,code:t.status});return}if(!t.body){e({type:`error`,message:`响应无 body`}),e({type:`done`,code:-1});return}await k(t,t=>{try{e(JSON.parse(t))}catch{}})}async function M(){let e=await fetch(`/api/app-restart`,{method:`POST`});if(!e.ok){let t=`HTTP ${e.status}`;try{let n=await e.json();n?.error&&(t=n.error)}catch{}throw Error(t)}if(!e.body)throw Error(`响应无 body`);return new Promise((t,n)=>{let r=!1,i=e=>{r||(r=!0,e())};k(e,e=>{try{let r=JSON.parse(e);r.type===`ready`&&typeof r.port==`number`&&r.port>0?i(()=>t(r.port)):r.type===`timeout`?i(()=>n(Error(`restart.timeout`))):r.type===`error`&&i(()=>n(Error(r.message||`restart.failed`)))}catch{}}).then(()=>{i(()=>n(Error(`restart.stream-closed`)))}).catch(()=>{})})}var N={key:3},P={key:4},F={key:5},I={key:0,class:`upgrade-hint`},L={key:0,class:`upgrade-countdown`},R=O(b({__name:`UpgradeDialog`,props:{modelValue:{type:Boolean},logs:{},status:{},countdown:{default:0}},emits:[`update:modelValue`,`retry`,`restart`,`cancel`],setup(a,{emit:f}){let h=a,y=f,b=_(null);s(()=>h.logs,async()=>{await o(),b.value&&(b.value.scrollTop=b.value.scrollHeight)});function C(){h.status!==`running`&&(h.status===`success`&&h.countdown>0&&y(`cancel`),y(`update:modelValue`,!1))}function E(){y(`retry`)}function O(){y(`restart`)}function k(){y(`cancel`),y(`update:modelValue`,!1)}return(o,s)=>{let f=p,h=e,_=t;return u(),g(_,{"model-value":a.modelValue,"onUpdate:modelValue":s[0]||=e=>y(`update:modelValue`,e),title:m(D)(`@F13B4:升级`),width:`680px`,"close-on-click-modal":!1,"close-on-press-escape":!1,"show-close":a.status!==`running`,"append-to-body":``,onClose:C},{footer:v(()=>[a.status===`failed`?(u(),g(h,{key:0,type:`primary`,onClick:E},{default:v(()=>[r(d(m(D)(`@F13B4:重试`)),1)]),_:1})):w(``,!0),a.status===`success`?(u(),g(h,{key:1,type:`primary`,onClick:O},{default:v(()=>[r(d(m(D)(`@F13B4:立即重启并刷新`)),1)]),_:1})):w(``,!0),a.status===`success`?(u(),g(h,{key:2,onClick:k},{default:v(()=>[r(d(m(D)(`@F13B4:稍后手动重启`)),1)]),_:1})):w(``,!0),a.status===`running`?(u(),g(h,{key:3,disabled:``},{default:v(()=>[r(d(m(D)(`@F13B4:升级中`))+`... `,1)]),_:1})):w(``,!0)]),default:v(()=>[T(`div`,{class:c([`upgrade-status`,`is-${a.status}`])},[a.status===`running`?(u(),g(f,{key:0,class:`is-loading`},{default:v(()=>[n(m(i))]),_:1})):a.status===`success`?(u(),g(f,{key:1,color:`#67c23a`},{default:v(()=>[n(m(x))]),_:1})):(u(),g(f,{key:2,color:`#f56c6c`},{default:v(()=>[n(m(S))]),_:1})),a.status===`running`?(u(),l(`span`,N,d(m(D)(`@F13B4:升级中`)),1)):a.status===`success`?(u(),l(`span`,P,d(m(D)(`@F13B4:升级完成`)),1)):(u(),l(`span`,F,d(m(D)(`@F13B4:升级失败`)),1))],2),a.status===`success`?(u(),l(`p`,I,[r(d(m(D)(`@F13B4:新版本已全局安装,需要重启服务才能生效`))+` `,1),a.countdown>0?(u(),l(`span`,L,d(m(D)(`@F13B4:{seconds} 秒后自动刷新`,{seconds:a.countdown})),1)):w(``,!0)])):w(``,!0),T(`pre`,{ref_key:`logEl`,ref:b,class:`upgrade-log`},d(a.logs||m(D)(`@F13B4:等待日志输出`)),513)]),_:1},8,[`model-value`,`title`,`show-close`])}}}),[[`__scopeId`,`data-v-3de8edac`]]),z={class:`app-version-badge`},B=[`aria-label`],V=[`aria-label`],H=[`title`,`aria-label`],U={class:`upgrade-btn__label`},W={class:`upgrade-btn__version`},G=`https://github.com/xz333221/zen-gitsync/releases`,K=5,q=`app-version-check`,J=3600*1e3,Y=O(b({__name:`AppVersionBadge`,setup(e){let t=`2.16.36`,r=_(t),i=C(()=>r.value||t),o=_(null),s=_(!1),c=_(``),p=_(`running`),g=_(0),b=null,x=_(!1);function S(){try{let e=sessionStorage.getItem(q);if(!e)return null;let{at:t,data:n}=JSON.parse(e);return Date.now()-t>J?null:n}catch{return null}}function O(e){try{sessionStorage.setItem(q,JSON.stringify({at:Date.now(),data:e}))}catch{}}async function k(e=!1){let t=S();t&&(o.value=t);try{let t=await A();o.value=t,O(t),t.current&&(r.value=t.current),!e&&t.success&&t.hasUpdate&&h({type:`success`,message:D(`@F13B4:发现新版本 {version}`,{version:t.latest??``}),duration:4e3})}catch(t){e||h({type:`warning`,message:D(`@F13B4:检查更新失败 {error}`,{error:t?.message||``})})}}async function N(){if(o.value?.hasUpdate){try{await y.confirm(D(`@F13B4:确定要从 {current} 升级到 {latest} 吗?`,{current:o.value.current,latest:o.value.latest}),D(`@F13B4:升级`),{confirmButtonText:D(`@F13B4:确定升级`),cancelButtonText:D(`@F13B4:取消`),type:`info`})}catch{return}c.value=``,p.value=`running`,s.value=!0,await j(e=>{if(e.type===`done`){if(p.value=e.code===0?`success`:`failed`,e.code===0){try{sessionStorage.removeItem(q)}catch{}o.value?.latest&&(r.value=o.value.latest),P()}}else e.message&&(c.value+=e.message)})}}function P(){F(),g.value=K,b=setInterval(()=>{--g.value,g.value<=0&&(b&&=(clearInterval(b),null),L())},1e3)}function F(){b&&=(clearInterval(b),null),g.value=0}function I(){F()}async function L(){if(F(),!x.value){x.value=!0;try{let e=await M(),{pathname:t,search:n,hash:r}=window.location;window.location.href=`http://localhost:${e}${t}${n}${r}`}catch(e){let t=e?.message||``,n={"restart.timeout":`@F13B4:重启超时`,"restart.stream-closed":`@F13B4:重启失败,请刷新页面重试`,"restart.failed":`@F13B4:重启失败`}[t];h({type:`warning`,message:n?D(n):D(`@F13B4:重启失败 {error}`,{error:t})}),x.value=!1}}}return E(()=>{F()}),f(()=>{k(!0)}),(e,t)=>{let r=a;return u(),l(`div`,z,[n(r,{content:m(D)(`@F13B4:在 GitHub 上查看源码`),placement:`top`,effect:`dark`,"show-after":300},{default:v(()=>[T(`a`,{href:`https://github.com/xz333221/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link`,"aria-label":m(D)(`@F13B4:在 GitHub 上查看源码`)},[...t[1]||=[T(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[T(`path`,{d:`M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z`})],-1)]],8,B)]),_:1},8,[`content`]),n(r,{content:m(D)(`@F13B4:在 npm 上查看包`),placement:`top`,effect:`dark`,"show-after":300},{default:v(()=>[T(`a`,{href:`https://www.npmjs.com/package/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link badge-icon-link--npm`,"aria-label":m(D)(`@F13B4:在 npm 上查看包`)},[...t[2]||=[T(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[T(`path`,{d:`M2 2v20h20V2H2zm17.167 17.167H12.5v-10h-3.333v10H4.833V4.833h14.334v14.334z`})],-1)]],8,V)]),_:1},8,[`content`]),n(r,{content:m(D)(`@F13B4:查看更新`),placement:`top`,effect:`dark`,"show-after":300},{default:v(()=>[T(`a`,{href:G,target:`_blank`,rel:`noopener noreferrer`,class:`version-link`},`v`+d(i.value),1)]),_:1},8,[`content`]),o.value?.hasUpdate?(u(),l(`button`,{key:0,type:`button`,class:`upgrade-btn`,title:m(D)(`@F13B4:发现新版本 {version},点击升级`,{version:o.value.latest??``}),"aria-label":m(D)(`@F13B4:发现新版本 {version},点击升级`,{version:o.value.latest??``}),onClick:N},[t[3]||=T(`span`,{class:`upgrade-btn__dot`,"aria-hidden":`true`},null,-1),T(`span`,U,d(m(D)(`@F13B4:升级`)),1),T(`span`,W,`v`+d(o.value.latest),1)],8,H)):w(``,!0),n(R,{modelValue:s.value,"onUpdate:modelValue":t[0]||=e=>s.value=e,logs:c.value,status:p.value,countdown:g.value,onRetry:N,onRestart:L,onCancel:I},null,8,[`modelValue`,`logs`,`status`,`countdown`])])}}}),[[`__scopeId`,`data-v-bc7c0d5c`]]);export{Y as default};
|