@co0ontty/wand 4.83.1 → 4.84.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.
Files changed (188) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -1
  3. package/dist/ai-team-availability.d.ts +1 -1
  4. package/dist/ai-team-prompts.js +13 -4
  5. package/dist/ai-team-runner.d.ts +80 -5
  6. package/dist/ai-team-runner.js +332 -55
  7. package/dist/ai-team-types.d.ts +92 -5
  8. package/dist/ai-team-types.js +9 -2
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli-api.d.ts +1 -0
  11. package/dist/cli-api.js +3 -0
  12. package/dist/cli.js +18 -1
  13. package/dist/commit-task-archive.d.ts +3 -1
  14. package/dist/commit-task-archive.js +21 -10
  15. package/dist/config.d.ts +1 -1
  16. package/dist/config.js +73 -0
  17. package/dist/conversation-mentions.d.ts +16 -0
  18. package/dist/conversation-mentions.js +32 -0
  19. package/dist/conversation-service.d.ts +90 -0
  20. package/dist/conversation-service.js +759 -0
  21. package/dist/conversation-session-preview.d.ts +9 -0
  22. package/dist/conversation-session-preview.js +47 -0
  23. package/dist/conversation-task-preview.d.ts +5 -0
  24. package/dist/conversation-task-preview.js +8 -0
  25. package/dist/conversation-types.d.ts +80 -0
  26. package/dist/conversation-types.js +7 -0
  27. package/dist/core-extension-host.d.ts +27 -0
  28. package/dist/core-extension-host.js +138 -0
  29. package/dist/core-runner.d.ts +86 -0
  30. package/dist/core-runner.js +757 -0
  31. package/dist/core-status-cli.d.ts +9 -0
  32. package/dist/core-status-cli.js +280 -0
  33. package/dist/core-turn-tracker.d.ts +12 -0
  34. package/dist/core-turn-tracker.js +36 -0
  35. package/dist/decision-runner.d.ts +6 -0
  36. package/dist/decision-runner.js +3 -1
  37. package/dist/decision-tool.d.ts +2 -0
  38. package/dist/decision-tool.js +49 -22
  39. package/dist/env-utils.d.ts +7 -3
  40. package/dist/env-utils.js +30 -5
  41. package/dist/git-quick-commit.d.ts +5 -0
  42. package/dist/git-quick-commit.js +9 -3
  43. package/dist/git-utils.js +2 -1
  44. package/dist/harness-engine.d.ts +110 -0
  45. package/dist/harness-engine.js +331 -0
  46. package/dist/mission-diff.js +2 -1
  47. package/dist/model-auto-assign.d.ts +50 -0
  48. package/dist/model-auto-assign.js +163 -0
  49. package/dist/model-group-runner.d.ts +4 -0
  50. package/dist/model-group-runner.js +72 -0
  51. package/dist/model-groups.d.ts +38 -0
  52. package/dist/model-groups.js +138 -0
  53. package/dist/models.d.ts +12 -0
  54. package/dist/models.js +102 -8
  55. package/dist/npm-update-utils.js +2 -1
  56. package/dist/openrouter-free-cli.d.ts +25 -0
  57. package/dist/openrouter-free-cli.js +109 -0
  58. package/dist/openrouter-free-models.d.ts +85 -0
  59. package/dist/openrouter-free-models.js +458 -0
  60. package/dist/openrouter-free-selection.d.ts +5 -0
  61. package/dist/openrouter-free-selection.js +7 -0
  62. package/dist/path-repair.d.ts +10 -0
  63. package/dist/path-repair.js +50 -5
  64. package/dist/pi-auto-resources.d.ts +12 -0
  65. package/dist/pi-auto-resources.js +151 -0
  66. package/dist/pi-execution-types.d.ts +48 -0
  67. package/dist/pi-execution-types.js +1 -0
  68. package/dist/pi-execution.d.ts +9 -0
  69. package/dist/pi-execution.js +187 -0
  70. package/dist/pi-extension-resources.d.ts +13 -0
  71. package/dist/pi-extension-resources.js +71 -0
  72. package/dist/pi-openrouter-free-bridge.d.ts +2 -0
  73. package/dist/pi-openrouter-free-bridge.js +80 -0
  74. package/dist/pi-resource-bridge.d.ts +2 -0
  75. package/dist/pi-resource-bridge.js +73 -0
  76. package/dist/pi-resource-catalog.d.ts +18 -0
  77. package/dist/pi-resource-catalog.js +82 -0
  78. package/dist/pi-resource-recommendation.d.ts +29 -0
  79. package/dist/pi-resource-recommendation.js +169 -0
  80. package/dist/pi-resource-run.d.ts +8 -0
  81. package/dist/pi-resource-run.js +28 -0
  82. package/dist/pi-session-settings.d.ts +157 -0
  83. package/dist/pi-session-settings.js +166 -0
  84. package/dist/platform-action-catalog.d.ts +24 -0
  85. package/dist/platform-action-catalog.js +141 -0
  86. package/dist/platform-action-service.d.ts +37 -0
  87. package/dist/platform-action-service.js +138 -0
  88. package/dist/platform-action-types.d.ts +125 -0
  89. package/dist/platform-action-types.js +7 -0
  90. package/dist/process-manager.d.ts +21 -1
  91. package/dist/process-manager.js +168 -15
  92. package/dist/provider-catalog.js +1 -1
  93. package/dist/provider-cli-updater.js +29 -8
  94. package/dist/provider-history-scanner.d.ts +9 -0
  95. package/dist/provider-history-scanner.js +107 -1
  96. package/dist/retention.d.ts +19 -7
  97. package/dist/retention.js +171 -28
  98. package/dist/server-ai-team-routes.js +6 -2
  99. package/dist/server-conversation-routes.d.ts +5 -0
  100. package/dist/server-conversation-routes.js +91 -0
  101. package/dist/server-employee-routes.d.ts +2 -0
  102. package/dist/server-employee-routes.js +85 -11
  103. package/dist/server-pi-recommendation-routes.d.ts +13 -0
  104. package/dist/server-pi-recommendation-routes.js +67 -0
  105. package/dist/server-resume-routes.js +1 -1
  106. package/dist/server-session-routes.d.ts +4 -3
  107. package/dist/server-session-routes.js +182 -37
  108. package/dist/server-settings-routes.d.ts +5 -0
  109. package/dist/server-settings-routes.js +46 -0
  110. package/dist/server-task-routes.js +49 -4
  111. package/dist/server-team-dispatch-routes.d.ts +19 -0
  112. package/dist/server-team-dispatch-routes.js +71 -0
  113. package/dist/server-update-routes.d.ts +1 -1
  114. package/dist/server-update-routes.js +10 -5
  115. package/dist/server-workspace-routes.js +44 -24
  116. package/dist/server.d.ts +1 -0
  117. package/dist/server.js +148 -30
  118. package/dist/session-ai-context.d.ts +5 -1
  119. package/dist/session-ai-context.js +9 -4
  120. package/dist/session-registry.d.ts +4 -0
  121. package/dist/session-registry.js +47 -13
  122. package/dist/session-transport.js +8 -1
  123. package/dist/settings-web-access.d.ts +15 -0
  124. package/dist/settings-web-access.js +37 -0
  125. package/dist/silicon-employee-defaults.d.ts +29 -0
  126. package/dist/silicon-employee-defaults.js +41 -0
  127. package/dist/silicon-employee-dispatch.d.ts +5 -1
  128. package/dist/silicon-employee-dispatch.js +10 -4
  129. package/dist/silicon-employee-draft.d.ts +21 -4
  130. package/dist/silicon-employee-draft.js +103 -16
  131. package/dist/storage.d.ts +73 -2
  132. package/dist/storage.js +362 -26
  133. package/dist/structured-claude-protocol.js +1 -1
  134. package/dist/structured-client-protocol.js +11 -33
  135. package/dist/structured-content.d.ts +23 -0
  136. package/dist/structured-content.js +72 -0
  137. package/dist/structured-failure.d.ts +16 -0
  138. package/dist/structured-failure.js +36 -0
  139. package/dist/structured-pi-adapter.d.ts +8 -3
  140. package/dist/structured-pi-adapter.js +131 -30
  141. package/dist/structured-provider-common.d.ts +6 -0
  142. package/dist/structured-provider-common.js +23 -1
  143. package/dist/structured-runner.d.ts +45 -0
  144. package/dist/structured-session-manager.d.ts +78 -4
  145. package/dist/structured-session-manager.js +513 -129
  146. package/dist/structured-thinking.d.ts +15 -0
  147. package/dist/structured-thinking.js +46 -0
  148. package/dist/subagent-dispatch.d.ts +56 -0
  149. package/dist/subagent-dispatch.js +102 -0
  150. package/dist/system-employee.d.ts +4 -3
  151. package/dist/system-employee.js +7 -4
  152. package/dist/task-retention.d.ts +23 -0
  153. package/dist/task-retention.js +69 -0
  154. package/dist/task-types.d.ts +4 -0
  155. package/dist/task-types.js +3 -0
  156. package/dist/team-dispatch.d.ts +101 -0
  157. package/dist/team-dispatch.js +269 -0
  158. package/dist/tool-preview.js +7 -1
  159. package/dist/tui/commands.js +50 -1
  160. package/dist/tui/ipc-protocol.d.ts +1 -1
  161. package/dist/tui/ipc-server.d.ts +4 -0
  162. package/dist/tui/ipc-server.js +23 -0
  163. package/dist/turn-heartbeat.d.ts +35 -0
  164. package/dist/turn-heartbeat.js +64 -0
  165. package/dist/types.d.ts +147 -1
  166. package/dist/user-profile.d.ts +33 -0
  167. package/dist/user-profile.js +70 -0
  168. package/dist/web-ui/chat-message-meta.d.ts +11 -0
  169. package/dist/web-ui/chat-message-meta.js +47 -0
  170. package/dist/web-ui/content/ai-teams.js +13 -13
  171. package/dist/web-ui/content/scripts.js +795 -95
  172. package/dist/web-ui/content/styles.css +1 -1
  173. package/dist/web-ui/content/tailwind.css +1 -6808
  174. package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
  175. package/dist/web-ui/embedded-assets.d.ts +1 -1
  176. package/dist/web-ui/embedded-assets.js +5 -5
  177. package/dist/web-ui/index.d.ts +1 -1
  178. package/dist/web-ui/index.js +3 -3
  179. package/dist/web-ui/markdown.d.ts +2 -0
  180. package/dist/web-ui/markdown.js +111 -0
  181. package/dist/web-ui/page.d.ts +4 -0
  182. package/dist/web-ui/page.js +8 -0
  183. package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
  184. package/dist/web-ui/react/local-preview/controller.js +159 -0
  185. package/dist/web-ui/running-activity.d.ts +63 -0
  186. package/dist/web-ui/running-activity.js +134 -0
  187. package/dist/web-ui/styles.js +2 -11
  188. package/package.json +7 -2
@@ -0,0 +1,759 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { statSync, rmSync, existsSync, realpathSync } from "node:fs";
3
+ import { resolve, sep } from "node:path";
4
+ import { AI_TEAM_ACTIVE_RUN_STATUSES, AI_TEAM_DEFAULT_MAX_STEPS, AI_TEAM_MAX_MEMBERS, memberAgents } from "./ai-team-types.js";
5
+ import { freezeTeamEmployees, projectTeamEmployees, requireTeamEmployee } from "./ai-team-employee-binding.js";
6
+ import { AiTeamConflictError } from "./ai-team-runner.js";
7
+ import { CONVERSATION_OWNER, CONVERSATION_RELAY_PREFIX, employeeConversationId } from "./conversation-types.js";
8
+ import { conversationLeaderMention, conversationMessageBody } from "./conversation-mentions.js";
9
+ import { getDefaultModelForProvider } from "./config.js";
10
+ import { defaultModelGroupSelector } from "./model-groups.js";
11
+ import { getErrorMessage } from "./error-utils.js";
12
+ import { resolveSessionCwd } from "./session-cwd.js";
13
+ import { resolveWorkspaceIdForNewSession } from "./workspace-binding.js";
14
+ import { CONVERSATION_SESSION_PREFIX, conversationSessionPreview } from "./conversation-session-preview.js";
15
+ import { conversationTaskLiveText } from "./conversation-task-preview.js";
16
+ import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
17
+ import { userAuthor, userDisplayName } from "./user-profile.js";
18
+ import { cleanupWorktreeSync } from "./git-worktree.js";
19
+ import { provisionalTaskTitleFromDescription } from "./task-title.js";
20
+ const now = () => new Date().toISOString();
21
+ const keyOf = (member) => member.employeeId ??
22
+ `${member.legacyTemplateId ?? "legacy"}:${member.legacyMemberId ?? member.id}`;
23
+ function shortText(value, label, max, required = false) {
24
+ if (value !== undefined && typeof value !== "string")
25
+ throw new Error(`${label}必须是文字。`);
26
+ const text = value?.trim() ?? "";
27
+ if (text.length > max || (required && !text))
28
+ throw new Error(`${label}${required ? "不能为空且" : ""}不能超过 ${max} 个字符。`);
29
+ return text;
30
+ }
31
+ function ids(value) {
32
+ if (value === undefined)
33
+ return [];
34
+ if (!Array.isArray(value) || value.some((id) => typeof id !== "string" || !id.trim()))
35
+ throw new Error("成员 ID 列表无效。");
36
+ if (new Set(value).size !== value.length)
37
+ throw new Error("同一员工不能重复选择。");
38
+ return value;
39
+ }
40
+ function stableJson(value) {
41
+ if (Array.isArray(value))
42
+ return `[${value.map(stableJson).join(",")}]`;
43
+ if (value && typeof value === "object")
44
+ return `{${Object.entries(value).filter(([, v]) => v !== undefined)
45
+ .sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${stableJson(v)}`).join(",")}}`;
46
+ return JSON.stringify(value) ?? "null";
47
+ }
48
+ /** Business coordination only; all execution remains in StructuredSessionManager / AiTeamRunner. */
49
+ export class ConversationService {
50
+ deps;
51
+ constructor(deps) {
52
+ this.deps = deps;
53
+ }
54
+ removals = new Map();
55
+ pendingOperations = new Map();
56
+ previewTimers = new Map();
57
+ /** Reuse session events; send only a bounded IM projection, never a second execution protocol. */
58
+ ingestSessionEvent(event) {
59
+ const session = this.deps.structured.get(event.sessionId);
60
+ if (!this.deps.notifySession || !session?.automationId?.startsWith(CONVERSATION_SESSION_PREFIX)
61
+ || this.previewTimers.has(event.sessionId))
62
+ return;
63
+ const timer = setTimeout(() => {
64
+ this.previewTimers.delete(event.sessionId);
65
+ const current = this.snapshot(event.sessionId);
66
+ const requestId = current?.automationId?.slice(CONVERSATION_SESSION_PREFIX.length);
67
+ const receipt = requestId ? this.receipt(requestId) : null;
68
+ if (!receipt || receipt.sessionId !== event.sessionId || !this.storage.getConversation(receipt.conversationId)
69
+ || this.storage.conversationListState(receipt.conversationId).deleting)
70
+ return;
71
+ this.deps.notifySession?.({ conversationId: receipt.conversationId, sessionId: event.sessionId,
72
+ preview: conversationSessionPreview(current, receipt) });
73
+ }, 100);
74
+ timer.unref?.();
75
+ this.previewTimers.set(event.sessionId, timer);
76
+ }
77
+ dispose() {
78
+ for (const timer of this.previewTimers.values())
79
+ clearTimeout(timer);
80
+ this.previewTimers.clear();
81
+ }
82
+ get storage() { return this.deps.storage; }
83
+ snapshot(id) {
84
+ return id ? this.deps.structured.get(id) ?? this.storage.getSession(id) : null;
85
+ }
86
+ /** Explicit startup/event migration, never a GET repair and never an invented group. */
87
+ importRun(run) {
88
+ if (!run.chatSessionId || run.conversationId)
89
+ return;
90
+ let group = this.storage.getConversationBySession(run.chatSessionId);
91
+ if (!group) {
92
+ const team = JSON.parse(JSON.stringify(run.team));
93
+ for (const member of team.members) {
94
+ if (!member.employeeId) {
95
+ member.legacyTemplateId = run.teamId;
96
+ member.legacyMemberId = member.id;
97
+ }
98
+ }
99
+ group = {
100
+ id: `group_${run.chatSessionId}`, owner: CONVERSATION_OWNER, kind: "group", peerEmployeeId: null,
101
+ name: team.name, nameSource: "auto", sourceTemplateId: run.teamId, team,
102
+ memberVersion: 1, joinedVersions: Object.fromEntries(team.members.map((m) => [keyOf(m), 1])),
103
+ sessionId: run.chatSessionId, communicationSessionId: null, createdAt: run.createdAt, updatedAt: run.updatedAt,
104
+ };
105
+ }
106
+ this.storage.transaction(() => {
107
+ this.storage.saveConversation(group);
108
+ if (this.storage.getWandTask(run.taskId))
109
+ this.storage.linkConversationTask(group.id, run.taskId, run.createdAt);
110
+ // Only additive identity metadata; preserve team, messages, timestamps and ownership.
111
+ this.storage.saveAiTeamRun({ ...run, conversationId: group.id, memberVersion: group.memberVersion,
112
+ roundNumber: this.storage.listAiTeamRuns({ taskId: run.taskId }).filter((r) => r.createdAt <= run.createdAt).length });
113
+ });
114
+ }
115
+ migrateExistingChats() {
116
+ for (const run of this.storage.listAiTeamRuns().reverse())
117
+ this.importRun(run);
118
+ }
119
+ pendingEmployee(employeeId) {
120
+ const employee = this.storage.getSiliconEmployee(employeeId);
121
+ return { id: employeeConversationId(employeeId), owner: CONVERSATION_OWNER, kind: "dm", peerEmployeeId: employeeId,
122
+ name: employee?.name ?? "原对话暂不可用", nameSource: "auto", sourceTemplateId: null, team: null,
123
+ memberVersion: 1, joinedVersions: {}, sessionId: null, communicationSessionId: null, createdAt: "", updatedAt: "" };
124
+ }
125
+ requireInstance(id) {
126
+ const found = this.storage.getConversation(id);
127
+ if (found)
128
+ return found;
129
+ if (id.startsWith("dm_e_"))
130
+ return this.pendingEmployee(id.slice(3));
131
+ throw new Error("对话不存在。");
132
+ }
133
+ /** 会话里「我自己」的署名与头像:跟随当前设置,改设置不改写历史回合。 */
134
+ selfAuthor() {
135
+ return userAuthor(this.deps.config.userProfile);
136
+ }
137
+ selfName() {
138
+ return userDisplayName(this.deps.config.userProfile);
139
+ }
140
+ memberReasons(instance) {
141
+ const reasons = {};
142
+ for (const member of instance.team?.members ?? []) {
143
+ if (!member.employeeId)
144
+ continue;
145
+ try {
146
+ requireTeamEmployee(this.storage, member.employeeId);
147
+ }
148
+ catch (error) {
149
+ reasons[member.id] = getErrorMessage(error);
150
+ }
151
+ }
152
+ return reasons;
153
+ }
154
+ turns(instance) {
155
+ const relay = this.snapshot(instance.sessionId)?.messages ?? [];
156
+ const channelIds = [...(instance.historySessionIds ?? []), instance.communicationSessionId]
157
+ .filter((id) => !!id && (instance.kind === "dm" || id !== instance.sessionId));
158
+ const communication = [...new Set(channelIds)].flatMap((id) => {
159
+ const channel = this.snapshot(id);
160
+ return (channel?.messages ?? []).filter((turn) => turn.role === "assistant").map((turn, index) => ({
161
+ ...turn, messageId: turn.messageId ?? `${id}:${index}`, conversationId: instance.id, conversationTarget: null,
162
+ ...(turn.role === "assistant" && !turn.author ? { author: {
163
+ id: channel?.employeeId ?? instance.team?.members.find((m) => m.isLeader)?.id ?? "leader",
164
+ name: channel?.employeeName ?? instance.team?.members.find((m) => m.isLeader)?.name ?? instance.name,
165
+ avatar: channel?.employeeAvatar, sessionId: id,
166
+ leader: instance.kind === "group" || undefined,
167
+ } } : {}),
168
+ }));
169
+ });
170
+ const base = instance.kind === "dm" ? [] : relay.map((turn, index) => ({ ...turn,
171
+ messageId: turn.messageId ?? `${instance.sessionId}:${index}`, conversationId: instance.id }));
172
+ // 用户发言的署名是当前资料的投影,不回写存储:改设置后历史消息跟着换名字。
173
+ const withSelf = (turn) => turn.role === "user" && !turn.author ? { ...turn, author: this.selfAuthor() } : turn;
174
+ return [...base, ...this.storage.conversationEvents(instance.id), ...communication].map(withSelf)
175
+ .sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? "") || (instance.kind === "dm" ? 0 : (a.role === "user" ? -1 : 0) - (b.role === "user" ? -1 : 0)));
176
+ }
177
+ summary(instance) {
178
+ const tasks = this.storage.conversationTasks(instance.id).flatMap((link) => {
179
+ const task = this.storage.getWandTask(link.taskId);
180
+ const receipt = task ? this.storage.conversationTaskStartup(task.id) : null;
181
+ return task ? [{ task, linkedAt: link.linkedAt,
182
+ runs: this.storage.listAiTeamRuns({ taskId: task.id }).filter((run) => run.conversationId === instance.id || (!!instance.sessionId && run.chatSessionId === instance.sessionId)),
183
+ ...(receipt?.startup ? { startup: { state: receipt.startup, error: receipt.error } } : {}) }] : [];
184
+ });
185
+ const employee = instance.peerEmployeeId ? this.storage.getSiliconEmployee(instance.peerEmployeeId) : null;
186
+ const lifecycle = this.storage.conversationListState(instance.id);
187
+ let unavailableReason = lifecycle.deleting ? "对话正在删除,请稍后重试。" : lifecycle.dissolvedAt ? "群聊已解散,恢复后可以继续聊天。" : null;
188
+ if (instance.kind === "dm") {
189
+ try {
190
+ requireTeamEmployee(this.storage, instance.peerEmployeeId ?? "");
191
+ }
192
+ catch (error) {
193
+ unavailableReason = getErrorMessage(error);
194
+ }
195
+ }
196
+ const messages = this.turns(instance);
197
+ const last = messages.at(-1);
198
+ const lastText = last?.sessionLink ? conversationSessionPreview(this.snapshot(last.sessionLink.sessionId), last.requestId ? this.receipt(last.requestId) ?? undefined : undefined).text
199
+ : last?.content.map((b) => b.type === "text" ? b.text : "").join(" ") ?? "";
200
+ const messageAt = [instance.updatedAt, last?.createdAt ?? "", ...tasks.flatMap((t) => t.runs.map((r) => r.updatedAt))].sort().at(-1) ?? "";
201
+ const title = instance.kind === "dm" ? employee?.name ?? instance.name
202
+ : instance.nameSource === "custom" ? instance.name : tasks.at(-1)?.task.title ?? instance.name;
203
+ return { ...instance, ...this.storage.conversationListState(instance.id), team: instance.team ? projectTeamEmployees(instance.team, this.storage) : null,
204
+ title, unavailableReason, memberUnavailableReasons: this.memberReasons(instance), tasks,
205
+ preview: last ? `${last.role === "user" ? this.selfName() : last.author?.name ?? "系统"}:${lastText.slice(0, 160)}`
206
+ : instance.kind === "group" ? "群已建立,尚未派任务" : "还没有消息", messageAt };
207
+ }
208
+ list() {
209
+ return this.storage.listConversations().map((instance) => this.summary(instance))
210
+ .sort((a, b) => Number(!!b.pinnedAt) - Number(!!a.pinnedAt) || (b.pinnedAt ?? "").localeCompare(a.pinnedAt ?? "") || b.messageAt.localeCompare(a.messageAt) || a.id.localeCompare(b.id));
211
+ }
212
+ updateListState(id, patch) {
213
+ const instance = this.requireInstance(id);
214
+ if (!this.storage.getConversation(id))
215
+ throw new Error("对话尚未创建。");
216
+ if (this.storage.conversationListState(id).deleting)
217
+ throw new AiTeamConflictError("对话正在删除。");
218
+ if (patch.dissolved !== undefined && instance.kind !== "group")
219
+ throw new Error("只有群聊可以解散或恢复。");
220
+ this.storage.updateConversationListState(id, { ...patch, dissolvedBy: this.selfName() });
221
+ return this.summary(instance);
222
+ }
223
+ /** Delete only this conversation's owned channels and, for groups, its linked task resources. */
224
+ remove(id) {
225
+ const previous = this.removals.get(id);
226
+ if (previous)
227
+ return previous;
228
+ const removal = this.removeOwnedResources(id);
229
+ this.removals.set(id, removal);
230
+ void removal.finally(() => { if (this.removals.get(id) === removal)
231
+ this.removals.delete(id); }).catch(() => { });
232
+ return removal;
233
+ }
234
+ async removeOwnedResources(id) {
235
+ let instance = this.storage.getConversation(id);
236
+ if (!instance)
237
+ return;
238
+ if (!this.deps.deleteSession)
239
+ throw new Error("当前服务未启用会话删除。");
240
+ this.storage.updateConversationListState(id, { deleting: true });
241
+ await Promise.allSettled([...(this.pendingOperations.get(id) ?? [])]);
242
+ // Pending prepare/send operations may have attached a channel while deletion waited.
243
+ instance = this.storage.getConversation(id);
244
+ if (!instance)
245
+ return;
246
+ const taskIds = instance.kind === "group" ? this.storage.conversationTasks(id).map(link => link.taskId) : [];
247
+ const runs = taskIds.flatMap(taskId => this.storage.listAiTeamRuns({ taskId }));
248
+ const sessionIds = new Set([instance.sessionId, instance.communicationSessionId, ...(instance.historySessionIds ?? []),
249
+ ...this.storage.conversationSessionIds(id),
250
+ ...taskIds.flatMap(taskId => this.storage.listWandTaskSessionIds(taskId)),
251
+ ...runs.flatMap(run => [run.chatSessionId, ...this.storage.listAiTeamSteps(run.id).map(step => step.sessionId)])].filter((value) => !!value));
252
+ const worktrees = taskIds.flatMap(taskId => {
253
+ const task = this.storage.getWandTask(taskId);
254
+ const tree = task?.workspaceTaskId ? this.storage.getWorkspaceTask(task.workspaceTaskId)?.worktree : null;
255
+ return tree ? [tree] : [];
256
+ });
257
+ for (const tree of worktrees) {
258
+ if (this.storage.loadSessionsSlim().some(session => !sessionIds.has(session.id) && (resolve(session.cwd) === resolve(tree.path) || resolve(session.cwd).startsWith(resolve(tree.path) + sep)))) {
259
+ this.storage.updateConversationListState(id, { deleting: false });
260
+ throw new AiTeamConflictError("关联工作目录仍被其他会话使用,请先移出这些会话。");
261
+ }
262
+ }
263
+ for (const run of runs) {
264
+ if (AI_TEAM_ACTIVE_RUN_STATUSES.includes(this.storage.getAiTeamRun(run.id)?.status ?? "done"))
265
+ await this.deps.runner.stop(run.id);
266
+ }
267
+ // A queued startup may have completed while stop waited on the run's serial owner.
268
+ for (const run of runs)
269
+ for (const step of this.storage.listAiTeamSteps(run.id))
270
+ if (step.sessionId)
271
+ sessionIds.add(step.sessionId);
272
+ for (const taskId of taskIds)
273
+ for (const sessionId of this.storage.listWandTaskSessionIds(taskId))
274
+ sessionIds.add(sessionId);
275
+ for (const sessionId of sessionIds)
276
+ this.deps.deleteSession(sessionId);
277
+ for (const run of runs) {
278
+ // Exact run-owned directory only; never follow a replaced report-root symlink.
279
+ if (!/^run_[a-zA-Z0-9_-]+$/.test(run.id))
280
+ throw new Error("运行资源路径无效。");
281
+ const root = resolve(run.cwd, ".wand-team"), directory = resolve(root, run.id);
282
+ if (existsSync(directory)) {
283
+ if (!realpathSync(directory).startsWith(realpathSync(run.cwd) + sep) || realpathSync(root) !== resolve(realpathSync(run.cwd), ".wand-team") || realpathSync(directory) !== resolve(realpathSync(root), run.id))
284
+ throw new Error("运行资源目录已改变,请核对后重试删除。");
285
+ rmSync(directory, { recursive: true, force: true });
286
+ }
287
+ this.storage.deleteAiTeamRun(run.id);
288
+ }
289
+ for (const tree of worktrees)
290
+ cleanupWorktreeSync(tree);
291
+ for (const taskId of taskIds)
292
+ this.storage.deleteWandTask(taskId);
293
+ this.storage.deleteConversation(id);
294
+ }
295
+ history(id) { return this.turns(this.requireInstance(id)); }
296
+ detail(id) {
297
+ const instance = this.requireInstance(id);
298
+ const summary = this.summary(instance);
299
+ return { ...summary, messages: this.turns(instance).map(turn => turn.sessionLink
300
+ ? { ...turn, sessionPreview: conversationSessionPreview(this.snapshot(turn.sessionLink.sessionId), turn.requestId ? this.receipt(turn.requestId) ?? undefined : undefined),
301
+ // Older clients can still open the exact source session without understanding sessionLink.
302
+ author: turn.author ? { ...turn.author, sessionId: turn.sessionLink.sessionId } : turn.author,
303
+ content: turn.content.length ? turn.content : [{ type: "text", text: "消息已接收,打开对应会话查看实时回复。" }] }
304
+ : this.taskPreview(turn)),
305
+ runDetails: summary.tasks.flatMap((task) => task.runs.map((run) => this.deps.runner.detail(run.id))) };
306
+ }
307
+ /** Read-only, bounded projection of the exact accepted run. Never follow a group's newest task. */
308
+ taskPreview(turn) {
309
+ const link = turn.conversationLink;
310
+ if (!link)
311
+ return turn;
312
+ const group = this.storage.getConversation(link.conversationId);
313
+ const task = this.storage.getWandTask(link.taskId);
314
+ if (!group || !task || this.storage.conversationListState(group.id).deleting
315
+ || !this.storage.conversationTasks(group.id).some(item => item.taskId === task.id)) {
316
+ return { ...turn, taskPreview: { runId: null, status: "unavailable", text: "任务群或任务已删除,原消息保留。" } };
317
+ }
318
+ const receipt = turn.requestId ? this.receipt(turn.requestId) : null;
319
+ const run = receipt?.runId ? this.storage.getAiTeamRun(receipt.runId) : null;
320
+ const exact = run?.taskId === task.id && run.conversationId === group.id ? run : null;
321
+ const status = receipt?.startup === "failed" ? "failed" : exact?.status ?? "starting";
322
+ const live = exact?.status === "running" ? conversationTaskLiveText(this.deps.runner.live(exact.id)) : "";
323
+ const latest = exact ? this.snapshot(group.sessionId)?.messages?.slice().reverse().find(message => message.role === "assistant" && message.conversationTarget?.runId === exact.id
324
+ && message.content.some(block => block.type === "text" && block.text.trim())) : null;
325
+ const text = (receipt?.startup === "failed" ? receipt.error : live)
326
+ || latest?.content.map(block => block.type === "text" ? block.text : "").join("\n")
327
+ || exact?.statusDetail || "任务已接收,正在启动;无需重复发送。";
328
+ return { ...turn, conversationLink: { ...link, title: task.title },
329
+ taskPreview: { runId: exact?.id ?? null, status, text: text.slice(-4000) } };
330
+ }
331
+ groupTeam(input, groupId) {
332
+ const templateId = shortText(input.templateId, "预设 ID", 100);
333
+ const template = templateId ? this.storage.getAiTeam(templateId) : null;
334
+ if (templateId && !template)
335
+ throw new Error("预设小组不存在。");
336
+ const excluded = new Set(ids(input.excludedMemberIds));
337
+ if ([...excluded].some((id) => !template?.members.some((m) => m.id === id)))
338
+ throw new Error("移除的预设成员不存在。");
339
+ const members = (template?.members ?? []).filter((m) => !excluded.has(m.id)).map((m) => ({
340
+ ...m, agents: memberAgents(m).map((a) => ({ ...a })),
341
+ ...(!m.employeeId ? { legacyTemplateId: template.id, legacyMemberId: m.id } : {}),
342
+ }));
343
+ for (const id of ids(input.employeeIds)) {
344
+ if (members.some((m) => m.employeeId === id))
345
+ continue;
346
+ const employee = requireTeamEmployee(this.storage, id);
347
+ members.push({ id: `m_${randomUUID().replace(/-/g, "").slice(0, 8)}`, employeeId: id, name: employee.name,
348
+ duty: employee.duty, avatar: employee.avatar, agents: employee.agents.map((a) => ({ ...a })),
349
+ agent: employee.agents[0], isLeader: members.length === 0 });
350
+ }
351
+ if (members.length < 1 || members.length > AI_TEAM_MAX_MEMBERS)
352
+ throw new Error(`群聊需要 1–${AI_TEAM_MAX_MEMBERS} 位员工。`);
353
+ const leaderId = shortText(input.leaderId, "负责人", 100);
354
+ if (leaderId) {
355
+ if (!members.some((m) => m.id === leaderId || m.employeeId === leaderId))
356
+ throw new Error("负责人必须是本群成员。");
357
+ members.forEach((m) => { m.isLeader = m.id === leaderId || m.employeeId === leaderId; });
358
+ }
359
+ if (members.filter((m) => m.isLeader).length !== 1)
360
+ throw new Error("请选择恰好一位负责人。");
361
+ for (const member of members) {
362
+ if (member.employeeId)
363
+ requireTeamEmployee(this.storage, member.employeeId);
364
+ if (input.duties?.[member.employeeId ?? member.id] !== undefined) {
365
+ member.duty = shortText(input.duties[member.employeeId ?? member.id], "群内职责", 2000);
366
+ }
367
+ }
368
+ const createdAt = now();
369
+ return { team: { id: groupId, name: template?.name ?? "群聊", description: template?.description ?? "",
370
+ instructions: template?.instructions ?? "", members, requirePlanApproval: template?.requirePlanApproval ?? true,
371
+ maxSteps: template?.maxSteps ?? AI_TEAM_DEFAULT_MAX_STEPS, createdAt, updatedAt: createdAt }, sourceTemplateId: template?.id ?? null };
372
+ }
373
+ /** `autoName` 是自动群的首轮任务标题:保持 auto 命名,名字继续跟随最新任务。 */
374
+ newGroup(input, autoName = "") {
375
+ const id = `group_${randomUUID()}`;
376
+ const { team, sourceTemplateId } = this.groupTeam(input, id);
377
+ const leader = team.members.find((m) => m.isLeader);
378
+ const name = shortText(input.name, "群名", 120);
379
+ return { id, owner: CONVERSATION_OWNER, kind: "group", peerEmployeeId: null,
380
+ name: name || shortText(autoName, "任务标题", 200).slice(0, 120)
381
+ || (sourceTemplateId ? team.name : team.members.length === 1 ? `我与${leader.name}的群聊` : `${leader.name}等的群聊`),
382
+ nameSource: name ? "custom" : "auto", sourceTemplateId, team, memberVersion: 1,
383
+ joinedVersions: Object.fromEntries(team.members.map((m) => [keyOf(m), 1])),
384
+ sessionId: null, communicationSessionId: null, createdAt: now(), updatedAt: now() };
385
+ }
386
+ request(requestId, kind, input, id, operation) {
387
+ const state = this.storage.conversationListState(id);
388
+ if (state.deleting || state.dissolvedAt)
389
+ operation = () => { throw new Error(state.deleting ? "对话正在删除。" : "群聊已解散,请先恢复群聊。"); };
390
+ const pending = this.performRequest(requestId, kind, input, id, operation);
391
+ const set = this.pendingOperations.get(id) ?? new Set();
392
+ this.pendingOperations.set(id, set);
393
+ set.add(pending);
394
+ void pending.finally(() => { set.delete(pending); if (!set.size)
395
+ this.pendingOperations.delete(id); }).catch(() => { });
396
+ return pending;
397
+ }
398
+ async performRequest(requestId, action, payload, id, operation) {
399
+ if (!/^[\w:-]{8,120}$/.test(requestId))
400
+ throw new Error("需要有效的 requestId。");
401
+ const fingerprint = createHash("sha256").update(stableJson({ action, id, payload })).digest("hex");
402
+ const existing = this.storage.getConversationRequest(requestId);
403
+ if (existing) {
404
+ if (existing.fingerprint !== fingerprint)
405
+ throw new AiTeamConflictError("requestId 已用于另一项操作,请先核对记录。");
406
+ return existing.receipt; // Pending survives restart: never implicitly execute twice.
407
+ }
408
+ const receipt = { requestId, state: "pending", conversationId: id };
409
+ const commit = () => this.storage.saveConversationRequest({ id: requestId, fingerprint, receipt, updatedAt: now() });
410
+ commit();
411
+ try {
412
+ await operation(receipt, commit);
413
+ }
414
+ catch (error) {
415
+ // Reload the committed fact: a rolled-back transaction's in-memory mutation is not acceptance.
416
+ const persisted = this.storage.getConversationRequest(requestId)?.receipt;
417
+ if (persisted) {
418
+ for (const key of Object.keys(receipt))
419
+ delete receipt[key];
420
+ Object.assign(receipt, persisted);
421
+ }
422
+ if (receipt.state === "pending")
423
+ receipt.state = "rejected";
424
+ else if (receipt.startup)
425
+ receipt.startup = "failed";
426
+ receipt.error = getErrorMessage(error);
427
+ commit();
428
+ return receipt;
429
+ }
430
+ commit();
431
+ return receipt;
432
+ }
433
+ receipt(requestId) { return this.storage.getConversationRequest(requestId)?.receipt ?? null; }
434
+ createGroup(requestId, input) {
435
+ return this.request(requestId, "create-group", input, "", (receipt, commit) => {
436
+ const group = this.newGroup(input);
437
+ this.storage.transaction(() => {
438
+ this.storage.saveConversation(group);
439
+ receipt.conversationId = group.id;
440
+ receipt.state = "accepted";
441
+ commit();
442
+ });
443
+ });
444
+ }
445
+ invite(id, requestId, version, input) {
446
+ return this.request(requestId, "invite", { version, input }, id, (receipt) => {
447
+ const group = this.requireInstance(id);
448
+ if (group.kind !== "group" || !group.team)
449
+ throw new Error("只能邀请到群聊。");
450
+ if (group.memberVersion !== version)
451
+ throw new AiTeamConflictError("群成员已更新,请核对名单后再邀请。");
452
+ const proposed = this.groupTeam(input, "invite").team.members;
453
+ const currentKeys = new Set(group.team.members.map(keyOf));
454
+ const added = proposed.filter((m) => !currentKeys.has(keyOf(m))).map((member) => ({ ...member,
455
+ id: `m_${randomUUID().replace(/-/g, "").slice(0, 8)}`, isLeader: false }));
456
+ if (!added.length)
457
+ throw new Error("所选成员已经在群中。");
458
+ if (group.team.members.length + added.length > AI_TEAM_MAX_MEMBERS)
459
+ throw new Error(`群最多 ${AI_TEAM_MAX_MEMBERS} 位员工。`);
460
+ group.memberVersion += 1;
461
+ for (const member of added)
462
+ group.joinedVersions[keyOf(member)] = group.memberVersion;
463
+ group.team = { ...group.team, members: [...group.team.members, ...added], updatedAt: now() };
464
+ group.updatedAt = now();
465
+ // The instance is the only writable roster; no template or run snapshot writer.
466
+ this.storage.saveConversation(group);
467
+ this.storage.appendConversationEvent(id, { role: "assistant", notice: true,
468
+ messageId: randomUUID(), requestId, conversationTarget: null,
469
+ content: [{ type: "text", text: `我邀请了 ${added.map((m) => m.name).join("、")}。下一次新派工生效。` }] });
470
+ receipt.state = "accepted";
471
+ });
472
+ }
473
+ cwd(explicit) {
474
+ if (!explicit?.trim() && !this.deps.config.defaultCwd?.trim())
475
+ throw new Error("请先选择聊天目录。");
476
+ const cwd = resolveSessionCwd(explicit, this.deps.config.defaultCwd);
477
+ if (!statSync(cwd).isDirectory())
478
+ throw new Error("聊天目录不存在。");
479
+ return cwd;
480
+ }
481
+ ensureRelay(group, cwd) {
482
+ if (group.sessionId && this.deps.structured.get(group.sessionId))
483
+ return group.sessionId;
484
+ if (group.sessionId)
485
+ throw new Error("群消息通道不可用,请核对服务状态;不会另建假群覆盖历史。");
486
+ const leader = group.team.members.find((m) => m.isLeader);
487
+ const relay = this.deps.structured.createRelaySession({ cwd, provider: leader.agent.provider,
488
+ mode: leader.agent.mode, title: group.name, automationId: `${CONVERSATION_RELAY_PREFIX}${group.id}`,
489
+ worktreeEnabled: false, sessionSource: "interactive" });
490
+ group.sessionId = relay.id;
491
+ this.storage.saveConversation(group);
492
+ return relay.id;
493
+ }
494
+ /** Explicit resource action may prepare a channel; it never sends or starts a model. */
495
+ prepare(id, requestId, explicitCwd) {
496
+ return this.request(requestId, "prepare", { explicitCwd }, id, (receipt) => {
497
+ const instance = this.requireInstance(id);
498
+ const session = this.communicationChannel(instance, this.cwd(explicitCwd));
499
+ receipt.sessionId = session.id;
500
+ receipt.state = "accepted";
501
+ });
502
+ }
503
+ communicationChannel(instance, cwd) {
504
+ const leader = instance.team?.members.find((m) => m.isLeader);
505
+ const employeeId = instance.peerEmployeeId ?? leader?.employeeId;
506
+ const validated = employeeId ? requireTeamEmployee(this.storage, employeeId) : undefined;
507
+ // The private default partner may use its own habits; group channels never inherit the DM projection.
508
+ const employee = instance.kind === "dm" && employeeId ? this.storage.getSiliconEmployee(employeeId) : validated;
509
+ const selected = employee ? selectEmployeeCandidate(employee) : null;
510
+ const agent = selected?.agent ?? leader?.agent;
511
+ if (!agent || agent.kind !== "structured")
512
+ throw new Error("群内沟通需要负责人的结构化候选;不会切换另一执行引擎。");
513
+ let session = instance.communicationSessionId ? this.deps.structured.get(instance.communicationSessionId) : null;
514
+ if (!session) {
515
+ if (instance.communicationSessionId)
516
+ instance.historySessionIds = [...(instance.historySessionIds ?? []), instance.communicationSessionId];
517
+ const model = agent.model === "default"
518
+ ? defaultModelGroupSelector(this.deps.config.modelGroups, agent.provider, getDefaultModelForProvider(this.deps.config, agent.provider))
519
+ || getDefaultModelForProvider(this.deps.config, agent.provider) || undefined : agent.model;
520
+ session = this.deps.structured.createSession({ cwd, provider: agent.provider, mode: agent.mode, model,
521
+ thinkingEffort: agent.thinkingEffort, worktreeEnabled: false, sessionSource: "interactive",
522
+ automationId: `conversation-talk:${instance.id}`, employeeId: employee?.id,
523
+ employeeName: employee?.name ?? leader?.name, employeeAvatar: employee?.avatar ?? leader?.avatar,
524
+ employeeCandidates: employee?.agents, employeeCandidateIndex: selected?.index,
525
+ systemPrompt: [employee?.prompt ?? "", instance.kind === "group"
526
+ ? `你在群聊「${instance.name}」中以负责人身份沟通。普通沟通不等同派任务、审批或执行;仅使用本群上下文,不引用你的日常私聊。\n${instance.team?.instructions ?? ""}` : ""].filter(Boolean).join("\n\n") });
527
+ instance.communicationSessionId = session.id;
528
+ if (instance.kind === "dm")
529
+ instance.sessionId = session.id;
530
+ }
531
+ instance.createdAt ||= now();
532
+ instance.updatedAt = now();
533
+ this.storage.saveConversation(instance);
534
+ return session;
535
+ }
536
+ send(id, requestId, input, target, explicitCwd, echo = true) {
537
+ return this.request(requestId, "send", { input, target, explicitCwd, echo }, id, async (receipt, commit) => {
538
+ const text = shortText(input, "消息", 256_000, true);
539
+ const instance = this.requireInstance(id);
540
+ if (target) {
541
+ const run = this.requireTarget(instance, target);
542
+ if (!AI_TEAM_ACTIVE_RUN_STATUSES.includes(run.status))
543
+ throw new Error("本轮已结束,请显式选择群内沟通或继续此任务。");
544
+ const mention = conversationLeaderMention(text, run.team.members);
545
+ if (mention && "error" in mention)
546
+ throw new Error(mention.error);
547
+ if (mention && mention.memberId !== run.team.members.find(m => m.isLeader)?.id) {
548
+ throw new AiTeamConflictError("本轮负责人已确定;请结束本轮,再用 @成员 派新任务,不会中途换人或重复执行。");
549
+ }
550
+ receipt.taskId = target.taskId;
551
+ receipt.runId = target.runId;
552
+ receipt.state = "accepted";
553
+ commit();
554
+ await this.deps.runner.inputForRun(run.id, text);
555
+ return;
556
+ }
557
+ if (instance.kind === "dm") {
558
+ this.startMessageSession(instance, requestId, text, explicitCwd, receipt, commit);
559
+ return;
560
+ }
561
+ if (instance.kind === "group" && instance.team) {
562
+ const mention = conversationLeaderMention(text, instance.team.members);
563
+ if (mention && "error" in mention)
564
+ throw new Error(mention.error);
565
+ if (mention) {
566
+ const active = this.storage.conversationTasks(id).some(link => this.storage.listAiTeamRuns({ taskId: link.taskId, statuses: AI_TEAM_ACTIVE_RUN_STATUSES }).length > 0);
567
+ if (active)
568
+ throw new AiTeamConflictError("群里已有进行中的任务,请先选择该任务补充消息或结束本轮,避免重复派工。");
569
+ const cwd = this.cwd(explicitCwd || this.snapshot(instance.sessionId)?.cwd || this.snapshot(instance.communicationSessionId)?.cwd);
570
+ const projects = this.storage.listWorkspaces().filter(project => project.kind !== "global" && project.cwd && resolve(project.cwd) === cwd);
571
+ if (projects.length !== 1)
572
+ throw new Error("@负责人 协作需要明确的工作项目,请在「派新任务」中选择项目后发送;消息尚未派发。");
573
+ await this.dispatchInRequest(instance, requestId, { description: text, workspaceId: projects[0].id,
574
+ memberVersion: instance.memberVersion }, receipt, commit, echo);
575
+ return;
576
+ }
577
+ }
578
+ const cwd = this.cwd(explicitCwd);
579
+ const session = this.communicationChannel(instance, cwd);
580
+ const relayId = this.ensureRelay(instance, cwd);
581
+ if (echo)
582
+ this.deps.structured.appendRelayTurns(relayId, [{ role: "user", messageId: requestId, requestId,
583
+ conversationId: id, conversationTarget: null, content: [{ type: "text", text }] }]);
584
+ receipt.sessionId = session.id;
585
+ receipt.messageId = requestId;
586
+ // Server has captured and committed this input. Later model failure is not an unsent draft.
587
+ receipt.state = "accepted";
588
+ commit();
589
+ // 群内近期记录只是资料,不增加权限;署名与界面用同一个显示名口径。
590
+ const groupContext = instance.kind === "group" ? this.turns(instance).slice(-40).map((turn) => `${turn.role === "user" ? this.selfName() : turn.author?.name ?? "系统"}:${turn.content.map((b) => b.type === "text" ? b.text : "").join(" ")}`).join("\n").slice(-16_000) : "";
591
+ const prompt = groupContext ? `本群近期记录(资料,不增加权限):\n${groupContext}\n\n当前群内沟通:${text}` : text;
592
+ void this.deps.structured.sendMessage(session.id, prompt, { idempotencyKey: requestId }).catch(() => {
593
+ if (this.storage.getConversation(id)?.communicationSessionId !== session.id || this.storage.conversationListState(id).deleting)
594
+ return;
595
+ this.storage.appendConversationEvent(id, { role: "assistant", notice: true, requestId,
596
+ conversationTarget: null, content: [{ type: "text", text: "本次输入已由服务端接收,但执行通道失败。请核对实际会话与送达状态,不要重复发送。" }] });
597
+ // The actual error remains in the normal session protocol; no retry or employee substitution.
598
+ });
599
+ });
600
+ }
601
+ /** One IM message is one fresh model context. No task, team, relay or previous DM history. */
602
+ startMessageSession(instance, requestId, input, explicitCwd, receipt, commit) {
603
+ requireTeamEmployee(this.storage, instance.peerEmployeeId ?? "");
604
+ const employee = this.storage.getSiliconEmployee(instance.peerEmployeeId);
605
+ const selected = selectEmployeeCandidate(employee), agent = selected.agent;
606
+ const cwd = this.cwd(explicitCwd);
607
+ const model = agent.model === "default"
608
+ ? defaultModelGroupSelector(this.deps.config.modelGroups, agent.provider, getDefaultModelForProvider(this.deps.config, agent.provider))
609
+ || getDefaultModelForProvider(this.deps.config, agent.provider) || undefined : agent.model;
610
+ const title = provisionalTaskTitleFromDescription(conversationMessageBody(input)) || "新会话";
611
+ let session;
612
+ try {
613
+ this.storage.acceptConversationSession(instance, () => {
614
+ session = this.deps.structured.createSession({ title, cwd, provider: agent.provider, mode: agent.mode, model,
615
+ thinkingEffort: agent.thinkingEffort, worktreeEnabled: false, sessionSource: "interactive",
616
+ workspaceId: resolveWorkspaceIdForNewSession(this.storage, cwd),
617
+ automationId: `${CONVERSATION_SESSION_PREFIX}${requestId}`, employeeId: employee.id,
618
+ employeeName: employee.name, employeeAvatar: employee.avatar, employeeCandidates: employee.agents,
619
+ employeeCandidateIndex: selected.index, systemPrompt: employee.prompt });
620
+ return session.id;
621
+ }, title, input, this.storage.getConversationRequest(requestId));
622
+ }
623
+ catch (error) {
624
+ if (session)
625
+ this.deps.deleteSession?.(session.id);
626
+ throw error;
627
+ }
628
+ Object.assign(receipt, this.storage.getConversationRequest(requestId).receipt);
629
+ receipt.startup = "started";
630
+ commit();
631
+ // The session and both messages are durable before the first byte can reach a model.
632
+ void this.deps.structured.sendMessage(session.id, input, { idempotencyKey: requestId }).catch(error => {
633
+ if (!this.storage.getConversation(instance.id) || this.storage.conversationListState(instance.id).deleting
634
+ || !this.snapshot(session.id))
635
+ return;
636
+ receipt.startup = "failed";
637
+ receipt.error = getErrorMessage(error);
638
+ commit();
639
+ this.ingestSessionEvent({ type: "status", sessionId: session.id, data: {} });
640
+ });
641
+ }
642
+ requireTarget(instance, target) {
643
+ if (instance.kind !== "group" || !this.storage.conversationTasks(instance.id).some((t) => t.taskId === target.taskId)) {
644
+ throw new Error("任务不属于这个群聊。");
645
+ }
646
+ const run = this.storage.getAiTeamRun(target.runId);
647
+ if (!run || run.taskId !== target.taskId || run.chatSessionId !== instance.sessionId)
648
+ throw new Error("本轮不属于这个群聊任务。");
649
+ return run;
650
+ }
651
+ dispatch(id, requestId, input) {
652
+ return this.request(requestId, "dispatch", input, id, (receipt, commit) => {
653
+ const instance = this.requireInstance(id);
654
+ // Older clients may still use the task form in a DM. It now starts a session, never a task group.
655
+ if (instance.kind === "dm") {
656
+ if (input.continueTaskId)
657
+ throw new Error("请到原任务群继续历史任务。");
658
+ const workspace = input.workspaceId ? this.storage.getWorkspace(input.workspaceId) : null;
659
+ if (input.workspaceId && (!workspace?.cwd || workspace.kind === "global"))
660
+ throw new Error("请选择有效的工作项目。");
661
+ this.startMessageSession(instance, requestId, shortText(input.description, "消息", 256_000, true), workspace?.cwd || input.cwd, receipt, commit);
662
+ return;
663
+ }
664
+ return this.dispatchInRequest(instance, requestId, input, receipt, commit);
665
+ });
666
+ }
667
+ /** Both explicit task forms and leading @ messages share one atomic acceptance/idempotency boundary. */
668
+ async dispatchInRequest(original, requestId, input, receipt, commit, echo = true) {
669
+ const description = shortText(input.description, "任务说明", 256_000, true);
670
+ const mention = original.kind === "group" ? conversationLeaderMention(description, (original.team?.members ?? [])) : null;
671
+ if (mention && "error" in mention)
672
+ throw new Error(mention.error);
673
+ // Explicit group work only; continuing a task keeps its original title.
674
+ const taskTitle = input.continueTaskId ? "" : shortText(input.title, "任务标题", 200)
675
+ || provisionalTaskTitleFromDescription(mention?.body ?? conversationMessageBody(description)) || "新任务";
676
+ // 自建/自动群跟随最新任务标题,实例名与列表里的任务标题投影保持一致;预设群保留模板名。
677
+ if (original.kind === "group" && original.nameSource === "auto" && !original.sourceTemplateId && taskTitle) {
678
+ original.name = taskTitle.slice(0, 120);
679
+ original.updatedAt = now();
680
+ }
681
+ const group = original;
682
+ if (!group.team)
683
+ throw new Error("群配置不可用。");
684
+ if (input.memberVersion !== undefined && input.memberVersion !== group.memberVersion)
685
+ throw new AiTeamConflictError("群成员已更新,请核对名单。");
686
+ // Change only this run's frozen roster. Never rewrite the group, template, employees or old runs.
687
+ const definition = mention ? { ...group.team, allowLeaderWork: true,
688
+ members: group.team.members.map(member => ({ ...member, isLeader: member.id === mention.memberId })) } : group.team;
689
+ const team = freezeTeamEmployees(definition, this.storage); // validate before committing task/group
690
+ const workspace = input.workspaceId ? this.storage.getWorkspace(input.workspaceId) : null;
691
+ if (!workspace || workspace.kind === "global" || !workspace.cwd)
692
+ throw new Error("请选择非全局工作项目。");
693
+ this.cwd(workspace.cwd);
694
+ let previous;
695
+ if (input.continueTaskId) {
696
+ if (!this.storage.conversationTasks(group.id).some((t) => t.taskId === input.continueTaskId))
697
+ throw new Error("任务不属于这个群聊。");
698
+ previous = this.storage.getWandTask(input.continueTaskId);
699
+ if (!previous || previous.workspaceId !== workspace.id)
700
+ throw new Error("继续任务必须使用原工作项目。");
701
+ if (this.storage.listAiTeamRuns({ taskId: previous.id, statuses: AI_TEAM_ACTIVE_RUN_STATUSES }).length)
702
+ throw new AiTeamConflictError("请先结束或停止该任务的旧轮次。");
703
+ }
704
+ const task = this.storage.acceptConversationDispatch(group, previous ? { continueTaskId: previous.id } : { workspaceId: workspace.id,
705
+ title: taskTitle, description, labels: ["conversation_task"], agent: team.members.find((m) => m.isLeader).agent }, this.storage.getConversationRequest(requestId));
706
+ Object.assign(receipt, this.storage.getConversationRequest(requestId).receipt);
707
+ // Everything after this atomic boundary is startup, never rejection or a new task.
708
+ const relayId = this.ensureRelay(group, workspace.cwd);
709
+ let accepted = false;
710
+ const completion = this.deps.runner.start({ team, taskId: task.id, note: description, chatSessionId: relayId,
711
+ conversationId: group.id, memberVersion: group.memberVersion, onAccepted: (run) => {
712
+ receipt.runId = run.id;
713
+ receipt.startup = "started";
714
+ accepted = true;
715
+ commit();
716
+ } });
717
+ if (!accepted)
718
+ await completion;
719
+ else
720
+ void completion.catch((error) => {
721
+ receipt.startup = "failed";
722
+ receipt.error = getErrorMessage(error);
723
+ commit();
724
+ // Keep the real run/steps and accepted task; no rollback, fallback or automatic restart.
725
+ });
726
+ if (echo)
727
+ this.deps.structured.appendRelayTurns(relayId, [{ role: "user", messageId: requestId, requestId,
728
+ conversationId: group.id, conversationTarget: receipt.runId ? { taskId: task.id, runId: receipt.runId } : null,
729
+ content: [{ type: "text", text: description }] }]);
730
+ }
731
+ act(id, requestId, target, action, extraSteps) {
732
+ return this.request(requestId, "act", { target, action, extraSteps }, id, async (receipt, commit) => {
733
+ const run = this.requireTarget(this.requireInstance(id), target);
734
+ if (!["approve", "stop", "continue"].includes(action))
735
+ throw new Error("未知运行操作。");
736
+ if (!AI_TEAM_ACTIVE_RUN_STATUSES.includes(run.status))
737
+ throw new Error("本轮已结束。");
738
+ if (action === "approve" && run.status !== "awaiting_approval")
739
+ throw new Error("本轮没有待批准的计划。");
740
+ if (action === "continue" && run.status !== "waiting_user")
741
+ throw new Error("本轮没有暂停在步数上限。");
742
+ receipt.taskId = target.taskId;
743
+ receipt.runId = target.runId;
744
+ receipt.state = "accepted";
745
+ commit();
746
+ if (action === "approve")
747
+ await this.deps.runner.approve(run.id);
748
+ else if (action === "stop")
749
+ await this.deps.runner.stop(run.id);
750
+ else if (action === "continue")
751
+ await this.deps.runner.continueRun(run.id, extraSteps ?? 10);
752
+ else
753
+ throw new Error("未知运行操作。");
754
+ receipt.taskId = target.taskId;
755
+ receipt.runId = target.runId;
756
+ receipt.state = "accepted";
757
+ });
758
+ }
759
+ }