oh-my-im 0.1.10 → 0.1.12
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/README.md +3 -2
- package/dist/bot-app.js +32 -2
- package/dist/dashboard-worker.js +89 -3
- package/dist/dws-dashboard.js +33 -9
- package/dist/dws-listener.js +11 -0
- package/dist/dws-rule-history.js +96 -0
- package/dist/logger.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,9 +75,9 @@ omi
|
|
|
75
75
|
http://127.0.0.1:12525
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Web 管理页、DWS 群监听器和钉钉机器人单聊分别运行在独立进程中。即使 DWS 尚未登录、机器人凭证尚未配置或单聊授权人员为空,管理页也可以正常打开。完成机器人凭证、授权人员和监听规则配置,然后点击“保存并生效”。配置修改会被运行中的进程自动读取,一般不需要重启。
|
|
79
79
|
|
|
80
|
-
>
|
|
80
|
+
> 单聊机器人在没有授权人员时会以 deny-all 模式启动,所有单聊消息都会被拒绝;在管理页添加授权人员后即可使用。
|
|
81
81
|
|
|
82
82
|
---
|
|
83
83
|
|
|
@@ -101,6 +101,7 @@ omi -h # 查看帮助
|
|
|
101
101
|
omi
|
|
102
102
|
├─ 检查 ~/.oh-my-im/omi-state.json
|
|
103
103
|
├─ 检查是否已有未受 omi 管理的群监听进程
|
|
104
|
+
├─ 后台启动 dashboard-worker(本地管理页)
|
|
104
105
|
├─ 后台启动 dws-listener(默认模式)
|
|
105
106
|
├─ 后台启动 bot-worker
|
|
106
107
|
├─ 将 stdout/stderr 写入 ~/.oh-my-im/omi.log
|
package/dist/bot-app.js
CHANGED
|
@@ -475,6 +475,10 @@ export async function runApp(configOverride, options = {}) {
|
|
|
475
475
|
}
|
|
476
476
|
state.paused = true;
|
|
477
477
|
state.abort();
|
|
478
|
+
// The task's card timers are owned by the message handler. Mark the
|
|
479
|
+
// task stopped immediately so a delayed update cannot restore the
|
|
480
|
+
// "处理中" title after the user has paused it.
|
|
481
|
+
state.stopCardUpdates?.();
|
|
478
482
|
return;
|
|
479
483
|
}
|
|
480
484
|
if (control && typeof control === "object") {
|
|
@@ -553,14 +557,28 @@ export async function runApp(configOverride, options = {}) {
|
|
|
553
557
|
try {
|
|
554
558
|
let latestStats = {};
|
|
555
559
|
let lastUpdateAt = 0;
|
|
556
|
-
const cardUpdateInterval = Math.max(500, options.getCardUpdateIntervalMs?.() ?? 3_000);
|
|
557
560
|
let pendingUpdate;
|
|
561
|
+
let cardUpdatesStopped = false;
|
|
562
|
+
const getCardUpdateInterval = () => Math.max(500, options.getCardUpdateIntervalMs?.() ?? 3_000);
|
|
563
|
+
const stopCardUpdates = () => {
|
|
564
|
+
cardUpdatesStopped = true;
|
|
565
|
+
if (pendingUpdate)
|
|
566
|
+
clearTimeout(pendingUpdate);
|
|
567
|
+
pendingUpdate = undefined;
|
|
568
|
+
};
|
|
569
|
+
state.stopCardUpdates = stopCardUpdates;
|
|
570
|
+
stopCardUpdatesForCurrentTask = stopCardUpdates;
|
|
558
571
|
const updateCard = (title, content, force = false) => {
|
|
572
|
+
if (cardUpdatesStopped)
|
|
573
|
+
return;
|
|
574
|
+
const cardUpdateInterval = getCardUpdateInterval();
|
|
559
575
|
latestCardContent = content;
|
|
560
576
|
if (reply.mode !== "card")
|
|
561
577
|
return;
|
|
562
578
|
const now = Date.now();
|
|
563
579
|
const run = () => {
|
|
580
|
+
if (cardUpdatesStopped)
|
|
581
|
+
return;
|
|
564
582
|
lastUpdateAt = Date.now();
|
|
565
583
|
pendingUpdate = undefined;
|
|
566
584
|
bot.updateReply(reply, title, content, { fallbackToText: false }).catch((err) => {
|
|
@@ -581,7 +599,8 @@ export async function runApp(configOverride, options = {}) {
|
|
|
581
599
|
}
|
|
582
600
|
};
|
|
583
601
|
elapsedTimer = setInterval(() => {
|
|
584
|
-
|
|
602
|
+
if (!cardUpdatesStopped)
|
|
603
|
+
updateCard(processingTitle(), latestCardContent);
|
|
585
604
|
}, 1_000);
|
|
586
605
|
elapsedTimer.unref();
|
|
587
606
|
prompt = await buildCodexPrompt(bot, message, state.selectedSessions[agent]?.cwd ?? state.defaultWorkDir);
|
|
@@ -608,7 +627,10 @@ export async function runApp(configOverride, options = {}) {
|
|
|
608
627
|
throw new Error("Agent task paused by user");
|
|
609
628
|
if (pendingUpdate) {
|
|
610
629
|
clearTimeout(pendingUpdate);
|
|
630
|
+
pendingUpdate = undefined;
|
|
611
631
|
}
|
|
632
|
+
state.stopCardUpdates = undefined;
|
|
633
|
+
stopCardUpdatesForCurrentTask = undefined;
|
|
612
634
|
state.sessions[agent] = result.sessionId ?? state.sessions[agent];
|
|
613
635
|
const toolCount = Object.values(result.toolStats).reduce((total, count) => total + count, 0);
|
|
614
636
|
const note = `处理详情 · 1 条消息 · ${toolCount} 次工具调用`;
|
|
@@ -635,6 +657,9 @@ export async function runApp(configOverride, options = {}) {
|
|
|
635
657
|
elapsedTimer = undefined;
|
|
636
658
|
}
|
|
637
659
|
if (state.paused) {
|
|
660
|
+
stopCardUpdatesForCurrentTask?.();
|
|
661
|
+
state.stopCardUpdates = undefined;
|
|
662
|
+
stopCardUpdatesForCurrentTask = undefined;
|
|
638
663
|
log.info(`${label} task paused by user`);
|
|
639
664
|
await bot.updateReply(reply, finishedTitle("🔴", "处理暂停"), latestCardContent);
|
|
640
665
|
await appendConversationLog({
|
|
@@ -648,6 +673,9 @@ export async function runApp(configOverride, options = {}) {
|
|
|
648
673
|
}, [{ senderName: message.senderNick || message.senderId, senderId: message.senderStaffId || message.senderId, content: prompt }]);
|
|
649
674
|
}
|
|
650
675
|
else {
|
|
676
|
+
stopCardUpdatesForCurrentTask?.();
|
|
677
|
+
state.stopCardUpdates = undefined;
|
|
678
|
+
stopCardUpdatesForCurrentTask = undefined;
|
|
651
679
|
log.error(`${label} execution failed`, err);
|
|
652
680
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
653
681
|
await bot.updateReply(reply, finishedTitle("❌", "处理失败"), `${label} 执行失败:${errorMessage}`);
|
|
@@ -671,6 +699,7 @@ export async function runApp(configOverride, options = {}) {
|
|
|
671
699
|
finally {
|
|
672
700
|
state.busy = false;
|
|
673
701
|
state.abort = undefined;
|
|
702
|
+
state.stopCardUpdates = undefined;
|
|
674
703
|
state.steer = undefined;
|
|
675
704
|
state.activeAgent = undefined;
|
|
676
705
|
state.paused = false;
|
|
@@ -699,6 +728,7 @@ export async function runApp(configOverride, options = {}) {
|
|
|
699
728
|
bot.stop();
|
|
700
729
|
process.exit(0);
|
|
701
730
|
});
|
|
731
|
+
let stopCardUpdatesForCurrentTask;
|
|
702
732
|
let privateChatConnected = false;
|
|
703
733
|
let privateChatStarting = false;
|
|
704
734
|
let privateChatGeneration = 0;
|
package/dist/dashboard-worker.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdir, readFile, writeFile, rename } from "node:fs/promises";
|
|
2
|
+
import { mkdir, open, readFile, stat, writeFile, rename } from "node:fs/promises";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { startDashboard, } from "./dws-dashboard.js";
|
|
6
8
|
import { getDwsAuthStatus, getCurrentDwsUser, startDwsDeviceLogin, getDwsDeviceLoginOutput, logoutDws, searchGroups, listGroupMembers, searchUsers, searchBots, } from "./dws-client.js";
|
|
7
9
|
const dataDir = join(homedir(), ".oh-my-im");
|
|
8
10
|
const configFile = join(dataDir, "dws-dashboard.json");
|
|
9
11
|
const serverFile = join(dataDir, "dws-dashboard-server.json");
|
|
10
12
|
const botStatusFile = join(dataDir, "omi-bot-status.json");
|
|
13
|
+
const stateFile = join(dataDir, "omi-state.json");
|
|
14
|
+
const logFile = join(dataDir, "omi.log");
|
|
15
|
+
const workerDir = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const omiPath = join(workerDir, "omi.js");
|
|
17
|
+
const packageFile = join(new URL(".", import.meta.url).pathname, "..", "package.json");
|
|
11
18
|
const repliesDir = join(dataDir, "replies");
|
|
12
19
|
const defaultConfig = () => ({
|
|
13
20
|
privateChatEnabled: true,
|
|
@@ -16,6 +23,8 @@ const defaultConfig = () => ({
|
|
|
16
23
|
historyGroupLimit: 10,
|
|
17
24
|
historyMessageLimit: 20,
|
|
18
25
|
historyPollIntervalSeconds: 5,
|
|
26
|
+
ruleHistoryMessageLimit: 20,
|
|
27
|
+
ruleHistoryPollIntervalSeconds: 5,
|
|
19
28
|
targets: [], botAllowedUserIds: [], botAllowedUserNames: {},
|
|
20
29
|
botSuperAdminUserIds: [], botSuperAdminUserNames: {}, robotSenderOpenDingTalkId: "",
|
|
21
30
|
commandKeywords: { pause: [], monitorOpen: [], monitorStop: [], switchPi: [], switchCodex: [] },
|
|
@@ -72,6 +81,77 @@ async function loadReplies() {
|
|
|
72
81
|
return [];
|
|
73
82
|
}
|
|
74
83
|
}
|
|
84
|
+
function processIsRunning(pid) {
|
|
85
|
+
try {
|
|
86
|
+
process.kill(pid, 0);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function systemStatus() {
|
|
94
|
+
let state = {};
|
|
95
|
+
try {
|
|
96
|
+
state = JSON.parse(await readFile(stateFile, "utf8"));
|
|
97
|
+
}
|
|
98
|
+
catch { /* report discovered processes below */ }
|
|
99
|
+
const ps = spawnSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf8" });
|
|
100
|
+
const rows = ps.status === 0 && typeof ps.stdout === "string" ? ps.stdout.split("\n").flatMap((line) => {
|
|
101
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
102
|
+
return match ? [{ pid: Number(match[1]), ppid: Number(match[2]), command: match[3] }] : [];
|
|
103
|
+
}) : [];
|
|
104
|
+
const roles = new Map();
|
|
105
|
+
for (const item of state.processes ?? [])
|
|
106
|
+
if (item.pid && item.role)
|
|
107
|
+
roles.set(item.pid, item.role);
|
|
108
|
+
rows.filter((item) => /\/dist\/(dashboard-worker|dws-listener|bot-worker)\.js(?:\s|$)/.test(item.command)).forEach((item) => {
|
|
109
|
+
if (!roles.has(item.pid))
|
|
110
|
+
roles.set(item.pid, item.command.match(/\/dist\/([^/]+)\.js/)?.[1] || "worker");
|
|
111
|
+
});
|
|
112
|
+
const processes = [...roles.entries()].map(([pid, role]) => {
|
|
113
|
+
const row = rows.find((item) => item.pid === pid);
|
|
114
|
+
return { role, pid, ppid: row?.ppid, running: processIsRunning(pid), command: row?.command };
|
|
115
|
+
});
|
|
116
|
+
return { mode: state.mode || "unknown", startedAt: state.startedAt, workspace: state.workspace, processes, checkedAt: new Date().toISOString() };
|
|
117
|
+
}
|
|
118
|
+
async function systemLogs(offset) {
|
|
119
|
+
try {
|
|
120
|
+
const info = await stat(logFile);
|
|
121
|
+
const initialTailBytes = 100_000;
|
|
122
|
+
const requestedOffset = offset === undefined ? Math.max(0, info.size - initialTailBytes) : Math.floor(offset);
|
|
123
|
+
const reset = requestedOffset > info.size;
|
|
124
|
+
const start = reset ? Math.max(0, info.size - initialTailBytes) : Math.max(0, requestedOffset);
|
|
125
|
+
const length = info.size - start;
|
|
126
|
+
const handle = await open(logFile, "r");
|
|
127
|
+
try {
|
|
128
|
+
const buffer = Buffer.alloc(length);
|
|
129
|
+
if (length > 0)
|
|
130
|
+
await handle.read(buffer, 0, length, start);
|
|
131
|
+
let content = buffer.toString("utf8");
|
|
132
|
+
// An initial tail may begin in the middle of a line; incremental reads
|
|
133
|
+
// always start at the exact previous byte offset and must stay intact.
|
|
134
|
+
if (offset === undefined && start > 0)
|
|
135
|
+
content = content.replace(/^[^\n]*\n/, "");
|
|
136
|
+
return { content, path: logFile, size: info.size, nextOffset: info.size, reset };
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
await handle.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
|
|
144
|
+
if (code === "ENOENT")
|
|
145
|
+
return { content: "", path: logFile, size: 0, nextOffset: 0, reset: false };
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function restartSystem() {
|
|
150
|
+
// Delay the detached manager so the HTTP 202 response can reach the browser
|
|
151
|
+
// before this dashboard worker is terminated as part of the restart.
|
|
152
|
+
const child = spawn(process.execPath, [omiPath, "restart"], { cwd: process.cwd(), detached: true, stdio: "ignore" });
|
|
153
|
+
child.unref();
|
|
154
|
+
}
|
|
75
155
|
async function botStatus() {
|
|
76
156
|
const config = await loadConfig();
|
|
77
157
|
try {
|
|
@@ -83,6 +163,11 @@ async function botStatus() {
|
|
|
83
163
|
}
|
|
84
164
|
}
|
|
85
165
|
async function main() {
|
|
166
|
+
let version = "unknown";
|
|
167
|
+
try {
|
|
168
|
+
version = JSON.parse(await readFile(packageFile, "utf8")).version?.trim() || version;
|
|
169
|
+
}
|
|
170
|
+
catch { /* package metadata is optional in development */ }
|
|
86
171
|
// Do not create a partial config here. The listener owns initialization and
|
|
87
172
|
// migration; this process only reads it and writes complete configurations
|
|
88
173
|
// submitted by the dashboard.
|
|
@@ -114,7 +199,8 @@ async function main() {
|
|
|
114
199
|
searchGroups, listGroupMembers, searchUsers, searchBots,
|
|
115
200
|
getCurrentDwsUser: async () => ({ ...(await getCurrentDwsUser()), auth: await getDwsAuthStatus() }),
|
|
116
201
|
getDwsAuthStatus, startDwsDeviceLogin, getDwsDeviceLoginOutput, logoutDws, getBotStatus: botStatus,
|
|
117
|
-
|
|
202
|
+
getSystemStatus: systemStatus, getSystemLogs: systemLogs, restartSystem,
|
|
203
|
+
}, { host: serverConfig.host, version });
|
|
118
204
|
console.log(`[OmiDashboard] dashboard started at http://${serverConfig.host}:${serverConfig.port}`);
|
|
119
205
|
}
|
|
120
206
|
main().catch((err) => {
|
package/dist/dws-dashboard.js
CHANGED
|
@@ -34,6 +34,10 @@ function normalizeConfig(value) {
|
|
|
34
34
|
const historyPollIntervalSeconds = source.historyPollIntervalSeconds === undefined ? 5 : Number(source.historyPollIntervalSeconds);
|
|
35
35
|
if (!Number.isFinite(historyPollIntervalSeconds) || historyPollIntervalSeconds < 0 || historyPollIntervalSeconds > 3600)
|
|
36
36
|
throw new Error("历史消息拉取间隔需为 0-3600 秒");
|
|
37
|
+
const ruleHistoryMessageLimit = positiveInteger(source.ruleHistoryMessageLimit, 20, "群监控绑定每群消息数量");
|
|
38
|
+
const ruleHistoryPollIntervalSeconds = source.ruleHistoryPollIntervalSeconds === undefined ? 5 : Number(source.ruleHistoryPollIntervalSeconds);
|
|
39
|
+
if (!Number.isFinite(ruleHistoryPollIntervalSeconds) || ruleHistoryPollIntervalSeconds < 0 || ruleHistoryPollIntervalSeconds > 3600)
|
|
40
|
+
throw new Error("群监控绑定拉取间隔需为 0-3600 秒");
|
|
37
41
|
if (!Array.isArray(source.targets))
|
|
38
42
|
throw new Error("钉钉规则格式无效");
|
|
39
43
|
const keys = new Set();
|
|
@@ -96,7 +100,7 @@ function normalizeConfig(value) {
|
|
|
96
100
|
};
|
|
97
101
|
if (typeof source.groupPromptPrefix !== "string")
|
|
98
102
|
throw new Error("消息前缀格式无效");
|
|
99
|
-
return { privateChatEnabled: source.privateChatEnabled !== false, cardUpdateIntervalMs, showElapsed: source.showElapsed !== false, historyGroupLimit, historyMessageLimit, historyPollIntervalSeconds, targets, botAllowedUserIds, botAllowedUserNames, botSuperAdminUserIds, botSuperAdminUserNames, robotSenderOpenDingTalkId: typeof source.robotSenderOpenDingTalkId === "string" ? source.robotSenderOpenDingTalkId.trim() : "", commandKeywords, groupPromptPrefix: source.groupPromptPrefix.trim(), replyFormat: source.replyFormat, robotName: source.robotName.trim(), clientId: source.clientId.trim(), clientSecret: source.clientSecret.trim(), agent: source.agent };
|
|
103
|
+
return { privateChatEnabled: source.privateChatEnabled !== false, cardUpdateIntervalMs, showElapsed: source.showElapsed !== false, historyGroupLimit, historyMessageLimit, historyPollIntervalSeconds, ruleHistoryMessageLimit, ruleHistoryPollIntervalSeconds, targets, botAllowedUserIds, botAllowedUserNames, botSuperAdminUserIds, botSuperAdminUserNames, robotSenderOpenDingTalkId: typeof source.robotSenderOpenDingTalkId === "string" ? source.robotSenderOpenDingTalkId.trim() : "", commandKeywords, groupPromptPrefix: source.groupPromptPrefix.trim(), replyFormat: source.replyFormat, robotName: source.robotName.trim(), clientId: source.clientId.trim(), clientSecret: source.clientSecret.trim(), agent: source.agent };
|
|
100
104
|
}
|
|
101
105
|
export function startDashboard(port, hooks, options = {}) {
|
|
102
106
|
const host = options.host?.trim() || "127.0.0.1";
|
|
@@ -119,7 +123,7 @@ export function startDashboard(port, hooks, options = {}) {
|
|
|
119
123
|
}
|
|
120
124
|
if (request.method === "GET" && url.pathname === "/") {
|
|
121
125
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
122
|
-
response.end(PAGE);
|
|
126
|
+
response.end(PAGE.replaceAll("__OMI_VERSION__", options.version ? `v${options.version.replace(/^v/i, "")}` : "unknown"));
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
125
129
|
if (request.method === "GET" && url.pathname === "/favicon.ico") {
|
|
@@ -131,6 +135,26 @@ export function startDashboard(port, hooks, options = {}) {
|
|
|
131
135
|
sendJson(response, 200, { config: publicConfig(hooks.getConfig()), status: hooks.getStatus(), replies: hooks.getReplies().slice(0, 10) });
|
|
132
136
|
return;
|
|
133
137
|
}
|
|
138
|
+
if (request.method === "GET" && url.pathname === "/api/system-status") {
|
|
139
|
+
if (!hooks.getSystemStatus)
|
|
140
|
+
return sendJson(response, 501, { error: "系统监控不可用" });
|
|
141
|
+
void hooks.getSystemStatus().then((status) => sendJson(response, 200, { status })).catch((err) => sendJson(response, 500, { error: err instanceof Error ? err.message : String(err) }));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (request.method === "GET" && url.pathname === "/api/system-logs") {
|
|
145
|
+
if (!hooks.getSystemLogs)
|
|
146
|
+
return sendJson(response, 501, { error: "系统日志不可用" });
|
|
147
|
+
const rawOffset = url.searchParams.get("offset");
|
|
148
|
+
const offset = rawOffset === null ? undefined : Number(rawOffset);
|
|
149
|
+
void hooks.getSystemLogs(Number.isFinite(offset) && offset >= 0 ? offset : undefined).then((logs) => sendJson(response, 200, logs)).catch((err) => sendJson(response, 500, { error: err instanceof Error ? err.message : String(err) }));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (request.method === "POST" && url.pathname === "/api/system-restart") {
|
|
153
|
+
if (!hooks.restartSystem)
|
|
154
|
+
return sendJson(response, 501, { error: "系统重启不可用" });
|
|
155
|
+
void hooks.restartSystem().then(() => sendJson(response, 202, { restarting: true })).catch((err) => sendJson(response, 500, { error: err instanceof Error ? err.message : String(err) }));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
134
158
|
if (request.method === "GET" && url.pathname === "/api/users") {
|
|
135
159
|
const query = url.searchParams.get("q")?.trim() ?? "";
|
|
136
160
|
if (!query)
|
|
@@ -237,15 +261,15 @@ export function startDashboard(port, hooks, options = {}) {
|
|
|
237
261
|
}
|
|
238
262
|
const PAGE = String.raw `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" type="image/x-icon" href="/favicon.ico"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32.png"><title>oh-my-im 控制台</title><style>
|
|
239
263
|
:root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#172033;background:#f4f6fa}*{box-sizing:border-box}body{margin:0}.wrap{max-width:1240px;margin:auto;padding:30px 24px 56px}header{display:flex;justify-content:space-between;gap:18px;margin-bottom:22px}h1{font-size:26px;margin:0}h2{font-size:17px;margin:0 0 14px}p{margin:6px 0;color:#596579}.status{font-size:13px;padding:8px 11px;border:1px solid #c8d1df;background:#fff;border-radius:6px;height:max-content}.connected{color:#047857}.stopped,.error{color:#b42318}.summary{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:18px}.metric,.panel{background:#fff;border:1px solid #dbe2ed;border-radius:7px}.metric{padding:12px}.metric small,.meta{color:#657287}.metric strong{display:block;font-size:18px;margin-top:5px}.grid{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:18px}.panel{padding:20px}.rule{border-top:1px solid #e7edf5;padding:16px 0}.rule:first-of-type{border-top:0;padding-top:0}.rule-grid{display:grid;grid-template-columns:minmax(380px,1.1fr) minmax(280px,1fr) 32px;gap:12px;align-items:start}.configured-grid{grid-template-columns:minmax(280px,.8fr) minmax(380px,1.2fr) 32px;align-items:center}.field{min-width:0}.field label{display:block;font-size:12px;font-weight:600;color:#526176;margin-bottom:6px}.rule-value{min-height:32px;display:flex;align-items:center;font-size:14px;color:#172033}.people{display:flex;flex-wrap:wrap;gap:6px}.person{padding:4px 8px;background:#f1f5fa;border-radius:4px;font-size:13px}.line{display:flex;gap:7px}.platform-link{display:inline-flex;align-items:center;justify-content:center;height:22px;padding:0 6px;border:1px solid #c9d3e0;border-radius:4px;background:#f5f8fc;color:#175cd3;font-size:11px;font-weight:400;text-decoration:none;white-space:nowrap;vertical-align:middle}.platform-link:hover{border-color:#8bb5ed;background:#edf5ff}.field input,.field select{width:100%;height:36px;padding:7px 9px;border:1px solid #c9d3e0;border-radius:5px;background:#fff;font:14px inherit;color:#172033}.field input:focus,.field select:focus{outline:2px solid #b9d7ff;border-color:#1677ff}.group-picker select{margin-top:7px}.member-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px 8px;max-height:106px;overflow:auto;border:1px solid #c9d3e0;border-radius:5px;background:#fff;padding:4px}.member-option{display:flex;align-items:center;min-width:0;min-height:28px;gap:10px;padding:4px 6px;font-size:13px;line-height:20px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.member-option:hover{background:#f2f7ff}.member-option input{width:14px;height:14px;flex:0 0 auto;align-self:center;margin:0;accent-color:#1677ff}.member-name{display:block;margin-left:2px;overflow:hidden;text-overflow:ellipsis}.member-role{color:#657287;font-size:11px;overflow:hidden;text-overflow:ellipsis}.search{height:36px;border:1px solid #a9c8f5;background:#eef5ff;color:#1769c2;border-radius:5px;cursor:pointer;padding:0 12px;font:13px inherit;white-space:nowrap}.search:hover{border-color:#79a9e8;background:#e2efff;color:#1058aa}.delete{height:36px;width:32px;border:0;background:transparent;color:#b42318;border-radius:5px;cursor:pointer;font-size:20px}.delete:hover{background:#fff0f0}.members{grid-column:1/3;font-size:12px;color:#657287;min-height:18px;padding-top:1px}.toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:16px;padding-top:16px;border-top:1px solid #e7edf5}.button{border:0;border-radius:5px;background:#1677ff;color:#fff;padding:9px 13px;font:inherit;cursor:pointer}.secondary{background:#eef4ff;color:#175cd3}.format{display:flex;gap:10px;align-items:center;font-size:14px}.notice{position:fixed;left:50%;bottom:82px;transform:translateX(-50%);z-index:20;display:none;min-width:220px;max-width:calc(100vw - 32px);padding:10px 16px;margin:0;border:1px solid #b7ebc6;border-radius:6px;background:#f0fff4;color:#166534;box-shadow:0 4px 14px rgba(0,0,0,.12);font-size:13px;text-align:center}.notice:not(:empty){display:block}.notice.error{border-color:#f3b4b4;background:#fff1f0;color:#b42318}.reply{border-top:1px solid #e8edf4;padding:15px 0}.reply:first-of-type{border-top:0;padding:0}.meta{font-size:12px;margin-bottom:7px}.reply pre{white-space:pre-wrap;word-break:break-word;margin:0;font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}.empty{color:#657287;font-size:14px}@media(max-width:980px){.grid{grid-template-columns:1fr}.rule-grid,.configured-grid{grid-template-columns:minmax(300px,1fr) minmax(260px,1fr) 32px}.members{grid-column:1/3}}@media(max-width:640px){.wrap{padding:22px 14px 40px}header{display:block}.status{display:inline-block;margin-top:14px}.summary{grid-template-columns:1fr 1fr}.rule-grid,.configured-grid{grid-template-columns:1fr}.member-list{grid-template-columns:1fr 1fr}.members{grid-column:1}.delete{position:absolute;right:0;top:12px}.rule{position:relative;padding-right:38px}.toolbar{align-items:flex-start;flex-wrap:wrap}.format{order:3;width:100%}}
|
|
240
|
-
.agent-settings{margin-bottom:18px}.agent-settings .agent-row{max-width:520px}.dws-settings{margin-bottom:18px}.dws-settings h2{display:none}.private-chat-settings{margin-bottom:18px}.private-chat-settings h2,.history-settings h2{display:none}.history-settings{margin-bottom:18px}.history-settings h2{display:block;margin:0 0 14px;color:#26354d;font-size:15px}.private-chat-row{display:flex;align-items:center;gap:14px;min-height:54px}.private-chat-row>div:first-child{flex:1}.private-chat-row label{display:block;color:#26354d;font-size:15px;font-weight:600}.private-chat-row p{margin:4px 0 0;color:#718096;font-size:12px}.private-chat-row strong{min-width:58px;color:#657287;font-size:12px}.switch{display:inline-flex!important;align-items:center;cursor:pointer}.switch input{position:absolute;opacity:0;width:1px!important;height:1px!important}.switch span{position:relative;display:block;width:44px;height:24px;border-radius:20px;background:#cbd5e1;transition:.2s}.switch span::after{content:'';position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #64748b;transition:.2s}.switch input:checked+span{background:#1769e0}.switch input:checked+span::after{left:23px}.dws-actions{display:flex;gap:10px;margin-bottom:14px}.check-field{display:flex!important;align-items:center;gap:8px;font-weight:400!important}.check-field input{width:16px!important;height:16px!important;margin:0!important}.history-config{grid-column:1/-1;margin-top:4px;padding:14px 16px;border:1px solid #e1e7ef;border-radius:6px;background:#f8fafc}.history-config h3{margin:0 0 12px;color:#26354d;font-size:14px}.history-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px 16px}.field-help{margin-top:5px;color:#718096;font-size:11px}.dws-device-panel{display:none;margin-bottom:14px;padding:14px 16px;border:1px solid #9fc4f5;border-radius:6px;background:#f5f9ff}.dws-device-panel.visible{display:block}.dws-device-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px;color:#174f9f;font-size:13px}.dws-device-head .button{height:32px;padding:0 10px}.dws-device-output{padding:11px 12px;border:1px solid #d6e3f5;border-radius:5px;background:#fff;color:#172033;font:12px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-all}.dws-device-output a{color:#175cd3;text-decoration:underline}.dws-device-tip{margin-top:9px;color:#5c6d82;font-size:12px;line-height:1.5}.danger-button{background:#fff1f0;color:#b42318;border:1px solid #f1b8b5}.dws-status-box{padding:14px 16px;border:1px solid #dfe6ef;border-radius:6px;background:#f8fafc}.dws-auth-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px 14px}.dws-auth-fields>div{padding:11px 12px;border:1px solid #e2e8f0;border-radius:6px;background:#fff}.dws-auth-fields span{display:block;margin-bottom:5px;color:#718096;font-size:11px}.dws-auth-fields strong,.dws-auth-fields code{display:block;color:#172033;font-size:13px;word-break:break-all}.dws-auth-fields code{font:12px ui-monospace,SFMono-Regular,Menlo,monospace}.dws-status-box pre{margin:0;white-space:pre-wrap;word-break:break-word;font:13px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace;color:#172033}.dws-status-loading{color:#657287;font-size:13px}.dws-login-guide{margin-top:12px;padding-top:12px;border-top:1px solid #e1e7ef;color:#526176;font-size:13px;line-height:1.6}.dws-login-guide a{color:#175cd3}.dws-help{margin:12px 0 0;color:#657287;font-size:12px;line-height:1.5}.agent-command-grid{margin-top:16px;padding-top:14px;border-top:1px solid #e7edf5}.prompt-settings{margin-bottom:18px}.prompt-settings textarea{width:100%;min-height:220px;padding:10px 11px;border:1px solid #c9d3e0;border-radius:5px;resize:vertical;font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}.prompt-help{margin-top:6px;color:#657287;font-size:12px;line-height:1.5}.section-toggle{display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;margin:0}.section-toggle::after{content:'展开';font-size:13px;font-weight:400;color:#175cd3}.collapsible-panel.expanded>.section-toggle::after{content:'收起'}.collapsible-panel:not(.expanded)>.collapsible-content{display:none}.collapsible-panel.expanded>.collapsible-content{margin-top:14px}.bot-user-results{display:flex;flex-direction:column;gap:5px;margin-top:7px;max-height:180px;overflow:auto}.bot-user-result,.bot-user-chip{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 9px;border:1px solid #dbe2ed;border-radius:5px;background:#f8fafc;font-size:13px}.super-admin-field{display:none!important;grid-column:1/-1;width:100%;padding:14px 16px;border:1px solid #dbe2ed;border-radius:8px;background:#f8fafc}.super-admin-field>label{display:flex;align-items:center;gap:8px;width:100%;margin:0 0 0;color:#172033;font-size:13px;line-height:20px}.super-admin-field>label::before{content:none;display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:4px;background:#eaf2ff;color:#175cd3;font-size:11px;font-weight:700}.super-admin-help{margin-top:5px;color:#657287;font-size:12px;line-height:1.5}.super-admin-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:10px;width:100%;max-height:none}.super-admin-list .bot-user-result{justify-content:flex-start;min-height:42px;padding:9px 11px;background:#fff;cursor:pointer;transition:border-color .15s,background .15s}.super-admin-list .bot-user-result:has(input:checked){border-color:#86b7ff;background:#eef6ff}.super-admin-list .bot-user-result input{width:16px;height:16px;margin:0;accent-color:#1677ff}.super-admin-list .bot-user-result span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bot-user-result{width:100%;text-align:left;cursor:pointer}.bot-user-result:hover{background:#eef5ff;border-color:#9fc4f5}.bot-user-chip{display:inline-flex;margin:6px 6px 0 0;background:#eef4ff;border-color:#c5dafa;color:#175cd3}.bot-user-chip button{border:0;background:transparent;color:#175cd3;cursor:pointer;font-size:16px;line-height:1;padding:0}.bot-user-selected{margin-top:6px;display:flex;flex-wrap:wrap}.save-floating{position:fixed;left:50%;bottom:10px;transform:translateX(-50%);z-index:10;background:#16a34a;color:#fff;box-shadow:0 4px 14px rgba(22,163,74,.35)}.rules-panel{margin-bottom:18px}.rules-toggle{display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;margin:0}.rules-toggle::after{content:'展开';font-size:13px;font-weight:400;color:#175cd3}.rules-panel.expanded .rules-toggle::after{content:'收起'}.rules-panel:not(.expanded) .rules-content{display:none}.rules-panel.expanded .rules-content{margin-top:14px}.command-settings{margin-bottom:18px}.command-toggle{display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;margin:0}.command-toggle::after{content:'展开';font-size:13px;font-weight:400;color:#175cd3}.command-settings.expanded .command-toggle::after{content:'收起'}.command-settings:not(.expanded) .command-grid{display:none}.command-settings.expanded .command-grid{margin-top:14px}.command-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 16px}.command-grid input{width:100%;height:36px;padding:7px 9px;border:1px solid #c9d3e0;border-radius:5px;background:#fff;font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.binding-commands{margin-bottom:18px;padding:14px 16px;border:1px solid #e1e8f2;border-radius:6px;background:#f8fafc}.binding-commands h3{margin:0 0 12px;font-size:14px;color:#26354d}.binding-help{margin:-5px 0 12px;color:#657287;font-size:12px;line-height:1.5}@media(max-width:640px){.command-grid{grid-template-columns:1fr}.dws-auth-fields{grid-template-columns:1fr}.dws-actions{display:grid;grid-template-columns:1fr}.dws-actions .button{width:100%}}.agent-options{display:flex;gap:18px;align-items:center;flex-wrap:wrap}.agent-options label{display:flex;align-items:center;gap:6px;font-size:14px;cursor:pointer}.agent-options input{width:16px;height:16px;accent-color:#1677ff}.robot-settings{margin-bottom:28px}.robot-settings .robot-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 16px;align-items:start}@media(max-width:640px){.agent-settings .agent-row{max-width:none}.robot-settings .robot-row{grid-template-columns:1fr}.super-admin-list{grid-template-columns:1fr}}
|
|
264
|
+
.agent-settings{margin-bottom:18px}.agent-settings .agent-row{max-width:520px}.dws-settings{margin-bottom:18px}.dws-settings h2{display:none}.private-chat-settings{margin-bottom:18px}.private-chat-settings h2,.history-settings h2,.rule-history-settings h2{display:none}.history-settings,.rule-history-settings{margin-bottom:18px}.history-settings h2,.rule-history-settings h2{display:block;margin:0 0 14px;color:#26354d;font-size:15px}.private-chat-row{display:flex;align-items:center;gap:14px;min-height:54px}.private-chat-row>div:first-child{flex:1}.private-chat-title{display:flex;align-items:center;gap:12px}.private-chat-row label{display:block;color:#26354d;font-size:15px;font-weight:600}.private-chat-row p{margin:4px 0 0;color:#718096;font-size:12px}.private-chat-row strong{min-width:58px;color:#657287;font-size:12px}.switch{display:inline-flex!important;align-items:center;cursor:pointer}.switch input{position:absolute;opacity:0;width:1px!important;height:1px!important}.switch span{position:relative;display:block;width:44px;height:24px;border-radius:20px;background:#cbd5e1;transition:.2s}.switch span::after{content:'';position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #64748b;transition:.2s}.switch input:checked+span{background:#1769e0}.switch input:checked+span::after{left:23px}.dws-actions{display:flex;gap:10px;margin-bottom:14px}.check-field{display:flex!important;align-items:center;gap:8px;font-weight:400!important}.check-field input{width:16px!important;height:16px!important;margin:0!important}.history-config{grid-column:1/-1;margin-top:4px;padding:14px 16px;border:1px solid #e1e7ef;border-radius:6px;background:#f8fafc}.history-config h3{margin:0 0 12px;color:#26354d;font-size:14px}.history-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px 16px}.field-help{margin-top:5px;color:#718096;font-size:11px}.dws-device-panel{display:none;margin-bottom:14px;padding:14px 16px;border:1px solid #9fc4f5;border-radius:6px;background:#f5f9ff}.dws-device-panel.visible{display:block}.dws-device-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px;color:#174f9f;font-size:13px}.dws-device-head .button{height:32px;padding:0 10px}.dws-device-output{padding:11px 12px;border:1px solid #d6e3f5;border-radius:5px;background:#fff;color:#172033;font:12px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-all}.dws-device-output a{color:#175cd3;text-decoration:underline}.dws-device-tip{margin-top:9px;color:#5c6d82;font-size:12px;line-height:1.5}.danger-button{background:#fff1f0;color:#b42318;border:1px solid #f1b8b5}.dws-status-box{padding:14px 16px;border:1px solid #dfe6ef;border-radius:6px;background:#f8fafc}.dws-auth-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px 14px}.dws-auth-fields>div{padding:11px 12px;border:1px solid #e2e8f0;border-radius:6px;background:#fff}.dws-auth-fields span{display:block;margin-bottom:5px;color:#718096;font-size:11px}.dws-auth-fields strong,.dws-auth-fields code{display:block;color:#172033;font-size:13px;word-break:break-all}.dws-auth-fields code{font:12px ui-monospace,SFMono-Regular,Menlo,monospace}.dws-status-box pre{margin:0;white-space:pre-wrap;word-break:break-word;font:13px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace;color:#172033}.dws-status-loading{color:#657287;font-size:13px}.dws-login-guide{margin-top:12px;padding-top:12px;border-top:1px solid #e1e7ef;color:#526176;font-size:13px;line-height:1.6}.dws-login-guide a{color:#175cd3}.dws-help{margin:12px 0 0;color:#657287;font-size:12px;line-height:1.5}.agent-command-grid{margin-top:16px;padding-top:14px;border-top:1px solid #e7edf5}.prompt-settings{margin-bottom:18px}.prompt-settings textarea{width:100%;min-height:220px;padding:10px 11px;border:1px solid #c9d3e0;border-radius:5px;resize:vertical;font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}.prompt-help{margin-top:6px;color:#657287;font-size:12px;line-height:1.5}.section-toggle{display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;margin:0}.section-toggle::after{content:'展开';font-size:13px;font-weight:400;color:#175cd3}.collapsible-panel.expanded>.section-toggle::after{content:'收起'}.collapsible-panel:not(.expanded)>.collapsible-content{display:none}.collapsible-panel.expanded>.collapsible-content{margin-top:14px}.bot-user-results{display:flex;flex-direction:column;gap:5px;margin-top:7px;max-height:180px;overflow:auto}.bot-user-result,.bot-user-chip{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 9px;border:1px solid #dbe2ed;border-radius:5px;background:#f8fafc;font-size:13px}.super-admin-field{display:none!important;grid-column:1/-1;width:100%;padding:14px 16px;border:1px solid #dbe2ed;border-radius:8px;background:#f8fafc}.super-admin-field>label{display:flex;align-items:center;gap:8px;width:100%;margin:0 0 0;color:#172033;font-size:13px;line-height:20px}.super-admin-field>label::before{content:none;display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:4px;background:#eaf2ff;color:#175cd3;font-size:11px;font-weight:700}.super-admin-help{margin-top:5px;color:#657287;font-size:12px;line-height:1.5}.super-admin-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:10px;width:100%;max-height:none}.super-admin-list .bot-user-result{justify-content:flex-start;min-height:42px;padding:9px 11px;background:#fff;cursor:pointer;transition:border-color .15s,background .15s}.super-admin-list .bot-user-result:has(input:checked){border-color:#86b7ff;background:#eef6ff}.super-admin-list .bot-user-result input{width:16px;height:16px;margin:0;accent-color:#1677ff}.super-admin-list .bot-user-result span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bot-user-result{width:100%;text-align:left;cursor:pointer}.bot-user-result:hover{background:#eef5ff;border-color:#9fc4f5}.bot-user-chip{display:inline-flex;margin:6px 6px 0 0;background:#eef4ff;border-color:#c5dafa;color:#175cd3}.bot-user-chip button{border:0;background:transparent;color:#175cd3;cursor:pointer;font-size:16px;line-height:1;padding:0}.bot-user-selected{margin-top:6px;display:flex;flex-wrap:wrap}.save-floating{position:fixed;left:50%;bottom:10px;transform:translateX(-50%);z-index:10;background:#16a34a;color:#fff;box-shadow:0 4px 14px rgba(22,163,74,.35)}.rules-panel{margin-bottom:18px}.rules-toggle{display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;margin:0}.rules-toggle::after{content:'展开';font-size:13px;font-weight:400;color:#175cd3}.rules-panel.expanded .rules-toggle::after{content:'收起'}.rules-panel:not(.expanded) .rules-content{display:none}.rules-panel.expanded .rules-content{margin-top:14px}.command-settings{margin-bottom:18px}.command-toggle{display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;margin:0}.command-toggle::after{content:'展开';font-size:13px;font-weight:400;color:#175cd3}.command-settings.expanded .command-toggle::after{content:'收起'}.command-settings:not(.expanded) .command-grid{display:none}.command-settings.expanded .command-grid{margin-top:14px}.command-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 16px}.command-grid input{width:100%;height:36px;padding:7px 9px;border:1px solid #c9d3e0;border-radius:5px;background:#fff;font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.binding-commands{margin-bottom:18px;padding:14px 16px;border:1px solid #e1e8f2;border-radius:6px;background:#f8fafc}.binding-commands h3{margin:0 0 12px;font-size:14px;color:#26354d}.binding-help{margin:-5px 0 12px;color:#657287;font-size:12px;line-height:1.5}@media(max-width:640px){.command-grid{grid-template-columns:1fr}.dws-auth-fields{grid-template-columns:1fr}.dws-actions{display:grid;grid-template-columns:1fr}.dws-actions .button{width:100%}}.agent-options{display:flex;gap:18px;align-items:center;flex-wrap:wrap}.agent-options label{display:flex;align-items:center;gap:6px;font-size:14px;cursor:pointer}.agent-options input{width:16px;height:16px;accent-color:#1677ff}.robot-settings{margin-bottom:28px}.robot-settings .robot-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 16px;align-items:start}@media(max-width:640px){.agent-settings .agent-row{max-width:none}.robot-settings .robot-row{grid-template-columns:1fr}.super-admin-list{grid-template-columns:1fr}}
|
|
241
265
|
.member-list{display:flex;flex-wrap:wrap;align-items:center;gap:6px 14px;max-height:128px}.field label.member-option{display:inline-flex!important;align-items:center;gap:8px;flex:0 0 auto;width:max-content;min-width:max-content;min-height:28px;margin:0!important;padding:4px 6px;overflow:visible;white-space:nowrap;line-height:18px}.member-option input[type=checkbox]{display:block;flex:0 0 14px;width:14px!important;height:14px!important;min-width:14px;min-height:14px;padding:0!important;margin:0!important;align-self:center;vertical-align:middle}.member-name,.member-role{display:inline-flex;align-items:center;height:18px;margin:0;overflow:visible;text-overflow:clip;white-space:nowrap;line-height:18px}.member-role{margin-left:-4px}
|
|
242
266
|
.replies-panel .replies-toggle{display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;margin:0}.replies-panel .replies-toggle::after{content:'展开';font-size:13px;font-weight:400;color:#175cd3}.replies-panel.expanded .replies-toggle::after{content:'收起'}.replies-panel:not(.expanded) #replies{display:none!important}.reply{border:1px dashed #8fa4c2!important;border-radius:6px;padding:14px!important;background:#fff}.reply .meta{margin:0 0 6px;padding:9px 11px;border:1px solid #c8d7ec;border-radius:4px;background:#eaf2ff;box-shadow:inset 0 1px 0 #fff,0 2px 5px rgba(48,74,110,.16);font-size:15px;font-weight:700;color:#25324a}
|
|
243
267
|
@media(max-width:640px){.save-floating{bottom:6px}body{min-width:0}.wrap{padding:16px 12px 32px}h1{font-size:22px;line-height:1.3}h2{font-size:18px;margin-bottom:12px}.panel{padding:14px}.robot-settings{margin-bottom:18px}.robot-settings .robot-row{gap:12px}.field label{font-size:13px}.field input,.field select,.search,.button{min-height:44px;height:44px;font-size:16px}.line{gap:8px}.line input{min-width:0}.search{padding:0 13px;flex:0 0 auto}.rule{padding:14px 42px 14px 0}.rule-grid,.configured-grid{gap:12px}.delete{top:17px;right:0;width:36px;height:44px;font-size:24px}.member-list{max-height:220px;padding:5px;gap:4px 6px;align-content:start}.field label.member-option{min-height:40px;padding:8px 7px;gap:9px;font-size:15px}.member-option input[type=checkbox]{flex-basis:18px;width:18px!important;height:18px!important;min-width:18px;min-height:18px}.member-name,.member-role{height:20px;line-height:20px}.member-role{font-size:12px}.members{padding-top:3px;line-height:1.45}.toolbar{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px;padding-top:14px}.toolbar .button{width:100%;padding:8px}.toolbar .format{grid-column:1/-1;order:0;display:flex;flex-wrap:wrap;gap:8px 12px;min-height:34px}.format span{width:100%;font-weight:600}.format label{display:inline-flex;align-items:center;min-height:30px}.format input{width:18px;height:18px;margin:0 5px 0 0}.reply{padding:10px!important}.reply .meta{padding:9px 10px;font-size:14px;line-height:1.45;word-break:break-word}.reply pre{font-size:12px!important}.notice{line-height:1.45}.status{font-size:13px}.empty{padding:4px 0}.grid{gap:14px}#replies{max-height:58vh!important}}
|
|
244
268
|
/* Application shell */
|
|
245
|
-
:root{--nav:#172033;--nav-muted:#9ba8bb;--line:#dfe5ed;--blue:#1769e0;--canvas:#f5f6f8}body{background:var(--canvas);color:#172033}.wrap{max-width:none;min-height:100vh;padding:0}.app-shell{display:grid;grid-template-columns:232px minmax(0,1fr);min-height:100vh}.app-sidebar{position:fixed;inset:0 auto 0 0;width:232px;z-index:30;display:flex;flex-direction:column;padding:14px 12px;background:var(--nav);color:#fff}.brand{display:flex;align-items:center;gap:11px;padding:0 6px 16px}.brand-mark{display:grid;place-items:center;width:34px;height:34px;border:1px solid #48566b;border-radius:7px;background:#222d3e;color:#fff;font:700 14px ui-monospace,SFMono-Regular,Menlo,monospace}.brand-icon{object-fit:cover}.brand-name{font-size:15px;font-weight:700;letter-spacing:.01em}.brand-sub{margin-top:2px;color:var(--nav-muted);font-size:11px}.nav-label{padding:0 10px 8px;color:#718096;font-size:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.app-nav{display:flex;flex-direction:column;gap:3px}.nav-item{display:flex;align-items:center;gap:10px;width:100%;min-height:42px;padding:0 11px;border:0;border-radius:6px;background:transparent;color:#b9c3d1;text-align:left;font:13px inherit;cursor:pointer}.nav-item:hover{background:#202c3e;color:#fff}.nav-item.active{background:#2a3850;color:#fff;font-weight:600}.nav-index{width:22px;color:#718096;font:11px ui-monospace,SFMono-Regular,Menlo,monospace}.nav-item.active .nav-index{color:#78aaf8}.current-user-card{margin-top:18px;padding:12px 14px;border:1px solid #dfe6ef;border-radius:6px;background:#f8fafc}.current-user-card .user-value{margin-top:4px;color:#172033;font-size:14px;font-weight:600}.current-user-card .user-id{margin-top:3px;color:#657287;font:11px ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}.sidebar-foot{margin-top:auto;padding:18px 8px 0;border-top:1px solid #2e3a4d;color:#8996a9;font-size:11px;line-height:1.6}.sidebar-foot .version{display:inline-block;margin-top:5px;color:#c0cad8;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.sidebar-foot a{color:#8996a9;text-decoration:none;word-break:break-all}.sidebar-foot a:hover{color:#fff;text-decoration:underline}.app-main{grid-column:2;min-width:0}.app-topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;min-height:58px;padding:8px 24px;border-bottom:1px solid var(--line);background:rgba(255,255,255,.94);backdrop-filter:blur(12px)}.topbar-copy h1{margin:0;color:#152033;font-size:21px;line-height:1.3}.topbar-copy p{margin:4px 0 0;color:#6b7789;font-size:12px}.topbar-actions{display:flex;align-items:center;gap:12px}.status{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 10px;border-color:#dbe2eb;border-radius:5px;background:#fff;color:#5c6878;font-size:12px}.status::before{content:'';width:7px;height:7px;border-radius:50%;background:#98a2b3}.status.connected::before{background:#20a064;box-shadow:0 0 0 3px #e1f5ea}.status.stopped::before,.status.error::before{background:#d14b45}.menu-button{display:none;width:40px;height:40px;border:1px solid var(--line);border-radius:6px;background:#fff;color:#223048;font-size:20px}.app-content{width:100%;padding:10px 24px 100px}.app-page{display:none}.app-page.active{display:block}.app-page>.panel{margin:0;border:1px solid var(--line);border-radius:8px;background:#fff;box-shadow:0 1px 2px rgba(16,24,40,.03)}.app-page>.panel>h2{display:none}.agent-settings .agent-row{max-width:none}.agent-options{padding:10px 0}.agent-options label{min-width:150px;padding:13px 15px;border:1px solid #dbe2eb;border-radius:6px;background:#fafbfc}.agent-options label:has(input:checked){border-color:#7daaf0;background:#f0f6ff;color:#174f9f}.agent-command-grid{display:flex;flex-direction:column;gap:14px;margin-top:20px}.agent-command-grid .field{width:100%}.command-settings:not(.expanded) .command-grid,.rules-panel:not(.expanded) .rules-content,.replies-panel:not(.expanded) #replies,.collapsible-panel:not(.expanded)>.collapsible-content{display:initial}.command-settings .command-grid,.rules-panel .rules-content,.replies-panel #replies,.collapsible-panel>.collapsible-content{margin-top:0!important}.robot-settings .robot-row{grid-template-columns:repeat(2,minmax(0,1fr))}.prompt-settings textarea{min-height:360px}.binding-commands{border-color:#dfe6ef;background:#fafbfc}.rules-panel{margin:0}.rule{padding:18px 0}.save-floating{position:fixed;right:auto;bottom:24px;left:calc(232px + (100vw - 232px)/2);z-index:40;min-width:148px;height:42px;padding:0 22px;border-radius:6px;background:#1769e0;box-shadow:0 8px 20px rgba(23,105,224,.24);transform:translateX(-50%)}.save-floating:hover{background:#105dc9}.replies-panel{margin:0!important}.reply{border:1px solid #dfe5ed!important;border-radius:7px!important}.reply .meta{border:0;border-bottom:1px solid #e8edf3;border-radius:0;background:#f8fafc;box-shadow:none;color:#344054;font-size:13px}.mobile-scrim{display:none}.notice{bottom:78px}.field input,.field select,.command-grid input,.prompt-settings textarea{border-color:#ccd5e1;border-radius:5px}.field input:focus,.field select:focus,.command-grid input:focus,.prompt-settings textarea:focus{outline:2px solid #c8dcfb;border-color:#4f88dd}.button,.search{border-radius:5px}.secondary{background:#edf4ff;color:#175bb8}.page-agent .panel,.page-prompt .panel,.page-command .panel,.page-robot .panel{padding:26px}.section-toggle::after,.rules-toggle::after,.command-toggle::after,.replies-toggle::after{display:none}
|
|
246
|
-
@media(max-width:820px){.app-shell{display:block}.app-sidebar{width:min(82vw,280px);padding-top:18px;transform:translateX(-105%);transition:transform .2s ease;box-shadow:16px 0 40px rgba(17,24,39,.18)}body.nav-open .app-sidebar{transform:translateX(0)}.mobile-scrim{position:fixed;inset:0;z-index:25;display:block;background:rgba(15,23,42,.42);opacity:0;pointer-events:none;transition:opacity .2s}body.nav-open .mobile-scrim{opacity:1;pointer-events:auto}.app-main{min-height:100vh}.app-topbar{min-height:70px;padding:12px 16px}.menu-button{display:block;order:-1}.topbar-actions{gap:8px}.topbar-copy{flex:1;min-width:0}.topbar-copy h1{font-size:18px}.topbar-copy p{display:none}.status{max-width:120px;overflow:hidden;white-space:nowrap}.app-content{padding:20px 14px 100px}.app-page>.panel,.page-agent .panel,.page-prompt .panel,.page-command .panel,.page-robot .panel{padding:16px}.robot-settings .robot-row,.command-grid{grid-template-columns:1fr}.agent-command-grid{display:flex}.prompt-settings textarea{min-height:300px}.agent-options{display:grid;grid-template-columns:1fr 1fr;gap:8px}.agent-options label{min-width:0;justify-content:center;padding:11px 8px}.agent-options input[type=radio]{width:18px!important;height:18px!important;min-width:18px!important;min-height:18px!important;padding:0!important}.save-floating{right:14px;bottom:14px;left:14px;width:calc(100% - 28px);height:48px;transform:none}.notice{bottom:72px}.binding-commands{padding:12px}.rule-grid,.configured-grid{grid-template-columns:1fr}.members{grid-column:1}.rule{padding-right:38px}.toolbar{grid-template-columns:1fr}.toolbar .format{grid-column:1}.member-list{max-height:260px}.line{width:100%}}
|
|
247
|
-
</style></head><body><main class="wrap"><header><div><h1>oh-my-im 控制台</h1><p>保存后立即生效。搜索并选择群,再从该群成员中选择需要监听的人。</p></div><div class="status" id="connection">加载中</div></header><section class="panel agent-settings"><h2>Agent 引擎设置</h2><div class="agent-row"><div class="field"><label>当前使用的 Agent</label><div class="agent-options"><label><input type="radio" name="agent" value="codex" checked> Codex Agent</label><label><input type="radio" name="agent" value="pi"> Pi Agent</label></div></div></div><div class="command-grid agent-command-grid"><div class="field"><label>切换到 Codex 指令</label><input type="text" id="keywordsSwitchCodex" placeholder="例如:切codex|切换codex"></div><div class="field"><label>切换到 Pi 指令</label><input type="text" id="keywordsSwitchPi" placeholder="例如:切pi|切换pi|换pi"></div></div></section><section class="panel dws-settings"><h2>DWS 配置</h2><div class="dws-actions"><button class="button secondary" id="dwsDeviceLogin">使用 Device Flow 登录</button><button class="button danger-button" id="dwsLogout">退出 DWS 登录</button></div><div id="dwsDevicePanel" class="dws-device-panel"><div class="dws-device-head"><strong>Device Flow 登录信息</strong><button class="button secondary" id="dwsDeviceCopy">复制登录信息</button></div><div id="dwsDeviceOutput" class="dws-device-output"></div><div class="dws-device-tip">请在其他电脑或手机打开上面的 URL,完成钉钉授权。登录信息保存在运行 DWS 的服务器上。</div></div><div class="dws-status-box"><div class="dws-status-loading">正在读取 dws auth status...</div><div id="dwsAuthFields" class="dws-auth-fields"><div><span>企业</span><strong id="dwsCorpName">-</strong></div><div><span>账号</span><strong id="dwsUserName">-</strong></div><div><span>user_id</span><code id="dwsUserId">-</code></div><div><span>Token 过期</span><strong id="dwsExpiresAt">-</strong></div><div><span>Refresh Token 过期</span><strong id="dwsRefreshExpiresAt">-</strong></div></div><pre id="dwsAuthStatus" hidden></pre><div id="dwsLoginGuide" class="dws-login-guide"></div></div><p class="dws-help">信息来自本机 <code>dws auth status</code>。登录与退出操作都在运行 DWS 的机器上执行。</p></section><section class="panel prompt-settings collapsible-panel"><h2 class="section-toggle">提示词前缀</h2><div class="collapsible-content"><div class="field"><label>提示词前缀</label><textarea id="groupPromptPrefix" placeholder="可留空。该内容会放在群消息事件 JSON 之前。"></textarea><div class="prompt-help">保存后立即用于新的群消息批次;程序只会在末尾附加“DingTalk 消息事件”和事件 JSON。</div></div></div></section><section class="panel private-chat-settings"><h2>个人与机器人私聊</h2><div class="private-chat-row"><div><label>个人与机器人私聊</label><
|
|
248
|
-
let selectedBotUsers=[],selectedSuperAdminIds=[];const agentSelect=()=>document.querySelector('input[name=agent]:checked'),rules=document.querySelector('#rules'),notice=document.querySelector('#notice'),replyList=document.querySelector('#replies'),layout=document.querySelector('.grid'),rulesPanel=layout.firstElementChild,repliesPanel=layout.lastElementChild,agentSettings=document.querySelector('.agent-settings'),promptSettings=document.querySelector('.prompt-settings'),robotSettings=document.querySelector('.robot-settings'),privateChatSettings=document.querySelector('.private-chat-settings'),commandSettings=document.querySelector('.command-settings'),root=document.querySelector('.wrap'),connection=document.querySelector('#connection'),saveControl=document.querySelector('#save');document.querySelector('#add').textContent='添加钉钉规则';const renderDwsDeviceOutput=(text)=>{const output=document.querySelector('#dwsDeviceOutput');if(!output)return;output.replaceChildren();const parts=String(text||'暂无登录信息').split(/(https?:\/\/[^\s]+)/g);parts.forEach(part=>{if(/^https?:\/\//.test(part)){const a=document.createElement('a');a.href=part.replace(/[),.;。!]$/,'');a.target='_blank';a.rel='noreferrer';a.textContent=part;output.append(a)}else output.append(document.createTextNode(part))})};const pollDwsDeviceLogin=async()=>{try{const r=await fetch('/api/dws-device-login',{cache:'no-store'}),b=await r.json();if(r.ok){renderDwsDeviceOutput(b.output);document.querySelector('#dwsDevicePanel')?.classList.add('visible');if(b.running)setTimeout(pollDwsDeviceLogin,1000);else void loadDwsAuth()}}catch(e){renderDwsDeviceOutput(e instanceof Error?e.message:String(e))}};const loadPrivateChatStatus=async()=>{try{const r=await fetch('/api/bot-status',{cache:'no-store'}),b=await r.json();if(!r.ok)throw new Error(b.error||'私聊机器人状态查询失败');const enabled=document.querySelector('#privateChatEnabled'),status=document.querySelector('#privateChatStatus');enabled.checked=b.status?.enabled===true;status.textContent=b.status?.connected?'已连接':'未连接'}catch(e){document.querySelector('#privateChatStatus').textContent='查询失败'}};document.querySelector('#privateChatEnabled').onchange=async event=>{const enabled=event.target.checked;try{const stateResponse=await fetch('/api/state',{cache:'no-store'}),state=await stateResponse.json();if(!stateResponse.ok||!state.config)throw new Error('读取当前配置失败');const r=await fetch('/api/config',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({...state.config,privateChatEnabled:enabled,clientSecret:state.config.clientSecret||''})});const body=await r.json().catch(()=>({}));if(!r.ok||body.saved!==true)throw new Error(body.error||'保存个人私聊开关失败');await loadPrivateChatStatus()}catch(e){event.target.checked=!enabled;alert(e instanceof Error?e.message:String(e))}};setInterval(()=>void loadPrivateChatStatus(),2000);const loadDwsAuth=async()=>{const output=document.querySelector('#dwsAuthStatus'),loading=document.querySelector('.dws-status-loading'),guide=document.querySelector('#dwsLoginGuide');try{const r=await fetch('/api/dws-auth-status',{cache:'no-store'}),b=await r.json();if(!r.ok)throw new Error(b.error||'DWS 登录状态查询失败');if(loading)loading.remove();const status=b.status||{};const set=(id,value)=>{const el=document.querySelector('#'+id);if(el)el.textContent=value||'-'};set('dwsCorpName',status.corp_name);set('dwsUserName',status.user_name);set('dwsUserId',status.user_id);set('dwsExpiresAt',status.expires_at);set('dwsRefreshExpiresAt',status.refresh_expires_at);if(output)output.textContent='';if(b.status?.authenticated!==true&&guide)guide.innerHTML='当前未登录 DWS。请在运行 DWS 的机器执行 <code>dws auth login --device</code>,或点击“使用 Device Flow 登录”。'}catch(e){if(loading)loading.textContent='DWS 登录状态读取失败';if(output)output.textContent=e instanceof Error?e.message:String(e);if(guide)guide.innerHTML='请在运行 DWS 的机器执行 <code>dws auth login --device</code> 完成跨设备登录。'}};const dwsLoginButton=document.querySelector('#dwsDeviceLogin');dwsLoginButton.onclick=async()=>{dwsLoginButton.disabled=true;try{const r=await fetch('/api/dws-device-login',{method:'POST'}),b=await r.json();if(!r.ok)throw new Error(b.error||'DWS 登录启动失败');renderDwsDeviceOutput(b.message);document.querySelector('#dwsDevicePanel')?.classList.add('visible');void pollDwsDeviceLogin()}catch(e){renderDwsDeviceOutput(e instanceof Error?e.message:String(e));document.querySelector('#dwsDevicePanel')?.classList.add('visible')}finally{dwsLoginButton.disabled=false}};document.querySelector('#dwsDeviceCopy').onclick=async()=>{const text=document.querySelector('#dwsDeviceOutput')?.textContent||'';try{await navigator.clipboard.writeText(text);notice.textContent='登录信息已复制';setTimeout(()=>notice.textContent='',2000)}catch{notice.textContent='复制失败,请手动选择上方登录信息复制';}};document.querySelector('#dwsLogout').onclick=async()=>{if(!confirm('确认退出当前 DWS 登录吗?'))return;try{const r=await fetch('/api/dws-logout',{method:'POST'}),b=await r.json();if(!r.ok)throw new Error(b.error||'DWS 退出失败');await loadDwsAuth()}catch(e){alert(e instanceof Error?e.message:String(e))}};void loadDwsAuth();void loadPrivateChatStatus();robotSettings.querySelector('.robot-row').innerHTML='<div class="field"><label>机器人名称 <a class="platform-link" href="https://open-dev.dingtalk.com/fe/app" target="_blank" rel="noreferrer">(开放平台)</a></label><input id="robotName" placeholder="例如:我的 AI 助手"></div><div class="field bot-identity-field"><label>机器人 openDingTalkId</label><div class="line"><input id="robotSenderOpenDingTalkId" placeholder="点击“获取 ID”自动填写" readonly><button class="search" id="botSearchButton" type="button">获取 ID</button></div><div id="botSearchResult" class="bot-user-results"></div></div><div class="field"><label>钉钉应用 Client ID</label><input id="clientId" placeholder="ding..."></div><div class="field"><label>钉钉应用 Client Secret</label><input id="clientSecret" type="password" autocomplete="new-password" placeholder="已配置时留空不修改"></div><div class="field"><label>卡片标题显示总耗时</label><label class="check-field"><input type="checkbox" id="showElapsed"> 显示任务总耗时</label></div><div class="field"><label>卡片更新间隔(秒)</label><input type="number" id="cardUpdateIntervalMs" min="0.5" max="60" step="0.5" placeholder="3"></div><div class="field"><label>机器人单聊授权人员</label><div class="line"><input id="botUserSearch" placeholder="搜索钉钉真实姓名"><button class="search" id="botUserSearchButton" type="button">搜索</button></div><div id="botUserResults" class="bot-user-results"></div><div id="botUserSelected" class="bot-user-selected"></div></div><div class="field super-admin-field"><label>Session 超级管理员</label><div class="super-admin-help">仅限已加入单聊授权人员的用户。超级管理员可以按目录查看和切换本机 Session。</div><div id="superAdminSelected" class="bot-user-results super-admin-list"></div></div>';[promptSettings,robotSettings,commandSettings,rulesPanel,repliesPanel].forEach(panel=>panel.classList.add('expanded'));const historySettings=document.querySelector('.history-settings');const robotPage=document.createElement('div');robotPage.append(robotSettings,privateChatSettings,historySettings);const pageDefs=[['robot','机器人与权限','配置钉钉应用、卡片机器人和单聊授权人员。',robotPage],['dws','DWS 配置','查看当前本机 DWS 登录账号信息。',document.querySelector('.dws-settings')],['agent','Agent 引擎','选择默认执行引擎,并配置群内切换指令。',agentSettings],['prompt','提示词前缀','定义发送给 Agent 的群消息前缀,事件 JSON 将自动附加。',promptSettings],['commands','任务控制','配置暂停当前任务的消息指令。',commandSettings],['rules','群监控规则','管理绑定指令以及需要监听的群和人员。',rulesPanel],['replies','Agent回复','查看最近的群聊与单聊 Agent 处理记录。',repliesPanel]];root.replaceChildren();const shell=document.createElement('div');shell.className='app-shell';const sidebar=document.createElement('aside');sidebar.className='app-sidebar';sidebar.innerHTML='<div class="brand"><img class="brand-mark brand-icon" src="/assets/omi-icon.png" alt="omi" /><div><div class="brand-name">oh-my-im</div><div class="brand-sub">DingTalk Agent Console</div></div></div><div class="nav-label">Workspace</div><nav class="app-nav"></nav><div class="sidebar-foot">版本:<span class="version">v0.1.4</span> · <a href="https://github.com/duzhenxun/oh-my-im" target="_blank" rel="noreferrer">开源项目</a></div>';const main=document.createElement('div');main.className='app-main';const topbar=document.createElement('header');topbar.className='app-topbar';topbar.innerHTML='<button class="menu-button" type="button" aria-label="打开菜单">☰</button><div class="topbar-copy"><h1></h1><p></p></div>';const content=document.createElement('div');content.className='app-content';pageDefs.forEach(([id,title,description,panel],index)=>{const nav=document.createElement('button');nav.type='button';nav.className='nav-item'+(index===0?' active':'');nav.dataset.page=id;nav.innerHTML='<span class="nav-index">0'+(index+1)+'</span><span>'+title+'</span>';sidebar.querySelector('.app-nav').append(nav);const page=document.createElement('section');page.className='app-page page-'+id+(index===0?' active':'');page.dataset.page=id;page.append(panel);content.append(page)});const scrim=document.createElement('div');scrim.className='mobile-scrim';main.append(topbar,content);shell.append(sidebar,main);root.append(shell,scrim,notice,saveControl);const desktopMedia=window.matchMedia('(min-width:821px)');const syncViewport=()=>{if(desktopMedia.matches)document.body.classList.remove('nav-open')};desktopMedia.addEventListener?.('change',syncViewport);syncViewport();const showPage=id=>{document.querySelectorAll('.nav-item').forEach(item=>item.classList.toggle('active',item.dataset.page===id));document.querySelectorAll('.app-page').forEach(page=>page.classList.toggle('active',page.dataset.page===id));const def=pageDefs.find(item=>item[0]===id)||pageDefs[0];topbar.querySelector('h1').textContent=def[1];topbar.querySelector('p').textContent=def[2];saveControl.style.display=id==='replies'?'none':'';if(location.hash!=='#'+id)history.replaceState(null,'','#'+id);document.body.classList.remove('nav-open');window.scrollTo({top:0,behavior:'instant'})};sidebar.querySelectorAll('.nav-item').forEach(item=>item.onclick=()=>showPage(item.dataset.page));topbar.querySelector('.menu-button').onclick=()=>document.body.classList.toggle('nav-open');scrim.onclick=()=>document.body.classList.remove('nav-open');const initialPage=pageDefs.some(item=>item[0]===location.hash.slice(1))?location.hash.slice(1):'robot';window.addEventListener('hashchange',()=>{const id=location.hash.slice(1);if(pageDefs.some(item=>item[0]===id))showPage(id)});showPage(initialPage);replyList.style.maxHeight='min(680px,calc(100vh - 230px))';replyList.style.overflowY='auto';replyList.style.display='flex';replyList.style.flexDirection='column';replyList.style.gap='12px';let loaded=false;
|
|
269
|
+
:root{--nav:#172033;--nav-muted:#9ba8bb;--line:#dfe5ed;--blue:#1769e0;--canvas:#f5f6f8}body{background:var(--canvas);color:#172033}.wrap{max-width:none;min-height:100vh;padding:0}.app-shell{display:grid;grid-template-columns:232px minmax(0,1fr);min-height:100vh}.app-sidebar{position:fixed;inset:0 auto 0 0;width:232px;z-index:30;display:flex;flex-direction:column;padding:14px 12px;background:var(--nav);color:#fff}.brand{display:flex;align-items:center;gap:11px;padding:0 6px 16px}.brand-mark{display:grid;place-items:center;width:34px;height:34px;border:1px solid #48566b;border-radius:7px;background:#222d3e;color:#fff;font:700 14px ui-monospace,SFMono-Regular,Menlo,monospace}.brand-icon{object-fit:cover}.brand-name{font-size:15px;font-weight:700;letter-spacing:.01em}.brand-sub{margin-top:2px;color:var(--nav-muted);font-size:11px}.nav-label{padding:0 10px 8px;color:#718096;font-size:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.app-nav{display:flex;flex-direction:column;gap:3px}.nav-item{display:flex;align-items:center;gap:10px;width:100%;min-height:42px;padding:0 11px;border:0;border-radius:6px;background:transparent;color:#b9c3d1;text-align:left;font:13px inherit;cursor:pointer}.nav-item:hover{background:#202c3e;color:#fff}.nav-item.active{background:#2a3850;color:#fff;font-weight:600}.nav-index{width:22px;color:#718096;font:11px ui-monospace,SFMono-Regular,Menlo,monospace}.nav-item.active .nav-index{color:#78aaf8}.current-user-card{margin-top:18px;padding:12px 14px;border:1px solid #dfe6ef;border-radius:6px;background:#f8fafc}.current-user-card .user-value{margin-top:4px;color:#172033;font-size:14px;font-weight:600}.current-user-card .user-id{margin-top:3px;color:#657287;font:11px ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}.sidebar-foot{margin-top:auto;padding:18px 8px 0;border-top:1px solid #2e3a4d;color:#8996a9;font-size:11px;line-height:1.6}.sidebar-foot .version{display:inline-block;margin-top:5px;color:#c0cad8;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.sidebar-foot a{color:#8996a9;text-decoration:none;word-break:break-all}.sidebar-foot a:hover{color:#fff;text-decoration:underline}.app-main{grid-column:2;min-width:0;transition:margin-left .2s}.sidebar-toggle{display:grid;place-items:center;width:32px;height:32px;margin-left:auto;border:1px solid #3b495e;border-radius:5px;background:transparent;color:#b9c3d1;cursor:pointer;font-size:16px}.sidebar-toggle:hover{background:#2a3850;color:#fff}.fullscreen-button{display:inline-flex;align-items:center;gap:6px;height:34px;padding:0 11px;border:1px solid #dbe2eb;border-radius:5px;background:#fff;color:#344054;cursor:pointer;font:12px inherit}.fullscreen-button:hover{border-color:#9fc4f5;background:#f5f9ff;color:#175cd3}body.sidebar-collapsed .app-sidebar{width:72px}body.sidebar-collapsed .app-main{grid-column:2;margin-left:-160px}body.sidebar-collapsed .brand{padding-inline:7px}body.sidebar-collapsed .brand-name,body.sidebar-collapsed .brand-sub,body.sidebar-collapsed .nav-label,body.sidebar-collapsed .nav-item>span:last-child,body.sidebar-collapsed .sidebar-foot{display:none}body.sidebar-collapsed .brand-mark{flex:0 0 34px}body.sidebar-collapsed .sidebar-toggle{position:absolute;top:62px;left:19px;width:34px}body.sidebar-collapsed .app-nav{margin-top:48px}body.sidebar-collapsed .nav-item{justify-content:center;padding:0}body.sidebar-collapsed .nav-index{width:auto}body.sidebar-collapsed .save-floating{left:calc(72px + (100vw - 72px)/2)}.app-topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;min-height:58px;padding:8px 24px;border-bottom:1px solid var(--line);background:rgba(255,255,255,.94);backdrop-filter:blur(12px)}.topbar-copy h1{margin:0;color:#152033;font-size:21px;line-height:1.3}.topbar-copy p{margin:4px 0 0;color:#6b7789;font-size:12px}.topbar-actions{display:flex;align-items:center;gap:12px}.status{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 10px;border-color:#dbe2eb;border-radius:5px;background:#fff;color:#5c6878;font-size:12px}.status::before{content:'';width:7px;height:7px;border-radius:50%;background:#98a2b3}.status.connected::before{background:#20a064;box-shadow:0 0 0 3px #e1f5ea}.status.stopped::before,.status.error::before{background:#d14b45}.menu-button{display:none;width:40px;height:40px;border:1px solid var(--line);border-radius:6px;background:#fff;color:#223048;font-size:20px}.app-content{width:100%;padding:10px 24px 100px}.app-page{display:none}.app-page.active{display:block}.app-page>.panel{margin:0;border:1px solid var(--line);border-radius:8px;background:#fff;box-shadow:0 1px 2px rgba(16,24,40,.03)}.app-page>.panel>h2{display:none}.agent-settings .agent-row{max-width:none}.agent-options{padding:10px 0}.agent-options label{min-width:150px;padding:13px 15px;border:1px solid #dbe2eb;border-radius:6px;background:#fafbfc}.agent-options label:has(input:checked){border-color:#7daaf0;background:#f0f6ff;color:#174f9f}.agent-command-grid{display:flex;flex-direction:column;gap:14px;margin-top:20px}.agent-command-grid .field{width:100%}.command-settings:not(.expanded) .command-grid,.rules-panel:not(.expanded) .rules-content,.replies-panel:not(.expanded) #replies,.collapsible-panel:not(.expanded)>.collapsible-content{display:initial}.command-settings .command-grid,.rules-panel .rules-content,.replies-panel #replies,.collapsible-panel>.collapsible-content{margin-top:0!important}.robot-settings .robot-row{grid-template-columns:repeat(2,minmax(0,1fr))}.prompt-settings textarea{min-height:360px}.binding-commands{border-color:#dfe6ef;background:#fafbfc}.rules-panel{margin:0}.rule{padding:18px 0}.save-floating{position:fixed;right:auto;bottom:24px;left:calc(232px + (100vw - 232px)/2);z-index:40;min-width:148px;height:42px;padding:0 22px;border-radius:6px;background:#1769e0;box-shadow:0 8px 20px rgba(23,105,224,.24);transform:translateX(-50%)}.save-floating:hover{background:#105dc9}.replies-panel{margin:0!important}.reply{border:1px solid #dfe5ed!important;border-radius:7px!important}.reply .meta{border:0;border-bottom:1px solid #e8edf3;border-radius:0;background:#f8fafc;box-shadow:none;color:#344054;font-size:13px}.mobile-scrim{display:none}.notice{bottom:78px}.field input,.field select,.command-grid input,.prompt-settings textarea{border-color:#ccd5e1;border-radius:5px}.field input:focus,.field select:focus,.command-grid input:focus,.prompt-settings textarea:focus{outline:2px solid #c8dcfb;border-color:#4f88dd}.button,.search{border-radius:5px}.secondary{background:#edf4ff;color:#175bb8}.page-agent .panel,.page-prompt .panel,.page-command .panel,.page-robot .panel,.page-monitor .panel{padding:26px}.monitor-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:16px}.monitor-toolbar-actions{display:flex;align-items:center;gap:10px}.monitor-processes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:18px}.process-card{padding:13px 14px;border:1px solid #dfe5ed;border-radius:6px;background:#fafbfc}.process-card strong{display:block;margin-bottom:6px;font-size:13px}.process-card code{color:#657287;font-size:11px;word-break:break-all}.process-state{display:inline-flex;align-items:center;gap:6px;margin-bottom:7px;color:#b42318;font-size:12px}.process-state::before{content:'';width:7px;height:7px;border-radius:50%;background:#d14b45}.process-state.running{color:#087443}.process-state.running::before{background:#20a064}.system-log-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}.system-log{height:calc(100vh - 330px);min-height:360px;margin:0;padding:14px;border:1px solid #26364d;border-radius:6px;background:#101722;color:#c8d4e3;overflow:auto;white-space:pre-wrap;word-break:break-word;font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace}.monitor-meta{color:#657287;font-size:12px}.section-toggle::after,.rules-toggle::after,.command-toggle::after,.replies-toggle::after{display:none}
|
|
270
|
+
@media(max-width:820px){body.sidebar-collapsed .app-sidebar{width:min(82vw,280px)}body.sidebar-collapsed .app-main{margin-left:0}body.sidebar-collapsed .brand-name,body.sidebar-collapsed .brand-sub,body.sidebar-collapsed .nav-label,body.sidebar-collapsed .nav-item>span:last-child,body.sidebar-collapsed .sidebar-foot{display:initial}body.sidebar-collapsed .sidebar-toggle{position:static;width:32px}body.sidebar-collapsed .app-nav{margin-top:0}body.sidebar-collapsed .nav-item{justify-content:flex-start;padding:0 11px}body.sidebar-collapsed .nav-index{width:22px}.app-shell{display:block}.app-sidebar{width:min(82vw,280px);padding-top:18px;transform:translateX(-105%);transition:transform .2s ease;box-shadow:16px 0 40px rgba(17,24,39,.18)}body.nav-open .app-sidebar{transform:translateX(0)}.mobile-scrim{position:fixed;inset:0;z-index:25;display:block;background:rgba(15,23,42,.42);opacity:0;pointer-events:none;transition:opacity .2s}body.nav-open .mobile-scrim{opacity:1;pointer-events:auto}.app-main{min-height:100vh}.app-topbar{min-height:70px;padding:12px 16px}.menu-button{display:block;order:-1}.topbar-actions{gap:8px}.topbar-copy{flex:1;min-width:0}.topbar-copy h1{font-size:18px}.topbar-copy p{display:none}.status{max-width:120px;overflow:hidden;white-space:nowrap}.app-content{padding:20px 14px 100px}.app-page>.panel,.page-agent .panel,.page-prompt .panel,.page-command .panel,.page-robot .panel{padding:16px}.robot-settings .robot-row,.command-grid{grid-template-columns:1fr}.agent-command-grid{display:flex}.prompt-settings textarea{min-height:300px}.agent-options{display:grid;grid-template-columns:1fr 1fr;gap:8px}.agent-options label{min-width:0;justify-content:center;padding:11px 8px}.monitor-toolbar{align-items:flex-start;flex-direction:column}.monitor-toolbar-actions{width:100%;justify-content:space-between}.monitor-processes{grid-template-columns:1fr}.system-log{height:55vh;min-height:300px}.agent-options input[type=radio]{width:18px!important;height:18px!important;min-width:18px!important;min-height:18px!important;padding:0!important}.save-floating{right:14px;bottom:14px;left:14px;width:calc(100% - 28px);height:48px;transform:none}.notice{bottom:72px}.binding-commands{padding:12px}.rule-grid,.configured-grid{grid-template-columns:1fr}.members{grid-column:1}.rule{padding-right:38px}.toolbar{grid-template-columns:1fr}.toolbar .format{grid-column:1}.member-list{max-height:260px}.line{width:100%}}
|
|
271
|
+
</style></head><body><main class="wrap"><header><div><h1>oh-my-im 控制台</h1><p>保存后立即生效。搜索并选择群,再从该群成员中选择需要监听的人。</p></div><div class="status" id="connection">加载中</div></header><section class="panel agent-settings"><h2>Agent 引擎设置</h2><div class="agent-row"><div class="field"><label>当前使用的 Agent</label><div class="agent-options"><label><input type="radio" name="agent" value="codex" checked> Codex Agent</label><label><input type="radio" name="agent" value="pi"> Pi Agent</label></div></div></div><div class="command-grid agent-command-grid"><div class="field"><label>切换到 Codex 指令</label><input type="text" id="keywordsSwitchCodex" placeholder="例如:切codex|切换codex"></div><div class="field"><label>切换到 Pi 指令</label><input type="text" id="keywordsSwitchPi" placeholder="例如:切pi|切换pi|换pi"></div></div></section><section class="panel dws-settings"><h2>DWS 配置</h2><div class="dws-actions"><button class="button secondary" id="dwsDeviceLogin">使用 Device Flow 登录</button><button class="button danger-button" id="dwsLogout">退出 DWS 登录</button></div><div id="dwsDevicePanel" class="dws-device-panel"><div class="dws-device-head"><strong>Device Flow 登录信息</strong><button class="button secondary" id="dwsDeviceCopy">复制登录信息</button></div><div id="dwsDeviceOutput" class="dws-device-output"></div><div class="dws-device-tip">请在其他电脑或手机打开上面的 URL,完成钉钉授权。登录信息保存在运行 DWS 的服务器上。</div></div><div class="dws-status-box"><div class="dws-status-loading">正在读取 dws auth status...</div><div id="dwsAuthFields" class="dws-auth-fields"><div><span>企业</span><strong id="dwsCorpName">-</strong></div><div><span>账号</span><strong id="dwsUserName">-</strong></div><div><span>user_id</span><code id="dwsUserId">-</code></div><div><span>Token 过期</span><strong id="dwsExpiresAt">-</strong></div><div><span>Refresh Token 过期</span><strong id="dwsRefreshExpiresAt">-</strong></div></div><pre id="dwsAuthStatus" hidden></pre><div id="dwsLoginGuide" class="dws-login-guide"></div></div><p class="dws-help">信息来自本机 <code>dws auth status</code>。登录与退出操作都在运行 DWS 的机器上执行。</p></section><section class="panel prompt-settings collapsible-panel"><h2 class="section-toggle">提示词前缀</h2><div class="collapsible-content"><div class="field"><label>提示词前缀</label><textarea id="groupPromptPrefix" placeholder="可留空。该内容会放在群消息事件 JSON 之前。"></textarea><div class="prompt-help">保存后立即用于新的群消息批次;程序只会在末尾附加“DingTalk 消息事件”和事件 JSON。</div></div></div></section><section class="panel private-chat-settings"><h2>个人与机器人私聊</h2><div class="private-chat-row"><div><div class="private-chat-title"><label>个人与机器人私聊</label><label class="switch"><input type="checkbox" id="privateChatEnabled"><span></span></label><strong id="privateChatStatus">查询中...</strong></div><p>关闭后会断开单聊 Stream 长连接;开启后自动重新连接。</p></div></div></section><section class="history-settings"><h2>拉取消息(所有群消息)</h2><div class="history-config"><div class="history-grid"><div class="field"><label>拉取群数量</label><input type="number" id="historyGroupLimit" min="1" max="500" step="1" placeholder="10"></div><div class="field"><label>每群消息数量</label><input type="number" id="historyMessageLimit" min="1" max="500" step="1" placeholder="20"></div><div class="field"><label>拉取间隔(秒)</label><input type="number" id="historyPollIntervalSeconds" min="0" max="3600" step="1" placeholder="5"><div class="field-help">小于等于 0 时不拉取历史消息。</div></div></div></div></section><section class="rule-history-settings"><h2>拉取消息(监控群)</h2><div class="history-config"><div class="history-grid"><div class="field"><label>每群消息数量</label><input type="number" id="ruleHistoryMessageLimit" min="1" max="500" step="1" placeholder="20"></div><div class="field"><label>拉取间隔(秒)</label><input type="number" id="ruleHistoryPollIntervalSeconds" min="0" max="3600" step="1" placeholder="15"><div class="field-help">建议设置 15-30 秒,频率过高容易触发钉钉 RATE_LIMIT_ERROR;设为 0 可关闭拉取。</div></div></div></div></section><section class="panel robot-settings collapsible-panel"><h2 class="section-toggle">机器人设置</h2><div class="robot-row collapsible-content"></div></section><section class="panel command-settings"><h2 class="command-toggle">任务指令关键词</h2><div class="command-grid"><div class="field"><label>暂停当前任务指令</label><input type="text" id="keywordsPause" placeholder="例如:停|暂停|停止当前任务"></div></div></section><section class="panel monitor-settings"><h2>进程日志</h2><div class="monitor-toolbar"><div><strong>系统进程</strong><div class="monitor-meta" id="monitorMeta">正在读取...</div></div><div class="monitor-toolbar-actions"><label class="check-field">日志级别 <select id="monitorLogLevel"><option value="all">全部</option><option value="INFO">INFO</option><option value="WARN">WARN</option><option value="ERROR">ERROR</option><option value="DEBUG">DEBUG</option></select></label><label class="check-field"><input type="checkbox" id="monitorAutoScroll" checked> 日志自动滚动</label><button class="button danger-button" id="restartSystem" type="button">重启系统</button></div></div><div class="monitor-processes" id="monitorProcesses"></div><div class="system-log-head"><strong>实时系统日志</strong><span class="monitor-meta" id="systemLogMeta"></span></div><pre class="system-log" id="systemLog">正在加载日志...</pre></section><section class="summary"><div class="metric"><small>监听规则</small><strong id="targetCount">-</strong></div><div class="metric"><small>处理中批次</small><strong id="activeBatches">-</strong></div><div class="metric"><small>最后收到消息</small><strong id="lastEvent">-</strong></div></section><button class="button save-floating" id="save">保存并生效</button><div class="notice" id="notice" role="status" aria-live="polite"></div><div class="grid"><section class="panel rules-panel"><h2 class="rules-toggle">钉钉群监控绑定</h2><div class="rules-content"><div class="binding-commands"><h3>绑定指令</h3><div class="binding-help">授权人员在包含配置机器人的群中发送以下关键词,可绑定或解绑“当前群 + 当前发送人”的监听规则。</div><div class="command-grid"><div class="field"><label>打开群监听</label><input type="text" id="keywordsMonitorOpen" placeholder="例如:打开ai|启动ai|醒醒"></div><div class="field"><label>关闭群监听</label><input type="text" id="keywordsMonitorStop" placeholder="例如:停止ai|关闭ai|睡吧"></div></div></div><div id="rules"></div><div class="toolbar"><button class="button secondary" id="add">添加监听规则</button><div class="format"><span>回复格式</span><label><input type="radio" name="format" value="markdown"> Markdown</label><label><input type="radio" name="format" value="plain"> 纯文本</label></div></div></div></section><section class="panel replies-panel"><h2 class="replies-toggle">Agent 最近回复</h2><div id="replies" class="empty">暂无回复</div></section></div></main><script>
|
|
272
|
+
let selectedBotUsers=[],selectedSuperAdminIds=[];const agentSelect=()=>document.querySelector('input[name=agent]:checked'),rules=document.querySelector('#rules'),notice=document.querySelector('#notice'),replyList=document.querySelector('#replies'),layout=document.querySelector('.grid'),rulesPanel=layout.firstElementChild,repliesPanel=layout.lastElementChild,agentSettings=document.querySelector('.agent-settings'),promptSettings=document.querySelector('.prompt-settings'),robotSettings=document.querySelector('.robot-settings'),privateChatSettings=document.querySelector('.private-chat-settings'),commandSettings=document.querySelector('.command-settings'),monitorSettings=document.querySelector('.monitor-settings'),root=document.querySelector('.wrap'),connection=document.querySelector('#connection'),saveControl=document.querySelector('#save');document.querySelector('#add').textContent='添加钉钉规则';const renderDwsDeviceOutput=(text)=>{const output=document.querySelector('#dwsDeviceOutput');if(!output)return;output.replaceChildren();const parts=String(text||'暂无登录信息').split(/(https?:\/\/[^\s]+)/g);parts.forEach(part=>{if(/^https?:\/\//.test(part)){const a=document.createElement('a');a.href=part.replace(/[),.;。!]$/,'');a.target='_blank';a.rel='noreferrer';a.textContent=part;output.append(a)}else output.append(document.createTextNode(part))})};const pollDwsDeviceLogin=async()=>{try{const r=await fetch('/api/dws-device-login',{cache:'no-store'}),b=await r.json();if(r.ok){renderDwsDeviceOutput(b.output);document.querySelector('#dwsDevicePanel')?.classList.add('visible');if(b.running)setTimeout(pollDwsDeviceLogin,1000);else void loadDwsAuth()}}catch(e){renderDwsDeviceOutput(e instanceof Error?e.message:String(e))}};const loadPrivateChatStatus=async()=>{try{const r=await fetch('/api/bot-status',{cache:'no-store'}),b=await r.json();if(!r.ok)throw new Error(b.error||'私聊机器人状态查询失败');const enabled=document.querySelector('#privateChatEnabled'),status=document.querySelector('#privateChatStatus');enabled.checked=b.status?.enabled===true;status.textContent=b.status?.connected?'已连接':'未连接'}catch(e){document.querySelector('#privateChatStatus').textContent='查询失败'}};document.querySelector('#privateChatEnabled').onchange=async event=>{const enabled=event.target.checked;try{const stateResponse=await fetch('/api/state',{cache:'no-store'}),state=await stateResponse.json();if(!stateResponse.ok||!state.config)throw new Error('读取当前配置失败');const r=await fetch('/api/config',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({...state.config,privateChatEnabled:enabled,clientSecret:state.config.clientSecret||''})});const body=await r.json().catch(()=>({}));if(!r.ok||body.saved!==true)throw new Error(body.error||'保存个人私聊开关失败');await loadPrivateChatStatus()}catch(e){event.target.checked=!enabled;alert(e instanceof Error?e.message:String(e))}};setInterval(()=>void loadPrivateChatStatus(),2000);const loadDwsAuth=async()=>{const output=document.querySelector('#dwsAuthStatus'),loading=document.querySelector('.dws-status-loading'),guide=document.querySelector('#dwsLoginGuide');try{const r=await fetch('/api/dws-auth-status',{cache:'no-store'}),b=await r.json();if(!r.ok)throw new Error(b.error||'DWS 登录状态查询失败');if(loading)loading.remove();const status=b.status||{};const set=(id,value)=>{const el=document.querySelector('#'+id);if(el)el.textContent=value||'-'};set('dwsCorpName',status.corp_name);set('dwsUserName',status.user_name);set('dwsUserId',status.user_id);set('dwsExpiresAt',status.expires_at);set('dwsRefreshExpiresAt',status.refresh_expires_at);if(output)output.textContent='';if(b.status?.authenticated!==true&&guide)guide.innerHTML='当前未登录 DWS。请在运行 DWS 的机器执行 <code>dws auth login --device</code>,或点击“使用 Device Flow 登录”。'}catch(e){if(loading)loading.textContent='DWS 登录状态读取失败';if(output)output.textContent=e instanceof Error?e.message:String(e);if(guide)guide.innerHTML='请在运行 DWS 的机器执行 <code>dws auth login --device</code> 完成跨设备登录。'}};const dwsLoginButton=document.querySelector('#dwsDeviceLogin');dwsLoginButton.onclick=async()=>{dwsLoginButton.disabled=true;try{const r=await fetch('/api/dws-device-login',{method:'POST'}),b=await r.json();if(!r.ok)throw new Error(b.error||'DWS 登录启动失败');renderDwsDeviceOutput(b.message);document.querySelector('#dwsDevicePanel')?.classList.add('visible');void pollDwsDeviceLogin()}catch(e){renderDwsDeviceOutput(e instanceof Error?e.message:String(e));document.querySelector('#dwsDevicePanel')?.classList.add('visible')}finally{dwsLoginButton.disabled=false}};document.querySelector('#dwsDeviceCopy').onclick=async()=>{const text=document.querySelector('#dwsDeviceOutput')?.textContent||'';try{await navigator.clipboard.writeText(text);notice.textContent='登录信息已复制';setTimeout(()=>notice.textContent='',2000)}catch{notice.textContent='复制失败,请手动选择上方登录信息复制';}};document.querySelector('#dwsLogout').onclick=async()=>{if(!confirm('确认退出当前 DWS 登录吗?'))return;try{const r=await fetch('/api/dws-logout',{method:'POST'}),b=await r.json();if(!r.ok)throw new Error(b.error||'DWS 退出失败');await loadDwsAuth()}catch(e){alert(e instanceof Error?e.message:String(e))}};void loadDwsAuth();void loadPrivateChatStatus();agentSettings.querySelector('.agent-command-grid').append(...commandSettings.querySelector('.command-grid').children);commandSettings.remove();robotSettings.querySelector('.robot-row').innerHTML='<div class="field"><label>机器人名称 <a class="platform-link" href="https://open-dev.dingtalk.com/fe/app" target="_blank" rel="noreferrer">(开放平台)</a></label><input id="robotName" placeholder="例如:我的 AI 助手"></div><div class="field bot-identity-field"><label>机器人 openDingTalkId</label><div class="line"><input id="robotSenderOpenDingTalkId" placeholder="点击“获取 ID”自动填写" readonly><button class="search" id="botSearchButton" type="button">获取 ID</button></div><div id="botSearchResult" class="bot-user-results"></div></div><div class="field"><label>钉钉应用 Client ID</label><input id="clientId" placeholder="ding..."></div><div class="field"><label>钉钉应用 Client Secret</label><input id="clientSecret" type="password" autocomplete="new-password" placeholder="已配置时留空不修改"></div><div class="field"><label>卡片标题显示总耗时</label><label class="check-field"><input type="checkbox" id="showElapsed"> 显示任务总耗时</label></div><div class="field"><label>卡片更新间隔(秒)</label><input type="number" id="cardUpdateIntervalMs" min="0.5" max="60" step="0.5" placeholder="3"></div><div class="field"><label>机器人单聊授权人员</label><div class="line"><input id="botUserSearch" placeholder="搜索钉钉真实姓名"><button class="search" id="botUserSearchButton" type="button">搜索</button></div><div id="botUserResults" class="bot-user-results"></div><div id="botUserSelected" class="bot-user-selected"></div></div><div class="field super-admin-field"><label>Session 超级管理员</label><div class="super-admin-help">仅限已加入单聊授权人员的用户。超级管理员可以按目录查看和切换本机 Session。</div><div id="superAdminSelected" class="bot-user-results super-admin-list"></div></div>';[promptSettings,robotSettings,rulesPanel,repliesPanel].forEach(panel=>panel.classList.add('expanded'));const historySettings=document.querySelector('.history-settings'),ruleHistorySettings=document.querySelector('.rule-history-settings');const robotPage=document.createElement('div');robotPage.append(robotSettings,privateChatSettings,historySettings,ruleHistorySettings);const pageDefs=[['robot','机器人与权限','配置钉钉应用、卡片机器人和单聊授权人员。',robotPage],['dws','DWS 配置','查看当前本机 DWS 登录账号信息。',document.querySelector('.dws-settings')],['agent','Agent 引擎','选择默认执行引擎,并配置群内切换指令。',agentSettings],['prompt','提示词前缀','定义发送给 Agent 的群消息前缀,事件 JSON 将自动附加。',promptSettings],['rules','群监控绑定','管理绑定指令以及需要监听的群和人员。',rulesPanel],['monitor','进程日志','查看系统进程和实时日志,必要时重启整个系统。',monitorSettings],['replies','Agent回复','查看最近的群聊与单聊 Agent 处理记录。',repliesPanel]];root.replaceChildren();const shell=document.createElement('div');shell.className='app-shell';const sidebar=document.createElement('aside');sidebar.className='app-sidebar';sidebar.innerHTML='<div class="brand"><img class="brand-mark brand-icon" src="/assets/omi-icon.png" alt="omi" /><div><div class="brand-name">oh-my-im</div><div class="brand-sub">DingTalk Agent Console</div></div><button class="sidebar-toggle" type="button" title="收起侧边栏" aria-label="收起侧边栏">‹</button></div><div class="nav-label">Workspace</div><nav class="app-nav"></nav><div class="sidebar-foot">版本:<span class="version">__OMI_VERSION__</span> · <a href="https://github.com/duzhenxun/oh-my-im" target="_blank" rel="noreferrer">开源项目</a></div>';const main=document.createElement('div');main.className='app-main';const topbar=document.createElement('header');topbar.className='app-topbar';topbar.innerHTML='<button class="menu-button" type="button" aria-label="打开菜单">☰</button><div class="topbar-copy"><h1></h1><p></p></div><div class="topbar-actions"><button class="fullscreen-button" type="button" title="进入全屏">⛶ <span>全屏</span></button></div>';const content=document.createElement('div');content.className='app-content';pageDefs.forEach(([id,title,description,panel],index)=>{const nav=document.createElement('button');nav.type='button';nav.className='nav-item'+(index===0?' active':'');nav.dataset.page=id;nav.innerHTML='<span class="nav-index">0'+(index+1)+'</span><span>'+title+'</span>';sidebar.querySelector('.app-nav').append(nav);const page=document.createElement('section');page.className='app-page page-'+id+(index===0?' active':'');page.dataset.page=id;page.append(panel);content.append(page)});const scrim=document.createElement('div');scrim.className='mobile-scrim';main.append(topbar,content);shell.append(sidebar,main);root.append(shell,scrim,notice,saveControl);const desktopMedia=window.matchMedia('(min-width:821px)');const syncViewport=()=>{if(desktopMedia.matches)document.body.classList.remove('nav-open')};desktopMedia.addEventListener?.('change',syncViewport);syncViewport();const showPage=id=>{document.querySelectorAll('.nav-item').forEach(item=>item.classList.toggle('active',item.dataset.page===id));document.querySelectorAll('.app-page').forEach(page=>page.classList.toggle('active',page.dataset.page===id));const def=pageDefs.find(item=>item[0]===id)||pageDefs[0];topbar.querySelector('h1').textContent=def[1];topbar.querySelector('p').textContent=def[2];saveControl.style.display=id==='replies'||id==='monitor'?'none':'';if(location.hash!=='#'+id)history.replaceState(null,'','#'+id);document.body.classList.remove('nav-open');window.scrollTo({top:0,behavior:'instant'})};sidebar.querySelectorAll('.nav-item').forEach(item=>item.onclick=()=>showPage(item.dataset.page));const sidebarToggle=sidebar.querySelector('.sidebar-toggle'),setSidebarCollapsed=collapsed=>{document.body.classList.toggle('sidebar-collapsed',collapsed);sidebarToggle.textContent=collapsed?'›':'‹';sidebarToggle.title=collapsed?'展开侧边栏':'收起侧边栏';sidebarToggle.setAttribute('aria-label',sidebarToggle.title);localStorage.setItem('omi-sidebar-collapsed',collapsed?'1':'0')};setSidebarCollapsed(localStorage.getItem('omi-sidebar-collapsed')==='1');sidebarToggle.onclick=()=>setSidebarCollapsed(!document.body.classList.contains('sidebar-collapsed'));topbar.querySelector('.menu-button').onclick=()=>document.body.classList.toggle('nav-open');const fullscreenButton=topbar.querySelector('.fullscreen-button'),syncFullscreen=()=>{const active=Boolean(document.fullscreenElement);fullscreenButton.innerHTML=active?'⊠ <span>退出全屏</span>':'⛶ <span>全屏</span>';fullscreenButton.title=active?'退出全屏':'进入全屏'};fullscreenButton.onclick=async()=>{try{if(document.fullscreenElement)await document.exitFullscreen();else await document.documentElement.requestFullscreen()}catch(e){alert(e instanceof Error?e.message:String(e))}};document.addEventListener('fullscreenchange',syncFullscreen);syncFullscreen();scrim.onclick=()=>document.body.classList.remove('nav-open');const initialPage=pageDefs.some(item=>item[0]===location.hash.slice(1))?location.hash.slice(1):'robot';window.addEventListener('hashchange',()=>{const id=location.hash.slice(1);if(pageDefs.some(item=>item[0]===id))showPage(id)});showPage(initialPage);replyList.style.maxHeight='min(680px,calc(100vh - 230px))';replyList.style.overflowY='auto';replyList.style.display='flex';replyList.style.flexDirection='column';replyList.style.gap='12px';let loaded=false;
|
|
249
273
|
const esc=v=>{const d=document.createElement('div');d.textContent=v??'';return d.innerHTML};const fmt=t=>t?new Date(t).toLocaleString('zh-CN',{hour12:false}):'-';const memberRole=role=>/群主|owner/i.test(role||'')?'(群主)':/管理|admin/i.test(role||'')?'(管理)':'';
|
|
250
274
|
function option(value,label){const o=document.createElement('option');o.value=value;o.textContent=label;return o}
|
|
251
275
|
function renderBotUsers(){const selected=document.querySelector('#botUserSelected'),admins=document.querySelector('#superAdminSelected');selected.replaceChildren();admins.replaceChildren();selectedBotUsers.forEach(user=>{const chip=document.createElement('span');chip.className='bot-user-chip';chip.append(document.createTextNode(user.name));const remove=document.createElement('button');remove.type='button';remove.textContent='×';remove.title='移除';remove.onclick=()=>{selectedBotUsers=selectedBotUsers.filter(item=>item.id!==user.id);selectedSuperAdminIds=selectedSuperAdminIds.filter(id=>id!==user.id);renderBotUsers()};chip.append(remove);selected.append(chip);const adminLabel=document.createElement('label');adminLabel.className='bot-user-result';const box=document.createElement('input');box.type='checkbox';box.checked=selectedSuperAdminIds.includes(user.id);box.onchange=()=>{selectedSuperAdminIds=box.checked?[...new Set([...selectedSuperAdminIds,user.id])]:selectedSuperAdminIds.filter(id=>id!==user.id)};const name=document.createElement('span');name.textContent=user.name;adminLabel.append(box,name);admins.append(adminLabel)})}
|
|
@@ -259,7 +283,7 @@ async function loadConfiguredMembers(rule,group){const people=rule.querySelector
|
|
|
259
283
|
function addConfiguredRule(group){const rule=document.createElement('article');rule.className='rule';rule.innerHTML='<div class="rule-grid configured-grid"><div class="field"><label>钉钉群</label><div class="rule-value group-value"></div></div><div class="field"><label>钉钉人员</label><div class="member-list people"></div></div><button class="delete" title="删除规则">×</button></div>';rule._targets=group.targets;rule._group=group;rule.querySelector('.group-value').textContent=group.groupName;rule.querySelector('.delete').onclick=()=>rule.remove();rules.append(rule);void loadConfiguredMembers(rule,group)}
|
|
260
284
|
const keywordIds={pause:'keywordsPause',monitorOpen:'keywordsMonitorOpen',monitorStop:'keywordsMonitorStop',switchPi:'keywordsSwitchPi',switchCodex:'keywordsSwitchCodex'};
|
|
261
285
|
const parseKeywords=id=>document.querySelector('#'+id).value.split(/[||]+/).map(value=>value.trim()).filter(Boolean);
|
|
262
|
-
function renderStatic(data,replaceConfig=false){rulesPanel.querySelector('.rules-toggle').textContent='
|
|
286
|
+
function renderStatic(data,replaceConfig=false){rulesPanel.querySelector('.rules-toggle').textContent='钉钉群监控绑定 ('+data.config.targets.length+')';if(replaceConfig){if(document.querySelector('#privateChatEnabled'))document.querySelector('#privateChatEnabled').checked=data.config.privateChatEnabled!==false;document.querySelectorAll('input[name=agent]').forEach(input=>{input.checked=input.value===(data.config.agent||'codex')});document.querySelector('#robotName').value=data.config.robotName||'';document.querySelector('#clientId').value=data.config.clientId||'';document.querySelector('#showElapsed').checked=data.config.showElapsed!==false;document.querySelector('#historyGroupLimit').value=data.config.historyGroupLimit||10;document.querySelector('#historyMessageLimit').value=data.config.historyMessageLimit||20;document.querySelector('#historyPollIntervalSeconds').value=data.config.historyPollIntervalSeconds??5;document.querySelector('#ruleHistoryMessageLimit').value=data.config.ruleHistoryMessageLimit||20;document.querySelector('#ruleHistoryPollIntervalSeconds').value=data.config.ruleHistoryPollIntervalSeconds??5;document.querySelector('#cardUpdateIntervalMs').value=(data.config.cardUpdateIntervalMs||3000)/1000;document.querySelector('#clientSecret').value='';document.querySelector('#robotSenderOpenDingTalkId').value=data.config.robotSenderOpenDingTalkId||'';document.querySelector('#groupPromptPrefix').value=data.config.groupPromptPrefix||'';setBotUsers(data.config.botAllowedUserIds||[],data.config.botAllowedUserNames||{},data.config.botSuperAdminUserIds||[]);Object.entries(keywordIds).forEach(([key,id])=>{document.querySelector('#'+id).value=(data.config.commandKeywords?.[key]||[]).join('|')})}const c=document.querySelector('#connection');if(c){c.textContent=data.status.eventConnected?'事件连接正常':'事件未连接';c.className='status '+(data.status.eventConnected?'connected':'stopped');}const list=document.querySelector('#replies');if(!list)return;if(!data.replies.length){list.className='empty';list.textContent='暂无回复';return}list.className='';const recentReplies=[...data.replies].sort((a,b)=>new Date(a.createdAt||0)-new Date(b.createdAt||0)).slice(-10);const replySignature=recentReplies.map(r=>r.id+':'+r.status+':'+r.content).join('|');if(list.dataset.signature===replySignature)return;const wasAtBottom=list.scrollHeight-list.scrollTop-list.clientHeight<24;list.dataset.signature=replySignature;list.innerHTML=recentReplies.map(r=>{const state=r.status==='processing'?'处理中':r.status==='completed'?'完成':'失败',replyStyle=r.status==='processing'?'#fffbeb;color:#92400e':r.status==='completed'?'#ecfdf3;color:#166534':'#fff1f2;color:#b42318',senderNames=[...new Set((r.senderNames||[]).filter(Boolean))].join('、');const conversationLabel=r.conversationType==='personal'?'个人':'群聊 · 群名:'+esc(r.conversationName||r.groupName||'-');return '<article class="reply"><div class="meta">'+conversationLabel+' · 发送人:'+esc(senderNames||'-')+' · '+fmt(r.createdAt)+' · '+esc(r.agent==='pi'?'Pi':'Codex')+' · '+state+' · '+r.messageCount+' 条消息</div><div style="margin-top:6px;padding:8px;border-radius:4px;background:#eff6ff;color:#174ea6"><pre style="margin:0;white-space:pre-wrap;word-break:break-word;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace">'+esc(r.question||'')+'</pre></div><div style="margin-top:6px;padding:8px;border-radius:4px;background:'+replyStyle+'"><pre style="margin:0;white-space:pre-wrap;word-break:break-word;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace">'+esc(r.content)+'</pre></div></article>'}).join('');if(wasAtBottom||recentReplies.length>0)list.scrollTop=list.scrollHeight}
|
|
263
287
|
async function refresh(full=false){try{const r=await fetch('/api/state',{cache:'no-store'});if(!r.ok)throw new Error('读取状态失败');const data=await r.json();renderStatic(data,full);if(full){rules.replaceChildren();const grouped=new Map();data.config.targets.forEach(t=>{const key=t.groupId;if(!grouped.has(key))grouped.set(key,{groupId:t.groupId,groupName:t.groupName,targets:[]});grouped.get(key).targets.push(t)});grouped.forEach(addConfiguredRule);const formatInput=document.querySelector('input[name=format][value='+data.config.replyFormat+']');if(formatInput)formatInput.checked=true;loaded=true}}catch(e){notice.textContent=e.message;notice.className='notice error'}}
|
|
264
|
-
setInterval(()=>{void refresh(false)},1000);document.querySelector('#add').onclick=()=>addDraftRule();document.querySelector('#save').onclick=async()=>{const saveButton=document.querySelector('#save');try{const stateResponse=await fetch('/api/state',{cache:'no-store'}),state=await stateResponse.json();if(!stateResponse.ok||!state.config)throw new Error('读取当前配置失败');const targets=[...rules.children].flatMap(rule=>{if(rule._targets&&!rule._members)return rule._targets;const groupId=rule._group?.groupId||rule.querySelector('[data-key=groupId]')?.value,groupName=rule._group?.groupName||rule.querySelector('[data-key=groupName]')?.value,members=rule._members||[],selected=[...rule.querySelectorAll('[data-sender-id]:checked')];return selected.map(box=>{const m=members.find(x=>x.senderId===box.dataset.senderId);return {groupId,groupName,senderId:box.dataset.senderId,senderName:m?m.senderName:''}})}),robotName=document.querySelector('#robotName').value.trim(),clientId=document.querySelector('#clientId').value.trim(),showElapsed=document.querySelector('#showElapsed').checked,historyGroupLimit=Number(document.querySelector('#historyGroupLimit').value||10),historyMessageLimit=Number(document.querySelector('#historyMessageLimit').value||20),historyPollIntervalSeconds=Number(document.querySelector('#historyPollIntervalSeconds').value||0),cardUpdateIntervalMs=Number(document.querySelector('#cardUpdateIntervalMs').value||3),clientSecret=document.querySelector('#clientSecret').value.trim(),robotSenderOpenDingTalkId=document.querySelector('#robotSenderOpenDingTalkId').value.trim(),groupPromptPrefix=document.querySelector('#groupPromptPrefix').value.trim(),botAllowedUserIds=selectedBotUsers.map(user=>user.id),botAllowedUserNames=Object.fromEntries(selectedBotUsers.map(user=>[user.id,user.name])),botSuperAdminUserIds=[...selectedSuperAdminIds],botSuperAdminUserNames=Object.fromEntries(selectedBotUsers.filter(user=>selectedSuperAdminIds.includes(user.id)).map(user=>[user.id,user.name])),commandKeywords={pause:parseKeywords(keywordIds.pause),monitorOpen:parseKeywords(keywordIds.monitorOpen),monitorStop:parseKeywords(keywordIds.monitorStop),switchPi:parseKeywords(keywordIds.switchPi),switchCodex:parseKeywords(keywordIds.switchCodex)},agent=agentSelect().value,replyFormat=document.querySelector('input[name=format]:checked')?.value; if(!replyFormat)throw new Error('请选择回复格式');notice.textContent='保存中...';notice.className='notice';saveButton.disabled=true;saveButton.textContent='保存中...';const r=await fetch('/api/config',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({...state.config,targets,botAllowedUserIds,botAllowedUserNames,robotSenderOpenDingTalkId,groupPromptPrefix,cardUpdateIntervalMs:cardUpdateIntervalMs*1000,showElapsed,historyGroupLimit,historyMessageLimit,historyPollIntervalSeconds,botSuperAdminUserIds,botSuperAdminUserNames,commandKeywords,replyFormat,robotName,clientId,clientSecret,agent})});const b=await r.json().catch(()=>({}));if(!r.ok||b.saved!==true)throw new Error(b.error||'保存失败:服务端未确认保存');notice.textContent='已保存,钉钉规则与机器人设置已立即生效。';notice.className='notice';await refresh(true)}catch(e){console.error('[Dashboard] save failed',e);notice.textContent=e instanceof Error?e.message:String(e);notice.className='notice error'}finally{saveButton.disabled=false;saveButton.textContent='保存并生效';setTimeout(()=>{if(!notice.classList.contains('error'))notice.textContent=''},3000)}};refresh(true);
|
|
288
|
+
let latestSystemLog='',systemLogOffset;const filterSystemLog=(content,level)=>{if(level==='all')return content;const lines=content.split('\n'),entries=[];let current=[];const isEntryStart=line=>/^\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+\[[^\]]+\]\s+\[(DEBUG|INFO|WARN|ERROR)\]/.test(line);lines.forEach(line=>{if(isEntryStart(line)){if(current.length)entries.push(current);current=[line]}else if(current.length)current.push(line)});if(current.length)entries.push(current);return entries.filter(entry=>entry[0].includes('['+level+']')).map(entry=>entry.join('\n')).join('\n')};const renderSystemLog=()=>{const log=document.querySelector('#systemLog'),level=document.querySelector('#monitorLogLevel').value,filtered=filterSystemLog(latestSystemLog,level),atBottom=log.scrollHeight-log.scrollTop-log.clientHeight<30;if(log.textContent!==filtered){log.textContent=filtered||'暂无匹配日志';if(document.querySelector('#monitorAutoScroll').checked&&(atBottom||!log.dataset.loaded))log.scrollTop=log.scrollHeight;log.dataset.loaded='1'}};document.querySelector('#monitorLogLevel').onchange=renderSystemLog;document.querySelector('#monitorAutoScroll').onchange=event=>{document.querySelector('#systemLogMeta').textContent=event.target.checked?'正在恢复实时拉取...':'实时拉取已暂停';if(event.target.checked)void loadSystemMonitor()};const loadSystemMonitor=async()=>{try{const statusResponse=await fetch('/api/system-status',{cache:'no-store'}),statusBody=await statusResponse.json();if(!statusResponse.ok)throw new Error(statusBody.error||'系统状态读取失败');const status=statusBody.status||{},processes=document.querySelector('#monitorProcesses'),meta=document.querySelector('#monitorMeta');meta.textContent='模式:'+(status.mode||'-')+' · 启动:'+fmt(status.startedAt)+' · 检查:'+fmt(status.checkedAt);processes.innerHTML=(status.processes||[]).map(item=>'<article class="process-card"><strong>'+esc(item.role)+'</strong><div class="process-state '+(item.running?'running':'')+'">'+(item.running?'运行中':'已停止')+'</div><code>PID '+esc(String(item.pid||'-'))+(item.ppid?' · PPID '+esc(String(item.ppid)):'')+'</code></article>').join('')||'<div class="empty">未发现系统进程</div>';if(!document.querySelector('#monitorAutoScroll').checked){document.querySelector('#systemLogMeta').textContent='实时拉取已暂停';return}const logUrl='/api/system-logs'+(systemLogOffset===undefined?'':'?offset='+encodeURIComponent(systemLogOffset)),logResponse=await fetch(logUrl,{cache:'no-store'}),logBody=await logResponse.json();if(!logResponse.ok)throw new Error(logBody.error||'系统日志读取失败');if(logBody.reset||systemLogOffset===undefined)latestSystemLog=logBody.content||'';else latestSystemLog+=logBody.content||'';systemLogOffset=Number(logBody.nextOffset||0);if(latestSystemLog.length>1000000){latestSystemLog=latestSystemLog.slice(-1000000).replace(/^[^\n]*\n/,'')}renderSystemLog();document.querySelector('#systemLogMeta').textContent=(logBody.path||'')+' · '+Number(logBody.size||0).toLocaleString()+' bytes · tail 实时增量'}catch(e){document.querySelector('#monitorMeta').textContent=e instanceof Error?e.message:String(e)}};document.querySelector('#restartSystem').onclick=async()=>{if(!confirm('确认重启整个 oh-my-im 系统吗?当前正在执行的任务会被中断。'))return;const button=document.querySelector('#restartSystem');button.disabled=true;button.textContent='正在重启...';try{const r=await fetch('/api/system-restart',{method:'POST'}),body=await r.json().catch(()=>({}));if(!r.ok)throw new Error(body.error||'重启失败');document.querySelector('#monitorMeta').textContent='重启指令已发送,等待服务恢复...';setTimeout(()=>location.reload(),4000)}catch(e){alert(e instanceof Error?e.message:String(e));button.disabled=false;button.textContent='重启系统'}};setInterval(()=>{if(document.querySelector('.page-monitor')?.classList.contains('active'))void loadSystemMonitor()},1000);void loadSystemMonitor();setInterval(()=>{void refresh(false)},1000);document.querySelector('#add').onclick=()=>addDraftRule();document.querySelector('#save').onclick=async()=>{const saveButton=document.querySelector('#save');try{const stateResponse=await fetch('/api/state',{cache:'no-store'}),state=await stateResponse.json();if(!stateResponse.ok||!state.config)throw new Error('读取当前配置失败');const targets=[...rules.children].flatMap(rule=>{if(rule._targets&&!rule._members)return rule._targets;const groupId=rule._group?.groupId||rule.querySelector('[data-key=groupId]')?.value,groupName=rule._group?.groupName||rule.querySelector('[data-key=groupName]')?.value,members=rule._members||[],selected=[...rule.querySelectorAll('[data-sender-id]:checked')];return selected.map(box=>{const m=members.find(x=>x.senderId===box.dataset.senderId);return {groupId,groupName,senderId:box.dataset.senderId,senderName:m?m.senderName:''}})}),robotName=document.querySelector('#robotName').value.trim(),clientId=document.querySelector('#clientId').value.trim(),showElapsed=document.querySelector('#showElapsed').checked,historyGroupLimit=Number(document.querySelector('#historyGroupLimit').value||10),historyMessageLimit=Number(document.querySelector('#historyMessageLimit').value||20),historyPollIntervalSeconds=Number(document.querySelector('#historyPollIntervalSeconds').value||0),ruleHistoryMessageLimit=Number(document.querySelector('#ruleHistoryMessageLimit').value||20),ruleHistoryPollIntervalSeconds=Number(document.querySelector('#ruleHistoryPollIntervalSeconds').value||0),cardUpdateIntervalMs=Number(document.querySelector('#cardUpdateIntervalMs').value||3),clientSecret=document.querySelector('#clientSecret').value.trim(),robotSenderOpenDingTalkId=document.querySelector('#robotSenderOpenDingTalkId').value.trim(),groupPromptPrefix=document.querySelector('#groupPromptPrefix').value.trim(),botAllowedUserIds=selectedBotUsers.map(user=>user.id),botAllowedUserNames=Object.fromEntries(selectedBotUsers.map(user=>[user.id,user.name])),botSuperAdminUserIds=[...selectedSuperAdminIds],botSuperAdminUserNames=Object.fromEntries(selectedBotUsers.filter(user=>selectedSuperAdminIds.includes(user.id)).map(user=>[user.id,user.name])),commandKeywords={pause:parseKeywords(keywordIds.pause),monitorOpen:parseKeywords(keywordIds.monitorOpen),monitorStop:parseKeywords(keywordIds.monitorStop),switchPi:parseKeywords(keywordIds.switchPi),switchCodex:parseKeywords(keywordIds.switchCodex)},agent=agentSelect().value,replyFormat=document.querySelector('input[name=format]:checked')?.value; if(!replyFormat)throw new Error('请选择回复格式');notice.textContent='保存中...';notice.className='notice';saveButton.disabled=true;saveButton.textContent='保存中...';const r=await fetch('/api/config',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({...state.config,targets,botAllowedUserIds,botAllowedUserNames,robotSenderOpenDingTalkId,groupPromptPrefix,cardUpdateIntervalMs:cardUpdateIntervalMs*1000,showElapsed,historyGroupLimit,historyMessageLimit,historyPollIntervalSeconds,ruleHistoryMessageLimit,ruleHistoryPollIntervalSeconds,botSuperAdminUserIds,botSuperAdminUserNames,commandKeywords,replyFormat,robotName,clientId,clientSecret,agent})});const b=await r.json().catch(()=>({}));if(!r.ok||b.saved!==true)throw new Error(b.error||'保存失败:服务端未确认保存');notice.textContent='已保存,钉钉规则与机器人设置已立即生效。';notice.className='notice';await refresh(true)}catch(e){console.error('[Dashboard] save failed',e);notice.textContent=e instanceof Error?e.message:String(e);notice.className='notice error'}finally{saveButton.disabled=false;saveButton.textContent='保存并生效';setTimeout(()=>{if(!notice.classList.contains('error'))notice.textContent=''},3000)}};refresh(true);
|
|
265
289
|
</script></body></html>`;
|
package/dist/dws-listener.js
CHANGED
|
@@ -47,6 +47,7 @@ const EMPTY_COMMAND_KEYWORDS = {
|
|
|
47
47
|
pause: [], monitorOpen: [], monitorStop: [], switchPi: [], switchCodex: [],
|
|
48
48
|
};
|
|
49
49
|
import { startDwsHistoryPolling } from "./dws-history.js";
|
|
50
|
+
import { startDwsRuleHistoryPolling } from "./dws-rule-history.js";
|
|
50
51
|
function hasMention(event) {
|
|
51
52
|
const hasValue = (value) => {
|
|
52
53
|
if (Array.isArray(value))
|
|
@@ -247,6 +248,8 @@ function normalizeDashboardConfig(parsed) {
|
|
|
247
248
|
historyGroupLimit: Number.isInteger(parsed.historyGroupLimit) ? Math.max(1, Math.min(500, parsed.historyGroupLimit)) : 10,
|
|
248
249
|
historyMessageLimit: Number.isInteger(parsed.historyMessageLimit) ? Math.max(1, Math.min(500, parsed.historyMessageLimit)) : 20,
|
|
249
250
|
historyPollIntervalSeconds: Number.isFinite(parsed.historyPollIntervalSeconds) ? Math.max(0, Math.min(3600, parsed.historyPollIntervalSeconds)) : 5,
|
|
251
|
+
ruleHistoryMessageLimit: Number.isInteger(parsed.ruleHistoryMessageLimit) ? Math.max(1, Math.min(500, parsed.ruleHistoryMessageLimit)) : 20,
|
|
252
|
+
ruleHistoryPollIntervalSeconds: Number.isFinite(parsed.ruleHistoryPollIntervalSeconds) ? Math.max(0, Math.min(3600, parsed.ruleHistoryPollIntervalSeconds)) : 5,
|
|
250
253
|
targets,
|
|
251
254
|
botAllowedUserIds: Array.isArray(parsed.botAllowedUserIds) && parsed.botAllowedUserIds.length > 0
|
|
252
255
|
? [...new Set(parsed.botAllowedUserIds.map((id) => id.trim()).filter(Boolean))]
|
|
@@ -337,6 +340,8 @@ async function loadDashboardConfig() {
|
|
|
337
340
|
historyGroupLimit: 10,
|
|
338
341
|
historyMessageLimit: 20,
|
|
339
342
|
historyPollIntervalSeconds: 5,
|
|
343
|
+
ruleHistoryMessageLimit: 20,
|
|
344
|
+
ruleHistoryPollIntervalSeconds: 5,
|
|
340
345
|
targets,
|
|
341
346
|
botAllowedUserIds: defaultBotAllowedUserIds(targets),
|
|
342
347
|
botAllowedUserNames: {},
|
|
@@ -1233,6 +1238,11 @@ function startGroupListener(seen, queues, cardState, sessions, cardClient, getDa
|
|
|
1233
1238
|
configuredGroupIds,
|
|
1234
1239
|
acceptEvent: (event) => acceptEvent(event),
|
|
1235
1240
|
});
|
|
1241
|
+
const stopRuleHistoryPolling = startDwsRuleHistoryPolling({
|
|
1242
|
+
getConfig: getDashboardConfig,
|
|
1243
|
+
configuredGroupIds,
|
|
1244
|
+
acceptEvent: (event) => acceptEvent(event),
|
|
1245
|
+
});
|
|
1236
1246
|
return new Promise((resolve, reject) => {
|
|
1237
1247
|
listener.on("error", (err) => {
|
|
1238
1248
|
runtime.eventConnected = false;
|
|
@@ -1242,6 +1252,7 @@ function startGroupListener(seen, queues, cardState, sessions, cardClient, getDa
|
|
|
1242
1252
|
listener.on("close", (code) => {
|
|
1243
1253
|
runtime.eventConnected = false;
|
|
1244
1254
|
stopHistoryPolling();
|
|
1255
|
+
stopRuleHistoryPolling();
|
|
1245
1256
|
const close = code === 0
|
|
1246
1257
|
? Promise.resolve()
|
|
1247
1258
|
: Promise.reject(new Error(`dws event consume exited with code ${code}`));
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { getCurrentDwsUser, listGroupMessages } from "./dws-client.js";
|
|
2
|
+
import { createLogger } from "./logger.js";
|
|
3
|
+
const log = createLogger("DwsRuleHistory");
|
|
4
|
+
const DEFAULT_POLL_INTERVAL_SECONDS = 5;
|
|
5
|
+
const DEFAULT_MESSAGE_LIMIT = 20;
|
|
6
|
+
function senderName(event) {
|
|
7
|
+
const raw = event.sender;
|
|
8
|
+
if (!raw || typeof raw !== "object")
|
|
9
|
+
return "";
|
|
10
|
+
const sender = raw;
|
|
11
|
+
return [sender.name, sender.nick, sender.displayName]
|
|
12
|
+
.find((value) => typeof value === "string" && Boolean(value.trim()))?.trim() || "";
|
|
13
|
+
}
|
|
14
|
+
function isCurrentUser(event, currentName) {
|
|
15
|
+
return Boolean(currentName && senderName(event) === currentName);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Poll only the groups present in the dashboard's group-monitoring rules.
|
|
19
|
+
* As with the general history poller, only messages whose sender name equals
|
|
20
|
+
* `dws contact +me`'s name are forwarded to the listener's shared filtering,
|
|
21
|
+
* deduplication, command and queue pipeline.
|
|
22
|
+
*/
|
|
23
|
+
export function startDwsRuleHistoryPolling(options) {
|
|
24
|
+
let stopped = false;
|
|
25
|
+
let polling = false;
|
|
26
|
+
let currentName = "";
|
|
27
|
+
let lastUserResolveFailure = "";
|
|
28
|
+
let lastPollStartedAt = Date.now();
|
|
29
|
+
const refreshCurrentUser = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const user = await getCurrentDwsUser();
|
|
32
|
+
currentName = user.name?.trim() || "";
|
|
33
|
+
lastUserResolveFailure = "";
|
|
34
|
+
log.info(`current DWS user for rule history name=${currentName || "<none>"}`);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const message = String(err);
|
|
38
|
+
if (message !== lastUserResolveFailure)
|
|
39
|
+
log.warn(`unable to resolve current DWS user for rule history: ${message}`);
|
|
40
|
+
lastUserResolveFailure = message;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const poll = async () => {
|
|
44
|
+
if (stopped || polling)
|
|
45
|
+
return;
|
|
46
|
+
polling = true;
|
|
47
|
+
try {
|
|
48
|
+
const config = options.getConfig();
|
|
49
|
+
const intervalSeconds = Number.isFinite(config.ruleHistoryPollIntervalSeconds)
|
|
50
|
+
? config.ruleHistoryPollIntervalSeconds : DEFAULT_POLL_INTERVAL_SECONDS;
|
|
51
|
+
if (intervalSeconds <= 0)
|
|
52
|
+
return;
|
|
53
|
+
const groups = [...new Set(options.configuredGroupIds(config).map((id) => id.trim()).filter(Boolean))];
|
|
54
|
+
// With no configured monitoring groups, return before resolving the
|
|
55
|
+
// current user so this poller makes no DWS API calls at all.
|
|
56
|
+
if (groups.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
if (!currentName)
|
|
59
|
+
await refreshCurrentUser();
|
|
60
|
+
if (!currentName)
|
|
61
|
+
return;
|
|
62
|
+
const messageLimit = config.ruleHistoryMessageLimit || DEFAULT_MESSAGE_LIMIT;
|
|
63
|
+
const lookbackMs = Math.max(20_000, intervalSeconds * 2_000);
|
|
64
|
+
log.info(`rule history poll groups=${groups.length} messages=${messageLimit} currentUserName=${currentName}`);
|
|
65
|
+
for (const groupId of groups) {
|
|
66
|
+
if (stopped)
|
|
67
|
+
break;
|
|
68
|
+
try {
|
|
69
|
+
const messages = await listGroupMessages(groupId, new Date(Date.now() - lookbackMs), messageLimit);
|
|
70
|
+
const currentUserMessages = messages.filter((event) => isCurrentUser(event, currentName));
|
|
71
|
+
if (messages.length > 0) {
|
|
72
|
+
log.debug(`rule history poll group=${groupId} received=${messages.length} matchedCurrentUserName=${currentUserMessages.length}`);
|
|
73
|
+
}
|
|
74
|
+
currentUserMessages.forEach(options.acceptEvent);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
log.warn(`rule history poll group failed group=${groupId}: ${String(err)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
polling = false;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
void poll().catch((err) => log.warn(`rule history poll failed: ${String(err)}`));
|
|
86
|
+
const scheduler = setInterval(() => {
|
|
87
|
+
const seconds = Number(options.getConfig().ruleHistoryPollIntervalSeconds);
|
|
88
|
+
const intervalMs = Math.max(1, Number.isFinite(seconds) ? seconds : DEFAULT_POLL_INTERVAL_SECONDS) * 1_000;
|
|
89
|
+
if (Date.now() - lastPollStartedAt < intervalMs)
|
|
90
|
+
return;
|
|
91
|
+
lastPollStartedAt = Date.now();
|
|
92
|
+
void poll().catch((err) => log.warn(`rule history poll failed: ${String(err)}`));
|
|
93
|
+
}, 1_000);
|
|
94
|
+
scheduler.unref();
|
|
95
|
+
return () => { stopped = true; clearInterval(scheduler); };
|
|
96
|
+
}
|
package/dist/logger.js
CHANGED
|
@@ -11,12 +11,18 @@ function parseLevel(value) {
|
|
|
11
11
|
}
|
|
12
12
|
return "info";
|
|
13
13
|
}
|
|
14
|
+
function chinaTimestamp(date = new Date()) {
|
|
15
|
+
// China Standard Time is UTC+8 and has no daylight-saving transitions.
|
|
16
|
+
// Format explicitly instead of using toISOString(), which always emits UTC.
|
|
17
|
+
const china = new Date(date.getTime() + 8 * 60 * 60 * 1_000);
|
|
18
|
+
return `${china.toISOString().slice(0, -1)}+08:00`;
|
|
19
|
+
}
|
|
14
20
|
export function createLogger(scope) {
|
|
15
21
|
const minLevel = parseLevel(process.env.LOG_LEVEL);
|
|
16
22
|
function emit(level, message, ...args) {
|
|
17
23
|
if (LEVEL_WEIGHT[level] < LEVEL_WEIGHT[minLevel])
|
|
18
24
|
return;
|
|
19
|
-
const prefix = `[${
|
|
25
|
+
const prefix = `[${chinaTimestamp()}] [${scope}] [${level.toUpperCase()}]`;
|
|
20
26
|
const line = `${prefix} ${message}`;
|
|
21
27
|
if (level === "error")
|
|
22
28
|
console.error(line, ...args);
|