@keyou007/agent-bot 0.1.20 → 0.1.21
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 +10 -0
- package/README.md +15 -5
- package/README.zh.md +15 -5
- package/dist/codex/CodexEventMapper.js +21 -1
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +3 -0
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +136 -4
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderModels.js +159 -0
- package/dist/codex/ProviderModels.js.map +1 -0
- package/dist/commands/CommandRouter.js +8 -3
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +2 -1
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +46 -16
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +34 -8
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +151 -16
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +725 -0
- package/dist/local-files/TurnPreviewPage.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +6 -1
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +142 -3
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +186 -80
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +7 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/utils/formatStorageSize.js +15 -0
- package/dist/utils/formatStorageSize.js.map +1 -0
- package/docs/technical-reference.md +7 -1
- package/docs/technical-reference.zh.md +7 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +5 -1
- package/src/codex/CodexEventMapper.ts +31 -1
- package/src/codex/CodexProcessManager.ts +4 -0
- package/src/codex/CodexRuntime.ts +153 -9
- package/src/codex/ProviderModels.ts +185 -0
- package/src/commands/CommandRouter.ts +7 -2
- package/src/commands/commandTypes.ts +1 -1
- package/src/console/ConsoleTurnPresenter.ts +3 -0
- package/src/feishu/CardRenderer.ts +47 -16
- package/src/feishu/FeishuTurnPresenter.ts +46 -4
- package/src/index.ts +2 -0
- package/src/local-files/LocalFileViewerServer.ts +166 -17
- package/src/local-files/TurnPreviewPage.ts +792 -0
- package/src/presentation/OutboundRouter.ts +8 -0
- package/src/presentation/TurnStateReducer.ts +175 -2
- package/src/presentation/turnViewTypes.ts +13 -0
- package/src/proxy/ProxySessionController.ts +186 -67
- package/src/runtime/types.ts +18 -1
- package/src/state/StateStore.ts +9 -0
- package/src/utils/formatStorageSize.ts +13 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
1
2
|
import type { Logger } from "pino";
|
|
2
3
|
import type {
|
|
3
4
|
AgentRuntime,
|
|
@@ -9,6 +10,7 @@ import type {
|
|
|
9
10
|
ModelProviderOption,
|
|
10
11
|
PermissionMode,
|
|
11
12
|
RemoteSessionActivity,
|
|
13
|
+
RemoteSessionMetrics,
|
|
12
14
|
RemoteSessionPage,
|
|
13
15
|
RemoteSessionSummary,
|
|
14
16
|
RemoteTurnPage,
|
|
@@ -30,6 +32,7 @@ import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
|
30
32
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
31
33
|
import { threadWriterLockPath } from "./ThreadWriterProcess.js";
|
|
32
34
|
import { assertProviderSettingsApplied, PROVIDER_SWITCH_BUSY, providerSwitchFailure } from "./ProviderSwitch.js";
|
|
35
|
+
import { fetchProviderModels, type ProviderModelConfig } from "./ProviderModels.js";
|
|
33
36
|
|
|
34
37
|
const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
35
38
|
"When capturing any screenshot on Windows, use one fresh process and make it Per-Monitor DPI Aware V2 before loading System.Windows.Forms, System.Drawing, or UI Automation, and before calling any screen, window, or bounds API.",
|
|
@@ -66,6 +69,7 @@ export interface AppServerClientProvider {
|
|
|
66
69
|
getAgentFamily?(): "codex" | "traex" | undefined;
|
|
67
70
|
getProcessInfo?(): AgentProcessInfo;
|
|
68
71
|
getCodexHome?(): string;
|
|
72
|
+
getEnvironmentVariable?(name: string): string | undefined;
|
|
69
73
|
onDisconnect?(listener: (error: Error) => void): () => void;
|
|
70
74
|
release?(): Promise<void>;
|
|
71
75
|
close(): void;
|
|
@@ -80,6 +84,9 @@ interface CodexSession extends RuntimeSession {
|
|
|
80
84
|
needsResume: boolean;
|
|
81
85
|
canReplaceEmptyThread: boolean;
|
|
82
86
|
settingsRecoveryError?: string;
|
|
87
|
+
rolloutPath?: string;
|
|
88
|
+
turnCount?: number;
|
|
89
|
+
knownTurnIds: Set<string>;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
interface PendingApproval {
|
|
@@ -291,6 +298,8 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
291
298
|
limit: input.limit ?? 20,
|
|
292
299
|
sortKey: this.provider.getAgentFamily?.() === "traex" ? "updated_at" : "recency_at",
|
|
293
300
|
sortDirection: "desc",
|
|
301
|
+
// Omitting this filter only lists the App Server's current Provider.
|
|
302
|
+
modelProviders: [],
|
|
294
303
|
sourceKinds: USER_RESUMABLE_THREAD_SOURCE_KINDS,
|
|
295
304
|
archived: false,
|
|
296
305
|
searchTerm: input.searchTerm,
|
|
@@ -312,6 +321,53 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
312
321
|
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary({ ...response.thread, turns }));
|
|
313
322
|
}
|
|
314
323
|
|
|
324
|
+
async readRemoteSessionMetrics(remoteSessionId: string): Promise<RemoteSessionMetrics> {
|
|
325
|
+
const client = await this.client();
|
|
326
|
+
const metadata = await client.request<ThreadReadResponse>(
|
|
327
|
+
"thread/read", { threadId: remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS,
|
|
328
|
+
);
|
|
329
|
+
const { turnCount, turnIds } = await this.countRemoteTurns(client, remoteSessionId);
|
|
330
|
+
const rolloutPath = stringValue(metadata.thread.path)?.trim() || undefined;
|
|
331
|
+
const loaded = [...this.sessions.values()].filter((session) => session.remoteSessionId === remoteSessionId);
|
|
332
|
+
for (const session of loaded) {
|
|
333
|
+
session.rolloutPath = rolloutPath ?? session.rolloutPath;
|
|
334
|
+
const knownTurnIds = new Set(turnIds);
|
|
335
|
+
if (session.activeTurnId && !knownTurnIds.has(session.activeTurnId)) knownTurnIds.add(session.activeTurnId);
|
|
336
|
+
session.knownTurnIds = knownTurnIds;
|
|
337
|
+
session.turnCount = knownTurnIds.size;
|
|
338
|
+
}
|
|
339
|
+
const effectivePath = rolloutPath ?? loaded[0]?.rolloutPath;
|
|
340
|
+
return {
|
|
341
|
+
turnCount: loaded[0]?.turnCount ?? turnCount,
|
|
342
|
+
storageBytes: rolloutStorageBytes(effectivePath),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async countRemoteTurns(
|
|
347
|
+
client: AppServerClient,
|
|
348
|
+
remoteSessionId: string,
|
|
349
|
+
): Promise<{ turnCount: number; turnIds: Set<string> }> {
|
|
350
|
+
const turnIds = new Set<string>();
|
|
351
|
+
const seenCursors = new Set<string>();
|
|
352
|
+
let cursor: string | undefined;
|
|
353
|
+
do {
|
|
354
|
+
const page = await this.readTurnPage(client, remoteSessionId, {
|
|
355
|
+
cursor,
|
|
356
|
+
limit: 100,
|
|
357
|
+
itemsView: "summary",
|
|
358
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
359
|
+
for (const turn of page.data) turnIds.add(turn.id);
|
|
360
|
+
const nextCursor = page.nextCursor ?? undefined;
|
|
361
|
+
if (!nextCursor) break;
|
|
362
|
+
if (nextCursor === cursor || seenCursors.has(nextCursor)) {
|
|
363
|
+
throw new Error("App Server repeated a Turn history cursor while counting task Turns.");
|
|
364
|
+
}
|
|
365
|
+
seenCursors.add(nextCursor);
|
|
366
|
+
cursor = nextCursor;
|
|
367
|
+
} while (cursor);
|
|
368
|
+
return { turnCount: turnIds.size, turnIds };
|
|
369
|
+
}
|
|
370
|
+
|
|
315
371
|
async listRemoteTurnSummaries(
|
|
316
372
|
remoteSessionId: string,
|
|
317
373
|
input: { cursor?: string; limit: number },
|
|
@@ -698,7 +754,43 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
698
754
|
this.emit({ type: "approval_resolved", sessionId, turnId: pending.turnId, requestId, decision });
|
|
699
755
|
}
|
|
700
756
|
|
|
701
|
-
async listModels(): Promise<ModelOption[]> {
|
|
757
|
+
async listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]> {
|
|
758
|
+
const requestedProvider = modelProvider?.trim();
|
|
759
|
+
if (!requestedProvider
|
|
760
|
+
|| (requestedProvider === BUILT_IN_CODEX_PROVIDER_ID && this.provider.getAgentFamily?.() !== "traex")) {
|
|
761
|
+
return this.listCatalogModels();
|
|
762
|
+
}
|
|
763
|
+
const config = await this.readConfig();
|
|
764
|
+
const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
|
|
765
|
+
const providerConfig = configuredProviders[requestedProvider];
|
|
766
|
+
let catalog: ModelOption[] = [];
|
|
767
|
+
try {
|
|
768
|
+
catalog = await this.listCatalogModels();
|
|
769
|
+
} catch (error) {
|
|
770
|
+
this.logger.warn({ error, modelProvider: requestedProvider },
|
|
771
|
+
"Failed to read the App Server model catalog while discovering Provider models.");
|
|
772
|
+
}
|
|
773
|
+
const configuredDefaultModel = stringValue(config.model_provider)?.trim() === requestedProvider
|
|
774
|
+
? stringValue(config.model)?.trim()
|
|
775
|
+
: undefined;
|
|
776
|
+
try {
|
|
777
|
+
return await fetchProviderModels({
|
|
778
|
+
providerId: requestedProvider,
|
|
779
|
+
config: (isRecord(providerConfig) ? providerConfig : {}) as ProviderModelConfig,
|
|
780
|
+
catalog,
|
|
781
|
+
configuredDefaultModel,
|
|
782
|
+
environmentValue: (name) => this.provider.getEnvironmentVariable?.(name) ?? process.env[name],
|
|
783
|
+
});
|
|
784
|
+
} catch (error) {
|
|
785
|
+
const fallback = fallbackProviderModel(catalog, fallbackModel, configuredDefaultModel);
|
|
786
|
+
if (!fallback) throw error;
|
|
787
|
+
this.logger.warn({ error, modelProvider: requestedProvider, fallbackModel: fallback.id },
|
|
788
|
+
"Provider model discovery failed; using the current or configured model as a fallback.");
|
|
789
|
+
return [fallback];
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private async listCatalogModels(): Promise<ModelOption[]> {
|
|
702
794
|
const response = await (await this.client()).request<{
|
|
703
795
|
data: Array<{
|
|
704
796
|
id: string;
|
|
@@ -724,12 +816,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
724
816
|
}
|
|
725
817
|
|
|
726
818
|
async listModelProviders(): Promise<ModelProviderOption[]> {
|
|
727
|
-
const
|
|
728
|
-
"config/read",
|
|
729
|
-
{},
|
|
730
|
-
CONTROL_REQUEST_TIMEOUT_MS,
|
|
731
|
-
);
|
|
732
|
-
const config = response.config ?? {};
|
|
819
|
+
const config = await this.readConfig();
|
|
733
820
|
const defaultProvider = stringValue(config.model_provider)?.trim();
|
|
734
821
|
const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
|
|
735
822
|
const providers = new Map<string, ModelProviderOption>();
|
|
@@ -757,6 +844,15 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
757
844
|
);
|
|
758
845
|
}
|
|
759
846
|
|
|
847
|
+
private async readConfig(): Promise<Record<string, unknown>> {
|
|
848
|
+
const response = await (await this.client()).request<{ config?: Record<string, unknown> }>(
|
|
849
|
+
"config/read",
|
|
850
|
+
{},
|
|
851
|
+
CONTROL_REQUEST_TIMEOUT_MS,
|
|
852
|
+
);
|
|
853
|
+
return response.config ?? {};
|
|
854
|
+
}
|
|
855
|
+
|
|
760
856
|
onEvent(listener: (event: RuntimeEvent) => void): () => void {
|
|
761
857
|
this.listeners.add(listener);
|
|
762
858
|
return () => this.listeners.delete(listener);
|
|
@@ -890,6 +986,18 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
890
986
|
turnId: mapped.turnId,
|
|
891
987
|
lastTokens: mapped.lastTokens,
|
|
892
988
|
cumulativeTokens: mapped.cumulativeTokens,
|
|
989
|
+
contextTokens: mapped.contextTokens,
|
|
990
|
+
});
|
|
991
|
+
} else if (mapped.kind === "context_compaction") {
|
|
992
|
+
this.emit({
|
|
993
|
+
type: "context_compaction",
|
|
994
|
+
sessionId,
|
|
995
|
+
turnId: mapped.turnId,
|
|
996
|
+
phase: mapped.phase,
|
|
997
|
+
compactionId: mapped.compactionId,
|
|
998
|
+
timestampMs: mapped.timestampMs,
|
|
999
|
+
turnCount: session.turnCount,
|
|
1000
|
+
storageBytes: rolloutStorageBytes(session.rolloutPath),
|
|
893
1001
|
});
|
|
894
1002
|
} else if (mapped.kind === "agent_message_phase") {
|
|
895
1003
|
session.messagePhases.set(mapped.itemId, mapped.phase);
|
|
@@ -1005,7 +1113,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1005
1113
|
if (input.reasoningEffort) return input.reasoningEffort;
|
|
1006
1114
|
if (response.reasoningEffort) return response.reasoningEffort;
|
|
1007
1115
|
const model = input.model ?? response.model;
|
|
1008
|
-
const models = await this.listModels();
|
|
1116
|
+
const models = await this.listModels(input.modelProvider ?? response.modelProvider, model);
|
|
1009
1117
|
return models.find((item) => item.id === model)?.defaultReasoningEffort
|
|
1010
1118
|
?? models.find((item) => item.isDefault)?.defaultReasoningEffort;
|
|
1011
1119
|
}
|
|
@@ -1091,6 +1199,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1091
1199
|
|
|
1092
1200
|
private adoptTurn(session: CodexSession, turnId: string, startedAt: number): void {
|
|
1093
1201
|
session.canReplaceEmptyThread = false;
|
|
1202
|
+
if (!session.knownTurnIds.has(turnId)) {
|
|
1203
|
+
session.knownTurnIds.add(turnId);
|
|
1204
|
+
if (session.turnCount !== undefined) session.turnCount += 1;
|
|
1205
|
+
}
|
|
1094
1206
|
session.activeTurnId = turnId;
|
|
1095
1207
|
session.activeTurnStartedAt = startedAt;
|
|
1096
1208
|
session.finalText = "";
|
|
@@ -1183,6 +1295,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1183
1295
|
messagePhases: new Map(),
|
|
1184
1296
|
needsResume: false,
|
|
1185
1297
|
canReplaceEmptyThread: !("remoteSessionId" in input),
|
|
1298
|
+
rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
|
|
1299
|
+
turnCount: "remoteSessionId" in input ? undefined : 0,
|
|
1300
|
+
knownTurnIds: new Set(),
|
|
1186
1301
|
};
|
|
1187
1302
|
}
|
|
1188
1303
|
|
|
@@ -1241,7 +1356,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1241
1356
|
}
|
|
1242
1357
|
|
|
1243
1358
|
interface ThreadResponse {
|
|
1244
|
-
thread: { id: string; name?: string | null; preview?: string };
|
|
1359
|
+
thread: { id: string; name?: string | null; preview?: string; path?: string | null };
|
|
1245
1360
|
modelProvider?: string;
|
|
1246
1361
|
model?: string;
|
|
1247
1362
|
reasoningEffort?: string | null;
|
|
@@ -1273,6 +1388,7 @@ interface CodexThreadSnapshot {
|
|
|
1273
1388
|
recencyAt?: number | null;
|
|
1274
1389
|
status?: { type?: string };
|
|
1275
1390
|
forkedFromId?: string | null;
|
|
1391
|
+
path?: string | null;
|
|
1276
1392
|
turns?: CodexTurnSnapshot[];
|
|
1277
1393
|
}
|
|
1278
1394
|
|
|
@@ -1341,6 +1457,34 @@ function stringValue(value: unknown): string | undefined {
|
|
|
1341
1457
|
return typeof value === "string" ? value : undefined;
|
|
1342
1458
|
}
|
|
1343
1459
|
|
|
1460
|
+
function rolloutStorageBytes(filePath: string | undefined): number | undefined {
|
|
1461
|
+
if (!filePath) return undefined;
|
|
1462
|
+
try {
|
|
1463
|
+
const stats = statSync(filePath);
|
|
1464
|
+
return stats.isFile() ? stats.size : undefined;
|
|
1465
|
+
} catch {
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function fallbackProviderModel(
|
|
1471
|
+
catalog: ModelOption[],
|
|
1472
|
+
requestedModel?: string,
|
|
1473
|
+
configuredDefaultModel?: string,
|
|
1474
|
+
): ModelOption | undefined {
|
|
1475
|
+
const id = requestedModel?.trim()
|
|
1476
|
+
|| configuredDefaultModel?.trim()
|
|
1477
|
+
|| catalog.find((model) => model.isDefault)?.id
|
|
1478
|
+
|| catalog[0]?.id;
|
|
1479
|
+
if (!id) return undefined;
|
|
1480
|
+
const catalogModel = catalog.find((model) => model.id === id);
|
|
1481
|
+
return {
|
|
1482
|
+
...(catalogModel ?? { id, supportedReasoningEfforts: [] }),
|
|
1483
|
+
id,
|
|
1484
|
+
isDefault: true,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1344
1488
|
function turnStartedAt(turn: CodexTurnSnapshot): number {
|
|
1345
1489
|
return typeof turn.startedAt === "number" ? turn.startedAt * 1_000 : Date.now();
|
|
1346
1490
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { ModelOption, ReasoningEffortOption } from "../runtime/types.js";
|
|
2
|
+
|
|
3
|
+
const PROVIDER_MODEL_LIST_TIMEOUT_MS = 10_000;
|
|
4
|
+
|
|
5
|
+
export interface ProviderModelConfig {
|
|
6
|
+
base_url?: unknown;
|
|
7
|
+
env_key?: unknown;
|
|
8
|
+
experimental_bearer_token?: unknown;
|
|
9
|
+
query_params?: unknown;
|
|
10
|
+
http_headers?: unknown;
|
|
11
|
+
env_http_headers?: unknown;
|
|
12
|
+
default_model?: unknown;
|
|
13
|
+
model?: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function fetchProviderModels(input: {
|
|
17
|
+
providerId: string;
|
|
18
|
+
config: ProviderModelConfig;
|
|
19
|
+
catalog: ModelOption[];
|
|
20
|
+
configuredDefaultModel?: string;
|
|
21
|
+
environmentValue: (name: string) => string | undefined;
|
|
22
|
+
}): Promise<ModelOption[]> {
|
|
23
|
+
const baseUrl = stringValue(input.config.base_url)?.trim();
|
|
24
|
+
if (!baseUrl) {
|
|
25
|
+
throw new Error(`Provider ${input.providerId} 没有配置 base_url,无法读取模型列表。`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const url = providerModelsUrl(baseUrl, input.config.query_params, input.providerId);
|
|
29
|
+
const headers = providerModelHeaders(input.config, input.environmentValue);
|
|
30
|
+
let response: Response;
|
|
31
|
+
try {
|
|
32
|
+
response = await fetch(url, {
|
|
33
|
+
method: "GET",
|
|
34
|
+
headers,
|
|
35
|
+
signal: AbortSignal.timeout(PROVIDER_MODEL_LIST_TIMEOUT_MS),
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error(`无法连接 Provider ${input.providerId} 的模型列表,请检查 Provider 服务和网络。`);
|
|
39
|
+
}
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`Provider ${input.providerId} 的模型列表请求失败(HTTP ${response.status})。`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let payload: unknown;
|
|
45
|
+
try {
|
|
46
|
+
payload = await response.json();
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error(`Provider ${input.providerId} 返回了无效的模型列表。`);
|
|
49
|
+
}
|
|
50
|
+
const models = normalizeProviderModels(payload, input.catalog, {
|
|
51
|
+
configuredDefaultModel: input.configuredDefaultModel,
|
|
52
|
+
providerDefaultModel: stringValue(input.config.default_model)?.trim()
|
|
53
|
+
?? stringValue(input.config.model)?.trim(),
|
|
54
|
+
});
|
|
55
|
+
if (models.length === 0) {
|
|
56
|
+
throw new Error(`Provider ${input.providerId} 没有返回可用模型。`);
|
|
57
|
+
}
|
|
58
|
+
return models;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function providerModelsUrl(baseUrl: string, queryParams: unknown, providerId: string): URL {
|
|
62
|
+
let url: URL;
|
|
63
|
+
try {
|
|
64
|
+
url = new URL(baseUrl);
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error(`Provider ${providerId} 的 base_url 无效,无法读取模型列表。`);
|
|
67
|
+
}
|
|
68
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
69
|
+
throw new Error(`Provider ${providerId} 的 base_url 不支持模型发现。`);
|
|
70
|
+
}
|
|
71
|
+
const path = url.pathname.replace(/\/+$/u, "");
|
|
72
|
+
if (!/\/models$/iu.test(path)) url.pathname = `${path}/models`;
|
|
73
|
+
if (isRecord(queryParams)) {
|
|
74
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
75
|
+
if (!key || value === null || value === undefined || typeof value === "object") continue;
|
|
76
|
+
url.searchParams.set(key, String(value));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return url;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function providerModelHeaders(
|
|
83
|
+
config: ProviderModelConfig,
|
|
84
|
+
environmentValue: (name: string) => string | undefined,
|
|
85
|
+
): Headers {
|
|
86
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
87
|
+
appendHeaderValues(headers, config.http_headers, (value) => value);
|
|
88
|
+
appendHeaderValues(headers, config.env_http_headers, environmentValue);
|
|
89
|
+
if (!headers.has("authorization")) {
|
|
90
|
+
const envKey = stringValue(config.env_key)?.trim();
|
|
91
|
+
const token = (envKey ? environmentValue(envKey) : undefined)
|
|
92
|
+
?? stringValue(config.experimental_bearer_token)?.trim();
|
|
93
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
94
|
+
}
|
|
95
|
+
return headers;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function appendHeaderValues(
|
|
99
|
+
headers: Headers,
|
|
100
|
+
values: unknown,
|
|
101
|
+
resolve: (value: string) => string | undefined,
|
|
102
|
+
): void {
|
|
103
|
+
if (!isRecord(values)) return;
|
|
104
|
+
for (const [name, rawValue] of Object.entries(values)) {
|
|
105
|
+
const value = stringValue(rawValue)?.trim();
|
|
106
|
+
const resolved = value ? resolve(value)?.trim() : undefined;
|
|
107
|
+
if (name.trim() && resolved) headers.set(name, resolved);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeProviderModels(
|
|
112
|
+
payload: unknown,
|
|
113
|
+
catalog: ModelOption[],
|
|
114
|
+
defaults: { configuredDefaultModel?: string; providerDefaultModel?: string },
|
|
115
|
+
): ModelOption[] {
|
|
116
|
+
const root = isRecord(payload) ? payload : undefined;
|
|
117
|
+
const entries = Array.isArray(root?.data)
|
|
118
|
+
? root.data
|
|
119
|
+
: Array.isArray(root?.models)
|
|
120
|
+
? root.models
|
|
121
|
+
: Array.isArray(payload)
|
|
122
|
+
? payload
|
|
123
|
+
: [];
|
|
124
|
+
const catalogById = new Map(catalog.map((model) => [model.id, model]));
|
|
125
|
+
const normalized = new Map<string, ModelOption>();
|
|
126
|
+
let itemDefaultModel: string | undefined;
|
|
127
|
+
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
const record = isRecord(entry) ? entry : undefined;
|
|
130
|
+
const id = (typeof entry === "string"
|
|
131
|
+
? entry
|
|
132
|
+
: stringValue(record?.id) ?? stringValue(record?.model) ?? stringValue(record?.name))?.trim();
|
|
133
|
+
if (!id || normalized.has(id)) continue;
|
|
134
|
+
const catalogModel = catalogById.get(id);
|
|
135
|
+
const efforts = reasoningEfforts(record) ?? catalogModel?.supportedReasoningEfforts ?? [];
|
|
136
|
+
const defaultEffort = stringValue(record?.defaultReasoningEffort)?.trim()
|
|
137
|
+
?? stringValue(record?.default_reasoning_effort)?.trim()
|
|
138
|
+
?? catalogModel?.defaultReasoningEffort;
|
|
139
|
+
const displayName = stringValue(record?.displayName)?.trim()
|
|
140
|
+
?? stringValue(record?.display_name)?.trim()
|
|
141
|
+
?? (stringValue(record?.name)?.trim() !== id ? stringValue(record?.name)?.trim() : undefined)
|
|
142
|
+
?? catalogModel?.displayName;
|
|
143
|
+
if (!itemDefaultModel && (record?.isDefault === true || record?.is_default === true || record?.default === true)) {
|
|
144
|
+
itemDefaultModel = id;
|
|
145
|
+
}
|
|
146
|
+
normalized.set(id, {
|
|
147
|
+
id,
|
|
148
|
+
...(displayName ? { displayName } : {}),
|
|
149
|
+
supportedReasoningEfforts: efforts,
|
|
150
|
+
...(defaultEffort ? { defaultReasoningEffort: defaultEffort } : {}),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const rootDefaultModel = stringValue(root?.defaultModel)?.trim()
|
|
155
|
+
?? stringValue(root?.default_model)?.trim();
|
|
156
|
+
const defaultModel = [rootDefaultModel, itemDefaultModel, defaults.providerDefaultModel, defaults.configuredDefaultModel]
|
|
157
|
+
.find((candidate) => candidate && normalized.has(candidate));
|
|
158
|
+
if (defaultModel) normalized.set(defaultModel, { ...normalized.get(defaultModel)!, isDefault: true });
|
|
159
|
+
return [...normalized.values()];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function reasoningEfforts(record: Record<string, unknown> | undefined): ReasoningEffortOption[] | undefined {
|
|
163
|
+
if (!record) return undefined;
|
|
164
|
+
const raw = record.supportedReasoningEfforts ?? record.supported_reasoning_efforts;
|
|
165
|
+
if (!Array.isArray(raw)) return undefined;
|
|
166
|
+
const efforts = raw.flatMap((entry): ReasoningEffortOption[] => {
|
|
167
|
+
if (typeof entry === "string" && entry.trim()) return [{ value: entry.trim() }];
|
|
168
|
+
if (!isRecord(entry)) return [];
|
|
169
|
+
const value = stringValue(entry.value)?.trim()
|
|
170
|
+
?? stringValue(entry.reasoningEffort)?.trim()
|
|
171
|
+
?? stringValue(entry.reasoning_effort)?.trim();
|
|
172
|
+
if (!value) return [];
|
|
173
|
+
const description = stringValue(entry.description)?.trim();
|
|
174
|
+
return [{ value, ...(description ? { description } : {}) }];
|
|
175
|
+
});
|
|
176
|
+
return efforts.length > 0 ? efforts : undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
180
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function stringValue(value: unknown): string | undefined {
|
|
184
|
+
return typeof value === "string" ? value : undefined;
|
|
185
|
+
}
|
|
@@ -128,8 +128,13 @@ export class CommandRouter {
|
|
|
128
128
|
throw new Error("/mute 只接受 on 或 off;不传参数等同于 /mute on。");
|
|
129
129
|
}
|
|
130
130
|
case "turns":
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
rejectOptions("/turn", args);
|
|
132
|
+
if (args.length > 1) throw new Error("/turn 只接受一个可选的 Turn ID 或序号。");
|
|
133
|
+
if (args[0] && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(args[0])
|
|
134
|
+
&& (!/^\d+$/.test(args[0]) || !Number.isSafeInteger(Number(args[0])) || Number(args[0]) < 1)) {
|
|
135
|
+
throw new Error("Turn 序号必须是从 1 开始的正整数。");
|
|
136
|
+
}
|
|
137
|
+
return args[0] ? { type: "turns", turnReference: args[0] } : { type: "turns" };
|
|
133
138
|
case "model":
|
|
134
139
|
return settingsCommand("/model", args, { type: "model" });
|
|
135
140
|
case "provider":
|
|
@@ -21,7 +21,7 @@ export type Command =
|
|
|
21
21
|
| { type: "restart"; force?: boolean }
|
|
22
22
|
| { type: "release" }
|
|
23
23
|
| { type: "mute"; enabled: boolean }
|
|
24
|
-
| { type: "turns" }
|
|
24
|
+
| { type: "turns"; turnReference?: string }
|
|
25
25
|
| { type: "model" }
|
|
26
26
|
| { type: "provider" }
|
|
27
27
|
| { type: "thinking" }
|
|
@@ -15,11 +15,14 @@ export class ConsoleTurnPresenter implements TurnPresenter {
|
|
|
15
15
|
_taskTitle?: string,
|
|
16
16
|
_projectCwd?: string,
|
|
17
17
|
agentLabel?: string,
|
|
18
|
+
_model?: string,
|
|
18
19
|
): void {
|
|
19
20
|
this.contexts.set(sessionId, contextKey);
|
|
20
21
|
if (agentLabel) this.agentLabels.set(sessionId, agentLabel);
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
updateSessionModel(): void {}
|
|
25
|
+
|
|
23
26
|
updateSessionTitle(): void {}
|
|
24
27
|
|
|
25
28
|
unregisterSession(sessionId: string): void {
|
|
@@ -123,6 +123,14 @@ export interface PermissionSelectorCardView {
|
|
|
123
123
|
|
|
124
124
|
export type ExecutionSettingsTab = "agent" | "provider" | "model" | "thinking" | "permission";
|
|
125
125
|
|
|
126
|
+
const EXECUTION_SETTING_EFFECTS: Record<ExecutionSettingsTab, string> = {
|
|
127
|
+
agent: "只影响新建任务,不切换当前任务。",
|
|
128
|
+
provider: "只影响当前任务,需空闲时切换。",
|
|
129
|
+
model: "只影响当前任务,下一轮生效。",
|
|
130
|
+
thinking: "只影响当前任务,下一轮生效。",
|
|
131
|
+
permission: "只影响当前任务,下一轮生效。",
|
|
132
|
+
};
|
|
133
|
+
|
|
126
134
|
export interface ExecutionSettingsAgentOption {
|
|
127
135
|
name: string;
|
|
128
136
|
title: string;
|
|
@@ -158,6 +166,7 @@ export interface TaskListCardAction {
|
|
|
158
166
|
text: string;
|
|
159
167
|
type?: "default" | "primary" | "danger";
|
|
160
168
|
value: Record<string, string>;
|
|
169
|
+
url?: string;
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
export interface HelpCardCommand {
|
|
@@ -542,7 +551,7 @@ export class CardRenderer {
|
|
|
542
551
|
|
|
543
552
|
if (view.activeTab === "agent") {
|
|
544
553
|
elements.push(...view.agents.map((agent) => settingsOptionRow({
|
|
545
|
-
label:
|
|
554
|
+
label: inlineCode(agent.name),
|
|
546
555
|
current: agent.name === view.currentAgent,
|
|
547
556
|
action: {
|
|
548
557
|
text: "Switch",
|
|
@@ -562,9 +571,6 @@ export class CardRenderer {
|
|
|
562
571
|
elements.push(...view.providers.map((provider) => settingsOptionRow({
|
|
563
572
|
label: [
|
|
564
573
|
inlineCode(provider.id),
|
|
565
|
-
provider.displayName && provider.displayName !== provider.id
|
|
566
|
-
? ` · ${escapeCardHtml(provider.displayName)}`
|
|
567
|
-
: "",
|
|
568
574
|
provider.isDefault ? " · 默认" : "",
|
|
569
575
|
].join(""),
|
|
570
576
|
current: provider.id === view.currentProvider,
|
|
@@ -631,6 +637,7 @@ export class CardRenderer {
|
|
|
631
637
|
})));
|
|
632
638
|
}
|
|
633
639
|
|
|
640
|
+
elements.push(markdown(`> ${EXECUTION_SETTING_EFFECTS[view.activeTab]}`));
|
|
634
641
|
return sectionCard("运行设置", elements, view.notice ? "green" : "blue");
|
|
635
642
|
}
|
|
636
643
|
|
|
@@ -1138,7 +1145,7 @@ export class CardRenderer {
|
|
|
1138
1145
|
};
|
|
1139
1146
|
}
|
|
1140
1147
|
|
|
1141
|
-
renderTurn(state: TurnViewState): Record<string, unknown> {
|
|
1148
|
+
renderTurn(state: TurnViewState, previewUrl?: string): Record<string, unknown> {
|
|
1142
1149
|
const elements = this.thinkingCardLayout === "timeline"
|
|
1143
1150
|
? renderTurnElements(state, "hidden")
|
|
1144
1151
|
: renderGroupedTurnElements(state, "hidden");
|
|
@@ -1149,23 +1156,31 @@ export class CardRenderer {
|
|
|
1149
1156
|
type: "danger",
|
|
1150
1157
|
value: { action: "turn_cancel", sessionId: state.sessionId, turnId: state.turnId },
|
|
1151
1158
|
});
|
|
1159
|
+
if (previewUrl) footerActions.push(turnPreviewAction(previewUrl));
|
|
1152
1160
|
elements.push(
|
|
1153
1161
|
{ tag: "hr" },
|
|
1154
|
-
taskActionRow(footerActions, renderTurnDuration(state)),
|
|
1162
|
+
taskActionRow(footerActions, renderTurnDuration(state), true),
|
|
1155
1163
|
);
|
|
1156
1164
|
} else if (state.status === "completed") {
|
|
1157
1165
|
footerActions.push({
|
|
1158
1166
|
text: "Reset",
|
|
1159
1167
|
value: { action: "turn_reset", sessionId: state.sessionId, turnId: state.turnId },
|
|
1160
1168
|
});
|
|
1169
|
+
if (previewUrl) footerActions.push(turnPreviewAction(previewUrl));
|
|
1161
1170
|
elements.push(
|
|
1162
1171
|
{ tag: "hr" },
|
|
1163
|
-
taskActionRow(footerActions),
|
|
1172
|
+
taskActionRow(footerActions, undefined, true),
|
|
1164
1173
|
);
|
|
1165
1174
|
} else if (state.status === "starting") {
|
|
1175
|
+
if (previewUrl) footerActions.push(turnPreviewAction(previewUrl));
|
|
1176
|
+
elements.push(
|
|
1177
|
+
{ tag: "hr" },
|
|
1178
|
+
taskActionRow(footerActions, renderTurnDuration(state), true),
|
|
1179
|
+
);
|
|
1180
|
+
} else if (previewUrl) {
|
|
1166
1181
|
elements.push(
|
|
1167
1182
|
{ tag: "hr" },
|
|
1168
|
-
taskActionRow([], renderTurnDuration(state)),
|
|
1183
|
+
taskActionRow([turnPreviewAction(previewUrl)], renderTurnDuration(state)),
|
|
1169
1184
|
);
|
|
1170
1185
|
}
|
|
1171
1186
|
if (state.status === "completed") {
|
|
@@ -1184,11 +1199,15 @@ export class CardRenderer {
|
|
|
1184
1199
|
);
|
|
1185
1200
|
}
|
|
1186
1201
|
|
|
1187
|
-
renderTurnDetails(state: TurnViewState): Record<string, unknown> {
|
|
1202
|
+
renderTurnDetails(state: TurnViewState, previewUrl?: string): Record<string, unknown> {
|
|
1188
1203
|
const title = state.taskTitle
|
|
1189
1204
|
? `任务执行详情:${truncateText(state.taskTitle.replace(/\s+/g, " ").trim(), 60)}`
|
|
1190
1205
|
: "任务执行详情";
|
|
1191
|
-
|
|
1206
|
+
const elements = [markdown(`Turn: \`${state.turnId}\``)];
|
|
1207
|
+
if (state.prompt) elements.push(markdown(`**Prompt**\n${truncateText(state.prompt, 1_000)}`));
|
|
1208
|
+
elements.push(...renderTurnElements(state, "always"));
|
|
1209
|
+
if (previewUrl) elements.push(taskActionRow([turnPreviewAction(previewUrl)]));
|
|
1210
|
+
return turnCard(title, "blue", elements, renderTurnSubtitle(state));
|
|
1192
1211
|
}
|
|
1193
1212
|
|
|
1194
1213
|
renderActivityHistory(state: TurnViewState, requestedPage: number): Record<string, unknown> {
|
|
@@ -2274,7 +2293,7 @@ function renderToolDetails(tool: ToolState, projectCwd?: string): string {
|
|
|
2274
2293
|
return codeBlock([`$ ${commandText}`, resultText].filter((part): part is string => part !== undefined).join("\n"), 2_003);
|
|
2275
2294
|
}
|
|
2276
2295
|
|
|
2277
|
-
function formatShellCommandForDisplay(command: string): string {
|
|
2296
|
+
export function formatShellCommandForDisplay(command: string): string {
|
|
2278
2297
|
const normalized = command.replace(/\r\n/g, "\n");
|
|
2279
2298
|
let result = "";
|
|
2280
2299
|
let quote: "'" | "\"" | undefined;
|
|
@@ -2379,7 +2398,7 @@ function toolPanelTitle(tool: ToolState): string {
|
|
|
2379
2398
|
const command = stripAnsi(tool.command ?? tool.title).trim();
|
|
2380
2399
|
const meaningfulCommand = tool.kind === "web_search"
|
|
2381
2400
|
? tool.title
|
|
2382
|
-
:
|
|
2401
|
+
: displayToolCommand(command) ?? tool.title;
|
|
2383
2402
|
const duration = toolDuration(tool);
|
|
2384
2403
|
const prefix = `${icon} `;
|
|
2385
2404
|
const suffix = duration ? ` · ${duration}` : "";
|
|
@@ -2451,6 +2470,11 @@ function shellCommandStatus(view: ShellCommandCardView): {
|
|
|
2451
2470
|
return { title: "命令执行失败", label: `退出码 ${view.exitCode ?? "未知"}`, template: "red" };
|
|
2452
2471
|
}
|
|
2453
2472
|
|
|
2473
|
+
export function displayToolCommand(command: string): string | undefined {
|
|
2474
|
+
const clean = stripAnsi(command).trim();
|
|
2475
|
+
return unwrapShellCommand(clean);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2454
2478
|
function unwrapShellCommand(command: string): string | undefined {
|
|
2455
2479
|
return unwrapPowerShellCommand(command) ?? unwrapPosixShellCommand(command);
|
|
2456
2480
|
}
|
|
@@ -3007,10 +3031,17 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
|
|
|
3007
3031
|
elements: [markdown(
|
|
3008
3032
|
`<font color='${action.type === "danger" ? "red" : "blue"}'>${escapeCardActionText(action.text)}</font>`,
|
|
3009
3033
|
)],
|
|
3010
|
-
behaviors:
|
|
3011
|
-
type: "
|
|
3012
|
-
value: action.value,
|
|
3013
|
-
|
|
3034
|
+
behaviors: action.url
|
|
3035
|
+
? [{ type: "open_url", default_url: action.url }]
|
|
3036
|
+
: [{ type: "callback", value: action.value }],
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
function turnPreviewAction(previewUrl: string): TaskListCardAction {
|
|
3041
|
+
return {
|
|
3042
|
+
text: "Preview",
|
|
3043
|
+
value: {},
|
|
3044
|
+
url: previewUrl,
|
|
3014
3045
|
};
|
|
3015
3046
|
}
|
|
3016
3047
|
|