fix: dfeat: command parameters

This commit is contained in:
hypercross 2026-02-28 17:15:41 +08:00
parent 91d46dea75
commit 28066c7fff
3 changed files with 161 additions and 27 deletions

View File

@ -45,16 +45,16 @@ export const defaultCommands: Record<string, MdCommanderCommand> = {
roll: {
command: "roll",
description: "掷骰子 - 支持骰池、修饰符和组合",
options: {
formula: {
option: "formula",
parameters: [
{
name: "formula",
description: "骰子公式,如 3d6+{d4,d8}kh2-5",
type: "string",
required: true,
},
},
],
handler: (args) => {
const formula = args.formula || "1d6";
const formula = args.params.formula || "1d6";
const { rollSimple } = useDiceRoller();
const result = rollSimple(formula);
return {
@ -68,16 +68,19 @@ export const defaultCommands: Record<string, MdCommanderCommand> = {
// ==================== 工具函数 ====================
export function parseInput(input: string): {
export function parseInput(input: string, commands?: Record<string, MdCommanderCommand>): {
command?: string;
params: Record<string, string>;
options: Record<string, string>;
incompleteOption?: string;
incompleteParam?: { index: number; value: string };
} {
const result: {
command?: string;
params: Record<string, string>;
options: Record<string, string>;
incompleteOption?: string;
incompleteParam?: { index: number; value: string };
} = {
params: {},
options: {},
};
@ -87,14 +90,23 @@ export function parseInput(input: string): {
const parts = trimmed.split(/\s+/);
let i = 0;
// 获取命令
if (parts[0] && !parts[0].startsWith("-")) {
result.command = parts[0];
i = 1;
}
// 获取命令的参数定义
const cmd = result.command ? commands?.[result.command] : undefined;
const paramDefs = cmd?.parameters || [];
let paramIndex = 0;
// 解析参数和选项
while (i < parts.length) {
const part = parts[i];
if (part.startsWith("--")) {
// 选项 --key=value 或 --key value
const eqIndex = part.indexOf("=");
if (eqIndex !== -1) {
const key = part.slice(2, eqIndex);
@ -106,13 +118,38 @@ export function parseInput(input: string): {
result.options[key] = parts[i + 1];
i++;
} else {
result.incompleteOption = key;
// 未完成的选项
}
}
} else if (part.startsWith("-") && part.length === 2) {
// 短选项 -k value
const key = part.slice(1);
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
result.options[key] = parts[i + 1];
i++;
}
} else {
// 位置参数
if (paramIndex < paramDefs.length) {
const paramDef = paramDefs[paramIndex];
result.params[paramDef.name] = part;
paramIndex++;
}
}
i++;
}
// 检查是否有未完成的位置参数
if (paramIndex < paramDefs.length) {
const lastPart = parts[parts.length - 1];
if (lastPart && !lastPart.startsWith("-")) {
result.incompleteParam = {
index: paramIndex,
value: lastPart,
};
}
}
return result;
}
@ -131,7 +168,7 @@ export function getCompletions(
}));
}
const parsed = parseInput(trimmed);
const parsed = parseInput(trimmed, commands);
if (!parsed.command || !commands[parsed.command]) {
const commandCompletions = Object.values(commands)
@ -148,21 +185,40 @@ export function getCompletions(
}
const cmd = commands[parsed.command!];
if (!cmd || !cmd.options) return [];
if (!cmd) return [];
if (parsed.incompleteOption) {
const option = cmd.options[parsed.incompleteOption];
if (option?.type === "enum" && option.values) {
return option.values
.filter((v) => parsed.incompleteOption && v.startsWith(parsed.incompleteOption))
// 检查是否需要参数补全
const paramDefs = cmd.parameters || [];
const usedParams = Object.keys(parsed.params);
// 如果还有未填的参数,提供参数值补全
if (paramDefs.length > usedParams.length) {
const paramDef = paramDefs[usedParams.length];
if (paramDef.type === "enum" && paramDef.values) {
const currentValue = parsed.incompleteParam?.value || "";
return paramDef.values
.filter((v) => v.startsWith(currentValue))
.map((v) => ({
label: v,
type: "value",
description: paramDef.description,
insertText: v,
}));
}
// 其他类型的参数,显示提示
if (parsed.incompleteParam) {
return [{
label: `<${paramDef.name}>`,
type: "value",
description: `${paramDef.type}${paramDef.required !== false ? " (必填)" : ""}: ${paramDef.description || ""}`,
insertText: "",
}];
}
}
// 选项补全
if (!cmd.options) return [];
const usedOptions = Object.keys(parsed.options);
return Object.values(cmd.options)
.filter((opt) => !usedOptions.includes(opt.option))
@ -208,6 +264,10 @@ export interface UseCommanderReturn {
updateCompletions: () => void;
acceptCompletion: () => void;
commands: Record<string, MdCommanderCommand>;
historyIndex: () => number;
setHistoryIndex: (v: number) => void;
commandHistory: () => string[];
navigateHistory: (direction: 'up' | 'down') => void;
}
export function useCommander(
@ -219,6 +279,10 @@ export function useCommander(
const [completions, setCompletions] = createSignal<CompletionItem[]>([]);
const [selectedCompletion, setSelectedCompletion] = createSignal(0);
const [isFocused, setIsFocused] = createSignal(false);
// 命令历史
const [commandHistory, setCommandHistory] = createSignal<string[]>([]);
const [historyIndex, setHistoryIndex] = createSignal(-1);
const commands = { ...defaultCommands, ...customCommands };
@ -233,17 +297,17 @@ export function useCommander(
const input = inputValue().trim();
if (!input) return;
const parsed = parseInput(input);
const parsed = parseInput(input, commands);
const commandName = parsed.command;
const cmd = commands[commandName!];
let result: { message: string; type?: "success" | "error" | "warning" | "info" };
let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean };
if (!cmd) {
result = { message: `未知命令:${commandName}`, type: "error" };
} else if (cmd.handler) {
try {
result = cmd.handler(parsed.options);
result = cmd.handler({ params: parsed.params, options: parsed.options });
} catch (e) {
result = {
message: `执行错误:${e instanceof Error ? e.message : String(e)}`,
@ -263,6 +327,11 @@ export function useCommander(
};
setEntries((prev) => [...prev, newEntry]);
// 添加到命令历史
setCommandHistory((prev) => [...prev, input]);
setHistoryIndex(-1); // 重置历史索引
setInputValue("");
setShowCompletions(false);
@ -285,7 +354,7 @@ export function useCommander(
if (!comp) return;
const input = inputValue();
const parsed = parseInput(input);
const parsed = parseInput(input, commands);
let newValue: string;
if (comp.type === "command") {
@ -296,24 +365,58 @@ export function useCommander(
.map(([k, v]) => `--${k}=${v}`)
.join(" ");
newValue = `${base} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`;
} else {
const optKey = parsed.incompleteOption;
if (optKey) {
} else if (comp.type === "value") {
// 参数值补全
const cmd = parsed.command ? commands[parsed.command] : null;
const paramDefs = cmd?.parameters || [];
const usedParams = Object.keys(parsed.params);
if (paramDefs.length > usedParams.length) {
// 当前参数的补全
const base = parsed.command || "";
const otherOptions = Object.entries(parsed.options)
.filter(([k]) => k !== optKey)
const existingParams = Object.values(parsed.params).join(" ");
const existingOptions = Object.entries(parsed.options)
.map(([k, v]) => `--${k}=${v}`)
.join(" ");
newValue = `${base} --${optKey}=${comp.insertText}${otherOptions ? " " + otherOptions : ""}`;
newValue = `${base} ${existingParams}${existingParams ? " " : ""}${comp.insertText}${existingOptions ? " " + existingOptions : ""}`;
} else {
newValue = input;
}
} else {
newValue = input;
}
setInputValue(newValue.trim());
setShowCompletions(false);
};
const navigateHistory = (direction: 'up' | 'down') => {
const history = commandHistory();
if (history.length === 0) return;
let newIndex = historyIndex();
if (direction === 'up') {
// 向上浏览历史(更早的命令)
if (newIndex < history.length - 1) {
newIndex++;
}
} else {
// 向下浏览历史(更新的命令)
if (newIndex > 0) {
newIndex--;
} else {
// 回到当前输入
setInputValue("");
setHistoryIndex(-1);
return;
}
}
setHistoryIndex(newIndex);
// 从历史末尾获取命令(最新的在前)
setInputValue(history[history.length - 1 - newIndex]);
};
return {
inputValue,
entries,
@ -330,5 +433,9 @@ export function useCommander(
updateCompletions,
acceptCompletion,
commands,
historyIndex,
setHistoryIndex,
commandHistory,
navigateHistory,
};
}

View File

@ -40,6 +40,18 @@ customElement<MdCommanderProps>(
}
}
// 补全未打开时,使用上下键浏览历史
if (e.key === "ArrowUp" && !commander.showCompletions()) {
e.preventDefault();
commander.navigateHistory("up");
return;
}
if (e.key === "ArrowDown" && !commander.showCompletions()) {
e.preventDefault();
commander.navigateHistory("down");
return;
}
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
commander.handleCommand();

View File

@ -10,14 +10,29 @@ export interface MdCommanderProps {
export interface MdCommanderCommand {
command: string;
description?: string;
parameters?: MdCommanderParameter[];
options?: Record<string, MdCommanderOption>;
handler?: (args: Record<string, any>) => {
handler?: (args: {
params: Record<string, string>;
options: Record<string, string>;
}) => {
message: string;
type?: "success" | "error" | "info" | "warning";
isHtml?: boolean;
};
}
export interface MdCommanderParameter {
name: string;
description?: string;
type: MdCommanderOptionType;
required?: boolean;
default?: string;
min?: number;
max?: number;
values?: string[];
}
export type MdCommanderOptionType =
| "string"
| "number"