@ian2018cs/agenthub 0.1.76 → 0.1.78
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/dist/assets/index-DkNpDSsg.css +32 -0
- package/dist/assets/index-NaMmXkCt.js +197 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/server/claude-sdk.js +7 -152
- package/server/index.js +119 -12
- package/server/routes/agents.js +331 -3
- package/server/services/builtin-tools/background-task-pool.js +316 -0
- package/server/services/builtin-tools/background-task.js +231 -0
- package/server/services/builtin-tools/index.js +146 -0
- package/server/services/builtin-tools/share-project-template.js +124 -0
- package/server/services/system-agent-repo.js +17 -1
- package/server/services/user-directories.js +1 -0
- package/shared/brand.js +4 -0
- package/dist/assets/index-B5imuFpg.js +0 -192
- package/dist/assets/index-oUz7uC99.css +0 -32
package/dist/index.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Prevent zoom on iOS -->
|
|
27
27
|
<meta name="format-detection" content="telephone=no" />
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-NaMmXkCt.js"></script>
|
|
29
29
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-Bv0Nkan8.js">
|
|
30
30
|
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-sVRjxPVQ.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vendor-utils-00TdZexr.js">
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
<link rel="modulepreload" crossorigin href="/assets/vendor-markdown-CjscLcYM.js">
|
|
35
35
|
<link rel="modulepreload" crossorigin href="/assets/vendor-syntax-BKENXTeY.js">
|
|
36
36
|
<link rel="modulepreload" crossorigin href="/assets/vendor-xterm-CvdiG4-n.js">
|
|
37
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
37
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DkNpDSsg.css">
|
|
38
38
|
</head>
|
|
39
39
|
<body>
|
|
40
40
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian2018cs/agenthub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.78",
|
|
4
4
|
"description": "A web-based UI for AI Agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
54
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.79",
|
|
55
55
|
"@codemirror/lang-css": "^6.3.1",
|
|
56
56
|
"@codemirror/lang-html": "^6.4.9",
|
|
57
57
|
"@codemirror/lang-javascript": "^6.2.4",
|
package/server/claude-sdk.js
CHANGED
|
@@ -13,9 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { query, renameSession, forkSession } from '@anthropic-ai/claude-agent-sdk';
|
|
16
|
-
// Used to mint unique approval request IDs when randomUUID is not available.
|
|
17
|
-
// This keeps parallel tool approvals from colliding; it does not add any crypto/security guarantees.
|
|
18
|
-
import crypto from 'crypto';
|
|
19
16
|
import { promises as fs } from 'fs';
|
|
20
17
|
import path from 'path';
|
|
21
18
|
import { CLAUDE_MODELS } from '../shared/modelConstants.js';
|
|
@@ -24,7 +21,7 @@ import { getUserPaths } from './services/user-directories.js';
|
|
|
24
21
|
import { usageDb } from './database/db.js';
|
|
25
22
|
import { calculateCost, normalizeModelName } from './services/pricing.js';
|
|
26
23
|
import { evaluate as evaluateToolGuard } from './services/tool-guard/index.js';
|
|
27
|
-
import {
|
|
24
|
+
import builtinTools, { createRequestId } from './services/builtin-tools/index.js';
|
|
28
25
|
|
|
29
26
|
// Session tracking: Map of session IDs to active query instances
|
|
30
27
|
const activeSessions = new Map();
|
|
@@ -78,41 +75,6 @@ const pendingToolApprovals = new Map();
|
|
|
78
75
|
// introduced to avoid hanging the run when no decision arrives.
|
|
79
76
|
const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
|
|
80
77
|
|
|
81
|
-
// ─── 分享项目模板:pending 请求管理 ───
|
|
82
|
-
const pendingShareTemplateRequests = new Map();
|
|
83
|
-
const SHARE_TEMPLATE_TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟(用户需要时间审阅和修改模板)
|
|
84
|
-
|
|
85
|
-
function waitForShareTemplateResponse(requestId) {
|
|
86
|
-
return new Promise(resolve => {
|
|
87
|
-
let settled = false;
|
|
88
|
-
const finalize = (result) => {
|
|
89
|
-
if (settled) return;
|
|
90
|
-
settled = true;
|
|
91
|
-
pendingShareTemplateRequests.delete(requestId);
|
|
92
|
-
clearTimeout(timeout);
|
|
93
|
-
resolve(result);
|
|
94
|
-
};
|
|
95
|
-
const timeout = setTimeout(() => finalize(null), SHARE_TEMPLATE_TIMEOUT_MS);
|
|
96
|
-
pendingShareTemplateRequests.set(requestId, finalize);
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function resolveShareTemplateRequest(requestId, result) {
|
|
101
|
-
const resolver = pendingShareTemplateRequests.get(requestId);
|
|
102
|
-
if (resolver) resolver(result);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Generate a stable request ID for UI approval flows.
|
|
106
|
-
// This does not encode tool details or get shown to users; it exists so the UI
|
|
107
|
-
// can respond to the correct pending request without collisions.
|
|
108
|
-
function createRequestId() {
|
|
109
|
-
// if clause is used because randomUUID is not available in older Node.js versions
|
|
110
|
-
if (typeof crypto.randomUUID === 'function') {
|
|
111
|
-
return crypto.randomUUID();
|
|
112
|
-
}
|
|
113
|
-
return crypto.randomBytes(16).toString('hex');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
78
|
// Wait for a UI approval decision, honoring SDK cancellation.
|
|
117
79
|
// This does not auto-approve or auto-deny; it only resolves with UI input,
|
|
118
80
|
// and it cleans up the pending map to avoid leaks, introduced to prevent
|
|
@@ -271,28 +233,7 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
271
233
|
console.log(`Using model: ${sdkOptions.model}`);
|
|
272
234
|
|
|
273
235
|
// Map system prompt configuration
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
## 分享项目模板工具
|
|
277
|
-
|
|
278
|
-
当用户想要分享项目、创建项目模板、发布共享项目时,使用以下 Bash 命令打开分享弹窗:
|
|
279
|
-
|
|
280
|
-
\`\`\`bash
|
|
281
|
-
__share_project_template__ '{"path":"/项目路径","displayName":"显示名称","description":"描述","updateNotes":"更新说明","skills":["skill1"],"mcps":["mcp1"],"files":["file1.md"]}'
|
|
282
|
-
\`\`\`
|
|
283
|
-
|
|
284
|
-
参数说明:
|
|
285
|
-
- path (必填): 项目文件夹路径,可以是当前项目、子文件夹或其他项目路径
|
|
286
|
-
- displayName (可选): 模板显示名称,仅首次创建时使用(更新时忽略)
|
|
287
|
-
- description (可选): 模板描述,仅首次创建时使用(更新时忽略)
|
|
288
|
-
- updateNotes (可选): 更新说明,仅更新已有模板时使用
|
|
289
|
-
- skills (可选): 要包含的技能名称数组,空数组=让用户在弹窗中选择
|
|
290
|
-
- mcps (可选): 要包含的 MCP 服务名称数组,空数组=让用户在弹窗中选择
|
|
291
|
-
- files (可选): 要包含的文件相对路径数组,空数组=让用户在弹窗中选择
|
|
292
|
-
|
|
293
|
-
该命令会打开一个 UI 弹窗供用户确认和修改,等待用户操作完成后返回结果。
|
|
294
|
-
`;
|
|
295
|
-
const baseAppend = PRODUCT_SYSTEM_DESC + SHARE_PROJECT_TEMPLATE_TOOL_DESC;
|
|
236
|
+
const baseAppend = PRODUCT_SYSTEM_DESC + builtinTools.getSystemPromptAppend();
|
|
296
237
|
const extraAppend = options.appendSystemPrompt ? `\n\n${options.appendSystemPrompt}` : '';
|
|
297
238
|
sdkOptions.systemPrompt = {
|
|
298
239
|
type: 'preset',
|
|
@@ -647,96 +588,11 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
647
588
|
...((sdkOptions.hooks || {}).PreToolUse || []),
|
|
648
589
|
{
|
|
649
590
|
hooks: [async (hookInput) => {
|
|
650
|
-
// =====
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (!mutableWriter?.current?.ws) {
|
|
656
|
-
return {
|
|
657
|
-
hookSpecificOutput: {
|
|
658
|
-
hookEventName: 'PreToolUse',
|
|
659
|
-
permissionDecision: 'deny',
|
|
660
|
-
permissionDecisionReason: '分享项目模板功能仅支持网页端使用。',
|
|
661
|
-
}
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// 解析参数
|
|
666
|
-
const rawArgs = hookInput.tool_input.command.replace(/^\s*__share_project_template__\s*/, '');
|
|
667
|
-
let jsonStr = rawArgs.trim();
|
|
668
|
-
if ((jsonStr.startsWith("'") && jsonStr.endsWith("'")) ||
|
|
669
|
-
(jsonStr.startsWith('"') && jsonStr.endsWith('"'))) {
|
|
670
|
-
jsonStr = jsonStr.slice(1, -1);
|
|
671
|
-
}
|
|
672
|
-
const params = JSON.parse(jsonStr);
|
|
673
|
-
|
|
674
|
-
if (!params.path) {
|
|
675
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny',
|
|
676
|
-
permissionDecisionReason: '缺少必需参数 path(项目路径)。' } };
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// 解析 path → projectKey,并判断是否为已有项目
|
|
680
|
-
const config = await loadProjectConfig(userUuid);
|
|
681
|
-
let projectKey = null;
|
|
682
|
-
let isExistingProject = false;
|
|
683
|
-
for (const [key, entry] of Object.entries(config)) {
|
|
684
|
-
if ((entry.originalPath || key.replace(/-/g, '/')) === params.path) {
|
|
685
|
-
projectKey = key;
|
|
686
|
-
isExistingProject = true;
|
|
687
|
-
break;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
if (!projectKey) {
|
|
691
|
-
// 路径不在项目列表中(可能是子文件夹),自动添加为新项目
|
|
692
|
-
try {
|
|
693
|
-
const project = await addProjectManually(params.path, params.displayName, userUuid);
|
|
694
|
-
projectKey = project.name;
|
|
695
|
-
isExistingProject = false;
|
|
696
|
-
} catch (e) {
|
|
697
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny',
|
|
698
|
-
permissionDecisionReason: `无法解析项目路径: ${e.message}` } };
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// 发送 WebSocket 消息打开分享弹窗
|
|
703
|
-
const shareRequestId = createRequestId();
|
|
704
|
-
mutableWriter.send({
|
|
705
|
-
type: 'share-project-template-request',
|
|
706
|
-
requestId: shareRequestId,
|
|
707
|
-
prefillData: {
|
|
708
|
-
projectKey,
|
|
709
|
-
projectPath: params.path,
|
|
710
|
-
isExistingProject,
|
|
711
|
-
displayName: params.displayName || '',
|
|
712
|
-
description: params.description || '',
|
|
713
|
-
updateNotes: params.updateNotes || '',
|
|
714
|
-
skills: params.skills || [],
|
|
715
|
-
mcps: params.mcps || [],
|
|
716
|
-
files: params.files || [],
|
|
717
|
-
}
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
// 等待前端响应(5 分钟超时)
|
|
721
|
-
const response = await waitForShareTemplateResponse(shareRequestId);
|
|
722
|
-
|
|
723
|
-
let reason;
|
|
724
|
-
if (!response) {
|
|
725
|
-
reason = '分享项目模板请求超时(5分钟内未收到响应)。';
|
|
726
|
-
} else if (response.cancelled) {
|
|
727
|
-
reason = '用户取消了分享项目模板操作。';
|
|
728
|
-
} else if (response.success) {
|
|
729
|
-
reason = `项目模板已成功提交!提交 ID: ${response.submissionId}。${response.message || ''}`;
|
|
730
|
-
} else {
|
|
731
|
-
reason = `提交失败: ${response.error || '未知错误'}`;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } };
|
|
735
|
-
} catch (err) {
|
|
736
|
-
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny',
|
|
737
|
-
permissionDecisionReason: `分享项目模板失败: ${err.message}` } };
|
|
738
|
-
}
|
|
739
|
-
}
|
|
591
|
+
// ===== 内置工具拦截(在 ToolGuard 之前,匹配后直接 return) =====
|
|
592
|
+
const builtinResult = await builtinTools.handlePreToolUse(hookInput, {
|
|
593
|
+
userUuid, mutableWriter, cwd: sdkOptions.cwd,
|
|
594
|
+
});
|
|
595
|
+
if (builtinResult) return builtinResult;
|
|
740
596
|
|
|
741
597
|
// ===== 安全守卫(Tool Guard)=====
|
|
742
598
|
try {
|
|
@@ -1140,7 +996,6 @@ export {
|
|
|
1140
996
|
isClaudeSDKSessionActive,
|
|
1141
997
|
getActiveClaudeSDKSessions,
|
|
1142
998
|
resolveToolApproval,
|
|
1143
|
-
resolveShareTemplateRequest,
|
|
1144
999
|
renameSessionForUser,
|
|
1145
1000
|
forkSessionForUser,
|
|
1146
1001
|
updateSessionWriter
|
package/server/index.js
CHANGED
|
@@ -64,7 +64,14 @@ import fetch from 'node-fetch';
|
|
|
64
64
|
import mime from 'mime-types';
|
|
65
65
|
|
|
66
66
|
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, updateProjectLastActivity } from './projects.js';
|
|
67
|
-
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval,
|
|
67
|
+
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, renameSessionForUser, forkSessionForUser, updateSessionWriter } from './claude-sdk.js';
|
|
68
|
+
import builtinTools, {
|
|
69
|
+
backgroundTaskPool,
|
|
70
|
+
enqueueResult,
|
|
71
|
+
dequeueResult,
|
|
72
|
+
hasResults,
|
|
73
|
+
getAllPendingForUser,
|
|
74
|
+
} from './services/builtin-tools/index.js';
|
|
68
75
|
import authRoutes from './routes/auth.js';
|
|
69
76
|
import mcpRoutes from './routes/mcp.js';
|
|
70
77
|
import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
@@ -88,6 +95,82 @@ import { startImageCleanup } from './services/image-cleanup.js';
|
|
|
88
95
|
const userWatchers = new Map(); // Map<userUuid, { watcher, clients: Set<ws> }>
|
|
89
96
|
const connectedClients = new Set();
|
|
90
97
|
|
|
98
|
+
// ─── 后台任务:用户连接映射 & 结果投递 ───
|
|
99
|
+
// Map<userUuid, Set<{ws, writer}>> — 追踪每个用户的所有 WebSocket 连接
|
|
100
|
+
const userConnections = new Map();
|
|
101
|
+
|
|
102
|
+
function addUserConnection(userUuid, ws, writer) {
|
|
103
|
+
if (!userUuid) return;
|
|
104
|
+
if (!userConnections.has(userUuid)) userConnections.set(userUuid, new Set());
|
|
105
|
+
userConnections.get(userUuid).add({ ws, writer });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function removeUserConnection(userUuid, ws) {
|
|
109
|
+
if (!userUuid) return;
|
|
110
|
+
const conns = userConnections.get(userUuid);
|
|
111
|
+
if (!conns) return;
|
|
112
|
+
for (const conn of conns) {
|
|
113
|
+
if (conn.ws === ws) { conns.delete(conn); break; }
|
|
114
|
+
}
|
|
115
|
+
if (conns.size === 0) userConnections.delete(userUuid);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 尝试向前端投递一条后台任务完成结果
|
|
120
|
+
* 条件:有待投递结果 + session 空闲 + 用户在线
|
|
121
|
+
*/
|
|
122
|
+
function tryDeliverBgResult(userUuid, sessionId) {
|
|
123
|
+
if (!hasResults(userUuid, sessionId)) return;
|
|
124
|
+
if (isClaudeSDKSessionActive(sessionId)) return; // session 忙,等 query 结束后再投递
|
|
125
|
+
|
|
126
|
+
const result = dequeueResult(userUuid, sessionId);
|
|
127
|
+
if (!result) return;
|
|
128
|
+
|
|
129
|
+
const conns = userConnections.get(userUuid);
|
|
130
|
+
if (!conns || conns.size === 0) {
|
|
131
|
+
// 用户不在线,放回队列(入队到头部)
|
|
132
|
+
enqueueResult(userUuid, sessionId, result);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 找到一个可用的 WebSocket 连接发送通知
|
|
137
|
+
for (const { ws } of conns) {
|
|
138
|
+
if (ws.readyState === 1) { // WebSocket.OPEN
|
|
139
|
+
try {
|
|
140
|
+
ws.send(JSON.stringify({
|
|
141
|
+
type: 'background-task-complete',
|
|
142
|
+
taskId: result.id,
|
|
143
|
+
sessionId: result.sessionId,
|
|
144
|
+
command: result.command,
|
|
145
|
+
label: result.label,
|
|
146
|
+
status: result.status,
|
|
147
|
+
exitCode: result.exitCode,
|
|
148
|
+
signal: result.signal,
|
|
149
|
+
stdout: result.stdout,
|
|
150
|
+
stderr: result.stderr,
|
|
151
|
+
truncated: result.truncated,
|
|
152
|
+
duration: (result.endTime || Date.now()) - result.startTime,
|
|
153
|
+
cwd: result.cwd,
|
|
154
|
+
}));
|
|
155
|
+
console.log(`[BgTask] Delivered result ${result.id} to user ${userUuid}, session ${sessionId}`);
|
|
156
|
+
return;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error(`[BgTask] Failed to send result:`, err.message);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 所有连接都不可用,放回队列
|
|
164
|
+
enqueueResult(userUuid, sessionId, result);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 监听后台任务完成事件
|
|
168
|
+
backgroundTaskPool.on('task-complete', (task) => {
|
|
169
|
+
enqueueResult(task.userUuid, task.sessionId, task);
|
|
170
|
+
// 立即尝试投递
|
|
171
|
+
tryDeliverBgResult(task.userUuid, task.sessionId);
|
|
172
|
+
});
|
|
173
|
+
|
|
91
174
|
// Setup file system watcher for a specific user's Claude projects folder
|
|
92
175
|
async function setupUserProjectsWatcher(userUuid, ws) {
|
|
93
176
|
if (!userUuid) {
|
|
@@ -364,6 +447,17 @@ app.use(express.static(path.join(__dirname, '../dist'), {
|
|
|
364
447
|
// /api/config endpoint removed - no longer needed
|
|
365
448
|
// Frontend now uses window.location for WebSocket URLs
|
|
366
449
|
|
|
450
|
+
// Public version endpoint - reads from package.json at runtime to avoid build-time lag
|
|
451
|
+
app.get('/api/version', (req, res) => {
|
|
452
|
+
try {
|
|
453
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
454
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
455
|
+
res.json({ version: pkg.version });
|
|
456
|
+
} catch (e) {
|
|
457
|
+
res.json({ version: 'unknown' });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
367
461
|
app.get('/api/projects', authenticateToken, async (req, res) => {
|
|
368
462
|
try {
|
|
369
463
|
const projects = await getProjects(req.user.uuid);
|
|
@@ -788,6 +882,17 @@ function handleChatConnection(ws, userData) {
|
|
|
788
882
|
// Wrap WebSocket with writer for consistent interface with SSEStreamWriter
|
|
789
883
|
const writer = new WebSocketWriter(ws);
|
|
790
884
|
|
|
885
|
+
// Register in user connections mapping (for background task result delivery)
|
|
886
|
+
addUserConnection(userUuid, ws, writer);
|
|
887
|
+
|
|
888
|
+
// On reconnect, deliver any pending background task results
|
|
889
|
+
if (userUuid) {
|
|
890
|
+
const pendingSessions = getAllPendingForUser(userUuid);
|
|
891
|
+
for (const { sessionId } of pendingSessions) {
|
|
892
|
+
tryDeliverBgResult(userUuid, sessionId);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
791
896
|
ws.on('message', async (message) => {
|
|
792
897
|
try {
|
|
793
898
|
const data = JSON.parse(message);
|
|
@@ -818,6 +923,14 @@ function handleChatConnection(ws, userData) {
|
|
|
818
923
|
}
|
|
819
924
|
}
|
|
820
925
|
}
|
|
926
|
+
|
|
927
|
+
// Query 结束后,尝试投递待处理的后台任务结果
|
|
928
|
+
{
|
|
929
|
+
const sid = data.options?.sessionId || writer.getSessionId();
|
|
930
|
+
if (userUuid && sid) {
|
|
931
|
+
tryDeliverBgResult(userUuid, sid);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
821
934
|
} else if (data.type === 'abort-session') {
|
|
822
935
|
console.log('[DEBUG] Abort session request:', data.sessionId);
|
|
823
936
|
// Use Claude Agents SDK
|
|
@@ -841,17 +954,9 @@ function handleChatConnection(ws, userData) {
|
|
|
841
954
|
rememberEntry: data.rememberEntry
|
|
842
955
|
});
|
|
843
956
|
}
|
|
844
|
-
} else if (data.type
|
|
845
|
-
// Relay the user's
|
|
846
|
-
|
|
847
|
-
resolveShareTemplateRequest(data.requestId, {
|
|
848
|
-
success: Boolean(data.success),
|
|
849
|
-
cancelled: Boolean(data.cancelled),
|
|
850
|
-
submissionId: data.submissionId || null,
|
|
851
|
-
message: data.message || '',
|
|
852
|
-
error: data.error || '',
|
|
853
|
-
});
|
|
854
|
-
}
|
|
957
|
+
} else if (builtinTools.canHandleResponse(data.type) && data.requestId) {
|
|
958
|
+
// Relay the user's builtin-tool response back to the PreToolUse hook.
|
|
959
|
+
builtinTools.resolveResponse(data.requestId, data);
|
|
855
960
|
} else if (data.type === 'check-session-status') {
|
|
856
961
|
// Check if a specific session is currently processing
|
|
857
962
|
const sessionId = data.sessionId;
|
|
@@ -891,6 +996,7 @@ function handleChatConnection(ws, userData) {
|
|
|
891
996
|
console.log('🔌 Chat client disconnected');
|
|
892
997
|
// Remove from connected clients
|
|
893
998
|
connectedClients.delete(ws);
|
|
999
|
+
removeUserConnection(userUuid, ws);
|
|
894
1000
|
// Cleanup projects watcher for this user
|
|
895
1001
|
if (userUuid) {
|
|
896
1002
|
cleanupUserProjectsWatcher(userUuid, ws);
|
|
@@ -901,6 +1007,7 @@ function handleChatConnection(ws, userData) {
|
|
|
901
1007
|
console.error('[ERROR] Chat WebSocket error:', error.message);
|
|
902
1008
|
// Ensure cleanup on error as well (close may not fire after error)
|
|
903
1009
|
connectedClients.delete(ws);
|
|
1010
|
+
removeUserConnection(userUuid, ws);
|
|
904
1011
|
if (userUuid) {
|
|
905
1012
|
cleanupUserProjectsWatcher(userUuid, ws);
|
|
906
1013
|
}
|