Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd3f648ccd | ||
|
|
ad0efe249c | ||
|
|
95d7ff1299 |
Generated
+1
@@ -5544,6 +5544,7 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"futures",
|
||||
"reqwest 0.12.28",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
|
||||
@@ -29,6 +29,7 @@ reqwest.workspace = true
|
||||
futures.workspace = true
|
||||
uuid.workspace = true
|
||||
dirs.workspace = true
|
||||
rusqlite.workspace = true
|
||||
xianren-core = { path = "../../crates/core" }
|
||||
xianren-engine = { path = "../../crates/engine" }
|
||||
xianren-download = { path = "../../crates/download" }
|
||||
|
||||
@@ -217,10 +217,12 @@ pub fn list_conversations(state: State<'_, App>) -> Result<Vec<xianren_core::Con
|
||||
pub fn create_conversation(
|
||||
state: State<'_, App>,
|
||||
title: String,
|
||||
model_id: Option<String>,
|
||||
) -> Result<xianren_core::Conversation, String> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let db = state.core.db.lock().unwrap();
|
||||
sessions_db::create_conversation(&db, &id, &title, None, None).map_err(|e| e.to_string())?;
|
||||
sessions_db::create_conversation(&db, &id, &title, model_id.as_deref(), None)
|
||||
.map_err(|e| e.to_string())?;
|
||||
sessions_db::get_conversation(&db, &id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "conversation not found".to_string())
|
||||
@@ -481,6 +483,9 @@ pub async fn chat_send(
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
// 始终把当前使用的模型写回会话,编辑/重新生成依赖它
|
||||
sessions_db::update_conversation_model(&db, &payload.conversation_id, &payload.model_id)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let user_message_id = sessions_db::add_message(
|
||||
&db,
|
||||
&payload.conversation_id,
|
||||
@@ -724,6 +729,21 @@ fn estimate_tokens(text: &str) -> usize {
|
||||
score / 4 + 1
|
||||
}
|
||||
|
||||
/// 解析会话使用的模型:优先会话记录;未记录时若本地只有一个模型则自动使用,否则提示。
|
||||
fn resolve_conversation_model(
|
||||
db: &rusqlite::Connection,
|
||||
conversation: &xianren_core::Conversation,
|
||||
) -> Result<String, String> {
|
||||
if let Some(id) = &conversation.model_id {
|
||||
return Ok(id.clone());
|
||||
}
|
||||
let all = models_db::list(db).map_err(|e| e.to_string())?;
|
||||
if all.len() == 1 {
|
||||
return Ok(all[0].id.clone());
|
||||
}
|
||||
Err("会话未记录模型:请先在该会话发送一条消息,或重新选择模型后重试".into())
|
||||
}
|
||||
|
||||
/// 重新生成某条助手回复:旧内容存入版本历史,截断后重新生成。
|
||||
#[tauri::command]
|
||||
pub async fn regenerate_message(
|
||||
@@ -768,10 +788,7 @@ pub async fn regenerate_message(
|
||||
images: m.images.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let model_id = conversation
|
||||
.model_id
|
||||
.clone()
|
||||
.ok_or_else(|| "conversation has no model".to_string())?;
|
||||
let model_id = resolve_conversation_model(&db, &conversation)?;
|
||||
let model = models_db::get(&db, &model_id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "model not found".to_string())?;
|
||||
@@ -824,7 +841,8 @@ pub async fn edit_message(
|
||||
}
|
||||
sessions_db::update_message_content(&db, &payload.message_id, &payload.content)
|
||||
.map_err(|e| e.to_string())?;
|
||||
sessions_db::delete_messages_after(&db, &payload.conversation_id, rowid)
|
||||
// 清空该提问之后的所有生成内容,保留编辑后的提问本身
|
||||
sessions_db::delete_messages_strictly_after(&db, &payload.conversation_id, rowid)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let history = sessions_db::list_messages(&db, &payload.conversation_id)
|
||||
.map_err(|e| e.to_string())?
|
||||
@@ -835,10 +853,7 @@ pub async fn edit_message(
|
||||
images: m.images,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let model_id = conversation
|
||||
.model_id
|
||||
.clone()
|
||||
.ok_or_else(|| "conversation has no model".to_string())?;
|
||||
let model_id = resolve_conversation_model(&db, &conversation)?;
|
||||
let model = models_db::get(&db, &model_id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "model not found".to_string())?;
|
||||
|
||||
@@ -65,6 +65,7 @@ impl CoreApp {
|
||||
("api_port", "1234".to_string()),
|
||||
("api_key", String::new()),
|
||||
("api_enabled", "false".to_string()),
|
||||
("upload_max_mb", "10".to_string()),
|
||||
];
|
||||
for (key, value) in defaults {
|
||||
let _ = settings::set(&db, key, &value);
|
||||
|
||||
@@ -85,6 +85,14 @@ pub fn touch_conversation(db: &Connection, id: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_conversation_model(db: &Connection, id: &str, model_id: &str) -> Result<()> {
|
||||
db.execute(
|
||||
"UPDATE conversations SET model_id = ?1 WHERE id = ?2",
|
||||
params![model_id, id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_conversation(db: &Connection, id: &str) -> Result<()> {
|
||||
db.execute("DELETE FROM conversations WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
@@ -193,6 +201,19 @@ pub fn delete_messages_after(db: &Connection, conversation_id: &str, rowid: i64)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除某条消息之后(不含该消息)的所有消息。
|
||||
pub fn delete_messages_strictly_after(
|
||||
db: &Connection,
|
||||
conversation_id: &str,
|
||||
rowid: i64,
|
||||
) -> Result<()> {
|
||||
db.execute(
|
||||
"DELETE FROM messages WHERE conversation_id = ?1 AND rowid > ?2",
|
||||
params![conversation_id, rowid],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_message_version(
|
||||
db: &Connection,
|
||||
message_id: &str,
|
||||
|
||||
+2
-2
@@ -194,8 +194,8 @@ export const api = {
|
||||
settingsSet: (key: string, value: string) =>
|
||||
invoke<void>("settings_set", { key, value }),
|
||||
listConversations: () => invoke<Conversation[]>("list_conversations"),
|
||||
createConversation: (title: string) =>
|
||||
invoke<Conversation>("create_conversation", { title }),
|
||||
createConversation: (title: string, modelId?: string) =>
|
||||
invoke<Conversation>("create_conversation", { title, modelId: modelId ?? null }),
|
||||
deleteConversation: (id: string) =>
|
||||
invoke<void>("delete_conversation", { id }),
|
||||
getMessages: (conversationId: string) =>
|
||||
|
||||
@@ -50,9 +50,6 @@ const TEXT_EXTENSIONS = new Set([
|
||||
"yml", "toml", "ini", "sh", "ps1", "bat", "sql", "env", "gitignore",
|
||||
]);
|
||||
|
||||
const MAX_IMAGE_SIZE = 3 * 1024 * 1024;
|
||||
const MAX_TEXT_SIZE = 100 * 1024;
|
||||
|
||||
export default function ChatPage() {
|
||||
const models = useStore((s) => s.models);
|
||||
const conversations = useStore((s) => s.conversations);
|
||||
@@ -75,6 +72,7 @@ export default function ChatPage() {
|
||||
const [editingDraft, setEditingDraft] = useState("");
|
||||
const [versionsFor, setVersionsFor] = useState<Record<string, MessageVersion[]>>({});
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [uploadLimitMb, setUploadLimitMb] = useState(10);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -104,6 +102,13 @@ export default function ChatPage() {
|
||||
}
|
||||
}, [models, selectedModelId]);
|
||||
|
||||
useEffect(() => {
|
||||
api.settingsGet().then((s) => {
|
||||
const v = Number(s.upload_max_mb);
|
||||
if (v > 0) setUploadLimitMb(v);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages]);
|
||||
@@ -302,11 +307,12 @@ export default function ChatPage() {
|
||||
}
|
||||
|
||||
async function handleFiles(files: FileList | File[]) {
|
||||
const maxBytes = uploadLimitMb * 1024 * 1024;
|
||||
for (const file of Array.from(files)) {
|
||||
const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
|
||||
if (file.type.startsWith("image/") || ["png", "jpg", "jpeg", "webp", "gif", "bmp"].includes(ext)) {
|
||||
if (file.size > MAX_IMAGE_SIZE) {
|
||||
setError(`图片 ${file.name} 超过 3MB,已跳过`);
|
||||
if (file.size > maxBytes) {
|
||||
setError(`图片 ${file.name} 超过上限 ${uploadLimitMb}MB,已跳过`);
|
||||
continue;
|
||||
}
|
||||
const dataUrl = await readAsDataUrl(file);
|
||||
@@ -315,8 +321,8 @@ export default function ChatPage() {
|
||||
{ id: crypto.randomUUID(), kind: "image", name: file.name, size: file.size, dataUrl },
|
||||
]);
|
||||
} else if (file.type.startsWith("text/") || TEXT_EXTENSIONS.has(ext)) {
|
||||
if (file.size > MAX_TEXT_SIZE) {
|
||||
setError(`文件 ${file.name} 超过 100KB,已跳过`);
|
||||
if (file.size > maxBytes) {
|
||||
setError(`文件 ${file.name} 超过上限 ${uploadLimitMb}MB,已跳过`);
|
||||
continue;
|
||||
}
|
||||
const text = await file.text();
|
||||
@@ -331,7 +337,7 @@ export default function ChatPage() {
|
||||
}
|
||||
|
||||
async function handleNewConversation() {
|
||||
const conv = await api.createConversation("新会话");
|
||||
const conv = await api.createConversation("新会话", selectedModelId || undefined);
|
||||
setActiveConvId(conv.id);
|
||||
setMessages([]);
|
||||
setError(null);
|
||||
@@ -468,7 +474,7 @@ export default function ChatPage() {
|
||||
<button
|
||||
className="mb-1 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border bg-panel-2 text-slate-400 hover:bg-panel-2/60 hover:text-slate-200"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
title="上传图片或文本文件"
|
||||
title={`上传图片或文本文件(上限 ${uploadLimitMb}MB)`}
|
||||
>
|
||||
<Icon name="paperclip" className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -77,6 +77,11 @@ export default function SettingsPage() {
|
||||
value={settings.backend ?? ""}
|
||||
onSave={(v) => handleSave("backend", v)}
|
||||
/>
|
||||
<SettingInput
|
||||
label="上传图片/文件大小上限(MB)"
|
||||
value={settings.upload_max_mb ?? "10"}
|
||||
onSave={(v) => handleSave("upload_max_mb", v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user