重构(前端): 持久化快速命令排序和密码切换

添加持久化排序字段并重新排序快速命令和标签的端点,更新前端以支持手动拖放排序,并为连接和凭据表单添加密码可见性切换。此外,将 SSH 连接测试作为连接列表中的默认操作,并刷新相关模块文档和更改日志。
This commit is contained in:
yinjianm
2026-04-19 02:50:44 +08:00
parent 00d7c6c2f3
commit 8ce007a305
33 changed files with 1996 additions and 975 deletions
@@ -334,6 +334,42 @@ const definedMigrations: Migration[] = [
ALTER TABLE connections ADD COLUMN login_credential_id INTEGER NULL REFERENCES login_credentials(id) ON DELETE SET NULL;
`
},
{
id: 12,
name: 'Add sort_order column to quick_commands table',
check: async (db: Database): Promise<boolean> => {
const columnAlreadyExists = await columnExists(db, 'quick_commands', 'sort_order');
return !columnAlreadyExists;
},
sql: `
ALTER TABLE quick_commands ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
UPDATE quick_commands SET sort_order = id WHERE sort_order = 0;
`
},
{
id: 13,
name: 'Add sort_order column to quick_command_tags table',
check: async (db: Database): Promise<boolean> => {
const columnAlreadyExists = await columnExists(db, 'quick_command_tags', 'sort_order');
return !columnAlreadyExists;
},
sql: `
ALTER TABLE quick_command_tags ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
UPDATE quick_command_tags SET sort_order = id WHERE sort_order = 0;
`
},
{
id: 14,
name: 'Add sort_order column to quick_command_tag_associations table',
check: async (db: Database): Promise<boolean> => {
const columnAlreadyExists = await columnExists(db, 'quick_command_tag_associations', 'sort_order');
return !columnAlreadyExists;
},
sql: `
ALTER TABLE quick_command_tag_associations ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
UPDATE quick_command_tag_associations SET sort_order = rowid WHERE sort_order = 0;
`
}
];
+3
View File
@@ -187,6 +187,7 @@ CREATE TABLE IF NOT EXISTS quick_commands (
command TEXT NOT NULL, -- 指令必选
usage_count INTEGER NOT NULL DEFAULT 0, -- 使用频率
variables TEXT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
@@ -198,6 +199,7 @@ export const createQuickCommandTagsTableSQL = `
CREATE TABLE IF NOT EXISTS quick_command_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
@@ -207,6 +209,7 @@ export const createQuickCommandTagAssociationsTableSQL = `
CREATE TABLE IF NOT EXISTS quick_command_tag_associations (
quick_command_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (quick_command_id, tag_id),
FOREIGN KEY (quick_command_id) REFERENCES quick_commands(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES quick_command_tags(id) ON DELETE CASCADE
@@ -1,25 +1,21 @@
import { Request, Response } from 'express';
import * as QuickCommandTagService from './quick-command-tag.service';
/**
* 处理获取所有快捷指令标签的请求
*/
const isNumberArray = (value: unknown): value is number[] =>
Array.isArray(value) && value.every((item) => typeof item === 'number' && Number.isFinite(item));
export const getAllQuickCommandTags = async (req: Request, res: Response): Promise<void> => {
try {
const tags = await QuickCommandTagService.getAllQuickCommandTags();
res.status(200).json(tags);
} catch (error: any) {
console.error('[Controller] 获取快捷指令标签列表失败:', error.message);
console.error('[QuickCommandTagController] 获取快捷指令标签列表失败:', error.message);
res.status(500).json({ message: error.message || '无法获取快捷指令标签列表' });
}
};
/**
* 处理添加新快捷指令标签的请求
*/
export const addQuickCommandTag = async (req: Request, res: Response): Promise<void> => {
const { name } = req.body;
if (!name || typeof name !== 'string' || name.trim().length === 0) {
res.status(400).json({ message: '标签名称不能为空且必须是字符串' });
return;
@@ -27,37 +23,33 @@ export const addQuickCommandTag = async (req: Request, res: Response): Promise<v
try {
const newId = await QuickCommandTagService.addQuickCommandTag(name);
// 成功添加后,获取新创建的标签信息返回给前端
const newTag = await QuickCommandTagService.getQuickCommandTagById(newId);
if (newTag) {
res.status(201).json({ message: '快捷指令标签已添加', tag: newTag });
} else {
// 理论上不应该发生,但作为健壮性检查
console.error(`[Controller] 添加快捷指令标签后未能找到 ID: ${newId}`);
res.status(201).json({ message: '快捷指令标签已添加,但无法检索新记录', id: newId });
res.status(201).json({ message: '快捷指令标签已添加', tag: newTag });
return;
}
res.status(201).json({ message: '快捷指令标签已添加,但无法检索新记录', id: newId });
} catch (error: any) {
console.error('[Controller] 添加快捷指令标签失败:', error.message);
// 检查是否是名称重复错误
if (error.message && error.message.includes('已存在')) {
res.status(409).json({ message: error.message }); // 409 Conflict
} else {
res.status(500).json({ message: error.message || '无法添加快捷指令标签' });
console.error('[QuickCommandTagController] 添加快捷指令标签失败:', error.message);
if (error.message?.includes('已存在')) {
res.status(409).json({ message: error.message });
return;
}
res.status(500).json({ message: error.message || '无法添加快捷指令标签' });
}
};
/**
* 处理更新快捷指令标签的请求
*/
export const updateQuickCommandTag = async (req: Request, res: Response): Promise<void> => {
const id = parseInt(req.params.id, 10);
const id = Number.parseInt(req.params.id, 10);
const { name } = req.body;
if (isNaN(id)) {
if (Number.isNaN(id)) {
res.status(400).json({ message: '无效的标签 ID' });
return;
}
if (!name || typeof name !== 'string' || name.trim().length === 0) {
res.status(400).json({ message: '标签名称不能为空且必须是字符串' });
return;
@@ -65,66 +57,67 @@ export const updateQuickCommandTag = async (req: Request, res: Response): Promis
try {
const success = await QuickCommandTagService.updateQuickCommandTag(id, name);
if (success) {
// 成功更新后,获取更新后的标签信息返回给前端
const updatedTag = await QuickCommandTagService.getQuickCommandTagById(id);
if (updatedTag) {
res.status(200).json({ message: '快捷指令标签已更新', tag: updatedTag });
} else {
console.error(`[Controller] 更新快捷指令标签后未能找到 ID: ${id}`);
res.status(200).json({ message: '快捷指令标签已更新,但无法检索更新后的记录' });
}
} else {
// 检查标签是否真的不存在
if (!success) {
const tagExists = await QuickCommandTagService.getQuickCommandTagById(id);
if (!tagExists) {
res.status(404).json({ message: '未找到要更新的快捷指令标签' });
} else {
// 如果标签存在但更新失败(理论上不太可能,除非并发问题),返回服务器错误
console.error(`[Controller] 更新快捷指令标签 ${id} 失败,但标签存在。`);
res.status(500).json({ message: '更新快捷指令标签时发生未知错误' });
}
res.status(tagExists ? 500 : 404).json({
message: tagExists ? '更新快捷指令标签时发生未知错误' : '未找到要更新的快捷指令标签',
});
return;
}
const updatedTag = await QuickCommandTagService.getQuickCommandTagById(id);
if (updatedTag) {
res.status(200).json({ message: '快捷指令标签已更新', tag: updatedTag });
return;
}
res.status(200).json({ message: '快捷指令标签已更新,但无法检索更新后的记录' });
} catch (error: any) {
console.error('[Controller] 更新快捷指令标签失败:', error.message);
// 检查是否是名称重复错误
if (error.message && error.message.includes('已存在')) {
res.status(409).json({ message: error.message }); // 409 Conflict
} else {
res.status(500).json({ message: error.message || '无法更新快捷指令标签' });
console.error('[QuickCommandTagController] 更新快捷指令标签失败:', error.message);
if (error.message?.includes('已存在')) {
res.status(409).json({ message: error.message });
return;
}
res.status(500).json({ message: error.message || '无法更新快捷指令标签' });
}
};
/**
* 处理删除快捷指令标签的请求
*/
export const deleteQuickCommandTag = async (req: Request, res: Response): Promise<void> => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
const id = Number.parseInt(req.params.id, 10);
if (Number.isNaN(id)) {
res.status(400).json({ message: '无效的标签 ID' });
return;
}
try {
// 先检查标签是否存在,以便返回 404
const tagExists = await QuickCommandTagService.getQuickCommandTagById(id);
if (!tagExists) {
res.status(404).json({ message: '未找到要删除的快捷指令标签' });
return;
res.status(404).json({ message: '未找到要删除的快捷指令标签' });
return;
}
const success = await QuickCommandTagService.deleteQuickCommandTag(id);
if (success) {
res.status(200).json({ message: '快捷指令标签已删除' });
} else {
// 如果上面检查存在但删除失败,说明有内部错误
console.error(`[Controller] 删除快捷指令标签 ${id} 失败,但标签存在。`);
res.status(500).json({ message: '删除快捷指令标签时发生未知错误' });
}
res.status(success ? 200 : 500).json({
message: success ? '快捷指令标签已删除' : '删除快捷指令标签时发生未知错误',
});
} catch (error: any) {
console.error('[Controller] 删除快捷指令标签失败:', error.message);
console.error('[QuickCommandTagController] 删除快捷指令标签失败:', error.message);
res.status(500).json({ message: error.message || '无法删除快捷指令标签' });
}
};
};
export const reorderQuickCommandTags = async (req: Request, res: Response): Promise<void> => {
const { tagIds } = req.body;
if (!isNumberArray(tagIds) || tagIds.length === 0) {
res.status(400).json({ message: 'tagIds 必须是非空数字数组' });
return;
}
try {
await QuickCommandTagService.reorderQuickCommandTags(tagIds);
res.status(200).json({ message: '快捷指令标签顺序已更新' });
} catch (error: any) {
console.error('[QuickCommandTagController] 更新快捷指令标签顺序失败:', error.message);
res.status(500).json({ message: error.message || '无法更新快捷指令标签顺序' });
}
};
@@ -1,197 +1,268 @@
import { Database } from 'sqlite3';
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// 定义 Quick Command Tag 类型
export interface QuickCommandTag {
id: number;
name: string;
sort_order: number;
created_at: number;
updated_at: number;
}
/**
* 获取所有快捷指令标签
*/
interface CommandTagAssociationRow {
tag_id: number;
sort_order: number;
}
const getNextTagSortOrder = async (db: Awaited<ReturnType<typeof getDbInstance>>): Promise<number> => {
const row = await getDbRow<{ nextSortOrder?: number }>(
db,
'SELECT COALESCE(MAX(sort_order), 0) + 1 AS nextSortOrder FROM quick_command_tags'
);
return row?.nextSortOrder ?? 1;
};
const getNextAssociationSortOrder = async (
db: Awaited<ReturnType<typeof getDbInstance>>,
tagId: number,
): Promise<number> => {
const row = await getDbRow<{ nextSortOrder?: number }>(
db,
'SELECT COALESCE(MAX(sort_order), 0) + 1 AS nextSortOrder FROM quick_command_tag_associations WHERE tag_id = ?',
[tagId],
);
return row?.nextSortOrder ?? 1;
};
export const findAllQuickCommandTags = async (): Promise<QuickCommandTag[]> => {
try {
const db = await getDbInstance();
const rows = await allDb<QuickCommandTag>(db, `SELECT * FROM quick_command_tags ORDER BY name ASC`);
return rows;
return await allDb<QuickCommandTag>(
db,
'SELECT * FROM quick_command_tags ORDER BY sort_order ASC, name ASC',
);
} catch (err: any) {
console.error('[仓库] 查询快捷指令标签列表时出错:', err.message);
console.error('[QuickCommandTagRepository] 查询快捷指令标签列表失败:', err.message);
throw new Error('获取快捷指令标签列表失败');
}
};
/**
* 根据 ID 获取单个快捷指令标签
*/
export const findQuickCommandTagById = async (id: number): Promise<QuickCommandTag | null> => {
try {
const db = await getDbInstance();
const row = await getDbRow<QuickCommandTag>(db, `SELECT * FROM quick_command_tags WHERE id = ?`, [id]);
return row || null;
} catch (err: any) {
console.error(`[仓库] 查询快捷指令标签 ${id} 时出错:`, err.message);
throw new Error('获取快捷指令标签信息失败');
}
};
/**
* 创建新快捷指令标签
*/
export const createQuickCommandTag = async (name: string): Promise<number> => {
const now = Math.floor(Date.now() / 1000);
const sql = `INSERT INTO quick_command_tags (name, created_at, updated_at) VALUES (?, ?, ?)`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, now, now]);
const row = await getDbRow<QuickCommandTag>(db, 'SELECT * FROM quick_command_tags WHERE id = ?', [id]);
return row ?? null;
} catch (err: any) {
console.error(`[QuickCommandTagRepository] 查询快捷指令标签 ${id} 失败:`, err.message);
throw new Error('获取快捷指令标签信息失败');
}
};
export const createQuickCommandTag = async (name: string): Promise<number> => {
try {
const db = await getDbInstance();
const now = Math.floor(Date.now() / 1000);
const sortOrder = await getNextTagSortOrder(db);
const result = await runDb(
db,
'INSERT INTO quick_command_tags (name, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?)',
[name, sortOrder, now, now],
);
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建快捷指令标签后未能获取有效的 lastID');
throw new Error('创建快捷指令标签后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('[仓库] 创建快捷指令标签时出错:', err.message);
console.error('[QuickCommandTagRepository] 创建快捷指令标签失败:', err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`快捷指令标签名称 "${name}" 已存在。`);
throw new Error(`快捷指令标签名称 "${name}" 已存在。`);
}
throw new Error(`创建快捷指令标签失败: ${err.message}`);
}
};
/**
* 更新快捷指令标签名称
*/
export const updateQuickCommandTag = async (id: number, name: string): Promise<boolean> => {
const now = Math.floor(Date.now() / 1000);
const sql = `UPDATE quick_command_tags SET name = ?, updated_at = ? WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, now, id]);
const now = Math.floor(Date.now() / 1000);
const result = await runDb(
db,
'UPDATE quick_command_tags SET name = ?, updated_at = ? WHERE id = ?',
[name, now, id],
);
return result.changes > 0;
} catch (err: any) {
console.error(`[仓库] 更新快捷指令标签 ${id} 时出错:`, err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`快捷指令标签名称 "${name}" 已存在。`);
}
throw new Error(`更新快捷指令标签失败: ${err.message}`);
console.error(`[QuickCommandTagRepository] 更新快捷指令标签 ${id} 失败:`, err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`快捷指令标签名称 "${name}" 已存在。`);
}
throw new Error(`更新快捷指令标签失败: ${err.message}`);
}
};
/**
* 删除快捷指令标签 (同时会通过外键 CASCADE 删除关联)
*/
export const deleteQuickCommandTag = async (id: number): Promise<boolean> => {
const sql = `DELETE FROM quick_command_tags WHERE id = ?`;
try {
const db = await getDbInstance();
// 由于 quick_command_tag_associations 设置了 ON DELETE CASCADE,
// 删除 quick_command_tags 中的记录会自动删除关联表中的相关记录。
const result = await runDb(db, sql, [id]);
const result = await runDb(db, 'DELETE FROM quick_command_tags WHERE id = ?', [id]);
return result.changes > 0;
} catch (err: any) {
console.error(`[仓库] 删除快捷指令标签 ${id} 时出错:`, err.message);
console.error(`[QuickCommandTagRepository] 删除快捷指令标签 ${id} 失败:`, err.message);
throw new Error('删除快捷指令标签失败');
}
};
/**
* 设置单个快捷指令的标签关联 (先删除旧关联,再插入新关联)
* @param commandId - 快捷指令 ID
* @param tagIds - 新的标签 ID 数组 (空数组表示清除所有关联)
* @returns Promise<void>
*/
export const setCommandTagAssociations = async (commandId: number, tagIds: number[]): Promise<void> => {
const db = await getDbInstance();
const deleteSql = `DELETE FROM quick_command_tag_associations WHERE quick_command_id = ?`;
const insertSql = `INSERT INTO quick_command_tag_associations (quick_command_id, tag_id) VALUES (?, ?)`;
export const reorderQuickCommandTags = async (tagIds: number[]): Promise<void> => {
const normalizedTagIds = Array.from(new Set(tagIds.filter((tagId) => Number.isInteger(tagId) && tagId > 0)));
if (normalizedTagIds.length === 0) {
return;
}
const db = await getDbInstance();
try {
await runDb(db, 'BEGIN TRANSACTION');
// 1. 删除该指令的所有旧关联
await runDb(db, deleteSql, [commandId]);
// 2. 插入新关联 (如果 tagIds 不为空)
if (tagIds && tagIds.length > 0) {
const stmt = await db.prepare(insertSql);
for (const tagId of tagIds) {
// 验证 tagId 是否为有效数字
if (typeof tagId !== 'number' || isNaN(tagId)) {
console.warn(`[Repo] setCommandTagAssociations: 无效的 tagId (${tagId}),跳过关联到指令 ${commandId}`);
continue;
}
await stmt.run(commandId, tagId);
}
await stmt.finalize();
for (let index = 0; index < normalizedTagIds.length; index += 1) {
await runDb(
db,
'UPDATE quick_command_tags SET sort_order = ?, updated_at = strftime(\'%s\', \'now\') WHERE id = ?',
[index + 1, normalizedTagIds[index]],
);
}
await runDb(db, 'COMMIT');
} catch (err: any) {
console.error('设置快捷指令标签关联时出错:', err.message);
await runDb(db, 'ROLLBACK'); // 出错时回滚
await runDb(db, 'ROLLBACK');
console.error('[QuickCommandTagRepository] 重排快捷指令标签失败:', err.message);
throw new Error('无法更新快捷指令标签顺序');
}
};
export const setCommandTagAssociations = async (commandId: number, tagIds: number[]): Promise<void> => {
const normalizedTagIds = Array.from(new Set(tagIds.filter((tagId) => Number.isInteger(tagId) && tagId > 0)));
const db = await getDbInstance();
try {
const existingAssociations = await allDb<CommandTagAssociationRow>(
db,
'SELECT tag_id, sort_order FROM quick_command_tag_associations WHERE quick_command_id = ?',
[commandId],
);
const existingTagIds = new Set(existingAssociations.map((association) => association.tag_id));
await runDb(db, 'BEGIN TRANSACTION');
if (normalizedTagIds.length === 0) {
await runDb(db, 'DELETE FROM quick_command_tag_associations WHERE quick_command_id = ?', [commandId]);
} else {
const placeholders = normalizedTagIds.map(() => '?').join(', ');
await runDb(
db,
`DELETE FROM quick_command_tag_associations WHERE quick_command_id = ? AND tag_id NOT IN (${placeholders})`,
[commandId, ...normalizedTagIds],
);
for (const tagId of normalizedTagIds) {
if (existingTagIds.has(tagId)) {
continue;
}
const nextSortOrder = await getNextAssociationSortOrder(db, tagId);
await runDb(
db,
'INSERT INTO quick_command_tag_associations (quick_command_id, tag_id, sort_order) VALUES (?, ?, ?)',
[commandId, tagId, nextSortOrder],
);
}
}
await runDb(db, 'COMMIT');
} catch (err: any) {
await runDb(db, 'ROLLBACK');
console.error('[QuickCommandTagRepository] 设置快捷指令标签关联失败:', err.message);
throw new Error('无法设置快捷指令标签关联');
}
};
/**
* 将单个标签批量添加到多个快捷指令
* @param commandIds - 需要添加标签的快捷指令 ID 数组
* @param tagId - 要添加的标签 ID
* @returns Promise<void>
*/
export const addTagToCommands = async (commandIds: number[], tagId: number): Promise<void> => {
if (!commandIds || commandIds.length === 0) {
return; // 没有指令需要关联
const normalizedCommandIds = Array.from(
new Set(commandIds.filter((commandId) => Number.isInteger(commandId) && commandId > 0)),
);
if (normalizedCommandIds.length === 0 || !Number.isInteger(tagId) || tagId <= 0) {
return;
}
const db = await getDbInstance();
const insertSql = `INSERT OR IGNORE INTO quick_command_tag_associations (quick_command_id, tag_id) VALUES (?, ?)`;
try {
await runDb(db, 'BEGIN TRANSACTION');
// 准备批量插入语句
const stmt = await db.prepare(insertSql);
for (const commandId of commandIds) {
// 验证 commandId 和 tagId 是否为有效数字(可选,但推荐)
if (typeof commandId !== 'number' || isNaN(commandId) || typeof tagId !== 'number' || isNaN(tagId)) {
console.warn(`[Repo] addTagToCommands: 无效的 commandId (${commandId}) 或 tagId (${tagId}),跳过关联。`);
continue;
for (const commandId of normalizedCommandIds) {
const existingAssociation = await getDbRow<{ quick_command_id: number }>(
db,
'SELECT quick_command_id FROM quick_command_tag_associations WHERE quick_command_id = ? AND tag_id = ?',
[commandId, tagId],
);
if (existingAssociation) {
continue;
}
await stmt.run(commandId, tagId);
const nextSortOrder = await getNextAssociationSortOrder(db, tagId);
await runDb(
db,
'INSERT INTO quick_command_tag_associations (quick_command_id, tag_id, sort_order) VALUES (?, ?, ?)',
[commandId, tagId, nextSortOrder],
);
}
await stmt.finalize(); // 完成批量插入
await runDb(db, 'COMMIT');
console.log(`[Repo] addTagToCommands: 成功将标签 ${tagId} 关联到 ${commandIds.length} 个指令。`);
} catch (err: any) {
console.error(`[Repo] addTagToCommands: 批量关联标签 ${tagId} 到指令时出错:`, err.message);
await runDb(db, 'ROLLBACK');
console.error(`[QuickCommandTagRepository] 批量关联标签 ${tagId} 失败:`, err.message);
throw new Error('无法批量关联标签到快捷指令');
}
};
/**
* 更新指定快捷指令的标签关联 (使用事务)
* @param commandId 快捷指令 ID
* @param tagIds 新的快捷指令标签 ID 数组 (空数组表示清除所有标签)
*/
// Removed the duplicate function declaration that returned Promise<boolean>
export const reorderCommandsInTag = async (tagId: number, commandIds: number[]): Promise<void> => {
const normalizedCommandIds = Array.from(
new Set(commandIds.filter((commandId) => Number.isInteger(commandId) && commandId > 0)),
);
if (!Number.isInteger(tagId) || tagId <= 0 || normalizedCommandIds.length === 0) {
return;
}
const db = await getDbInstance();
try {
await runDb(db, 'BEGIN TRANSACTION');
for (let index = 0; index < normalizedCommandIds.length; index += 1) {
await runDb(
db,
'UPDATE quick_command_tag_associations SET sort_order = ? WHERE tag_id = ? AND quick_command_id = ?',
[index + 1, tagId, normalizedCommandIds[index]],
);
}
await runDb(db, 'COMMIT');
} catch (err: any) {
await runDb(db, 'ROLLBACK');
console.error(`[QuickCommandTagRepository] 重排标签 ${tagId} 内命令失败:`, err.message);
throw new Error('无法更新标签内快捷指令顺序');
}
};
/**
* 查找指定快捷指令的所有标签
* @param commandId 快捷指令 ID
* @returns 标签对象数组 { id: number, name: string }[]
*/
export const findTagsByCommandId = async (commandId: number): Promise<QuickCommandTag[]> => {
const sql = `
SELECT t.id, t.name, t.created_at, t.updated_at
SELECT t.id, t.name, t.sort_order, t.created_at, t.updated_at
FROM quick_command_tags t
JOIN quick_command_tag_associations ta ON t.id = ta.tag_id
WHERE ta.quick_command_id = ?
ORDER BY t.name ASC`;
ORDER BY ta.sort_order ASC, t.name ASC`;
try {
const db = await getDbInstance();
const rows = await allDb<QuickCommandTag>(db, sql, [commandId]);
return rows;
return await allDb<QuickCommandTag>(db, sql, [commandId]);
} catch (err: any) {
console.error(`Repository: 查询快捷指令 ${commandId} 的标签时出错:`, err.message);
console.error(`[QuickCommandTagRepository] 查询快捷指令 ${commandId} 的标签失败:`, err.message);
throw new Error('获取快捷指令标签失败');
}
};
};
@@ -1,19 +1,13 @@
import express from 'express';
import * as QuickCommandTagController from './quick-command-tag.controller';
import { isAuthenticated } from '../auth/auth.middleware'; // 假设需要认证
import { isAuthenticated } from '../auth/auth.middleware';
const router = express.Router();
// 获取所有快捷指令标签
router.get('/', isAuthenticated, QuickCommandTagController.getAllQuickCommandTags);
// 添加新的快捷指令标签
router.post('/', isAuthenticated, QuickCommandTagController.addQuickCommandTag);
// 更新快捷指令标签
router.put('/reorder', isAuthenticated, QuickCommandTagController.reorderQuickCommandTags);
router.put('/:id', isAuthenticated, QuickCommandTagController.updateQuickCommandTag);
// 删除快捷指令标签
router.delete('/:id', isAuthenticated, QuickCommandTagController.deleteQuickCommandTag);
export default router;
export default router;
@@ -1,118 +1,50 @@
import * as QuickCommandTagRepository from '../quick-command-tags/quick-command-tag.repository';
import { QuickCommandTag } from '../quick-command-tags/quick-command-tag.repository';
/**
* 获取所有快捷指令标签
*/
export const getAllQuickCommandTags = async (): Promise<QuickCommandTag[]> => {
return QuickCommandTagRepository.findAllQuickCommandTags();
};
/**
* 根据 ID 获取单个快捷指令标签
*/
export const getQuickCommandTagById = async (id: number): Promise<QuickCommandTag | null> => {
return QuickCommandTagRepository.findQuickCommandTagById(id);
};
/**
* 添加新的快捷指令标签
* @param name 标签名称
* @returns 返回新标签的 ID
*/
export const addQuickCommandTag = async (name: string): Promise<number> => {
if (!name || name.trim().length === 0) {
throw new Error('标签名称不能为空');
}
const trimmedName = name.trim();
// 可以在这里添加更多验证逻辑,例如检查名称格式等
try {
const newId = await QuickCommandTagRepository.createQuickCommandTag(trimmedName);
return newId;
} catch (error: any) {
// Service 层可以重新抛出或处理 Repository 抛出的错误
console.error(`[Service] 添加快捷指令标签 "${trimmedName}" 失败:`, error.message);
throw error; // 重新抛出,让 Controller 处理 HTTP 响应
}
return QuickCommandTagRepository.createQuickCommandTag(name.trim());
};
/**
* 更新快捷指令标签
* @param id 标签 ID
* @param name 新的标签名称
* @returns 返回是否成功更新
*/
export const updateQuickCommandTag = async (id: number, name: string): Promise<boolean> => {
if (!name || name.trim().length === 0) {
throw new Error('标签名称不能为空');
}
const trimmedName = name.trim();
// 可以在这里添加更多验证逻辑
try {
const success = await QuickCommandTagRepository.updateQuickCommandTag(id, trimmedName);
if (!success) {
// 可能需要检查标签是否存在,或者让 Repository 处理
console.warn(`[Service] 尝试更新不存在的快捷指令标签 ID: ${id}`);
}
return success;
} catch (error: any) {
console.error(`[Service] 更新快捷指令标签 ${id} 失败:`, error.message);
throw error;
}
return QuickCommandTagRepository.updateQuickCommandTag(id, name.trim());
};
/**
* 删除快捷指令标签
* @param id 标签 ID
* @returns 返回是否成功删除
*/
export const deleteQuickCommandTag = async (id: number): Promise<boolean> => {
try {
const success = await QuickCommandTagRepository.deleteQuickCommandTag(id);
if (!success) {
console.warn(`[Service] 尝试删除不存在的快捷指令标签 ID: ${id}`);
}
return success;
} catch (error: any) {
console.error(`[Service] 删除快捷指令标签 ${id} 失败:`, error.message);
throw error;
}
return QuickCommandTagRepository.deleteQuickCommandTag(id);
};
/**
* 设置指定快捷指令的标签关联
* @param commandId 快捷指令 ID
* @param tagIds 新的快捷指令标签 ID 数组
* @returns Promise<void>
*/
export const setCommandTags = async (commandId: number, tagIds: number[]): Promise<void> => {
// 验证 tagIds 是否为数字数组 (基本验证)
if (!Array.isArray(tagIds) || !tagIds.every(id => typeof id === 'number')) {
throw new Error('标签 ID 列表必须是一个数字数组');
if (!Array.isArray(tagIds) || !tagIds.every((id) => typeof id === 'number')) {
throw new Error('标签 ID 列表必须是数字数组');
}
// 可以在这里添加更复杂的验证,例如检查 tagIds 是否都存在于 quick_command_tags 表中
// 但 Repository 中的 setCommandTagAssociations 已包含基本的检查和错误处理
try {
// 直接调用 Repository 处理关联更新 (Repository 函数现在返回 void)
await QuickCommandTagRepository.setCommandTagAssociations(commandId, tagIds);
// Service 函数也返回 void,所以不需要 return
} catch (error: any) {
console.error(`[Service] 设置快捷指令 ${commandId} 的标签失败:`, error.message);
throw error;
}
await QuickCommandTagRepository.setCommandTagAssociations(commandId, tagIds);
};
/**
* 获取指定快捷指令的所有标签
* @param commandId 快捷指令 ID
* @returns 标签对象数组
*/
export const getTagsForCommand = async (commandId: number): Promise<QuickCommandTag[]> => {
try {
return await QuickCommandTagRepository.findTagsByCommandId(commandId);
} catch (error: any) {
console.error(`[Service] 获取快捷指令 ${commandId} 的标签失败:`, error.message);
throw error;
return QuickCommandTagRepository.findTagsByCommandId(commandId);
};
export const reorderQuickCommandTags = async (tagIds: number[]): Promise<void> => {
if (!Array.isArray(tagIds) || !tagIds.every((id) => typeof id === 'number')) {
throw new Error('tagIds 必须是数字数组');
}
};
await QuickCommandTagRepository.reorderQuickCommandTags(tagIds);
};
@@ -2,202 +2,202 @@ import { Request, Response } from 'express';
import * as QuickCommandsService from './quick-commands.service';
import { QuickCommandSortBy } from './quick-commands.service';
/**
* 处理添加新快捷指令的请求
*/
const isNumberArray = (value: unknown): value is number[] =>
Array.isArray(value) && value.every((item) => typeof item === 'number' && Number.isFinite(item));
export const addQuickCommand = async (req: Request, res: Response): Promise<void> => {
// 从请求体中解构出 name, command, 以及可选的 tagIds 和 variables
const { name, command, tagIds, variables } = req.body;
// --- 基本验证 ---
if (!command || typeof command !== 'string' || command.trim().length === 0) {
res.status(400).json({ message: '指令内容不能为空' });
return;
}
// 名称可以是 null 或 string
if (name !== null && typeof name !== 'string') {
res.status(400).json({ message: '名称必须是字符串或 null' });
return;
}
// 验证 tagIds (如果提供的话)
if (tagIds !== undefined && (!Array.isArray(tagIds) || !tagIds.every(id => typeof id === 'number'))) {
res.status(400).json({ message: 'tagIds 必须是一个数字数组' });
res.status(400).json({ message: '名称必须是字符串或 null' });
return;
}
// 验证 variables (如果提供的话)
if (tagIds !== undefined && !isNumberArray(tagIds)) {
res.status(400).json({ message: 'tagIds 必须是数字数组' });
return;
}
if (variables !== undefined && (typeof variables !== 'object' || variables === null || Array.isArray(variables))) {
res.status(400).json({ message: 'variables 必须是一个对象' });
res.status(400).json({ message: 'variables 必须是对象' });
return;
}
try {
// 将 tagIds 和 variables 传递给 Service 层
const newId = await QuickCommandsService.addQuickCommand(name, command, tagIds, variables);
// 尝试获取新创建的带标签的指令信息返回
const newCommand = await QuickCommandsService.getQuickCommandById(newId);
if (newCommand) {
res.status(201).json({ message: '快捷指令已添加', command: newCommand });
} else {
console.error(`[Controller] 添加快捷指令后未能找到 ID: ${newId}`);
res.status(201).json({ message: '快捷指令已添加,但无法检索新记录', id: newId });
return;
}
res.status(201).json({ message: '快捷指令已添加,但无法检索新记录', id: newId });
} catch (error: any) {
console.error('[Controller] 添加快捷指令失败:', error.message);
console.error('[QuickCommandsController] 添加快捷指令失败:', error.message);
res.status(500).json({ message: error.message || '无法添加快捷指令' });
}
};
/**
* 处理获取所有快捷指令的请求 (支持排序)
*/
export const getAllQuickCommands = async (req: Request, res: Response): Promise<void> => {
const sortBy = req.query.sortBy as QuickCommandSortBy | undefined;
// 验证 sortBy 参数
const validSortBy: QuickCommandSortBy = (sortBy === 'name' || sortBy === 'usage_count') ? sortBy : 'name';
const validSortBy: QuickCommandSortBy =
sortBy === 'name' || sortBy === 'usage_count' || sortBy === 'manual' ? sortBy : 'manual';
try {
const commands = await QuickCommandsService.getAllQuickCommands(validSortBy);
res.status(200).json(commands);
} catch (error: any) {
console.error('获取快捷指令控制器出错:', error);
console.error('[QuickCommandsController] 获取快捷指令失败:', error.message);
res.status(500).json({ message: error.message || '无法获取快捷指令' });
}
};
/**
* 处理更新快捷指令的请求
*/
export const updateQuickCommand = async (req: Request, res: Response): Promise<void> => {
const id = parseInt(req.params.id, 10);
// 从请求体中解构出 name, command, 以及可选的 tagIds 和 variables
const id = Number.parseInt(req.params.id, 10);
const { name, command, tagIds, variables } = req.body;
// --- 基本验证 ---
if (isNaN(id)) {
if (Number.isNaN(id)) {
res.status(400).json({ message: '无效的 ID' });
return;
}
if (!command || typeof command !== 'string' || command.trim().length === 0) {
res.status(400).json({ message: '指令内容不能为空' });
return;
}
if (name !== null && typeof name !== 'string') {
res.status(400).json({ message: '名称必须是字符串或 null' });
return;
}
// 验证 tagIds (如果提供的话)
// 注意: tagIds 为 undefined 表示不更新标签,空数组 [] 表示清除所有标签
if (tagIds !== undefined && (!Array.isArray(tagIds) || !tagIds.every(id => typeof id === 'number'))) {
res.status(400).json({ message: 'tagIds 必须是一个数字数组' });
res.status(400).json({ message: '名称必须是字符串或 null' });
return;
}
// 验证 variables (如果提供的话)
// undefined 表示不更新 variables, null 或对象表示要更新
if (variables !== undefined && variables !== null && (typeof variables !== 'object' || Array.isArray(variables))) {
if (tagIds !== undefined && !isNumberArray(tagIds)) {
res.status(400).json({ message: 'tagIds 必须是数字数组' });
return;
}
if (
variables !== undefined &&
variables !== null &&
(typeof variables !== 'object' || Array.isArray(variables))
) {
res.status(400).json({ message: 'variables 必须是对象或 null' });
return;
}
try {
// 将 tagIds 和 variables 传递给 Service 层
const success = await QuickCommandsService.updateQuickCommand(id, name, command, tagIds, variables);
if (success) {
// 尝试获取更新后的带标签的指令信息返回
const updatedCommand = await QuickCommandsService.getQuickCommandById(id);
if (updatedCommand) {
res.status(200).json({ message: '快捷指令已更新', command: updatedCommand });
} else {
console.error(`[Controller] 更新快捷指令后未能找到 ID: ${id}`);
res.status(200).json({ message: '快捷指令已更新,但无法检索更新后的记录' });
}
} else {
// 检查指令是否真的不存在
if (!success) {
const commandExists = await QuickCommandsService.getQuickCommandById(id);
if (!commandExists) {
res.status(404).json({ message: '未找到要更新的快捷指令' });
} else {
console.error(`[Controller] 更新快捷指令 ${id} 失败,但指令存在。`);
res.status(500).json({ message: '更新快捷指令时发生未知错误' });
}
res.status(commandExists ? 500 : 404).json({
message: commandExists ? '更新快捷指令时发生未知错误' : '未找到要更新的快捷指令',
});
return;
}
const updatedCommand = await QuickCommandsService.getQuickCommandById(id);
if (updatedCommand) {
res.status(200).json({ message: '快捷指令已更新', command: updatedCommand });
return;
}
res.status(200).json({ message: '快捷指令已更新,但无法检索更新后的记录' });
} catch (error: any) {
console.error('更新快捷指令控制器出错:', error);
console.error('[QuickCommandsController] 更新快捷指令失败:', error.message);
res.status(500).json({ message: error.message || '无法更新快捷指令' });
}
};
/**
* 处理删除快捷指令的请求
*/
export const deleteQuickCommand = async (req: Request, res: Response): Promise<void> => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
const id = Number.parseInt(req.params.id, 10);
if (Number.isNaN(id)) {
res.status(400).json({ message: '无效的 ID' });
return;
}
try {
const success = await QuickCommandsService.deleteQuickCommand(id);
if (success) {
res.status(200).json({ message: '快捷指令已删除' });
} else {
res.status(404).json({ message: '未找到要删除的快捷指令' });
}
res.status(success ? 200 : 404).json({
message: success ? '快捷指令已删除' : '未找到要删除的快捷指令',
});
} catch (error: any) {
console.error('删除快捷指令控制器出错:', error);
console.error('[QuickCommandsController] 删除快捷指令失败:', error.message);
res.status(500).json({ message: error.message || '无法删除快捷指令' });
}
};
/**
* 处理增加快捷指令使用次数的请求
*/
export const incrementUsage = async (req: Request, res: Response): Promise<void> => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
const id = Number.parseInt(req.params.id, 10);
if (Number.isNaN(id)) {
res.status(400).json({ message: '无效的 ID' });
return;
}
try {
const success = await QuickCommandsService.incrementUsageCount(id);
if (success) {
res.status(200).json({ message: '使用次数已增加' });
} else {
// 即使没找到也可能返回成功,避免不必要的错误提示
console.warn(`尝试增加不存在的快捷指令 (ID: ${id}) 的使用次数`);
res.status(200).json({ message: '使用次数已记录 (或指令不存在)' });
}
res.status(200).json({
message: success ? '使用次数已增加' : '使用次数已记录(或指令不存在)',
});
} catch (error: any) {
console.error('增加快捷指令使用次数控制器出错:', error);
console.error('[QuickCommandsController] 增加快捷指令使用次数失败:', error.message);
res.status(500).json({ message: error.message || '无法增加使用次数' });
}
};
/**
* 批量将标签分配给多个快捷指令
*/
export const assignTagToCommands = async (req: Request, res: Response): Promise<void> => { // Add : Promise<void>
export const assignTagToCommands = async (req: Request, res: Response): Promise<void> => {
const { commandIds, tagId } = req.body;
// 基本验证
if (!Array.isArray(commandIds) || commandIds.length === 0 || typeof tagId !== 'number') {
res.status(400).json({ success: false, message: '请求体必须包含 commandIds (非空数组) 和 tagId (数字)。' });
return; // Use return without value to exit early
if (!isNumberArray(commandIds) || commandIds.length === 0 || typeof tagId !== 'number') {
res.status(400).json({ success: false, message: '请求体必须包含 commandIds 和 tagId' });
return;
}
try {
// 调用 Service 函数处理批量分配
console.log(`[Controller] assignTagToCommands: Received commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`);
await QuickCommandsService.assignTagToCommands(commandIds, tagId);
res.status(200).json({ success: true, message: `标签 ${tagId} 已成功尝试关联到 ${commandIds.length} 个指令。` });
res.status(200).json({
success: true,
message: `标签 ${tagId} 已成功关联到 ${commandIds.length} 个指令`,
});
} catch (error: any) {
console.error('[Controller] 批量分配标签时出错:', error.message);
// 根据错误类型返回不同的状态码可能更好,但这里简化处理
res.status(500).json({ success: false, message: error.message || '批量分配标签时发生内部服务器错误。' });
// No return needed here, error handling completes the response
console.error('[QuickCommandsController] 批量分配标签失败:', error.message);
res.status(500).json({ success: false, message: error.message || '批量分配标签失败' });
}
};
export const reorderQuickCommands = async (req: Request, res: Response): Promise<void> => {
const { commandIds } = req.body;
if (!isNumberArray(commandIds) || commandIds.length === 0) {
res.status(400).json({ message: 'commandIds 必须是非空数字数组' });
return;
}
try {
await QuickCommandsService.reorderQuickCommands(commandIds);
res.status(200).json({ message: '快捷指令顺序已更新' });
} catch (error: any) {
console.error('[QuickCommandsController] 更新快捷指令顺序失败:', error.message);
res.status(500).json({ message: error.message || '无法更新快捷指令顺序' });
}
};
export const reorderCommandsByTag = async (req: Request, res: Response): Promise<void> => {
const { tagId, commandIds } = req.body;
if (typeof tagId !== 'number' || !isNumberArray(commandIds) || commandIds.length === 0) {
res.status(400).json({ message: 'tagId 和 commandIds 必须有效' });
return;
}
try {
await QuickCommandsService.reorderCommandsByTag(tagId, commandIds);
res.status(200).json({ message: '标签内快捷指令顺序已更新' });
} catch (error: any) {
console.error('[QuickCommandsController] 更新标签内快捷指令顺序失败:', error.message);
res.status(500).json({ message: error.message || '无法更新标签内快捷指令顺序' });
}
};
@@ -1,193 +1,247 @@
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// 定义基础快捷指令接口
export interface QuickCommand {
id: number;
name: string | null; // 名称可选
name: string | null;
command: string;
usage_count: number;
variables?: string; // 存储 JSON 格式的变量键值对
created_at: number; // Unix 时间戳 (秒)
updated_at: number; // Unix 时间戳 (秒)
sort_order: number;
variables?: string | null;
created_at: number;
updated_at: number;
}
// 定义包含标签 ID 和解析后变量的接口
export type QuickCommandWithTags = Omit<QuickCommand, 'variables'> & {
tagIds: number[];
variables: Record<string, string> | null; // API 层面使用对象
tagOrders: Record<number, number>;
variables: Record<string, string> | null;
};
// 用于从数据库获取带 tag_ids_str 的行
interface DbQuickCommandWithTagsRow extends QuickCommand {
tag_ids_str: string | null;
// variables 字段已包含在 QuickCommand 中,这里不需要重复定义,因为 QuickCommand 将包含 variables?: string
interface QuickCommandTagOrderRow {
quick_command_id: number;
tag_id: number;
sort_order: number;
}
type QuickCommandSortBy = 'manual' | 'name' | 'usage_count';
/**
* 添加一条新的快捷指令
* @param name - 指令名称 (可选)
* @param command - 指令内容
* @param variables - 变量对象 (可选)
* @returns 返回插入记录的 ID
*/
export const addQuickCommand = async (name: string | null, command: string, variables?: Record<string, string>): Promise<number> => {
const sql = `INSERT INTO quick_commands (name, command, variables, created_at, updated_at) VALUES (?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`;
const parseVariables = (variables: string | null | undefined, commandId: number): Record<string, string> | null => {
if (!variables) {
return null;
}
try {
return JSON.parse(variables);
} catch (error) {
console.error(`[QuickCommandsRepository] 解析快捷指令 ${commandId} 的 variables 失败:`, error);
return null;
}
};
const getNextQuickCommandSortOrder = async (db: Awaited<ReturnType<typeof getDbInstance>>): Promise<number> => {
const row = await getDbRow<{ nextSortOrder?: number }>(
db,
'SELECT COALESCE(MAX(sort_order), 0) + 1 AS nextSortOrder FROM quick_commands',
);
return row?.nextSortOrder ?? 1;
};
const getTagOrderMap = async (
db: Awaited<ReturnType<typeof getDbInstance>>,
commandIds: number[],
): Promise<Map<number, { tagIds: number[]; tagOrders: Record<number, number> }>> => {
const tagState = new Map<number, { tagIds: number[]; tagOrders: Record<number, number> }>();
if (commandIds.length === 0) {
return tagState;
}
const placeholders = commandIds.map(() => '?').join(', ');
const rows = await allDb<QuickCommandTagOrderRow>(
db,
`SELECT quick_command_id, tag_id, sort_order
FROM quick_command_tag_associations
WHERE quick_command_id IN (${placeholders})
ORDER BY quick_command_id ASC, sort_order ASC, tag_id ASC`,
commandIds,
);
for (const row of rows) {
if (!tagState.has(row.quick_command_id)) {
tagState.set(row.quick_command_id, { tagIds: [], tagOrders: {} });
}
const currentState = tagState.get(row.quick_command_id)!;
currentState.tagIds.push(row.tag_id);
currentState.tagOrders[row.tag_id] = row.sort_order;
}
return tagState;
};
const buildQuickCommandsWithTags = async (
db: Awaited<ReturnType<typeof getDbInstance>>,
rows: QuickCommand[],
): Promise<QuickCommandWithTags[]> => {
const commandIds = rows.map((row) => row.id);
const tagState = await getTagOrderMap(db, commandIds);
return rows.map((row) => {
const { variables, ...rest } = row;
const currentTagState = tagState.get(row.id);
return {
...rest,
variables: parseVariables(variables, row.id),
tagIds: currentTagState?.tagIds ?? [],
tagOrders: currentTagState?.tagOrders ?? {},
};
});
};
export const addQuickCommand = async (
name: string | null,
command: string,
variables?: Record<string, string>,
): Promise<number> => {
try {
const db = await getDbInstance();
const variablesJson = variables ? JSON.stringify(variables) : null;
const result = await runDb(db, sql, [name, command, variablesJson]);
const sortOrder = await getNextQuickCommandSortOrder(db);
const result = await runDb(
db,
`INSERT INTO quick_commands (name, command, usage_count, variables, sort_order, created_at, updated_at)
VALUES (?, ?, 0, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`,
[name, command, variablesJson, sortOrder],
);
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('添加快捷指令后未能获取有效的 lastID');
throw new Error('添加快捷指令后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('添加快捷指令时出错:', err.message);
console.error('[QuickCommandsRepository] 添加快捷指令失败:', err.message);
throw new Error('无法添加快捷指令');
}
};
/**
* 更新指定的快捷指令
* @param id - 要更新的记录 ID
* @param name - 新的指令名称 (可选)
* @param command - 新的指令内容
* @param variables - 新的变量对象 (可选)
* @returns 返回是否成功更新 (true/false)
*/
export const updateQuickCommand = async (id: number, name: string | null, command: string, variables?: Record<string, string>): Promise<boolean> => {
const sql = `UPDATE quick_commands SET name = ?, command = ?, variables = ?, updated_at = strftime('%s', 'now') WHERE id = ?`;
export const updateQuickCommand = async (
id: number,
name: string | null,
command: string,
variables?: Record<string, string>,
): Promise<boolean> => {
try {
const db = await getDbInstance();
const variablesJson = variables ? JSON.stringify(variables) : null;
const result = await runDb(db, sql, [name, command, variablesJson, id]);
const result = await runDb(
db,
'UPDATE quick_commands SET name = ?, command = ?, variables = ?, updated_at = strftime(\'%s\', \'now\') WHERE id = ?',
[name, command, variablesJson, id],
);
return result.changes > 0;
} catch (err: any) {
console.error('更新快捷指令时出错:', err.message);
console.error('[QuickCommandsRepository] 更新快捷指令失败:', err.message);
throw new Error('无法更新快捷指令');
}
};
/**
* 根据 ID 删除指定的快捷指令
* @param id - 要删除的记录 ID
* @returns 返回是否成功删除 (true/false)
*/
export const deleteQuickCommand = async (id: number): Promise<boolean> => {
const sql = `DELETE FROM quick_commands WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
const result = await runDb(db, 'DELETE FROM quick_commands WHERE id = ?', [id]);
return result.changes > 0;
} catch (err: any) {
console.error('删除快捷指令时出错:', err.message);
console.error('[QuickCommandsRepository] 删除快捷指令失败:', err.message);
throw new Error('无法删除快捷指令');
}
};
/**
* 获取所有快捷指令及其关联的标签 ID
* @param sortBy - 排序字段 ('name' 或 'usage_count')
* @returns 返回包含所有快捷指令条目及标签 ID 的数组
*/
export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name'): Promise<QuickCommandWithTags[]> => {
let orderByClause = 'ORDER BY qc.name ASC'; // 默认按名称升序
export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'manual'): Promise<QuickCommandWithTags[]> => {
let orderByClause = 'ORDER BY sort_order ASC, id ASC';
if (sortBy === 'usage_count') {
orderByClause = 'ORDER BY qc.usage_count DESC, qc.name ASC'; // 按使用频率降序,同频率按名称升序
orderByClause = 'ORDER BY usage_count DESC, name ASC, id ASC';
} else if (sortBy === 'name') {
orderByClause = 'ORDER BY name ASC, id ASC';
}
// 使用 LEFT JOIN 连接关联表,并使用 GROUP_CONCAT 获取标签 ID 字符串
const sql = `
SELECT
qc.id, qc.name, qc.command, qc.usage_count, qc.variables, qc.created_at, qc.updated_at,
GROUP_CONCAT(qta.tag_id) as tag_ids_str
FROM quick_commands qc
LEFT JOIN quick_command_tag_associations qta ON qc.id = qta.quick_command_id
GROUP BY qc.id
${orderByClause}`;
try {
const db = await getDbInstance();
const rows = await allDb<DbQuickCommandWithTagsRow>(db, sql);
// 将 tag_ids_str 解析为数字数组,并解析 variables
return rows.map(row => {
let parsedVariables: Record<string, string> | null = null;
if (row.variables) {
try {
parsedVariables = JSON.parse(row.variables);
} catch (e) {
console.error(`Error parsing variables for quick command ${row.id}:`, e);
//保持 parsedVariables 为 null
}
}
const { variables, ...restOfRow } = row; // 从 row 中移除原始的 string 类型的 variables
return {
...restOfRow,
variables: parsedVariables,
tagIds: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
};
});
const rows = await allDb<QuickCommand>(
db,
`SELECT id, name, command, usage_count, sort_order, variables, created_at, updated_at
FROM quick_commands
${orderByClause}`,
);
return await buildQuickCommandsWithTags(db, rows);
} catch (err: any) {
console.error('获取快捷指令(带标签)时出错:', err.message);
console.error('[QuickCommandsRepository] 获取快捷指令失败:', err.message);
throw new Error('无法获取快捷指令');
}
};
/**
* 增加指定快捷指令的使用次数
* @param id - 要增加次数的记录 ID
* @returns 返回是否成功更新 (true/false)
*/
export const incrementUsageCount = async (id: number): Promise<boolean> => {
const sql = `UPDATE quick_commands SET usage_count = usage_count + 1, updated_at = strftime('%s', 'now') WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
const result = await runDb(
db,
'UPDATE quick_commands SET usage_count = usage_count + 1, updated_at = strftime(\'%s\', \'now\') WHERE id = ?',
[id],
);
return result.changes > 0;
} catch (err: any) {
console.error('增加快捷指令使用次数时出错:', err.message);
console.error('[QuickCommandsRepository] 增加快捷指令使用次数失败:', err.message);
throw new Error('无法增加快捷指令使用次数');
}
};
/**
* 根据 ID 查找快捷指令及其关联的标签 ID
* @param id - 要查找的记录 ID
* @returns 返回找到的快捷指令条目及标签 ID,如果未找到则返回 undefined
*/
export const findQuickCommandById = async (id: number): Promise<QuickCommandWithTags | undefined> => {
// 使用 LEFT JOIN 连接关联表,并使用 GROUP_CONCAT 获取标签 ID 字符串
const sql = `
SELECT
qc.id, qc.name, qc.command, qc.usage_count, qc.variables, qc.created_at, qc.updated_at,
GROUP_CONCAT(qta.tag_id) as tag_ids_str
FROM quick_commands qc
LEFT JOIN quick_command_tag_associations qta ON qc.id = qta.quick_command_id
WHERE qc.id = ?
GROUP BY qc.id`;
try {
const db = await getDbInstance();
const row = await getDbRow<DbQuickCommandWithTagsRow>(db, sql, [id]);
if (row && typeof row.id !== 'undefined') {
// 将 tag_ids_str 解析为数字数组,并解析 variables
let parsedVariables: Record<string, string> | null = null;
if (row.variables) {
try {
parsedVariables = JSON.parse(row.variables);
} catch (e) {
console.error(`Error parsing variables for quick command ${row.id}:`, e);
//保持 parsedVariables 为 null
}
}
const { variables, ...restOfRow } = row; // 从 row 中移除原始的 string 类型的 variables
return {
...restOfRow,
variables: parsedVariables,
tagIds: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : []
};
} else {
const row = await getDbRow<QuickCommand>(
db,
`SELECT id, name, command, usage_count, sort_order, variables, created_at, updated_at
FROM quick_commands
WHERE id = ?`,
[id],
);
if (!row) {
return undefined;
}
const [hydratedRow] = await buildQuickCommandsWithTags(db, [row]);
return hydratedRow;
} catch (err: any) {
console.error('查找快捷指令(带标签)时出错:', err.message);
throw new Error('无法查快捷指令');
console.error('[QuickCommandsRepository] 查询快捷指令失败:', err.message);
throw new Error('无法查快捷指令');
}
};
export const reorderQuickCommands = async (commandIds: number[]): Promise<void> => {
const normalizedCommandIds = Array.from(
new Set(commandIds.filter((commandId) => Number.isInteger(commandId) && commandId > 0)),
);
if (normalizedCommandIds.length === 0) {
return;
}
const db = await getDbInstance();
try {
await runDb(db, 'BEGIN TRANSACTION');
for (let index = 0; index < normalizedCommandIds.length; index += 1) {
await runDb(
db,
'UPDATE quick_commands SET sort_order = ?, updated_at = strftime(\'%s\', \'now\') WHERE id = ?',
[index + 1, normalizedCommandIds[index]],
);
}
await runDb(db, 'COMMIT');
} catch (err: any) {
await runDb(db, 'ROLLBACK');
console.error('[QuickCommandsRepository] 重排快捷指令失败:', err.message);
throw new Error('无法更新快捷指令顺序');
}
};
@@ -9,7 +9,9 @@ router.use(isAuthenticated);
// 定义路由
router.post('/', QuickCommandsController.addQuickCommand); // POST /api/v1/quick-commands
router.get('/', QuickCommandsController.getAllQuickCommands); // GET /api/v1/quick-commands?sortBy=name|usage_count
router.get('/', QuickCommandsController.getAllQuickCommands); // GET /api/v1/quick-commands?sortBy=manual|name|usage_count
router.put('/reorder', QuickCommandsController.reorderQuickCommands); // PUT /api/v1/quick-commands/reorder
router.put('/reorder-by-tag', QuickCommandsController.reorderCommandsByTag); // PUT /api/v1/quick-commands/reorder-by-tag
router.put('/:id', QuickCommandsController.updateQuickCommand); // PUT /api/v1/quick-commands/:id
router.delete('/:id', QuickCommandsController.deleteQuickCommand); // DELETE /api/v1/quick-commands/:id
router.post('/:id/increment-usage', QuickCommandsController.incrementUsage); // POST /api/v1/quick-commands/:id/increment-usage
@@ -1,133 +1,102 @@
import * as QuickCommandsRepository from '../quick-commands/quick-commands.repository';
import { QuickCommandWithTags } from '../quick-commands/quick-commands.repository';
import * as QuickCommandTagRepository from '../quick-command-tags/quick-command-tag.repository';
import { QuickCommandWithTags } from '../quick-commands/quick-commands.repository';
import * as QuickCommandTagRepository from '../quick-command-tags/quick-command-tag.repository';
// 定义排序类型
export type QuickCommandSortBy = 'name' | 'usage_count';
export type QuickCommandSortBy = 'manual' | 'name' | 'usage_count';
/**
* 添加快捷指令
* @param name - 指令名称 (可选)
* @param command - 指令内容
* @param tagIds - 关联的快捷指令标签 ID 数组 (可选)
* @param variables - 变量对象 (可选)
* @returns 返回添加记录的 ID
*/
export const addQuickCommand = async (name: string | null, command: string, tagIds?: number[], variables?: Record<string, string>): Promise<number> => {
export const addQuickCommand = async (
name: string | null,
command: string,
tagIds?: number[],
variables?: Record<string, string>,
): Promise<number> => {
if (!command || command.trim().length === 0) {
throw new Error('指令内容不能为空');
}
// 如果 name 是空字符串,则视为 null
const finalName = name && name.trim().length > 0 ? name.trim() : null;
const commandId = await QuickCommandsRepository.addQuickCommand(finalName, command.trim(), variables);
// 添加成功后,设置标签关联
if (commandId > 0 && tagIds && Array.isArray(tagIds)) {
try {
await QuickCommandTagRepository.setCommandTagAssociations(commandId, tagIds);
} catch (tagError: any) {
// 如果标签关联失败,可以选择记录警告或回滚(但通常不回滚主记录)
console.warn(`[Service] 添加快捷指令 ${commandId} 成功,但设置标签关联失败:`, tagError.message);
// 可以考虑是否需要通知用户部分操作失败
console.warn(`[QuickCommandsService] 快捷指令 ${commandId} 已创建,但设置标签关联失败:`, tagError.message);
}
}
return commandId;
};
/**
* 更新快捷指令
* @param id - 要更新的记录 ID
* @param name - 新的指令名称 (可选)
* @param command - 新的指令内容
* @param tagIds - 新的关联标签 ID 数组 (可选, undefined 表示不更新标签)
* @param variables - 新的变量对象 (可选)
* @returns 返回是否成功更新 (更新行数 > 0)
*/
export const updateQuickCommand = async (id: number, name: string | null, command: string, tagIds?: number[], variables?: Record<string, string>): Promise<boolean> => {
export const updateQuickCommand = async (
id: number,
name: string | null,
command: string,
tagIds?: number[],
variables?: Record<string, string>,
): Promise<boolean> => {
if (!command || command.trim().length === 0) {
throw new Error('指令内容不能为空');
}
const finalName = name && name.trim().length > 0 ? name.trim() : null;
const commandUpdated = await QuickCommandsRepository.updateQuickCommand(id, finalName, command.trim(), variables);
// 如果指令更新成功,并且提供了 tagIds (即使是空数组也表示要更新),则更新标签关联
if (commandUpdated && typeof tagIds !== 'undefined') {
try {
try {
await QuickCommandTagRepository.setCommandTagAssociations(id, tagIds);
} catch (tagError: any) {
console.warn(`[Service] 更新快捷指令 ${id} 成功,但更新标签关联失败:`, tagError.message);
// 即使标签更新失败,主记录已更新,通常返回 true
}
} catch (tagError: any) {
console.warn(`[QuickCommandsService] 快捷指令 ${id} 已更新,但更新标签关联失败:`, tagError.message);
}
}
// 返回主记录是否更新成功
return commandUpdated;
};
/**
* 删除快捷指令
* @param id - 要删除的记录 ID
* @returns 返回是否成功删除 (删除行数 > 0)
*/
export const deleteQuickCommand = async (id: number): Promise<boolean> => {
const changes = await QuickCommandsRepository.deleteQuickCommand(id);
return changes;
return QuickCommandsRepository.deleteQuickCommand(id);
};
/**
* 获取所有快捷指令,并按指定方式排序
* @param sortBy - 排序字段 ('name' 或 'usage_count')
* @returns 返回排序后的快捷指令数组 (包含 tagIds)
*/
export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'name'): Promise<QuickCommandWithTags[]> => {
// Repository 已返回带 tagIds 的数据
export const getAllQuickCommands = async (sortBy: QuickCommandSortBy = 'manual'): Promise<QuickCommandWithTags[]> => {
return QuickCommandsRepository.getAllQuickCommands(sortBy);
};
/**
* 增加快捷指令的使用次数
* @param id - 记录 ID
* @returns 返回是否成功更新 (更新行数 > 0)
*/
export const incrementUsageCount = async (id: number): Promise<boolean> => {
const changes = await QuickCommandsRepository.incrementUsageCount(id);
return changes;
return QuickCommandsRepository.incrementUsageCount(id);
};
/**
* 根据 ID 获取单个快捷指令 (可能用于编辑)
* @param id - 记录 ID
* @returns 返回找到的快捷指令 (包含 tagIds),或 undefined
*/
export const getQuickCommandById = async (id: number): Promise<QuickCommandWithTags | undefined> => {
// Repository 已返回带 tagIds 的数据
return QuickCommandsRepository.findQuickCommandById(id);
};
/**
* 将单个标签批量关联到多个快捷指令
* @param commandIds - 需要添加标签的快捷指令 ID 数组
* @param tagId - 要添加的标签 ID
* @returns Promise<void>
*/
export const assignTagToCommands = async (commandIds: number[], tagId: number): Promise<void> => {
try {
// 基本验证
if (!Array.isArray(commandIds) || commandIds.some(id => typeof id !== 'number' || isNaN(id))) {
throw new Error('无效的指令 ID 列表');
}
if (typeof tagId !== 'number' || isNaN(tagId)) {
throw new Error('无效的标签 ID');
}
// 调用 Repository 函数执行批量关联
// 注意:这里需要导入 QuickCommandTagRepository
console.log(`[Service] assignTagToCommands: Calling repo with commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`);
await QuickCommandTagRepository.addTagToCommands(commandIds, tagId);
console.log(`[Service] assignTagToCommands: Repo call finished for tag ${tagId}.`); // +++ 修改日志 +++
// 可以在这里添加额外的业务逻辑,例如发送事件通知等
} catch (error: any) {
console.error(`[Service] assignTagToCommands: 批量关联标签 ${tagId} 到指令时出错:`, error.message);
// 向上抛出错误,让 Controller 处理 HTTP 响应
throw error;
if (!Array.isArray(commandIds) || commandIds.some((id) => typeof id !== 'number' || Number.isNaN(id))) {
throw new Error('无效的指令 ID 列表');
}
if (typeof tagId !== 'number' || Number.isNaN(tagId)) {
throw new Error('无效的标签 ID');
}
await QuickCommandTagRepository.addTagToCommands(commandIds, tagId);
};
export const reorderQuickCommands = async (commandIds: number[]): Promise<void> => {
if (!Array.isArray(commandIds) || commandIds.some((id) => typeof id !== 'number' || Number.isNaN(id))) {
throw new Error('commandIds 必须是数字数组');
}
await QuickCommandsRepository.reorderQuickCommands(commandIds);
};
export const reorderCommandsByTag = async (tagId: number, commandIds: number[]): Promise<void> => {
if (typeof tagId !== 'number' || Number.isNaN(tagId)) {
throw new Error('tagId 必须是数字');
}
if (!Array.isArray(commandIds) || commandIds.some((id) => typeof id !== 'number' || Number.isNaN(id))) {
throw new Error('commandIds 必须是数字数组');
}
await QuickCommandTagRepository.reorderCommandsInTag(tagId, commandIds);
};