@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.951",
3
+ "version": "0.1.952",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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;