zen-gitsync 2.17.7 → 2.17.8
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 +21 -4
- package/package.json +1 -1
- package/src/cli/ai/agent.js +11 -7
- package/src/cli/ai/cli.integration.test.js +27 -2
- package/src/cli/ai/runtime.test.js +29 -0
- package/src/cli/ai/termui.js +29 -12
- package/src/cli/ai/termui.test.js +102 -2
- package/src/cli/ai/turn.js +1 -1
- package/src/ui/public/assets/AgentView-CdJaeRh0.js +5 -0
- package/src/ui/public/assets/AgentView-DAY4S5WN.css +1 -0
- package/src/ui/public/assets/{AppVersionBadge-Gs5lUl7P.js → AppVersionBadge-CiijSYfM.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-IjaFk0S-.js → BranchSelector-BS7ajwvv.js} +1 -1
- package/src/ui/public/assets/{CommitForm-C-sowOXH.js → CommitForm-CEOSqXCA.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-QyptFD1R.js → CommonDialog-CAaEi-4T.js} +1 -1
- package/src/ui/public/assets/{EditorView-CvvluFCY.js → EditorView-DyaFyRDD.js} +1 -1
- package/src/ui/public/assets/{FileDiffViewer-BXVsdCxE.js → FileDiffViewer-BvX9cQjR.js} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-CRxipXru.js → FlowExecutionViewer-uXrzZjsW.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-AE1ZHTKF.js → FlowOrchestrationWorkspace-DIBpUSt_.js} +1 -1
- package/src/ui/public/assets/{IconButton-BmSueF_H.js → IconButton-CnoW2on6.js} +1 -1
- package/src/ui/public/assets/{LogList-iaVA-_aC.js → LogList-D2espSVo.js} +1 -1
- package/src/ui/public/assets/{MindmapView-DcG4jpCh.js → MindmapView-CZF-rZVs.js} +1 -1
- package/src/ui/public/assets/{MonitorView-DO59syvl.js → MonitorView-DjLr7qOG.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-DYppceyw.js → ProjectStartupButton-DzsU-QIO.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-CDbxG6pZ.js → RemoteManagerDialog-CuuG2ucT.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-Cko2KB8w.js → RemoteRepoCard-BGf8L0of.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-CwWPVdz-.js → SourceMapView-CFb02m6z.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-Cf7nOKc8.js → SvgIcon-SRAeYjnN.js} +1 -1
- package/src/ui/public/assets/{TemplateManager-DWPDjWxM.js → TemplateManager-gedqAXl8.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-YOziN-5A.js → UserInputNode-BOCuMQW6.js} +1 -1
- package/src/ui/public/assets/{WorkbenchView-DNnEpIYy.js → WorkbenchView-CRoR21LQ.js} +4 -4
- package/src/ui/public/assets/{_plugin-vue_export-helper-CQjg8MXm.js → _plugin-vue_export-helper-C8OE7U9H.js} +4 -4
- package/src/ui/public/assets/{configStore-C7HOx5S1.js → configStore-CuMElTgV.js} +1 -1
- package/src/ui/public/assets/{gitStore-DIsBd6n4.js → gitStore-CpcS-MzM.js} +1 -1
- package/src/ui/public/assets/index-cApp6w0b.css +1 -0
- package/src/ui/public/assets/index-yXm9JLGv.js +24 -0
- package/src/ui/public/assets/{previewSandbox-TKucqLxt.js → previewSandbox-DkXYEpLP.js} +0 -0
- package/src/ui/public/assets/{toolsStore-BiC2mmC3.js → toolsStore-DGxlRe3Y.js} +1 -1
- package/src/ui/public/assets/vendor-4ojsjDYc.css +1 -0
- package/src/ui/public/assets/{vendor-92fKgtnz.js → vendor-dO4B7wQf.js} +28 -28
- package/src/ui/public/index.html +14 -14
- package/src/ui/server/routes/fs.js +37 -0
- package/src/ui/server/utils/directoryFetch.js +209 -0
- package/src/ui/server/utils/directoryFetch.test.js +232 -0
- package/src/ui/public/assets/AgentView-CZc191Xm.js +0 -4
- package/src/ui/public/assets/AgentView-CzUeyQfR.css +0 -1
- package/src/ui/public/assets/index-FOe1cHkh.css +0 -1
- package/src/ui/public/assets/index-MmcnT2Dn.js +0 -24
- package/src/ui/public/assets/vendor-B1UrVTTI.css +0 -1
package/src/ui/public/index.html
CHANGED
|
@@ -10,28 +10,28 @@
|
|
|
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-yXm9JLGv.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/element-plus-KTmRV-so.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/monaco-DkJuNcCr.js">
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/IconButton-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/assets/configStore-
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-dO4B7wQf.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-C8OE7U9H.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-SRAeYjnN.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/IconButton-CnoW2on6.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/assets/configStore-CuMElTgV.js">
|
|
22
22
|
<link rel="modulepreload" crossorigin href="/assets/socket-io-TBtw0msS.js">
|
|
23
|
-
<link rel="modulepreload" crossorigin href="/assets/gitStore-
|
|
23
|
+
<link rel="modulepreload" crossorigin href="/assets/gitStore-CpcS-MzM.js">
|
|
24
24
|
<link rel="modulepreload" crossorigin href="/assets/useThemeObserver-B01D-Vl6.js">
|
|
25
25
|
<link rel="modulepreload" crossorigin href="/assets/flow-mindmap-CurCIiCp.js">
|
|
26
|
-
<link rel="modulepreload" crossorigin href="/assets/previewSandbox-
|
|
27
|
-
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-
|
|
28
|
-
<link rel="modulepreload" crossorigin href="/assets/toolsStore-
|
|
29
|
-
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-
|
|
30
|
-
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-
|
|
26
|
+
<link rel="modulepreload" crossorigin href="/assets/previewSandbox-DkXYEpLP.js">
|
|
27
|
+
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-BvX9cQjR.js">
|
|
28
|
+
<link rel="modulepreload" crossorigin href="/assets/toolsStore-DGxlRe3Y.js">
|
|
29
|
+
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-CAaEi-4T.js">
|
|
30
|
+
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-gedqAXl8.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vue-flow-CsvDD38k.js">
|
|
32
32
|
<link rel="stylesheet" crossorigin href="/assets/element-plus-BWbXh9mk.css">
|
|
33
33
|
<link rel="stylesheet" crossorigin href="/assets/monaco-B2lPvkrc.css">
|
|
34
|
-
<link rel="stylesheet" crossorigin href="/assets/vendor-
|
|
34
|
+
<link rel="stylesheet" crossorigin href="/assets/vendor-4ojsjDYc.css">
|
|
35
35
|
<link rel="stylesheet" crossorigin href="/assets/SvgIcon-BQ6bR7l0.css">
|
|
36
36
|
<link rel="stylesheet" crossorigin href="/assets/IconButton-CTwyZrv2.css">
|
|
37
37
|
<link rel="stylesheet" crossorigin href="/assets/flow-mindmap-DnzM9SlK.css">
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<link rel="stylesheet" crossorigin href="/assets/CommonDialog-C_RjF9Mp.css">
|
|
41
41
|
<link rel="stylesheet" crossorigin href="/assets/TemplateManager-C0rm8xfC.css">
|
|
42
42
|
<link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
|
|
43
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
43
|
+
<link rel="stylesheet" crossorigin href="/assets/index-cApp6w0b.css">
|
|
44
44
|
</head>
|
|
45
45
|
<body>
|
|
46
46
|
<div id="app"></div>
|
|
@@ -23,6 +23,7 @@ import { spawn, exec, execSync } from 'child_process';
|
|
|
23
23
|
import { ensureWithinCwd, normalizeProjectPath } from '../utils/pathGuard.js';
|
|
24
24
|
import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
|
|
25
25
|
import { probeDirectoryGitStates, normalizeDirKey } from '../utils/directoryGitState.js';
|
|
26
|
+
import { fetchDirectoryRemotes } from '../utils/directoryFetch.js';
|
|
26
27
|
import { invalidateCurrentProjectKey, invalidateRawConfigCache } from '../../../config.js';
|
|
27
28
|
import { invalidateCwdCache } from '../../../utils/index.js';
|
|
28
29
|
|
|
@@ -404,6 +405,42 @@ export function registerFsRoutes({
|
|
|
404
405
|
res.json({ success: true, results });
|
|
405
406
|
}));
|
|
406
407
|
|
|
408
|
+
// 对**单个**「最近目录」执行 git fetch,让卡片上的「领先/落后」从
|
|
409
|
+
// "上次 fetch 时的快照"回到真实状态。
|
|
410
|
+
//
|
|
411
|
+
// 为什么是单目录接口,而不是一个"一次刷新全部"的批量接口:
|
|
412
|
+
// 十几个仓库并发联网既慢又容易撞凭据提示;拆成一个个请求后,每个请求的
|
|
413
|
+
// 耗时被单目录超时箍住(不会出现一挂两分钟、还看不到进度的大请求),
|
|
414
|
+
// 刷新顺序与并发由调用方(列表组件)决定,还能每完成一个就地更新那张卡片。
|
|
415
|
+
// 服务端因此不需要任何调度逻辑,也不会被一个慢仓库拖住其他仓库。
|
|
416
|
+
//
|
|
417
|
+
// 安全口径与上面的 git-state 完全一致:只接受**配置里的最近目录**。
|
|
418
|
+
// 否则这个接口就是一个"对磁盘上任意仓库发起网络请求"的口子。
|
|
419
|
+
app.post('/api/recent_directories/fetch', asyncRoute(async (req, res) => {
|
|
420
|
+
const recentDirs = (await configManager.getRecentDirectories()) || [];
|
|
421
|
+
const allowed = new Map(recentDirs.map(d => [normalizeDirKey(d), d]));
|
|
422
|
+
|
|
423
|
+
const requested = typeof req.body?.path === 'string' ? req.body.path.trim() : '';
|
|
424
|
+
if (!requested) throw new HttpError(400, '缺少 path 参数');
|
|
425
|
+
const target = allowed.get(normalizeDirKey(requested));
|
|
426
|
+
if (!target) throw new HttpError(403, '该目录不在最近项目列表中');
|
|
427
|
+
|
|
428
|
+
const result = await fetchDirectoryRemotes(target);
|
|
429
|
+
// fetch 动了本地 remote-tracking 引用 → 顺手重探一次状态回给前端:
|
|
430
|
+
// 省掉一次往返,且 useCache:false 保证读到的是 fetch 之后的值
|
|
431
|
+
// (否则会拿到 15s TTL 里那份 fetch 之前的快照,等于白刷)。
|
|
432
|
+
let state = null;
|
|
433
|
+
if (result.status === 'ok') {
|
|
434
|
+
const probed = await probeDirectoryGitStates([target], { useCache: false });
|
|
435
|
+
state = probed[target] || null;
|
|
436
|
+
}
|
|
437
|
+
logger.info(`[recent_directories/fetch] ${result.status}${result.reason ? `(${result.reason})` : ''} ${target}`);
|
|
438
|
+
|
|
439
|
+
// success 表示"这次请求处理完了",fetch 本身的结果在 status 里 ——
|
|
440
|
+
// 跳过(非仓库/无 remote)不是接口错误,前端要按三态分别计数。
|
|
441
|
+
res.json({ success: true, path: target, ...result, state });
|
|
442
|
+
}));
|
|
443
|
+
|
|
407
444
|
// 在资源管理器/访达中打开当前目录
|
|
408
445
|
app.post('/api/open_directory', asyncRoute(async (req, res) => {
|
|
409
446
|
try {
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
// 对「最近项目」里的**一个**目录执行 git fetch,把卡片上「领先/落后」徽标
|
|
16
|
+
// 从"上次 fetch 时的快照"拉回真实状态。
|
|
17
|
+
//
|
|
18
|
+
// 为什么单独一个模块,不塞进 directoryGitState.js:
|
|
19
|
+
// 那个模块的契约是「纯读探测、不联网」(见其文件头注释)。fetch 是网络操作,
|
|
20
|
+
// 混进去会让"探测十几个目录"这条路径随时可能变成十几个网络请求 ——
|
|
21
|
+
// 那是它当初刻意避开的坑,不能在同一个文件里再挖回来。
|
|
22
|
+
//
|
|
23
|
+
// 为什么一次只处理一个目录、并发交给调用方:
|
|
24
|
+
// 列表里十几个仓库同时联网会互相抢网络与代理,也更容易撞出一堆凭据提示。
|
|
25
|
+
// 拆成单目录请求后,每个请求的耗时被**单个超时**箍住(不会出现一挂两分钟、
|
|
26
|
+
// 还看不到进度的大请求),前端也能每完成一个项目就地更新那张卡片的徽标。
|
|
27
|
+
//
|
|
28
|
+
// 绝不能停在交互式凭据提示上(这是批量动作,卡一个仓库就卡掉整轮刷新):
|
|
29
|
+
// - GIT_TERMINAL_PROMPT=0 让 git 直接失败,而不是在终端上等人输密码
|
|
30
|
+
// - GIT_SSH_COMMAND 注入 BatchMode=yes,让 ssh 的 passphrase 也变成明确报错;
|
|
31
|
+
// 用户自己设过 GIT_SSH_COMMAND 就不动他的(那可能是他的跳板机/密钥配置)
|
|
32
|
+
// 两者叠加的效果是:需要认证又没存凭据的仓库会**快速失败**并回报一句人话,
|
|
33
|
+
// 而不是留下一个永远不返回的进程。
|
|
34
|
+
//
|
|
35
|
+
// 不做的事:
|
|
36
|
+
// - 不加 --prune:prune 与否跟随仓库/全局的 fetch.prune 配置(与应用里
|
|
37
|
+
// 「Git Fetch All」按钮同口径),不在这里替用户改口径。
|
|
38
|
+
// - 不走 execGitCommand:那个函数固定跑在当前项目 cwd,还会写命令历史并
|
|
39
|
+
// 广播 socket —— 刷新十几个无关目录不该污染当前项目的命令历史。
|
|
40
|
+
import { execFile } from 'node:child_process';
|
|
41
|
+
import fs from 'node:fs/promises';
|
|
42
|
+
import { normalizeDirKey } from './directoryGitState.js';
|
|
43
|
+
|
|
44
|
+
/** 单个目录 fetch 的超时。网络操作,比 status 那 5s 宽松得多 */
|
|
45
|
+
export const DEFAULT_FETCH_TIMEOUT_MS = 30000;
|
|
46
|
+
|
|
47
|
+
/** fetch 结果状态 */
|
|
48
|
+
export const FETCH_STATUS = {
|
|
49
|
+
OK: 'ok',
|
|
50
|
+
SKIPPED: 'skipped',
|
|
51
|
+
FAILED: 'failed',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** 跳过原因(都不算错误,前端只用于计数与排障) */
|
|
55
|
+
export const SKIP_REASON = {
|
|
56
|
+
/** 目录不存在(或不是目录) */
|
|
57
|
+
MISSING: 'missing',
|
|
58
|
+
/** 目录存在但不是 Git 仓库 */
|
|
59
|
+
NOT_A_REPO: 'not-a-repo',
|
|
60
|
+
/** 是仓库但一个 remote 都没配 —— fetch 无事可做 */
|
|
61
|
+
NO_REMOTE: 'no-remote',
|
|
62
|
+
/** 同一目录已有一轮 fetch 在进行中(多标签页/重复点击) */
|
|
63
|
+
IN_FLIGHT: 'in-flight',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const MAX_BUFFER = 4 * 1024 * 1024;
|
|
67
|
+
/** 报错只回第一行、并截断到这个长度:tooltip 里塞整份 git 输出没法看 */
|
|
68
|
+
const ERROR_LINE_MAX = 200;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 正在 fetch 的目录集合(normalizeDirKey 形式)。
|
|
72
|
+
*
|
|
73
|
+
* 为什么需要它:git fetch 之间会抢 `.git/FETCH_HEAD.lock`,后到的会以
|
|
74
|
+
* "Unable to create '.../FETCH_HEAD.lock': File exists" 失败 ——
|
|
75
|
+
* 用户看到的就是一条莫名其妙的"刷新失败"。前端按钮本身就防了重复点击,
|
|
76
|
+
* 这里防的是多标签页/多实例同时刷同一个项目。
|
|
77
|
+
*/
|
|
78
|
+
const inFlight = new Set();
|
|
79
|
+
|
|
80
|
+
/** 判定 git 的失败输出是否属于"这不是个仓库"(与 directoryGitState 同口径) */
|
|
81
|
+
export function isNotARepoMessage(text) {
|
|
82
|
+
return /not a git repository|not a git repo/i.test(String(text || ''));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 取 git 输出的第一行有效内容。
|
|
87
|
+
* git 的 stderr 里混着以 \r 结尾的进度条("Receiving objects: 42%")和多行提示,
|
|
88
|
+
* 整份塞进 tooltip 没法读,所以要按 \r 和 \n **都**拆行。
|
|
89
|
+
*/
|
|
90
|
+
export function firstErrorLine(text, maxLen = ERROR_LINE_MAX) {
|
|
91
|
+
const lines = String(text || '')
|
|
92
|
+
.split(/[\r\n]+/)
|
|
93
|
+
.map(l => l.trim())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
const line = lines[0] || '';
|
|
96
|
+
return line.length > maxLen ? `${line.slice(0, maxLen)}…` : line;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* fetch 子进程的环境变量。纯函数,单测覆盖。
|
|
101
|
+
*
|
|
102
|
+
* GIT_CONFIG_PARAMETERS 与目录状态探测保持一致(关闭路径转义,否则中文/空格
|
|
103
|
+
* 路径在报错里会变成 \xxx 八进制,用户没法对着看)。
|
|
104
|
+
*/
|
|
105
|
+
export function buildFetchEnv(baseEnv = process.env) {
|
|
106
|
+
const env = {
|
|
107
|
+
...baseEnv,
|
|
108
|
+
GIT_CONFIG_PARAMETERS: "'core.quotepath=false'",
|
|
109
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
110
|
+
};
|
|
111
|
+
if (!baseEnv.GIT_SSH_COMMAND) env.GIT_SSH_COMMAND = 'ssh -oBatchMode=yes';
|
|
112
|
+
return env;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function execFileAsync(file, args, options) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
execFile(file, args, options, (error, stdout, stderr) => {
|
|
118
|
+
if (error) {
|
|
119
|
+
error.stdout = stdout;
|
|
120
|
+
error.stderr = stderr;
|
|
121
|
+
reject(error);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
resolve({ stdout, stderr });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function runGit(args, timeoutMs) {
|
|
130
|
+
return execFileAsync('git', args, {
|
|
131
|
+
timeout: timeoutMs,
|
|
132
|
+
maxBuffer: MAX_BUFFER,
|
|
133
|
+
windowsHide: true,
|
|
134
|
+
env: buildFetchEnv(process.env),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 把 catch 到的子进程错误压成一句能在 tooltip 里显示的话 */
|
|
139
|
+
export function describeFetchError(error, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
140
|
+
if (error?.killed || error?.signal) {
|
|
141
|
+
// 超时的话,git 自己不会说什么有用的 —— 由前端把 timeout 翻成本地化文案
|
|
142
|
+
return { error: `git fetch timed out after ${Math.round(timeoutMs / 1000)}s`, timeout: true };
|
|
143
|
+
}
|
|
144
|
+
const text = `${error?.stderr || ''}\n${error?.message || ''}`;
|
|
145
|
+
if (isNotARepoMessage(text)) return { error: 'not a git repository' };
|
|
146
|
+
return { error: firstErrorLine(error?.stderr) || firstErrorLine(error?.message) || 'git fetch failed' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 对单个目录执行 `git fetch --all`。
|
|
151
|
+
*
|
|
152
|
+
* @param {string} dirPath
|
|
153
|
+
* @param {{ timeoutMs?: number }} [options]
|
|
154
|
+
* @returns {Promise<{ status: 'ok'|'skipped'|'failed', reason?: string, error?: string, timeout?: boolean, timeoutSeconds?: number }>}
|
|
155
|
+
* 只有 ok / skipped / failed 三种终态,**不抛异常** ——
|
|
156
|
+
* 列表里一个仓库失败不该让整轮刷新中断。
|
|
157
|
+
*/
|
|
158
|
+
export async function fetchDirectoryRemotes(dirPath, { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS } = {}) {
|
|
159
|
+
const dir = String(dirPath || '').trim();
|
|
160
|
+
if (!dir) return { status: FETCH_STATUS.SKIPPED, reason: SKIP_REASON.MISSING };
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const stat = await fs.stat(dir);
|
|
164
|
+
if (!stat.isDirectory()) return { status: FETCH_STATUS.SKIPPED, reason: SKIP_REASON.MISSING };
|
|
165
|
+
} catch {
|
|
166
|
+
return { status: FETCH_STATUS.SKIPPED, reason: SKIP_REASON.MISSING };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const key = normalizeDirKey(dir);
|
|
170
|
+
if (inFlight.has(key)) return { status: FETCH_STATUS.SKIPPED, reason: SKIP_REASON.IN_FLIGHT };
|
|
171
|
+
inFlight.add(key);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// 先问一句"有哪些 remote":一次纯本地读,就能区分三种不需要联网的情况
|
|
175
|
+
// (不是仓库 / 没配 remote / 正常该 fetch),避免对非仓库目录发起网络请求。
|
|
176
|
+
let remotesStdout = '';
|
|
177
|
+
try {
|
|
178
|
+
({ stdout: remotesStdout } = await runGit(['-C', dir, 'remote'], timeoutMs));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const text = `${error?.stderr || ''}\n${error?.message || ''}`;
|
|
181
|
+
if (isNotARepoMessage(text)) return { status: FETCH_STATUS.SKIPPED, reason: SKIP_REASON.NOT_A_REPO };
|
|
182
|
+
const described = describeFetchError(error, timeoutMs);
|
|
183
|
+
return { status: FETCH_STATUS.FAILED, ...described, timeoutSeconds: described.timeout ? Math.round(timeoutMs / 1000) : undefined };
|
|
184
|
+
}
|
|
185
|
+
if (!String(remotesStdout || '').trim()) {
|
|
186
|
+
return { status: FETCH_STATUS.SKIPPED, reason: SKIP_REASON.NO_REMOTE };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
// --no-optional-locks:不抢 index.lock,不打扰用户正在跑的 git 操作
|
|
191
|
+
await runGit(['-C', dir, '--no-optional-locks', 'fetch', '--all'], timeoutMs);
|
|
192
|
+
return { status: FETCH_STATUS.OK };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const described = describeFetchError(error, timeoutMs);
|
|
195
|
+
return {
|
|
196
|
+
status: FETCH_STATUS.FAILED,
|
|
197
|
+
...described,
|
|
198
|
+
timeoutSeconds: described.timeout ? Math.round(timeoutMs / 1000) : undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
inFlight.delete(key);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 仅供测试:清空 in-flight 标记 */
|
|
207
|
+
export function clearFetchInFlight() {
|
|
208
|
+
inFlight.clear();
|
|
209
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
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
|
+
// 单目录 fetch 单元测试(node:test 内置)。
|
|
16
|
+
//
|
|
17
|
+
// 最重要的一条是"真实远端"那组:先复现列表误报的现象(远端有新提交,但本地
|
|
18
|
+
// remote-tracking 引用还没更新 → 探测出的 behind 仍是 0),再证明 fetch 之后
|
|
19
|
+
// 能读到真实落后数 —— 这正是「刷新全部」按钮存在的理由,退化回去必须报红。
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs/promises';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { execFile } from 'node:child_process';
|
|
26
|
+
import { promisify } from 'node:util';
|
|
27
|
+
import {
|
|
28
|
+
fetchDirectoryRemotes,
|
|
29
|
+
buildFetchEnv,
|
|
30
|
+
firstErrorLine,
|
|
31
|
+
isNotARepoMessage,
|
|
32
|
+
describeFetchError,
|
|
33
|
+
clearFetchInFlight,
|
|
34
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
35
|
+
FETCH_STATUS,
|
|
36
|
+
SKIP_REASON,
|
|
37
|
+
} from './directoryFetch.js';
|
|
38
|
+
import { probeDirectoryGitState } from './directoryGitState.js';
|
|
39
|
+
|
|
40
|
+
const execFileAsync = promisify(execFile);
|
|
41
|
+
const git = (args, cwd) => execFileAsync('git', args, { cwd, windowsHide: true });
|
|
42
|
+
// 提交必须显式给身份并关掉签名,否则依赖开发机的全局 git 配置 → CI 上必挂
|
|
43
|
+
const gitCommit = (dir, msg) => git([
|
|
44
|
+
'-c', 'user.name=zen-test',
|
|
45
|
+
'-c', 'user.email=zen-test@example.invalid',
|
|
46
|
+
'-c', 'commit.gpgsign=false',
|
|
47
|
+
'commit', '-q', '-m', msg,
|
|
48
|
+
], dir);
|
|
49
|
+
// 本地路径当 remote 时统一成正斜杠,免得 Windows 反斜杠被当转义
|
|
50
|
+
const asRemote = p => p.replace(/\\/g, '/');
|
|
51
|
+
|
|
52
|
+
// ── 纯函数 ──────────────────────────────────────────────────────────────
|
|
53
|
+
test('buildFetchEnv: 关掉交互式凭据提示,且不覆盖用户自己的 GIT_SSH_COMMAND', () => {
|
|
54
|
+
const withOwn = buildFetchEnv({ PATH: '/usr/bin', GIT_SSH_COMMAND: 'ssh -i /my/key' });
|
|
55
|
+
assert.equal(withOwn.GIT_TERMINAL_PROMPT, '0', '批量刷新绝不能停在终端等输密码');
|
|
56
|
+
assert.equal(withOwn.GIT_SSH_COMMAND, 'ssh -i /my/key', '用户自己的 ssh 配置不能被顶掉');
|
|
57
|
+
assert.equal(withOwn.PATH, '/usr/bin', '原有环境变量要带过去');
|
|
58
|
+
assert.match(withOwn.GIT_CONFIG_PARAMETERS, /core\.quotepath=false/);
|
|
59
|
+
|
|
60
|
+
const injected = buildFetchEnv({ PATH: '/usr/bin' });
|
|
61
|
+
assert.match(injected.GIT_SSH_COMMAND, /BatchMode=yes/, 'ssh 的 passphrase 提示也要变成明确报错');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('isNotARepoMessage: 认得 git 的两种措辞', () => {
|
|
65
|
+
assert.equal(isNotARepoMessage('fatal: not a git repository (or any of the parent directories): .git'), true);
|
|
66
|
+
assert.equal(isNotARepoMessage('fatal: not a git repo'), true);
|
|
67
|
+
assert.equal(isNotARepoMessage('fatal: unable to access ... Could not resolve host'), false);
|
|
68
|
+
assert.equal(isNotARepoMessage(''), false);
|
|
69
|
+
assert.equal(isNotARepoMessage(undefined), false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('firstErrorLine: 跳过空行与 \\r 进度条,并截断过长内容', () => {
|
|
73
|
+
assert.equal(firstErrorLine('Receiving objects: 1%\rReceiving objects: 50%\r\n\nfatal: boom\nsecond line'), 'Receiving objects: 1%');
|
|
74
|
+
assert.equal(firstErrorLine('', 10), '');
|
|
75
|
+
const long = firstErrorLine('x'.repeat(500), 20);
|
|
76
|
+
assert.equal(long.length, 21, '20 个字符 + 省略号');
|
|
77
|
+
assert.ok(long.endsWith('…'));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('describeFetchError: 超时被单独标出来(前端据此换成中文文案)', () => {
|
|
81
|
+
const timedOut = describeFetchError({ killed: true, signal: 'SIGTERM', stderr: '' }, 30000);
|
|
82
|
+
assert.equal(timedOut.timeout, true);
|
|
83
|
+
assert.match(timedOut.error, /30s/);
|
|
84
|
+
|
|
85
|
+
const netFail = describeFetchError({ stderr: 'fatal: unable to access ...', message: 'Command failed' }, 30000);
|
|
86
|
+
assert.equal(netFail.timeout, undefined);
|
|
87
|
+
assert.equal(netFail.error, 'fatal: unable to access ...');
|
|
88
|
+
|
|
89
|
+
// 什么都没有时给一句兜底文案,不能返回空串(tooltip 上会是空的一行)
|
|
90
|
+
assert.equal(describeFetchError({}, 30000).error.length > 0, true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── 真实文件系统 + git ───────────────────────────────────────────────────
|
|
94
|
+
test('不存在的目录 / 普通目录 → skipped,不抛错也不联网', async () => {
|
|
95
|
+
const missing = path.join(os.tmpdir(), 'zen-fetch-missing-' + Date.now());
|
|
96
|
+
const missingResult = await fetchDirectoryRemotes(missing);
|
|
97
|
+
assert.equal(missingResult.status, FETCH_STATUS.SKIPPED);
|
|
98
|
+
assert.equal(missingResult.reason, SKIP_REASON.MISSING);
|
|
99
|
+
|
|
100
|
+
const plain = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-fetch-plain-'));
|
|
101
|
+
try {
|
|
102
|
+
const plainResult = await fetchDirectoryRemotes(plain);
|
|
103
|
+
assert.equal(plainResult.status, FETCH_STATUS.SKIPPED);
|
|
104
|
+
assert.equal(plainResult.reason, SKIP_REASON.NOT_A_REPO);
|
|
105
|
+
} finally {
|
|
106
|
+
await fs.rm(plain, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('空参数 → skipped(missing),不碰文件系统', async () => {
|
|
111
|
+
for (const input of ['', ' ', undefined, null]) {
|
|
112
|
+
const r = await fetchDirectoryRemotes(input);
|
|
113
|
+
assert.equal(r.status, FETCH_STATUS.SKIPPED);
|
|
114
|
+
assert.equal(r.reason, SKIP_REASON.MISSING);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('仓库没配 remote → skipped(no-remote):fetch 无事可做', async () => {
|
|
119
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-fetch-noremote-'));
|
|
120
|
+
try {
|
|
121
|
+
await git(['init', '-q', '-b', 'main'], dir);
|
|
122
|
+
const r = await fetchDirectoryRemotes(dir);
|
|
123
|
+
assert.equal(r.status, FETCH_STATUS.SKIPPED);
|
|
124
|
+
assert.equal(r.reason, SKIP_REASON.NO_REMOTE);
|
|
125
|
+
} finally {
|
|
126
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('远端不可达 → failed 且带一句可读的报错,不抛异常', async () => {
|
|
131
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-fetch-dead-'));
|
|
132
|
+
try {
|
|
133
|
+
await git(['init', '-q', '-b', 'main'], dir);
|
|
134
|
+
// 指向一个不存在的本地路径:git 会立刻报 "does not appear to be a git repository",
|
|
135
|
+
// 从而稳定地覆盖 failed 分支(不用真实网络,也就不会 flaky)
|
|
136
|
+
await git(['remote', 'add', 'origin', asRemote(path.join(dir, 'nope.git'))], dir);
|
|
137
|
+
const r = await fetchDirectoryRemotes(dir);
|
|
138
|
+
assert.equal(r.status, FETCH_STATUS.FAILED);
|
|
139
|
+
assert.equal(r.timeout, undefined);
|
|
140
|
+
assert.ok(r.error && r.error.length > 0, '必须带回非空的报错文案');
|
|
141
|
+
} finally {
|
|
142
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('同一目录并发 fetch → 不出现 FETCH_HEAD.lock 互撞的失败', async () => {
|
|
147
|
+
clearFetchInFlight();
|
|
148
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-fetch-race-'));
|
|
149
|
+
const originDir = path.join(root, 'origin.git');
|
|
150
|
+
const seedDir = path.join(root, 'seed');
|
|
151
|
+
const workDir = path.join(root, 'work');
|
|
152
|
+
try {
|
|
153
|
+
await git(['init', '-q', '--bare', '-b', 'main', originDir], root);
|
|
154
|
+
await git(['init', '-q', '-b', 'main', seedDir], root);
|
|
155
|
+
await fs.writeFile(path.join(seedDir, 'README.md'), 'seed\n');
|
|
156
|
+
await git(['add', '.'], seedDir);
|
|
157
|
+
await gitCommit(seedDir, 'init');
|
|
158
|
+
await git(['remote', 'add', 'origin', asRemote(originDir)], seedDir);
|
|
159
|
+
await git(['push', '-q', '-u', 'origin', 'main'], seedDir);
|
|
160
|
+
await git(['clone', '-q', asRemote(originDir), workDir], root);
|
|
161
|
+
|
|
162
|
+
const results = await Promise.all([
|
|
163
|
+
fetchDirectoryRemotes(workDir),
|
|
164
|
+
fetchDirectoryRemotes(workDir),
|
|
165
|
+
]);
|
|
166
|
+
assert.ok(results.some(r => r.status === FETCH_STATUS.OK), '其中一个应该真的执行了 fetch');
|
|
167
|
+
assert.ok(
|
|
168
|
+
results.every(r => r.status !== FETCH_STATUS.FAILED),
|
|
169
|
+
'并发时必须被 in-flight 拦下,而不是让两个 git 去抢 FETCH_HEAD.lock'
|
|
170
|
+
);
|
|
171
|
+
} finally {
|
|
172
|
+
clearFetchInFlight();
|
|
173
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ── 核心回归:按钮存在的理由 ────────────────────────────────────────────────
|
|
178
|
+
// 复现"列表看不到真实状态"的现场:别人推了新提交,本地 remote-tracking 引用
|
|
179
|
+
// 还停在旧位置 → 探测出的 behind 是 0(列表显示"已同步",其实该 pull 了)。
|
|
180
|
+
// fetch 之后必须能读到真实的落后数。
|
|
181
|
+
test('真实远端:fetch 之前 behind 滞后,fetch 之后读到真实落后数', async () => {
|
|
182
|
+
clearFetchInFlight();
|
|
183
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-fetch-real-'));
|
|
184
|
+
const originDir = path.join(root, 'origin.git');
|
|
185
|
+
const seedDir = path.join(root, 'seed');
|
|
186
|
+
const workDir = path.join(root, 'work');
|
|
187
|
+
const otherDir = path.join(root, 'other');
|
|
188
|
+
try {
|
|
189
|
+
// 1) 造一个已有 main 的裸远端
|
|
190
|
+
await git(['init', '-q', '--bare', '-b', 'main', originDir], root);
|
|
191
|
+
await git(['init', '-q', '-b', 'main', seedDir], root);
|
|
192
|
+
await fs.writeFile(path.join(seedDir, 'README.md'), 'seed\n');
|
|
193
|
+
await git(['add', '.'], seedDir);
|
|
194
|
+
await gitCommit(seedDir, 'init');
|
|
195
|
+
await git(['remote', 'add', 'origin', asRemote(originDir)], seedDir);
|
|
196
|
+
await git(['push', '-q', '-u', 'origin', 'main'], seedDir);
|
|
197
|
+
|
|
198
|
+
// 2) work 克隆出来与远端同步
|
|
199
|
+
await git(['clone', '-q', asRemote(originDir), workDir], root);
|
|
200
|
+
const synced = await probeDirectoryGitState(workDir);
|
|
201
|
+
assert.equal(synced.behind, 0);
|
|
202
|
+
|
|
203
|
+
// 3) 另一个人推了两个提交,work 还没 fetch
|
|
204
|
+
await git(['clone', '-q', asRemote(originDir), otherDir], root);
|
|
205
|
+
await fs.writeFile(path.join(otherDir, 'a.txt'), 'a\n');
|
|
206
|
+
await git(['add', '.'], otherDir);
|
|
207
|
+
await gitCommit(otherDir, 'remote work 1');
|
|
208
|
+
await fs.writeFile(path.join(otherDir, 'b.txt'), 'b\n');
|
|
209
|
+
await git(['add', '.'], otherDir);
|
|
210
|
+
await gitCommit(otherDir, 'remote work 2');
|
|
211
|
+
await git(['push', '-q'], otherDir);
|
|
212
|
+
|
|
213
|
+
const stale = await probeDirectoryGitState(workDir);
|
|
214
|
+
assert.equal(stale.behind, 0, 'fetch 之前本地引用还是旧的 —— 列表就是在这一步误报"已同步"');
|
|
215
|
+
|
|
216
|
+
// 4) 刷新这个目录
|
|
217
|
+
const result = await fetchDirectoryRemotes(workDir);
|
|
218
|
+
assert.equal(result.status, FETCH_STATUS.OK);
|
|
219
|
+
|
|
220
|
+
const fresh = await probeDirectoryGitState(workDir);
|
|
221
|
+
assert.equal(fresh.behind, 2, 'fetch 之后必须读出真实的落后提交数');
|
|
222
|
+
assert.equal(fresh.ahead, 0);
|
|
223
|
+
assert.equal(fresh.hasUpstream, true);
|
|
224
|
+
} finally {
|
|
225
|
+
clearFetchInFlight();
|
|
226
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('DEFAULT_FETCH_TIMEOUT_MS: 比状态探测宽松(网络操作)', () => {
|
|
231
|
+
assert.equal(DEFAULT_FETCH_TIMEOUT_MS, 30000);
|
|
232
|
+
});
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import"./rolldown-runtime-C0FnF6B9.js";import{$n as e,At as t,Bn as n,Dn as r,En as i,Er as a,G as o,Gt as s,In as c,On as l,Pn as u,Pr as d,Qr as f,Tn as p,Wn as m,Y as h,Yr as g,Zn as _,_n as ee,a as v,bn as te,br as y,cn as ne,cr as b,gt as re,i as x,kn as S,lr as C,o as ie,or as w,pr as T,qr as E,tt as D,un as O,ut as ae,vr as k,xn as oe,zt as se}from"./element-plus-KTmRV-so.js";import{a as A,n as j,r as ce}from"./vendor-92fKgtnz.js";import{n as M,t as N}from"./_plugin-vue_export-helper-CQjg8MXm.js";import{t as P}from"./configStore-C7HOx5S1.js";T();var F=65536;function I(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:z(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>F&&(a=`…(前文已截断)\n${a.slice(-65536)}`);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||j(),name:e.function?.name||``,arguments:e.function?.arguments||``,argsPreview:L(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>F&&(e=`…(前文已截断)\n${e.slice(-65536)}`),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 L(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 R(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 z(e){let t=/^data:([^;,]+)/.exec(e);return t?t[1]:``}function B(){let e=P(),t=y([]),n=y(!1),r=y(null),i=y([]),a=y(!1),o=y(!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){v.error(n.error||M(`@AGENT:加载会话列表失败`));return}t.value=Array.isArray(n.sessions)?n.sessions:[]}catch(e){if(r!==l)return;v.error(M(`@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){v.error(t.error||M(`@AGENT:加载会话失败`));return}i.value=I(t.session)}catch(e){v.error(M(`@AGENT:加载会话失败`)+`: `+(e?.message||e))}finally{o.value=!1}}async function f(e){try{await x.confirm(M(`@AGENT:确认删除该会话`),M(`@AGENT:删除后无法恢复`),{type:`warning`})}catch{return}try{let n=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`DELETE`}).then(e=>e.json());if(!n.success){v.error(n.error||M(`@AGENT:删除失败`));return}t.value=t.value.filter(t=>t.sessionId!==e),r.value===e&&(r.value=null,i.value=[]),v.success(M(`@AGENT:已删除`))}catch(e){v.error(M(`@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){v.error(r.error||M(`@AGENT:重命名失败`));return}let i=t.value.find(t=>t.sessionId===e);i&&(i.title=n),v.success(M(`@AGENT:已重命名`))}catch(e){v.error(M(`@AGENT:重命名失败`)+`: `+(e?.message||e))}}function m(){r.value=null,i.value=[]}async function h(t,n=[]){let o=n.filter(e=>e?.file?.type?.startsWith(`image/`));if(o.length<n.length&&v.warning(M(`@AGENT:仅支持发送图片,非图片文件已忽略`)),!t.trim()&&o.length===0||a.value)return;let l=(await Promise.allSettled(o.map(e=>R(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:j(),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=k({id:j(),role:`assistant`,content:``,status:`pending`,createdAt:Date.now()});i.value.push(m),a.value=!0;let h=[];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=A(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=k({id:i.toolCallId||j(),name:i.name||``,argsPreview:i.argsPreview||``,arguments:i.argsPreview||``,status:`running`,result:``});h.push(e),m.toolCalls||=[],m.toolCalls.push(e),m.status===`pending`&&(m.status=`streaming`);break}case`tool_result`:{let e=h.find(e=>e.id===i.toolCallId);e&&(e.result=String(i.result||``),e.status=`done`);break}case`done`:{let e=i.content||m.content,t=A(e);t.reasoning&&!m.reasoning&&(m.reasoning=t.reasoning),m.content=t.content,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`);break}case`error`:m.status=`error`,m.error=String(i.error||M(`@AGENT:未知错误`)),m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`)}}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
|
-
[`+M(`@AGENT:已停止`)+`]`,m.status=`done`,m.reasoningStatus===`streaming`&&(m.reasoningStatus=`done`)):(m.status=`error`,m.error=e?.message||String(e),v.error(m.error||M(`@AGENT:对话失败`)))}finally{d===c&&(a.value=!1,s=null)}}function g(){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:h,stop:g}}n(),T(),d(),O();var V={class:`agent-view`},H={class:`sidebar-header`},U={class:`sidebar-search`},W=[`placeholder`],G={class:`session-list`},le={key:0,class:`empty-state`},ue={class:`empty-text`},de={key:0,class:`empty-hint`},fe=[`onClick`],pe={class:`session-item-main`},K={class:`session-item-title`},me={class:`session-item-meta`},he={class:`meta-time`},ge={class:`meta-count`},_e={key:0,class:`meta-source cli`},ve=[`onClick`],ye=[`onClick`],be={class:`agent-chat-area`},xe={key:0,class:`chat-loading`},Se={key:0,class:`stop-button-bar`},q=N(c({__name:`AgentView`,setup(n){let c=P(),{sessions:d,sessionsLoading:T,currentSessionId:O,messages:k,isStreaming:A,sessionLoading:j,loadSessions:N,loadSession:F,deleteSession:I,renameSession:L,newSession:R,sendMessage:z,stop:q}=B(),Ce=p(()=>{let e=c.theme;return e===`dark`||e===`auto`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}),J=y(``),Y=p(()=>{if(!J.value.trim())return d.value;let e=J.value.toLowerCase();return d.value.filter(t=>(t.title||``).toLowerCase().includes(e)||(t.model||``).toLowerCase().includes(e))}),we=p(()=>[{id:`p1`,label:M(`@AGENT:查看项目结构`),prompt:M(`@AGENT:prompt_p1`)},{id:`p2`,label:M(`@AGENT:分析代码质量`),prompt:M(`@AGENT:prompt_p2`)},{id:`p3`,label:M(`@AGENT:帮我写测试`),prompt:M(`@AGENT:prompt_p3`)},{id:`p4`,label:M(`@AGENT:Git 状态检查`),prompt:M(`@AGENT:prompt_p4`)},{id:`p5`,label:M(`@AGENT:帮我启动项目`),prompt:M(`@AGENT:prompt_p5`)}]);async function Te(e){await z(e.text,e.files),await m(),Z()}async function Ee(e){await z(e.prompt),await m(),Z()}let X=y(null);function Z(e=!0){X.value?.scrollToBottom(e)}w(()=>k.value.length,()=>{m(()=>Z(!1))});function De(e){if(A.value){v.warning(M(`@AGENT:请先停止当前生成`));return}F(e)}function Oe(){if(A.value){v.warning(M(`@AGENT:请先停止当前生成`));return}R()}function ke(e,t){if(t.stopPropagation(),A.value&&O.value===e){v.warning(M(`@AGENT:请先停止当前生成`));return}I(e)}async function Ae(e,t,n){n.stopPropagation();try{let{value:n}=await x.prompt(M(`@AGENT:请输入新的会话标题`),M(`@AGENT:重命名会话`),{inputValue:t,inputPattern:/.+/,inputErrorMessage:M(`@AGENT:标题不能为空`)});n&&n!==t&&await L(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<6048e5){let e=Math.floor(r/864e5);return e===0?M(`@AGENT:今天`):`${e}${M(`@AGENT:天前`)}`}return`${t.getFullYear()}-${i(t.getMonth()+1)}-${i(t.getDate())}`}catch{return e}}let Q=y(280),$=y(!1);function Me(e){$.value=!0;let t=e.clientX,n=Q.value,r=e=>{let r=e.clientX-t,i=Math.max(200,Math.min(500,n+r));Q.value=i},i=()=>{$.value=!1,document.removeEventListener(`mousemove`,r),document.removeEventListener(`mouseup`,i)};document.addEventListener(`mousemove`,r),document.addEventListener(`mouseup`,i)}return w(()=>[c.currentDirectory,A.value],async([e,t],n)=>{let r=!n||e!==n[0],i=!!n&&n[1]===!0&&!t;(r||i)&&await N(),O.value&&!A.value&&!d.value.some(e=>e.sessionId===O.value)&&R()},{immediate:!0}),(n,c)=>{let d=ie;return _(),S(`div`,V,[i(`aside`,{class:`agent-sidebar`,style:g({width:Q.value+`px`})},[i(`div`,H,[i(`button`,{class:`new-session-btn`,onClick:Oe},[u(a(h),null,{default:b(()=>[u(a(se))]),_:1}),i(`span`,null,f(a(M)(`@AGENT:新建会话`)),1)])]),i(`div`,U,[u(a(h),{class:`search-icon`},{default:b(()=>[u(a(s))]),_:1}),C(i(`input`,{"onUpdate:modelValue":c[0]||=e=>J.value=e,placeholder:a(M)(`@AGENT:搜索会话...`),class:`search-input`,type:`text`},null,8,W),[[ee,J.value]])]),C((_(),S(`div`,G,[Y.value.length===0&&!a(T)?(_(),S(`div`,le,[u(a(h),{size:28,color:`var(--text-tertiary)`},{default:b(()=>[u(a(D))]),_:1}),i(`div`,ue,f(J.value?a(M)(`@AGENT:未找到匹配的会话`):a(M)(`@AGENT:暂无会话`)),1),J.value?l(``,!0):(_(),S(`div`,de,f(a(M)(`@AGENT:点击上方按钮开始对话`)),1))])):l(``,!0),(_(!0),S(oe,null,e(Y.value,e=>(_(),S(`div`,{key:e.sessionId,class:E([`session-item`,{active:a(O)===e.sessionId}]),onClick:t=>De(e.sessionId)},[i(`div`,pe,[i(`div`,K,f(e.title||a(M)(`@AGENT:无标题`)),1),i(`div`,me,[i(`span`,he,f(je(e.updatedAt)),1),c[3]||=i(`span`,{class:`meta-dot`},`·`,-1),i(`span`,ge,f(e.messageCount)+` `+f(a(M)(`@AGENT:条`)),1),e.source===`cli`?(_(),S(`span`,_e,`CLI`)):l(``,!0)])]),i(`div`,{class:`session-item-actions`,onClick:c[1]||=te(()=>{},[`stop`])},[u(a(o),{content:a(M)(`@AGENT:重命名`),placement:`top`,"show-after":500},{default:b(()=>[i(`button`,{class:`item-action-btn`,onClick:t=>Ae(e.sessionId,e.title,t)},[u(a(h),null,{default:b(()=>[u(a(re))]),_:1})],8,ve)]),_:2},1032,[`content`]),u(a(o),{content:a(M)(`@AGENT:删除`),placement:`top`,"show-after":500},{default:b(()=>[i(`button`,{class:`item-action-btn danger`,onClick:t=>ke(e.sessionId,t)},[u(a(h),null,{default:b(()=>[u(a(ae))]),_:1})],8,ye)]),_:2},1032,[`content`])])],10,fe))),128))])),[[d,a(T)]])],4),i(`div`,{class:E([`sidebar-resizer`,{active:$.value}]),onMousedown:Me},null,34),i(`main`,be,[a(j)?(_(),S(`div`,xe,[u(a(h),{class:`is-loading`,size:32},{default:b(()=>[u(a(t))]),_:1}),i(`span`,null,f(a(M)(`@AGENT:加载会话中...`)),1)])):(_(),r(a(ce),{key:1,ref_key:`chatContainerRef`,ref:X,messages:a(k),"preset-questions":we.value,"welcome-title":a(M)(`@AGENT:智能体助手`),"welcome-description":a(M)(`@AGENT:我可以帮你阅读代码、执行命令、修改文件。选择下方话题或直接输入你的问题。`),"assistant-name":`g ai`,theme:Ce.value,disabled:a(A),"upload-config":{accept:`image/*`},placeholder:a(A)?a(M)(`@AGENT:正在生成中...`):a(M)(`@AGENT:输入消息,Enter 发送`),onSend:Te,onSelect:Ee},null,8,[`messages`,`preset-questions`,`welcome-title`,`welcome-description`,`theme`,`disabled`,`placeholder`])),u(ne,{name:`fade`},{default:b(()=>[a(A)?(_(),S(`div`,Se,[i(`button`,{class:`stop-button`,onClick:c[2]||=(...e)=>a(q)&&a(q)(...e)},[u(a(h),null,{default:b(()=>[u(a(t),{class:`is-loading`})]),_:1}),i(`span`,null,f(a(M)(`@AGENT:停止生成`)),1)])])):l(``,!0)]),_:1})])])}}}),[[`__scopeId`,`data-v-a1a388e1`]]);export{q as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.agent-view[data-v-a1a388e1]{background:var(--bg-container);height:100%;min-height:0;display:flex}.agent-sidebar[data-v-a1a388e1]{background:var(--bg-panel);border-right:1px solid var(--border-color);flex-direction:column;flex-shrink:0;display:flex;overflow:hidden}.sidebar-header[data-v-a1a388e1]{flex-shrink:0;padding:10px 12px}.new-session-btn[data-v-a1a388e1]{border:1px dashed var(--border-color);border-radius:var(--radius-md);width:100%;color:var(--text-secondary);cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;padding:8px 12px;font-family:inherit;font-size:13px;transition:all .15s;display:flex}.new-session-btn[data-v-a1a388e1]:hover{border-color:var(--color-primary);color:var(--color-primary);background:color-mix(in srgb, var(--color-primary) 6%, transparent)}.sidebar-search[data-v-a1a388e1]{flex-shrink:0;align-items:center;gap:6px;padding:0 12px 8px;display:flex}.sidebar-search .search-icon[data-v-a1a388e1]{color:var(--text-tertiary);flex-shrink:0}.sidebar-search .search-input[data-v-a1a388e1]{color:var(--text-primary);background:0 0;border:none;outline:none;flex:1;padding:4px 0;font-family:inherit;font-size:13px}.sidebar-search .search-input[data-v-a1a388e1]::placeholder{color:var(--text-tertiary)}.session-list[data-v-a1a388e1]{flex:1;min-height:0;padding:0 6px 8px;overflow-y:auto}.empty-state[data-v-a1a388e1]{text-align:center;color:var(--text-tertiary);flex-direction:column;justify-content:center;align-items:center;padding:48px 16px;display:flex}.empty-state .empty-text[data-v-a1a388e1]{color:var(--text-secondary);margin-top:10px;font-size:14px}.empty-state .empty-hint[data-v-a1a388e1]{margin-top:4px;font-size:12px}.session-item[data-v-a1a388e1]{border-radius:var(--radius-sm);cursor:pointer;align-items:flex-start;gap:4px;padding:8px 10px;transition:background .12s;display:flex;position:relative}.session-item[data-v-a1a388e1]:hover{background:var(--bg-hover)}.session-item:hover .session-item-actions[data-v-a1a388e1]{opacity:1}.session-item.active[data-v-a1a388e1]{background:color-mix(in srgb, var(--color-primary) 10%, transparent)}.session-item.active .session-item-title[data-v-a1a388e1]{color:var(--color-primary)}.session-item.active[data-v-a1a388e1]:before{content:"";background:var(--color-primary);border-radius:0 2px 2px 0;width:3px;height:20px;position:absolute;top:50%;left:0;transform:translateY(-50%)}.session-item-main[data-v-a1a388e1]{flex:1;min-width:0}.session-item-title[data-v-a1a388e1]{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;margin-bottom:2px;font-size:13px;font-weight:500;overflow:hidden}.session-item-meta[data-v-a1a388e1]{color:var(--text-tertiary);align-items:center;gap:4px;font-size:11px;display:flex}.session-item-meta .meta-dot[data-v-a1a388e1]{opacity:.5}.session-item-meta .meta-source[data-v-a1a388e1]{border-radius:3px;padding:0 4px;font-size:10px;font-weight:600}.session-item-meta .meta-source.cli[data-v-a1a388e1]{background:color-mix(in srgb, var(--color-warning) 20%, transparent);color:var(--color-warning)}.session-item-actions[data-v-a1a388e1]{opacity:0;flex-shrink:0;gap:2px;transition:opacity .12s;display:flex}.item-action-btn[data-v-a1a388e1]{width:24px;height:24px;color:var(--text-tertiary);cursor:pointer;border-radius:var(--radius-xs);background:0 0;border:none;justify-content:center;align-items:center;padding:0;transition:all .12s;display:flex}.item-action-btn[data-v-a1a388e1]:hover{background:var(--bg-hover);color:var(--text-secondary)}.item-action-btn.danger[data-v-a1a388e1]:hover{color:var(--color-danger);background:color-mix(in srgb, var(--color-danger) 10%, transparent)}.sidebar-resizer[data-v-a1a388e1]{cursor:col-resize;background:0 0;flex-shrink:0;width:4px;transition:background .15s}.sidebar-resizer[data-v-a1a388e1]:hover,.sidebar-resizer.active[data-v-a1a388e1]{background:var(--color-primary);opacity:.3}.agent-chat-area[data-v-a1a388e1]{flex-direction:column;flex:1;min-width:0;display:flex;position:relative;overflow:hidden}.chat-loading[data-v-a1a388e1]{color:var(--text-tertiary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:12px;font-size:14px;display:flex}.stop-button-bar[data-v-a1a388e1]{z-index:10;position:absolute;bottom:80px;left:50%;transform:translate(-50%)}.stop-button[data-v-a1a388e1]{border:1px solid var(--color-danger);background:var(--bg-container);color:var(--color-danger);cursor:pointer;border-radius:20px;align-items:center;gap:6px;padding:6px 16px;font-family:inherit;font-size:13px;transition:all .15s;display:flex;box-shadow:0 2px 8px #0000001f}.stop-button[data-v-a1a388e1]:hover{background:var(--color-danger);color:#fff}.fade-enter-active[data-v-a1a388e1],.fade-leave-active[data-v-a1a388e1]{transition:opacity .2s}.fade-enter-from[data-v-a1a388e1],.fade-leave-to[data-v-a1a388e1]{opacity:0}[data-theme=dark]{background:var(--bg-panel)}
|