@keyou007/agent-bot 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +22 -10
- package/README.zh.md +22 -10
- package/assets/agent-bot-logo.png +0 -0
- package/dist/acp/AcpSessionManager.js +78 -46
- package/dist/acp/AcpSessionManager.js.map +1 -1
- package/dist/cli/FeishuAppConfiguration.js +84 -22
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/FeishuAppRegistration.js +12 -9
- package/dist/cli/FeishuAppRegistration.js.map +1 -1
- package/dist/cli/Initializer.js +7 -6
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/LocalControlClient.js +3 -2
- package/dist/cli/LocalControlClient.js.map +1 -1
- package/dist/cli/LocalControlServer.js +2 -1
- package/dist/cli/LocalControlServer.js.map +1 -1
- package/dist/cli/OptionalAuthorizationSkip.js +27 -6
- package/dist/cli/OptionalAuthorizationSkip.js.map +1 -1
- package/dist/cli/ServerStarter.js +12 -3
- package/dist/cli/ServerStarter.js.map +1 -1
- package/dist/cli/SkillRegistry.js +5 -4
- package/dist/cli/SkillRegistry.js.map +1 -1
- package/dist/cli/VerificationOutput.js +2 -1
- package/dist/cli/VerificationOutput.js.map +1 -1
- package/dist/cli/help.js +48 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/i18n.js +27 -0
- package/dist/cli/i18n.js.map +1 -0
- package/dist/cli/initOptions.js +5 -4
- package/dist/cli/initOptions.js.map +1 -1
- package/dist/cli/packageVersion.js +2 -1
- package/dist/cli/packageVersion.js.map +1 -1
- package/dist/cli/profile.js +4 -3
- package/dist/cli/profile.js.map +1 -1
- package/dist/cli/serverStatus.js +15 -14
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli/taskChatRoute.js +3 -2
- package/dist/cli/taskChatRoute.js.map +1 -1
- package/dist/cli.js +165 -107
- package/dist/cli.js.map +1 -1
- package/dist/codex/AppServerConnection.js +15 -1
- package/dist/codex/AppServerConnection.js.map +1 -1
- package/dist/codex/CodexLocalActivityDetector.js +4 -1
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +1 -0
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +77 -2
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +108 -47
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +1 -1
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +345 -11
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +51 -5
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +8 -3
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +18 -17
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +595 -205
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/runtime/AcpRuntimeAdapter.js +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/runtime/AgentRuntimeRegistry.js +7 -4
- package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
- package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
- package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
- package/dist/startup/SessionMetadataHydrator.js +2 -2
- package/dist/startup/SessionMetadataHydrator.js.map +1 -1
- package/dist/startup/StartupNotifier.js +1 -0
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/state/StateStore.js +21 -15
- package/dist/state/StateStore.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +5 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/docs/technical-reference.md +27 -6
- package/docs/technical-reference.zh.md +27 -6
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/skills/agent-bot/SKILL.md +19 -9
- package/src/acp/AcpSessionManager.ts +73 -54
- package/src/cli/FeishuAppConfiguration.ts +130 -18
- package/src/cli/FeishuAppRegistration.ts +24 -9
- package/src/cli/Initializer.ts +25 -6
- package/src/cli/LocalControlClient.ts +9 -2
- package/src/cli/LocalControlServer.ts +2 -1
- package/src/cli/OptionalAuthorizationSkip.ts +51 -6
- package/src/cli/ServerStarter.ts +20 -3
- package/src/cli/SkillRegistry.ts +17 -4
- package/src/cli/VerificationOutput.ts +4 -1
- package/src/cli/help.ts +50 -1
- package/src/cli/i18n.ts +38 -0
- package/src/cli/initOptions.ts +17 -4
- package/src/cli/packageVersion.ts +5 -1
- package/src/cli/profile.ts +13 -3
- package/src/cli/serverStatus.ts +19 -14
- package/src/cli/taskChatRoute.ts +9 -2
- package/src/cli.ts +290 -103
- package/src/codex/AppServerConnection.ts +21 -1
- package/src/codex/CodexLocalActivityDetector.ts +8 -1
- package/src/codex/CodexProcessManager.ts +1 -0
- package/src/codex/CodexRuntime.ts +91 -2
- package/src/commands/CommandRouter.ts +123 -46
- package/src/commands/commandTypes.ts +6 -11
- package/src/console/ConsoleTurnPresenter.ts +1 -1
- package/src/feishu/CardRenderer.ts +418 -11
- package/src/feishu/FeishuMessageClient.ts +66 -3
- package/src/feishu/FeishuTurnPresenter.ts +7 -2
- package/src/index.ts +21 -22
- package/src/proxy/ProxySessionController.ts +712 -208
- package/src/runtime/AcpRuntimeAdapter.ts +1 -0
- package/src/runtime/AgentRuntimeRegistry.ts +7 -6
- package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
- package/src/runtime/types.ts +18 -0
- package/src/startup/SessionMetadataHydrator.ts +2 -2
- package/src/startup/StartupNotifier.ts +1 -0
- package/src/state/StateStore.ts +29 -15
- package/src/supervision/SafeRestartScheduler.ts +5 -0
|
@@ -26,6 +26,21 @@ interface PendingRequest {
|
|
|
26
26
|
|
|
27
27
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
28
28
|
|
|
29
|
+
export class AppServerRequestError extends Error {
|
|
30
|
+
readonly data: unknown;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
readonly method: string,
|
|
34
|
+
readonly code: number,
|
|
35
|
+
readonly serverMessage: string,
|
|
36
|
+
data?: unknown,
|
|
37
|
+
) {
|
|
38
|
+
super(`${method} failed: ${serverMessage}`);
|
|
39
|
+
this.name = "AppServerRequestError";
|
|
40
|
+
this.data = data;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
export class AppServerConnection {
|
|
30
45
|
private nextId = 1;
|
|
31
46
|
private closed = false;
|
|
@@ -131,7 +146,12 @@ export class AppServerConnection {
|
|
|
131
146
|
this.pending.delete(message.id);
|
|
132
147
|
if (pending.timeout) clearTimeout(pending.timeout);
|
|
133
148
|
if ("error" in message) {
|
|
134
|
-
pending.reject(new
|
|
149
|
+
pending.reject(new AppServerRequestError(
|
|
150
|
+
pending.method,
|
|
151
|
+
message.error.code,
|
|
152
|
+
message.error.message,
|
|
153
|
+
message.error.data,
|
|
154
|
+
));
|
|
135
155
|
} else {
|
|
136
156
|
pending.resolve(message.result);
|
|
137
157
|
}
|
|
@@ -16,12 +16,14 @@ interface ThreadPathRow {
|
|
|
16
16
|
|
|
17
17
|
interface ThreadSettingsRow {
|
|
18
18
|
id: string;
|
|
19
|
+
model_provider?: string | null;
|
|
19
20
|
model: string | null;
|
|
20
21
|
reasoning_effort: string | null;
|
|
21
22
|
approval_mode: string | null;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface CodexLocalThreadSettings {
|
|
26
|
+
modelProvider?: string;
|
|
25
27
|
model?: string;
|
|
26
28
|
reasoningEffort?: string;
|
|
27
29
|
permissionMode?: PermissionMode;
|
|
@@ -79,10 +81,15 @@ export class CodexLocalActivityDetector {
|
|
|
79
81
|
fileMustExist: true,
|
|
80
82
|
});
|
|
81
83
|
const placeholders = ids.map(() => "?").join(", ");
|
|
84
|
+
const columns = new Set(
|
|
85
|
+
(database.pragma("table_info(threads)") as Array<{ name: string }>).map((column) => column.name),
|
|
86
|
+
);
|
|
87
|
+
const providerColumn = columns.has("model_provider") ? ", model_provider" : "";
|
|
82
88
|
const rows = database.prepare(
|
|
83
|
-
`SELECT id, model, reasoning_effort, approval_mode FROM threads WHERE id IN (${placeholders})`,
|
|
89
|
+
`SELECT id${providerColumn}, model, reasoning_effort, approval_mode FROM threads WHERE id IN (${placeholders})`,
|
|
84
90
|
).all(...ids) as ThreadSettingsRow[];
|
|
85
91
|
return new Map(rows.map((row) => [row.id, {
|
|
92
|
+
...(row.model_provider?.trim() ? { modelProvider: row.model_provider.trim() } : {}),
|
|
86
93
|
...(row.model?.trim() ? { model: row.model.trim() } : {}),
|
|
87
94
|
...(row.reasoning_effort?.trim() ? { reasoningEffort: row.reasoning_effort.trim() } : {}),
|
|
88
95
|
...(row.approval_mode?.trim()
|
|
@@ -40,6 +40,7 @@ export class CodexProcessManager implements AppServerClientProvider {
|
|
|
40
40
|
});
|
|
41
41
|
await client.request("initialize", {
|
|
42
42
|
clientInfo: { name: "agent-bot", title: "Agent Bot", version: "0.1.0" },
|
|
43
|
+
capabilities: { experimentalApi: true },
|
|
43
44
|
});
|
|
44
45
|
client.notify("initialized", {});
|
|
45
46
|
return client;
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
CreateRuntimeSessionInput,
|
|
6
6
|
ForkRuntimeSessionInput,
|
|
7
7
|
ModelOption,
|
|
8
|
+
ModelProviderOption,
|
|
8
9
|
PermissionMode,
|
|
9
10
|
RemoteSessionActivity,
|
|
10
11
|
RemoteSessionPage,
|
|
@@ -13,12 +14,14 @@ import type {
|
|
|
13
14
|
RuntimeGoal,
|
|
14
15
|
RuntimeGoalUpdate,
|
|
15
16
|
RuntimeEvent,
|
|
17
|
+
RuntimeExecutionSettings,
|
|
16
18
|
RuntimePrompt,
|
|
17
19
|
RuntimeSession,
|
|
18
20
|
RuntimeSessionMetadata,
|
|
19
21
|
} from "../runtime/types.js";
|
|
20
22
|
import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
|
|
21
23
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
24
|
+
import { AppServerRequestError } from "./AppServerConnection.js";
|
|
22
25
|
import { mapCodexNotification } from "./CodexEventMapper.js";
|
|
23
26
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
24
27
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
@@ -100,6 +103,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
100
103
|
const response = await client.request<ThreadResponse>("thread/start", {
|
|
101
104
|
cwd: input.cwd,
|
|
102
105
|
model: input.model,
|
|
106
|
+
...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
|
|
103
107
|
threadSource: "user",
|
|
104
108
|
...threadLifecycleParams(input.cwd),
|
|
105
109
|
...permissionParams(input.permissionMode),
|
|
@@ -122,8 +126,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
122
126
|
const client = await this.client();
|
|
123
127
|
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
124
128
|
threadId: input.remoteSessionId,
|
|
129
|
+
excludeTurns: true,
|
|
125
130
|
cwd: input.cwd,
|
|
126
131
|
model: input.model,
|
|
132
|
+
...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
|
|
127
133
|
...threadLifecycleParams(input.cwd),
|
|
128
134
|
...permissionParams(input.permissionMode),
|
|
129
135
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
@@ -135,15 +141,30 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
135
141
|
|
|
136
142
|
async forkSession(input: ForkRuntimeSessionInput): Promise<RuntimeSession> {
|
|
137
143
|
const client = await this.client();
|
|
138
|
-
const
|
|
144
|
+
const forkParams = {
|
|
139
145
|
threadId: input.remoteSessionId,
|
|
140
146
|
lastTurnId: input.lastTurnId,
|
|
141
147
|
cwd: input.cwd,
|
|
142
148
|
model: input.model,
|
|
149
|
+
...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
|
|
143
150
|
threadSource: "user",
|
|
144
151
|
...threadLifecycleParams(input.cwd),
|
|
145
152
|
...permissionParams(input.permissionMode),
|
|
146
|
-
}
|
|
153
|
+
};
|
|
154
|
+
let response: ThreadResponse;
|
|
155
|
+
try {
|
|
156
|
+
response = await client.request<ThreadResponse>("thread/fork", {
|
|
157
|
+
...forkParams,
|
|
158
|
+
excludeTurns: true,
|
|
159
|
+
}, FORK_REQUEST_TIMEOUT_MS);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!isUnsupportedExcludeTurnsError(error)) throw error;
|
|
162
|
+
this.logger.warn(
|
|
163
|
+
{ error },
|
|
164
|
+
"Codex App Server does not support thread/fork excludeTurns; retrying without it.",
|
|
165
|
+
);
|
|
166
|
+
response = await client.request<ThreadResponse>("thread/fork", forkParams, FORK_REQUEST_TIMEOUT_MS);
|
|
167
|
+
}
|
|
147
168
|
const requestedTitle = normalizeTaskTitle(input.title);
|
|
148
169
|
if (requestedTitle) {
|
|
149
170
|
await client.request("thread/name/set", {
|
|
@@ -375,6 +396,28 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
375
396
|
this.requireSession(sessionId).permissionMode = mode;
|
|
376
397
|
}
|
|
377
398
|
|
|
399
|
+
async setExecutionSettings(
|
|
400
|
+
sessionId: string,
|
|
401
|
+
settings: RuntimeExecutionSettings,
|
|
402
|
+
): Promise<RuntimeSession> {
|
|
403
|
+
const session = this.requireSession(sessionId);
|
|
404
|
+
const response = await (await this.client()).request<ThreadResponse>("thread/resume", {
|
|
405
|
+
threadId: session.remoteSessionId,
|
|
406
|
+
excludeTurns: true,
|
|
407
|
+
cwd: session.cwd,
|
|
408
|
+
modelProvider: settings.modelProvider,
|
|
409
|
+
model: settings.model,
|
|
410
|
+
...threadLifecycleParams(session.cwd),
|
|
411
|
+
...permissionParams(settings.permissionMode),
|
|
412
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
413
|
+
session.modelProvider = response.modelProvider ?? settings.modelProvider;
|
|
414
|
+
session.model = response.model ?? settings.model;
|
|
415
|
+
session.reasoningEffort = settings.reasoningEffort;
|
|
416
|
+
session.permissionMode = settings.permissionMode;
|
|
417
|
+
session.needsResume = false;
|
|
418
|
+
return session;
|
|
419
|
+
}
|
|
420
|
+
|
|
378
421
|
async respondToApproval(
|
|
379
422
|
sessionId: string,
|
|
380
423
|
requestId: string,
|
|
@@ -412,6 +455,33 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
412
455
|
}));
|
|
413
456
|
}
|
|
414
457
|
|
|
458
|
+
async listModelProviders(): Promise<ModelProviderOption[]> {
|
|
459
|
+
const response = await (await this.client()).request<{ config?: Record<string, unknown> }>(
|
|
460
|
+
"config/read",
|
|
461
|
+
{},
|
|
462
|
+
CONTROL_REQUEST_TIMEOUT_MS,
|
|
463
|
+
);
|
|
464
|
+
const config = response.config ?? {};
|
|
465
|
+
const defaultProvider = stringValue(config.model_provider)?.trim();
|
|
466
|
+
const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
|
|
467
|
+
const providers = new Map<string, ModelProviderOption>();
|
|
468
|
+
for (const [id, value] of Object.entries(configuredProviders)) {
|
|
469
|
+
if (!id.trim()) continue;
|
|
470
|
+
const displayName = isRecord(value) ? stringValue(value.name)?.trim() : undefined;
|
|
471
|
+
providers.set(id, {
|
|
472
|
+
id,
|
|
473
|
+
...(displayName ? { displayName } : {}),
|
|
474
|
+
...(id === defaultProvider ? { isDefault: true } : {}),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (defaultProvider && !providers.has(defaultProvider)) {
|
|
478
|
+
providers.set(defaultProvider, { id: defaultProvider, isDefault: true });
|
|
479
|
+
}
|
|
480
|
+
return [...providers.values()].sort((left, right) =>
|
|
481
|
+
Number(Boolean(right.isDefault)) - Number(Boolean(left.isDefault)) || left.id.localeCompare(right.id),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
415
485
|
onEvent(listener: (event: RuntimeEvent) => void): () => void {
|
|
416
486
|
this.listeners.add(listener);
|
|
417
487
|
return () => this.listeners.delete(listener);
|
|
@@ -642,8 +712,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
642
712
|
if (!session.needsResume) return;
|
|
643
713
|
await client.request("thread/resume", {
|
|
644
714
|
threadId: session.remoteSessionId,
|
|
715
|
+
excludeTurns: true,
|
|
645
716
|
cwd: session.cwd,
|
|
646
717
|
model: session.model,
|
|
718
|
+
...(session.modelProvider ? { modelProvider: session.modelProvider } : {}),
|
|
647
719
|
...threadLifecycleParams(session.cwd),
|
|
648
720
|
...permissionParams(session.permissionMode),
|
|
649
721
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
@@ -772,6 +844,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
772
844
|
title: normalizeTaskTitle(response.thread.name)
|
|
773
845
|
?? normalizeTaskTitle(response.thread.preview)
|
|
774
846
|
?? input.title,
|
|
847
|
+
modelProvider: input.modelProvider ?? response.modelProvider,
|
|
775
848
|
model: input.model ?? response.model,
|
|
776
849
|
reasoningEffort,
|
|
777
850
|
permissionMode: input.permissionMode,
|
|
@@ -835,6 +908,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
835
908
|
|
|
836
909
|
interface ThreadResponse {
|
|
837
910
|
thread: { id: string; name?: string | null; preview?: string };
|
|
911
|
+
modelProvider?: string;
|
|
838
912
|
model?: string;
|
|
839
913
|
reasoningEffort?: string | null;
|
|
840
914
|
}
|
|
@@ -853,6 +927,7 @@ interface CodexThreadSnapshot {
|
|
|
853
927
|
name?: string | null;
|
|
854
928
|
preview?: string;
|
|
855
929
|
cwd?: string;
|
|
930
|
+
modelProvider?: string;
|
|
856
931
|
source?: unknown;
|
|
857
932
|
createdAt?: number;
|
|
858
933
|
updatedAt?: number;
|
|
@@ -933,6 +1008,19 @@ function uniqueStrings(values: string[]): string[] {
|
|
|
933
1008
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
934
1009
|
}
|
|
935
1010
|
|
|
1011
|
+
function isUnsupportedExcludeTurnsError(error: unknown): boolean {
|
|
1012
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/fork") return false;
|
|
1013
|
+
const data = typeof error.data === "string"
|
|
1014
|
+
? error.data
|
|
1015
|
+
: error.data === undefined
|
|
1016
|
+
? ""
|
|
1017
|
+
: JSON.stringify(error.data);
|
|
1018
|
+
const details = `${error.serverMessage} ${data}`;
|
|
1019
|
+
if (!details.toLowerCase().replaceAll(/[^a-z]/g, "").includes("excludeturns")) return false;
|
|
1020
|
+
return error.code === -32602
|
|
1021
|
+
|| /unknown|unrecognized|unexpected|unsupported|not supported|experimental/i.test(details);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
936
1024
|
function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary {
|
|
937
1025
|
const lastTurn = thread.turns?.at(-1);
|
|
938
1026
|
const lastCompletedTurn = [...(thread.turns ?? [])]
|
|
@@ -956,6 +1044,7 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
|
|
|
956
1044
|
cwd: thread.cwd ?? "",
|
|
957
1045
|
source: codexSourceLabel(thread.source),
|
|
958
1046
|
status,
|
|
1047
|
+
modelProvider: thread.modelProvider,
|
|
959
1048
|
createdAt: thread.createdAt,
|
|
960
1049
|
updatedAt: thread.updatedAt,
|
|
961
1050
|
recencyAt: thread.recencyAt ?? undefined,
|
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import type { Command } from "./commandTypes.js";
|
|
2
2
|
|
|
3
|
+
const COMMAND_NAMES = [
|
|
4
|
+
"agent",
|
|
5
|
+
"fork",
|
|
6
|
+
"forkgroup",
|
|
7
|
+
"goal",
|
|
8
|
+
"help",
|
|
9
|
+
"model",
|
|
10
|
+
"new",
|
|
11
|
+
"newgroup",
|
|
12
|
+
"nosteer",
|
|
13
|
+
"permissions",
|
|
14
|
+
"provider",
|
|
15
|
+
"queue",
|
|
16
|
+
"restart",
|
|
17
|
+
"sessions",
|
|
18
|
+
"status",
|
|
19
|
+
"stop",
|
|
20
|
+
"switch",
|
|
21
|
+
"thinking",
|
|
22
|
+
"title",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
type CommandName = (typeof COMMAND_NAMES)[number];
|
|
26
|
+
|
|
27
|
+
const COMMAND_INITIALISMS: Partial<Record<CommandName, string>> = {
|
|
28
|
+
forkgroup: "fg",
|
|
29
|
+
newgroup: "ng",
|
|
30
|
+
nosteer: "ns",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const DISABLED_COMMAND_NAMES = new Set(["mode", "modes"]);
|
|
34
|
+
|
|
3
35
|
export class CommandRouter {
|
|
4
36
|
parse(text: string): Command {
|
|
5
37
|
const trimmed = text.trim();
|
|
@@ -13,28 +45,20 @@ export class CommandRouter {
|
|
|
13
45
|
}
|
|
14
46
|
|
|
15
47
|
const [rawCommand, ...args] = splitArgs(trimmed);
|
|
16
|
-
const command = rawCommand
|
|
48
|
+
const command = resolveCommandName(rawCommand);
|
|
17
49
|
|
|
18
50
|
switch (command) {
|
|
19
|
-
case "agents":
|
|
20
|
-
return { type: "agent" };
|
|
21
51
|
case "new":
|
|
22
52
|
return parseNewCommand(args);
|
|
23
53
|
case "newgroup":
|
|
24
54
|
return parseNewGroupCommand(args);
|
|
25
55
|
case "forkgroup":
|
|
26
|
-
return
|
|
27
|
-
type: "forkgroup",
|
|
28
|
-
title: args.join(" ").trim() || undefined,
|
|
29
|
-
};
|
|
56
|
+
return parseForkGroupCommand(args);
|
|
30
57
|
case "fork":
|
|
31
|
-
|
|
32
|
-
return args[0] ? { type: "fork", sessionId: args[0] } : { type: "fork" };
|
|
58
|
+
return parseForkCommand(args);
|
|
33
59
|
case "title":
|
|
34
60
|
if (args.length === 0) throw new Error("请输入新标题,例如:/title 修复会话列表。");
|
|
35
61
|
return { type: "title", title: args.join(" ") };
|
|
36
|
-
case "ask":
|
|
37
|
-
return { type: "ask", text: trimmed.slice(rawCommand.length).trim() };
|
|
38
62
|
case "queue":
|
|
39
63
|
case "nosteer": {
|
|
40
64
|
const prompt = trimmed.slice(rawCommand.length).trim();
|
|
@@ -47,8 +71,6 @@ export class CommandRouter {
|
|
|
47
71
|
return args[0] ? { type: "switch", sessionId: args[0] } : { type: "switch" };
|
|
48
72
|
case "agent":
|
|
49
73
|
return args[0] ? { type: "agent", agent: args[0] } : { type: "agent" };
|
|
50
|
-
case "use":
|
|
51
|
-
return requireArg(args[0], "agent name", (agent) => ({ type: "use", agent, cwd: args[1] }));
|
|
52
74
|
case "stop":
|
|
53
75
|
return { type: "stop" };
|
|
54
76
|
case "status":
|
|
@@ -68,47 +90,88 @@ export class CommandRouter {
|
|
|
68
90
|
return { type: "goal", action: "set", objective: args.join(" ") };
|
|
69
91
|
}
|
|
70
92
|
case "restart":
|
|
71
|
-
return { type: "restart" };
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
case "mode":
|
|
75
|
-
return requireArg(args[0], "mode value", (value) => ({ type: "mode", value }));
|
|
93
|
+
if (args.length === 0) return { type: "restart" };
|
|
94
|
+
if (args.length === 1 && args[0] === "--force") return { type: "restart", force: true };
|
|
95
|
+
throw new Error("/restart 只接受一个可选的 --force 参数。");
|
|
76
96
|
case "model":
|
|
77
|
-
return { type: "model"
|
|
97
|
+
return settingsCommand("/model", args, { type: "model" });
|
|
98
|
+
case "provider":
|
|
99
|
+
return settingsCommand("/provider", args, { type: "provider" });
|
|
78
100
|
case "thinking":
|
|
79
|
-
return { type: "thinking"
|
|
80
|
-
case "permissions":
|
|
81
|
-
|
|
82
|
-
if (mode !== undefined && mode !== "auto" && mode !== "confirm") {
|
|
83
|
-
throw new Error("权限模式只能是 auto 或 confirm。");
|
|
84
|
-
}
|
|
85
|
-
return { type: "permissions", mode };
|
|
86
|
-
}
|
|
101
|
+
return settingsCommand("/thinking", args, { type: "thinking" });
|
|
102
|
+
case "permissions":
|
|
103
|
+
return settingsCommand("/permissions", args, { type: "permissions" });
|
|
87
104
|
case "help":
|
|
88
105
|
return { type: "help" };
|
|
89
|
-
default:
|
|
90
|
-
throw new Error(`未知命令:${rawCommand}。发送 /help 查看可用命令。`);
|
|
91
106
|
}
|
|
92
107
|
}
|
|
93
108
|
}
|
|
94
109
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
function resolveCommandName(rawCommand: string): CommandName {
|
|
111
|
+
const input = rawCommand.slice(1).toLowerCase();
|
|
112
|
+
if (!input) throw unknownCommand(rawCommand);
|
|
113
|
+
if (DISABLED_COMMAND_NAMES.has(input)) throw unknownCommand(rawCommand);
|
|
114
|
+
|
|
115
|
+
const exact = COMMAND_NAMES.find((command) => command === input);
|
|
116
|
+
if (exact) return exact;
|
|
117
|
+
|
|
118
|
+
const matches = COMMAND_NAMES.filter(
|
|
119
|
+
(command) => command.startsWith(input) || COMMAND_INITIALISMS[command] === input,
|
|
120
|
+
);
|
|
121
|
+
if (matches.length === 1) return matches[0]!;
|
|
122
|
+
if (matches.length > 1) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`命令前缀 ${rawCommand} 不唯一,可匹配:${matches.map((command) => `/${command}`).join("、")}。请输入更长的命令前缀。`,
|
|
125
|
+
);
|
|
98
126
|
}
|
|
127
|
+
throw unknownCommand(rawCommand);
|
|
128
|
+
}
|
|
99
129
|
|
|
100
|
-
|
|
130
|
+
function unknownCommand(rawCommand: string): Error {
|
|
131
|
+
return new Error(`未知命令:${rawCommand}。发送 /help 查看可用命令。`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function settingsCommand<T extends Command>(name: string, args: string[], command: T): T {
|
|
135
|
+
if (args.length > 0) {
|
|
136
|
+
throw new Error(`${name} 不接受参数,请在设置卡片中完成选择。`);
|
|
137
|
+
}
|
|
138
|
+
return command;
|
|
101
139
|
}
|
|
102
140
|
|
|
103
141
|
function parseNewCommand(args: string[]): Extract<Command, { type: "new" }> {
|
|
142
|
+
const options = parseNewTaskOptions("/new", args, "工作目录", "D:\\dev\\project");
|
|
143
|
+
return {
|
|
144
|
+
type: "new",
|
|
145
|
+
title: options.title,
|
|
146
|
+
cwd: options.cwd,
|
|
147
|
+
...(options.projectless ? { projectless: true } : {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseNewGroupCommand(args: string[]): Extract<Command, { type: "newgroup" }> {
|
|
152
|
+
const options = parseNewTaskOptions("/newgroup", args, "项目目录", "~/dev/project");
|
|
153
|
+
return {
|
|
154
|
+
type: "newgroup",
|
|
155
|
+
title: options.title,
|
|
156
|
+
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
157
|
+
...(options.projectless ? { projectless: true } : {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseNewTaskOptions(
|
|
162
|
+
commandName: "/new" | "/newgroup",
|
|
163
|
+
args: string[],
|
|
164
|
+
directoryLabel: string,
|
|
165
|
+
exampleDirectory: string,
|
|
166
|
+
): { title?: string; cwd?: string; projectless: boolean } {
|
|
104
167
|
const titleParts: string[] = [];
|
|
105
168
|
let cwd: string | undefined;
|
|
106
169
|
let projectless = false;
|
|
107
170
|
for (let index = 0; index < args.length; index += 1) {
|
|
108
171
|
const argument = args[index]!;
|
|
109
172
|
if (argument === "--nodir") {
|
|
110
|
-
if (projectless) throw new Error(
|
|
111
|
-
if (cwd !== undefined) throw new Error(
|
|
173
|
+
if (projectless) throw new Error(`${commandName} 只能指定一次 --nodir。`);
|
|
174
|
+
if (cwd !== undefined) throw new Error(`${commandName} 的 --dir 和 --nodir 不能同时使用。`);
|
|
112
175
|
projectless = true;
|
|
113
176
|
continue;
|
|
114
177
|
}
|
|
@@ -116,33 +179,47 @@ function parseNewCommand(args: string[]): Extract<Command, { type: "new" }> {
|
|
|
116
179
|
titleParts.push(argument);
|
|
117
180
|
continue;
|
|
118
181
|
}
|
|
119
|
-
if (projectless) throw new Error(
|
|
120
|
-
if (cwd !== undefined) throw new Error(
|
|
182
|
+
if (projectless) throw new Error(`${commandName} 的 --dir 和 --nodir 不能同时使用。`);
|
|
183
|
+
if (cwd !== undefined) throw new Error(`${commandName} 只能指定一次 --dir。`);
|
|
121
184
|
const directory = args[index + 1];
|
|
122
|
-
if (!directory || directory === "--dir") {
|
|
123
|
-
throw new Error(
|
|
185
|
+
if (!directory || directory === "--dir" || directory === "--nodir") {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`请在 --dir 后指定${directoryLabel},例如:${commandName} 修复会话列表 --dir ${exampleDirectory}。`,
|
|
188
|
+
);
|
|
124
189
|
}
|
|
125
190
|
cwd = directory;
|
|
126
191
|
index += 1;
|
|
127
192
|
}
|
|
128
193
|
return {
|
|
129
|
-
type: "new",
|
|
130
194
|
title: titleParts.join(" ").trim() || undefined,
|
|
131
195
|
cwd,
|
|
132
|
-
|
|
196
|
+
projectless,
|
|
133
197
|
};
|
|
134
198
|
}
|
|
135
199
|
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
200
|
+
function parseForkCommand(args: string[]): Extract<Command, { type: "fork" }> {
|
|
201
|
+
rejectOptions("/fork", args);
|
|
202
|
+
if (args.length > 1) throw new Error("/fork 只接受一个可选的任务序号或任务 ID。");
|
|
140
203
|
return {
|
|
141
|
-
type: "
|
|
204
|
+
type: "fork",
|
|
205
|
+
...(args[0] ? { sessionId: args[0] } : {}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseForkGroupCommand(args: string[]): Extract<Command, { type: "forkgroup" }> {
|
|
210
|
+
rejectOptions("/forkgroup", args);
|
|
211
|
+
return {
|
|
212
|
+
type: "forkgroup",
|
|
142
213
|
title: args.join(" ").trim() || undefined,
|
|
143
214
|
};
|
|
144
215
|
}
|
|
145
216
|
|
|
217
|
+
function rejectOptions(command: string, args: string[]): void {
|
|
218
|
+
for (const argument of args) {
|
|
219
|
+
if (argument.startsWith("--")) throw new Error(`${command} 不支持参数:${argument}。`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
146
223
|
function splitArgs(input: string): string[] {
|
|
147
224
|
const args: string[] = [];
|
|
148
225
|
let current = "";
|
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
import type { PermissionMode } from "../runtime/types.js";
|
|
2
|
-
|
|
3
1
|
export type Command =
|
|
4
2
|
| { type: "shell"; command: string }
|
|
5
3
|
| { type: "new"; title?: string; cwd?: string; projectless?: boolean }
|
|
6
|
-
| { type: "newgroup"; title?: string }
|
|
4
|
+
| { type: "newgroup"; title?: string; cwd?: string; projectless?: boolean }
|
|
7
5
|
| { type: "forkgroup"; title?: string }
|
|
8
6
|
| { type: "fork"; sessionId?: string }
|
|
9
7
|
| { type: "title"; title: string }
|
|
10
|
-
| { type: "ask"; text: string }
|
|
11
8
|
| { type: "nosteer"; text: string }
|
|
12
9
|
| { type: "sessions"; searchTerm?: string }
|
|
13
10
|
| { type: "switch"; sessionId?: string }
|
|
14
11
|
| { type: "agent"; agent?: string }
|
|
15
|
-
| { type: "use"; agent: string; cwd?: string }
|
|
16
12
|
| { type: "stop" }
|
|
17
13
|
| { type: "status"; sessionId?: string }
|
|
18
14
|
| { type: "goal"; action: "show" }
|
|
19
15
|
| { type: "goal"; action: "set" | "edit"; objective: string }
|
|
20
16
|
| { type: "goal"; action: "pause" | "resume" | "clear" }
|
|
21
|
-
| { type: "restart" }
|
|
22
|
-
| { type: "
|
|
23
|
-
| { type: "
|
|
24
|
-
| { type: "
|
|
25
|
-
| { type: "
|
|
26
|
-
| { type: "permissions"; mode?: PermissionMode }
|
|
17
|
+
| { type: "restart"; force?: boolean }
|
|
18
|
+
| { type: "model" }
|
|
19
|
+
| { type: "provider" }
|
|
20
|
+
| { type: "thinking" }
|
|
21
|
+
| { type: "permissions" }
|
|
27
22
|
| { type: "help" }
|
|
28
23
|
| { type: "prompt"; text: string };
|
|
@@ -39,7 +39,7 @@ export class ConsoleTurnPresenter implements TurnPresenter {
|
|
|
39
39
|
} else if (event.type === "tool_started") {
|
|
40
40
|
await this.outbound.sendText(contextKey, `> ${event.tool.title}`);
|
|
41
41
|
} else if (event.type === "approval_requested") {
|
|
42
|
-
await this.outbound.sendText(contextKey, `Approval required: ${event.request.title}. Use Feishu
|
|
42
|
+
await this.outbound.sendText(contextKey, `Approval required: ${event.request.title}. Use Feishu's Permission settings card.`);
|
|
43
43
|
} else if (event.type === "turn_completed" && !this.delivered.has(event.turnId)) {
|
|
44
44
|
this.delivered.add(event.turnId);
|
|
45
45
|
await this.outbound.sendMarkdown(contextKey, event.finalResponse || "(completed without text)");
|