im-agent 0.1.1 → 0.2.1
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/README.md +1 -1
- package/package.json +1 -1
- package/src/agents/claude/account.ts +100 -0
- package/src/agents/claude/driver.ts +47 -2
- package/src/agents/claude/permissions.ts +35 -2
- package/src/agents/claude/probe.ts +54 -0
- package/src/agents/claude/session.ts +35 -10
- package/src/agents/codex/driver.ts +4 -0
- package/src/agents/codex/session.ts +24 -6
- package/src/channels/feishu/bot.ts +60 -0
- package/src/channels/feishu/cards.ts +52 -1
- package/src/channels/feishu/envelope.ts +7 -2
- package/src/channels/feishu/index.ts +11 -1
- package/src/channels/feishu/slash-sync.ts +192 -0
- package/src/channels/web/api.ts +54 -0
- package/src/channels/web/server.ts +12 -0
- package/src/core/command-catalog.ts +112 -0
- package/src/core/commands.ts +22 -17
- package/src/core/config.ts +5 -0
- package/src/core/hub.ts +58 -0
- package/src/core/interactions.ts +13 -1
- package/src/core/store.ts +2 -0
- package/src/core/types.ts +16 -1
- package/web/app.js +547 -15
- package/web/style.css +147 -0
package/src/core/hub.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
StepKind,
|
|
35
35
|
UserInput,
|
|
36
36
|
} from "./types.js";
|
|
37
|
+
import { buildCatalog, type AgentCommand, type CommandEntry, type CommandPrefs } from "./command-catalog.js";
|
|
37
38
|
import { helpText, type ParsedCommand } from "./commands.js";
|
|
38
39
|
|
|
39
40
|
// ---------- 事件 ----------
|
|
@@ -95,6 +96,8 @@ export type CommandReply =
|
|
|
95
96
|
effortSupported: boolean;
|
|
96
97
|
}
|
|
97
98
|
| { type: "agents"; agents: { id: string; label: string }[]; current: string }
|
|
99
|
+
/** 只发了「/」:可用命令(已按用户配置排序、去掉隐藏的) */
|
|
100
|
+
| { type: "commands"; agent: string; commands: CommandEntry[] }
|
|
98
101
|
| { type: "none" };
|
|
99
102
|
|
|
100
103
|
export type ConversationInfo = {
|
|
@@ -368,6 +371,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
|
|
|
368
371
|
...(effort ? { effort } : {}),
|
|
369
372
|
...(resumeId ? { resumeId } : {}),
|
|
370
373
|
...(fork ? { fork: true } : {}),
|
|
374
|
+
...(conv.autoEdit ? { autoEdit: true } : {}),
|
|
371
375
|
},
|
|
372
376
|
{
|
|
373
377
|
onTurnStart: () => alive() && this.emitEvent(key, { type: "turn_start" }),
|
|
@@ -756,6 +760,28 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
|
|
|
756
760
|
return `思考程度已切换为 ${effort}。`;
|
|
757
761
|
}
|
|
758
762
|
|
|
763
|
+
/** /autoedit:on 不给就是切换。只影响正在跑的会话和以后新建的,不追溯已经弹出的授权。 */
|
|
764
|
+
private async setAutoEditNow(key: string, on: boolean | undefined): Promise<string> {
|
|
765
|
+
const conv = getConversation(key)!;
|
|
766
|
+
const next = on ?? !conv.autoEdit;
|
|
767
|
+
const live = this.live.get(key);
|
|
768
|
+
if (live?.session.setAutoEdit) await withTimeout(live.session.setAutoEdit(next), "切换自动改文件");
|
|
769
|
+
updateConversation(key, { autoEdit: next });
|
|
770
|
+
this.emitEvent(key, { type: "meta" });
|
|
771
|
+
const driver = this.requireDriver(conv.agent);
|
|
772
|
+
const note =
|
|
773
|
+
conv.agent === "codex"
|
|
774
|
+
? `\n\n注意:${driver.label} 改文件由沙箱控制(设置 → Agent → 沙箱),这个开关只对 Claude 生效。`
|
|
775
|
+
: "";
|
|
776
|
+
return next
|
|
777
|
+
? `已开启:这条对话里 Claude 修改工作目录(${conv.cwd})内的文件不再询问;目录外的文件、Bash 命令照常询问。发 /autoedit off 关闭。${note}`
|
|
778
|
+
: `已关闭:改文件恢复逐次询问。${note}`;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
setAutoEdit(key: string, on?: boolean): Promise<string> {
|
|
782
|
+
return this.serial(key, () => this.setAutoEditNow(key, on));
|
|
783
|
+
}
|
|
784
|
+
|
|
759
785
|
/** 切 agent:各 agent 的会话各自保留,切回来接着聊。 */
|
|
760
786
|
private async setAgentNow(key: string, agent: string, allowed: string[], op: SwitchOp): Promise<string> {
|
|
761
787
|
const conv = getConversation(key)!;
|
|
@@ -809,6 +835,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
|
|
|
809
835
|
`目录:${conv.cwd}`,
|
|
810
836
|
`模型:${runtime.model ?? "默认"}`,
|
|
811
837
|
...(runtime.effort ? [`思考程度:${runtime.effort}`] : []),
|
|
838
|
+
...(conv.autoEdit ? ["自动允许改文件:开(/autoedit off 关闭)"] : []),
|
|
812
839
|
...(extra.length > 0 ? ["", ...extra] : []),
|
|
813
840
|
].join("\n");
|
|
814
841
|
}
|
|
@@ -879,8 +906,15 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
|
|
|
879
906
|
switch (cmd.kind) {
|
|
880
907
|
case "help":
|
|
881
908
|
return text(helpText(ctx.allowedAgents));
|
|
909
|
+
case "commands": {
|
|
910
|
+
const conv = getConversation(key)!;
|
|
911
|
+
const all = await this.commandCatalog(conv.agent, { cwd: conv.cwd, allowedAgents: ctx.allowedAgents });
|
|
912
|
+
return { type: "commands", agent: conv.agent, commands: all.filter((c) => !c.hidden) };
|
|
913
|
+
}
|
|
882
914
|
case "pwd":
|
|
883
915
|
return text(getConversation(key)!.cwd);
|
|
916
|
+
case "autoedit":
|
|
917
|
+
return text(await this.setAutoEdit(key, cmd.on));
|
|
884
918
|
case "status":
|
|
885
919
|
return text(await this.status(key));
|
|
886
920
|
case "cd":
|
|
@@ -916,6 +950,9 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
|
|
|
916
950
|
return text(await this.stop(key));
|
|
917
951
|
case "approve": {
|
|
918
952
|
const settled = interactions.decide(cmd.requestId, cmd.decision as ApprovalDecision, ctx.by);
|
|
953
|
+
if (!settled && interactions.isPending(cmd.requestId)) {
|
|
954
|
+
return text(`请求 ${cmd.requestId} 不支持 ${cmd.decision}。`);
|
|
955
|
+
}
|
|
919
956
|
if (!settled) return text(`请求 ${cmd.requestId} 不存在或已处理。`);
|
|
920
957
|
return text(`${SETTLEMENT_LABEL[cmd.decision]}:${cmd.requestId}`);
|
|
921
958
|
}
|
|
@@ -924,6 +961,27 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
|
|
|
924
961
|
}
|
|
925
962
|
}
|
|
926
963
|
|
|
964
|
+
/**
|
|
965
|
+
* 某个 agent 的完整命令表(含隐藏项,带 hidden 标记),已按配置排序。
|
|
966
|
+
* 原生命令读不到、或者超时,就只给 im-agent 自己的 —— 列表少几项,总比「/」没反应强。
|
|
967
|
+
* 未启用的 agent 不去问:问一次就可能拉起它的进程。
|
|
968
|
+
*/
|
|
969
|
+
async commandCatalog(
|
|
970
|
+
agent: string,
|
|
971
|
+
opts: { cwd?: string; allowedAgents: string[] },
|
|
972
|
+
): Promise<CommandEntry[]> {
|
|
973
|
+
const driver = this.enabledAgents().find((d) => d.id === agent);
|
|
974
|
+
let native: AgentCommand[] = [];
|
|
975
|
+
if (driver?.commands) {
|
|
976
|
+
native = await withTimeout(driver.commands(opts.cwd), "读取命令列表").catch((err) => {
|
|
977
|
+
console.error(`[${agent}] 读取命令列表失败:`, err instanceof Error ? err.message : err);
|
|
978
|
+
return [];
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
const prefs = (config().commands as Record<string, CommandPrefs | undefined>)[agent];
|
|
982
|
+
return buildCatalog({ allowedAgents: opts.allowedAgents, native, prefs });
|
|
983
|
+
}
|
|
984
|
+
|
|
927
985
|
async shutdown(): Promise<void> {
|
|
928
986
|
for (const key of [...this.live.keys()]) await this.drop(key, "服务正在重启");
|
|
929
987
|
for (const driver of this.drivers.values()) await driver.stop().catch(() => {});
|
package/src/core/interactions.ts
CHANGED
|
@@ -89,7 +89,18 @@ class InteractionCenter extends EventEmitter<InteractionEvents> {
|
|
|
89
89
|
decide(id: string, decision: Settlement, by: string): Interaction | undefined {
|
|
90
90
|
const entry = this.pending.get(id);
|
|
91
91
|
if (!entry || entry.spec.kind !== "approval") return undefined;
|
|
92
|
-
|
|
92
|
+
// 卡上没有这个按钮(比如批准计划)就不认:不能靠手打 /approve 绕过去
|
|
93
|
+
if (decision === "allow-session" && !entry.spec.allowSession) return undefined;
|
|
94
|
+
const settled = this.settle(id, decision, by);
|
|
95
|
+
if (decision === "allow-session") {
|
|
96
|
+
// 并行的工具调用会同时挂着好几张卡:同一个会话进程里能一并放行的,一起放掉
|
|
97
|
+
for (const other of [...this.pending.values()]) {
|
|
98
|
+
if (other.convKey !== entry.convKey || other.owner !== entry.owner) continue;
|
|
99
|
+
if (other.spec.kind !== "approval" || !other.spec.allowSession) continue;
|
|
100
|
+
this.settle(other.id, "allow-session", `${by}(随「本会话全部允许」)`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return settled;
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
/** 把某个选项设成选中 / 未选中。全单选且都答完时自动收束。 */
|
|
@@ -251,6 +262,7 @@ export function answerTextOf(entry: Interaction): Record<string, string> {
|
|
|
251
262
|
export const SETTLEMENT_LABEL: Record<Settlement, string> = {
|
|
252
263
|
"allow-once": "已允许(本次)",
|
|
253
264
|
"allow-always": "已允许",
|
|
265
|
+
"allow-session": "已允许(本会话全部放行)",
|
|
254
266
|
deny: "已拒绝",
|
|
255
267
|
timeout: "已超时,按拒绝处理",
|
|
256
268
|
cancelled: "已取消",
|
package/src/core/store.ts
CHANGED
|
@@ -38,6 +38,8 @@ export type Conversation = {
|
|
|
38
38
|
createdAt: number;
|
|
39
39
|
/** Web 列表里隐藏(Web 上删除的会话只是隐藏,agent 侧的历史还在) */
|
|
40
40
|
hidden?: boolean;
|
|
41
|
+
/** /autoedit:这条对话里 agent 在工作目录内改文件不再询问(跨 /new 保留,属于对话不属于某个会话) */
|
|
42
|
+
autoEdit?: boolean;
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
type StoreShape = Record<string, Conversation>;
|
package/src/core/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 核心类型:渠道(飞书 / Web / 以后的 Telegram)与 agent(Claude / Codex / 以后的 ACP)
|
|
2
2
|
// 之间只通过这里的类型说话。渠道不认识 Claude,agent 不认识飞书。
|
|
3
|
+
import type { AgentCommand } from "./command-catalog.js";
|
|
3
4
|
|
|
4
5
|
// ---------- 用户输入 ----------
|
|
5
6
|
|
|
@@ -19,7 +20,11 @@ export type UserInput = { text: string; attachments: SavedAttachment[] };
|
|
|
19
20
|
|
|
20
21
|
// ---------- 交互(审批 / 提问) ----------
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* allow-session:本会话内后续的审批全部自动放行(不落盘)。会话重建
|
|
25
|
+
* (/new、切目录、切 agent、恢复历史、进程重启)后失效。
|
|
26
|
+
*/
|
|
27
|
+
export type ApprovalDecision = "allow-once" | "allow-always" | "allow-session" | "deny";
|
|
23
28
|
|
|
24
29
|
/** 一个卡点的收束方式。timeout / cancelled 和 deny 一样都是「没放行」,但显示不同。 */
|
|
25
30
|
export type Settlement = ApprovalDecision | "timeout" | "cancelled" | "answered" | "skipped";
|
|
@@ -43,6 +48,8 @@ export type InteractionSpec =
|
|
|
43
48
|
detail: string;
|
|
44
49
|
/** 「总是允许」按钮的文字;不给就没有这个按钮 */
|
|
45
50
|
alwaysLabel?: string;
|
|
51
|
+
/** 给了就显示「本会话全部允许」按钮。只有 agent 能真正兑现时才给 */
|
|
52
|
+
allowSession?: boolean;
|
|
46
53
|
}
|
|
47
54
|
| { kind: "question"; title: string; questions: QuestionSpec[] };
|
|
48
55
|
|
|
@@ -118,6 +125,8 @@ export type SessionOptions = {
|
|
|
118
125
|
fork?: boolean;
|
|
119
126
|
/** 附件所在目录,agent 需要能直接读 */
|
|
120
127
|
uploadsDir: string;
|
|
128
|
+
/** 工作目录内改文件不再询问(/autoedit)。Bash 等照常询问 */
|
|
129
|
+
autoEdit?: boolean;
|
|
121
130
|
};
|
|
122
131
|
|
|
123
132
|
export interface AgentSession {
|
|
@@ -127,6 +136,8 @@ export interface AgentSession {
|
|
|
127
136
|
setModel(model?: string, effort?: string): Promise<void>;
|
|
128
137
|
describeRuntime(): Promise<RuntimeInfo>;
|
|
129
138
|
dispose(): Promise<void>;
|
|
139
|
+
/** 运行中切换「自动允许改文件」;不支持的 agent 不实现 */
|
|
140
|
+
setAutoEdit?(on: boolean): Promise<void>;
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
export type Diagnosis = {
|
|
@@ -151,6 +162,10 @@ export interface AgentDriver {
|
|
|
151
162
|
readHistory(id: string, cwd: string): Promise<HistoryMessage[]>;
|
|
152
163
|
/** 有的 agent(Claude)要借一个活着的会话才能问到模型列表 */
|
|
153
164
|
listModels(session: AgentSession): Promise<ModelInfo[]>;
|
|
165
|
+
/** 不依赖会话的模型列表,设置页的下拉框用 */
|
|
166
|
+
catalog(): Promise<ModelInfo[]>;
|
|
167
|
+
/** agent 自己的斜杠命令(skills 之类),没有就不实现。cwd 影响项目级命令 */
|
|
168
|
+
commands?(cwd?: string): Promise<AgentCommand[]>;
|
|
154
169
|
/** 不建会话也能知道的默认模型 / 思考程度 */
|
|
155
170
|
defaults(): Promise<RuntimeInfo>;
|
|
156
171
|
/** /status 里额外的几行(比如 Codex 的额度) */
|