@keyou007/agent-bot 0.1.15-alpha.1 → 0.1.15-alpha.3

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;
@@ -821,6 +833,30 @@ export class ProxySessionController {
821
833
  const filePath = directoryFileActionPath(scopedAction.value.filePath);
822
834
  await this.assertSendableFile(filePath);
823
835
  await this.outbound.sendFile(contextKey, filePath);
836
+ } else if (kind === "directory_new_folder_prompt") {
837
+ const directory = directoryActionPath(scopedAction.value.directory);
838
+ await this.assertBrowsableDirectory(directory);
839
+ await this.outbound.updateInteractiveCard(
840
+ contextKey,
841
+ requiredCardMessageId(scopedAction.messageId),
842
+ this.cardRenderer.renderDirectoryNewFolderCard({
843
+ directory,
844
+ displayDirectory: abbreviateHomeDirectory(directory),
845
+ contextKey,
846
+ page: directoryPageValue(scopedAction.value.page),
847
+ }),
848
+ );
849
+ } else if (kind === "directory_new_folder_submit") {
850
+ await this.createDirectoryFromCard(contextKey, scopedAction);
851
+ } else if (kind === "directory_new_folder_cancel") {
852
+ await this.openDirectoryBrowser(
853
+ contextKey,
854
+ directoryActionPath(scopedAction.value.directory),
855
+ {
856
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
857
+ page: directoryPageValue(scopedAction.value.page),
858
+ },
859
+ );
824
860
  } else if (kind === "directory_new" || kind === "directory_new_group") {
825
861
  const directory = directoryActionPath(scopedAction.value.directory);
826
862
  await this.assertBrowsableDirectory(directory);
@@ -1639,6 +1675,16 @@ export class ProxySessionController {
1639
1675
  : action.contextKey;
1640
1676
  }
1641
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
+
1642
1688
  private async execute(
1643
1689
  contextKey: string,
1644
1690
  command: Command,
@@ -4842,12 +4888,12 @@ export class ProxySessionController {
4842
4888
  const card = this.cardRenderer.renderDirectoryBrowserCard({
4843
4889
  directory: abbreviateHomeDirectory(directory),
4844
4890
  entries: visibleEntries,
4845
- currentActions: directoryCreationActions(directory, contextKey),
4891
+ currentActions: directoryCreationActions(directory, contextKey, page),
4846
4892
  navigationActions,
4847
4893
  footerLines: [
4848
4894
  `第 ${page + 1}/${totalPages} 页 · ${directoryCount} 个目录 · ${entries.length - directoryCount} 个文件`,
4849
4895
  "",
4850
- "> 点击目录名称进入,点击文件名称发送到当前会话;卡片顶部的 **New** 和 **NewGroup** 使用当前目录。",
4896
+ "> 点击目录名称进入,点击文件名称发送到当前会话;卡片顶部可新建目录、任务或群聊。",
4851
4897
  ],
4852
4898
  });
4853
4899
  if (options.updateMessageId) {
@@ -4913,6 +4959,27 @@ export class ProxySessionController {
4913
4959
  if (!stats.isDirectory()) throw new Error(`这不是目录:${directory}`);
4914
4960
  }
4915
4961
 
4962
+ private async createDirectoryFromCard(contextKey: string, action: CardAction): Promise<void> {
4963
+ const directory = directoryActionPath(action.value.directory);
4964
+ await this.assertBrowsableDirectory(directory);
4965
+ const folderName = directoryFolderName(cardFormValue(action.value, "folderName"));
4966
+ const newDirectory = path.join(directory, folderName);
4967
+ try {
4968
+ await fs.mkdir(newDirectory);
4969
+ } catch (error) {
4970
+ const code = typeof error === "object" && error !== null && "code" in error
4971
+ ? String(error.code)
4972
+ : undefined;
4973
+ if (code === "EEXIST") throw new Error(`目录已存在:${newDirectory}`);
4974
+ throw new Error(`无法创建目录 ${newDirectory}:${runtimeErrorMessage(error)}`);
4975
+ }
4976
+ await this.openDirectoryBrowser(contextKey, directory, {
4977
+ updateMessageId: requiredCardMessageId(action.messageId),
4978
+ page: directoryPageValue(action.value.page),
4979
+ });
4980
+ await this.outbound.sendText(contextKey, `已创建目录:${newDirectory}`);
4981
+ }
4982
+
4916
4983
  private async assertSendableFile(filePath: string): Promise<void> {
4917
4984
  let stats;
4918
4985
  try {
@@ -4952,13 +5019,34 @@ export class ProxySessionController {
4952
5019
  const remoteHint = remoteErrors.length > 0
4953
5020
  ? `部分 Agent 的任务读取失败:${remoteErrors.join(";")}`
4954
5021
  : undefined;
4955
-
4956
- const entries = orderTaskListByProject(
4957
- 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
+ )
4958
5045
  .filter((entry) => !normalizedSearch
4959
5046
  || [entry.id, entry.title, entry.cwd, entry.agentName]
4960
5047
  .some((value) => value.toLowerCase().includes(normalizedSearch))),
4961
5048
  );
5049
+ const entries = groupTaskListPagesByProject(globallyOrderedEntries, SESSION_PAGE_SIZE);
4962
5050
  const activeCount = entries.filter((entry) => entry.active).length;
4963
5051
  const lastKnownPage = Math.max(0, Math.ceil(entries.length / SESSION_PAGE_SIZE) - 1);
4964
5052
  const page = remoteHasMore ? requestedPage : Math.min(requestedPage, lastKnownPage);
@@ -4972,6 +5060,9 @@ export class ProxySessionController {
4972
5060
  const hasPrevious = page > 0;
4973
5061
  const hasNext = remoteHasMore || entries.length > offset + SESSION_PAGE_SIZE;
4974
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);
4975
5066
  const cardEntries = visibleEntries.map((entry, index): {
4976
5067
  project: TaskProjectInfo;
4977
5068
  projectActions: TaskListCardAction[];
@@ -4979,11 +5070,11 @@ export class ProxySessionController {
4979
5070
  } => {
4980
5071
  const project = taskProjectInfo(entry);
4981
5072
  const projectActionSource = projectActionSources.get(project.key) ?? entry;
4982
- const marker = entry.current ? "✅" : entry.active ? "🟢" : "•";
5073
+ const marker = entry.current ? "✅ " : entry.active ? "🟢 " : "";
4983
5074
  const showStop = entry.status === "外部执行中"
4984
5075
  && entry.reference !== options.forceSwitchTaskId
4985
5076
  && entry.id !== options.forceSwitchTaskId;
4986
- const actions: TaskListCardAction[] = entry.current ? [] : [{
5077
+ const actions: TaskListCardAction[] = entry.current ? [] : [bindCardAction({
4987
5078
  text: showStop ? "Stop" : "Switch",
4988
5079
  type: showStop ? "danger" as const : "default" as const,
4989
5080
  value: {
@@ -4993,8 +5084,8 @@ export class ProxySessionController {
4993
5084
  page: String(page),
4994
5085
  contextKey,
4995
5086
  },
4996
- }];
4997
- const projectActions: TaskListCardAction[] = [{
5087
+ })];
5088
+ const projectActions: TaskListCardAction[] = [bindCardAction({
4998
5089
  text: "New",
4999
5090
  value: {
5000
5091
  action: "session_new",
@@ -5003,7 +5094,7 @@ export class ProxySessionController {
5003
5094
  page: String(page),
5004
5095
  contextKey,
5005
5096
  },
5006
- }, {
5097
+ }), bindCardAction({
5007
5098
  text: "NewGroup",
5008
5099
  value: {
5009
5100
  action: "session_new_group",
@@ -5012,8 +5103,8 @@ export class ProxySessionController {
5012
5103
  page: String(page),
5013
5104
  contextKey,
5014
5105
  },
5015
- }];
5016
- actions.push({
5106
+ })];
5107
+ actions.push(bindCardAction({
5017
5108
  text: "Fork",
5018
5109
  value: {
5019
5110
  action: "session_fork",
@@ -5022,8 +5113,8 @@ export class ProxySessionController {
5022
5113
  page: String(page),
5023
5114
  contextKey,
5024
5115
  },
5025
- });
5026
- actions.push({
5116
+ }));
5117
+ actions.push(bindCardAction({
5027
5118
  text: "ForkGroup",
5028
5119
  value: {
5029
5120
  action: "session_fork_group",
@@ -5032,16 +5123,16 @@ export class ProxySessionController {
5032
5123
  page: String(page),
5033
5124
  contextKey,
5034
5125
  },
5035
- });
5036
- actions.push({
5126
+ }));
5127
+ actions.push(bindCardAction({
5037
5128
  text: "Status",
5038
5129
  value: {
5039
5130
  action: "session_status",
5040
5131
  sessionId: entry.reference,
5041
5132
  contextKey,
5042
5133
  },
5043
- });
5044
- actions.push({
5134
+ }));
5135
+ actions.push(bindCardAction({
5045
5136
  text: "Archive",
5046
5137
  value: {
5047
5138
  action: "session_archive",
@@ -5050,18 +5141,18 @@ export class ProxySessionController {
5050
5141
  page: String(page),
5051
5142
  contextKey,
5052
5143
  },
5053
- });
5144
+ }));
5054
5145
  const title = truncateText(entry.title.replace(/\s+/gu, " ").trim() || "未命名任务", 56);
5146
+ const lastUserPrompt = sessionPromptPreview(entry.lastUserPrompt);
5055
5147
  return {
5056
5148
  project,
5057
5149
  projectActions,
5058
5150
  cardEntry: {
5059
5151
  reference: entry.reference,
5060
- summary: `${offset + index + 1}. ${marker} ${title} · ${entry.agentName}`,
5152
+ summary: `${offset + index + 1}. ${marker}${title} · ${entry.agentName}`,
5061
5153
  detailLines: [
5062
- `**状态 / 更新时间**:${cardText(entry.status)} / ${cardText(entry.updatedLabel)}`,
5063
- `**Agent / 任务 ID**:${cardCode(entry.agentName)} / ${cardCode(entry.id)}`,
5064
- `**目录**:${cardCode(entry.cwd || "目录未知")}`,
5154
+ cardText(lastUserPrompt),
5155
+ `**更新时间**:${cardText(entry.updatedLabel)}`,
5065
5156
  ],
5066
5157
  actions,
5067
5158
  current: entry.current,
@@ -5091,11 +5182,11 @@ export class ProxySessionController {
5091
5182
  ...(remoteHint ? [remoteHint] : []),
5092
5183
  `第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
5093
5184
  "",
5094
- "> 项目行:**New** 新建任务,**NewGroup** 新建群。",
5185
+ "> 项目菜单:**New** 新建任务,**NewGroup** 新建群。",
5095
5186
  "> 任务详情:**Switch** 切换,**Stop** 停止,**Fork** / **ForkGroup** 创建分支,**Status** 查看状态,**Archive** 归档。",
5096
5187
  ],
5097
5188
  [
5098
- ...(hasPrevious ? [{
5189
+ ...(hasPrevious ? [bindCardAction({
5099
5190
  text: "Previous",
5100
5191
  value: {
5101
5192
  action: "session_page",
@@ -5103,8 +5194,8 @@ export class ProxySessionController {
5103
5194
  page: String(page - 1),
5104
5195
  contextKey,
5105
5196
  },
5106
- }] : []),
5107
- ...(hasNext ? [{
5197
+ })] : []),
5198
+ ...(hasNext ? [bindCardAction({
5108
5199
  text: "Next",
5109
5200
  value: {
5110
5201
  action: "session_page",
@@ -5112,13 +5203,20 @@ export class ProxySessionController {
5112
5203
  page: String(page + 1),
5113
5204
  contextKey,
5114
5205
  },
5115
- }] : []),
5206
+ })] : []),
5116
5207
  ],
5117
5208
  );
5209
+ const bindings = [...cardActionBindings.values()];
5118
5210
  if (options.updateMessageId) {
5211
+ this.store.upsertCardActionBindings(options.updateMessageId, bindings);
5119
5212
  await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
5213
+ this.store.retainCardActionBindings(options.updateMessageId, bindings.map((binding) => binding.token));
5120
5214
  } else {
5121
- 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
+ }
5122
5220
  }
5123
5221
  }
5124
5222
 
@@ -5928,6 +6026,7 @@ interface UnifiedTaskListEntry {
5928
6026
  status: string;
5929
6027
  active: boolean;
5930
6028
  current: boolean;
6029
+ lastUserPrompt?: string;
5931
6030
  updatedAt: number;
5932
6031
  updatedLabel: string;
5933
6032
  }
@@ -5937,26 +6036,28 @@ interface TaskProjectInfo {
5937
6036
  title: string;
5938
6037
  }
5939
6038
 
5940
- function orderTaskListByProject(entries: UnifiedTaskListEntry[]): UnifiedTaskListEntry[] {
5941
- const groups = new Map<string, { project: TaskProjectInfo; entries: UnifiedTaskListEntry[] }>();
5942
- for (const entry of entries) {
5943
- const project = taskProjectInfo(entry);
5944
- const group = groups.get(project.key);
5945
- if (group) group.entries.push(entry);
5946
- else groups.set(project.key, { project, entries: [entry] });
5947
- }
5948
- const orderedGroups = [...groups.values()]
5949
- .map((group) => ({
5950
- ...group,
5951
- current: group.entries.some((entry) => entry.current),
5952
- active: group.entries.some((entry) => entry.active),
5953
- updatedAt: Math.max(...group.entries.map((entry) => entry.updatedAt)),
5954
- }))
5955
- .sort((left, right) => Number(right.current) - Number(left.current)
5956
- || Number(right.active) - Number(left.active)
5957
- || right.updatedAt - left.updatedAt
5958
- || left.project.title.localeCompare(right.project.title, undefined, { sensitivity: "base" }));
5959
- 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;
5960
6061
  }
5961
6062
 
5962
6063
  function compareTaskListEntries(left: UnifiedTaskListEntry, right: UnifiedTaskListEntry): number {
@@ -5985,6 +6086,8 @@ function mergeTaskList(
5985
6086
  localSessions: SessionRecord[],
5986
6087
  remoteSessions: AgentRemoteSessionSummary[],
5987
6088
  currentLocalSessionId?: string,
6089
+ latestLocalUserPrompts: ReadonlyMap<string, string> = new Map(),
6090
+ latestRemoteUserPrompts: ReadonlyMap<string, string> = new Map(),
5988
6091
  ): UnifiedTaskListEntry[] {
5989
6092
  const localByRemoteId = new Map(
5990
6093
  localSessions
@@ -6009,6 +6112,9 @@ function mergeTaskList(
6009
6112
  status,
6010
6113
  active,
6011
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)),
6012
6118
  updatedAt: (recencyAt ?? 0) * 1_000,
6013
6119
  updatedLabel: formatRemoteTime(recencyAt),
6014
6120
  };
@@ -6028,6 +6134,7 @@ function mergeTaskList(
6028
6134
  status: sessionStatusLabel(local.status),
6029
6135
  active: local.status === "running",
6030
6136
  current: local.localSessionId === currentLocalSessionId,
6137
+ lastUserPrompt: latestLocalUserPrompts.get(local.localSessionId),
6031
6138
  updatedAt: Number.isNaN(updatedAt) ? 0 : updatedAt,
6032
6139
  updatedLabel: formatStatusTime(local.updatedAt),
6033
6140
  });
@@ -6057,6 +6164,20 @@ function parseRemoteSessionReference(reference: string): { agentName: string; re
6057
6164
  }
6058
6165
  }
6059
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
+
6060
6181
  function runtimeErrorMessage(error: unknown): string {
6061
6182
  return error instanceof Error ? error.message : String(error);
6062
6183
  }
@@ -6079,6 +6200,22 @@ function turnViewSnapshot(value: unknown): TurnViewState | undefined {
6079
6200
  : undefined;
6080
6201
  }
6081
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
+
6082
6219
  function isTerminalTurnViewStatus(status: TurnViewState["status"]): boolean {
6083
6220
  return status === "completed" || status === "cancelled" || status === "failed";
6084
6221
  }
@@ -6424,10 +6561,45 @@ function directoryFileActionPath(value: unknown): string {
6424
6561
  return path.resolve(value);
6425
6562
  }
6426
6563
 
6427
- function directoryCreationActions(directory: string, contextKey: string): TaskListCardAction[] {
6564
+ function cardFormValue(value: Record<string, unknown>, name: string): unknown {
6565
+ const formValue = value.formValue;
6566
+ return typeof formValue === "object" && formValue !== null
6567
+ ? (formValue as Record<string, unknown>)[name]
6568
+ : undefined;
6569
+ }
6570
+
6571
+ function directoryFolderName(value: unknown): string {
6572
+ if (typeof value !== "string" || !value.trim()) throw new Error("请输入目录名。");
6573
+ const name = value.trim();
6574
+ if (name === "." || name === "..") throw new Error("目录名不能是 . 或 ..。");
6575
+ if (name.length > 255) throw new Error("目录名不能超过 255 个字符。");
6576
+ if (/[<>:"/\\|?*\u0000-\u001F]/u.test(name)) {
6577
+ throw new Error("目录名不能包含路径分隔符或以下字符:< > : \" / \\ | ? *");
6578
+ }
6579
+ if (/[. ]$/u.test(name)) throw new Error("目录名不能以空格或句点结尾。");
6580
+ if (/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu.test(name)) {
6581
+ throw new Error("该名称是系统保留名称,请使用其他目录名。");
6582
+ }
6583
+ return name;
6584
+ }
6585
+
6586
+ function directoryCreationActions(
6587
+ directory: string,
6588
+ contextKey: string,
6589
+ page: number,
6590
+ ): TaskListCardAction[] {
6428
6591
  return [
6429
6592
  {
6430
- text: "New",
6593
+ text: "NewFolder",
6594
+ value: {
6595
+ action: "directory_new_folder_prompt",
6596
+ directory,
6597
+ contextKey,
6598
+ page: String(page),
6599
+ },
6600
+ },
6601
+ {
6602
+ text: "NewTask",
6431
6603
  value: {
6432
6604
  action: "directory_new",
6433
6605
  directory,
@@ -6435,7 +6607,7 @@ function directoryCreationActions(directory: string, contextKey: string): TaskLi
6435
6607
  },
6436
6608
  },
6437
6609
  {
6438
- text: "NewGroup",
6610
+ text: "NewGroupTask",
6439
6611
  value: {
6440
6612
  action: "directory_new_group",
6441
6613
  directory,
@@ -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;