zen-gitsync 2.18.27 → 2.19.1

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.
Files changed (51) hide show
  1. package/package.json +6 -2
  2. package/src/cli/ai/tools.js +71 -8
  3. package/src/cli/ai/webSearch.js +108 -21
  4. package/src/cli/peer.js +272 -0
  5. package/src/config.js +14 -4
  6. package/src/gitCommit.js +34 -1
  7. package/src/paths.js +6 -0
  8. package/src/peer/client.js +165 -0
  9. package/src/peer/localAsk.js +130 -0
  10. package/src/peer/server.js +183 -0
  11. package/src/peer/store.js +292 -0
  12. package/src/ui/public/assets/{AgentView-DunbYHMn.js → AgentView-BtnAYtte.js} +1 -1
  13. package/src/ui/public/assets/AppVersionBadge-BcXo-yuu.js +2 -0
  14. package/src/ui/public/assets/{BranchSelector-BKA3fL9E.js → BranchSelector-5t-T3FB6.js} +1 -1
  15. package/src/ui/public/assets/{CommitForm-CVPGpci8.js → CommitForm-BMfp3Eqh.js} +1 -1
  16. package/src/ui/public/assets/{CommonDialog-ZwJlTF5A.js → CommonDialog-BiF2fMgr.js} +1 -1
  17. package/src/ui/public/assets/{EditorView-DDzACKq1.js → EditorView-CtmZ7RZp.js} +1 -1
  18. package/src/ui/public/assets/{FlowExecutionViewer-BJOiCa6S.js → FlowExecutionViewer-D2lchSo7.js} +1 -1
  19. package/src/ui/public/assets/{FlowOrchestrationWorkspace-BDnAdRr9.js → FlowOrchestrationWorkspace-BY2zLwWB.js} +1 -1
  20. package/src/ui/public/assets/{LogList-DbGz7_dy.js → LogList-DTfAGnlt.js} +1 -1
  21. package/src/ui/public/assets/{MindmapView-mbTFeVj8.js → MindmapView-DArneR4u.js} +1 -1
  22. package/src/ui/public/assets/{MonitorView-O7I40RiB.js → MonitorView-BQvMyjEj.js} +1 -1
  23. package/src/ui/public/assets/{ProjectManagerView-D1NVbIOW.js → ProjectManagerView-BRUyrHfg.js} +1 -1
  24. package/src/ui/public/assets/{ProjectStartupButton-C21zy3Bg.js → ProjectStartupButton-D0f8OAb-.js} +1 -1
  25. package/src/ui/public/assets/{RemoteManagerDialog-CpSrItn2.js → RemoteManagerDialog-DXDXCfyD.js} +1 -1
  26. package/src/ui/public/assets/{RemoteRepoCard-CXDXPJK2.js → RemoteRepoCard-BRnjC6vm.js} +1 -1
  27. package/src/ui/public/assets/{SourceMapView-C9qTeaOC.js → SourceMapView-B6xgG5mi.js} +1 -1
  28. package/src/ui/public/assets/{SvgIcon-BmXhx-vA.js → SvgIcon-GLj_3KcS.js} +1 -1
  29. package/src/ui/public/assets/{UserInputNode-i7ER8ZCn.js → UserInputNode-C6Z6Kys_.js} +1 -1
  30. package/src/ui/public/assets/{WorkbenchView-pZz9ykZT.js → WorkbenchView-B2EUXleS.js} +1 -1
  31. package/src/ui/public/assets/_plugin-vue_export-helper-DaO48JOV.js +19 -0
  32. package/src/ui/public/assets/configStore-B_2ifbl_.js +1 -0
  33. package/src/ui/public/assets/{index-DW-2cjNW.js → index-DUzH5036.js} +63 -60
  34. package/src/ui/public/assets/index-zdGFM_7W.css +1 -0
  35. package/src/ui/public/assets/{relativeTime-DsG6UERK.js → relativeTime-DzYQUHQo.js} +1 -1
  36. package/src/ui/public/assets/{taskExecutionExport-C3dV5cuE.js → taskExecutionExport-C3vFixdJ.js} +1 -1
  37. package/src/ui/public/assets/{useWorkbenchProjects-B6k5mmVC.js → useWorkbenchProjects-0fWZsDRX.js} +1 -1
  38. package/src/ui/public/index.html +6 -6
  39. package/src/ui/server/index.js +130 -10
  40. package/src/ui/server/routes/config.js +1317 -1316
  41. package/src/ui/server/routes/peerSettings.js +164 -0
  42. package/src/ui/server/routes/workbench/agentChat.js +811 -811
  43. package/src/ui/server/routes/workbench/agentRoutes.js +9 -0
  44. package/src/ui/server/utils/ensureHub.js +101 -0
  45. package/src/ui/server/utils/instanceRegistry.js +8 -3
  46. package/src/ui/server/utils/peerRuntime.js +77 -0
  47. package/src/ui/server/utils/startServerOnAvailablePort.js +6 -0
  48. package/src/ui/public/assets/AppVersionBadge-9B4b0hbP.js +0 -2
  49. package/src/ui/public/assets/_plugin-vue_export-helper-D6ACRIgd.js +0 -19
  50. package/src/ui/public/assets/configStore-BQoByr2v.js +0 -1
  51. package/src/ui/public/assets/index-B9mqQJGC.css +0 -1
@@ -0,0 +1,130 @@
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
+ // 让"本机的 g ai 跑一轮"—— 供 peer 服务端(远端提问)使用。
16
+ //
17
+ // 为什么走本机 HTTP 而不是直接调 runAgentTurn:
18
+ // · runAgentTurn 需要 session / model / send / askUser / 各种 ctx 注入,这些
19
+ // 脚手架全在 /api/agent/chat 这个路由里,重写一遍必然和它对不上口径
20
+ // (工具集合、上下文注入、请求预算、引擎选择都会分叉);
21
+ // · 这台机器上"能跑 g ai"的唯一权威入口就是那个路由,环回打它等于复用全部行为。
22
+ // 代价是多一跳 localhost HTTP(几十微秒),换来零分叉。
23
+ //
24
+ // 走环回的请求**没有 Origin 头**,createOriginChecker() 对这种请求是放行的
25
+ // (见 middleware/originGuard.js 的头一段注释),所以不会被自己的同源守卫挡掉。
26
+
27
+ import logger from '../ui/server/utils/logger.js';
28
+
29
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
30
+
31
+ /**
32
+ * 让本机 g ai 回答一个问题。
33
+ *
34
+ * @param {object} opts
35
+ * @param {number} opts.port 本机 g ui 的端口(环回调用)
36
+ * @param {string} opts.question 提问正文
37
+ * @param {string} [opts.sessionId] 续聊用的会话 id;空 = 新会话
38
+ * @param {string} [opts.locale] 'zh' | 'en'
39
+ * @param {number} [opts.timeoutMs]
40
+ * @param {(text:string)=>void} [opts.onDelta] 正文增量回调(可选,供日志/调试)
41
+ * @returns {Promise<{answer:string, sessionId:string, ms:number}>}
42
+ */
43
+ export async function askLocalAgent({
44
+ port,
45
+ question,
46
+ sessionId = '',
47
+ locale = 'zh',
48
+ timeoutMs = DEFAULT_TIMEOUT_MS,
49
+ onDelta
50
+ } = {}) {
51
+ const text = String(question || '').trim();
52
+ if (!text) throw new Error('问题不能为空');
53
+ if (!port) throw new Error('本机端口不可用');
54
+
55
+ const startedAt = Date.now();
56
+ const controller = new AbortController();
57
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
58
+ timer.unref?.();
59
+
60
+ let res;
61
+ try {
62
+ res = await fetch(`http://127.0.0.1:${port}/api/agent/chat`, {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ Accept: 'text/event-stream'
67
+ },
68
+ body: JSON.stringify({ userMessage: text, sessionId: sessionId || undefined, locale }),
69
+ signal: controller.signal
70
+ });
71
+ } catch (err) {
72
+ clearTimeout(timer);
73
+ if (err?.name === 'AbortError') throw new Error(`本机智能体超时(${Math.round(timeoutMs / 1000)}s)`);
74
+ throw new Error(`连不上本机 g ui(127.0.0.1:${port}):${err?.message || err}`);
75
+ }
76
+
77
+ if (!res.ok || !res.body) {
78
+ clearTimeout(timer);
79
+ let detail = '';
80
+ try { detail = (await res.text()).slice(0, 500); } catch {}
81
+ throw new Error(`本机智能体返回 ${res.status}${detail ? `:${detail}` : ''}`);
82
+ }
83
+
84
+ let answer = '';
85
+ let newSessionId = sessionId || '';
86
+ let failure = '';
87
+ let buf = '';
88
+
89
+ try {
90
+ const decoder = new TextDecoder();
91
+ for await (const chunk of res.body) {
92
+ buf += decoder.decode(chunk, { stream: true });
93
+ // SSE 事件以空行分隔;一条事件里可能有多行 data:
94
+ let idx;
95
+ while ((idx = buf.indexOf('\n\n')) >= 0) {
96
+ const raw = buf.slice(0, idx);
97
+ buf = buf.slice(idx + 2);
98
+ for (const line of raw.split('\n')) {
99
+ if (!line.startsWith('data:')) continue;
100
+ const payload = line.slice(5).trim();
101
+ if (!payload) continue;
102
+ let evt;
103
+ try { evt = JSON.parse(payload); } catch { continue; }
104
+
105
+ if (evt.type === 'meta' && evt.sessionId) newSessionId = evt.sessionId;
106
+ else if (evt.type === 'content' && typeof evt.delta === 'string') {
107
+ answer += evt.delta;
108
+ try { onDelta?.(evt.delta); } catch {}
109
+ } else if (evt.type === 'done') {
110
+ // done 带的是**完整正文**,比逐段拼接更权威(重试时前面那截会被回退)
111
+ if (typeof evt.content === 'string') answer = evt.content;
112
+ } else if (evt.type === 'error') {
113
+ failure = String(evt.error || '未知错误');
114
+ }
115
+ }
116
+ }
117
+ }
118
+ } catch (err) {
119
+ clearTimeout(timer);
120
+ if (err?.name === 'AbortError') throw new Error(`本机智能体超时(${Math.round(timeoutMs / 1000)}s)`);
121
+ throw new Error(`读取本机智能体输出失败:${err?.message || err}`);
122
+ }
123
+ clearTimeout(timer);
124
+
125
+ if (failure) throw new Error(`本机智能体报错:${failure}`);
126
+ if (!answer.trim()) {
127
+ logger.warn('[peer] 本机智能体返回了空正文(可能是工具轮次结束或模型空答)');
128
+ }
129
+ return { answer: answer.trim(), sessionId: newSessionId, ms: Date.now() - startedAt };
130
+ }
@@ -0,0 +1,183 @@
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
+ // 跨机器 peer 的服务端:**一个独立端口、一张极短的路由表**。
16
+ //
17
+ // 为什么不复用主服务的端口:
18
+ // 主服务是"没有认证层,能打进来就是全权限"(api/exec、api/terminal、api/fs/write
19
+ // 都在同一张表上),而 peer 端口必须能在局域网/公网上被别的机器访问。把整张
20
+ // /api 挂到 0.0.0.0 等于把 RCE 对外开放;这里只挂 3 个只读/受限接口,
21
+ // 即使令牌泄露,攻击面也只有"问它一句话"。
22
+ // 主服务仍然只听 127.0.0.1(见 ui/server/index.js 的 Origin 守卫那一段)。
23
+ //
24
+ // 鉴权:Authorization: Bearer <本机令牌>,常量时间比较(store.tokenEquals)。
25
+ // 失败一律 401,且**不区分**"没带令牌"和"令牌错",避免探测。
26
+
27
+ import http from 'node:http';
28
+ import express from 'express';
29
+
30
+ import { tokenEquals, getSessionId, setSessionId, maskToken } from './store.js';
31
+ import { askLocalAgent } from './localAsk.js';
32
+ import logger from '../ui/server/utils/logger.js';
33
+
34
+ const MAX_QUESTION_CHARS = 4000;
35
+ const DEFAULT_ASK_TIMEOUT_MS = 10 * 60 * 1000;
36
+ const STARTED_AT = new Date().toISOString();
37
+
38
+ /**
39
+ * 造 peer 的 express app(不含 listen,方便单测直接 supertest/裸 http 打)。
40
+ *
41
+ * @param {object} opts
42
+ * @param {{id:string,name:string,avatar:string,token:string}} opts.self 本机身份
43
+ * @param {() => number|string} opts.getLocalPort 返回本机 g ui 端口(环回调用用)
44
+ * @param {(args:object)=>Promise<{answer:string,sessionId:string,ms:number}>} [opts.ask]
45
+ * 真正执行提问的函数;默认走 askLocalAgent(单测可替换)
46
+ * @param {number} [opts.timeoutMs]
47
+ * @param {() => string} [opts.getVersion]
48
+ * @param {(key:string)=>Promise<string>} [opts.getSession] 会话读取(单测注入内存版)
49
+ * @param {(key:string,id:string)=>Promise<void>} [opts.setSession] 会话写回
50
+ */
51
+ export function createPeerApp({
52
+ self,
53
+ getLocalPort,
54
+ ask,
55
+ timeoutMs = DEFAULT_ASK_TIMEOUT_MS,
56
+ getVersion = () => '',
57
+ getSession = getSessionId,
58
+ setSession = setSessionId
59
+ } = {}) {
60
+ if (!self || !self.token) throw new Error('createPeerApp 需要带令牌的本机身份(先 ensureSelf)');
61
+
62
+ const app = express();
63
+ app.disable('x-powered-by');
64
+ app.use(express.json({ limit: '1mb' }));
65
+
66
+ // 令牌校验:所有 /api/peer/* 都要过
67
+ app.use('/api/peer', (req, res, next) => {
68
+ const header = String(req.headers.authorization || '');
69
+ const given = header.startsWith('Bearer ') ? header.slice(7).trim() : '';
70
+ if (!given || !tokenEquals(given, self.token)) {
71
+ // 不回显令牌、不提示"差几位"—— 只给一个统一的结果
72
+ return res.status(401).json({ ok: false, error: '令牌无效' });
73
+ }
74
+ next();
75
+ });
76
+
77
+ /**
78
+ * GET /api/peer/info — 我是谁、能不能问。
79
+ *
80
+ * 刻意**不返回**项目清单 / 文件路径 / 任务数:这一层只负责"让对端知道有个谁在线",
81
+ * 项目清单让对端 agent 用自己的 list_projects 现场回答(能问的就不复制)。
82
+ */
83
+ app.get('/api/peer/info', (req, res) => {
84
+ res.json({
85
+ ok: true,
86
+ id: self.id,
87
+ name: self.name,
88
+ avatar: self.avatar || '',
89
+ version: getVersion(),
90
+ capabilities: ['ask'],
91
+ startedAt: STARTED_AT
92
+ });
93
+ });
94
+
95
+ /**
96
+ * POST /api/peer/ask — 让本机 g ai 回答一个问题。
97
+ *
98
+ * body: { question: string, from?: string /* 提问方节点 id *\/ }
99
+ * 返回: { ok, answer, sessionId, ms }
100
+ *
101
+ * 会话连续性:每个提问方在**本机**占一个会话(key = `in:<from>`),这样 A 问
102
+ * 第二句时 B 还记得第一句,但 A 和 C 的上下文不会串。
103
+ */
104
+ app.post('/api/peer/ask', async (req, res) => {
105
+ const question = String(req.body?.question || '').trim();
106
+ const from = String(req.body?.from || '').trim() || 'unknown';
107
+ if (!question) return res.status(400).json({ ok: false, error: 'question 不能为空' });
108
+ if (question.length > MAX_QUESTION_CHARS) {
109
+ return res.status(413).json({ ok: false, error: `问题过长(上限 ${MAX_QUESTION_CHARS} 字)` });
110
+ }
111
+
112
+ const sessionKey = `in:${from}`;
113
+ const sessionId = await getSession(sessionKey).catch(() => '');
114
+ logger.info?.(`[peer] 收到提问 from=${from} len=${question.length}`);
115
+
116
+ try {
117
+ const runner = typeof ask === 'function'
118
+ ? ask
119
+ : (args) => askLocalAgent({ ...args, port: getLocalPort(), timeoutMs });
120
+ const result = await runner({ question, sessionId, from });
121
+ if (result?.sessionId && result.sessionId !== sessionId) {
122
+ await setSession(sessionKey, result.sessionId).catch(() => {});
123
+ }
124
+ res.json({
125
+ ok: true,
126
+ answer: result?.answer || '',
127
+ sessionId: result?.sessionId || '',
128
+ ms: result?.ms || 0
129
+ });
130
+ } catch (err) {
131
+ logger.warn?.(`[peer] 提问处理失败: ${err?.message || err}`);
132
+ res.status(502).json({ ok: false, error: String(err?.message || err) });
133
+ }
134
+ });
135
+
136
+ // 其余任何路径:404,且不回 express 的默认 HTML(别暴露技术栈)
137
+ app.use((req, res) => {
138
+ res.status(404).json({ ok: false, error: 'not found' });
139
+ });
140
+
141
+ return app;
142
+ }
143
+
144
+ /**
145
+ * 在固定端口上起 peer 服务。
146
+ *
147
+ * 端口失败**不自动顺延**:peers 表里其他机器配的就是这个端口,悄悄换一个会让
148
+ * 对端一直连到别人(或连不上)。所以这里把错误原样抛给调用方去提示用户。
149
+ *
150
+ * @param {object} opts
151
+ * @param {object} opts.self 本机身份(含 port/host/token)
152
+ * @param {() => number|string} opts.getLocalPort
153
+ * @param {object} [opts.ask]
154
+ * @param {() => string} [opts.getVersion]
155
+ * @returns {Promise<http.Server>}
156
+ */
157
+ export async function startPeerServer({ self, getLocalPort, ask, getVersion }) {
158
+ const app = createPeerApp({ self, getLocalPort, ask, getVersion });
159
+ const server = http.createServer(app);
160
+
161
+ await new Promise((resolve, reject) => {
162
+ const onError = (err) => {
163
+ server.removeListener('listening', onListening);
164
+ if (err?.code === 'EADDRINUSE') {
165
+ reject(new Error(`peer 端口 ${self.port} 已被占用(固定端口不会自动顺延,请换端口或关掉占用它的进程)`));
166
+ } else if (err?.code === 'EACCES') {
167
+ reject(new Error(`peer 端口 ${self.port} 无权绑定(可能落在 Windows 的保留端口段,换一个端口)`));
168
+ } else {
169
+ reject(err);
170
+ }
171
+ };
172
+ const onListening = () => {
173
+ server.removeListener('error', onError);
174
+ resolve();
175
+ };
176
+ server.once('error', onError);
177
+ server.once('listening', onListening);
178
+ server.listen(self.port, self.host || '0.0.0.0');
179
+ });
180
+
181
+ logger.info?.(`[peer] 已监听 ${self.host || '0.0.0.0'}:${self.port}(节点 ${self.id},令牌 ${maskToken(self.token)})`);
182
+ return server;
183
+ }
@@ -0,0 +1,292 @@
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
+ // 跨机器 peer 的本地状态:<DATA_DIR>/peers.json。
16
+ //
17
+ // 形状(version 1):
18
+ // self 这台机器的身份 —— id / 昵称 / 头像 / 固定端口 / 是否开启 / 访问令牌
19
+ // peers 对端清单 —— 每条含 id / 昵称 / url / token(对方的令牌)
20
+ // sessions 每个对端用于续聊的会话 id(对端 agent 的会话,不是本机的)
21
+ //
22
+ // 为什么单独一个文件而不是塞进 config.json:
23
+ // · config.json 是**按项目分片**的(configSplit),而 peer 身份只跟机器有关;
24
+ // · config.json 会被备份/同步,令牌不该跟着项目配置流动;
25
+ // · 这里没有 read-modify-write 的并发焦虑 —— 改写频率是"人手动加一台机器"级别,
26
+ // 真正的并发热点(任务、job)不在这里。
27
+ //
28
+ // 令牌纪律:只在**内存里**参与比较,日志/CLI 输出一律走 maskToken()。这个文件
29
+ // 不进 git,路径也不在任何项目的同步范围里。
30
+
31
+ import { randomBytes, timingSafeEqual, createHash } from 'node:crypto';
32
+ import { promises as fs } from 'node:fs';
33
+
34
+ import { PEERS_FILE } from '../paths.js';
35
+ import { atomicWriteText } from '../fsAtomic.js';
36
+
37
+ // 5600:5599 留给 hub 的固定端口(见 ui/server/utils/ensureHub.js 的 HUB_DEFAULT_PORT)。
38
+ // 两个都是长驻服务,撞在一起会让「开 peer」和「开 hub」变成互斥。
39
+ export const PEER_DEFAULT_PORT = 5600;
40
+
41
+ /** peers.json 的默认形状(读不到 / 坏文件时的兜底)。 */
42
+ function emptyState() {
43
+ return { version: 1, self: null, peers: [], sessions: {} };
44
+ }
45
+
46
+ /** 生成 `node-xxxxxxxx` 形态的节点 id(8 位十六进制,够区分局域网里的几台机器)。 */
47
+ export function makeNodeId() {
48
+ return `node-${randomBytes(4).toString('hex')}`;
49
+ }
50
+
51
+ /** 生成 32 字节令牌的十六进制串。 */
52
+ export function makeToken() {
53
+ return randomBytes(32).toString('hex');
54
+ }
55
+
56
+ /**
57
+ * 令牌的展示形态:只留末 4 位。
58
+ * @param {string} token
59
+ */
60
+ export function maskToken(token) {
61
+ const t = String(token || '');
62
+ if (!t) return '(未设置)';
63
+ if (t.length <= 8) return '****';
64
+ return `${'*'.repeat(8)}${t.slice(-4)}`;
65
+ }
66
+
67
+ /**
68
+ * 常量时间比较两个令牌。
69
+ *
70
+ * 为什么不用 `===`:字符串比较会在第一个不同的字节提前返回,理论上可以按响应
71
+ * 时间逐字节猜出令牌。这里先各做一次 sha256 再比 —— 长度不同也不会因为
72
+ * timingSafeEqual 抛错(它要求等长),且比较的是定长摘要。
73
+ *
74
+ * @param {string} given 请求带来的令牌
75
+ * @param {string} expected 本机令牌
76
+ */
77
+ export function tokenEquals(given, expected) {
78
+ const a = createHash('sha256').update(String(given || '')).digest();
79
+ const b = createHash('sha256').update(String(expected || '')).digest();
80
+ return timingSafeEqual(a, b);
81
+ }
82
+
83
+ /** 把磁盘上/调用方给的对象归一成固定形状,脏字段一律丢掉。 */
84
+ function normalizeState(raw) {
85
+ const base = emptyState();
86
+ if (!raw || typeof raw !== 'object') return base;
87
+
88
+ const self = raw.self && typeof raw.self === 'object' ? raw.self : null;
89
+ if (self && typeof self.id === 'string' && self.id) {
90
+ base.self = {
91
+ id: self.id,
92
+ name: typeof self.name === 'string' && self.name.trim() ? self.name.trim() : '未命名节点',
93
+ avatar: typeof self.avatar === 'string' ? self.avatar : '',
94
+ port: Number.isInteger(self.port) && self.port > 0 && self.port < 65536 ? self.port : PEER_DEFAULT_PORT,
95
+ host: typeof self.host === 'string' && self.host ? self.host : '0.0.0.0',
96
+ enabled: self.enabled === true,
97
+ token: typeof self.token === 'string' ? self.token : ''
98
+ };
99
+ }
100
+
101
+ if (Array.isArray(raw.peers)) {
102
+ for (const p of raw.peers) {
103
+ if (!p || typeof p !== 'object') continue;
104
+ const url = typeof p.url === 'string' ? p.url.trim().replace(/\/+$/, '') : '';
105
+ if (!url) continue;
106
+ const id = typeof p.id === 'string' && p.id ? p.id : '';
107
+ base.peers.push({
108
+ id,
109
+ name: typeof p.name === 'string' && p.name.trim() ? p.name.trim() : (id || url),
110
+ url,
111
+ token: typeof p.token === 'string' ? p.token : ''
112
+ });
113
+ }
114
+ }
115
+
116
+ if (raw.sessions && typeof raw.sessions === 'object' && !Array.isArray(raw.sessions)) {
117
+ for (const [k, v] of Object.entries(raw.sessions)) {
118
+ if (typeof v === 'string' && v) base.sessions[k] = v;
119
+ }
120
+ }
121
+ return base;
122
+ }
123
+
124
+ /**
125
+ * 读 peers.json。文件不存在或损坏一律返回空状态 —— **不抛异常**:
126
+ * 这个文件坏了最多是"没有对端",不该让 g ui 起不来。
127
+ *
128
+ * @param {string} [filePath]
129
+ */
130
+ export async function readState(filePath = PEERS_FILE) {
131
+ try {
132
+ const text = await fs.readFile(filePath, 'utf-8');
133
+ return normalizeState(JSON.parse(text));
134
+ } catch {
135
+ return emptyState();
136
+ }
137
+ }
138
+
139
+ /**
140
+ * 原子写回。
141
+ * @param {object} state
142
+ * @param {string} [filePath]
143
+ */
144
+ export async function writeState(state, filePath = PEERS_FILE) {
145
+ const normalized = normalizeState(state);
146
+ await atomicWriteText(filePath, `${JSON.stringify(normalized, null, 2)}\n`);
147
+ return normalized;
148
+ }
149
+
150
+ /**
151
+ * 保证本机身份存在:第一次调用时生成 id 与令牌并落盘。
152
+ *
153
+ * 幂等,且**不覆盖**已有字段 —— 昵称、端口这些是用户手动设的。
154
+ *
155
+ * @param {string} [filePath]
156
+ */
157
+ export async function ensureSelf(filePath = PEERS_FILE) {
158
+ const state = await readState(filePath);
159
+ if (state.self && state.self.id && state.self.token) return state;
160
+
161
+ state.self = {
162
+ id: state.self?.id || makeNodeId(),
163
+ name: state.self?.name || '这台电脑',
164
+ avatar: state.self?.avatar || '',
165
+ port: state.self?.port || PEER_DEFAULT_PORT,
166
+ host: state.self?.host || '0.0.0.0',
167
+ enabled: state.self?.enabled === true,
168
+ token: state.self?.token || makeToken()
169
+ };
170
+ return writeState(state, filePath);
171
+ }
172
+
173
+ /**
174
+ * 改本机身份(昵称 / 头像 / 端口 / 开关)。只接受白名单字段。
175
+ *
176
+ * @param {object} patch
177
+ * @param {string} [filePath]
178
+ */
179
+ export async function updateSelf(patch, filePath = PEERS_FILE) {
180
+ const state = await ensureSelf(filePath);
181
+ const next = { ...state, self: { ...state.self } };
182
+ if (typeof patch.name === 'string' && patch.name.trim()) next.self.name = patch.name.trim();
183
+ if (typeof patch.avatar === 'string') next.self.avatar = patch.avatar;
184
+ if (Number.isInteger(patch.port) && patch.port > 0 && patch.port < 65536) next.self.port = patch.port;
185
+ if (typeof patch.host === 'string' && patch.host) next.self.host = patch.host;
186
+ if (typeof patch.enabled === 'boolean') next.self.enabled = patch.enabled;
187
+ return writeState(next, filePath);
188
+ }
189
+
190
+ /**
191
+ * 按 id / 昵称 / 地址找对端(忽略大小写;昵称重复时以先加入的为准)。
192
+ * 地址也认,是为了让"只有一台对端"这类场景能直接用 listPeers() 的结果回查。
193
+ *
194
+ * @param {string} ref
195
+ * @param {string} [filePath]
196
+ */
197
+ export async function findPeer(ref, filePath = PEERS_FILE) {
198
+ const key = String(ref || '').trim().toLowerCase();
199
+ if (!key) return null;
200
+ const state = await readState(filePath);
201
+ return state.peers.find((p) => p.id.toLowerCase() === key
202
+ || p.name.toLowerCase() === key
203
+ || p.url.toLowerCase() === key
204
+ || p.url.toLowerCase() === key.replace(/\/+$/, '')) || null;
205
+ }
206
+
207
+ /**
208
+ * 新增 / 覆盖一条对端。
209
+ *
210
+ * 以 id 为键覆盖;没有 id 时用 url 兜底(重复添加同一地址 = 改令牌,不产生两条)。
211
+ *
212
+ * @param {{id?:string,name?:string,url:string,token?:string}} peer
213
+ * @param {string} [filePath]
214
+ */
215
+ export async function upsertPeer(peer, filePath = PEERS_FILE) {
216
+ const url = String(peer?.url || '').trim().replace(/\/+$/, '');
217
+ if (!url) throw new Error('对端地址不能为空');
218
+
219
+ const state = await readState(filePath);
220
+ const id = String(peer?.id || '').trim();
221
+ const idx = state.peers.findIndex((p) => (id && p.id === id) || p.url === url);
222
+ const entry = {
223
+ id: id || (idx >= 0 ? state.peers[idx].id : ''),
224
+ name: String(peer?.name || '').trim() || (idx >= 0 ? state.peers[idx].name : url),
225
+ url,
226
+ token: String(peer?.token || '').trim()
227
+ };
228
+ if (idx >= 0) state.peers[idx] = { ...state.peers[idx], ...entry, id: entry.id || state.peers[idx].id };
229
+ else state.peers.push(entry);
230
+ return writeState(state, filePath);
231
+ }
232
+
233
+ /**
234
+ * 删一条对端(连带删掉与它绑定的会话 id)。
235
+ * @param {string} ref id 或昵称
236
+ * @param {string} [filePath]
237
+ */
238
+ export async function removePeer(ref, filePath = PEERS_FILE) {
239
+ const target = await findPeer(ref, filePath);
240
+ if (!target) return { removed: false, peer: null };
241
+ const state = await readState(filePath);
242
+ state.peers = state.peers.filter((p) => p !== target && !(target.id && p.id === target.id) &&
243
+ !(!target.id && p.url === target.url));
244
+ if (target.id) delete state.sessions[target.id];
245
+ await writeState(state, filePath);
246
+ return { removed: true, peer: target };
247
+ }
248
+
249
+ /**
250
+ * 取某个对端的续聊会话 id(对端机器上那个会话)。
251
+ * @param {string} peerId
252
+ * @param {string} [filePath]
253
+ */
254
+ export async function getSessionId(peerId, filePath = PEERS_FILE) {
255
+ if (!peerId) return '';
256
+ const state = await readState(filePath);
257
+ return state.sessions[peerId] || '';
258
+ }
259
+
260
+ /**
261
+ * 记住某个对端的续聊会话 id。
262
+ * @param {string} peerId
263
+ * @param {string} sessionId
264
+ * @param {string} [filePath]
265
+ */
266
+ export async function setSessionId(peerId, sessionId, filePath = PEERS_FILE) {
267
+ if (!peerId || !sessionId) return;
268
+ const state = await ensureSelf(filePath);
269
+ state.sessions[peerId] = sessionId;
270
+ await writeState(state, filePath);
271
+ }
272
+
273
+ /**
274
+ * 换一把新令牌(旧令牌立即失效 —— 已经配对过的机器要重新 `g peer add`)。
275
+ * @param {string} [filePath]
276
+ */
277
+ export async function rotateSelfToken(filePath = PEERS_FILE) {
278
+ const state = await ensureSelf(filePath);
279
+ state.self.token = makeToken();
280
+ return writeState(state, filePath);
281
+ }
282
+
283
+ /** 对端清单(供 CLI / 工具描述用,不含令牌明文)。 */
284
+ export async function listPeers(filePath = PEERS_FILE) {
285
+ const state = await readState(filePath);
286
+ return state.peers.map((p) => ({
287
+ id: p.id,
288
+ name: p.name,
289
+ url: p.url,
290
+ hasToken: !!p.token
291
+ }));
292
+ }
@@ -1,4 +1,4 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{An as e,Bn as t,C as n,Cn as r,Dt as i,Et as a,Ft as o,Gn as s,Gt as c,Ht as l,In as u,J as d,Kr as f,M as p,Mn as m,Pn as h,Q as g,Qn as _,Rt as v,S as y,Sn as b,Tn as x,Un as S,Vt as C,W as w,Wr as T,Xn as E,Xr as D,Y as O,Yn as k,_n as A,a as j,ar as M,at as N,cr as P,ct as ee,dn as F,en as te,et as I,fn as L,gr as R,i as z,ir as B,j as V,jr as H,kn as U,ln as ne,lr as W,m as re,mn as ie,mr as ae,nr as G,on as K,pn as q,pr as oe,pt as se,qn as J,r as ce,w as le,wn as Y,xn as X,xt as ue,yn as de}from"./element-plus-CosPN4uA.js";import{a as fe,c as pe,n as me,o as he,s as ge,t as _e}from"./vendor-DSY591Nv.js";import{n as Z,t as Q}from"./_plugin-vue_export-helper-D6ACRIgd.js";import{t as ve}from"./SvgIcon-BmXhx-vA.js";import{d as ye,f as $,m as be,t as xe,u as Se}from"./configStore-BQoByr2v.js";import{t as Ce}from"./CommonDialog-ZwJlTF5A.js";import{H as we,L as Te,St as Ee,Tt as De,V as Oe,_ as ke,d as Ae,f as je,ft as Me,g as Ne,h as Pe,m as Fe,n as Ie,nt as Le,p as Re,r as ze,t as Be,u as Ve,v as He,wt as Ue,y as We}from"./index-DW-2cjNW.js";import{c as Ge,l as Ke,n as qe,s as Je,u as Ye}from"./taskExecutionExport-C3dV5cuE.js";import{n as Xe}from"./relativeTime-DsG6UERK.js";function Ze(e,t=200){let n=typeof e==`string`?e:``;if(!n.trim())return``;let r=n.split(`
1
+ import"./rolldown-runtime-C0FnF6B9.js";import{An as e,Bn as t,C as n,Cn as r,Dt as i,Et as a,Ft as o,Gn as s,Gt as c,Ht as l,In as u,J as d,Kr as f,M as p,Mn as m,Pn as h,Q as g,Qn as _,Rt as v,S as y,Sn as b,Tn as x,Un as S,Vt as C,W as w,Wr as T,Xn as E,Xr as D,Y as O,Yn as k,_n as A,a as j,ar as M,at as N,cr as P,ct as ee,dn as F,en as te,et as I,fn as L,gr as R,i as z,ir as B,j as V,jr as H,kn as U,ln as ne,lr as W,m as re,mn as ie,mr as ae,nr as G,on as K,pn as q,pr as oe,pt as se,qn as J,r as ce,w as le,wn as Y,xn as X,xt as ue,yn as de}from"./element-plus-CosPN4uA.js";import{a as fe,c as pe,n as me,o as he,s as ge,t as _e}from"./vendor-DSY591Nv.js";import{n as Z,t as Q}from"./_plugin-vue_export-helper-DaO48JOV.js";import{t as ve}from"./SvgIcon-GLj_3KcS.js";import{d as ye,f as $,m as be,t as xe,u as Se}from"./configStore-B_2ifbl_.js";import{t as Ce}from"./CommonDialog-BiF2fMgr.js";import{H as we,L as Te,St as Ee,Tt as De,V as Oe,_ as ke,d as Ae,f as je,ft as Me,g as Ne,h as Pe,m as Fe,n as Ie,nt as Le,p as Re,r as ze,t as Be,u as Ve,v as He,wt as Ue,y as We}from"./index-DUzH5036.js";import{c as Ge,l as Ke,n as qe,s as Je,u as Ye}from"./taskExecutionExport-C3vFixdJ.js";import{n as Xe}from"./relativeTime-DzYQUHQo.js";function Ze(e,t=200){let n=typeof e==`string`?e:``;if(!n.trim())return``;let r=n.split(`
2
2
  `).map(e=>e.trim()).filter(Boolean),i=(r.length?r[r.length-1]:``).replace(/\s+/g,` `);return i.length>t?`${i.slice(0,t-1)}…`:i}function Qe(e){return Z(e===`done`?`@AGENT:对话已完成`:`@AGENT:对话出错`)}function $e(){let e={page:!1,browser:!1,sound:!1};try{let t=xe();e.page=!!t.notifyPageOnTaskDone,e.browser=!!t.notifyBrowserOnTaskDone,e.sound=!!t.notifySoundOnTaskDone}catch{}return e}function et(){return $e().sound?ze():!1}function tt(e){let t=$e();if(!Be(t))return!1;let n=String(e.title||``).trim()||Z(`@AGENT:无标题`),r=Ze(e.detail),i=e.tag||`zen-gitsync-agent`,a=r?`${n}\n${r}`:n;return t.sound&&Ie(e.kind),we(t)&&Oe({title:Qe(e.kind),body:a,tag:i})||!t.page||e.alreadyToast||(e.kind===`done`?z.success(Z(`@AGENT:对话完成:{name}`,{name:n})):z.error(Z(`@AGENT:对话出错:{name}`,{name:n}))),!0}var nt=`g ai`,rt=`g-ai`,it=[{id:`gai`,name:nt},...ye.map(e=>({id:e.id,name:e.name}))],at=`zen-gitsync-agent-engine`;function ot(e){return e===`gai`||$(e)}function st(e){return it.find(t=>t.id===e)?.name||`g ai`}function ct(e){return $(e)}function lt(){try{let e=localStorage.getItem(at);if(ot(e))return e}catch{}return`gai`}function ut(e){try{localStorage.setItem(at,e)}catch{}}u(),W();var dt=[`image/*`,`.pdf`,`.txt`,`.md`,`.json`,`.csv`,`.log`,`.yml`,`.yaml`,`.xml`,`.toml`,`.ini`,`.html`,`.css`,`.js`,`.ts`,`.tsx`,`.vue`,`.py`,`.java`,`.go`,`.rs`,`.sql`,`.sh`].join(`,`),ft=65536,pt=`local-`,mt=0;function ht(e){if(!e||!Array.isArray(e.messages))return[];let t=[],n=e.messages.length,r=new Map;for(let t of e.turnTimings||[])typeof t?.turnIndex==`number`&&Number.isFinite(t?.durationMs)&&r.set(t.turnIndex,t);let i=-1;for(let a=0;a<n;a++){let o=e.messages[a];if(o.role!==`system`){if(o.role===`user`){i+=1;let e=``,n;if(typeof o.content==`string`)e=o.content;else if(Array.isArray(o.content)){e=o.content.filter(e=>e?.type===`text`).map(e=>e.text||``).join(` `);let t=o.content.filter(e=>e?.type===`image_url`&&typeof e.image_url?.url==`string`).map((e,t)=>({id:`att-${a}-${t}`,name:`image`,size:0,type:vt(e.image_url?.url||``)||`image/*`,preview:e.image_url?.url}));t.length>0&&(n=t)}(e||n)&&t.push({id:`msg-${a}`,role:`user`,content:e,attachments:n,status:`done`,createdAt:Date.now()})}else if(o.role===`assistant`){let o=`msg-${a}`,s=``,c=[],l=a;for(;l<n;){let t=e.messages[l];if(t.role!==`assistant`&&t.role!==`tool`)break;if(t.role===`assistant`){if(typeof t.content==`string`&&(s+=t.content),t.tool_calls&&t.tool_calls.length>0)for(let e of t.tool_calls)c.push({id:e.id||e.function?.name||ge(),name:e.function?.name||``,arguments:e.function?.arguments||``,argsPreview:gt(e.function?.name||``,e.function?.arguments||``),status:`done`,result:``})}else{let e=t.tool_call_id||t.name||``,n=c.find(t=>t.id===e);if(n){let e;e=typeof t.content==`string`?t.content:Array.isArray(t.content)&&t.content.filter(e=>e?.type===`text`).map(e=>e.text||``).join(`
3
3
  `)||`(non-text result)`,e.length>ft&&(e=`…(前文已截断)\n${e.slice(-65536)}`),n.result=e}}l++}a=l-1,s.length>ft&&(s=`…(前文已截断)\n${s.slice(-65536)}`);let u=!!s,d=c.length>0;if(u||d){let e=r.get(i),n=e?.finishedAt?Date.parse(e.finishedAt):NaN;t.push({id:o,role:`assistant`,content:s,toolCalls:d?c:void 0,status:`done`,createdAt:Number.isFinite(n)&&e?n-e.durationMs:Date.now(),finishedAt:Number.isFinite(n)?n:void 0,meta:e?{durationMs:e.durationMs}:void 0})}}}}return t}function gt(e,t){try{let n=JSON.parse(t);switch(e){case`run_command`:return(n.run_in_background?`[后台] `:``)+String(n.command||``).slice(0,200);case`background_task`:{let e=String(n.action||`read`);if(e===`list`)return`列出后台任务`;if(e===`kill`)return`停止 ${n.id||`?`}`;let t=Number(n.wait_seconds)||0;return`${n.id||`?`}${t?` · 等 ${t}s`:``}`}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||``);case`update_plan`:{let e=_e(t,`update_plan`);if(!e)return``;let{total:r,done:i}=he(e),a=String(n.explanation||``).replace(/\s+/g,` `).trim();return[`${i}/${r}`,a].filter(Boolean).join(` — `).slice(0,200)}default:return JSON.stringify(n).slice(0,200)}}catch{return t.slice(0,200)}}function _t(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 vt(e){let t=/^data:([^;,]+)/.exec(e);return t?t[1]:``}function yt(){let e=xe(),t=We(),n=++mt,r=R([]),i=R(!1),a=R(lt());function o(e){ot(e)&&(a.value=e,ut(e))}let s=ae(new Map),c=R(null),l=R(!1),u=0,d=0,f=new Set,p=new Map;function m(e){return{key:e,sessionId:e.startsWith(pt)?null:e,title:``,messages:[],isStreaming:!1,abortController:null,nonce:0,pendingQuestion:null,answeringQuestion:!1,contextUsage:null,queue:[],queuePaused:!1,lastEventAt:null}}function h(e){let t=s.get(e);return t||=(s.set(e,m(e)),s.get(e)),t}let g=X(()=>c.value?s.get(c.value)??null:null),_=X(()=>g.value?.messages??[]),v=X(()=>{let e=_.value,t=g.value?.sessionId??null;if(!t)return e;let n=!1,r=e.map(e=>{if(!e.content||!e.content.includes(`![`))return e;let r=Ye(e.content,t);return r===e.content?e:(n=!0,{...e,content:r})});return n?r:e}),y=X(()=>g.value?.isStreaming??!1),b=R(Date.now()),x=null,S=X(()=>{let e=g.value;return!e||!e.isStreaming||!e.lastEventAt?null:Math.max(0,b.value-e.lastEventAt)});G(y,e=>{e?(b.value=Date.now(),x||=setInterval(()=>{b.value=Date.now()},1e3)):x&&=(clearInterval(x),null)},{immediate:!0}),P()&&oe(()=>{x&&=(clearInterval(x),null)});let C=X(()=>g.value?.contextUsage??null),w=X(()=>{let e=C.value;return e?{ratio:Math.min(Math.max(e.tokenRatio,e.messageRatio),1),current:e.tokens,total:e.maxTokens,unit:`K`,suffix:`上下文已使用`,detail:[`${e.messages} / ${e.maxMessages} 条消息`,typeof e.actualInputTokens==`number`&&e.actualInputTokens>0?`实测 ${e.actualInputTokens.toLocaleString()} token`:`估算 ${Math.round(e.estTokens).toLocaleString()} token`,e.droppedMessages>0?`会话共 ${e.transcriptMessages} 条,本次只带入 ${e.messages} 条(其余已摘成梗概)`:``].filter(Boolean).join(`
4
4
  `)}:null}),T=X(()=>{let e=r.value.find(e=>e.sessionId===c.value);return ot(e?.engine)?e.engine:a.value}),E=X(()=>r.value.some(e=>e.sessionId===c.value)),D=X(()=>g.value?.pendingQuestion??null),O=X(()=>g.value?.answeringQuestion??!1),k=X(()=>g.value?.pendingQuestion??null),A=X(()=>g.value?.queuePaused??!1),j=X(()=>(g.value?.queue??[]).map(e=>({id:e.id,text:e.text,attachmentNames:e.files.map(e=>e.file.name)})));function M(e){let t=g.value;if(!t)return;let n=t.queue.findIndex(t=>t.id===e);n<0||(t.queue.splice(n,1),t.queue.length===0&&(t.queuePaused=!1))}function N(e){if(e.isStreaming)return;let t=e.queue.shift();t&&K(t.text,t.files,{...t.options,targetKey:e.key,background:!0})}function ee(){let e=g.value;e&&!e.isStreaming&&e.queue.length!==0&&(e.queuePaused=!1,N(e))}function F(e){return!!s.get(e)?.isStreaming}function te(t){let n=[...t];for(let t of s.values()){if(!t.sessionId)continue;let r=n.findIndex(e=>e.sessionId===t.sessionId);if(r!==-1){f.delete(t.sessionId),t.isStreaming&&(n[r]={...n[r],isGenerating:!0});continue}if(!t.isStreaming&&!f.has(t.sessionId))continue;let i=new Date().toISOString();n.unshift({sessionId:t.sessionId,title:t.title||Z(`@AGENT:无标题`),source:`web`,cwd:e.currentDirectory||``,model:``,engine:a.value,createdAt:i,updatedAt:i,messageCount:t.messages.filter(e=>e.role===`user`).length||1,size:0,isGenerating:t.isStreaming})}return n}function I(e,t){return(t.updatedAt||``).localeCompare(e.updatedAt||``)}function L(t){if(!t?.sessionId||r.value.some(e=>e.sessionId===t.sessionId))return;let n={sessionId:t.sessionId,title:t.title||Z(`@AGENT:无标题`),source:t.source||`web`,cwd:t.cwd||``,model:t.model||``,engine:t.engine,createdAt:t.createdAt||``,updatedAt:t.updatedAt||``,messageCount:Array.isArray(t.messages)?t.messages.length:0,size:0};p.set(t.sessionId,{meta:n,cwd:e.currentDirectory||``}),r.value=[...r.value,n].sort(I)}function B(){if(p.size===0)return;let t=e.currentDirectory||``,n=[];for(let[e,i]of[...p]){if(i.cwd!==t||r.value.some(t=>t.sessionId===e)){p.delete(e);continue}n.push(i.meta)}n.length&&(r.value=[...r.value,...n].sort(I))}async function V(){let t=++u,n=e.currentDirectory||``;if(!n){r.value=[],i.value=!1;return}i.value=!0;try{let e=`/api/agent/sessions?cwd=${encodeURIComponent(n)}`,i=await fetch(e).then(e=>e.json());if(t!==u)return;if(!i.success){z.error(i.error||Z(`@AGENT:加载会话列表失败`));return}let a=Array.isArray(i.sessions)?i.sessions:[];r.value=te(a),B()}catch(e){if(t!==u)return;z.error(Z(`@AGENT:加载会话列表失败`)+`: `+(e?.message||e))}finally{t===u&&(i.value=!1)}}function H(t,n){let i=new Date().toISOString(),o=r.value.find(e=>e.sessionId===t);if(o){o.isGenerating=!0,o.updatedAt=i;return}r.value=[{sessionId:t,title:n||Z(`@AGENT:无标题`),source:`web`,cwd:e.currentDirectory||``,model:``,engine:a.value,createdAt:i,updatedAt:i,messageCount:1,size:0,isGenerating:!0},...r.value]}function U(e){let t=r.value.find(t=>t.sessionId===e);t&&(t.isGenerating=!1)}async function ne(e){if(s.get(e)?.isStreaming){c.value=e;return}l.value=!0,c.value=e;try{let t=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`).then(e=>e.json());if(!t.success){z.error(t.error||Z(`@AGENT:加载会话失败`));return}let n=h(e);n.sessionId=e,n.messages=ht(t.session),t.session?.title&&(n.title=t.session.title),L(t.session)}catch(e){z.error(Z(`@AGENT:加载会话失败`)+`: `+(e?.message||e))}finally{l.value=!1}}async function W(e){try{await ce.confirm(Z(`@AGENT:确认删除该会话`),Z(`@AGENT:删除后无法恢复`),{type:`warning`})}catch{return}let t=s.get(e);if(t?.abortController)try{t.abortController.abort()}catch{}s.delete(e);try{let t=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`DELETE`}).then(e=>e.json());if(!t.success){z.error(t.error||Z(`@AGENT:删除失败`));return}r.value=r.value.filter(t=>t.sessionId!==e),p.delete(e),c.value===e&&(c.value=null),z.success(Z(`@AGENT:已删除`))}catch(e){z.error(Z(`@AGENT:删除失败`)+`: `+(e?.message||e))}}async function re(e,t){try{let n=await fetch(`/api/agent/sessions/${encodeURIComponent(e)}`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({title:t})}).then(e=>e.json());if(!n.success){z.error(n.error||Z(`@AGENT:重命名失败`));return}let i=r.value.find(t=>t.sessionId===e);i&&(i.title=t);let a=s.get(e);a&&(a.title=t),z.success(Z(`@AGENT:已重命名`))}catch(e){z.error(Z(`@AGENT:重命名失败`)+`: `+(e?.message||e))}}function ie(){c.value=null}async function K(r,i=[],a={}){let o=i.filter(e=>e?.file?.type?.startsWith(`image/`)),s=i.filter(e=>!e?.file?.type?.startsWith(`image/`)),l=[],u=0;for(let e of s){let t=e.file.size||0;if(!(t>10485760)){if(l.length>=10||u+t>20971520)break;u+=t,l.push(e)}}let f=s.filter(e=>!l.includes(e));f.length>0&&z.warning(`${Z(`@AGENT:以下附件超过大小或数量上限,已忽略:`)}${f.map(e=>e.file.name).join(`、`)}`);let p=a.targetKey||c.value||`${pt}${Date.now()}-${++d}`,m=h(p);if(!r.trim()&&o.length===0&&l.length===0)return;if(m.isStreaming){m.queue.push({id:ge(),text:r,files:[...o,...l],options:a});return}let g=(await Promise.allSettled(o.map(e=>_t(e.file)))).map(e=>e.status===`fulfilled`?e.value:``).filter(e=>e.startsWith(`data:image/`)),_=(await Promise.allSettled(l.map(async e=>({name:e.file.name||`file`,dataUrl:await _t(e.file)})))).map(e=>e.status===`fulfilled`?e.value:null).filter(e=>!!e&&typeof e.dataUrl==`string`&&e.dataUrl.startsWith(`data:`));a.background||(c.value=m.key),m.nonce+=1;let v=m.nonce;m.abortController=new AbortController;let y=m.abortController;m.pendingQuestion=null,m.answeringQuestion=!1;let b={id:ge(),role:`user`,content:r,status:`done`,createdAt:Date.now()},x=[...o,...l];x.length>0&&(b.attachments=x.map(e=>({id:e.id,name:e.file.name||`image`,size:e.file.size||0,type:e.file.type||``,preview:e.preview}))),m.messages.push(b);let S=ae({id:ge(),role:`assistant`,content:``,status:`pending`,createdAt:Date.now()});m.messages.push(S),m.isStreaming=!0,m.lastEventAt=Date.now(),m.lastSendOptions=a;let C=`${n}:${p}:${v}`;t.begin(C),await q(m,v,y,S,C,{sessionId:m.sessionId||``,userMessage:r,cwd:e.currentDirectory||``,engine:a.engine??T.value,...g.length>0?{images:g}:{},..._.length>0?{attachments:_}:{},...a.openFilePath?{openFilePath:a.openFilePath}:{},...a.docNote?{docNote:a.docNote}:{},...a.dirStatus?.length?{dirStatus:a.dirStatus}:{},...a.dirSummary?{dirSummary:a.dirSummary}:{},...a.allowDispatch?{allowDispatch:!0,dispatchExecutor:a.dispatchExecutor||``,dispatchUseDefaultPrompt:a.dispatchUseDefaultPrompt!==!1}:{}})}async function q(e,n,r,i,a,o){let l=String(o.userMessage||``),u=[],d=e.sessionId,p=!1,m=null;try{let t=await fetch(`/api/agent/chat`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`text/event-stream`},body:JSON.stringify(o),signal:r.signal});if(n!==e.nonce)return;if(!t.ok||!t.body){let e=await t.text().catch(()=>``);throw Error(e||`HTTP ${t.status}`)}let a=t.body.getReader(),f=new TextDecoder(`utf-8`),p=``;for(;;){let{value:t,done:r}=await a.read();if(n!==e.nonce)return;if(r)break;p+=f.decode(t,{stream:!0});let o=p.split(`
@@ -0,0 +1,2 @@
1
+ import"./rolldown-runtime-C0FnF6B9.js";import{An as e,Bn as t,Cn as n,Dt as r,Gn as i,In as a,J as o,Mn as s,Sn as c,Tn as l,Un as u,W as d,Wr as f,Xr as p,gr as m,i as h,ir as g,jr as _,kn as v,lr as y,nr as b,nt as x,qn as S,r as C,tt as w,wn as T,xn as E,z as D}from"./element-plus-CosPN4uA.js";import{n as O,t as k}from"./_plugin-vue_export-helper-DaO48JOV.js";import{t as A}from"./CommonDialog-BiF2fMgr.js";async function j(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 M(){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 N(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 j(t,t=>{try{e(JSON.parse(t))}catch{}})}async function P(){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())};j(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(()=>{})})}a(),y();var F=`npm install -g zen-gitsync --registry https://registry.npmjs.org/`,I=s({__name:`UpgradeDialog`,props:{modelValue:{type:Boolean,required:!0},logs:{type:String,required:!0},status:{type:String,required:!0},countdown:{type:Number,required:!1,default:0}},emits:[`update:modelValue`,`retry`,`restart`,`cancel`],setup(e,{expose:n,emit:i}){n();let a=e,o=i,s=m(null);b(()=>a.logs,async()=>{await t(),s.value&&(s.value.scrollTop=s.value.scrollHeight)});function c(){a.status!==`running`&&(a.status===`success`&&a.countdown>0&&o(`cancel`),o(`update:modelValue`,!1))}function l(){o(`retry`)}function u(){o(`restart`)}function d(){o(`cancel`),o(`update:modelValue`,!1)}async function f(){try{await navigator.clipboard.writeText(F),h.success(O(`@F13B4:已复制升级命令`))}catch{h.warning(O(`@F13B4:复制失败,请手动复制`))}}let p={MANUAL_COMMAND:F,props:a,emit:o,logEl:s,close:c,onRetry:l,onRestart:u,onLater:d,onCopyCommand:f,get Loading(){return r},get CircleCheck(){return w},get CircleClose(){return x},get $t(){return O},CommonDialog:A};return Object.defineProperty(p,"__isScriptSetup",{enumerable:!1,value:!0}),p}});a(),_();var L={key:3},R={key:4},z={key:5},B={key:0,class:`upgrade-hint`},V={key:0,class:`upgrade-countdown`},H={key:1,class:`upgrade-manual`},U={class:`upgrade-manual__label`};function W(t,r,i,a,s,u){let d=o,m=D;return S(),n(a.CommonDialog,{"model-value":i.modelValue,"onUpdate:modelValue":r[0]||=e=>a.emit(`update:modelValue`,e),title:a.$t(`@F13B4:升级`),width:`680px`,type:`flex`,"close-on-click-modal":!1,"close-on-press-escape":!1,"show-close":i.status!==`running`,"append-to-body":``,onClose:a.close},{footer:g(()=>[i.status===`failed`?(S(),n(m,{key:0,type:`primary`,onClick:a.onRetry},{default:g(()=>[v(p(a.$t(`@F13B4:重试`)),1)]),_:1})):T(`v-if`,!0),i.status===`failed`?(S(),n(m,{key:1,onClick:a.onCopyCommand},{default:g(()=>[v(p(a.$t(`@F13B4:复制升级命令`)),1)]),_:1})):T(`v-if`,!0),i.status===`success`?(S(),n(m,{key:2,type:`primary`,onClick:a.onRestart},{default:g(()=>[v(p(a.$t(`@F13B4:立即重启并刷新`)),1)]),_:1})):T(`v-if`,!0),i.status===`success`?(S(),n(m,{key:3,onClick:a.onLater},{default:g(()=>[v(p(a.$t(`@F13B4:稍后手动重启`)),1)]),_:1})):T(`v-if`,!0),i.status===`running`?(S(),n(m,{key:4,disabled:``},{default:g(()=>[v(p(a.$t(`@F13B4:升级中`))+`... `,1)]),_:1})):T(`v-if`,!0)]),default:g(()=>[c(`div`,{class:f([`upgrade-status`,`is-${i.status}`])},[i.status===`running`?(S(),n(d,{key:0,class:`is-loading`},{default:g(()=>[e(a.Loading)]),_:1})):i.status===`success`?(S(),n(d,{key:1,color:`var(--color-success)`},{default:g(()=>[e(a.CircleCheck)]),_:1})):(S(),n(d,{key:2,color:`var(--color-danger)`},{default:g(()=>[e(a.CircleClose)]),_:1})),i.status===`running`?(S(),l(`span`,L,p(a.$t(`@F13B4:升级中`)),1)):i.status===`success`?(S(),l(`span`,R,p(a.$t(`@F13B4:升级完成`)),1)):(S(),l(`span`,z,p(a.$t(`@F13B4:升级失败`)),1))],2),i.status===`success`?(S(),l(`p`,B,[v(p(a.$t(`@F13B4:新版本已全局安装,需要重启服务才能生效`))+` `,1),i.countdown>0?(S(),l(`span`,V,p(a.$t(`@F13B4:{seconds} 秒后自动刷新`,{seconds:i.countdown})),1)):T(`v-if`,!0)])):T(`v-if`,!0),i.status===`failed`?(S(),l(`p`,H,[c(`span`,U,p(a.$t(`@F13B4:手动升级命令`)),1),c(`code`,{class:`upgrade-manual__cmd`},p(a.MANUAL_COMMAND))])):T(`v-if`,!0),c(`pre`,{ref:`logEl`,class:`upgrade-log`},p(i.logs||a.$t(`@F13B4:等待日志输出`)),513)]),_:1},8,[`model-value`,`title`,`show-close`])}var G=k(I,[[`render`,W],[`__scopeId`,`data-v-bc566bba`],[`__file`,`D:/workspace/github_workspace/zen-gitsync/src/ui/client/src/components/UpgradeDialog.vue`]]);a(),y();var K=`https://github.com/xz333221/zen-gitsync/tags`,q=5,J=`app-version-check`,Y=36e5,X=s({__name:`AppVersionBadge`,setup(e,{expose:t}){t();let n=`2.19.1`,r=m(n),a=E(()=>r.value||n),o=m(null),s=m(!1),c=m(``),l=m(`running`),d=m(0),f=null,p=m(!1);function g(){try{let e=sessionStorage.getItem(J);if(!e)return null;let{at:t,data:n}=JSON.parse(e);return Date.now()-t>Y?null:n}catch{return null}}function _(e){try{sessionStorage.setItem(J,JSON.stringify({at:Date.now(),data:e}))}catch{}}async function v(e=!1){let t=g();t&&(o.value=t);try{let t=await M();o.value=t,_(t),t.current&&(r.value=t.current),!e&&t.success&&t.hasUpdate&&h({type:`success`,message:O(`@F13B4:发现新版本 {version}`,{version:t.latest??``}),duration:4e3})}catch(t){e||h({type:`warning`,message:O(`@F13B4:检查更新失败 {error}`,{error:t?.message||``})})}}async function y(){if(o.value?.hasUpdate){try{await C.confirm(O(`@F13B4:确定要从 {current} 升级到 {latest} 吗?`,{current:o.value.current,latest:o.value.latest}),O(`@F13B4:升级`),{confirmButtonText:O(`@F13B4:确定升级`),cancelButtonText:O(`@F13B4:取消`),type:`info`})}catch{return}c.value=``,l.value=`running`,s.value=!0,await N(e=>{if(e.type===`done`){if(l.value=e.code===0?`success`:`failed`,e.code===0){try{sessionStorage.removeItem(J)}catch{}o.value?.latest&&(r.value=o.value.latest),b()}}else e.message&&(c.value+=e.message)})}}function b(){x(),d.value=q,f=setInterval(()=>{--d.value,d.value<=0&&(f&&=(clearInterval(f),null),w())},1e3)}function x(){f&&=(clearInterval(f),null),d.value=0}function S(){x()}async function w(){if(x(),!p.value){p.value=!0;try{let e=await P(),{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?O(n):O(`@F13B4:重启失败 {error}`,{error:t})}),p.value=!1}}}u(()=>{x()}),i(()=>{v(!0)});let T={buildVersion:n,releasesUrl:K,runtimeVersion:r,displayVersion:a,latestInfo:o,upgradeDialogVisible:s,upgradeLogs:c,upgradeStatus:l,AUTO_REFRESH_SECONDS:q,countdown:d,get countdownTimer(){return f},set countdownTimer(e){f=e},restartInFlight:p,CACHE_KEY:J,CACHE_TTL_MS:Y,readCache:g,writeCache:_,checkUpdate:v,handleUpgrade:y,startAutoRefreshCountdown:b,cancelAutoRefreshCountdown:x,handleCancelAutoRefresh:S,handleRestart:w,get $t(){return O},UpgradeDialog:G};return Object.defineProperty(T,"__isScriptSetup",{enumerable:!1,value:!0}),T}});a(),_();var Z={class:`app-version-badge`},Q=[`aria-label`],$=[`aria-label`],ee=[`title`,`aria-label`],te={class:`upgrade-btn__label`},ne={class:`upgrade-btn__version`};function re(t,n,r,i,a,o){let s=d;return S(),l(`div`,Z,[T(` GitHub 链接 `),e(s,{content:i.$t(`@F13B4:在 GitHub 上查看源码`),placement:`top`,effect:`dark`,"show-after":300},{default:g(()=>[c(`a`,{href:`https://github.com/xz333221/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link`,"aria-label":i.$t(`@F13B4:在 GitHub 上查看源码`)},[...n[1]||=[c(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[c(`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,Q)]),_:1},8,[`content`]),T(` NPM 链接 `),e(s,{content:i.$t(`@F13B4:在 npm 上查看包`),placement:`top`,effect:`dark`,"show-after":300},{default:g(()=>[c(`a`,{href:`https://www.npmjs.com/package/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link badge-icon-link--npm`,"aria-label":i.$t(`@F13B4:在 npm 上查看包`)},[...n[2]||=[c(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[c(`path`,{d:`M2 2v20h20V2H2zm17.167 17.167H12.5v-10h-3.333v10H4.833V4.833h14.334v14.334z`})],-1)]],8,$)]),_:1},8,[`content`]),T(` 版本号 `),e(s,{content:i.$t(`@F13B4:查看更新`),placement:`top`,effect:`dark`,"show-after":300},{default:g(()=>[c(`a`,{href:i.releasesUrl,target:`_blank`,rel:`noopener noreferrer`,class:`version-link`},`v`+p(i.displayVersion),1)]),_:1},8,[`content`]),i.latestInfo?.hasUpdate?(S(),l(`button`,{key:0,type:`button`,class:`upgrade-btn`,title:i.$t(`@F13B4:发现新版本 {version},点击升级`,{version:i.latestInfo.latest??``}),"aria-label":i.$t(`@F13B4:发现新版本 {version},点击升级`,{version:i.latestInfo.latest??``}),onClick:i.handleUpgrade},[n[3]||=c(`span`,{class:`upgrade-btn__dot`,"aria-hidden":`true`},null,-1),c(`span`,te,p(i.$t(`@F13B4:升级`)),1),c(`span`,ne,`v`+p(i.latestInfo.latest),1)],8,ee)):T(`v-if`,!0),e(i.UpgradeDialog,{modelValue:i.upgradeDialogVisible,"onUpdate:modelValue":n[0]||=e=>i.upgradeDialogVisible=e,logs:i.upgradeLogs,status:i.upgradeStatus,countdown:i.countdown,onRetry:i.handleUpgrade,onRestart:i.handleRestart,onCancel:i.handleCancelAutoRefresh},null,8,[`modelValue`,`logs`,`status`,`countdown`])])}var ie=k(X,[[`render`,re],[`__scopeId`,`data-v-7e0300ec`],[`__file`,`D:/workspace/github_workspace/zen-gitsync/src/ui/client/src/components/AppVersionBadge.vue`]]);export{ie as default};