返回 DeepSeek-Reasonix
sidebarImProjection.ts
根目录 / desktop / frontend / src / app-runtime / sidebarImProjection.ts
1 import { asArray } from "../lib/array";
2 import type { Translator } from "../lib/i18n";
3 import type { BotConnectionView, BotRuntimeStatusView, BotSettingsView, SessionMeta } from "../lib/types";
4
5 export type SidebarImPlatform = "qq" | "feishu" | "lark" | "weixin";
6 type SidebarImStatus = "connected" | "disabled" | "pending" | "error" | "disconnected";
7 export type SidebarImConnection = {
8 id: string;
9 connectionId: string;
10 platform: SidebarImPlatform;
11 title: string;
12 platformLabel: string;
13 subtitle: string;
14 status: SidebarImStatus;
15 statusLabel: string;
16 remoteId: string;
17 sessionId: string;
18 sessionSource: string;
19 scope: "global" | "project";
20 workspaceRoot: string;
21 allowAll: boolean;
22 allowlistEnabled: boolean;
23 allowlistUsers: string[];
24 allowlistMatched: boolean;
25 };
26 export type SidebarImTopicSource = {
27 platform: SidebarImPlatform;
28 label: string;
29 title: string;
30 remoteId: string;
31 connectionId: string;
32 };
33 function isSidebarImConnection(connection: BotConnectionView): boolean {
34 return connection.provider === "feishu" || connection.provider === "weixin";
35 }
36
37 function sidebarImPlatform(connection: BotConnectionView): SidebarImPlatform {
38 if (connection.provider === "weixin") return "weixin";
39 return connection.domain === "lark" ? "lark" : "feishu";
40 }
41
42 function sidebarImPlatformLabel(platform: SidebarImPlatform, translate: Translator): string {
43 if (platform === "qq") return "QQ";
44 if (platform === "lark") return "Lark";
45 if (platform === "weixin") return translate("settings.botWeixin");
46 return translate("settings.botFeishu");
47 }
48
49 function botMappingScope(mapping: BotConnectionView["sessionMappings"][number] | null | undefined, connectionWorkspaceRoot: string): "global" | "project" {
50 if (mapping?.scope === "project") return "project";
51 if ((mapping?.workspaceRoot ?? "").trim()) return "project";
52 return connectionWorkspaceRoot.trim() ? "project" : "global";
53 }
54
55 function botMappingWorkspaceRoot(
56 mapping: BotConnectionView["sessionMappings"][number] | null | undefined,
57 connectionWorkspaceRoot: string,
58 ): string {
59 const workspaceRoot = (mapping?.workspaceRoot ?? "").trim() || connectionWorkspaceRoot.trim();
60 return botMappingScope(mapping, connectionWorkspaceRoot) === "project" ? workspaceRoot : "";
61 }
62
63 function compactRemoteId(value: string): string {
64 const trimmed = value.trim();
65 if (trimmed.length <= 28) return trimmed;
66 return `${trimmed.slice(0, 12)}…${trimmed.slice(-8)}`;
67 }
68
69 function botMappingIdentityLabel(mapping: BotConnectionView["sessionMappings"][number] | null | undefined): string {
70 const chatType = (mapping?.chatType ?? "").trim();
71 const userId = (mapping?.userId ?? "").trim();
72 const threadId = (mapping?.threadId ?? "").trim();
73 if (threadId) return compactRemoteId(threadId);
74 if ((chatType === "group" || chatType === "guild") && userId) return compactRemoteId(userId);
75 return "";
76 }
77
78 function sidebarImStatus(connection: BotConnectionView, botEnabled: boolean): SidebarImStatus {
79 if (!botEnabled || !connection.enabled) return "disabled";
80 if (connection.status === "connected") return "connected";
81 if (connection.status === "pending") return "pending";
82 if (connection.status === "error") return "error";
83 return "disconnected";
84 }
85
86 function sidebarImStatusLabel(status: SidebarImStatus, translate: Translator): string {
87 switch (status) {
88 case "connected":
89 return translate("sidebar.imConnected");
90 case "disabled":
91 return translate("sidebar.imDisabled");
92 case "pending":
93 return translate("sidebar.imPending");
94 case "error":
95 return translate("sidebar.imError");
96 default:
97 return translate("sidebar.imDisconnected");
98 }
99 }
100
101 function uniqueTrimmedValues(values: string[]): string[] {
102 return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
103 }
104
105 function sidebarImAllowlistUsers(bot: BotSettingsView, platform: SidebarImPlatform): string[] {
106 if (platform === "qq") return uniqueTrimmedValues(asArray(bot.allowlist.qqUsers));
107 if (platform === "weixin") return uniqueTrimmedValues(asArray(bot.allowlist.weixinUsers));
108 return uniqueTrimmedValues(asArray(bot.allowlist.feishuUsers));
109 }
110
111 function sidebarImQQAdded(qq: BotSettingsView["qq"]): boolean {
112 return Boolean(qq.enabled || qq.secretSet || qq.appId.trim());
113 }
114
115 function sidebarImQQStatus(bot: BotSettingsView, runtimeStatus: BotRuntimeStatusView | null | undefined, nativeRuntime: boolean): SidebarImStatus {
116 const appId = bot.qq.appId.trim();
117 if (!bot.enabled || !bot.qq.enabled) return "disabled";
118 if (!appId || !bot.qq.secretSet) return "disconnected";
119 if (!nativeRuntime) return "pending";
120 if (!runtimeStatus) return "pending";
121 const status = runtimeStatus.status.trim().toLowerCase();
122 if (runtimeStatus.running && runtimeStatus.connections > 0 && status === "running") {
123 return "connected";
124 }
125 if (status === "error" || status === "blocked" || status === "degraded") return "error";
126 if (status === "stopped") return "disconnected";
127 return "pending";
128 }
129
130 function sidebarImQQConnection(bot: BotSettingsView, translate: Translator, runtimeStatus: BotRuntimeStatusView | null | undefined, nativeRuntime: boolean): SidebarImConnection | null {
131 if (!sidebarImQQAdded(bot.qq)) return null;
132 const remoteId = bot.qq.appId.trim();
133 const status = sidebarImQQStatus(bot, runtimeStatus, nativeRuntime);
134 const statusLabel = sidebarImStatusLabel(status, translate);
135 const allowlistUsers = sidebarImAllowlistUsers(bot, "qq");
136 const subtitleParts = [
137 remoteId ? compactRemoteId(remoteId) : "QQ",
138 statusLabel,
139 ].filter(Boolean);
140 return {
141 id: "__qq_bot__",
142 connectionId: "__qq_bot__",
143 platform: "qq",
144 title: "QQ Bot",
145 platformLabel: "QQ",
146 subtitle: subtitleParts.join(" · "),
147 status,
148 statusLabel,
149 remoteId,
150 sessionId: "",
151 sessionSource: "",
152 scope: "global",
153 workspaceRoot: "",
154 allowAll: bot.allowlist.allowAll,
155 allowlistEnabled: bot.allowlist.enabled,
156 allowlistUsers,
157 allowlistMatched: remoteId ? allowlistUsers.includes(remoteId) : false,
158 };
159 }
160
161 export function sidebarImConnectionsFromBot(
162 bot: BotSettingsView | null | undefined,
163 translate: Translator,
164 runtimeStatus: BotRuntimeStatusView | null | undefined,
165 nativeRuntime: boolean,
166 ): SidebarImConnection[] {
167 if (!bot) return [];
168 const qqConnection = sidebarImQQConnection(bot, translate, runtimeStatus, nativeRuntime);
169 const connectionItems: SidebarImConnection[] = [];
170 for (const connection of asArray(bot.connections)) {
171 if (!isSidebarImConnection(connection)) continue;
172 const mappings = connection.sessionMappings.filter((mapping) => mapping.sessionId.trim() || mapping.remoteId.trim());
173 const rowMappings = mappings.length > 0 ? mappings : [null];
174 rowMappings.forEach((mapping, index) => {
175 const platform = sidebarImPlatform(connection);
176 const platformLabel = sidebarImPlatformLabel(platform, translate);
177 const remoteId = mapping?.remoteId.trim() ?? "";
178 const sessionId = mapping?.sessionId.trim() ?? "";
179 const sessionSource = mapping?.sessionSource.trim() ?? "";
180 const scope = botMappingScope(mapping, connection.workspaceRoot);
181 const workspaceRoot = botMappingWorkspaceRoot(mapping, connection.workspaceRoot);
182 const status = sidebarImStatus(connection, bot.enabled);
183 const title = connection.label.trim() || platformLabel;
184 const allowlistUsers = sidebarImAllowlistUsers(bot, platform);
185 const identityLabel = botMappingIdentityLabel(mapping);
186 const mappedUserId = mapping?.userId.trim() ?? "";
187 const subtitleParts = [
188 remoteId ? compactRemoteId(remoteId) : platformLabel,
189 identityLabel,
190 connection.model.trim() || "",
191 sidebarImStatusLabel(status, translate),
192 ].filter(Boolean);
193 connectionItems.push({
194 id: mapping ? `${connection.id}:mapping:${index}` : connection.id,
195 connectionId: connection.id,
196 platform,
197 title,
198 platformLabel,
199 subtitle: subtitleParts.join(" · "),
200 status,
201 statusLabel: sidebarImStatusLabel(status, translate),
202 remoteId,
203 sessionId,
204 sessionSource,
205 scope,
206 workspaceRoot,
207 allowAll: bot.allowlist.allowAll,
208 allowlistEnabled: bot.allowlist.enabled,
209 allowlistUsers,
210 allowlistMatched: remoteId
211 ? allowlistUsers.includes(remoteId) || (mappedUserId ? allowlistUsers.includes(mappedUserId) : false)
212 : false,
213 });
214 });
215 }
216 return qqConnection ? [qqConnection, ...connectionItems] : connectionItems;
217 }
218
219 function mappedSessionTarget(sessionId: string): { kind: "path" | "topic"; value: string } | null {
220 const trimmed = sessionId.trim();
221 if (!trimmed) return null;
222 const lower = trimmed.toLowerCase();
223 if (lower.startsWith("path:")) {
224 const value = trimmed.slice(5).trim();
225 return value ? { kind: "path", value } : null;
226 }
227 if (lower.startsWith("topic:")) {
228 const value = trimmed.slice(6).trim();
229 return value ? { kind: "topic", value } : null;
230 }
231 if (trimmed.endsWith(".jsonl") || trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith("~")) {
232 return { kind: "path", value: trimmed };
233 }
234 return { kind: "topic", value: trimmed };
235 }
236
237 export function taskSessionIDFromPath(path: string): string {
238 const base = path.replace(/\\/g, "/").split("/").pop() || "";
239 const extension = base.lastIndexOf(".");
240 return extension > 0 ? base.slice(0, extension) : base;
241 }
242
243 export function sidebarImSessionTarget(connection: SidebarImConnection): { kind: "path" | "topic"; value: string } | null {
244 return mappedSessionTarget(connection.sessionId);
245 }
246
247 export function isChannelSession(session: SessionMeta): boolean {
248 return session.kind === "channel" || session.sessionSource === "auto";
249 }
250
251 export function sidebarImTopicSourcesFromBot(bot: BotSettingsView | null | undefined, translate: Translator): Record<string, SidebarImTopicSource> {
252 if (!bot?.connections?.length) return {};
253 const sources: Record<string, SidebarImTopicSource> = {};
254 for (const connection of bot.connections) {
255 if (!isSidebarImConnection(connection)) continue;
256 const platform = sidebarImPlatform(connection);
257 const label = sidebarImPlatformLabel(platform, translate);
258 const title = connection.label.trim() || label;
259 for (const mapping of asArray(connection.sessionMappings)) {
260 const scope = botMappingScope(mapping, connection.workspaceRoot);
261 if (scope !== "global") continue;
262 const target = mappedSessionTarget(mapping.sessionId);
263 if (!target || target.kind !== "topic") continue;
264 if (sources[target.value]) continue;
265 sources[target.value] = {
266 platform,
267 label,
268 title,
269 remoteId: mapping.remoteId.trim(),
270 connectionId: connection.id,
271 };
272 }
273 }
274 return sources;
275 }
276
277 export function sidebarImScopeLabel(connection: SidebarImConnection, translate: Translator): string {
278 if (connection.scope === "project") return translate("botDetail.scopeProject", { name: connection.workspaceRoot || "Project" });
279 return translate("botDetail.scopeGlobal");
280 }
281
281 lines TYPESCRIPT