@yeaft/webchat-agent 0.1.951 → 0.1.952
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/workbench/transfer.js +74 -1
package/package.json
CHANGED
package/workbench/transfer.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join, basename, extname } from 'path';
|
|
3
3
|
import ctx from '../context.js';
|
|
4
|
+
import { getProvider, DEFAULT_PROVIDER } from '../providers/index.js';
|
|
5
|
+
import { sendOutput, sendConversationList } from '../conversation.js';
|
|
4
6
|
|
|
5
7
|
// 临时文件目录名 (不易冲突)
|
|
6
8
|
const TEMP_UPLOAD_DIR = '.claude-tmp-attachments';
|
|
7
9
|
|
|
8
10
|
export async function handleTransferFiles(msg) {
|
|
9
11
|
const { conversationId, files, prompt, workDir, claudeSessionId } = msg;
|
|
10
|
-
const { startClaudeQuery } = await import('../claude.js');
|
|
11
12
|
|
|
12
13
|
let state = ctx.conversations.get(conversationId);
|
|
13
14
|
const effectiveWorkDir = workDir || state?.workDir || ctx.CONFIG.workDir;
|
|
@@ -54,6 +55,78 @@ export async function handleTransferFiles(msg) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// ★ Non-Claude providers (e.g. Copilot): route attachment sends through the
|
|
59
|
+
// driver, mirroring conversation.js `handleUserInput`. Before this branch
|
|
60
|
+
// existed, handleTransferFiles unconditionally spawned the Claude CLI via
|
|
61
|
+
// startClaudeQuery — so a copilot conversation that sent an attachment was
|
|
62
|
+
// misrouted to `claude --resume <stale-id>` and returned
|
|
63
|
+
// error_during_execution instantly ("一发送就挂了"). Copilot accepts inline
|
|
64
|
+
// image bytes over ACP, so images are handed to the driver as attachments
|
|
65
|
+
// instead of being inlined as disk paths the way the Claude path does.
|
|
66
|
+
//
|
|
67
|
+
// Note: when state is null here, providerName resolves to DEFAULT_PROVIDER
|
|
68
|
+
// ('claude-code'), so this branch only runs for an already-created
|
|
69
|
+
// non-claude conversation — state is guaranteed non-null inside it.
|
|
70
|
+
const providerName = state?.providerName || DEFAULT_PROVIDER;
|
|
71
|
+
if (providerName !== 'claude-code') {
|
|
72
|
+
const driver = getProvider(providerName);
|
|
73
|
+
if (workDir) state.workDir = workDir;
|
|
74
|
+
|
|
75
|
+
// Images go inline as provider attachments ([{type,data,mimeType}] — the
|
|
76
|
+
// shape copilot.sendInput understands). Non-image files can't be inlined,
|
|
77
|
+
// so reference their saved paths in the text and let the provider read
|
|
78
|
+
// them with its own file tools (it runs in effectiveWorkDir).
|
|
79
|
+
const attachments = imageFiles.map(img => ({
|
|
80
|
+
type: 'image',
|
|
81
|
+
data: img.data,
|
|
82
|
+
mimeType: img.mimeType,
|
|
83
|
+
}));
|
|
84
|
+
const nonImageFiles = savedFiles.filter(f => !f.isImage);
|
|
85
|
+
let effectivePrompt = prompt || '';
|
|
86
|
+
if (nonImageFiles.length > 0) {
|
|
87
|
+
const fileListText = nonImageFiles.map(f => `- ${f.path} (${f.mimeType})`).join('\n');
|
|
88
|
+
effectivePrompt = `${effectivePrompt}\n\n用户上传了以下文件(已保存到工作目录):\n${fileListText}`.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Echo the user turn so it persists and renders. We echo the RAW user text
|
|
92
|
+
// (not the path-augmented effectivePrompt) so the DB stores a clean user
|
|
93
|
+
// turn. Dedup against the frontend's optimistic copy is by clientMessageId
|
|
94
|
+
// (the server backfills it onto this echo) — see agent-output.js + dedup.js.
|
|
95
|
+
//
|
|
96
|
+
// Attachment-only send (no text): the server persistence gate drops a
|
|
97
|
+
// user message with empty content (agent-output.js), and copilot mirrors
|
|
98
|
+
// our own prompt back as a dropped user_message_chunk — so this echo is the
|
|
99
|
+
// ONLY source of the user turn. Use a non-empty placeholder so the turn
|
|
100
|
+
// survives a page refresh instead of vanishing. Matches the frontend's
|
|
101
|
+
// own attachment-only placeholder (web/stores/chat.js).
|
|
102
|
+
const echoContent = prompt && prompt.trim() ? prompt : '(attached files)';
|
|
103
|
+
sendOutput(conversationId, { type: 'user', message: { role: 'user', content: echoContent } });
|
|
104
|
+
state.turnActive = true;
|
|
105
|
+
sendConversationList();
|
|
106
|
+
try {
|
|
107
|
+
await driver.sendInput(state, effectivePrompt, { conversationId, raw: msg, attachments });
|
|
108
|
+
} catch (err) {
|
|
109
|
+
sendOutput(conversationId, {
|
|
110
|
+
type: 'result',
|
|
111
|
+
subtype: 'error',
|
|
112
|
+
session_id: state.sessionId || null,
|
|
113
|
+
is_error: true,
|
|
114
|
+
error: `${providerName} error: ${err?.message || err}`,
|
|
115
|
+
});
|
|
116
|
+
} finally {
|
|
117
|
+
state.turnActive = false;
|
|
118
|
+
if (state._abortKillTimer) {
|
|
119
|
+
clearTimeout(state._abortKillTimer);
|
|
120
|
+
state._abortKillTimer = null;
|
|
121
|
+
}
|
|
122
|
+
sendConversationList();
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- Claude-code path ----
|
|
128
|
+
const { startClaudeQuery } = await import('../claude.js');
|
|
129
|
+
|
|
57
130
|
// 如果没有活跃的查询,启动新的
|
|
58
131
|
if (!state || !state.query || !state.inputStream) {
|
|
59
132
|
const resumeSessionId = claudeSessionId || state?.claudeSessionId || null;
|