@keyou007/agent-bot 0.1.15-alpha.2 → 0.1.15-alpha.4

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.
@@ -1,6 +1,7 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { promises as fs, type Dirent } from "node:fs";
2
- import path from "node:path";
3
3
  import os from "node:os";
4
+ import path from "node:path";
4
5
  import type { Logger } from "pino";
5
6
  import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
6
7
  import { resolveUserPath } from "../config/paths.js";
@@ -48,6 +49,7 @@ import type {
48
49
  } from "../runtime/types.js";
49
50
  import {
50
51
  StateStore,
52
+ type CardActionBinding,
51
53
  type MessageReactionRecord,
52
54
  type MessageReactionStatus,
53
55
  type QueuedPromptRecord,
@@ -96,7 +98,8 @@ const MESSAGE_RECEIVED_REACTION = "OnIt";
96
98
  const MESSAGE_COMPLETED_REACTION = "DONE";
97
99
  const MESSAGE_FAILED_REACTION = "ERROR";
98
100
  const MESSAGE_CANCELLED_REACTION = "CrossMark";
99
- const SESSION_PAGE_SIZE = 5;
101
+ const SESSION_PAGE_SIZE = 10;
102
+ const SESSION_PROMPT_PREVIEW_LENGTH = 50;
100
103
  const DIRECTORY_PAGE_SIZE = 15;
101
104
  const RESET_HISTORY_PAGE_SIZE = 10;
102
105
  const MAX_LLM_TURN_RETRIES = 3;
@@ -749,8 +752,17 @@ export class ProxySessionController {
749
752
  return;
750
753
  }
751
754
  if (!this.store.claimInboundEvent(action.actionId, "card_action")) return;
752
- const contextKey = this.cardActionContextKey(action);
753
- const scopedAction = contextKey === action.contextKey ? action : { ...action, contextKey };
755
+ let resolvedAction: CardAction;
756
+ try {
757
+ resolvedAction = this.resolveCardActionBinding(action);
758
+ } catch (error) {
759
+ await this.sendError(action.contextKey, error);
760
+ return;
761
+ }
762
+ const contextKey = this.cardActionContextKey(resolvedAction);
763
+ const scopedAction = contextKey === resolvedAction.contextKey
764
+ ? resolvedAction
765
+ : { ...resolvedAction, contextKey };
754
766
  const replyTarget = isThreadContextKey(contextKey) && action.messageId
755
767
  ? { messageId: action.messageId, replyInThread: true as const }
756
768
  : undefined;
@@ -1663,6 +1675,16 @@ export class ProxySessionController {
1663
1675
  : action.contextKey;
1664
1676
  }
1665
1677
 
1678
+ private resolveCardActionBinding(action: CardAction): CardAction {
1679
+ if (typeof action.value.action === "string") return action;
1680
+ const token = typeof action.value.t === "string" ? action.value.t : undefined;
1681
+ if (!token) return action;
1682
+ if (!action.messageId) throw new Error("任务列表卡片缺少消息 ID,请重新发送 /sessions。");
1683
+ const value = this.store.getCardActionBinding(action.messageId, token);
1684
+ if (!value) throw new Error("任务列表卡片已失效,请重新发送 /sessions。");
1685
+ return { ...action, value };
1686
+ }
1687
+
1666
1688
  private async execute(
1667
1689
  contextKey: string,
1668
1690
  command: Command,
@@ -4997,13 +5019,34 @@ export class ProxySessionController {
4997
5019
  const remoteHint = remoteErrors.length > 0
4998
5020
  ? `部分 Agent 的任务读取失败:${remoteErrors.join(";")}`
4999
5021
  : undefined;
5000
-
5001
- const entries = orderTaskListByProject(
5002
- mergeTaskList(localSessions, remoteSessions, context.currentSessionId)
5022
+ const latestLocalUserPrompts = new Map<string, string>();
5023
+ for (const session of localSessions) {
5024
+ const persisted = this.store.findLatestTurnSnapshotForSession(session.localSessionId);
5025
+ const prompt = latestUserPromptFromTurnView(turnViewSnapshot(persisted?.snapshot));
5026
+ if (prompt) latestLocalUserPrompts.set(session.localSessionId, prompt);
5027
+ }
5028
+ const latestRemoteUserPrompts = new Map<string, string>();
5029
+ for (const { agentName, session: remote } of remoteSessions) {
5030
+ const local = this.store.findSessionByRemoteSessionId(remote.id, undefined, agentName);
5031
+ if (!local) continue;
5032
+ const persisted = this.store.findLatestTurnSnapshotForSession(local.localSessionId);
5033
+ const prompt = latestUserPromptFromTurnView(turnViewSnapshot(persisted?.snapshot));
5034
+ if (prompt) latestRemoteUserPrompts.set(agentRemoteKey(agentName, remote.id), prompt);
5035
+ }
5036
+
5037
+ const globallyOrderedEntries = orderTaskListByRecency(
5038
+ mergeTaskList(
5039
+ localSessions,
5040
+ remoteSessions,
5041
+ context.currentSessionId,
5042
+ latestLocalUserPrompts,
5043
+ latestRemoteUserPrompts,
5044
+ )
5003
5045
  .filter((entry) => !normalizedSearch
5004
5046
  || [entry.id, entry.title, entry.cwd, entry.agentName]
5005
5047
  .some((value) => value.toLowerCase().includes(normalizedSearch))),
5006
5048
  );
5049
+ const entries = groupTaskListPagesByProject(globallyOrderedEntries, SESSION_PAGE_SIZE);
5007
5050
  const activeCount = entries.filter((entry) => entry.active).length;
5008
5051
  const lastKnownPage = Math.max(0, Math.ceil(entries.length / SESSION_PAGE_SIZE) - 1);
5009
5052
  const page = remoteHasMore ? requestedPage : Math.min(requestedPage, lastKnownPage);
@@ -5017,6 +5060,9 @@ export class ProxySessionController {
5017
5060
  const hasPrevious = page > 0;
5018
5061
  const hasNext = remoteHasMore || entries.length > offset + SESSION_PAGE_SIZE;
5019
5062
  this.lastSessionListings.set(contextKey, entries.map((entry) => entry.reference));
5063
+ const cardActionBindings = new Map<string, CardActionBinding>();
5064
+ const bindCardAction = (action: TaskListCardAction): TaskListCardAction =>
5065
+ bindSessionCardAction(action, cardActionBindings);
5020
5066
  const cardEntries = visibleEntries.map((entry, index): {
5021
5067
  project: TaskProjectInfo;
5022
5068
  projectActions: TaskListCardAction[];
@@ -5024,11 +5070,11 @@ export class ProxySessionController {
5024
5070
  } => {
5025
5071
  const project = taskProjectInfo(entry);
5026
5072
  const projectActionSource = projectActionSources.get(project.key) ?? entry;
5027
- const marker = entry.current ? "✅" : entry.active ? "🟢" : "•";
5073
+ const marker = entry.current ? "✅ " : entry.active ? "🟢 " : "";
5028
5074
  const showStop = entry.status === "外部执行中"
5029
5075
  && entry.reference !== options.forceSwitchTaskId
5030
5076
  && entry.id !== options.forceSwitchTaskId;
5031
- const actions: TaskListCardAction[] = entry.current ? [] : [{
5077
+ const actions: TaskListCardAction[] = entry.current ? [] : [bindCardAction({
5032
5078
  text: showStop ? "Stop" : "Switch",
5033
5079
  type: showStop ? "danger" as const : "default" as const,
5034
5080
  value: {
@@ -5038,8 +5084,8 @@ export class ProxySessionController {
5038
5084
  page: String(page),
5039
5085
  contextKey,
5040
5086
  },
5041
- }];
5042
- const projectActions: TaskListCardAction[] = [{
5087
+ })];
5088
+ const projectActions: TaskListCardAction[] = [bindCardAction({
5043
5089
  text: "New",
5044
5090
  value: {
5045
5091
  action: "session_new",
@@ -5048,7 +5094,7 @@ export class ProxySessionController {
5048
5094
  page: String(page),
5049
5095
  contextKey,
5050
5096
  },
5051
- }, {
5097
+ }), bindCardAction({
5052
5098
  text: "NewGroup",
5053
5099
  value: {
5054
5100
  action: "session_new_group",
@@ -5057,8 +5103,8 @@ export class ProxySessionController {
5057
5103
  page: String(page),
5058
5104
  contextKey,
5059
5105
  },
5060
- }];
5061
- actions.push({
5106
+ })];
5107
+ actions.push(bindCardAction({
5062
5108
  text: "Fork",
5063
5109
  value: {
5064
5110
  action: "session_fork",
@@ -5067,8 +5113,8 @@ export class ProxySessionController {
5067
5113
  page: String(page),
5068
5114
  contextKey,
5069
5115
  },
5070
- });
5071
- actions.push({
5116
+ }));
5117
+ actions.push(bindCardAction({
5072
5118
  text: "ForkGroup",
5073
5119
  value: {
5074
5120
  action: "session_fork_group",
@@ -5077,16 +5123,16 @@ export class ProxySessionController {
5077
5123
  page: String(page),
5078
5124
  contextKey,
5079
5125
  },
5080
- });
5081
- actions.push({
5126
+ }));
5127
+ actions.push(bindCardAction({
5082
5128
  text: "Status",
5083
5129
  value: {
5084
5130
  action: "session_status",
5085
5131
  sessionId: entry.reference,
5086
5132
  contextKey,
5087
5133
  },
5088
- });
5089
- actions.push({
5134
+ }));
5135
+ actions.push(bindCardAction({
5090
5136
  text: "Archive",
5091
5137
  value: {
5092
5138
  action: "session_archive",
@@ -5095,18 +5141,18 @@ export class ProxySessionController {
5095
5141
  page: String(page),
5096
5142
  contextKey,
5097
5143
  },
5098
- });
5144
+ }));
5099
5145
  const title = truncateText(entry.title.replace(/\s+/gu, " ").trim() || "未命名任务", 56);
5146
+ const lastUserPrompt = sessionPromptPreview(entry.lastUserPrompt);
5100
5147
  return {
5101
5148
  project,
5102
5149
  projectActions,
5103
5150
  cardEntry: {
5104
5151
  reference: entry.reference,
5105
- summary: `${offset + index + 1}. ${marker} ${title} · ${entry.agentName}`,
5152
+ summary: `${offset + index + 1}. ${marker}${title} · ${entry.agentName}`,
5106
5153
  detailLines: [
5107
- `**状态 / 更新时间**:${cardText(entry.status)} / ${cardText(entry.updatedLabel)}`,
5108
- `**Agent / 任务 ID**:${cardCode(entry.agentName)} / ${cardCode(entry.id)}`,
5109
- `**目录**:${cardCode(entry.cwd || "目录未知")}`,
5154
+ cardText(lastUserPrompt),
5155
+ `**更新时间**:${cardText(entry.updatedLabel)}`,
5110
5156
  ],
5111
5157
  actions,
5112
5158
  current: entry.current,
@@ -5136,11 +5182,11 @@ export class ProxySessionController {
5136
5182
  ...(remoteHint ? [remoteHint] : []),
5137
5183
  `第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
5138
5184
  "",
5139
- "> 项目行:**New** 新建任务,**NewGroup** 新建群。",
5185
+ "> 项目菜单:**New** 新建任务,**NewGroup** 新建群。",
5140
5186
  "> 任务详情:**Switch** 切换,**Stop** 停止,**Fork** / **ForkGroup** 创建分支,**Status** 查看状态,**Archive** 归档。",
5141
5187
  ],
5142
5188
  [
5143
- ...(hasPrevious ? [{
5189
+ ...(hasPrevious ? [bindCardAction({
5144
5190
  text: "Previous",
5145
5191
  value: {
5146
5192
  action: "session_page",
@@ -5148,8 +5194,8 @@ export class ProxySessionController {
5148
5194
  page: String(page - 1),
5149
5195
  contextKey,
5150
5196
  },
5151
- }] : []),
5152
- ...(hasNext ? [{
5197
+ })] : []),
5198
+ ...(hasNext ? [bindCardAction({
5153
5199
  text: "Next",
5154
5200
  value: {
5155
5201
  action: "session_page",
@@ -5157,13 +5203,20 @@ export class ProxySessionController {
5157
5203
  page: String(page + 1),
5158
5204
  contextKey,
5159
5205
  },
5160
- }] : []),
5206
+ })] : []),
5161
5207
  ],
5162
5208
  );
5209
+ const bindings = [...cardActionBindings.values()];
5163
5210
  if (options.updateMessageId) {
5211
+ this.store.upsertCardActionBindings(options.updateMessageId, bindings);
5164
5212
  await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
5213
+ this.store.retainCardActionBindings(options.updateMessageId, bindings.map((binding) => binding.token));
5165
5214
  } else {
5166
- await this.outbound.sendInteractiveCard(contextKey, card);
5215
+ const messageId = await this.outbound.sendInteractiveCard(contextKey, card);
5216
+ if (messageId) {
5217
+ this.store.upsertCardActionBindings(messageId, bindings);
5218
+ this.store.retainCardActionBindings(messageId, bindings.map((binding) => binding.token));
5219
+ }
5167
5220
  }
5168
5221
  }
5169
5222
 
@@ -5973,6 +6026,7 @@ interface UnifiedTaskListEntry {
5973
6026
  status: string;
5974
6027
  active: boolean;
5975
6028
  current: boolean;
6029
+ lastUserPrompt?: string;
5976
6030
  updatedAt: number;
5977
6031
  updatedLabel: string;
5978
6032
  }
@@ -5982,26 +6036,28 @@ interface TaskProjectInfo {
5982
6036
  title: string;
5983
6037
  }
5984
6038
 
5985
- function orderTaskListByProject(entries: UnifiedTaskListEntry[]): UnifiedTaskListEntry[] {
5986
- const groups = new Map<string, { project: TaskProjectInfo; entries: UnifiedTaskListEntry[] }>();
5987
- for (const entry of entries) {
5988
- const project = taskProjectInfo(entry);
5989
- const group = groups.get(project.key);
5990
- if (group) group.entries.push(entry);
5991
- else groups.set(project.key, { project, entries: [entry] });
5992
- }
5993
- const orderedGroups = [...groups.values()]
5994
- .map((group) => ({
5995
- ...group,
5996
- current: group.entries.some((entry) => entry.current),
5997
- active: group.entries.some((entry) => entry.active),
5998
- updatedAt: Math.max(...group.entries.map((entry) => entry.updatedAt)),
5999
- }))
6000
- .sort((left, right) => Number(right.current) - Number(left.current)
6001
- || Number(right.active) - Number(left.active)
6002
- || right.updatedAt - left.updatedAt
6003
- || left.project.title.localeCompare(right.project.title, undefined, { sensitivity: "base" }));
6004
- return orderedGroups.flatMap((group) => group.entries.sort(compareTaskListEntries));
6039
+ function orderTaskListByRecency(entries: UnifiedTaskListEntry[]): UnifiedTaskListEntry[] {
6040
+ return [...entries].sort(compareTaskListEntries);
6041
+ }
6042
+
6043
+ function groupTaskListPagesByProject(
6044
+ entries: UnifiedTaskListEntry[],
6045
+ pageSize: number,
6046
+ ): UnifiedTaskListEntry[] {
6047
+ const result: UnifiedTaskListEntry[] = [];
6048
+ for (let offset = 0; offset < entries.length; offset += pageSize) {
6049
+ const groups = new Map<string, UnifiedTaskListEntry[]>();
6050
+ for (const entry of entries.slice(offset, offset + pageSize)) {
6051
+ const key = taskProjectInfo(entry).key;
6052
+ const group = groups.get(key);
6053
+ if (group) group.push(entry);
6054
+ else groups.set(key, [entry]);
6055
+ }
6056
+ for (const group of groups.values()) {
6057
+ result.push(...group.sort(compareTaskListEntries));
6058
+ }
6059
+ }
6060
+ return result;
6005
6061
  }
6006
6062
 
6007
6063
  function compareTaskListEntries(left: UnifiedTaskListEntry, right: UnifiedTaskListEntry): number {
@@ -6030,6 +6086,8 @@ function mergeTaskList(
6030
6086
  localSessions: SessionRecord[],
6031
6087
  remoteSessions: AgentRemoteSessionSummary[],
6032
6088
  currentLocalSessionId?: string,
6089
+ latestLocalUserPrompts: ReadonlyMap<string, string> = new Map(),
6090
+ latestRemoteUserPrompts: ReadonlyMap<string, string> = new Map(),
6033
6091
  ): UnifiedTaskListEntry[] {
6034
6092
  const localByRemoteId = new Map(
6035
6093
  localSessions
@@ -6054,6 +6112,9 @@ function mergeTaskList(
6054
6112
  status,
6055
6113
  active,
6056
6114
  current: Boolean(local && local.localSessionId === currentLocalSessionId),
6115
+ lastUserPrompt: remote.lastUserPrompt
6116
+ ?? (local ? latestLocalUserPrompts.get(local.localSessionId) : undefined)
6117
+ ?? latestRemoteUserPrompts.get(agentRemoteKey(agentName, remote.id)),
6057
6118
  updatedAt: (recencyAt ?? 0) * 1_000,
6058
6119
  updatedLabel: formatRemoteTime(recencyAt),
6059
6120
  };
@@ -6073,6 +6134,7 @@ function mergeTaskList(
6073
6134
  status: sessionStatusLabel(local.status),
6074
6135
  active: local.status === "running",
6075
6136
  current: local.localSessionId === currentLocalSessionId,
6137
+ lastUserPrompt: latestLocalUserPrompts.get(local.localSessionId),
6076
6138
  updatedAt: Number.isNaN(updatedAt) ? 0 : updatedAt,
6077
6139
  updatedLabel: formatStatusTime(local.updatedAt),
6078
6140
  });
@@ -6102,6 +6164,20 @@ function parseRemoteSessionReference(reference: string): { agentName: string; re
6102
6164
  }
6103
6165
  }
6104
6166
 
6167
+ function bindSessionCardAction(
6168
+ action: TaskListCardAction,
6169
+ bindings: Map<string, CardActionBinding>,
6170
+ ): TaskListCardAction {
6171
+ const serialized = JSON.stringify(action.value);
6172
+ const token = createHash("sha256").update(serialized).digest("hex").slice(0, 16);
6173
+ const existing = bindings.get(token);
6174
+ if (existing && JSON.stringify(existing.value) !== serialized) {
6175
+ throw new Error("任务列表卡片操作令牌发生冲突,请重试。");
6176
+ }
6177
+ bindings.set(token, { token, value: action.value });
6178
+ return { ...action, value: { t: token } };
6179
+ }
6180
+
6105
6181
  function runtimeErrorMessage(error: unknown): string {
6106
6182
  return error instanceof Error ? error.message : String(error);
6107
6183
  }
@@ -6124,6 +6200,22 @@ function turnViewSnapshot(value: unknown): TurnViewState | undefined {
6124
6200
  : undefined;
6125
6201
  }
6126
6202
 
6203
+ function latestUserPromptFromTurnView(snapshot: TurnViewState | undefined): string | undefined {
6204
+ const latestUserActivity = [...(snapshot?.activities ?? [])]
6205
+ .reverse()
6206
+ .find((activity): activity is Extract<TurnActivity, { kind: "user" }> =>
6207
+ activity.kind === "user" && Boolean(activity.text.trim()));
6208
+ return latestUserActivity?.text.trim() || snapshot?.prompt?.trim() || undefined;
6209
+ }
6210
+
6211
+ function sessionPromptPreview(prompt: string | undefined): string {
6212
+ const normalized = prompt?.replace(/\s+/gu, " ").trim() || "暂无用户 Prompt";
6213
+ const characters = Array.from(normalized);
6214
+ return characters.length > SESSION_PROMPT_PREVIEW_LENGTH
6215
+ ? `${characters.slice(0, SESSION_PROMPT_PREVIEW_LENGTH).join("")}...`
6216
+ : normalized;
6217
+ }
6218
+
6127
6219
  function isTerminalTurnViewStatus(status: TurnViewState["status"]): boolean {
6128
6220
  return status === "completed" || status === "cancelled" || status === "failed";
6129
6221
  }
@@ -123,6 +123,7 @@ export interface RemoteSessionSummary {
123
123
  lastTurnId?: string;
124
124
  lastCompletedTurnId?: string;
125
125
  lastTurnStatus?: "completed" | "interrupted" | "failed" | "inProgress";
126
+ lastUserPrompt?: string;
126
127
  lastActivity?: string;
127
128
  finalResponse?: string;
128
129
  lastError?: string;
@@ -75,6 +75,11 @@ export interface GoalCardDeliveryRecord {
75
75
  updatedAt: string;
76
76
  }
77
77
 
78
+ export interface CardActionBinding {
79
+ token: string;
80
+ value: Record<string, string>;
81
+ }
82
+
78
83
  export interface TurnRuntimeOriginRecord {
79
84
  turnId: string;
80
85
  localSessionId: string;
@@ -289,6 +294,52 @@ export class StateStore {
289
294
  return rows.map(goalCardDeliveryFromRow);
290
295
  }
291
296
 
297
+ upsertCardActionBindings(messageId: string, bindings: CardActionBinding[]): void {
298
+ if (bindings.length === 0) return;
299
+ const createdAt = new Date().toISOString();
300
+ const upsert = this.db.prepare(`
301
+ INSERT INTO card_action_bindings (message_id, token, value_json, created_at)
302
+ VALUES (?, ?, ?, ?)
303
+ ON CONFLICT(message_id, token) DO UPDATE SET
304
+ value_json = excluded.value_json,
305
+ created_at = excluded.created_at
306
+ `);
307
+ this.db.transaction(() => {
308
+ for (const binding of bindings) {
309
+ upsert.run(messageId, binding.token, JSON.stringify(binding.value), createdAt);
310
+ }
311
+ })();
312
+ }
313
+
314
+ retainCardActionBindings(messageId: string, tokens: string[]): void {
315
+ if (tokens.length === 0) {
316
+ this.db.prepare("DELETE FROM card_action_bindings WHERE message_id = ?").run(messageId);
317
+ return;
318
+ }
319
+ const placeholders = tokens.map(() => "?").join(", ");
320
+ this.db.prepare(`
321
+ DELETE FROM card_action_bindings
322
+ WHERE message_id = ? AND token NOT IN (${placeholders})
323
+ `).run(messageId, ...tokens);
324
+ }
325
+
326
+ getCardActionBinding(messageId: string, token: string): Record<string, unknown> | undefined {
327
+ const row = this.db.prepare(`
328
+ SELECT value_json
329
+ FROM card_action_bindings
330
+ WHERE message_id = ? AND token = ?
331
+ `).get(messageId, token) as { value_json: string } | undefined;
332
+ if (!row) return undefined;
333
+ try {
334
+ const value = JSON.parse(row.value_json) as unknown;
335
+ return value && typeof value === "object" && !Array.isArray(value)
336
+ ? value as Record<string, unknown>
337
+ : undefined;
338
+ } catch {
339
+ return undefined;
340
+ }
341
+ }
342
+
292
343
  close(): void {
293
344
  this.db.close();
294
345
  }
@@ -184,4 +184,15 @@ export const migrations = [
184
184
  CREATE INDEX IF NOT EXISTS idx_goal_card_deliveries_session
185
185
  ON goal_card_deliveries(local_session_id);
186
186
  `,
187
+ `
188
+ CREATE TABLE IF NOT EXISTS card_action_bindings (
189
+ message_id TEXT NOT NULL,
190
+ token TEXT NOT NULL,
191
+ value_json TEXT NOT NULL,
192
+ created_at TEXT NOT NULL,
193
+ PRIMARY KEY (message_id, token)
194
+ );
195
+ CREATE INDEX IF NOT EXISTS idx_card_action_bindings_created
196
+ ON card_action_bindings(created_at);
197
+ `,
187
198
  ] as const;