im-agent 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +100 -2
- package/bin/cli.js +52 -0
- package/package.json +64 -4
- package/src/agents/claude/driver.ts +149 -0
- package/src/agents/claude/errors.ts +129 -0
- package/src/agents/claude/permissions.ts +227 -0
- package/src/agents/claude/session.ts +430 -0
- package/src/agents/codex/account.ts +92 -0
- package/src/agents/codex/app-server.ts +346 -0
- package/src/agents/codex/driver.ts +183 -0
- package/src/agents/codex/errors.ts +158 -0
- package/src/agents/codex/protocol.ts +284 -0
- package/src/agents/codex/refusals.ts +62 -0
- package/src/agents/codex/session.ts +738 -0
- package/src/channels/feishu/bot.ts +609 -0
- package/src/channels/feishu/cards.ts +309 -0
- package/src/channels/feishu/client.ts +185 -0
- package/src/channels/feishu/envelope.ts +112 -0
- package/src/channels/feishu/index.ts +52 -0
- package/src/channels/feishu/message.ts +115 -0
- package/src/channels/feishu/register.ts +171 -0
- package/src/channels/feishu/turn-stream.ts +98 -0
- package/src/channels/web/api.ts +505 -0
- package/src/channels/web/auth.ts +204 -0
- package/src/channels/web/http.ts +118 -0
- package/src/channels/web/server.ts +363 -0
- package/src/cli/doctor.ts +39 -0
- package/src/cli/import.ts +155 -0
- package/src/cli/pair.ts +35 -0
- package/src/cli/passwd.ts +15 -0
- package/src/cli/service.ts +117 -0
- package/src/core/agent-env.ts +11 -0
- package/src/core/commands.ts +99 -0
- package/src/core/config.ts +227 -0
- package/src/core/dedup.ts +16 -0
- package/src/core/hub.ts +933 -0
- package/src/core/interactions.ts +261 -0
- package/src/core/pairing.ts +156 -0
- package/src/core/store.ts +174 -0
- package/src/core/types.ts +159 -0
- package/src/core/uploads.ts +200 -0
- package/src/home.ts +36 -0
- package/src/main.ts +68 -0
- package/web/app.js +1939 -0
- package/web/icon.svg +6 -0
- package/web/index.html +23 -0
- package/web/manifest.webmanifest +13 -0
- package/web/style.css +1267 -0
- package/web/sw.js +5 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// 把 app-server / codex 抛出的原始错误翻译成人能看懂、且知道下一步做什么的提示。
|
|
2
|
+
//
|
|
3
|
+
// 原则:聊天里给一句人话 + 一条明确动作;终端给完整修复步骤。
|
|
4
|
+
// 不要把原始错误直接甩给用户 —— 那既吓人又没有信息量。
|
|
5
|
+
import { config } from "../../core/config.js";
|
|
6
|
+
|
|
7
|
+
export type DiagnosisKind =
|
|
8
|
+
| "auth_expired"
|
|
9
|
+
| "auth_missing"
|
|
10
|
+
| "cli_missing"
|
|
11
|
+
| "runtime_missing"
|
|
12
|
+
| "rate_limit"
|
|
13
|
+
| "billing"
|
|
14
|
+
| "turn_aborted"
|
|
15
|
+
| "protocol"
|
|
16
|
+
| "unknown";
|
|
17
|
+
|
|
18
|
+
export type Diagnosis = {
|
|
19
|
+
kind: DiagnosisKind;
|
|
20
|
+
/** 发到聊天里的简短说明 */
|
|
21
|
+
chatMessage: string;
|
|
22
|
+
/** 打在终端的修复步骤 */
|
|
23
|
+
consoleHint: string;
|
|
24
|
+
/** 是否属于「配置好之前重试也没用」的问题 */
|
|
25
|
+
fatal: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const LOGIN_STEPS = [
|
|
29
|
+
" 1) codex login # 用 ChatGPT 账号登录,或配 OPENAI_API_KEY",
|
|
30
|
+
" 2) codex doctor # 确认认证与运行时都正常",
|
|
31
|
+
' 3) codex exec "回复 OK" # 验证真的能跑',
|
|
32
|
+
" 4) 重启im-agent",
|
|
33
|
+
].join("\n");
|
|
34
|
+
|
|
35
|
+
export function diagnose(err: unknown): Diagnosis {
|
|
36
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
37
|
+
const text = raw.toLowerCase();
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
text.includes("not logged in") ||
|
|
41
|
+
text.includes("session expired") ||
|
|
42
|
+
text.includes("token has expired") ||
|
|
43
|
+
text.includes("refresh token")
|
|
44
|
+
) {
|
|
45
|
+
return {
|
|
46
|
+
kind: "auth_expired",
|
|
47
|
+
chatMessage:
|
|
48
|
+
"Codex 的登录状态已过期,我暂时无法工作。\n请机器主人在终端重新登录(`codex login`),完成后我就能继续。",
|
|
49
|
+
consoleHint: `Codex 登录已过期,需要重新登录:\n${LOGIN_STEPS}`,
|
|
50
|
+
fatal: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
text.includes("unauthorized") ||
|
|
56
|
+
text.includes("invalid api key") ||
|
|
57
|
+
text.includes("401")
|
|
58
|
+
) {
|
|
59
|
+
return {
|
|
60
|
+
kind: "auth_missing",
|
|
61
|
+
chatMessage:
|
|
62
|
+
"Codex 的凭证无效,我暂时无法工作。\n请机器主人检查登录状态或 OPENAI_API_KEY。",
|
|
63
|
+
consoleHint: `Codex 凭证无效。若用 ChatGPT 账号,重新登录:\n${LOGIN_STEPS}\n若用 API key,检查 OPENAI_API_KEY 是否正确。`,
|
|
64
|
+
fatal: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 本机没装 codex,或装在 PATH 之外(服务托管时的经典症状)
|
|
69
|
+
if (
|
|
70
|
+
text.includes("enoent") ||
|
|
71
|
+
text.includes("command not found") ||
|
|
72
|
+
text.includes("无法启动") ||
|
|
73
|
+
text.includes("spawn")
|
|
74
|
+
) {
|
|
75
|
+
return {
|
|
76
|
+
kind: "cli_missing",
|
|
77
|
+
chatMessage: "本机还没装好 Codex,我暂时无法工作。\n请机器主人先完成安装。",
|
|
78
|
+
consoleHint:
|
|
79
|
+
`找不到可执行的 \`${config().agents.codex.path}\`。\n` +
|
|
80
|
+
" 1) npm i -g @openai/codex # 或用你惯用的安装方式\n" +
|
|
81
|
+
" 2) command -v codex # 确认它在 PATH 里\n" +
|
|
82
|
+
" 3) 装在非标准位置的话,在 .env 里设 BRIDGE_CODEX_PATH 指到绝对路径\n" +
|
|
83
|
+
`${LOGIN_STEPS}`,
|
|
84
|
+
fatal: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// codex 把执行器做成了单独下载的运行时二进制,CDN 拉不到时它就跑不了命令。
|
|
89
|
+
// 症状很隐蔽:模型照样回话,但每次要执行命令都失败。
|
|
90
|
+
if (text.includes("code-mode host") || text.includes("codex-code-mode-host")) {
|
|
91
|
+
return {
|
|
92
|
+
kind: "runtime_missing",
|
|
93
|
+
chatMessage:
|
|
94
|
+
"Codex 能说话,但执行不了命令 —— 本机缺少它的运行时组件。\n请机器主人跑一次 `codex doctor` 看看。",
|
|
95
|
+
consoleHint:
|
|
96
|
+
"Codex 的命令执行器(code-mode host)没能启动,通常是运行时二进制没下下来。\n" +
|
|
97
|
+
" 1) codex doctor # 看 reachability 一节,多半是 CDN 拉不通\n" +
|
|
98
|
+
" 2) 确认能访问 chatgpt.com,必要时配好代理后重试\n" +
|
|
99
|
+
" 3) codex update # 让它重新拉一次运行时",
|
|
100
|
+
fatal: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (text.includes("rate limit") || text.includes("usage limit") || text.includes("429")) {
|
|
105
|
+
return {
|
|
106
|
+
kind: "rate_limit",
|
|
107
|
+
chatMessage: "Codex 的用量已达上限,请稍后再试。",
|
|
108
|
+
consoleHint: "已达用量上限。等配额恢复,或换用额度更充足的账号 / API key。",
|
|
109
|
+
fatal: false,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (text.includes("quota") || text.includes("billing") || text.includes("credit")) {
|
|
114
|
+
return {
|
|
115
|
+
kind: "billing",
|
|
116
|
+
chatMessage: "Codex 账户余额或配额不足,请机器主人处理后再试。",
|
|
117
|
+
consoleHint: "余额 / 配额不足,去 OpenAI 控制台检查订阅与用量。",
|
|
118
|
+
fatal: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (text.includes("interrupt") || text.includes("aborted") || text.includes("cancel")) {
|
|
123
|
+
return {
|
|
124
|
+
kind: "turn_aborted",
|
|
125
|
+
chatMessage:
|
|
126
|
+
"这一轮被中断了(/stop、审批超时,或 Codex 自己停了)。\n上下文没丢,把刚才的需求再说一遍就行。",
|
|
127
|
+
consoleHint:
|
|
128
|
+
"一轮执行被中途打断。常见原因:用户发了 /stop、审批超时按拒绝处理。\n偶发不用管,下一条消息会带着原 threadId 继续。",
|
|
129
|
+
fatal: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// app-server 标着 experimental,协议跟着 codex 版本走,没有版本协商。
|
|
134
|
+
// 方法名对不上多半就是 codex 升级过了。
|
|
135
|
+
if (text.includes("method not found") || text.includes("-32601")) {
|
|
136
|
+
return {
|
|
137
|
+
kind: "protocol",
|
|
138
|
+
chatMessage: "im-agent和本机 Codex 的协议对不上了,请机器主人升级im-agent。",
|
|
139
|
+
consoleHint:
|
|
140
|
+
"app-server 拒绝了某个方法,多半是 codex 升级后协议变了。\n" +
|
|
141
|
+
" 1) im-agent doctor # 会比对版本\n" +
|
|
142
|
+
" 2) npm i -g im-agent@latest # 升级 im-agent",
|
|
143
|
+
fatal: false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
kind: "unknown",
|
|
149
|
+
// 未知错误才把原文带给用户 —— 有原文总比一句「出错了」强
|
|
150
|
+
chatMessage: `出错了:${raw}`,
|
|
151
|
+
consoleHint: raw,
|
|
152
|
+
fatal: false,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 同一类问题只在终端刷一次完整指引,避免每条消息都刷屏 */
|
|
157
|
+
const announced = new Set<DiagnosisKind>();
|
|
158
|
+
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// `codex app-server` 的 JSON-RPC 协议,只抄我们真正用到的那一小块。
|
|
2
|
+
//
|
|
3
|
+
// 为什么手写而不是用 `codex app-server generate-ts`:那条命令会吐出七百多个
|
|
4
|
+
// ts-rs 生成的文件,其中绝大部分我们碰都不碰,而且它们互相 import,扔进仓库
|
|
5
|
+
// 就是七百个必须跟着 codex 版本一起漂移的死代码。这里只声明用得上的字段,
|
|
6
|
+
// 其余一律留给 `unknown` —— 协议加字段不会弄坏我们,删字段才会,而删字段
|
|
7
|
+
// 本来就该炸出来。
|
|
8
|
+
//
|
|
9
|
+
// ⚠️ app-server 在 codex 里标着 [experimental],`initialize` 的响应里**没有**
|
|
10
|
+
// 协议版本号,也就没有版本协商这回事:协议跟着 codex 二进制走。所以下面记一个
|
|
11
|
+
// 「开发时对着的版本」,doctor 会拿它和本机实际的 codex 比对,不一致时明确
|
|
12
|
+
// 提醒,而不是让它在运行时以一个莫名其妙的形态崩掉。
|
|
13
|
+
export const DEVELOPED_AGAINST_CODEX = "0.153.0";
|
|
14
|
+
|
|
15
|
+
// ---------- 通用 JSON-RPC ----------
|
|
16
|
+
|
|
17
|
+
export type RequestId = number | string;
|
|
18
|
+
|
|
19
|
+
export type JsonRpcRequest = {
|
|
20
|
+
jsonrpc: "2.0";
|
|
21
|
+
id: RequestId;
|
|
22
|
+
method: string;
|
|
23
|
+
params?: unknown;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type JsonRpcNotification = {
|
|
27
|
+
jsonrpc: "2.0";
|
|
28
|
+
method: string;
|
|
29
|
+
params?: unknown;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type JsonRpcResponse = {
|
|
33
|
+
jsonrpc?: "2.0";
|
|
34
|
+
id: RequestId;
|
|
35
|
+
result?: unknown;
|
|
36
|
+
error?: { code: number; message: string; data?: unknown };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** 服务端发来的一帧,可能是响应、通知,或它反过来向我们发起的请求。 */
|
|
40
|
+
export type Incoming = JsonRpcResponse & Partial<JsonRpcRequest>;
|
|
41
|
+
|
|
42
|
+
// ---------- 客户端 → 服务端 ----------
|
|
43
|
+
|
|
44
|
+
export type InitializeResult = {
|
|
45
|
+
userAgent?: string;
|
|
46
|
+
codexHome?: string;
|
|
47
|
+
platformOs?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ReasoningEffortOption = { reasoningEffort: string; description?: string };
|
|
51
|
+
|
|
52
|
+
export type Model = {
|
|
53
|
+
id: string;
|
|
54
|
+
model: string;
|
|
55
|
+
displayName?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
hidden?: boolean;
|
|
58
|
+
isDefault?: boolean;
|
|
59
|
+
supportedReasoningEfforts?: ReasoningEffortOption[];
|
|
60
|
+
defaultReasoningEffort?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type ModelListResponse = { data: Model[]; nextCursor?: string | null };
|
|
64
|
+
|
|
65
|
+
/** 只列出我们会传的那些;协议里还有一堆可选项。 */
|
|
66
|
+
export type ThreadStartParams = {
|
|
67
|
+
cwd?: string;
|
|
68
|
+
model?: string | null;
|
|
69
|
+
approvalPolicy?: ApprovalPolicy;
|
|
70
|
+
sandbox?: SandboxMode;
|
|
71
|
+
config?: Record<string, unknown>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type Thread = {
|
|
75
|
+
id: string;
|
|
76
|
+
sessionId?: string;
|
|
77
|
+
preview?: string;
|
|
78
|
+
name?: string | null;
|
|
79
|
+
model?: string | null;
|
|
80
|
+
reasoningEffort?: string | null;
|
|
81
|
+
cwd?: string | null;
|
|
82
|
+
createdAt?: number;
|
|
83
|
+
updatedAt?: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type ThreadResponse = { thread: Thread };
|
|
87
|
+
export type ThreadListResponse = { data: Thread[]; nextCursor?: string | null };
|
|
88
|
+
|
|
89
|
+
/** 只列出我们会发的:文字,以及落在本机的图片。 */
|
|
90
|
+
export type UserInput = { type: "text"; text: string } | { type: "localImage"; path: string };
|
|
91
|
+
|
|
92
|
+
export type TurnStartParams = {
|
|
93
|
+
threadId: string;
|
|
94
|
+
input: UserInput[];
|
|
95
|
+
cwd?: string;
|
|
96
|
+
model?: string;
|
|
97
|
+
effort?: string;
|
|
98
|
+
approvalPolicy?: ApprovalPolicy;
|
|
99
|
+
sandboxPolicy?: unknown;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 审批策略。
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ 必须是 `on-request`。设成 `never` 会让审批请求根本不再发过来 ——
|
|
106
|
+
* 卡片授权那一整条路就成了死代码,Codex 在沙箱允许的范围内直接动手。
|
|
107
|
+
* 注意 codex 0.153 起 `untrusted` 已被移除,写进 config.toml 会让进程起不来。
|
|
108
|
+
*/
|
|
109
|
+
export type ApprovalPolicy = "never" | "on-request" | "on-failure";
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 沙箱模式 —— Codex 侧真正的第一道闸。
|
|
113
|
+
*
|
|
114
|
+
* 值得先建立的心智模型:命令**默认在沙箱里跑**,只有需要越界(写工作区外、
|
|
115
|
+
* 联网)才会请求审批。所以授权卡不会条条都弹 —— 而这并不意味着放松:挡住
|
|
116
|
+
* 越界的是操作系统级的沙箱,比一张需要人点的卡片更靠得住。
|
|
117
|
+
*/
|
|
118
|
+
export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
|
119
|
+
|
|
120
|
+
// ---------- 服务端 → 客户端:审批请求 ----------
|
|
121
|
+
|
|
122
|
+
export type CommandAction =
|
|
123
|
+
| { type: "read"; command: string; name?: string; path?: string }
|
|
124
|
+
| { type: "listFiles"; command: string; path?: string | null }
|
|
125
|
+
| { type: "search"; command: string; query?: string | null; path?: string | null }
|
|
126
|
+
| { type: "unknown"; command: string };
|
|
127
|
+
|
|
128
|
+
export type CommandExecutionApprovalParams = {
|
|
129
|
+
kind?: "command" | "writeStdin";
|
|
130
|
+
threadId: string;
|
|
131
|
+
turnId: string;
|
|
132
|
+
itemId: string;
|
|
133
|
+
approvalId?: string | null;
|
|
134
|
+
reason?: string | null;
|
|
135
|
+
command?: string | null;
|
|
136
|
+
cwd?: string | null;
|
|
137
|
+
commandActions?: CommandAction[] | null;
|
|
138
|
+
/**
|
|
139
|
+
* Codex 主动提出的「以后同类命令别再问」的 execpolicy 补丁。
|
|
140
|
+
* 这就是「总是允许」的原生实现 —— 粒度由 Codex 自己判断,
|
|
141
|
+
* 比我们在外面按命令首词瞎猜要准。
|
|
142
|
+
*/
|
|
143
|
+
proposedExecpolicyAmendment?: string[] | null;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type FileChangeApprovalParams = {
|
|
147
|
+
threadId: string;
|
|
148
|
+
turnId: string;
|
|
149
|
+
itemId: string;
|
|
150
|
+
reason?: string | null;
|
|
151
|
+
grantRoot?: string | null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export type PermissionsApprovalParams = {
|
|
155
|
+
threadId: string;
|
|
156
|
+
turnId: string;
|
|
157
|
+
itemId: string;
|
|
158
|
+
cwd?: string;
|
|
159
|
+
reason?: string | null;
|
|
160
|
+
/** Codex 想额外拿到的网络 / 文件系统权限,批准时原样回给它即可。 */
|
|
161
|
+
permissions: { network?: unknown | null; fileSystem?: unknown | null };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** 权限授予的范围:只这一轮,还是整条会话。 */
|
|
165
|
+
export type PermissionGrantScope = "turn" | "session";
|
|
166
|
+
|
|
167
|
+
/** 命令审批的决策。`acceptForSession` 对应卡片上的「本次会话内总是允许」。 */
|
|
168
|
+
export type CommandApprovalDecision =
|
|
169
|
+
| "accept"
|
|
170
|
+
| "acceptForSession"
|
|
171
|
+
| "decline"
|
|
172
|
+
| "cancel"
|
|
173
|
+
| { acceptWithExecpolicyAmendment: { execpolicy_amendment: string[] } };
|
|
174
|
+
|
|
175
|
+
export type FileChangeApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel";
|
|
176
|
+
|
|
177
|
+
// ---------- 服务端 → 客户端:提问 ----------
|
|
178
|
+
|
|
179
|
+
export type UserInputOption = { label: string; description?: string };
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 一问。
|
|
183
|
+
*
|
|
184
|
+
* `id` 是回答时的键 —— 回填 answers 表时用它,不是用问题原文。`isOther` 表示
|
|
185
|
+
* 这一问允许自由输入,`isSecret` 表示答案敏感(密码之类),后者我们一律不接:
|
|
186
|
+
* 飞书上的按钮和消息都不是填密码的地方。
|
|
187
|
+
*/
|
|
188
|
+
export type UserInputQuestion = {
|
|
189
|
+
id: string;
|
|
190
|
+
header?: string;
|
|
191
|
+
question: string;
|
|
192
|
+
isOther?: boolean;
|
|
193
|
+
isSecret?: boolean;
|
|
194
|
+
options?: UserInputOption[] | null;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export type ToolRequestUserInputParams = {
|
|
198
|
+
threadId: string;
|
|
199
|
+
turnId: string;
|
|
200
|
+
itemId: string;
|
|
201
|
+
questions: UserInputQuestion[];
|
|
202
|
+
isBlocking?: boolean;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/** 回答:问题 id → 选中的一个或多个答案文本。没答的问题不出现在表里。 */
|
|
206
|
+
export type ToolRequestUserInputResponse = {
|
|
207
|
+
answers: Record<string, { answers: string[] }>;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// ---------- 服务端 → 客户端:通知 ----------
|
|
211
|
+
|
|
212
|
+
export type ThreadItem =
|
|
213
|
+
| { type: "userMessage"; id: string }
|
|
214
|
+
| { type: "agentMessage"; id: string; text: string; phase?: string | null }
|
|
215
|
+
| { type: "reasoning"; id: string; summary?: string[]; content?: string[] }
|
|
216
|
+
| { type: "plan"; id: string; text: string }
|
|
217
|
+
| {
|
|
218
|
+
type: "commandExecution";
|
|
219
|
+
id: string;
|
|
220
|
+
command: string;
|
|
221
|
+
cwd?: string;
|
|
222
|
+
status: "inProgress" | "completed" | "failed";
|
|
223
|
+
exitCode?: number | null;
|
|
224
|
+
aggregatedOutput?: string | null;
|
|
225
|
+
commandActions?: CommandAction[];
|
|
226
|
+
}
|
|
227
|
+
| {
|
|
228
|
+
type: "fileChange";
|
|
229
|
+
id: string;
|
|
230
|
+
changes: { path: string; kind: "add" | "delete" | "update" }[];
|
|
231
|
+
status: "completed" | "failed";
|
|
232
|
+
}
|
|
233
|
+
| {
|
|
234
|
+
type: "mcpToolCall";
|
|
235
|
+
id: string;
|
|
236
|
+
server: string;
|
|
237
|
+
tool: string;
|
|
238
|
+
status: "inProgress" | "completed" | "failed";
|
|
239
|
+
}
|
|
240
|
+
| { type: "webSearch"; id: string; query?: string }
|
|
241
|
+
| { type: "error"; id: string; message: string }
|
|
242
|
+
| { type: string; id: string };
|
|
243
|
+
|
|
244
|
+
export type ItemNotification = {
|
|
245
|
+
item: ThreadItem;
|
|
246
|
+
threadId: string;
|
|
247
|
+
turnId: string;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export type AgentMessageDeltaNotification = {
|
|
251
|
+
threadId: string;
|
|
252
|
+
turnId: string;
|
|
253
|
+
itemId: string;
|
|
254
|
+
delta: string;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export type TurnError = { message: string; additionalDetails?: string | null };
|
|
258
|
+
|
|
259
|
+
export type Turn = {
|
|
260
|
+
id: string;
|
|
261
|
+
status: "inProgress" | "completed" | "failed" | "aborted" | string;
|
|
262
|
+
error?: TurnError | null;
|
|
263
|
+
durationMs?: number | null;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export type TurnCompletedNotification = { threadId: string; turn: Turn };
|
|
267
|
+
export type TurnStartedNotification = { threadId: string; turnId?: string };
|
|
268
|
+
export type ErrorNotification = {
|
|
269
|
+
error: TurnError;
|
|
270
|
+
willRetry: boolean;
|
|
271
|
+
threadId: string;
|
|
272
|
+
turnId: string;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export type TokenUsageNotification = {
|
|
276
|
+
threadId: string;
|
|
277
|
+
turnId: string;
|
|
278
|
+
tokenUsage?: {
|
|
279
|
+
totalTokens?: number;
|
|
280
|
+
inputTokens?: number;
|
|
281
|
+
outputTokens?: number;
|
|
282
|
+
cachedInputTokens?: number;
|
|
283
|
+
};
|
|
284
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// 「我不同意 / 我答不了」这句话,每一种反向请求都有自己的说法。
|
|
2
|
+
//
|
|
3
|
+
// 这件事值得单独一个模块,因为回错结构的代价特别不直观:Codex 那一轮会挂在
|
|
4
|
+
// 等待上,而聊天这边只是安静地没反应 —— 既没有报错也没有超时,看不出哪儿坏了。
|
|
5
|
+
// 实测就撞到过:MCP 的 elicitation 要的是 `{action}`,不是审批那套 `{decision}`。
|
|
6
|
+
//
|
|
7
|
+
// 所以这里把每种请求的「安全否定」集中列出来,任何新增的反向请求都必须先在
|
|
8
|
+
// 这儿登记,否则一律走 unsupported 那条路 —— 明确报错,好过瞎猜一个形状。
|
|
9
|
+
|
|
10
|
+
/** 无法应答的请求:交给 JSON-RPC 的 error 通道,而不是编一个结构出来。 */
|
|
11
|
+
export class UnsupportedRequest extends Error {
|
|
12
|
+
constructor(method: string) {
|
|
13
|
+
super(`im-agent不支持 ${method}`);
|
|
14
|
+
this.name = "UnsupportedRequest";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 某种反向请求的「安全否定」。
|
|
20
|
+
*
|
|
21
|
+
* 用在两处:找不到对应会话时的兜底,以及处理器自己出错时的收尾。
|
|
22
|
+
* 认不出来的方法返回 null,调用方应当抛 UnsupportedRequest。
|
|
23
|
+
*/
|
|
24
|
+
export function refusalFor(method: string): unknown | null {
|
|
25
|
+
switch (method) {
|
|
26
|
+
// v2 的三种审批
|
|
27
|
+
case "item/commandExecution/requestApproval":
|
|
28
|
+
case "item/fileChange/requestApproval":
|
|
29
|
+
return { decision: "decline" };
|
|
30
|
+
|
|
31
|
+
case "item/permissions/requestApproval":
|
|
32
|
+
// 拒绝 = 一样都不给,而不是原样放行
|
|
33
|
+
return { permissions: {}, scope: "turn" };
|
|
34
|
+
|
|
35
|
+
case "item/tool/requestUserInput":
|
|
36
|
+
return { answers: {} };
|
|
37
|
+
|
|
38
|
+
// MCP 服务器想向用户要一份表单输入。我们还没做表单渲染,明确拒绝。
|
|
39
|
+
// 注意字段是 action 而不是 decision,content / _meta 必须显式给 null。
|
|
40
|
+
case "mcpServer/elicitation/request":
|
|
41
|
+
return { action: "decline", content: null, _meta: null };
|
|
42
|
+
|
|
43
|
+
// 老协议里的两种审批。新版 codex 走上面的 v2 方法,但握手后到底发哪一套
|
|
44
|
+
// 取决于对端版本,两边都接住才不会在升级时突然卡住。
|
|
45
|
+
// 它们用的是 ReviewDecision,拒绝要带一句理由。
|
|
46
|
+
case "execCommandApproval":
|
|
47
|
+
case "applyPatchApproval":
|
|
48
|
+
return { decision: { denied: { rejection: "im-agent未获授权" } } };
|
|
49
|
+
|
|
50
|
+
// 服务端让我们代为调一个工具。没实现,如实说失败。
|
|
51
|
+
case "item/tool/call":
|
|
52
|
+
return {
|
|
53
|
+
contentItems: [{ type: "text", text: "im-agent不提供客户端工具" }],
|
|
54
|
+
success: false,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// 刷新 ChatGPT 令牌、生成设备证明 —— 这两件事只有真正的 Codex 客户端做得了,
|
|
58
|
+
// 我们编不出合法的返回值,只能走 error 通道。
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|