feat: secure Telegram access and connect AI Office

This commit is contained in:
pompy 2026-08-20 11:56:53 +08:00
parent e3ae772106
commit b0606159e0
30 changed files with 2045 additions and 302 deletions

View file

@ -0,0 +1,150 @@
import * as React from 'react';
import { h } from '../../i18n.js';
import {
OFFICE_PROTOCOL_VERSION,
OFFICE_RPC_ENDPOINTS,
normalizeOfficeStatus,
officeHookUrls,
unwrapOfficeRpc,
} from './api.js';
function Button({ children, kind = 'secondary', ...props }) {
return h('button', { ...props, type: 'button', className: 'ddt-button', 'data-kind': kind }, children);
}
function mapText(value) {
return Object.entries(value ?? {}).map(([key, item]) => `${key}=${item}`).join('\n');
}
function parseMap(value, label) {
const output = {};
for (const raw of value.split(/\r?\n/)) {
const line = raw.trim();
if (!line) continue;
const index = line.indexOf('=');
if (index < 1 || !line.slice(index + 1).trim()) {
throw new Error(label === 'Workspace 映射'
? 'Workspace 映射每行必须使用 alias=value'
: 'Instruction Preset 映射每行必须使用 alias=value');
}
output[line.slice(0, index).trim()] = line.slice(index + 1).trim();
}
return output;
}
function stateLabel(model) {
if (model.connected) return '已连接 Office';
if (!model.configured) return '尚未配置';
if (model.state === 'connecting') return '正在连接';
if (model.state === 'reconnecting') return '等待重连';
if (model.state === 'missing-token') return '凭据缺失';
return '已配置';
}
export function OfficeSettingsTab({ rpcCall, initialStatus }) {
const [model, setModel] = React.useState(normalizeOfficeStatus(initialStatus));
const [phase, setPhase] = React.useState(initialStatus === undefined ? 'loading' : 'ready');
const [busy, setBusy] = React.useState('');
const [error, setError] = React.useState('');
const [notice, setNotice] = React.useState('');
const [form, setForm] = React.useState({
baseUrl: 'https://fission.gridmind.ai', deviceId: 'local-harness', deviceToken: '',
maxConcurrency: '1', heartbeatSeconds: '30', workspaces: '', instructionPresets: '',
});
const invoke = React.useCallback(async (endpoint, payload = {}) => {
if (typeof rpcCall !== 'function') throw new Error('AI Office 设置页缺少 RPC 连接');
return unwrapOfficeRpc(await rpcCall(endpoint, payload));
}, [rpcCall]);
const adopt = React.useCallback((value) => {
const next = normalizeOfficeStatus(value?.snapshot ?? value);
setModel(next);
if (next.config) setForm((current) => ({
...current,
baseUrl: next.config.baseUrl,
deviceId: next.config.deviceId,
maxConcurrency: String(next.config.maxConcurrency),
heartbeatSeconds: String(next.config.heartbeatSeconds),
workspaces: mapText(next.config.workspaces),
instructionPresets: mapText(next.config.instructionPresets),
deviceToken: '',
}));
return next;
}, []);
const load = React.useCallback(async () => {
try { adopt(await invoke(OFFICE_RPC_ENDPOINTS.status)); setPhase('ready'); setError(''); }
catch (caught) { setPhase('error'); setError(caught.message); }
}, [adopt, invoke]);
React.useEffect(() => { void load(); }, [load]);
const run = async (name, operation) => {
setBusy(name); setError(''); setNotice('');
try { const value = await operation(); adopt(value); setNotice(name === 'test' ? '连接测试通过。' : '配置已保存。'); }
catch (caught) { setError(caught.message); }
finally { setBusy(''); }
};
const hooks = React.useMemo(() => {
try { return officeHookUrls(form.baseUrl); } catch { return {}; }
}, [form.baseUrl]);
const health = model.health ?? {};
if (phase === 'loading') return h('div', { className: 'ddt-card ddt-loading', 'aria-busy': 'true' }, '正在读取 AI Office Connector…');
return h('section', { className: 'dof-page', 'aria-label': 'AI Office 设置' },
h('div', { className: 'dof-hero' },
h('div', { className: 'dof-heroCopy' },
h('h3', null, 'AI Office Connector'),
h('p', null, '本机主动连接公网 Office;Harness 不开放端口。协议 Hook 固定为 ', OFFICE_PROTOCOL_VERSION, '。')),
h('span', { className: 'dof-status', 'data-connected': String(model.connected) },
h('span', { className: 'dof-dot' }), stateLabel(model))),
model.configured ? h('div', { className: 'dof-metrics' },
h('div', { className: 'dof-metric' }, h('span', null, '最近心跳'), h('strong', null, health.lastHeartbeatAt ?? '尚无')),
h('div', { className: 'dof-metric' }, h('span', null, '最近事件'), h('strong', null, health.lastEventType ?? '尚无')),
h('div', { className: 'dof-metric' }, h('span', null, '重连次数'), h('strong', null, String(health.reconnects ?? 0))),
h('div', { className: 'dof-metric' }, h('span', null, 'Job Offer'), h('strong', null, String(health.jobsOffered ?? 0)))) : null,
h('div', { className: 'dof-card' },
h('div', { className: 'dof-cardTitle' }, h('h4', null, '设备连接'), h('span', null, 'Token 只写入本机凭据存储')),
h('div', { className: 'dof-grid' },
h('label', { className: 'dof-field', 'data-wide': 'true' }, 'Office Base URL',
h('input', { value: form.baseUrl, placeholder: 'https://fission.gridmind.ai', onChange: (event) => setForm({ ...form, baseUrl: event.target.value }) })),
h('label', { className: 'dof-field' }, 'Device ID',
h('input', { value: form.deviceId, placeholder: 'local-harness', onChange: (event) => setForm({ ...form, deviceId: event.target.value }) })),
h('label', { className: 'dof-field' }, 'Device Token',
h('input', { type: 'password', value: form.deviceToken, placeholder: model.tokenConfigured ? '已安全保存;留空保持不变' : '粘贴 Office 一次性凭据', autoComplete: 'new-password', onChange: (event) => setForm({ ...form, deviceToken: event.target.value }) })),
h('label', { className: 'dof-field' }, '最大并发',
h('input', { type: 'number', min: 1, max: 4, value: form.maxConcurrency, onChange: (event) => setForm({ ...form, maxConcurrency: event.target.value }) })),
h('label', { className: 'dof-field' }, 'Heartbeat 秒数',
h('input', { type: 'number', min: 10, max: 300, value: form.heartbeatSeconds, onChange: (event) => setForm({ ...form, heartbeatSeconds: event.target.value }) })),
h('label', { className: 'dof-field', 'data-wide': 'true' }, 'Workspace 映射',
h('textarea', { value: form.workspaces, placeholder: 'office-project=/Users/you/projects/ai-office', onChange: (event) => setForm({ ...form, workspaces: event.target.value }) }),
h('small', null, '每行 alias=/本机/绝对路径;Office 只能看到 alias。')),
h('label', { className: 'dof-field', 'data-wide': 'true' }, 'Instruction Preset 映射',
h('textarea', { value: form.instructionPresets, placeholder: 'action-items=转换为负责人、截止和验收明确的工单', onChange: (event) => setForm({ ...form, instructionPresets: event.target.value }) }),
h('small', null, '每行 alias=指令;新增 preset 不需要改 Office 代码。'))),
error ? h('p', { className: 'dof-error', role: 'alert' }, error) : null,
notice ? h('p', { className: 'dof-notice', role: 'status' }, notice) : null,
health.error?.message ? h('p', { className: 'dof-error' }, health.error.message) : null,
h('div', { className: 'dof-actions' },
h(Button, { kind: 'primary', disabled: Boolean(busy), onClick: () => void run('save', () => invoke(OFFICE_RPC_ENDPOINTS.configure, {
baseUrl: form.baseUrl,
deviceId: form.deviceId,
...(form.deviceToken ? { deviceToken: form.deviceToken } : {}),
maxConcurrency: Number(form.maxConcurrency),
heartbeatSeconds: Number(form.heartbeatSeconds),
workspaces: parseMap(form.workspaces, 'Workspace 映射'),
instructionPresets: parseMap(form.instructionPresets, 'Instruction Preset 映射'),
})) }, busy === 'save' ? '保存中…' : '保存并连接'),
h(Button, { disabled: !model.configured || Boolean(busy), onClick: () => void run('test', () => invoke(OFFICE_RPC_ENDPOINTS.test)) }, busy === 'test' ? '测试中…' : '测试连接'),
h(Button, { disabled: !model.configured || Boolean(busy), onClick: () => void run('reconnect', () => invoke(OFFICE_RPC_ENDPOINTS.reconnect)) }, '重新连接'),
h(Button, { kind: 'danger', disabled: !model.configured || Boolean(busy), onClick: () => void run('remove', () => invoke(OFFICE_RPC_ENDPOINTS.remove, { confirm: true })) }, '移除连接'))),
h('div', { className: 'dof-card' },
h('div', { className: 'dof-cardTitle' }, h('h4', null, '协议 Hook 预览'), h('span', null, '由 Base URL 自动派生,不单独填写')),
h('div', { className: 'dof-hooks' },
[['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 无效'))))),
h('p', { className: 'dof-notice' }, 'Office Hook 尚未部署时,配置会安全保存并自动重试;出现 HTTP 404 代表协议端点待上线,不代表 Harness 故障。'));
}