@auto-ai/agent 2.1.94 → 2.1.96
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/bin/agent.js +0 -0
- package/dist/404/index.html +1 -1
- package/dist/404.html +1 -1
- package/dist/agent-office.html +40 -35
- package/dist/index.html +1 -1
- package/dist/index.txt +1 -1
- package/dist/manage/about/index.html +2 -2
- package/dist/manage/about/index.txt +1 -1
- package/dist/manage/add-account/basic/index.html +2 -2
- package/dist/manage/add-account/basic/index.txt +3 -4
- package/dist/manage/add-account/index.html +2 -2
- package/dist/manage/add-account/index.txt +4 -3
- package/dist/manage/add-account/prompt/index.html +2 -2
- package/dist/manage/add-account/prompt/index.txt +1 -1
- package/dist/manage/agent-teams/index.html +2 -2
- package/dist/manage/agent-teams/index.txt +1 -1
- package/dist/manage/env/index.html +2 -2
- package/dist/manage/env/index.txt +1 -1
- package/dist/manage/general/index.html +2 -2
- package/dist/manage/general/index.txt +1 -1
- package/dist/manage/index.html +1 -1
- package/dist/manage/index.txt +1 -1
- package/dist/manage/mcp/index.html +2 -2
- package/dist/manage/mcp/index.txt +1 -1
- package/dist/manage/permissions/index.html +2 -2
- package/dist/manage/permissions/index.txt +1 -1
- package/dist/manage/skills/index.html +2 -2
- package/dist/manage/skills/index.txt +1 -1
- package/dist/manage/task/index.html +1 -1
- package/dist/manage/task/index.txt +1 -1
- package/dist/manage/teams/index.html +1 -1
- package/dist/manage/teams/index.txt +1 -1
- package/dist/manage/tools/index.html +2 -2
- package/dist/manage/tools/index.txt +1 -1
- package/dist/ws-test.html +369 -108
- package/mcps-runtime/claude-tuitui/.mcp.json +18 -0
- package/mcps-runtime/claude-tuitui/cli.mjs +78 -0
- package/mcps-runtime/claude-tuitui/package-lock.json +1167 -0
- package/mcps-runtime/claude-tuitui/server/boot.mjs +25 -0
- package/mcps-runtime/claude-tuitui/server/index.mjs +935 -0
- package/mcps-runtime/claude-tuitui/server/state.mjs +229 -0
- package/mcps-runtime/claude-tuitui/server/tuitui-api.mjs +358 -0
- package/package.json +7 -6
- /package/dist/_next/static/{8ct8wdwNecCOvJ8dbcAlk → q0is0IhxOAG7ogFx9qQFk}/_buildManifest.js +0 -0
- /package/dist/_next/static/{8ct8wdwNecCOvJ8dbcAlk → q0is0IhxOAG7ogFx9qQFk}/_clientMiddlewareManifest.json +0 -0
- /package/dist/_next/static/{8ct8wdwNecCOvJ8dbcAlk → q0is0IhxOAG7ogFx9qQFk}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
appendFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
+
import * as z from "zod";
|
|
15
|
+
import {
|
|
16
|
+
CHAT_TYPE_DIRECT,
|
|
17
|
+
CHAT_TYPE_GROUP,
|
|
18
|
+
CHAT_TYPE_CHANNEL,
|
|
19
|
+
createWebSocketClient,
|
|
20
|
+
buildMessageBody,
|
|
21
|
+
sendTextMsg,
|
|
22
|
+
sendMediaMsg,
|
|
23
|
+
emojiReaction,
|
|
24
|
+
guessChatType,
|
|
25
|
+
teamsBuildChatId,
|
|
26
|
+
} from "./tuitui-api.mjs";
|
|
27
|
+
import { StateStore } from "./state.mjs";
|
|
28
|
+
|
|
29
|
+
const stateDir =
|
|
30
|
+
process.env.TUITUI_STATE_DIR ||
|
|
31
|
+
join(homedir(), ".claude", "channels", "tuitui");
|
|
32
|
+
const bootLogFile = join(stateDir, "boot.log");
|
|
33
|
+
const notifyTraceFile = join(stateDir, "notify-trace.log");
|
|
34
|
+
const projectSettingsDir = join(process.cwd(), ".claude");
|
|
35
|
+
const projectSettingsLocalFile = join(
|
|
36
|
+
projectSettingsDir,
|
|
37
|
+
"settings.local.json",
|
|
38
|
+
);
|
|
39
|
+
const execFileAsync = promisify(execFile);
|
|
40
|
+
const CLAUDE_BIN = process.env.CLAUDE_BIN || "claude";
|
|
41
|
+
const AUTO_REPLY_ENABLED =
|
|
42
|
+
String(process.env.TUITUI_AUTO_REPLY || "true").toLowerCase() !== "false";
|
|
43
|
+
const account = {
|
|
44
|
+
appId: process.env.TUITUI_APP_ID,
|
|
45
|
+
appSecret: process.env.TUITUI_APP_SECRET,
|
|
46
|
+
dmPolicy: process.env.TUITUI_DM_POLICY || "pairing",
|
|
47
|
+
groupPolicy: process.env.TUITUI_GROUP_POLICY || "allowlist",
|
|
48
|
+
requireMention:
|
|
49
|
+
String(process.env.TUITUI_REQUIRE_MENTION || "true").toLowerCase() !==
|
|
50
|
+
"false",
|
|
51
|
+
channelContext: process.env.TUITUI_CHANNEL_CONTEXT || "thread",
|
|
52
|
+
};
|
|
53
|
+
mkdirSync(stateDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
function logBoot(line) {
|
|
56
|
+
try {
|
|
57
|
+
appendFileSync(bootLogFile, `${new Date().toISOString()} ${line}\n`, {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
mode: 0o600,
|
|
60
|
+
flag: "a",
|
|
61
|
+
});
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
logBoot(`[BOOT] tuitui-channel index.mjs start pid=${process.pid}`);
|
|
66
|
+
logBoot("[BOOT] marker=2026-03-23-v2-runtime-probe");
|
|
67
|
+
logBoot(
|
|
68
|
+
`[BOOT] env TUITUI_DM_POLICY=${process.env.TUITUI_DM_POLICY || ""} TUITUI_STATE_DIR=${process.env.TUITUI_STATE_DIR || ""}`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
process.on("uncaughtException", (err) => {
|
|
72
|
+
logBoot(`[BOOT] uncaughtException ${err?.stack || err}`);
|
|
73
|
+
});
|
|
74
|
+
process.on("unhandledRejection", (err) => {
|
|
75
|
+
logBoot(`[BOOT] unhandledRejection ${err?.stack || err}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const store = new StateStore(stateDir);
|
|
79
|
+
const server = new McpServer(
|
|
80
|
+
{
|
|
81
|
+
name: "tuitui-channel",
|
|
82
|
+
version: "0.1.0",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
capabilities: {
|
|
86
|
+
experimental: {
|
|
87
|
+
"claude/channel": {},
|
|
88
|
+
"claude/channel/permission": {},
|
|
89
|
+
},
|
|
90
|
+
tools: {},
|
|
91
|
+
},
|
|
92
|
+
instructions: [
|
|
93
|
+
"Messages from TuiTui users arrive through notifications/claude/channel.",
|
|
94
|
+
"Reply with the tuitui_reply tool.",
|
|
95
|
+
"Use sender_id / chat_id from meta when deciding where to reply.",
|
|
96
|
+
"Prefer concise Chinese replies unless the user writes in another language.",
|
|
97
|
+
"Avoid markdown tables; TuiTui is a chat surface.",
|
|
98
|
+
].join("\n"),
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
let ws = null;
|
|
102
|
+
const recentMessageIds = new Set();
|
|
103
|
+
const pendingPermissionRequests = new Map();
|
|
104
|
+
const PERSIST_PERMISSION_REPLY_RE =
|
|
105
|
+
/^\s*(?:yes-dont-ask-again|y-dont-ask-again)\s+([a-km-z]{5})\s*$/i;
|
|
106
|
+
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
|
|
107
|
+
|
|
108
|
+
function normalizePermissionReplyText(envelope) {
|
|
109
|
+
const text = String(envelope?.text || "").trim();
|
|
110
|
+
if (!text) return text;
|
|
111
|
+
|
|
112
|
+
const commandMatch = text.match(
|
|
113
|
+
/(?:^|\s)(yes-dont-ask-again|y-dont-ask-again|yes|y|no|n)\s+([a-km-z]{5})(?=\s|$)/i,
|
|
114
|
+
);
|
|
115
|
+
if (!commandMatch) return text;
|
|
116
|
+
|
|
117
|
+
if (!envelope?.atMe && commandMatch.index !== 0) return text;
|
|
118
|
+
|
|
119
|
+
return `${commandMatch[1]} ${commandMatch[2]}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readJsonFile(file, fallback) {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
125
|
+
} catch {
|
|
126
|
+
return fallback;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function writeProjectPermissionAllow(toolName) {
|
|
131
|
+
mkdirSync(projectSettingsDir, { recursive: true });
|
|
132
|
+
const current = existsSync(projectSettingsLocalFile)
|
|
133
|
+
? readJsonFile(projectSettingsLocalFile, {})
|
|
134
|
+
: {};
|
|
135
|
+
const next =
|
|
136
|
+
current && typeof current === "object" && !Array.isArray(current)
|
|
137
|
+
? { ...current }
|
|
138
|
+
: {};
|
|
139
|
+
const permissions =
|
|
140
|
+
next.permissions &&
|
|
141
|
+
typeof next.permissions === "object" &&
|
|
142
|
+
!Array.isArray(next.permissions)
|
|
143
|
+
? { ...next.permissions }
|
|
144
|
+
: {};
|
|
145
|
+
const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
|
|
146
|
+
const added = !allow.includes(toolName);
|
|
147
|
+
if (added) allow.push(toolName);
|
|
148
|
+
permissions.allow = allow;
|
|
149
|
+
next.permissions = permissions;
|
|
150
|
+
writeFileSync(
|
|
151
|
+
projectSettingsLocalFile,
|
|
152
|
+
`${JSON.stringify(next, null, 2)}\n`,
|
|
153
|
+
{
|
|
154
|
+
encoding: "utf8",
|
|
155
|
+
mode: 0o600,
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
return {
|
|
159
|
+
added,
|
|
160
|
+
settingsPath: projectSettingsLocalFile,
|
|
161
|
+
allow,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function rememberLastDeliveredChat(envelope) {
|
|
166
|
+
const meta = store.loadMeta();
|
|
167
|
+
meta.lastDeliveredChat = {
|
|
168
|
+
chatId: envelope.chatId,
|
|
169
|
+
chatType: envelope.chatType,
|
|
170
|
+
senderId: envelope.senderId,
|
|
171
|
+
senderName: envelope.senderName,
|
|
172
|
+
groupName: envelope.groupName || null,
|
|
173
|
+
updatedAt: Date.now(),
|
|
174
|
+
};
|
|
175
|
+
store.saveMeta(meta);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildPermissionRelayText(params) {
|
|
179
|
+
const lines = [
|
|
180
|
+
"Claude 请求工具审批",
|
|
181
|
+
`工具: ${params.tool_name}`,
|
|
182
|
+
`说明: ${params.description}`,
|
|
183
|
+
];
|
|
184
|
+
if (params.input_preview) lines.push(`参数: ${params.input_preview}`);
|
|
185
|
+
lines.push(
|
|
186
|
+
`回复 yes ${params.request_id} 允许,或 no ${params.request_id} 拒绝`,
|
|
187
|
+
);
|
|
188
|
+
lines.push(
|
|
189
|
+
`回复 yes-dont-ask-again ${params.request_id} 允许并写入本地项目设置`,
|
|
190
|
+
);
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function relayPermissionRequest(params) {
|
|
195
|
+
pendingPermissionRequests.set(params.request_id, {
|
|
196
|
+
toolName: params.tool_name,
|
|
197
|
+
description: params.description,
|
|
198
|
+
inputPreview: params.input_preview,
|
|
199
|
+
relayedAt: Date.now(),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const meta = store.loadMeta();
|
|
203
|
+
meta.lastPermissionRequestAt = Date.now();
|
|
204
|
+
meta.lastPermissionRelay = {
|
|
205
|
+
requestId: params.request_id,
|
|
206
|
+
toolName: params.tool_name,
|
|
207
|
+
description: params.description,
|
|
208
|
+
inputPreview: params.input_preview,
|
|
209
|
+
target: meta.lastDeliveredChat || null,
|
|
210
|
+
relayedAt: Date.now(),
|
|
211
|
+
};
|
|
212
|
+
store.saveMeta(meta);
|
|
213
|
+
|
|
214
|
+
traceNotify("permission_request:received", {
|
|
215
|
+
requestId: params.request_id,
|
|
216
|
+
toolName: params.tool_name,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const target = meta.lastDeliveredChat;
|
|
220
|
+
if (!target?.chatId || !target?.chatType) {
|
|
221
|
+
traceNotify("permission_request:skipped_no_target", {
|
|
222
|
+
requestId: params.request_id,
|
|
223
|
+
});
|
|
224
|
+
console.error(
|
|
225
|
+
`[tuitui-channel] permission relay skipped requestId=${params.request_id} reason=no_active_chat`,
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await sendTextMsg(
|
|
231
|
+
account,
|
|
232
|
+
target.chatId,
|
|
233
|
+
target.chatType,
|
|
234
|
+
buildPermissionRelayText(params),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
traceNotify("permission_request:relayed", {
|
|
238
|
+
requestId: params.request_id,
|
|
239
|
+
chatId: target.chatId,
|
|
240
|
+
chatType: target.chatType,
|
|
241
|
+
});
|
|
242
|
+
console.error(
|
|
243
|
+
`[tuitui-channel] permission relay sent requestId=${params.request_id} chatId=${target.chatId}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeId(v) {
|
|
248
|
+
return String(v || "")
|
|
249
|
+
.toLowerCase()
|
|
250
|
+
.trim();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function traceNotify(msg, extra = undefined) {
|
|
254
|
+
try {
|
|
255
|
+
const line = `${new Date().toISOString()} ${msg}${extra !== undefined ? ` ${JSON.stringify(extra)}` : ""}\n`;
|
|
256
|
+
appendFileSync(notifyTraceFile, line, {
|
|
257
|
+
encoding: "utf8",
|
|
258
|
+
mode: 0o600,
|
|
259
|
+
flag: "a",
|
|
260
|
+
});
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildInboundEnvelope(json) {
|
|
265
|
+
const msg = json?.body || {};
|
|
266
|
+
const data = msg.data || {};
|
|
267
|
+
const event = msg.event;
|
|
268
|
+
const senderId = normalizeId(msg.user_account || msg.uid);
|
|
269
|
+
const senderName = msg.user_name || msg.user_account || msg.uid || "unknown";
|
|
270
|
+
let chatType = CHAT_TYPE_DIRECT;
|
|
271
|
+
let chatId = senderId;
|
|
272
|
+
let groupName = undefined;
|
|
273
|
+
let text = buildMessageBody(data);
|
|
274
|
+
let messageId = data.msgid || data.post_id || `${Date.now()}`;
|
|
275
|
+
if (event === "group_chat") {
|
|
276
|
+
chatType = CHAT_TYPE_GROUP;
|
|
277
|
+
chatId = normalizeId(data.group_id);
|
|
278
|
+
groupName = data.group_name;
|
|
279
|
+
} else if (event === "teams_post_create") {
|
|
280
|
+
chatType = CHAT_TYPE_CHANNEL;
|
|
281
|
+
const team_id = data.team_id;
|
|
282
|
+
const channel_id = data.channel_id;
|
|
283
|
+
const thread_id =
|
|
284
|
+
data.parent_id && data.parent_id !== "0" ? data.parent_id : data.post_id;
|
|
285
|
+
chatId = teamsBuildChatId(team_id, channel_id, thread_id);
|
|
286
|
+
messageId = data.post_id;
|
|
287
|
+
text = data.content || text;
|
|
288
|
+
groupName = `team:${team_id}/channel:${channel_id}`;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
event,
|
|
292
|
+
senderId,
|
|
293
|
+
senderName,
|
|
294
|
+
userAccount: normalizeId(msg.user_account),
|
|
295
|
+
uid: msg.uid,
|
|
296
|
+
chatType,
|
|
297
|
+
chatId,
|
|
298
|
+
groupName,
|
|
299
|
+
text,
|
|
300
|
+
messageId,
|
|
301
|
+
timestamp: msg.timestamp || new Date().toISOString(),
|
|
302
|
+
atMe: !!data.at_me,
|
|
303
|
+
raw: json,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function isAllowed(envelope) {
|
|
308
|
+
const access = store.loadAccess();
|
|
309
|
+
if (envelope.chatType === CHAT_TYPE_DIRECT) {
|
|
310
|
+
const policy = access.dmPolicy || account.dmPolicy;
|
|
311
|
+
if (policy === "disabled") return { allowed: false, reason: "dm_disabled" };
|
|
312
|
+
if (policy === "open") return { allowed: true };
|
|
313
|
+
const allowed = new Set((access.allowFrom || []).map(normalizeId));
|
|
314
|
+
if (allowed.has(normalizeId(envelope.senderId))) return { allowed: true };
|
|
315
|
+
if (policy === "pairing")
|
|
316
|
+
return { allowed: false, reason: "pairing_required" };
|
|
317
|
+
return { allowed: false, reason: "dm_allowlist" };
|
|
318
|
+
}
|
|
319
|
+
const groupPolicy = access.groupPolicy || account.groupPolicy;
|
|
320
|
+
if (groupPolicy === "disabled")
|
|
321
|
+
return { allowed: false, reason: "group_disabled" };
|
|
322
|
+
if (account.requireMention && !envelope.atMe) {
|
|
323
|
+
return { allowed: false, reason: "mention_required" };
|
|
324
|
+
}
|
|
325
|
+
if (envelope.chatType === CHAT_TYPE_CHANNEL) {
|
|
326
|
+
const teamAllow = new Set((access.teamAllowFrom || []).map(normalizeId));
|
|
327
|
+
const teamId = normalizeId(envelope.raw?.body?.data?.team_id);
|
|
328
|
+
if (teamAllow.has(teamId)) return { allowed: true };
|
|
329
|
+
return { allowed: false, reason: "team_allowlist" };
|
|
330
|
+
}
|
|
331
|
+
const groupAllow = new Set((access.groupAllowFrom || []).map(normalizeId));
|
|
332
|
+
if (groupAllow.has(normalizeId(envelope.chatId))) return { allowed: true };
|
|
333
|
+
return { allowed: false, reason: "group_allowlist" };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function maybeHandleUnauthorized(envelope, decision) {
|
|
337
|
+
if (!account.appId || !account.appSecret) return;
|
|
338
|
+
if (decision.reason === "pairing_required") {
|
|
339
|
+
const code = store.createPairing(envelope.senderId, envelope.chatId);
|
|
340
|
+
await sendTextMsg(
|
|
341
|
+
account,
|
|
342
|
+
envelope.chatId,
|
|
343
|
+
envelope.chatType,
|
|
344
|
+
`当前 Claude Code TuiTui 插件需要配对,请在 Claude Code 中执行配对工具并输入 code: ${code}`,
|
|
345
|
+
);
|
|
346
|
+
} else if (decision.reason === "dm_allowlist") {
|
|
347
|
+
await sendTextMsg(
|
|
348
|
+
account,
|
|
349
|
+
envelope.chatId,
|
|
350
|
+
envelope.chatType,
|
|
351
|
+
`当前会话未放行。请在 Claude Code 中把白名单加入:${envelope.senderId}`,
|
|
352
|
+
);
|
|
353
|
+
} else if (decision.reason === "group_allowlist") {
|
|
354
|
+
await sendTextMsg(
|
|
355
|
+
account,
|
|
356
|
+
envelope.chatId,
|
|
357
|
+
envelope.chatType,
|
|
358
|
+
`当前群聊未放行。请在 Claude Code 中把群白名单加入:${envelope.chatId}`,
|
|
359
|
+
);
|
|
360
|
+
} else if (decision.reason === "team_allowlist") {
|
|
361
|
+
const teamId = envelope.raw?.body?.data?.team_id;
|
|
362
|
+
await sendTextMsg(
|
|
363
|
+
account,
|
|
364
|
+
envelope.chatId,
|
|
365
|
+
envelope.chatType,
|
|
366
|
+
`当前团队未放行。请在 Claude Code 中把团队白名单加入:${teamId}`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function emitClaudeChannelNotification(envelope) {
|
|
372
|
+
const rawMeta = {
|
|
373
|
+
sender: envelope.senderName,
|
|
374
|
+
sender_id: envelope.senderId,
|
|
375
|
+
chat_id: envelope.chatId,
|
|
376
|
+
chat_type: envelope.chatType,
|
|
377
|
+
message_id: envelope.messageId,
|
|
378
|
+
timestamp: envelope.timestamp,
|
|
379
|
+
group_name: envelope.groupName,
|
|
380
|
+
user_account: envelope.userAccount,
|
|
381
|
+
uid: envelope.uid,
|
|
382
|
+
event: envelope.event,
|
|
383
|
+
reply_to_id: envelope.raw?.body?.data?.ref?.msgid,
|
|
384
|
+
teams_post_id: envelope.raw?.body?.data?.post_id,
|
|
385
|
+
teams_parent_id: envelope.raw?.body?.data?.parent_id,
|
|
386
|
+
teams_team_id: envelope.raw?.body?.data?.team_id,
|
|
387
|
+
teams_channel_id: envelope.raw?.body?.data?.channel_id,
|
|
388
|
+
cursor: envelope.cursor,
|
|
389
|
+
};
|
|
390
|
+
const meta = Object.fromEntries(
|
|
391
|
+
Object.entries(rawMeta)
|
|
392
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== "")
|
|
393
|
+
.map(([k, v]) => [k, String(v)]),
|
|
394
|
+
);
|
|
395
|
+
const params = {
|
|
396
|
+
content: String(envelope.text ?? ""),
|
|
397
|
+
meta,
|
|
398
|
+
};
|
|
399
|
+
traceNotify("emitClaudeChannelNotification:before", {
|
|
400
|
+
messageId: envelope.messageId,
|
|
401
|
+
chatId: envelope.chatId,
|
|
402
|
+
cursor: envelope.cursor,
|
|
403
|
+
text: String(envelope.text || "").slice(0, 120),
|
|
404
|
+
});
|
|
405
|
+
try {
|
|
406
|
+
await server.server.notification({
|
|
407
|
+
method: "notifications/claude/channel",
|
|
408
|
+
params,
|
|
409
|
+
});
|
|
410
|
+
rememberLastDeliveredChat(envelope);
|
|
411
|
+
traceNotify("emitClaudeChannelNotification:after", {
|
|
412
|
+
messageId: envelope.messageId,
|
|
413
|
+
chatId: envelope.chatId,
|
|
414
|
+
cursor: envelope.cursor,
|
|
415
|
+
});
|
|
416
|
+
} catch (err) {
|
|
417
|
+
traceNotify("emitClaudeChannelNotification:error", {
|
|
418
|
+
messageId: envelope.messageId,
|
|
419
|
+
chatId: envelope.chatId,
|
|
420
|
+
cursor: envelope.cursor,
|
|
421
|
+
error: err?.message || String(err),
|
|
422
|
+
});
|
|
423
|
+
console.error(
|
|
424
|
+
"[tuitui-channel] notifications/claude/channel failed:",
|
|
425
|
+
err?.message || err,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function emitPermissionDecision(envelope, requestId, behavior) {
|
|
431
|
+
const meta = store.loadMeta();
|
|
432
|
+
meta.lastPermissionDecisionAt = Date.now();
|
|
433
|
+
meta.lastPermissionDecision = {
|
|
434
|
+
requestId,
|
|
435
|
+
behavior,
|
|
436
|
+
senderId: envelope.senderId,
|
|
437
|
+
chatId: envelope.chatId,
|
|
438
|
+
chatType: envelope.chatType,
|
|
439
|
+
decidedAt: Date.now(),
|
|
440
|
+
};
|
|
441
|
+
store.saveMeta(meta);
|
|
442
|
+
|
|
443
|
+
traceNotify("permission_decision:before", {
|
|
444
|
+
requestId,
|
|
445
|
+
behavior,
|
|
446
|
+
senderId: envelope.senderId,
|
|
447
|
+
chatId: envelope.chatId,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
await server.server.notification({
|
|
451
|
+
method: "notifications/claude/channel/permission",
|
|
452
|
+
params: {
|
|
453
|
+
request_id: requestId,
|
|
454
|
+
behavior,
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
pendingPermissionRequests.delete(requestId);
|
|
459
|
+
|
|
460
|
+
traceNotify("permission_decision:after", {
|
|
461
|
+
requestId,
|
|
462
|
+
behavior,
|
|
463
|
+
senderId: envelope.senderId,
|
|
464
|
+
chatId: envelope.chatId,
|
|
465
|
+
});
|
|
466
|
+
console.error(
|
|
467
|
+
`[tuitui-channel] permission decision sent requestId=${requestId} behavior=${behavior} chatId=${envelope.chatId}`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function persistPermissionAllow(envelope, requestId) {
|
|
472
|
+
const pending = pendingPermissionRequests.get(requestId);
|
|
473
|
+
if (!pending?.toolName) {
|
|
474
|
+
traceNotify("permission_persist:missing_request", {
|
|
475
|
+
requestId,
|
|
476
|
+
senderId: envelope.senderId,
|
|
477
|
+
chatId: envelope.chatId,
|
|
478
|
+
});
|
|
479
|
+
console.error(
|
|
480
|
+
`[tuitui-channel] permission persist skipped requestId=${requestId} reason=missing_request`,
|
|
481
|
+
);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const persisted = writeProjectPermissionAllow(pending.toolName);
|
|
486
|
+
const meta = store.loadMeta();
|
|
487
|
+
meta.lastPermissionPersistAt = Date.now();
|
|
488
|
+
meta.lastPermissionPersist = {
|
|
489
|
+
requestId,
|
|
490
|
+
toolName: pending.toolName,
|
|
491
|
+
settingsPath: persisted.settingsPath,
|
|
492
|
+
added: persisted.added,
|
|
493
|
+
savedAt: Date.now(),
|
|
494
|
+
};
|
|
495
|
+
store.saveMeta(meta);
|
|
496
|
+
|
|
497
|
+
traceNotify("permission_persist:after", {
|
|
498
|
+
requestId,
|
|
499
|
+
toolName: pending.toolName,
|
|
500
|
+
settingsPath: persisted.settingsPath,
|
|
501
|
+
added: persisted.added,
|
|
502
|
+
});
|
|
503
|
+
console.error(
|
|
504
|
+
`[tuitui-channel] permission persist saved requestId=${requestId} tool=${pending.toolName} added=${persisted.added}`,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
await emitPermissionDecision(envelope, requestId, "allow");
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function handleInboundJson(json) {
|
|
512
|
+
const envelope = buildInboundEnvelope(json);
|
|
513
|
+
const eventId = json?.event_id || json?.header?.event_id || "";
|
|
514
|
+
const dedupeKey = [
|
|
515
|
+
eventId,
|
|
516
|
+
envelope.chatId,
|
|
517
|
+
envelope.messageId,
|
|
518
|
+
envelope.timestamp,
|
|
519
|
+
envelope.text,
|
|
520
|
+
].join("|");
|
|
521
|
+
store.appendRawEvent({
|
|
522
|
+
receivedAt: Date.now(),
|
|
523
|
+
dedupeKey,
|
|
524
|
+
eventId,
|
|
525
|
+
envelope,
|
|
526
|
+
raw: json,
|
|
527
|
+
});
|
|
528
|
+
if (!envelope.chatId || !envelope.messageId) {
|
|
529
|
+
console.error("[tuitui-channel] inbound missing chatId/messageId");
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (recentMessageIds.has(dedupeKey) || store.hasSeen(dedupeKey)) {
|
|
533
|
+
console.error(
|
|
534
|
+
`[tuitui-channel] deduped inbound eventId=${eventId} messageId=${envelope.messageId}`,
|
|
535
|
+
);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
recentMessageIds.add(dedupeKey);
|
|
539
|
+
store.rememberSeen(dedupeKey);
|
|
540
|
+
if (recentMessageIds.size > 500) {
|
|
541
|
+
const first = recentMessageIds.values().next().value;
|
|
542
|
+
if (first) recentMessageIds.delete(first);
|
|
543
|
+
}
|
|
544
|
+
console.error(
|
|
545
|
+
`[tuitui-channel] inbound accept eventId=${eventId} messageId=${envelope.messageId} text=${JSON.stringify(envelope.text).slice(0, 300)}`,
|
|
546
|
+
);
|
|
547
|
+
const decision = isAllowed(envelope);
|
|
548
|
+
if (!decision.allowed) {
|
|
549
|
+
console.error(
|
|
550
|
+
`[tuitui-channel] inbound unauthorized reason=${decision.reason} eventId=${eventId} messageId=${envelope.messageId}`,
|
|
551
|
+
);
|
|
552
|
+
await maybeHandleUnauthorized(envelope, decision);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (account.appId && account.appSecret && envelope.atMe) {
|
|
557
|
+
try {
|
|
558
|
+
await emojiReaction(
|
|
559
|
+
account,
|
|
560
|
+
envelope.chatId,
|
|
561
|
+
envelope.chatType,
|
|
562
|
+
envelope.messageId,
|
|
563
|
+
"收到",
|
|
564
|
+
);
|
|
565
|
+
} catch {}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const permissionText = normalizePermissionReplyText(envelope);
|
|
569
|
+
|
|
570
|
+
const persistPermissionMatch =
|
|
571
|
+
PERSIST_PERMISSION_REPLY_RE.exec(permissionText);
|
|
572
|
+
if (persistPermissionMatch) {
|
|
573
|
+
const requestId = persistPermissionMatch[1].toLowerCase();
|
|
574
|
+
await persistPermissionAllow(envelope, requestId);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const permissionMatch = PERMISSION_REPLY_RE.exec(permissionText);
|
|
579
|
+
if (permissionMatch) {
|
|
580
|
+
const requestId = permissionMatch[2].toLowerCase();
|
|
581
|
+
const behavior = permissionMatch[1].toLowerCase().startsWith("y")
|
|
582
|
+
? "allow"
|
|
583
|
+
: "deny";
|
|
584
|
+
await emitPermissionDecision(envelope, requestId, behavior);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const cursor = store.appendInbox({ ...envelope, readAt: null, eventId });
|
|
589
|
+
await emitClaudeChannelNotification({ ...envelope, cursor, eventId });
|
|
590
|
+
console.error(
|
|
591
|
+
`[tuitui-channel] delivered channel notification messageId=${envelope.messageId} cursor=${cursor}`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function ensureWs() {
|
|
596
|
+
if (!account.appId || !account.appSecret) {
|
|
597
|
+
console.error("[tuitui-channel] missing TUITUI_APP_ID / TUITUI_APP_SECRET");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (ws && (ws.readyState === 0 || ws.readyState === 1)) return;
|
|
601
|
+
ws = createWebSocketClient(account, {
|
|
602
|
+
onOpen: () => {
|
|
603
|
+
const meta = store.loadMeta();
|
|
604
|
+
meta.lastWsConnectAt = Date.now();
|
|
605
|
+
store.saveMeta(meta);
|
|
606
|
+
console.error("[tuitui-channel] websocket connected");
|
|
607
|
+
},
|
|
608
|
+
onJson: handleInboundJson,
|
|
609
|
+
onError: (err) =>
|
|
610
|
+
console.error("[tuitui-channel] websocket error", err?.message || err),
|
|
611
|
+
onClose: () => {
|
|
612
|
+
console.error("[tuitui-channel] websocket closed, retry in 5s");
|
|
613
|
+
setTimeout(ensureWs, 5000).unref?.();
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
server.registerTool(
|
|
619
|
+
"tuitui_reply",
|
|
620
|
+
{
|
|
621
|
+
description: "Send a text message back to a TuiTui chat/channel/thread.",
|
|
622
|
+
inputSchema: {
|
|
623
|
+
chat_id: z
|
|
624
|
+
.string()
|
|
625
|
+
.optional()
|
|
626
|
+
.describe(
|
|
627
|
+
"Target chat id, group id, or teams_<team>_<channel>_<thread>",
|
|
628
|
+
),
|
|
629
|
+
sender_id: z
|
|
630
|
+
.string()
|
|
631
|
+
.optional()
|
|
632
|
+
.describe(
|
|
633
|
+
"Direct-chat sender id from notifications/claude/channel meta",
|
|
634
|
+
),
|
|
635
|
+
text: z.string().describe("Message text"),
|
|
636
|
+
chat_type: z
|
|
637
|
+
.enum([CHAT_TYPE_DIRECT, CHAT_TYPE_GROUP, CHAT_TYPE_CHANNEL])
|
|
638
|
+
.optional()
|
|
639
|
+
.describe("Optional explicit chat type"),
|
|
640
|
+
reply_to_post_id: z
|
|
641
|
+
.string()
|
|
642
|
+
.optional()
|
|
643
|
+
.describe(
|
|
644
|
+
"Teams channel reply target post_id for direct in-thread replies",
|
|
645
|
+
),
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
async ({ chat_id, sender_id, text, chat_type, reply_to_post_id }) => {
|
|
649
|
+
const target = chat_id || sender_id;
|
|
650
|
+
if (!target) throw new Error("chat_id or sender_id is required");
|
|
651
|
+
await sendTextMsg(
|
|
652
|
+
account,
|
|
653
|
+
target,
|
|
654
|
+
chat_type || guessChatType(target),
|
|
655
|
+
text,
|
|
656
|
+
{ reply_to_post_id },
|
|
657
|
+
);
|
|
658
|
+
return { content: [{ type: "text", text: `Sent to ${target}` }] };
|
|
659
|
+
},
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
server.registerTool(
|
|
663
|
+
"tuitui_send_media",
|
|
664
|
+
{
|
|
665
|
+
description: "Send an image/file to a TuiTui conversation.",
|
|
666
|
+
inputSchema: {
|
|
667
|
+
chat_id: z.string(),
|
|
668
|
+
media_url: z.string().describe("http(s), local path, or data: URL"),
|
|
669
|
+
chat_type: z
|
|
670
|
+
.enum([CHAT_TYPE_DIRECT, CHAT_TYPE_GROUP, CHAT_TYPE_CHANNEL])
|
|
671
|
+
.optional(),
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
async ({ chat_id, media_url, chat_type }) => {
|
|
675
|
+
await sendMediaMsg(
|
|
676
|
+
account,
|
|
677
|
+
chat_id,
|
|
678
|
+
chat_type || guessChatType(chat_id),
|
|
679
|
+
media_url,
|
|
680
|
+
);
|
|
681
|
+
return { content: [{ type: "text", text: `Media sent to ${chat_id}` }] };
|
|
682
|
+
},
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
server.registerTool(
|
|
686
|
+
"tuitui_react",
|
|
687
|
+
{
|
|
688
|
+
description: "Add a TuiTui emoji reaction to a specific message.",
|
|
689
|
+
inputSchema: {
|
|
690
|
+
chat_id: z.string(),
|
|
691
|
+
message_id: z.string(),
|
|
692
|
+
emoji: z.string().default("收到"),
|
|
693
|
+
chat_type: z
|
|
694
|
+
.enum([CHAT_TYPE_DIRECT, CHAT_TYPE_GROUP, CHAT_TYPE_CHANNEL])
|
|
695
|
+
.optional(),
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
async ({ chat_id, message_id, emoji, chat_type }) => {
|
|
699
|
+
await emojiReaction(
|
|
700
|
+
account,
|
|
701
|
+
chat_id,
|
|
702
|
+
chat_type || guessChatType(chat_id),
|
|
703
|
+
message_id,
|
|
704
|
+
emoji,
|
|
705
|
+
);
|
|
706
|
+
return {
|
|
707
|
+
content: [{ type: "text", text: `Reacted ${emoji} to ${message_id}` }],
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
server.registerTool(
|
|
713
|
+
"tuitui_check_new",
|
|
714
|
+
{
|
|
715
|
+
description: "Quickly check whether new TuiTui messages have arrived.",
|
|
716
|
+
inputSchema: { after_cursor: z.number().optional().default(0) },
|
|
717
|
+
},
|
|
718
|
+
async ({ after_cursor }) => {
|
|
719
|
+
const rows = store.listMessages(20, true, after_cursor);
|
|
720
|
+
return {
|
|
721
|
+
content: [
|
|
722
|
+
{
|
|
723
|
+
type: "text",
|
|
724
|
+
text: rows.length
|
|
725
|
+
? `There are ${rows.length} unread messages.`
|
|
726
|
+
: "No unread messages.",
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
structuredContent: { unread: rows.length, messages: rows },
|
|
730
|
+
};
|
|
731
|
+
},
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
server.registerTool(
|
|
735
|
+
"tuitui_get_messages",
|
|
736
|
+
{
|
|
737
|
+
description:
|
|
738
|
+
"Fetch recent TuiTui inbox messages stored by the local bridge.",
|
|
739
|
+
inputSchema: {
|
|
740
|
+
limit: z.number().min(1).max(100).default(20),
|
|
741
|
+
unread_only: z.boolean().default(false),
|
|
742
|
+
after_cursor: z.number().optional().default(0),
|
|
743
|
+
mark_read: z.boolean().default(false),
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
async ({ limit, unread_only, after_cursor, mark_read }) => {
|
|
747
|
+
const rows = store.listMessages(limit, unread_only, after_cursor);
|
|
748
|
+
if (mark_read && rows.length) {
|
|
749
|
+
const maxCursor = Math.max(...rows.map((x) => Number(x.cursor || 0)));
|
|
750
|
+
store.markRead(maxCursor);
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
content: [
|
|
754
|
+
{
|
|
755
|
+
type: "text",
|
|
756
|
+
text:
|
|
757
|
+
rows
|
|
758
|
+
.map((m) => `[#${m.cursor}] ${m.senderName}: ${m.text}`)
|
|
759
|
+
.join("\n\n") || "No messages.",
|
|
760
|
+
},
|
|
761
|
+
],
|
|
762
|
+
structuredContent: { messages: rows },
|
|
763
|
+
};
|
|
764
|
+
},
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
server.registerTool(
|
|
768
|
+
"tuitui_access",
|
|
769
|
+
{
|
|
770
|
+
description:
|
|
771
|
+
"Manage local pairing / allowlist state for the TuiTui Claude Code channel.",
|
|
772
|
+
inputSchema: {
|
|
773
|
+
action: z.enum([
|
|
774
|
+
"status",
|
|
775
|
+
"pair",
|
|
776
|
+
"allow",
|
|
777
|
+
"remove",
|
|
778
|
+
"policy",
|
|
779
|
+
"group-allow",
|
|
780
|
+
"team-allow",
|
|
781
|
+
]),
|
|
782
|
+
code: z.string().optional(),
|
|
783
|
+
sender_id: z.string().optional(),
|
|
784
|
+
value: z.string().optional(),
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
async ({ action, code, sender_id, value }) => {
|
|
788
|
+
const access = store.loadAccess();
|
|
789
|
+
let text = "";
|
|
790
|
+
if (action === "status") {
|
|
791
|
+
text = JSON.stringify(
|
|
792
|
+
{
|
|
793
|
+
dmPolicy: access.dmPolicy,
|
|
794
|
+
allowFrom: access.allowFrom || [],
|
|
795
|
+
groupPolicy: access.groupPolicy,
|
|
796
|
+
groupAllowFrom: access.groupAllowFrom || [],
|
|
797
|
+
teamAllowFrom: access.teamAllowFrom || [],
|
|
798
|
+
pending: access.pending || {},
|
|
799
|
+
},
|
|
800
|
+
null,
|
|
801
|
+
2,
|
|
802
|
+
);
|
|
803
|
+
} else if (action === "pair") {
|
|
804
|
+
if (!code) throw new Error("code is required");
|
|
805
|
+
const approved = store.approvePairing(code);
|
|
806
|
+
if (!approved) throw new Error(`pairing code not found: ${code}`);
|
|
807
|
+
text = `paired ${approved.senderId}`;
|
|
808
|
+
} else if (action === "allow") {
|
|
809
|
+
if (!sender_id) throw new Error("sender_id is required");
|
|
810
|
+
access.allowFrom = Array.from(
|
|
811
|
+
new Set([...(access.allowFrom || []), normalizeId(sender_id)]),
|
|
812
|
+
);
|
|
813
|
+
store.saveAccess(access);
|
|
814
|
+
text = `allowed ${sender_id}`;
|
|
815
|
+
} else if (action === "remove") {
|
|
816
|
+
if (!sender_id) throw new Error("sender_id is required");
|
|
817
|
+
access.allowFrom = (access.allowFrom || []).filter(
|
|
818
|
+
(x) => normalizeId(x) !== normalizeId(sender_id),
|
|
819
|
+
);
|
|
820
|
+
store.saveAccess(access);
|
|
821
|
+
text = `removed ${sender_id}`;
|
|
822
|
+
} else if (action === "policy") {
|
|
823
|
+
if (!value) throw new Error("value is required");
|
|
824
|
+
access.dmPolicy = value;
|
|
825
|
+
store.saveAccess(access);
|
|
826
|
+
text = `dmPolicy=${value}`;
|
|
827
|
+
} else if (action === "group-allow") {
|
|
828
|
+
if (!value) throw new Error("value is required");
|
|
829
|
+
access.groupAllowFrom = Array.from(
|
|
830
|
+
new Set([...(access.groupAllowFrom || []), normalizeId(value)]),
|
|
831
|
+
);
|
|
832
|
+
store.saveAccess(access);
|
|
833
|
+
text = `groupAllowFrom += ${value}`;
|
|
834
|
+
} else if (action === "team-allow") {
|
|
835
|
+
if (!value) throw new Error("value is required");
|
|
836
|
+
access.teamAllowFrom = Array.from(
|
|
837
|
+
new Set([...(access.teamAllowFrom || []), normalizeId(value)]),
|
|
838
|
+
);
|
|
839
|
+
store.saveAccess(access);
|
|
840
|
+
text = `teamAllowFrom += ${value}`;
|
|
841
|
+
}
|
|
842
|
+
return { content: [{ type: "text", text }] };
|
|
843
|
+
},
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
server.registerTool(
|
|
847
|
+
"tuitui_debug_status",
|
|
848
|
+
{
|
|
849
|
+
description: "Inspect bridge runtime status and local state paths.",
|
|
850
|
+
inputSchema: {},
|
|
851
|
+
},
|
|
852
|
+
async () => {
|
|
853
|
+
const meta = store.loadMeta();
|
|
854
|
+
const access = store.loadAccess();
|
|
855
|
+
return {
|
|
856
|
+
content: [
|
|
857
|
+
{
|
|
858
|
+
type: "text",
|
|
859
|
+
text: JSON.stringify(
|
|
860
|
+
{
|
|
861
|
+
stateDir,
|
|
862
|
+
meta,
|
|
863
|
+
accessSummary: {
|
|
864
|
+
dmPolicy: access.dmPolicy,
|
|
865
|
+
allowFrom: access.allowFrom?.length || 0,
|
|
866
|
+
groupAllowFrom: access.groupAllowFrom?.length || 0,
|
|
867
|
+
teamAllowFrom: access.teamAllowFrom?.length || 0,
|
|
868
|
+
pending: Object.keys(access.pending || {}).length,
|
|
869
|
+
},
|
|
870
|
+
permissionRelaySummary: {
|
|
871
|
+
lastDeliveredChat: meta.lastDeliveredChat,
|
|
872
|
+
lastPermissionRequestAt: meta.lastPermissionRequestAt,
|
|
873
|
+
lastPermissionRelay: meta.lastPermissionRelay,
|
|
874
|
+
lastPermissionDecisionAt: meta.lastPermissionDecisionAt,
|
|
875
|
+
lastPermissionDecision: meta.lastPermissionDecision,
|
|
876
|
+
lastPermissionPersistAt: meta.lastPermissionPersistAt,
|
|
877
|
+
lastPermissionPersist: meta.lastPermissionPersist,
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
null,
|
|
881
|
+
2,
|
|
882
|
+
),
|
|
883
|
+
},
|
|
884
|
+
],
|
|
885
|
+
structuredContent: { stateDir, meta, access },
|
|
886
|
+
};
|
|
887
|
+
},
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
const PermissionRequestSchema = z.object({
|
|
891
|
+
method: z.literal("notifications/claude/channel/permission_request"),
|
|
892
|
+
params: z.object({
|
|
893
|
+
request_id: z.string(),
|
|
894
|
+
tool_name: z.string(),
|
|
895
|
+
description: z.string(),
|
|
896
|
+
input_preview: z.string(),
|
|
897
|
+
}),
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
server.server.setNotificationHandler(
|
|
901
|
+
PermissionRequestSchema,
|
|
902
|
+
async ({ params }) => {
|
|
903
|
+
try {
|
|
904
|
+
await relayPermissionRequest(params);
|
|
905
|
+
} catch (err) {
|
|
906
|
+
traceNotify("permission_request:error", {
|
|
907
|
+
requestId: params?.request_id,
|
|
908
|
+
error: err?.message || String(err),
|
|
909
|
+
});
|
|
910
|
+
console.error(
|
|
911
|
+
"[tuitui-channel] permission relay failed",
|
|
912
|
+
err?.message || err,
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
async function main() {
|
|
919
|
+
logBoot("[BOOT] main() starting");
|
|
920
|
+
ensureWs();
|
|
921
|
+
const transport = new StdioServerTransport();
|
|
922
|
+
await server.connect(transport);
|
|
923
|
+
logBoot("[BOOT] server.connect completed");
|
|
924
|
+
console.error("[tuitui-channel] MCP server running");
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
main()
|
|
928
|
+
.then(() => {
|
|
929
|
+
logBoot("[BOOT] main resolved");
|
|
930
|
+
})
|
|
931
|
+
.catch((err) => {
|
|
932
|
+
logBoot(`[BOOT] fatal ${err?.stack || err}`);
|
|
933
|
+
console.error("[tuitui-channel] fatal", err);
|
|
934
|
+
process.exit(1);
|
|
935
|
+
});
|