231 lines
6.9 KiB
TypeScript
231 lines
6.9 KiB
TypeScript
import { defineStore } from 'pinia';
|
|
import { useInputStore } from './input';
|
|
|
|
interface ToastPayload {
|
|
title?: string;
|
|
message?: string;
|
|
type?: string;
|
|
}
|
|
|
|
interface ChatActionDependencies {
|
|
pushToast: (payload: ToastPayload) => void;
|
|
autoResizeInput: () => void;
|
|
focusComposer: () => void;
|
|
isConnected: () => boolean;
|
|
getSocket: () => { emit: (event: string, payload: any) => void } | null;
|
|
downloadResource: (url: string, filename: string) => Promise<void>;
|
|
}
|
|
|
|
const defaultDependencies: ChatActionDependencies = {
|
|
pushToast: () => {},
|
|
autoResizeInput: () => {},
|
|
focusComposer: () => {},
|
|
isConnected: () => false,
|
|
getSocket: () => null,
|
|
downloadResource: async () => {}
|
|
};
|
|
|
|
function escapeAttributeSelector(value: string) {
|
|
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
return CSS.escape(value);
|
|
}
|
|
return value.replace(/["\\]/g, '\\$&');
|
|
}
|
|
|
|
function decodeHtmlEntities(input: string) {
|
|
const textarea = document.createElement('textarea');
|
|
textarea.innerHTML = input;
|
|
return textarea.value;
|
|
}
|
|
|
|
export const useChatActionStore = defineStore('chatActions', {
|
|
state: () => ({
|
|
dependencies: { ...defaultDependencies }
|
|
}),
|
|
actions: {
|
|
registerDependencies(partial: Partial<ChatActionDependencies>) {
|
|
this.dependencies = {
|
|
...this.dependencies,
|
|
...partial
|
|
};
|
|
},
|
|
async copyActionContent(action: any, blockId?: string | number) {
|
|
const content = (action && (action.content || action.text || ''))?.toString();
|
|
if (!content) {
|
|
this.dependencies.pushToast({
|
|
title: '复制失败',
|
|
message: '未找到可复制的内容',
|
|
type: 'warning'
|
|
});
|
|
return false;
|
|
}
|
|
try {
|
|
await this.copyText(content);
|
|
this.dependencies.pushToast({
|
|
title: '已复制',
|
|
message: blockId ? `片段 ${blockId} 已复制到剪贴板` : '内容已复制到剪贴板',
|
|
type: 'success'
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
console.warn('复制失败:', error);
|
|
this.dependencies.pushToast({
|
|
title: '复制失败',
|
|
message: '浏览器阻止了复制操作,请手动选择文本后复制。',
|
|
type: 'error'
|
|
});
|
|
return false;
|
|
}
|
|
},
|
|
async copyCodeBlock(blockId: string) {
|
|
if (!blockId) {
|
|
return false;
|
|
}
|
|
const selector = `[data-code-id="${escapeAttributeSelector(blockId)}"]`;
|
|
const codeEl = document.querySelector(selector) as HTMLElement | null;
|
|
if (!codeEl) {
|
|
this.dependencies.pushToast({
|
|
title: '复制失败',
|
|
message: '未找到对应的代码块',
|
|
type: 'warning'
|
|
});
|
|
return false;
|
|
}
|
|
const encoded = codeEl.getAttribute('data-original-code');
|
|
const content = encoded ? decodeHtmlEntities(encoded) : codeEl.textContent || '';
|
|
if (!content.trim()) {
|
|
this.dependencies.pushToast({
|
|
title: '复制失败',
|
|
message: '代码内容为空',
|
|
type: 'warning'
|
|
});
|
|
return false;
|
|
}
|
|
try {
|
|
await this.copyText(content);
|
|
this.dependencies.pushToast({
|
|
title: '已复制',
|
|
message: '代码已复制到剪贴板',
|
|
type: 'success'
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
console.warn('复制代码失败:', error);
|
|
this.dependencies.pushToast({
|
|
title: '复制失败',
|
|
message: '浏览器阻止了复制操作,请手动复制。',
|
|
type: 'error'
|
|
});
|
|
return false;
|
|
}
|
|
},
|
|
applyActionContent(action: any) {
|
|
if (!action || !action.content) {
|
|
this.dependencies.pushToast({
|
|
title: '无法应用',
|
|
message: '缺少可应用的内容',
|
|
type: 'warning'
|
|
});
|
|
return false;
|
|
}
|
|
const inputStore = useInputStore();
|
|
inputStore.setInputMessage(action.content);
|
|
this.dependencies.autoResizeInput();
|
|
this.dependencies.focusComposer();
|
|
this.dependencies.pushToast({
|
|
title: '已填充',
|
|
message: '请检查内容后发送以应用这些修改。',
|
|
type: 'info'
|
|
});
|
|
return true;
|
|
},
|
|
runCommand(action: any) {
|
|
const command = (action && (action.command || action.content))?.toString();
|
|
if (!command) {
|
|
this.dependencies.pushToast({
|
|
title: '无法执行',
|
|
message: '没有可执行的命令内容',
|
|
type: 'warning'
|
|
});
|
|
return false;
|
|
}
|
|
if (!this.dependencies.isConnected()) {
|
|
this.dependencies.pushToast({
|
|
title: '连接已断开',
|
|
message: '请重新连接后再试。',
|
|
type: 'error'
|
|
});
|
|
return false;
|
|
}
|
|
const socket = this.dependencies.getSocket();
|
|
if (!socket || typeof socket.emit !== 'function') {
|
|
this.dependencies.pushToast({
|
|
title: '无法执行',
|
|
message: '命令通道不可用,请稍后重试。',
|
|
type: 'error'
|
|
});
|
|
return false;
|
|
}
|
|
socket.emit('send_command', { command });
|
|
this.dependencies.pushToast({
|
|
title: '命令已发送',
|
|
message: command,
|
|
type: 'success'
|
|
});
|
|
return true;
|
|
},
|
|
async downloadActionAttachment(action: any) {
|
|
if (!action || !action.path) {
|
|
this.dependencies.pushToast({
|
|
title: '下载失败',
|
|
message: '没有可下载的目标文件',
|
|
type: 'warning'
|
|
});
|
|
return false;
|
|
}
|
|
return this.downloadFile(action.path);
|
|
},
|
|
async downloadFile(path: string) {
|
|
if (!path) {
|
|
this.dependencies.pushToast({
|
|
title: '下载失败',
|
|
message: '没有提供有效的文件路径',
|
|
type: 'warning'
|
|
});
|
|
return false;
|
|
}
|
|
const url = `/api/download/file?path=${encodeURIComponent(path)}`;
|
|
const name = path.split('/').pop() || 'file';
|
|
try {
|
|
await this.dependencies.downloadResource(url, name);
|
|
return true;
|
|
} catch (error) {
|
|
console.warn('下载失败:', error);
|
|
if (error && (error as Error).message) {
|
|
this.dependencies.pushToast({
|
|
title: '下载失败',
|
|
message: (error as Error).message,
|
|
type: 'error'
|
|
});
|
|
}
|
|
return false;
|
|
}
|
|
},
|
|
async copyText(content: string) {
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
await navigator.clipboard.writeText(content);
|
|
return;
|
|
}
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = content;
|
|
textarea.style.position = 'fixed';
|
|
textarea.style.top = '-1000px';
|
|
document.body.appendChild(textarea);
|
|
textarea.focus();
|
|
textarea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textarea);
|
|
}
|
|
}
|
|
});
|