@ian2018cs/agenthub 0.1.78 → 0.1.80
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-NaMmXkCt.js → index-ByqBXYb8.js} +49 -49
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/claude-sdk.js +12 -0
- package/server/index.js +5 -2
- package/server/routes/agents.js +58 -2
- package/server/services/builtin-tools/background-task.js +15 -0
- package/server/services/builtin-tools/index.js +18 -0
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-ByqBXYb8.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">
|
package/package.json
CHANGED
package/server/claude-sdk.js
CHANGED
|
@@ -760,6 +760,18 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
760
760
|
|
|
761
761
|
// Transform and send message to WebSocket
|
|
762
762
|
const transformedMessage = transformMessage(message);
|
|
763
|
+
|
|
764
|
+
// 为内置工具拦截的 tool_result 打 _builtinHandled 标记,前端据此不显示错误样式
|
|
765
|
+
if (transformedMessage.message?.role === 'user' && Array.isArray(transformedMessage.message?.content)) {
|
|
766
|
+
for (const part of transformedMessage.message.content) {
|
|
767
|
+
if (part.type === 'tool_result' && part.is_error && part.tool_use_id) {
|
|
768
|
+
if (builtinTools.consumeBuiltinHandled(part.tool_use_id)) {
|
|
769
|
+
part._builtinHandled = true;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
763
775
|
mutableWriter.send({
|
|
764
776
|
type: 'claude-response',
|
|
765
777
|
data: transformedMessage
|
package/server/index.js
CHANGED
|
@@ -924,11 +924,14 @@ function handleChatConnection(ws, userData) {
|
|
|
924
924
|
}
|
|
925
925
|
}
|
|
926
926
|
|
|
927
|
-
// Query
|
|
927
|
+
// Query 结束后,延迟投递待处理的后台任务结果
|
|
928
|
+
// 必须用 setTimeout 与 claude-complete 消息错开,避免两条 WS 消息
|
|
929
|
+
// 在同一事件循环 tick 内发出,导致前端 React 批量合并渲染时
|
|
930
|
+
// claude-complete 被跳过、isLoading 永久为 true 的竞态问题。
|
|
928
931
|
{
|
|
929
932
|
const sid = data.options?.sessionId || writer.getSessionId();
|
|
930
933
|
if (userUuid && sid) {
|
|
931
|
-
tryDeliverBgResult(userUuid, sid);
|
|
934
|
+
setTimeout(() => tryDeliverBgResult(userUuid, sid), 100);
|
|
932
935
|
}
|
|
933
936
|
}
|
|
934
937
|
} else if (data.type === 'abort-session') {
|
package/server/routes/agents.js
CHANGED
|
@@ -347,6 +347,47 @@ async function ensureSharedGitRepo(repoUrl, branch) {
|
|
|
347
347
|
return targetPath;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Try to move an existing local git clone directly into the shared location,
|
|
352
|
+
* avoiding a fresh network clone. Only succeeds when:
|
|
353
|
+
* - localPath is a real directory (not a symlink) with a .git folder
|
|
354
|
+
* - The shared target path does not yet exist
|
|
355
|
+
* Returns the shared path on success, or null if promotion is not possible.
|
|
356
|
+
*/
|
|
357
|
+
async function tryPromoteLocalRepoToShared(localPath, repoUrl, branch) {
|
|
358
|
+
const parsed = parseGitUrl(repoUrl);
|
|
359
|
+
if (!parsed) return null;
|
|
360
|
+
|
|
361
|
+
const publicPaths = getPublicPaths();
|
|
362
|
+
const targetPath = path.join(publicPaths.gitRepoDir, parsed.owner, parsed.repo, branch);
|
|
363
|
+
|
|
364
|
+
// Shared repo already exists — nothing to promote
|
|
365
|
+
try {
|
|
366
|
+
await fs.access(targetPath);
|
|
367
|
+
return null;
|
|
368
|
+
} catch {}
|
|
369
|
+
|
|
370
|
+
// Source must be a real git directory (not a symlink, must have .git)
|
|
371
|
+
try {
|
|
372
|
+
const stat = await fs.lstat(localPath);
|
|
373
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
|
374
|
+
await fs.access(path.join(localPath, '.git'));
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
381
|
+
await fs.rename(localPath, targetPath);
|
|
382
|
+
console.log(`[GitRepo] Promoted local repo to shared: ${localPath} → ${targetPath}`);
|
|
383
|
+
return targetPath;
|
|
384
|
+
} catch (e) {
|
|
385
|
+
// rename may fail on cross-device links — fall back to normal clone
|
|
386
|
+
console.warn(`[GitRepo] Could not promote local repo (will clone instead): ${e.message}`);
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
350
391
|
/**
|
|
351
392
|
* Create a symlink from a project subdirectory to a shared git repo.
|
|
352
393
|
* Removes existing directory or symlink at the target location.
|
|
@@ -513,6 +554,7 @@ async function scanClaudeMdRefs(projectPath) {
|
|
|
513
554
|
if (!ref) continue;
|
|
514
555
|
if (/^https?:\/\/|^mailto:|^\/\//.test(ref)) continue; // skip absolute URLs
|
|
515
556
|
if (path.isAbsolute(ref)) continue; // skip absolute paths
|
|
557
|
+
if (ref.endsWith('/')) continue; // skip directory references like [src/utils/]
|
|
516
558
|
if (seen.has(ref)) continue;
|
|
517
559
|
seen.add(ref);
|
|
518
560
|
|
|
@@ -1530,8 +1572,22 @@ router.post('/submissions/:id/approve', async (req, res) => {
|
|
|
1530
1572
|
for (const gitRepo of yamlGitRepos) {
|
|
1531
1573
|
if (!gitRepo.name || !gitRepo.repo) continue;
|
|
1532
1574
|
try {
|
|
1533
|
-
//
|
|
1534
|
-
|
|
1575
|
+
// Try to promote the submitter's existing local clone to shared location first
|
|
1576
|
+
// (avoids a full network re-clone if the user already has the repo cloned)
|
|
1577
|
+
let sharedPath = null;
|
|
1578
|
+
if (submitterUuid) {
|
|
1579
|
+
const submitterProjectDir = path.join(getUserPaths(submitterUuid).projectsDir, submission.agent_name);
|
|
1580
|
+
const localRepoPath = path.join(submitterProjectDir, gitRepo.name);
|
|
1581
|
+
sharedPath = await tryPromoteLocalRepoToShared(localRepoPath, gitRepo.repo, gitRepo.branch || 'main');
|
|
1582
|
+
if (sharedPath) {
|
|
1583
|
+
console.log(`[AgentApprove] Promoted local repo "${gitRepo.name}" to shared (no re-clone needed)`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Fall back to normal clone/update if promotion was not possible
|
|
1588
|
+
if (!sharedPath) {
|
|
1589
|
+
sharedPath = await ensureSharedGitRepo(gitRepo.repo, gitRepo.branch || 'main');
|
|
1590
|
+
}
|
|
1535
1591
|
console.log(`[AgentApprove] Ensured shared git repo "${gitRepo.name}" at ${sharedPath}`);
|
|
1536
1592
|
|
|
1537
1593
|
// Replace submitter's original subdirectory with symlink to shared repo
|
|
@@ -124,6 +124,21 @@ __bg_status__ bg_xxxxxxxxxxxx
|
|
|
124
124
|
return { decision: 'deny', reason: `后台任务提交失败: ${err.message}` };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// sessionId 可能在新会话的第一个 turn 内尚未建立(getSessionId() 返回 null)。
|
|
128
|
+
// 轮询直到 session-created 事件触发后 mutableWriter 持有真实 sessionId,
|
|
129
|
+
// 避免任务完成时以 null sessionId 投递导致前端无法匹配。
|
|
130
|
+
if (!task.sessionId) {
|
|
131
|
+
const pollTimer = setInterval(() => {
|
|
132
|
+
const sid = mutableWriter.getSessionId();
|
|
133
|
+
if (sid) {
|
|
134
|
+
task.sessionId = sid;
|
|
135
|
+
clearInterval(pollTimer);
|
|
136
|
+
}
|
|
137
|
+
}, 200);
|
|
138
|
+
// 最多等 60 秒,之后放弃(任务完成时 sessionId 仍为 null 则依赖前端兜底匹配)
|
|
139
|
+
setTimeout(() => clearInterval(pollTimer), 60_000);
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
const timeoutSec = Math.round(task.timeout / 1000);
|
|
128
143
|
return {
|
|
129
144
|
decision: 'deny',
|
|
@@ -31,6 +31,8 @@ class BuiltinToolRegistry {
|
|
|
31
31
|
#tools = [];
|
|
32
32
|
#responseTypes = new Set();
|
|
33
33
|
#pendingRequests = new Map();
|
|
34
|
+
/** tool_use_id → true,记录被内置工具拦截处理的工具调用(deny 即成功)*/
|
|
35
|
+
#builtinHandledIds = new Set();
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* 注册一个内置工具
|
|
@@ -67,6 +69,10 @@ class BuiltinToolRegistry {
|
|
|
67
69
|
|
|
68
70
|
try {
|
|
69
71
|
const result = await tool.execute(hookInput, toolContext);
|
|
72
|
+
// deny 作为正常拦截响应时,记录 tool_use_id 供后续标记
|
|
73
|
+
if (result.decision === 'deny' && hookInput.tool_use_id) {
|
|
74
|
+
this.#builtinHandledIds.add(hookInput.tool_use_id);
|
|
75
|
+
}
|
|
70
76
|
return {
|
|
71
77
|
hookSpecificOutput: {
|
|
72
78
|
hookEventName: 'PreToolUse',
|
|
@@ -89,6 +95,18 @@ class BuiltinToolRegistry {
|
|
|
89
95
|
return null;
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
/**
|
|
99
|
+
* 检查某个 tool_use_id 是否由内置工具拦截处理(deny = 成功)
|
|
100
|
+
* 调用后自动从集合中移除(一次性消费)
|
|
101
|
+
*/
|
|
102
|
+
consumeBuiltinHandled(toolUseId) {
|
|
103
|
+
if (this.#builtinHandledIds.has(toolUseId)) {
|
|
104
|
+
this.#builtinHandledIds.delete(toolUseId);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
/**
|
|
93
111
|
* 检查 WS 消息类型是否属于内置工具的响应
|
|
94
112
|
*/
|