@keyou007/agent-bot 0.1.20-alpha.1 → 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 +21 -0
- package/README.md +27 -4
- package/README.zh.md +23 -4
- package/dist/cli/AgentPrerequisites.js +11 -47
- package/dist/cli/AgentPrerequisites.js.map +1 -1
- package/dist/cli/DefaultAgentSelection.js +1 -1
- package/dist/cli/DefaultAgentSelection.js.map +1 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +21 -1
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexLocalActivityDetector.js +32 -10
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +56 -7
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +361 -168
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/CodexVersion.js +18 -0
- package/dist/codex/CodexVersion.js.map +1 -0
- package/dist/codex/ProviderModels.js +159 -0
- package/dist/codex/ProviderModels.js.map +1 -0
- package/dist/codex/ProviderSwitch.js +32 -0
- package/dist/codex/ProviderSwitch.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 +82 -27
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +17 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +34 -8
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/InboundText.js +11 -0
- package/dist/feishu/InboundText.js.map +1 -1
- package/dist/feishu/MergedForwardMessage.js +17 -5
- package/dist/feishu/MergedForwardMessage.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +164 -19
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/MarkdownFileLinks.js +65 -0
- package/dist/local-files/MarkdownFileLinks.js.map +1 -0
- 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 +573 -325
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +65 -65
- package/dist/state/StateStore.js.map +1 -1
- package/dist/utils/formatStorageSize.js +15 -0
- package/dist/utils/formatStorageSize.js.map +1 -0
- package/dist/utils/semanticVersion.js +39 -0
- package/dist/utils/semanticVersion.js.map +1 -0
- package/docs/technical-reference.md +11 -3
- package/docs/technical-reference.zh.md +11 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +11 -1
- package/src/cli/AgentPrerequisites.ts +14 -40
- package/src/cli/DefaultAgentSelection.ts +1 -1
- package/src/cli.ts +13 -0
- package/src/codex/CodexEventMapper.ts +31 -1
- package/src/codex/CodexLocalActivityDetector.ts +41 -9
- package/src/codex/CodexProcessManager.ts +48 -7
- package/src/codex/CodexRuntime.ts +397 -204
- package/src/codex/CodexVersion.ts +18 -0
- package/src/codex/ProviderModels.ts +185 -0
- package/src/codex/ProviderSwitch.ts +36 -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 +84 -43
- package/src/feishu/FeishuConnector.ts +15 -5
- package/src/feishu/FeishuTurnPresenter.ts +46 -4
- package/src/feishu/InboundText.ts +11 -0
- package/src/feishu/MergedForwardMessage.ts +15 -5
- package/src/index.ts +3 -1
- package/src/local-files/LocalFileViewerServer.ts +179 -20
- package/src/local-files/MarkdownFileLinks.ts +64 -0
- 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 +547 -297
- package/src/runtime/types.ts +33 -3
- package/src/state/StateStore.ts +86 -90
- package/src/utils/formatStorageSize.ts +13 -0
- package/src/utils/semanticVersion.ts +28 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { compareSemanticVersions } from "../utils/semanticVersion.js";
|
|
2
|
+
|
|
3
|
+
// Verified protocol baseline: paginated Turn summaries and excludeTurns.
|
|
4
|
+
export const MINIMUM_CODEX_VERSION = "0.153.4";
|
|
5
|
+
|
|
6
|
+
export class CodexVersionError extends Error {}
|
|
7
|
+
|
|
8
|
+
export function codexVersionIssue(version: string | undefined): string | undefined {
|
|
9
|
+
if (version && /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/u.test(version)
|
|
10
|
+
&& compareSemanticVersions(version, MINIMUM_CODEX_VERSION) >= 0) return undefined;
|
|
11
|
+
return `Codex ${version ?? "版本未知"} 不受支持,AgentBot 需要 Codex >= ${MINIMUM_CODEX_VERSION}。`
|
|
12
|
+
+ "请运行 codex update(或 npm install -g @openai/codex@latest)升级,然后安全重启 AgentBot。";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertSupportedCodexVersion(version: string | undefined): void {
|
|
16
|
+
const issue = codexVersionIssue(version);
|
|
17
|
+
if (issue) throw new CodexVersionError(issue);
|
|
18
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AppServerRequestError } from "./AppServerConnection.js";
|
|
2
|
+
import type { RuntimeExecutionSettings } from "../runtime/types.js";
|
|
3
|
+
|
|
4
|
+
export const PROVIDER_SWITCH_BUSY = "当前任务正在执行,暂时不能切换 Provider。请等待任务完成,或停止任务后重试。";
|
|
5
|
+
|
|
6
|
+
export function isMissingRolloutError(error: unknown): boolean {
|
|
7
|
+
if (error instanceof AppServerRequestError && error.method !== "thread/resume") return false;
|
|
8
|
+
const message = error instanceof AppServerRequestError ? error.serverMessage
|
|
9
|
+
: error instanceof Error ? error.message : String(error);
|
|
10
|
+
if (/paginated history lineage|missing source rollout/iu.test(message)) return false;
|
|
11
|
+
return /no rollout found|rollout[^\n]*(?:not found|missing)/iu.test(message);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function assertProviderSettingsApplied(
|
|
15
|
+
actual: { modelProvider?: string; model?: string },
|
|
16
|
+
requested: Pick<RuntimeExecutionSettings, "modelProvider" | "model">,
|
|
17
|
+
): void {
|
|
18
|
+
if (actual.modelProvider !== requested.modelProvider) {
|
|
19
|
+
throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}。`);
|
|
20
|
+
}
|
|
21
|
+
if (actual.model !== requested.model) {
|
|
22
|
+
throw new Error(`模型未生效:请求 ${requested.model},实际返回 ${actual.model ?? "未知"}。请确认目标 Provider 支持该模型。`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function providerSwitchFailure(error: unknown): string {
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
+
if (/active writer/iu.test(message)) return "任务被另一个 App Server 占用。请在原客户端释放任务后重试。";
|
|
29
|
+
if (/model[^\n]*(?:unsupported|not supported|not found|unavailable|does not exist)|unknown model/iu.test(message)) {
|
|
30
|
+
return `目标 Provider 不支持当前模型,请选择兼容的模型后重试。${message}`;
|
|
31
|
+
}
|
|
32
|
+
if (/unauthorized|forbidden|auth(?:entication|orization)|api.?key|provider[^\n]*(?:not found|unknown|not configured)/iu.test(message)) {
|
|
33
|
+
return `请检查目标 Provider 的配置和认证信息。${message}`;
|
|
34
|
+
}
|
|
35
|
+
return message;
|
|
36
|
+
}
|
|
@@ -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> {
|
|
@@ -1531,18 +1550,9 @@ export class CardRenderer {
|
|
|
1531
1550
|
if (view.entries.length === 0) {
|
|
1532
1551
|
elements.push(markdown("当前任务还没有成功完成的 turn。"));
|
|
1533
1552
|
} else {
|
|
1553
|
+
const sequenceWidth = `${Math.max(16, ...view.entries.map((entry) => String(entry.sequence).length * 10))}px`;
|
|
1534
1554
|
view.entries.forEach((entry) => {
|
|
1535
|
-
|
|
1536
|
-
elements.push(resetHistoryEntryRow(
|
|
1537
|
-
entry.graphNodeLine,
|
|
1538
|
-
entry.graphConnectorLine,
|
|
1539
|
-
entry.lines,
|
|
1540
|
-
entry.timestamp,
|
|
1541
|
-
action,
|
|
1542
|
-
entry.current === true,
|
|
1543
|
-
entry.running === true,
|
|
1544
|
-
entry.resetting === true,
|
|
1545
|
-
));
|
|
1555
|
+
elements.push(resetHistoryEntryRow(entry, sequenceWidth));
|
|
1546
1556
|
});
|
|
1547
1557
|
}
|
|
1548
1558
|
if (view.footerLines.length > 0) {
|
|
@@ -2283,7 +2293,7 @@ function renderToolDetails(tool: ToolState, projectCwd?: string): string {
|
|
|
2283
2293
|
return codeBlock([`$ ${commandText}`, resultText].filter((part): part is string => part !== undefined).join("\n"), 2_003);
|
|
2284
2294
|
}
|
|
2285
2295
|
|
|
2286
|
-
function formatShellCommandForDisplay(command: string): string {
|
|
2296
|
+
export function formatShellCommandForDisplay(command: string): string {
|
|
2287
2297
|
const normalized = command.replace(/\r\n/g, "\n");
|
|
2288
2298
|
let result = "";
|
|
2289
2299
|
let quote: "'" | "\"" | undefined;
|
|
@@ -2388,7 +2398,7 @@ function toolPanelTitle(tool: ToolState): string {
|
|
|
2388
2398
|
const command = stripAnsi(tool.command ?? tool.title).trim();
|
|
2389
2399
|
const meaningfulCommand = tool.kind === "web_search"
|
|
2390
2400
|
? tool.title
|
|
2391
|
-
:
|
|
2401
|
+
: displayToolCommand(command) ?? tool.title;
|
|
2392
2402
|
const duration = toolDuration(tool);
|
|
2393
2403
|
const prefix = `${icon} `;
|
|
2394
2404
|
const suffix = duration ? ` · ${duration}` : "";
|
|
@@ -2460,6 +2470,11 @@ function shellCommandStatus(view: ShellCommandCardView): {
|
|
|
2460
2470
|
return { title: "命令执行失败", label: `退出码 ${view.exitCode ?? "未知"}`, template: "red" };
|
|
2461
2471
|
}
|
|
2462
2472
|
|
|
2473
|
+
export function displayToolCommand(command: string): string | undefined {
|
|
2474
|
+
const clean = stripAnsi(command).trim();
|
|
2475
|
+
return unwrapShellCommand(clean);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2463
2478
|
function unwrapShellCommand(command: string): string | undefined {
|
|
2464
2479
|
return unwrapPowerShellCommand(command) ?? unwrapPosixShellCommand(command);
|
|
2465
2480
|
}
|
|
@@ -2825,24 +2840,31 @@ function settingsOptionRow(input: {
|
|
|
2825
2840
|
}
|
|
2826
2841
|
|
|
2827
2842
|
function resetHistoryEntryRow(
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
lines: string[],
|
|
2831
|
-
timestamp: string | undefined,
|
|
2832
|
-
action: TaskListCardAction | undefined,
|
|
2833
|
-
current: boolean,
|
|
2834
|
-
running: boolean,
|
|
2835
|
-
resetting: boolean,
|
|
2843
|
+
entry: ResetHistoryCardEntry,
|
|
2844
|
+
sequenceWidth: string,
|
|
2836
2845
|
): Record<string, unknown> {
|
|
2846
|
+
const { graphNodeLine, graphConnectorLine, lines, timestamp, current, running, resetting } = entry;
|
|
2847
|
+
const action = entry.actions?.[0];
|
|
2837
2848
|
const branchConnectorLine = graphConnectorLine && /[╱╲]/u.test(graphConnectorLine)
|
|
2838
2849
|
? graphConnectorLine
|
|
2839
2850
|
: undefined;
|
|
2840
|
-
const
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2851
|
+
const color = current ? "green" : running || resetting ? "orange" : "blue";
|
|
2852
|
+
const nodeLanes = graphNodeLine.match(/[●│]/gu) ?? [];
|
|
2853
|
+
const connectorLanes = branchConnectorLine?.split(" ");
|
|
2854
|
+
// Each lane has its own center; shorter connectors and wider turn numbers cannot shift it.
|
|
2855
|
+
const graphColumns = nodeLanes.map((node, lane) => ({
|
|
2856
|
+
tag: "column",
|
|
2857
|
+
width: "16px",
|
|
2858
|
+
vertical_align: "top",
|
|
2859
|
+
padding: "0px",
|
|
2860
|
+
elements: [{
|
|
2861
|
+
...markdown([
|
|
2862
|
+
`<font color='${color}'>${node}</font>`,
|
|
2863
|
+
...(connectorLanes?.[lane] ? [`<font color='grey'>${escapeCardHtml(connectorLanes[lane])}</font>`] : []),
|
|
2864
|
+
].join("\n")),
|
|
2865
|
+
text_align: "center",
|
|
2866
|
+
}],
|
|
2867
|
+
}));
|
|
2846
2868
|
const trailingElements = current
|
|
2847
2869
|
? [markdown("✅ 当前")]
|
|
2848
2870
|
: resetting
|
|
@@ -2863,8 +2885,20 @@ function resetHistoryEntryRow(
|
|
|
2863
2885
|
width: "auto",
|
|
2864
2886
|
vertical_align: "top",
|
|
2865
2887
|
elements: [{
|
|
2866
|
-
|
|
2867
|
-
|
|
2888
|
+
tag: "column_set",
|
|
2889
|
+
flex_mode: "none",
|
|
2890
|
+
horizontal_spacing: "0px",
|
|
2891
|
+
horizontal_align: "left",
|
|
2892
|
+
columns: [
|
|
2893
|
+
...graphColumns,
|
|
2894
|
+
{
|
|
2895
|
+
tag: "column",
|
|
2896
|
+
width: sequenceWidth,
|
|
2897
|
+
vertical_align: "top",
|
|
2898
|
+
padding: "0px",
|
|
2899
|
+
elements: [{ ...markdown(`<font color='${color}'>${entry.sequence}</font>`), text_align: "left" }],
|
|
2900
|
+
},
|
|
2901
|
+
],
|
|
2868
2902
|
}],
|
|
2869
2903
|
},
|
|
2870
2904
|
{
|
|
@@ -2997,10 +3031,17 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
|
|
|
2997
3031
|
elements: [markdown(
|
|
2998
3032
|
`<font color='${action.type === "danger" ? "red" : "blue"}'>${escapeCardActionText(action.text)}</font>`,
|
|
2999
3033
|
)],
|
|
3000
|
-
behaviors:
|
|
3001
|
-
type: "
|
|
3002
|
-
value: action.value,
|
|
3003
|
-
|
|
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,
|
|
3004
3045
|
};
|
|
3005
3046
|
}
|
|
3006
3047
|
|
|
@@ -3,7 +3,7 @@ import type { WSClient as LarkWsClient, WSConnectionStatus } from "@larksuiteoap
|
|
|
3
3
|
import type { AppConfig } from "../config/schema.js";
|
|
4
4
|
import { threadContextKey } from "./contextKey.js";
|
|
5
5
|
import { resolveFeishuBotOpenId } from "./FeishuBotIdentity.js";
|
|
6
|
-
import { normalizeFeishuPostText } from "./InboundText.js";
|
|
6
|
+
import { normalizeFeishuPostText, renderFeishuCodeBlock } from "./InboundText.js";
|
|
7
7
|
import { allowsFeishuUser } from "./ownerAccess.js";
|
|
8
8
|
import type { CardAction, ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
|
|
9
9
|
|
|
@@ -327,19 +327,29 @@ function parseMessageContent(
|
|
|
327
327
|
const title = typeof locale.title === "string" ? normalizeFeishuPostText(locale.title) : "";
|
|
328
328
|
if (title) paragraphs.push(title);
|
|
329
329
|
const images: string[] = [];
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
const rows = Array.isArray(locale.content) ? locale.content : locale.content_v2;
|
|
331
|
+
if (Array.isArray(rows)) {
|
|
332
|
+
for (const row of rows) {
|
|
332
333
|
if (!Array.isArray(row)) continue;
|
|
333
334
|
let rowText = "";
|
|
335
|
+
let preserveFormatting = false;
|
|
334
336
|
for (const element of row) {
|
|
335
337
|
if (!isRecord(element)) continue;
|
|
336
338
|
if ((element.tag === "text" || element.tag === "a") && typeof element.text === "string") {
|
|
337
339
|
rowText += element.text;
|
|
340
|
+
} else if (element.tag === "md" && typeof element.text === "string") {
|
|
341
|
+
rowText += element.text;
|
|
342
|
+
preserveFormatting = true;
|
|
343
|
+
} else if (element.tag === "code_block" && typeof element.text === "string") {
|
|
344
|
+
rowText += renderFeishuCodeBlock(element.text, element.language);
|
|
345
|
+
preserveFormatting = true;
|
|
338
346
|
} else if (element.tag === "img" && typeof element.image_key === "string") {
|
|
339
347
|
images.push(element.image_key);
|
|
340
348
|
}
|
|
341
349
|
}
|
|
342
|
-
const normalizedRow =
|
|
350
|
+
const normalizedRow = preserveFormatting
|
|
351
|
+
? rowText.replace(/\r\n?/gu, "\n").trim()
|
|
352
|
+
: normalizeFeishuPostText(rowText);
|
|
343
353
|
if (normalizedRow) paragraphs.push(normalizedRow);
|
|
344
354
|
}
|
|
345
355
|
}
|
|
@@ -347,7 +357,7 @@ function parseMessageContent(
|
|
|
347
357
|
}
|
|
348
358
|
|
|
349
359
|
function selectPostLocale(content: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
350
|
-
if (Array.isArray(content.content)) return content;
|
|
360
|
+
if (Array.isArray(content.content) || Array.isArray(content.content_v2)) return content;
|
|
351
361
|
for (const locale of ["zh_cn", "en_us", "ja_jp"]) {
|
|
352
362
|
if (isRecord(content[locale])) return content[locale];
|
|
353
363
|
}
|