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

View File

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