@xmanrui/dsh-im 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +25 -10
- package/README.md +26 -11
- package/assets/logo-dsh-im-connecting-readme-3x2.png +0 -0
- package/assets/logo-dsh-im-connecting-square-v4.png +0 -0
- package/assets/logo-plugin-phone.png +0 -0
- package/lib/client.js +815 -173
- package/lib/index.js +142 -130
- package/package.json +4 -3
- package/plugin-src/client/build.mjs +1 -6
- package/plugin-src/client/channel-logos.js +12 -0
- package/plugin-src/client/channels/office/api.js +44 -0
- package/plugin-src/client/channels/office/index.js +152 -0
- package/plugin-src/client/channels/office/styles.js +50 -0
- package/plugin-src/client/channels/shared/token-api.js +5 -1
- package/plugin-src/client/channels/shared/token-channel.js +16 -1
- package/plugin-src/client/channels/telegram/api.js +17 -2
- package/plugin-src/client/channels/telegram/index.js +112 -0
- package/plugin-src/client/channels/telegram/styles.js +26 -0
- package/plugin-src/client/i18n.js +76 -0
- package/plugin-src/client/index.js +22 -11
- package/plugin-src/client/styles.js +7 -4
- package/plugin-src/host/channels/office/index.mjs +10 -0
- package/plugin-src/host/channels/office/production.mjs +45 -0
- package/plugin-src/host/channels/office/rpc.mjs +48 -0
- package/plugin-src/host/channels/shared/production.mjs +9 -1
- package/plugin-src/host/channels/telegram/production.mjs +2 -0
- package/plugin-src/host/channels/telegram/rpc.mjs +58 -8
- package/plugin-src/host/index.mjs +3 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/office/config-store.mjs +107 -0
- package/src/channels/office/office-controller.mjs +167 -0
- package/src/channels/office/office-job-executor.mjs +390 -0
- package/src/channels/office/office-runtime.mjs +171 -0
- package/src/channels/office/office-transport.mjs +170 -0
- package/src/channels/office/protocol.mjs +43 -0
- package/src/channels/shared/token-bot-controller.mjs +28 -0
- package/src/channels/shared/token-config-store.mjs +14 -1
- package/src/channels/telegram/config-store.mjs +62 -1
- package/src/channels/telegram/telegram-controller.mjs +25 -1
- package/src/channels/telegram/telegram-runtime.mjs +26 -1
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-controller.mjs +56 -12
- package/src/channels/weixin/weixin-runtime.mjs +12 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"slack",
|
|
20
20
|
"telegram",
|
|
21
21
|
"discord",
|
|
22
|
-
"whatsapp"
|
|
22
|
+
"whatsapp",
|
|
23
|
+
"ai-office"
|
|
23
24
|
],
|
|
24
25
|
"author": {
|
|
25
26
|
"name": "xmanrui",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir,
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
@@ -8,8 +8,6 @@ const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
|
|
8
8
|
const packageRoot = resolve(sourceDirectory, '../..');
|
|
9
9
|
const outputPath = resolve(packageRoot, 'lib/client.js');
|
|
10
10
|
const loaderId = process.env.DSH_IM_CLIENT_ID ?? '@xmanrui/dsh-im';
|
|
11
|
-
const logoPath = resolve(packageRoot, 'assets/logo-icon.png');
|
|
12
|
-
const logoDataUrl = `data:image/png;base64,${(await readFile(logoPath)).toString('base64')}`;
|
|
13
11
|
|
|
14
12
|
const result = await build({
|
|
15
13
|
entryPoints: [resolve(sourceDirectory, 'index.js')],
|
|
@@ -18,9 +16,6 @@ const result = await build({
|
|
|
18
16
|
platform: 'browser',
|
|
19
17
|
target: ['chrome100'],
|
|
20
18
|
external: ['react', 'react-dom'],
|
|
21
|
-
define: {
|
|
22
|
-
'globalThis.__DSH_IM_LOGO_DATA_URL__': JSON.stringify(logoDataUrl),
|
|
23
|
-
},
|
|
24
19
|
write: false,
|
|
25
20
|
minify: process.env.NODE_ENV === 'production',
|
|
26
21
|
legalComments: 'none',
|
|
@@ -131,3 +131,15 @@ export function WhatsappLogoGlyph({ size } = {}) {
|
|
|
131
131
|
d: 'M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.521.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51a12.8 12.8 0 0 0-.57-.01c-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479s1.065 2.875 1.213 3.074c.149.198 2.095 3.2 5.077 4.487.709.306 1.262.489 1.694.626.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.29.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.981.999-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.83 9.83 0 0 1 2.893 6.991c-.003 5.45-4.437 9.884-9.886 9.884m8.413-18.297A11.8 11.8 0 0 0 12.055 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.9 11.9 0 0 0 5.688 1.448h.005c6.557 0 11.892-5.335 11.895-11.893a11.82 11.82 0 0 0-3.486-8.413',
|
|
132
132
|
}));
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
export function OfficeLogoGlyph({ size } = {}) {
|
|
136
|
+
return h('svg', {
|
|
137
|
+
...dimensions(size),
|
|
138
|
+
viewBox: '0 0 24 24',
|
|
139
|
+
focusable: 'false',
|
|
140
|
+
'aria-hidden': 'true',
|
|
141
|
+
'data-im-channel-logo': 'office',
|
|
142
|
+
},
|
|
143
|
+
h('path', { fill: 'currentColor', d: 'M4 3.5h10.5a2 2 0 0 1 2 2v13H4v-15Zm2.2 3v1.8h2V6.5h-2Zm4.1 0v1.8h2V6.5h-2Zm-4.1 4v1.8h2v-1.8h-2Zm4.1 0v1.8h2v-1.8h-2ZM8.4 15v3.5h3V15h-3Z' }),
|
|
144
|
+
h('path', { fill: 'currentColor', d: 'M18.3 8.2h1.5v3h3v1.5h-3v3h-1.5v-3h-3v-1.5h3v-3Z' }));
|
|
145
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OFFICE_PROTOCOL_VERSION,
|
|
3
|
+
OFFICE_RPC_CHANNEL,
|
|
4
|
+
OFFICE_RPC_ENDPOINTS,
|
|
5
|
+
officeHookUrls,
|
|
6
|
+
} from '../../../../src/channels/office/protocol.mjs';
|
|
7
|
+
|
|
8
|
+
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
9
|
+
|
|
10
|
+
export function unwrapOfficeRpc(result) {
|
|
11
|
+
if (!record(result) || typeof result.ok !== 'boolean') throw new Error('AI Office 服务返回了无法识别的响应');
|
|
12
|
+
if (!result.ok) {
|
|
13
|
+
const error = new Error(typeof result.error?.message === 'string' ? result.error.message : 'AI Office 操作失败');
|
|
14
|
+
error.code = typeof result.error?.code === 'string' ? result.error.code : 'office-rpc-error';
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
return result.value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeOfficeStatus(value) {
|
|
21
|
+
if (!record(value) || value.configured !== true) {
|
|
22
|
+
return { configured: false, connected: false, state: 'unconfigured', config: null, health: null };
|
|
23
|
+
}
|
|
24
|
+
const config = record(value.config) ? value.config : {};
|
|
25
|
+
return {
|
|
26
|
+
configured: true,
|
|
27
|
+
connected: value.connected === true,
|
|
28
|
+
state: typeof value.state === 'string' ? value.state : 'idle',
|
|
29
|
+
tokenConfigured: value.tokenConfigured === true,
|
|
30
|
+
config: {
|
|
31
|
+
protocolVersion: config.protocolVersion ?? OFFICE_PROTOCOL_VERSION,
|
|
32
|
+
baseUrl: typeof config.baseUrl === 'string' ? config.baseUrl : '',
|
|
33
|
+
deviceId: typeof config.deviceId === 'string' ? config.deviceId : '',
|
|
34
|
+
maxConcurrency: Number(config.maxConcurrency ?? 1),
|
|
35
|
+
heartbeatSeconds: Number(config.heartbeatSeconds ?? 30),
|
|
36
|
+
workspaces: record(config.workspaces) ? config.workspaces : {},
|
|
37
|
+
instructionPresets: record(config.instructionPresets) ? config.instructionPresets : {},
|
|
38
|
+
hooks: record(config.hooks) ? config.hooks : {},
|
|
39
|
+
},
|
|
40
|
+
health: record(value.health) ? value.health : null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { OFFICE_PROTOCOL_VERSION, OFFICE_RPC_CHANNEL, OFFICE_RPC_ENDPOINTS, officeHookUrls };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { h } from '../../i18n.js';
|
|
4
|
+
import {
|
|
5
|
+
OFFICE_PROTOCOL_VERSION,
|
|
6
|
+
OFFICE_RPC_ENDPOINTS,
|
|
7
|
+
normalizeOfficeStatus,
|
|
8
|
+
officeHookUrls,
|
|
9
|
+
unwrapOfficeRpc,
|
|
10
|
+
} from './api.js';
|
|
11
|
+
|
|
12
|
+
function Button({ children, kind = 'secondary', ...props }) {
|
|
13
|
+
return h('button', { ...props, type: 'button', className: 'ddt-button', 'data-kind': kind }, children);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mapText(value) {
|
|
17
|
+
return Object.entries(value ?? {}).map(([key, item]) => `${key}=${item}`).join('\n');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseMap(value, label) {
|
|
21
|
+
const output = {};
|
|
22
|
+
for (const raw of value.split(/\r?\n/)) {
|
|
23
|
+
const line = raw.trim();
|
|
24
|
+
if (!line) continue;
|
|
25
|
+
const index = line.indexOf('=');
|
|
26
|
+
if (index < 1 || !line.slice(index + 1).trim()) {
|
|
27
|
+
throw new Error(label === 'Workspace 映射'
|
|
28
|
+
? 'Workspace 映射每行必须使用 alias=value'
|
|
29
|
+
: 'Instruction Preset 映射每行必须使用 alias=value');
|
|
30
|
+
}
|
|
31
|
+
output[line.slice(0, index).trim()] = line.slice(index + 1).trim();
|
|
32
|
+
}
|
|
33
|
+
return output;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stateLabel(model) {
|
|
37
|
+
if (model.connected) return '已连接 Office';
|
|
38
|
+
if (!model.configured) return '尚未配置';
|
|
39
|
+
if (model.state === 'connecting') return '正在连接';
|
|
40
|
+
if (model.state === 'reconnecting') return '等待重连';
|
|
41
|
+
if (model.state === 'missing-token') return '凭据缺失';
|
|
42
|
+
return '已配置';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function OfficeSettingsTab({ rpcCall, initialStatus }) {
|
|
46
|
+
const [model, setModel] = React.useState(normalizeOfficeStatus(initialStatus));
|
|
47
|
+
const [phase, setPhase] = React.useState(initialStatus === undefined ? 'loading' : 'ready');
|
|
48
|
+
const [busy, setBusy] = React.useState('');
|
|
49
|
+
const [error, setError] = React.useState('');
|
|
50
|
+
const [notice, setNotice] = React.useState('');
|
|
51
|
+
const [form, setForm] = React.useState({
|
|
52
|
+
baseUrl: '', deviceId: 'local-harness', deviceToken: '',
|
|
53
|
+
maxConcurrency: '1', heartbeatSeconds: '30', workspaces: '', instructionPresets: '',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const invoke = React.useCallback(async (endpoint, payload = {}) => {
|
|
57
|
+
if (typeof rpcCall !== 'function') throw new Error('AI Office 设置页缺少 RPC 连接');
|
|
58
|
+
return unwrapOfficeRpc(await rpcCall(endpoint, payload));
|
|
59
|
+
}, [rpcCall]);
|
|
60
|
+
|
|
61
|
+
const adopt = React.useCallback((value) => {
|
|
62
|
+
const next = normalizeOfficeStatus(value?.snapshot ?? value);
|
|
63
|
+
setModel(next);
|
|
64
|
+
if (next.config) setForm((current) => ({
|
|
65
|
+
...current,
|
|
66
|
+
baseUrl: next.config.baseUrl,
|
|
67
|
+
deviceId: next.config.deviceId,
|
|
68
|
+
maxConcurrency: String(next.config.maxConcurrency),
|
|
69
|
+
heartbeatSeconds: String(next.config.heartbeatSeconds),
|
|
70
|
+
workspaces: mapText(next.config.workspaces),
|
|
71
|
+
instructionPresets: mapText(next.config.instructionPresets),
|
|
72
|
+
deviceToken: '',
|
|
73
|
+
}));
|
|
74
|
+
return next;
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const load = React.useCallback(async () => {
|
|
78
|
+
try { adopt(await invoke(OFFICE_RPC_ENDPOINTS.status)); setPhase('ready'); setError(''); }
|
|
79
|
+
catch (caught) { setPhase('error'); setError(caught.message); }
|
|
80
|
+
}, [adopt, invoke]);
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => { void load(); }, [load]);
|
|
83
|
+
|
|
84
|
+
const run = async (name, operation) => {
|
|
85
|
+
setBusy(name); setError(''); setNotice('');
|
|
86
|
+
try { const value = await operation(); adopt(value); setNotice(name === 'test' ? '连接测试通过。' : '配置已保存。'); }
|
|
87
|
+
catch (caught) { setError(caught.message); }
|
|
88
|
+
finally { setBusy(''); }
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const hooks = React.useMemo(() => {
|
|
92
|
+
try { return officeHookUrls(form.baseUrl); } catch { return {}; }
|
|
93
|
+
}, [form.baseUrl]);
|
|
94
|
+
const health = model.health ?? {};
|
|
95
|
+
|
|
96
|
+
if (phase === 'loading') return h('div', { className: 'ddt-card ddt-loading', 'aria-busy': 'true' }, '正在读取 AI Office Connector…');
|
|
97
|
+
|
|
98
|
+
return h('section', { className: 'dof-page', 'aria-label': 'AI Office 设置' },
|
|
99
|
+
h('div', { className: 'dof-hero' },
|
|
100
|
+
h('div', { className: 'dof-heroCopy' },
|
|
101
|
+
h('h3', null, 'AI Office Connector'),
|
|
102
|
+
h('p', null, '本机主动连接公网 Office;Harness 不开放端口。协议 Hook 固定为 ', OFFICE_PROTOCOL_VERSION, '。')),
|
|
103
|
+
h('span', { className: 'dof-status', 'data-connected': String(model.connected) },
|
|
104
|
+
h('span', { className: 'dof-dot' }), stateLabel(model))),
|
|
105
|
+
model.configured ? h('div', { className: 'dof-metrics' },
|
|
106
|
+
h('div', { className: 'dof-metric' }, h('span', null, '最近心跳'), h('strong', null, health.lastHeartbeatAt ?? '尚无')),
|
|
107
|
+
h('div', { className: 'dof-metric' }, h('span', null, '最近事件'), h('strong', null, health.lastEventType ?? '尚无')),
|
|
108
|
+
h('div', { className: 'dof-metric' }, h('span', null, '重连次数'), h('strong', null, String(health.reconnects ?? 0))),
|
|
109
|
+
h('div', { className: 'dof-metric' }, h('span', null, 'Job Offer'), h('strong', null, String(health.jobsOffered ?? 0))),
|
|
110
|
+
h('div', { className: 'dof-metric' }, h('span', null, '运行 Job'), h('strong', null, String(health.jobs?.running ?? 0))),
|
|
111
|
+
h('div', { className: 'dof-metric' }, h('span', null, '完成 Job'), h('strong', null, String(health.jobs?.completed ?? 0)))) : null,
|
|
112
|
+
h('div', { className: 'dof-card' },
|
|
113
|
+
h('div', { className: 'dof-cardTitle' }, h('h4', null, '设备连接'), h('span', null, 'Token 只写入本机凭据存储')),
|
|
114
|
+
h('div', { className: 'dof-grid' },
|
|
115
|
+
h('label', { className: 'dof-field', 'data-wide': 'true' }, 'Office Base URL',
|
|
116
|
+
h('input', { value: form.baseUrl, placeholder: 'https://office.example.com', onChange: (event) => setForm({ ...form, baseUrl: event.target.value }) })),
|
|
117
|
+
h('label', { className: 'dof-field' }, 'Device ID',
|
|
118
|
+
h('input', { value: form.deviceId, placeholder: 'local-harness', onChange: (event) => setForm({ ...form, deviceId: event.target.value }) })),
|
|
119
|
+
h('label', { className: 'dof-field' }, 'Device Token',
|
|
120
|
+
h('input', { type: 'password', value: form.deviceToken, placeholder: model.tokenConfigured ? '已安全保存;留空保持不变' : '粘贴 Office 一次性凭据', autoComplete: 'new-password', onChange: (event) => setForm({ ...form, deviceToken: event.target.value }) })),
|
|
121
|
+
h('label', { className: 'dof-field' }, '最大并发',
|
|
122
|
+
h('input', { type: 'number', min: 1, max: 4, value: form.maxConcurrency, onChange: (event) => setForm({ ...form, maxConcurrency: event.target.value }) })),
|
|
123
|
+
h('label', { className: 'dof-field' }, 'Heartbeat 秒数',
|
|
124
|
+
h('input', { type: 'number', min: 10, max: 300, value: form.heartbeatSeconds, onChange: (event) => setForm({ ...form, heartbeatSeconds: event.target.value }) })),
|
|
125
|
+
h('label', { className: 'dof-field', 'data-wide': 'true' }, 'Workspace 映射',
|
|
126
|
+
h('textarea', { value: form.workspaces, placeholder: 'office-project=/Users/you/projects/ai-office', onChange: (event) => setForm({ ...form, workspaces: event.target.value }) }),
|
|
127
|
+
h('small', null, '每行 alias=/本机/绝对路径;Office 只能看到 alias。')),
|
|
128
|
+
h('label', { className: 'dof-field', 'data-wide': 'true' }, 'Instruction Preset 映射',
|
|
129
|
+
h('textarea', { value: form.instructionPresets, placeholder: 'action-items=转换为负责人、截止和验收明确的工单', onChange: (event) => setForm({ ...form, instructionPresets: event.target.value }) }),
|
|
130
|
+
h('small', null, '每行 alias=指令;新增 preset 不需要改 Office 代码。'))),
|
|
131
|
+
error ? h('p', { className: 'dof-error', role: 'alert' }, error) : null,
|
|
132
|
+
notice ? h('p', { className: 'dof-notice', role: 'status' }, notice) : null,
|
|
133
|
+
health.error?.message ? h('p', { className: 'dof-error' }, health.error.message) : null,
|
|
134
|
+
h('div', { className: 'dof-actions' },
|
|
135
|
+
h(Button, { kind: 'primary', disabled: Boolean(busy), onClick: () => void run('save', () => invoke(OFFICE_RPC_ENDPOINTS.configure, {
|
|
136
|
+
baseUrl: form.baseUrl,
|
|
137
|
+
deviceId: form.deviceId,
|
|
138
|
+
...(form.deviceToken ? { deviceToken: form.deviceToken } : {}),
|
|
139
|
+
maxConcurrency: Number(form.maxConcurrency),
|
|
140
|
+
heartbeatSeconds: Number(form.heartbeatSeconds),
|
|
141
|
+
workspaces: parseMap(form.workspaces, 'Workspace 映射'),
|
|
142
|
+
instructionPresets: parseMap(form.instructionPresets, 'Instruction Preset 映射'),
|
|
143
|
+
})) }, busy === 'save' ? '保存中…' : '保存并连接'),
|
|
144
|
+
h(Button, { disabled: !model.configured || Boolean(busy), onClick: () => void run('test', () => invoke(OFFICE_RPC_ENDPOINTS.test)) }, busy === 'test' ? '测试中…' : '测试连接'),
|
|
145
|
+
h(Button, { disabled: !model.configured || Boolean(busy), onClick: () => void run('reconnect', () => invoke(OFFICE_RPC_ENDPOINTS.reconnect)) }, '重新连接'),
|
|
146
|
+
h(Button, { kind: 'danger', disabled: !model.configured || Boolean(busy), onClick: () => void run('remove', () => invoke(OFFICE_RPC_ENDPOINTS.remove, { confirm: true })) }, '移除连接'))),
|
|
147
|
+
h('div', { className: 'dof-card' },
|
|
148
|
+
h('div', { className: 'dof-cardTitle' }, h('h4', null, '协议 Hook 预览'), h('span', null, '由 Base URL 自动派生,不单独填写')),
|
|
149
|
+
h('div', { className: 'dof-hooks' },
|
|
150
|
+
[['SSE', hooks.stream], ['Heartbeat', hooks.heartbeat], ['Job', hooks.job], ['Result', hooks.result]].map(([label, url]) => h('div', { className: 'dof-hook', key: label }, h('strong', null, label), h('code', null, url ?? 'Base URL 无效'))))),
|
|
151
|
+
h('p', { className: 'dof-notice' }, 'Office Hook 尚未部署时,配置会安全保存并自动重试;出现 HTTP 404 代表协议端点待上线,不代表 Harness 故障。'));
|
|
152
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const OFFICE_STYLE_ID = 'xmanrui-dsh-im-office-settings';
|
|
2
|
+
|
|
3
|
+
const CSS = `
|
|
4
|
+
.dof-page { --dof-accent: var(--dsw-alias-brand-primary, #3964fe); }
|
|
5
|
+
.dof-hero { position: relative; overflow: hidden; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 16px; align-items: center; margin-bottom: 12px; padding: 18px; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 16px; background: linear-gradient(135deg, color-mix(in srgb, var(--dof-accent) 9%, var(--dsw-alias-bg-layer-1, #fff)), var(--dsw-alias-bg-layer-1, #fff) 62%); }
|
|
6
|
+
.dof-hero::after { content: ""; position: absolute; width: 150px; height: 150px; right: -75px; top: -90px; border: 24px solid color-mix(in srgb, var(--dof-accent) 12%, transparent); border-radius: 50%; pointer-events: none; }
|
|
7
|
+
.dof-heroCopy { min-width: 0; }
|
|
8
|
+
.dof-heroCopy h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 17px; line-height: 1.35; }
|
|
9
|
+
.dof-heroCopy p { margin: 6px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 1.6; }
|
|
10
|
+
.dof-status { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 7px; padding: 7px 10px; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 999px; background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; white-space: nowrap; }
|
|
11
|
+
.dof-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--dsw-alias-state-warn-primary, #d97706); }
|
|
12
|
+
.dof-status[data-connected="true"] .dof-dot { background: var(--dsw-alias-state-success-primary, #20a162); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary, #20a162) 14%, transparent); }
|
|
13
|
+
.dof-card { margin-top: 10px; padding: 16px; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); }
|
|
14
|
+
.dof-cardTitle { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; margin-bottom: 12px; }
|
|
15
|
+
.dof-cardTitle h4 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; }
|
|
16
|
+
.dof-cardTitle span { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; }
|
|
17
|
+
.dof-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
|
18
|
+
.dof-field { min-width: 0; display: flex; flex-direction: column; gap: 6px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; }
|
|
19
|
+
.dof-field[data-wide="true"] { grid-column: 1 / -1; }
|
|
20
|
+
.dof-field input, .dof-field textarea { box-sizing: border-box; width: 100%; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #1f2329); font: inherit; font-size: 13px; line-height: 1.5; outline: none; }
|
|
21
|
+
.dof-field input { height: 38px; padding: 0 11px; }
|
|
22
|
+
.dof-field textarea { min-height: 86px; resize: vertical; padding: 9px 11px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
23
|
+
.dof-field input:focus, .dof-field textarea:focus { border-color: var(--dof-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dof-accent) 12%, transparent); }
|
|
24
|
+
.dof-field small { color: var(--dsw-alias-label-tertiary, #8f959e); line-height: 1.45; }
|
|
25
|
+
.dof-hooks { display: grid; gap: 7px; }
|
|
26
|
+
.dof-hook { min-width: 0; display: grid; grid-template-columns: 82px minmax(0, 1fr); gap: 10px; align-items: center; padding: 8px 10px; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
27
|
+
.dof-hook strong { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; }
|
|
28
|
+
.dof-hook code { overflow: hidden; color: var(--dsw-alias-label-primary, #1f2329); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
|
29
|
+
.dof-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
|
|
30
|
+
.dof-actions .ddt-button[data-kind="primary"] { color: #fff; border-color: var(--dof-accent); background: var(--dof-accent); }
|
|
31
|
+
.dof-error, .dof-notice { margin: 10px 0 0; padding: 9px 11px; border-radius: 9px; font-size: 12px; line-height: 1.5; }
|
|
32
|
+
.dof-error { color: var(--dsw-alias-state-error-primary, #d54941); background: var(--dsw-alias-state-error-secondary, #fff0ef); }
|
|
33
|
+
.dof-notice { color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
34
|
+
.dof-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }
|
|
35
|
+
.dof-metric { min-width: 0; padding: 9px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
36
|
+
.dof-metric span { display: block; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 10px; }
|
|
37
|
+
.dof-metric strong { display: block; overflow: hidden; margin-top: 4px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
|
38
|
+
@container (max-width: 680px) { .dof-grid { grid-template-columns: minmax(0, 1fr); } .dof-field[data-wide="true"] { grid-column: auto; } .dof-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
39
|
+
@media (prefers-reduced-motion: reduce) { .dof-page * { transition: none !important; } }
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export function installOfficeStyles() {
|
|
43
|
+
if (typeof document === 'undefined') return () => {};
|
|
44
|
+
if (document.querySelector(`style[data-plugin-css="${OFFICE_STYLE_ID}"]`)) return () => {};
|
|
45
|
+
const style = document.createElement('style');
|
|
46
|
+
style.dataset.pluginCss = OFFICE_STYLE_ID;
|
|
47
|
+
style.textContent = CSS;
|
|
48
|
+
document.head.append(style);
|
|
49
|
+
return () => style.remove();
|
|
50
|
+
}
|
|
@@ -27,7 +27,9 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
|
|
|
27
27
|
setWorkspace: 'bot.workspace.set',
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
export function createTokenChannelApi(channel, connectionSummary
|
|
30
|
+
export function createTokenChannelApi(channel, connectionSummary, {
|
|
31
|
+
normalizeBotExtension = () => ({}),
|
|
32
|
+
} = {}) {
|
|
31
33
|
const unwrapRpcResult = (result) => {
|
|
32
34
|
if (!isRecord(result) || typeof result.ok !== 'boolean') {
|
|
33
35
|
throw new Error(`${channel} 服务返回了无法识别的响应`);
|
|
@@ -44,6 +46,7 @@ export function createTokenChannelApi(channel, connectionSummary) {
|
|
|
44
46
|
if (!isRecord(value) || !id(value.botId)) return undefined;
|
|
45
47
|
const connected = value.connected === true;
|
|
46
48
|
const state = ACCOUNT_STATES.has(value.state) ? value.state : 'offline';
|
|
49
|
+
const extension = normalizeBotExtension(value);
|
|
47
50
|
return {
|
|
48
51
|
botId: id(value.botId),
|
|
49
52
|
connected,
|
|
@@ -65,6 +68,7 @@ export function createTokenChannelApi(channel, connectionSummary) {
|
|
|
65
68
|
code: text(value.error.code, `${channel.toUpperCase()}_ACCOUNT_ERROR`, 80),
|
|
66
69
|
message: text(value.error.message, `${channel}连接尚未就绪`),
|
|
67
70
|
} : null,
|
|
71
|
+
...(isRecord(extension) ? extension : {}),
|
|
68
72
|
};
|
|
69
73
|
};
|
|
70
74
|
|
|
@@ -59,9 +59,11 @@ export function createTokenChannelSettings(definition) {
|
|
|
59
59
|
credentialCloseLabel = '收起凭据',
|
|
60
60
|
credentialNoun = 'Bot Token',
|
|
61
61
|
emptyActionLabel = '填写 Bot Token',
|
|
62
|
+
AccountSettings = null,
|
|
63
|
+
accountSettingsEndpoint = null,
|
|
62
64
|
} = definition;
|
|
63
65
|
|
|
64
|
-
function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
|
66
|
+
function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onAccountSettingsSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
|
65
67
|
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
|
66
68
|
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
|
67
69
|
const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
|
|
@@ -88,6 +90,11 @@ export function createTokenChannelSettings(definition) {
|
|
|
88
90
|
disabled: Boolean(busy),
|
|
89
91
|
onSave: onWorkspaceSave,
|
|
90
92
|
}),
|
|
93
|
+
AccountSettings ? h(AccountSettings, {
|
|
94
|
+
account,
|
|
95
|
+
busy: Boolean(busy),
|
|
96
|
+
onSave: onAccountSettingsSave,
|
|
97
|
+
}) : null,
|
|
91
98
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
92
99
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
93
100
|
testNotice ? h('div', { className: 'ddt-summary dim-cardSummary', role: 'status' }, testNotice) : null,
|
|
@@ -260,6 +267,14 @@ export function createTokenChannelSettings(definition) {
|
|
|
260
267
|
endpoints.setWorkspace,
|
|
261
268
|
{ botId: account.botId, workspace },
|
|
262
269
|
),
|
|
270
|
+
onAccountSettingsSave: AccountSettings && accountSettingsEndpoint
|
|
271
|
+
? (payload) => botAction(
|
|
272
|
+
account,
|
|
273
|
+
'settings',
|
|
274
|
+
accountSettingsEndpoint,
|
|
275
|
+
{ botId: account.botId, ...payload },
|
|
276
|
+
)
|
|
277
|
+
: undefined,
|
|
263
278
|
onRequestRemove: () => setRemoveTarget(account.botId),
|
|
264
279
|
onCancelRemove: () => setRemoveTarget(null),
|
|
265
280
|
onConfirmRemove: async () => {
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { TOKEN_BOT_ENDPOINTS, createTokenChannelApi } from '../shared/token-api.js';
|
|
2
2
|
|
|
3
3
|
export const TELEGRAM_RPC_CHANNEL = '/telegram';
|
|
4
|
-
export const TELEGRAM_ENDPOINTS =
|
|
4
|
+
export const TELEGRAM_ENDPOINTS = Object.freeze({
|
|
5
|
+
...TOKEN_BOT_ENDPOINTS,
|
|
6
|
+
setAccessPolicy: 'bot.access-policy.set',
|
|
7
|
+
});
|
|
5
8
|
|
|
6
|
-
const api = createTokenChannelApi('Telegram', ' Bot API 长轮询'
|
|
9
|
+
const api = createTokenChannelApi('Telegram', ' Bot API 长轮询', {
|
|
10
|
+
normalizeBotExtension: (value) => {
|
|
11
|
+
const source = value?.accessPolicy;
|
|
12
|
+
const accessMode = source?.accessMode === 'private-allowlist'
|
|
13
|
+
? 'private-allowlist' : 'compatible';
|
|
14
|
+
const allowedUsers = Array.isArray(source?.allowedUsers)
|
|
15
|
+
? [...new Set(source.allowedUsers.filter((entry) => (
|
|
16
|
+
typeof entry === 'string' && /^[1-9]\d{0,15}$/.test(entry)
|
|
17
|
+
)))]
|
|
18
|
+
: [];
|
|
19
|
+
return { accessPolicy: { accessMode, allowedUsers } };
|
|
20
|
+
},
|
|
21
|
+
});
|
|
7
22
|
|
|
8
23
|
export const unwrapRpcResult = api.unwrapRpcResult;
|
|
9
24
|
export const normalizeSnapshot = api.normalizeSnapshot;
|
|
@@ -1,11 +1,121 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
1
3
|
import { TelegramLogoGlyph } from '../../channel-logos.js';
|
|
2
4
|
import { createTokenChannelSettings } from '../shared/token-channel.js';
|
|
5
|
+
import { h } from '../../i18n.js';
|
|
3
6
|
import {
|
|
4
7
|
TELEGRAM_ENDPOINTS,
|
|
5
8
|
telegramClientApi,
|
|
6
9
|
} from './api.js';
|
|
7
10
|
import { installTelegramStyles } from './styles.js';
|
|
8
11
|
|
|
12
|
+
function policyFor(account) {
|
|
13
|
+
return {
|
|
14
|
+
accessMode: account?.accessPolicy?.accessMode === 'private-allowlist'
|
|
15
|
+
? 'private-allowlist' : 'compatible',
|
|
16
|
+
allowedUsers: Array.isArray(account?.accessPolicy?.allowedUsers)
|
|
17
|
+
? account.accessPolicy.allowedUsers : [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function allowedUsersFromText(value) {
|
|
22
|
+
const entries = value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
|
|
23
|
+
if (entries.some((entry) => !/^[1-9]\d{0,15}$/.test(entry))) {
|
|
24
|
+
throw new TypeError('User ID 必须是 1–16 位正整数,每行一个。');
|
|
25
|
+
}
|
|
26
|
+
return [...new Set(entries)];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function TelegramAccessSettings({ account, busy = false, onSave }) {
|
|
30
|
+
const policy = policyFor(account);
|
|
31
|
+
const sourceUsers = policy.allowedUsers.join('\n');
|
|
32
|
+
const accessHelpId = React.useId();
|
|
33
|
+
const [accessMode, setAccessMode] = React.useState(policy.accessMode);
|
|
34
|
+
const [allowedUsers, setAllowedUsers] = React.useState(sourceUsers);
|
|
35
|
+
const [error, setError] = React.useState(null);
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
setAccessMode(policy.accessMode);
|
|
39
|
+
setAllowedUsers(sourceUsers);
|
|
40
|
+
setError(null);
|
|
41
|
+
}, [policy.accessMode, sourceUsers]);
|
|
42
|
+
|
|
43
|
+
const save = async (event) => {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
setError(null);
|
|
46
|
+
try {
|
|
47
|
+
const normalized = allowedUsersFromText(allowedUsers);
|
|
48
|
+
if (typeof onSave !== 'function') throw new Error('Telegram 访问设置暂不可用。');
|
|
49
|
+
await onSave({ accessMode, allowedUsers: normalized });
|
|
50
|
+
} catch (caught) {
|
|
51
|
+
setError(caught?.message ?? 'Telegram 访问设置保存失败。');
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const privateAllowlist = accessMode === 'private-allowlist';
|
|
56
|
+
const savedPrivateAllowlist = policy.accessMode === 'private-allowlist';
|
|
57
|
+
const emptyAllowlist = privateAllowlist && allowedUsers.trim() === '';
|
|
58
|
+
return h('form', { className: 'dtg-access', onSubmit: save },
|
|
59
|
+
h('div', { className: 'dtg-accessHeading' },
|
|
60
|
+
h('strong', null, '访问设置'),
|
|
61
|
+
h('span', { className: 'dtg-accessStatus' },
|
|
62
|
+
h('span', { className: 'dtg-accessBadge', 'data-mode': policy.accessMode },
|
|
63
|
+
savedPrivateAllowlist ? '已生效:安全模式' : '已生效:兼容模式'),
|
|
64
|
+
h('span', { className: 'dtg-accessHelp' },
|
|
65
|
+
h('button', {
|
|
66
|
+
type: 'button',
|
|
67
|
+
className: 'dtg-accessHelpButton',
|
|
68
|
+
'aria-label': '查看 Telegram 访问模式说明',
|
|
69
|
+
'aria-describedby': accessHelpId,
|
|
70
|
+
}, h('span', { 'aria-hidden': 'true' }, '?')),
|
|
71
|
+
h('span', {
|
|
72
|
+
id: accessHelpId,
|
|
73
|
+
className: 'dtg-accessTooltip',
|
|
74
|
+
role: 'tooltip',
|
|
75
|
+
},
|
|
76
|
+
h('span', { className: 'dtg-accessTooltipItem' },
|
|
77
|
+
h('strong', null, '兼容模式'),
|
|
78
|
+
h('span', null, '保持原有行为:私聊直接响应,群聊在被提及或回复时响应。')),
|
|
79
|
+
h('span', { className: 'dtg-accessTooltipItem' },
|
|
80
|
+
h('strong', null, '安全模式'),
|
|
81
|
+
h('span', null, '群聊全部忽略,私聊仅允许白名单用户。')))))),
|
|
82
|
+
h('label', { className: 'dtg-accessField' },
|
|
83
|
+
h('span', null, '模式'),
|
|
84
|
+
h('select', {
|
|
85
|
+
value: accessMode,
|
|
86
|
+
disabled: busy,
|
|
87
|
+
'aria-label': 'Telegram 访问模式',
|
|
88
|
+
onChange: (event) => { setAccessMode(event.target.value); setError(null); },
|
|
89
|
+
},
|
|
90
|
+
h('option', { value: 'compatible' }, '兼容模式(默认)'),
|
|
91
|
+
h('option', { value: 'private-allowlist' }, '安全模式(私聊白名单)'))),
|
|
92
|
+
h('label', { className: 'dtg-accessField' },
|
|
93
|
+
h('span', null, '允许私聊的 Telegram User ID'),
|
|
94
|
+
h('textarea', {
|
|
95
|
+
value: allowedUsers,
|
|
96
|
+
disabled: busy || !privateAllowlist,
|
|
97
|
+
rows: 3,
|
|
98
|
+
placeholder: '每行一个数字 User ID',
|
|
99
|
+
'aria-label': '允许私聊的 Telegram User ID',
|
|
100
|
+
onChange: (event) => { setAllowedUsers(event.target.value); setError(null); },
|
|
101
|
+
}),
|
|
102
|
+
h('small', null, privateAllowlist
|
|
103
|
+
? '白名单仅属于当前机器人。'
|
|
104
|
+
: '兼容模式下暂不使用白名单,切换模式时会保留。')),
|
|
105
|
+
emptyAllowlist
|
|
106
|
+
? h('p', { className: 'dtg-accessWarning', role: 'status' },
|
|
107
|
+
'白名单为空;保存后该机器人会拒绝所有入站消息。')
|
|
108
|
+
: null,
|
|
109
|
+
error ? h('p', { className: 'dtg-accessError', role: 'alert' }, error) : null,
|
|
110
|
+
h('div', { className: 'dtg-accessActions' },
|
|
111
|
+
h('button', {
|
|
112
|
+
type: 'submit',
|
|
113
|
+
className: 'ddt-button',
|
|
114
|
+
'data-kind': 'secondary',
|
|
115
|
+
disabled: busy,
|
|
116
|
+
}, busy ? '正在保存…' : '保存访问设置')));
|
|
117
|
+
}
|
|
118
|
+
|
|
9
119
|
const channel = createTokenChannelSettings({
|
|
10
120
|
channel: 'Telegram',
|
|
11
121
|
endpoints: TELEGRAM_ENDPOINTS,
|
|
@@ -19,6 +129,8 @@ const channel = createTokenChannelSettings({
|
|
|
19
129
|
emptyTitle: '接入 Telegram 机器人',
|
|
20
130
|
emptyDescription: '先通过 @BotFather 获取 Bot Token,再在这里完成接入。',
|
|
21
131
|
platformLabel: 'Telegram',
|
|
132
|
+
AccountSettings: TelegramAccessSettings,
|
|
133
|
+
accountSettingsEndpoint: TELEGRAM_ENDPOINTS.setAccessPolicy,
|
|
22
134
|
});
|
|
23
135
|
|
|
24
136
|
export const TelegramSettingsTab = channel.SettingsTab;
|
|
@@ -4,6 +4,32 @@ const CSS = String.raw`
|
|
|
4
4
|
.dtg-page { --ddt-accent: #229ed9; --ddt-accent-deep: #1687bd; --ddt-accent-wash: #eaf7fd; }
|
|
5
5
|
.dtg-avatar { color: #fff; background: #229ed9; }
|
|
6
6
|
.dtg-avatar svg { display: block; }
|
|
7
|
+
.dtg-access { display: grid; gap: 10px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 10px; background: var(--dsw-alias-bg-layer-2, #f7f8fa); }
|
|
8
|
+
.dtg-accessHeading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
9
|
+
.dtg-accessHeading > strong { font-size: 13px; }
|
|
10
|
+
.dtg-accessStatus { min-width: 0; display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; }
|
|
11
|
+
.dtg-accessBadge { flex: none; padding: 3px 8px; border-radius: 999px; color: #1687bd; background: #eaf7fd; font-size: 11px; font-weight: 700; }
|
|
12
|
+
.dtg-accessBadge[data-mode="private-allowlist"] { color: #a15c00; background: #fff3d6; }
|
|
13
|
+
.dtg-accessHelp { position: relative; display: inline-flex; flex: none; }
|
|
14
|
+
.dtg-accessHelpButton { width: 20px; height: 20px; display: grid; place-items: center; padding: 0; border: 1px solid color-mix(in srgb, #229ed9 28%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 50%; color: #1687bd; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; line-height: 1; font-weight: 750; cursor: help; transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; }
|
|
15
|
+
.dtg-accessHelpButton:hover { border-color: #229ed9; color: #1178a8; background: #eaf7fd; }
|
|
16
|
+
.dtg-accessHelpButton:focus-visible { outline: none; border-color: #229ed9; box-shadow: 0 0 0 3px color-mix(in srgb, #229ed9 18%, transparent); }
|
|
17
|
+
.dtg-accessTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: 260px; max-width: min(280px, calc(100vw - 48px)); display: grid; gap: 8px; padding: 10px 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
|
|
18
|
+
.dtg-accessTooltipItem { display: grid; gap: 2px; }
|
|
19
|
+
.dtg-accessTooltipItem + .dtg-accessTooltipItem { padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l2, #eef0f3); }
|
|
20
|
+
.dtg-accessTooltipItem strong { color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 17px; font-weight: 700; }
|
|
21
|
+
.dtg-accessTooltipItem > span { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; font-weight: 400; }
|
|
22
|
+
.dtg-accessHelp:hover .dtg-accessTooltip, .dtg-accessHelp:focus-within .dtg-accessTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
23
|
+
.dtg-accessField { display: grid; gap: 5px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; font-weight: 600; }
|
|
24
|
+
.dtg-accessField select, .dtg-accessField textarea { width: 100%; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1, #c9cdd4); border-radius: 7px; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-weight: 400; }
|
|
25
|
+
.dtg-accessField select { height: 34px; padding: 0 9px; }
|
|
26
|
+
.dtg-accessField textarea { min-height: 68px; padding: 8px 9px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
27
|
+
.dtg-accessField textarea:disabled { color: var(--dsw-alias-label-tertiary, #8f959e); background: var(--dsw-alias-bg-module-platform, #f2f3f5); cursor: not-allowed; resize: none; opacity: 1; }
|
|
28
|
+
.dtg-accessField small { color: var(--dsw-alias-label-secondary, #646a73); font-weight: 400; }
|
|
29
|
+
.dtg-accessWarning, .dtg-accessError { margin: 0; font-size: 12px; line-height: 1.5; }
|
|
30
|
+
.dtg-accessWarning { color: #a15c00; }
|
|
31
|
+
.dtg-accessError { color: var(--dsw-alias-state-error-primary, #d83931); }
|
|
32
|
+
.dtg-accessActions { display: flex; justify-content: flex-end; }
|
|
7
33
|
`;
|
|
8
34
|
|
|
9
35
|
export function installTelegramStyles() {
|