@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.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/codex/CodexRuntime.js +19 -0
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/feishu/CardRenderer.js +70 -14
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +4 -1
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +208 -57
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +46 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +11 -0
- package/dist/state/migrations.js.map +1 -1
- package/docs/technical-reference.md +2 -2
- package/docs/technical-reference.zh.md +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/codex/CodexRuntime.ts +22 -0
- package/src/feishu/CardRenderer.ts +79 -11
- package/src/feishu/FeishuConnector.ts +4 -1
- package/src/proxy/ProxySessionController.ts +228 -56
- package/src/runtime/types.ts +1 -0
- package/src/state/StateStore.ts +51 -0
- package/src/state/migrations.ts +11 -0
|
@@ -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 =
|
|
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
|
-
|
|
753
|
-
|
|
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
|
-
">
|
|
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
|
|
4957
|
-
|
|
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}
|
|
5152
|
+
summary: `${offset + index + 1}. ${marker}${title} · ${entry.agentName}`,
|
|
5061
5153
|
detailLines: [
|
|
5062
|
-
|
|
5063
|
-
|
|
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
|
-
">
|
|
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
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
const
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
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
|
|
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: "
|
|
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: "
|
|
6610
|
+
text: "NewGroupTask",
|
|
6439
6611
|
value: {
|
|
6440
6612
|
action: "directory_new_group",
|
|
6441
6613
|
directory,
|
package/src/runtime/types.ts
CHANGED
|
@@ -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;
|
package/src/state/StateStore.ts
CHANGED
|
@@ -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
|
}
|
package/src/state/migrations.ts
CHANGED
|
@@ -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;
|