Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions electron/main/channels/dingtalk/dingtalk-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1033,8 +1033,9 @@ export class DingTalkAdapter extends ChannelAdapter {
"发送消息即可开始对话。可用命令:",
"`/cancel` — 取消当前正在运行的消息",
"`/status` — 查看会话信息",
"`/mode` — 切换模式",
"`/model` — 切换模型",
"`/mode` — 切换当前会话模式",
"`/model` — 切换当前会话模型",
"`/effort` — 切换当前会话推理级别",
"`/history` — 查看会话历史记录",
"`/help` — 显示可用命令",
].join("\n");
Expand Down
5 changes: 3 additions & 2 deletions electron/main/channels/feishu/feishu-card-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ export function buildGroupWelcomeCard(
const commands = [
"/cancel — 取消当前正在运行的消息",
"/status — 查看会话信息",
"/mode agent|plan|build — 切换模式",
"/model list / /model model-id — 切换模型",
"/mode list / /mode mode-id — 切换当前会话模式",
"/model list / /model model-id — 切换当前会话模型",
"/effort list / /effort low|medium|high|max — 切换当前会话推理级别",
"/history — 查看会话历史记录",
"/help — 显示可用命令",
].join("\n");
Expand Down
13 changes: 13 additions & 0 deletions electron/main/channels/gateway-ws-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type EngineType,
type EngineInfo,
type EngineCapabilities,
type AgentMode,
type UnifiedSession,
type UnifiedMessage,
type ModelListResult,
Expand All @@ -30,6 +31,8 @@ import {
type ProjectSetEngineRequest,
type ModelSetRequest,
type ModeSetRequest,
type SessionConfigPatch,
type SessionConfigUpdateRequest,
} from "../../../src/types/unified";
import { channelLog } from "../services/logger";

Expand Down Expand Up @@ -321,8 +324,18 @@ export class GatewayWsClient {
return this.request(GatewayRequestType.MODEL_SET, req);
}

updateSessionConfig(sessionId: string, config: SessionConfigPatch): Promise<void> {
const req: SessionConfigUpdateRequest = { sessionId, config };
return this.request(GatewayRequestType.SESSION_CONFIG_UPDATE, req);
}

// --- Mode API ---

async listModes(engineType: EngineType): Promise<AgentMode[]> {
const engines = await this.listEngines();
return engines.find((engine) => engine.type === engineType)?.capabilities.availableModes ?? [];
}

setMode(req: ModeSetRequest): Promise<void> {
return this.request(GatewayRequestType.MODE_SET, req);
}
Expand Down
10 changes: 10 additions & 0 deletions electron/main/channels/shared/command-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,28 @@ const COMMAND_PREFIX = "/";

/** Sub-commands recognised for a given top-level command. */
const SUBCOMMANDS: Record<string, Set<string>> = {
// /mode list — list available modes
// /mode <id> — switch mode (handled as args, not subcommand)
mode: new Set(["list"]),
// /model list — list available models
// /model <id> — switch model (handled as args, not subcommand)
model: new Set(["list"]),
// /effort list — list available reasoning efforts
// /effort <level> — switch reasoning effort (handled as args, not subcommand)
effort: new Set(["list"]),
};

/**
* Parse a text message into a command structure. Returns null if not a command.
*
* "/help" → { command: "help", args: [], raw: "/help" }
* "/help@MyBot" → { command: "help", args: [], raw: "/help@MyBot" }
* "/mode list" → { command: "mode", subcommand: "list", args: [], raw: "..." }
* "/mode plan" → { command: "mode", args: ["plan"], raw: "..." }
* "/model list" → { command: "model", subcommand: "list", args: [], raw: "..." }
* "/model gpt-4o" → { command: "model", args: ["gpt-4o"], raw: "..." }
* "/effort list" → { command: "effort", subcommand: "list", args: [], raw: "..." }
* "/effort high" → { command: "effort", args: ["high"], raw: "..." }
*/
export function parseCommand(text: string): ParsedCommand | null {
const trimmed = text.trim();
Expand Down
3 changes: 2 additions & 1 deletion electron/main/channels/shared/command-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface ParsedCommand {
export interface CommandCapabilities {
/** Class A: project/session navigation (only meaningful in P2P). */
navigation: boolean;
/** Class B: current-session ops (cancel/status/mode/model/history). */
/** Class B: current-session ops (cancel/status/mode/model/effort/history). */
sessionOps: boolean;
/** Class C: help/start are always true; included for symmetry. */
general: boolean;
Expand Down Expand Up @@ -52,6 +52,7 @@ export const KNOWN_COMMANDS = [
"status",
"mode",
"model",
"effort",
"history",
// Class C — general
"help",
Expand Down
5 changes: 3 additions & 2 deletions electron/main/channels/shared/help-text-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ export function buildHelpText(
if (capabilities.sessionOps) {
lines.push("`/status` · 查看会话信息");
lines.push("`/cancel` · 取消运行中的消息");
lines.push("`/mode agent|plan|build` · 切换模式");
lines.push("`/model list` / `/model model-id` · 切换模型");
lines.push("`/mode list` / `/mode mode-id` · 切换当前会话模式");
lines.push("`/model list` / `/model model-id` · 切换当前会话模型");
lines.push("`/effort list` / `/effort low|medium|high|max` · 切换当前会话推理级别");
lines.push("`/history` · 查看历史");
}

Expand Down
159 changes: 138 additions & 21 deletions electron/main/channels/shared/session-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,49 @@
// ============================================================================

import type { GatewayWsClient } from "../gateway-ws-client";
import type { EngineType } from "../../../../src/types/unified";
import {
isReasoningEffort,
type EngineType,
type ReasoningEffort,
type UnifiedModelInfo,
} from "../../../../src/types/unified";
import type { ParsedCommand } from "./command-types";
import { buildHistoryEntries } from "./list-builders";

function escapeMarkdownInline(value: string): string {
return value.replace(/[\\*`]/g, "\\$&");
}

const effortLabels: Record<ReasoningEffort, string> = {
low: "低",
medium: "中",
high: "高",
max: "最大",
};

function getModelEfforts(models: UnifiedModelInfo[], modelId: string | undefined): ReasoningEffort[] {
if (!modelId) return [];
return models.find((model) => model.modelId === modelId)?.capabilities?.supportedReasoningEfforts ?? [];
}

function getModelDefaultEffort(models: UnifiedModelInfo[], modelId: string | undefined): ReasoningEffort | undefined {
if (!modelId) return undefined;
return models.find((model) => model.modelId === modelId)?.capabilities?.defaultReasoningEffort;
}

function resolveEffortForModelChange(
models: UnifiedModelInfo[],
modelId: string,
currentEffort: ReasoningEffort | undefined,
): ReasoningEffort | null | undefined {
const model = models.find((item) => item.modelId === modelId);
if (!model) return undefined;
const supportedEfforts = model.capabilities?.supportedReasoningEfforts ?? [];
if (supportedEfforts.length === 0) return null;
if (currentEffort && supportedEfforts.includes(currentEffort)) return currentEffort;
return model.capabilities?.defaultReasoningEffort ?? null;
}

/**
* The minimal context a session-ops command needs. Channels build this from
* their own state (P2P temp session, group binding, etc.) and pass it in.
Expand Down Expand Up @@ -55,6 +90,7 @@ export async function handleSessionOpsCommand(
case "status":
case "mode":
case "model":
case "effort":
case "history":
break;
default:
Expand Down Expand Up @@ -85,23 +121,46 @@ export async function handleSessionOpsCommand(
}

case "mode": {
if (!command.args || command.args.length === 0) {
await args.sendText([
"**📋 模式列表**",
"",
"- `agent` · 默认 Agent 模式",
"- `plan` · 规划模式",
"- `build` · 构建模式",
"",
"使用 `/mode agent`、`/mode plan` 或 `/mode build` 切换模式。",
].join("\n"));
const modes = await args.gatewayClient.listModes(ctx.engineType);
if (modes.length === 0) {
await args.sendText("📋 当前引擎不支持模式切换。");
return true;
}

const isList =
subcommand === "list" ||
(!subcommand && (!command.args || command.args.length === 0));
if (isList) {
const session = await args.gatewayClient.getSession(ctx.conversationId);
const currentModeId = session.mode ?? modes[0]?.id;
const lines = ["**📋 模式列表**", ""];
for (const mode of modes) {
const current = mode.id === currentModeId ? "(当前会话)" : "";
const modeId = escapeMarkdownInline(mode.id);
const label = escapeMarkdownInline(mode.label || mode.id);
if (mode.description) {
lines.push(`- ${label} · \`${modeId}\`${current} — ${escapeMarkdownInline(mode.description)}`);
} else {
lines.push(`- ${label} · \`${modeId}\`${current}`);
}
}
lines.push("");
lines.push("使用 `/mode mode-id` 切换当前会话模式。");
await args.sendText(lines.join("\n"));
return true;
}

const modeId = command.args?.[0];
if (!modeId || !modes.some((mode) => mode.id === modeId)) {
await args.sendText(`📋 当前引擎支持的模式:${modes.map((mode) => `\`${escapeMarkdownInline(mode.id)}\``).join("、")}`);
return true;
}

await args.gatewayClient.setMode({
sessionId: ctx.conversationId,
modeId: command.args[0],
modeId,
});
await args.sendText(`📋 模式已切换为:${command.args[0]}`);
await args.sendText(`📋 当前会话模式已切换为:${modeId}`);
return true;
}

Expand All @@ -110,27 +169,85 @@ export async function handleSessionOpsCommand(
subcommand === "list" ||
(!subcommand && (!command.args || command.args.length === 0));
if (isList) {
const result = await args.gatewayClient.listModels(ctx.engineType);
const [result, session] = await Promise.all([
args.gatewayClient.listModels(ctx.engineType),
args.gatewayClient.getSession(ctx.conversationId),
]);
const currentModelId = session.modelId ?? result.currentModelId;
let currentMarked = false;
const lines = ["**📋 模型列表**", ""];
for (const m of result.models) {
const current = m.modelId === result.currentModelId ? "(当前)" : "";
const current = m.modelId === currentModelId ? "(当前会话)" : "";
if (current) currentMarked = true;
const modelId = escapeMarkdownInline(m.modelId);
if (m.name && m.name !== m.modelId) {
lines.push(`- ${escapeMarkdownInline(m.name)} · \`${modelId}\`${current}`);
} else {
lines.push(`- \`${modelId}\`${current}`);
}
}
if (currentModelId && !currentMarked) {
lines.push(`- \`${escapeMarkdownInline(currentModelId)}\`(当前会话)`);
}
lines.push("");
lines.push("使用 `/model model-id` 切换模型。");
lines.push("使用 `/model model-id` 切换当前会话模型。");
await args.sendText(lines.join("\n"));
} else if (command.args && command.args.length > 0) {
await args.gatewayClient.setModel({
sessionId: ctx.conversationId,
modelId: command.args[0],
});
await args.sendText(`📋 模型已切换为:${command.args[0]}`);
const modelId = command.args[0];
const [result, session] = await Promise.all([
args.gatewayClient.listModels(ctx.engineType),
args.gatewayClient.getSession(ctx.conversationId),
]);
const nextEffort = resolveEffortForModelChange(result.models, modelId, session.reasoningEffort);
const config = nextEffort === undefined
? { modelId }
: { modelId, reasoningEffort: nextEffort };
await args.gatewayClient.updateSessionConfig(ctx.conversationId, config);
await args.sendText(`📋 当前会话模型已切换为:${modelId}`);
}
return true;
}

case "effort": {
const [result, session] = await Promise.all([
args.gatewayClient.listModels(ctx.engineType),
args.gatewayClient.getSession(ctx.conversationId),
]);
const currentModelId = session.modelId ?? result.currentModelId;
const supportedEfforts = getModelEfforts(result.models, currentModelId);
if (supportedEfforts.length === 0) {
await args.sendText("📋 当前模型不支持推理级别设置。");
return true;
}

const currentEffort = session.reasoningEffort && supportedEfforts.includes(session.reasoningEffort)
? session.reasoningEffort
: getModelDefaultEffort(result.models, currentModelId);
const isList =
subcommand === "list" ||
(!subcommand && (!command.args || command.args.length === 0));

if (isList) {
const lines = ["**📋 推理级别**", ""];
if (currentModelId) lines.push(`当前模型:\`${escapeMarkdownInline(currentModelId)}\``);
for (const effort of supportedEfforts) {
const current = effort === currentEffort ? "(当前会话)" : "";
lines.push(`- \`${effort}\` · ${effortLabels[effort]}${current}`);
}
lines.push("");
lines.push("使用 `/effort low|medium|high|max` 切换当前会话推理级别。");
await args.sendText(lines.join("\n"));
return true;
}

const nextEffort = command.args?.[0];
if (!isReasoningEffort(nextEffort) || !supportedEfforts.includes(nextEffort)) {
await args.sendText(`📋 当前模型支持的推理级别:${supportedEfforts.map((effort) => `\`${effort}\``).join("、")}`);
return true;
}

await args.gatewayClient.updateSessionConfig(ctx.conversationId, { reasoningEffort: nextEffort });
await args.sendText(`📋 当前会话推理级别已切换为:${effortLabels[nextEffort]}(${nextEffort})`);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ export class WeixinIlinkAdapter extends ChannelAdapter {
): Promise<void> {
if (!command || !this.transport) return;

// Try shared Class-B session-ops first (cancel/status/mode/model/history)
// Try shared Class-B session-ops first (cancel/status/mode/model/effort/history)
if (this.gatewayClient) {
const handled = await handleSessionOpsCommand(command, {
sendText: (text) => this.transport!.sendMarkdown(chatId, text),
Expand Down
Loading
Loading