dsh-social-trace 0.1.13 → 0.1.20
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 +12 -5
- package/assets/NOTICE.md +2 -0
- package/assets/douyin.ico +0 -0
- package/lib/agent.js +4 -4
- package/lib/business.js +74 -1
- package/lib/chat-context.js +9 -6
- package/lib/chat-model.js +5 -4
- package/lib/chat-runtime.js +117 -0
- package/lib/chat.js +129 -82
- package/lib/client.js +159 -45
- package/lib/documents.js +13 -1
- package/lib/index.js +69 -39
- package/lib/plain-text.js +10 -0
- package/lib/platforms/douyin/client.js +44 -0
- package/lib/platforms/douyin/config.js +33 -0
- package/lib/platforms/douyin/data.js +49 -0
- package/lib/platforms/douyin/index.js +163 -0
- package/lib/platforms/douyin/library.js +154 -0
- package/lib/platforms/douyin/media.js +12 -0
- package/lib/platforms/douyin/module.js +18 -0
- package/lib/platforms/douyin/service.js +34 -0
- package/lib/platforms/douyin/storage.js +39 -0
- package/lib/platforms/douyin/transcribe.py +27 -0
- package/lib/platforms/registry.js +7 -0
- package/lib/platforms/weibo/index.js +1 -1
- package/lib/platforms/weibo/media.js +19 -0
- package/lib/platforms/weibo/module.js +5 -0
- package/lib/platforms/weibo/recovery.js +17 -0
- package/lib/platforms/xueqiu/module.js +5 -0
- package/lib/store.js +42 -11
- package/lib/text.js +10 -35
- package/lib/worker.js +71 -4
- package/package.json +9 -4
package/lib/client.js
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
.st-independent{border-top:1px solid var(--line);padding-top:12px}.st-independent h3{font-size:13px;margin:0 0 10px}.st-independent-list{max-height:180px;overflow:auto}.st-independent-list>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 12px;padding:10px 0;border-bottom:1px solid var(--line);font-size:12px}.st-independent-list small{grid-column:1;color:var(--muted);line-height:1.6}.st-independent-list button{grid-column:2;grid-row:1/3;align-self:center}
|
|
56
56
|
|
|
57
57
|
.st .st-library-tree{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;padding-top:14px}.st .st-library-tree .st-author-folder{margin:0;min-width:0}.st .st-library-tree .st-author-folder[open]{grid-column:1/-1}.st .st-library-tree .st-author-folder>summary{display:grid;grid-template-columns:24px minmax(0,1fr);gap:2px 6px;padding:10px;align-items:center}.st-library-tree .st-author-folder>summary>.st-chevron{display:none}.st-library-tree .st-author-folder>summary>.st-flow-avatar{grid-row:1/3}.st-library-tree .st-author-folder>summary>strong{font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.st-library-tree .st-author-folder>summary>small{font-size:10px;margin:0}.st-library-tree .st-author-folder>summary>b{grid-column:1/-1;font-size:10px;font-weight:400;margin:5px 0 0;text-align:left;white-space:normal}.st-library-pages{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:12px 0 0;font-size:11px;color:var(--muted)}.st .st-library-pages button{padding:2px 9px;font-size:16px}
|
|
58
|
-
.st .st-meta-tag{display:inline-flex;align-items:center;max-width:100%;font-size:10px;font-weight:400;line-height:1.6;white-space:nowrap;padding:2px 6px;border:1px solid var(--line);border-radius:5px;background:var(--inset);color:var(--muted)}.st .st-author-open .st-uid-tag{margin-top:5px}.st .st-platform-badge{color:var(--accent);border-color:#7b5cff25;background:#7b5cff08}.st-author-table th:first-child{width:26%}.st .st-sort-heading{border:0;background:transparent;padding:0;display:inline-flex;gap:6px;align-items:center;color:inherit;font-size:12px}.st-sort-heading>span{color:var(--muted);font-size:10px}.st-reader-facts{max-width:900px}
|
|
58
|
+
.st .st-meta-tag{display:inline-flex;align-items:center;max-width:100%;font-size:10px;font-weight:400;line-height:1.6;white-space:nowrap;padding:2px 6px;border:1px solid var(--line);border-radius:5px;background:var(--inset);color:var(--muted)}.st .st-author-open .st-uid-tag{max-width:360px;overflow:hidden;text-overflow:ellipsis;margin-top:5px}.st .st-platform-badge{color:var(--accent);border-color:#7b5cff25;background:#7b5cff08}.st-author-table th:first-child{width:26%}.st .st-sort-heading{border:0;background:transparent;padding:0;display:inline-flex;gap:6px;align-items:center;color:inherit;font-size:12px}.st-sort-heading>span{color:var(--muted);font-size:10px}.st-reader-facts{max-width:900px}
|
|
59
59
|
|
|
60
60
|
.st-reader-tabs{display:flex;align-items:center;gap:24px;border-bottom:1px solid var(--line);margin-bottom:20px}.st .st-reader-tabs button{border:0;border-bottom:2px solid transparent;background:transparent;border-radius:0;padding:12px 2px;color:var(--muted);font-size:13px}.st .st-reader-tabs button[aria-selected=true]{border-bottom-color:var(--accent);color:var(--accent)}.st-inline-settings{max-width:900px;padding:0 0 24px}.st-inline-settings form{display:grid;gap:16px}.st-inline-settings form label:not(.st-schedule-switch){display:grid;gap:6px}.st-inline-settings .st-task-control{grid-template-columns:72px auto;justify-content:start}.st-inline-settings .st-history-panel{max-width:700px}.st .st-author-progress{border-top:0;padding-top:0}.st-inline-settings .st-history-panel>.st-caption{margin-bottom:14px}
|
|
61
61
|
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
const channelNames = { weixin: '微信', qq: 'QQ', feishu: '飞书', dingtalk: '钉钉', telegram: 'Telegram', slack: 'Slack', discord: 'Discord', wecom: '企业微信' };
|
|
98
98
|
const pushDestinations = config => Array.isArray(config?.destinations) ? config.destinations : config?.botId && config?.targetId ? [{ botId: config.botId, targetId: config.targetId, enabled: true }] : [];
|
|
99
99
|
const pushLabels = { unavailable: 'dsh-im 未接入', paused: '已暂停', checking: '正在读取配置', invalid: '配置需检查', failed: '发送失败', accepted: '最近发送成功', configured: '已配置 · 尚无成功发送记录' };
|
|
100
|
-
const platformNames = { weibo: '微博', xueqiu: '雪球', x: 'X', bilibili: 'B 站', rednote: '小红书' };
|
|
100
|
+
const platformNames = { douyin: '抖音', weibo: '微博', xueqiu: '雪球', x: 'X', bilibili: 'B 站', rednote: '小红书' };
|
|
101
101
|
const platformAccount = (state, id) => state.accounts?.[id] || state.account;
|
|
102
102
|
const platformReady = (state, id) => {
|
|
103
103
|
const account = platformAccount(state, id);
|
|
@@ -112,15 +112,18 @@
|
|
|
112
112
|
?? state?.targets.find(item => item.uid === uid)?.interval_ms ?? source?.targetDefaults?.intervalMs ?? state?.targetDefaults?.intervalMs;
|
|
113
113
|
}
|
|
114
114
|
const chinaClock = (value, seconds = true) => new Date(value).toLocaleTimeString('zh-CN', { timeZone: 'Asia/Shanghai', hourCycle: 'h23', hour: '2-digit', minute: '2-digit', ...(seconds ? { second: '2-digit' } : {}) });
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
const shortUid = value => { const text = String(value || ''); return text.length > 22 ? text.slice(0, 10) + '…' + text.slice(-6) : text; };
|
|
116
|
+
const taskProblem = job => job.contentIssue
|
|
117
|
+
? job.contentIssue.exhausted ? '自动重试仍未获取这条微博的全文;可查看原文,或跳过这条继续。' : '正在自动重试这条内容,无需操作。'
|
|
118
|
+
: job.kind === 'history' && /长文接口未返回全文/.test(job.message || '')
|
|
119
|
+
? '这条微博的全文未获取到,插件会先自动重试,已有内容保留。'
|
|
120
|
+
: (job.kind === 'history' ? '历史缓存已暂停:' : '新动态检查已暂停:') + (job.message || '读取未完成,已保留进度');
|
|
121
|
+
function checkHealthReason(source, uid = '', targets = []) {
|
|
119
122
|
const health = source?.health;
|
|
120
123
|
if (!health) return '';
|
|
121
124
|
const delayed = health.delayed.filter(check => !uid || check.uid === uid);
|
|
122
125
|
const reasons = [];
|
|
123
|
-
if (delayed.length) reasons.push((
|
|
126
|
+
if (delayed.length) reasons.push(delayed.map(check => targets.find(target => target.uid === check.uid)?.name || 'UID ' + shortUid(check.uid)).join('、') + '未按设定频率及时检查');
|
|
124
127
|
if (health.capacityLimited) reasons.push(health.activeAuthors + ' 位博主共用账号,仅检查一轮最新页就至少需要 ' + Math.ceil(health.minimumCycleMs / 1000) + ' 秒,超出了当前频率的处理能力');
|
|
125
128
|
return reasons.join(';');
|
|
126
129
|
}
|
|
@@ -128,7 +131,7 @@
|
|
|
128
131
|
const target = state?.targets.find(item => item.uid === uid);
|
|
129
132
|
const source = state?.platforms?.find(item => item.id === platform);
|
|
130
133
|
const account = state && platformAccount(state, platform);
|
|
131
|
-
const result = (label, kind, reason, section = 'account') => ({ label, kind, reason, section });
|
|
134
|
+
const result = (label, kind, reason, section = 'account', affectedUid = '') => ({ label, kind, reason, section, ...(affectedUid ? { uid: affectedUid } : {}) });
|
|
132
135
|
if (state?.update?.phase === 'applying') return result('插件更新中', 'paused', '等待版本更新完成,监控配置保持不变', 'service');
|
|
133
136
|
if (!state || state.stopped) return result('后台未就绪', 'blocked', state?.serviceError || '等待本地服务连接');
|
|
134
137
|
if (state.enabled === false) return result('总控已暂停', 'paused', '开启总控制器后继续,博主和平台设置保持不变');
|
|
@@ -143,14 +146,16 @@
|
|
|
143
146
|
if (targets.length && targets.every(item => item.capture_mode === 'pending')) return result('等待配置博主', 'paused', '选择博主并确认记录方式', 'archive');
|
|
144
147
|
if (targets.length && targets.every(item => item.enabled === false)) return result('博主均已暂停', 'paused', '选择至少一位博主开启监控', 'archive');
|
|
145
148
|
const jobs = state.jobs.filter(job => (!uid || job.uid === uid) && (job.platform || state.targets.find(item => item.uid === job.uid)?.platform || 'weibo') === platform);
|
|
149
|
+
const retrying = jobs.filter(job => job.status === 'queued' && job.contentIssue && !job.contentIssue.exhausted);
|
|
150
|
+
if (retrying.length) return result('正在恢复缓存 · 无需操作', 'partial', taskProblem(retrying[0]), 'tasks', retrying[0].uid);
|
|
146
151
|
const problems = jobs.filter(job => job.status === 'needs_review');
|
|
147
152
|
if (problems.length) {
|
|
148
153
|
const historyOnly = problems.every(job => job.kind === 'history');
|
|
149
154
|
const names = [...new Set(problems.map(job => state.targets.find(item => item.uid === job.uid)?.name || job.uid))];
|
|
150
|
-
return result(historyOnly ? names.length + '
|
|
155
|
+
return result(historyOnly ? names[0] + (names.length > 1 ? '等 ' + names.length + ' 位博主' : '') + (problems.some(job => job.contentIssue || /长文接口未返回全文/.test(job.message || '')) ? ':有微博未读完整' : ':历史缓存中断') : names.length + ' 位博主的新动态检查需处理', jobs.some(job => job.status === 'queued') ? 'partial' : 'blocked', names.join('、') + ':' + taskProblem(problems[0]), 'tasks', problems[0].uid);
|
|
151
156
|
}
|
|
152
157
|
if (jobs.length && jobs.every(job => ['paused', 'complete'].includes(job.status))) return result('任务已暂停', 'paused', '在大屏开启需要执行的任务', 'tasks');
|
|
153
|
-
const healthReason = jobs.some(job => job.kind === 'updates' && job.status === 'queued') && checkHealthReason(source, uid);
|
|
158
|
+
const healthReason = jobs.some(job => job.kind === 'updates' && job.status === 'queued') && checkHealthReason(source, uid, state.targets);
|
|
154
159
|
if (healthReason) return result('检查延迟', 'partial', healthReason, 'frequency');
|
|
155
160
|
const active = state.activeJobIds || [state.activeJobId];
|
|
156
161
|
if (jobs.some(job => active.includes(job.id))) return result('正在读取', 'active', '新内容保存后会显示在本地档案中');
|
|
@@ -181,7 +186,8 @@
|
|
|
181
186
|
if (job?.status === 'complete') return '已完成';
|
|
182
187
|
if (kind === 'history' && job.status === 'paused' && /未开启历史缓存|尚未开启历史缓存/.test(job.message || '')) return '未开启';
|
|
183
188
|
if (state.enabled === false || state.platforms?.find(item => item.id === target.platform)?.enabled === false || target.enabled === false || job?.status === 'paused') return '已暂停';
|
|
184
|
-
if (job.status === 'needs_review') return '需处理';
|
|
189
|
+
if (job.status === 'needs_review') return job.contentIssue ? '有内容未读完整' : '需处理';
|
|
190
|
+
if (job.status === 'queued' && job.contentIssue && !job.contentIssue.exhausted) return '自动重试中';
|
|
185
191
|
if (!platformReady(state, target.platform || 'weibo')) return '等待连接';
|
|
186
192
|
if (kind === 'updates' && state.platforms?.find(item => item.id === (target.platform || 'weibo'))?.health?.delayed.some(check => check.uid === target.uid)) return '检查延迟';
|
|
187
193
|
if (kind === 'updates' && progressFor(state, target.uid).catchup) return '补查中';
|
|
@@ -474,20 +480,104 @@
|
|
|
474
480
|
}
|
|
475
481
|
function AddAuthorDialog({ state, act, busy, error, close, connect, initialPlatform = 'weibo', clearError = () => {} }) {
|
|
476
482
|
const dialog = React.useRef(null), [url, setUrl] = React.useState('');
|
|
477
|
-
const [selectedPlatform, setSelectedPlatform] = React.useState(['weibo', 'xueqiu'].includes(initialPlatform) ? initialPlatform : 'weibo');
|
|
483
|
+
const [selectedPlatform, setSelectedPlatform] = React.useState(['weibo', 'xueqiu', 'douyin'].includes(initialPlatform) ? initialPlatform : 'weibo');
|
|
478
484
|
const name = platformNames[selectedPlatform], connected = platformAccount(state, selectedPlatform)?.status === 'ready';
|
|
479
485
|
React.useEffect(() => { dialog.current?.showModal(); dialog.current?.querySelector('input')?.focus(); }, []);
|
|
480
486
|
return h('dialog', { ref: dialog, className: 'st-node-dialog', 'aria-label': '添加' + name + '博主', onCancel: close },
|
|
481
487
|
h('div', { className: 'st-dialog-head' }, h('h2', null, '添加' + name + '博主'), h('button', { className: 'st-dialog-close', 'aria-label': '关闭添加博主', onClick: close }, '×')),
|
|
482
|
-
h('div', { className: 'st-platform-tabs', role: 'tablist', 'aria-label': '添加博主的平台' }, ['weibo', 'xueqiu'].map(id => h('button', { key: id, role: 'tab', 'aria-selected': selectedPlatform === id, onClick: () => { setSelectedPlatform(id); setUrl(''); clearError(); } }, platformNames[id]))),
|
|
488
|
+
h('div', { className: 'st-platform-tabs', role: 'tablist', 'aria-label': '添加博主的平台' }, ['weibo', 'xueqiu', 'douyin'].map(id => h('button', { key: id, role: 'tab', 'aria-selected': selectedPlatform === id, onClick: () => { setSelectedPlatform(id); setUrl(''); clearError(); } }, platformNames[id]))),
|
|
483
489
|
!connected ? h('div', null, h('p', null, '先连接你的' + name + '账号,再读取要关注的博主。'), h('button', { className: 'st-primary', onClick: () => connect(selectedPlatform) }, '去连接' + name))
|
|
484
490
|
: h('form', { onSubmit: async event => { event.preventDefault(); if (await act('/targets', { platform: selectedPlatform, url })) close(); } },
|
|
485
|
-
h('label', null, '博主主页链接或数字 UID', h('input', { 'aria-label': name + '博主主页或 UID', value: url, required: true, placeholder: selectedPlatform === 'weibo' ? 'https://weibo.com/u/…' : 'https://xueqiu.com/u/…', onChange: event => { setUrl(event.target.value); clearError(); } })),
|
|
491
|
+
h('label', null, selectedPlatform === 'douyin' ? '博主主页链接或 sec_uid' : '博主主页链接或数字 UID', h('input', { 'aria-label': name + '博主主页或 UID', value: url, required: true, placeholder: selectedPlatform === 'douyin' ? 'https://www.douyin.com/user/…' : selectedPlatform === 'weibo' ? 'https://weibo.com/u/…' : 'https://xueqiu.com/u/…', onChange: event => { setUrl(event.target.value); clearError(); } })),
|
|
486
492
|
h('p', { className: 'st-caption' }, '先读取昵称、头像和消息总数,再由你决定从现在记录,还是缓存以前的消息。'),
|
|
487
493
|
error ? h('p', { className: 'st-inline-error', role: 'alert' }, error) : null,
|
|
488
494
|
h('button', { className: 'st-primary', disabled: busy }, busy ? '正在添加…' : '添加并读取资料')));
|
|
489
495
|
}
|
|
490
|
-
|
|
496
|
+
/* DOUYIN_UI_START */
|
|
497
|
+
const { DouyinConfigForm, DouyinNodeSettings } = (function createDouyinUI({ React, h, SettingRow, TaskControl, LegacyNodeSettings }) {
|
|
498
|
+
function DouyinConfigForm({ state, uid = '', act, busy, part = 'all' }) {
|
|
499
|
+
const base = { times: ['20:00'], repeat: 'daily', platformAI: true, saveVideo: false, saveAudio: false, transcribe: false, maxFileMB: 256, python: 'python3', ffmpeg: 'ffmpeg', modelPath: '', threads: 2, ...(state.douyin?.config || {}) };
|
|
500
|
+
const [value, setValue] = React.useState(() => ({ ...base, ...(state.douyin?.overrides?.[uid] || {}) }));
|
|
501
|
+
const [inherit, setInherit] = React.useState(!!uid && !state.douyin?.overrides?.[uid]);
|
|
502
|
+
const [message, setMessage] = React.useState('');
|
|
503
|
+
const change = (key, val) => { if (uid && inherit) { setInherit(false); setMessage('已切换为这位博主的独立配置,保存后生效'); } else setMessage(''); setValue(old => ({ ...old, [key]: val })); };
|
|
504
|
+
const disabled = busy;
|
|
505
|
+
return h('form', { className: 'st-douyin-config', 'aria-label': uid ? '抖音博主配置' : '抖音平台默认配置', onSubmit: async event => {
|
|
506
|
+
event.preventDefault();
|
|
507
|
+
if (await act('/douyin/config', { platform: 'douyin', uid: uid || undefined, inherit, section: part, config: value })) setMessage('抖音配置已保存');
|
|
508
|
+
} },
|
|
509
|
+
uid ? h(SettingRow, { label: '跟随平台', description: '修改下方选项会自动切换为独立配置,其他博主不受影响' }, h('input', { type: 'checkbox', role: 'switch', 'aria-label': '跟随抖音平台配置', checked: inherit, disabled: busy, onChange: event => { setInherit(event.target.checked); if (event.target.checked) setValue(base); } })) : h('p', { className: 'st-caption' }, '只更新跟随平台的博主;独立配置保持不变。'),
|
|
510
|
+
part !== 'media' ? h('section', { 'aria-label': '抖音定时检查' }, h('h3', null, '定时检查'),
|
|
511
|
+
h(SettingRow, { label: '执行日期' }, h('select', { 'aria-label': '抖音检查日期', value: value.repeat, disabled, onChange: event => change('repeat', event.target.value) }, h('option', { value: 'daily' }, '每天'), h('option', { value: 'weekdays' }, '工作日(周一至周五)'))),
|
|
512
|
+
value.times.map((time, index) => h(SettingRow, { key: index, label: '检查时间 ' + (index + 1), description: '北京时间' }, h('div', { className: 'st-actions' }, h('input', { type: 'time', 'aria-label': '抖音检查时间 ' + (index + 1), value: time, required: true, disabled, onChange: event => change('times', value.times.map((item, at) => at === index ? event.target.value : item)) }),
|
|
513
|
+
value.times.length > 1 ? h('button', { type: 'button', disabled, onClick: () => change('times', value.times.filter((item, at) => at !== index)) }, '删除') : null))),
|
|
514
|
+
h('button', { type: 'button', disabled: disabled || value.times.length >= 8, onClick: () => change('times', [...value.times, '']) }, '+ 添加检查时间'),
|
|
515
|
+
h('p', { className: 'st-caption' }, '只在指定时间检查;重启后会补查离线更新。下载与转写单独排队,不阻塞检查。')) : null,
|
|
516
|
+
part !== 'frequency' ? h('section', { 'aria-label': '抖音内容保存' }, h('h3', null, '内容保存'),
|
|
517
|
+
[['platformAI', '平台 AI 解析', '保存平台返回的摘要与章节,不是作者逐字原话'], ['saveVideo', '保存视频', '把新发现的视频下载到本地'], ['saveAudio', '保存音频', '从视频音轨提取,不把背景音乐当人声'], ['transcribe', '本地转写', '用本地 SenseVoice 生成识别稿;一次处理一个作品']].map(([key, label, description]) => h(SettingRow, { key, label, description }, h('input', { type: 'checkbox', role: 'switch', 'aria-label': label, checked: value[key], disabled, onChange: event => change(key, event.target.checked) }))),
|
|
518
|
+
h(SettingRow, { label: '单文件上限', description: 'MB,超过上限会停止该文件下载' }, h('input', { type: 'number', 'aria-label': '抖音单文件上限', min: 16, max: 2048, value: value.maxFileMB, disabled, onChange: event => change('maxFileMB', Number(event.target.value)) })),
|
|
519
|
+
value.saveAudio || value.transcribe ? h(SettingRow, { label: 'ffmpeg', description: '程序名或完整路径;音频和转写需要' }, h('input', { 'aria-label': 'ffmpeg 程序', value: value.ffmpeg, disabled, onChange: event => change('ffmpeg', event.target.value) })) : null,
|
|
520
|
+
value.transcribe ? h(React.Fragment, null,
|
|
521
|
+
h(SettingRow, { label: 'Python', description: '需安装 FunASR、PyTorch 依赖' }, h('input', { 'aria-label': '转写 Python', value: value.python, disabled, onChange: event => change('python', event.target.value) })),
|
|
522
|
+
h(SettingRow, { label: '模型目录', description: '已下载的 SenseVoice 本地目录;不会自动下载模型' }, h('input', { 'aria-label': 'SenseVoice 模型目录', value: value.modelPath, disabled, onChange: event => change('modelPath', event.target.value) })),
|
|
523
|
+
h(SettingRow, { label: 'CPU 线程', description: '1–4;空闲时不常驻模型' }, h('input', { type: 'number', 'aria-label': '转写 CPU 线程', min: 1, max: 4, value: value.threads, disabled, onChange: event => change('threads', Number(event.target.value)) }))) : null,
|
|
524
|
+
h('p', { className: 'st-caption' }, '关闭选项会停止后续处理,已有文件保留;音频和转写支持一小时以内的作品。图集保留配图,不生成音轨。')) : null,
|
|
525
|
+
message ? h('p', { role: 'status', className: 'st-caption' }, message) : null,
|
|
526
|
+
h('button', { className: 'st-primary', disabled: busy, type: 'submit' }, '保存抖音配置'));
|
|
527
|
+
}
|
|
528
|
+
function DouyinNodeSettings(props) {
|
|
529
|
+
const { context, state, act, busy, close, embedded } = props;
|
|
530
|
+
const dialog = React.useRef(null), [tab, setTab] = React.useState(context.tab || 'frequency');
|
|
531
|
+
const target = state.targets.find(item => item.uid === context.uid);
|
|
532
|
+
React.useEffect(() => { if (!embedded) dialog.current?.showModal(); }, []);
|
|
533
|
+
return h(embedded ? 'section' : 'dialog', { ref: embedded ? undefined : dialog, className: embedded ? 'st-inline-settings' : 'st-node-dialog', 'aria-label': '抖音详细设置', onCancel: close },
|
|
534
|
+
!embedded ? h('div', { className: 'st-dialog-head' }, h('h2', null, (target?.name || '抖音平台') + '的详细设置'), h('button', { onClick: close, 'aria-label': '关闭监控设置' }, '×')) : null,
|
|
535
|
+
h('div', { className: 'st-platform-tabs', role: 'tablist', 'aria-label': '抖音配置分组' }, [['frequency', '定时检查'], ['media', '内容保存'], ['history', '历史缓存']].map(([id, title]) => h('button', { key: id, type: 'button', role: 'tab', 'aria-selected': tab === id, onClick: () => setTab(id) }, title))),
|
|
536
|
+
props.error ? h('p', { role: 'alert', className: 'st-inline-error' }, props.error) : null,
|
|
537
|
+
h('div', { hidden: tab === 'history' }, target && tab === 'frequency' ? h(TaskControl, { state, target, kind: 'updates', act, busy, openSettings: props.openSettings }) : null, h(DouyinConfigForm, { state, uid: target?.uid, act, busy, part: tab === 'media' ? 'media' : 'frequency' })), tab === 'history' ? h(LegacyNodeSettings, { ...props, embedded: true, context: { ...context, tab: 'history' } }) : null);
|
|
538
|
+
}
|
|
539
|
+
return { DouyinConfigForm, DouyinNodeSettings };
|
|
540
|
+
})({ React, h, SettingRow, TaskControl, LegacyNodeSettings });
|
|
541
|
+
/* DOUYIN_UI_END */
|
|
542
|
+
function NodeSettings(props) { return h(props.context.platform === 'douyin' ? DouyinNodeSettings : LegacyNodeSettings, props); }
|
|
543
|
+
function ContentProblem({ job, name, act, busy, paused = false }) {
|
|
544
|
+
const issue = job.contentIssue;
|
|
545
|
+
if (!issue) return h('p', { className: 'st-inline-error', role: 'alert' }, h('strong', null, name), ' · ', taskProblem(job), ' ', h('button', { disabled: busy, onClick: () => act('/jobs/' + job.id + '/resume') }, job.kind === 'history' ? '重试历史缓存' : '重试新动态监控'));
|
|
546
|
+
const waiting = !issue.exhausted, stopped = paused || job.status === 'paused';
|
|
547
|
+
return h('section', { className: 'st-inline-error st-content-problem', role: waiting ? 'status' : 'alert', 'aria-label': '未读完整的内容' },
|
|
548
|
+
h('strong', null, stopped ? '已暂停,恢复后继续读取' : waiting ? '正在自动恢复,无需操作' : '这条微博仍未读完整'),
|
|
549
|
+
h('p', null, name, ' · ', issue.stage, issue.publishedAt ? ' · 发布于 ' + date(issue.publishedAt) : ''),
|
|
550
|
+
issue.excerpt ? h('p', { className: 'st-text' }, issue.excerpt) : null,
|
|
551
|
+
h('p', null, stopped ? '你已暂停监控或缓存,插件不会继续请求。' : waiting ? '微博暂未返回全文;将在 ' + chinaClock(issue.nextRetryAt, false) + ' 自动重试(' + issue.attempts + '/' + issue.maxAttempts + '),不需要点击重试。' : '已自动重试 ' + issue.attempts + ' 次,微博仍未返回全文。' + (job.kind === 'history' ? '历史缓存停在这条;已有记录和新动态监控不受影响。' : '新动态检查停在这条;其他博主和已存记录不受影响。')),
|
|
552
|
+
h('div', { className: 'st-actions' }, /^https:\/\/weibo\.com\/detail\/\d+$/.test(issue.url || '') ? h('a', { href: issue.url, target: '_blank', rel: 'noreferrer' }, '查看这条微博 ↗') : null,
|
|
553
|
+
!waiting && job.status === 'needs_review' ? h('button', { disabled: busy, className: 'st-primary', onClick: () => act('/jobs/skip-content', { id: job.id, postId: issue.postId, platform: job.platform }) }, job.kind === 'history' ? '跳过这条,继续缓存' : '跳过这条,继续检查') : null),
|
|
554
|
+
!waiting ? h('p', { className: 'st-caption' }, '跳过时保留已经读到的片段和原文链接,不标记为全文。') : null);
|
|
555
|
+
}
|
|
556
|
+
function CheckHealthNotice({ state, source, uid, openSettings, navigate }) {
|
|
557
|
+
const delayed = source?.health?.delayed?.filter(check => !uid || check.uid === uid) || [];
|
|
558
|
+
if (!delayed.length && !source?.health?.capacityLimited) return null;
|
|
559
|
+
const active = state.activeJobIds || [state.activeJobId];
|
|
560
|
+
return h('section', { className: 'st-inline-error', role: 'status', 'aria-label': '检查延迟的博主' },
|
|
561
|
+
h('p', null, delayed.length ? '以下博主的新动态检查有延迟,后台会继续执行,无需反复点击重试。' : checkHealthReason(source, uid, state.targets)),
|
|
562
|
+
delayed.length ? h('div', { style: { overflowX: 'auto' } }, h('table', null,
|
|
563
|
+
h('thead', null, h('tr', null, ['博主', '最近完成检查', '超过计划时间', '当前情况', '查看'].map(label => h('th', { key: label }, label)))),
|
|
564
|
+
h('tbody', null, delayed.map(check => {
|
|
565
|
+
const owner = state.targets.find(target => target.uid === check.uid);
|
|
566
|
+
const job = state.jobs.find(job => job.uid === check.uid && job.kind === 'updates');
|
|
567
|
+
const progress = state.progress?.find(item => item.uid === check.uid);
|
|
568
|
+
const name = owner?.name || 'UID ' + shortUid(check.uid);
|
|
569
|
+
const late = Number(check.lateMs);
|
|
570
|
+
const reason = active.includes(job?.id) ? '正在检查' : progress?.catchup ? '正在补查离线期间的消息' : '等待后台检查,延迟原因尚未确认';
|
|
571
|
+
return h('tr', { key: check.uid },
|
|
572
|
+
h('td', null, h('strong', null, name), h('small', { style: { display: 'block' } }, platformNames[source.id] || source.id, ' · UID ', shortUid(owner?.external_uid || check.uid))),
|
|
573
|
+
h('td', null, check.lastCheckedAt ? date(check.lastCheckedAt) : '尚无完成记录'),
|
|
574
|
+
h('td', null, Number.isFinite(late) ? late < 60000 ? Math.ceil(late / 1000) + ' 秒' : Math.ceil(late / 60000) + ' 分钟' : '待确认'),
|
|
575
|
+
h('td', null, reason),
|
|
576
|
+
h('td', null, uid ? h('button', { type: 'button', className: 'st-link-button', onClick: () => navigate('archive', { uid: check.uid, tab: 'frequency' }) }, '查看检查记录 →')
|
|
577
|
+
: h('button', { type: 'button', className: 'st-link-button', 'aria-label': '查看' + name + '的检查详情', onClick: () => openSettings({ scope: 'target', uid: check.uid, platform: source.id, tab: 'frequency' }) }, '查看详情 →')));
|
|
578
|
+
})))) : null);
|
|
579
|
+
}
|
|
580
|
+
function LegacyNodeSettings({ context, state, act, busy, error, navigate, close, openSettings, embedded = false }) {
|
|
491
581
|
const dialog = React.useRef(null);
|
|
492
582
|
const target = state.targets.find(item => item.uid === context.uid), source = state.platforms?.find(item => item.id === context.platform);
|
|
493
583
|
const [value, setValue] = React.useState(() => ({ intervalMinutes: (target?.policy?.intervalMs || target?.interval_ms || source?.targetDefaults?.intervalMs || 900000) / 60000,
|
|
@@ -507,23 +597,23 @@
|
|
|
507
597
|
const running = owners.some(owner => streamStatus(state, owner, 'history') === '缓存中');
|
|
508
598
|
const started = history.some(job => job.pages > 0 || job.pending > 0 || job.page > 1) || owners.some(owner => owner.capture_mode === 'history' || progressFor(state, owner.uid).history.started);
|
|
509
599
|
const progress = target && progressFor(state, target.uid);
|
|
510
|
-
const problems = state.jobs.filter(job => job.status === 'needs_review' && job.kind === (tab === 'history' ? 'history' : 'updates') && owners.some(owner => owner.uid === job.uid));
|
|
600
|
+
const problems = state.jobs.filter(job => (job.status === 'needs_review' || job.contentIssue) && job.kind === (tab === 'history' ? 'history' : 'updates') && owners.some(owner => owner.uid === job.uid));
|
|
511
601
|
const run = async (path, body, message) => { if (await act(path, { platform: context.platform, ...body })) { setMessage(message); return true; } return false; };
|
|
512
602
|
React.useEffect(() => { if (!embedded) dialog.current?.showModal(); }, []);
|
|
513
603
|
return h(embedded ? 'section' : 'dialog', { ref: embedded ? undefined : dialog, className: embedded ? 'st-inline-settings' : 'st-node-dialog', role: embedded ? 'region' : undefined, 'aria-label': '节点监控设置', onCancel: embedded ? undefined : close },
|
|
514
604
|
!embedded ? h('div', { className: 'st-dialog-head' }, h('h2', null, target ? target.name + '的监控设置' : context.scope === 'all' ? '平台默认监控设置' : platformNames[context.platform] + '监控设置'), h('button', { className: 'st-dialog-close', 'aria-label': '关闭监控设置', onClick: close }, '×')) : null,
|
|
515
605
|
!embedded ? h('p', { className: 'st-caption' }, target ? '只影响这位博主。' : tab === 'frequency' ? '这是平台默认频率。只有跟随平台设置的博主会更新,单独设置的博主保持不变。' : '批量管理博主的历史缓存,不改变各博主的监控频率。') : null,
|
|
516
|
-
!['weibo', 'xueqiu'].includes(context.platform) ? h('p', null, '开发中') : h(React.Fragment, null,
|
|
606
|
+
!['weibo', 'xueqiu', 'douyin'].includes(context.platform) ? h('p', null, '开发中') : h(React.Fragment, null,
|
|
517
607
|
error ? h('p', { className: 'st-inline-error', role: 'alert' }, error) : null,
|
|
518
608
|
message ? h('p', { className: 'st-dialog-status', role: 'status' }, message) : null,
|
|
519
609
|
!embedded ? h('div', { className: 'st-platform-tabs', role: 'tablist', 'aria-label': '节点配置' }, [['frequency', '新动态监控'], ['history', '历史消息缓存']].map(([id, label]) =>
|
|
520
610
|
h('button', { key: id, id: 'st-node-tab-' + id, role: 'tab', 'aria-selected': tab === id, 'aria-controls': 'st-node-panel-' + id, onClick: () => { setTab(id); setMessage(''); } }, label))) : null,
|
|
521
|
-
problems.map(job => h(
|
|
611
|
+
problems.map(job => h(ContentProblem, { key: job.id, job, name: owners.find(owner => owner.uid === job.uid)?.name || job.uid, act, busy, paused })),
|
|
522
612
|
tab === 'frequency' && progress?.catchup ? h('p', { className: 'st-caption', role: 'status' }, streamStatus(state, target, 'updates') === '补查中' ? '正在补查离线期间的动态:从 ' : '离线补查' + streamStatus(state, target, 'updates') + ':从 ', date(progress.catchup.from), ' 比对,第 ', progress.catchup.page, ' 页,已补存 ', progress.catchup.saved, ' 条。完成后恢复设定频率。') : null,
|
|
523
613
|
tab === 'frequency' ? h('form', { id: 'st-node-panel-frequency', role: 'tabpanel', 'aria-labelledby': embedded ? 'st-reader-tab-frequency' : 'st-node-tab-frequency', onSubmit: event => { event.preventDefault(); void run(target ? '/targets/policy' : '/settings', target ? { uid: target.uid, inherit: false, intervalMinutes: value.intervalMinutes, schedule: value.schedule } : { intervalMinutes: value.intervalMinutes, schedule: value.schedule }, '监控时间与频率已保存'); } },
|
|
524
614
|
target ? h('p', { className: 'st-caption' }, target.policy ? '当前为这位博主单独设置。' : '已填入' + platformNames[context.platform] + '平台的时间;保存后只对这位博主生效。') : null,
|
|
525
615
|
target ? h(TaskControl, { state, target, kind: 'updates', act, busy, openSettings: context => { setTab(context.tab || 'frequency'); } }) : null,
|
|
526
|
-
|
|
616
|
+
h(CheckHealthNotice, { state, source, uid: target?.uid, openSettings, navigate }),
|
|
527
617
|
progress?.recentChecks?.length ? h('details', null, h('summary', null, '最近检查记录'), h('table', null,
|
|
528
618
|
h('thead', null, h('tr', null, ['检查时间', '接口结果', '返回的最新发布时间'].map(label => h('th', { key: label }, label)))),
|
|
529
619
|
h('tbody', null, progress.recentChecks.slice(-6).reverse().map((check, index) => h('tr', { key: index },
|
|
@@ -546,8 +636,9 @@
|
|
|
546
636
|
h('p', { className: 'st-history-state' }, '历史消息:', target ? streamStatus(state, target, 'history') : running ? '缓存中' : history.some(job => job.status === 'queued') ? '等待执行' : complete ? '已完成' : '已暂停'),
|
|
547
637
|
h('p', { className: 'st-visibility-note' }, '只能缓存当前登录账号可见的消息。已删除或当前账号无权查看的消息无法缓存。'),
|
|
548
638
|
target && progress ? h('dl', { className: 'st-cache-facts' },
|
|
549
|
-
h('div', null, h('dt', null, '博主消息总数(含转发)'), h('dd', null, totalText(progress.history))),
|
|
639
|
+
h('div', null, h('dt', null, context.platform === 'douyin' ? '博主作品总数(视频与图集)' : '博主消息总数(含转发)'), h('dd', null, totalText(progress.history))),
|
|
550
640
|
h('div', null, h('dt', null, '本地累计(新+历史)'), h('dd', null, progress.saved.toLocaleString(), ' 条')),
|
|
641
|
+
progress.history.incompleteCount ? h('div', null, h('dt', null, '其中仅保存片段'), h('dd', null, progress.history.incompleteCount, ' 条,未标记为全文')) : null,
|
|
551
642
|
progress.history.originalOnly ? h('div', null, h('dt', null, '所选范围内原创已存'), h('dd', null, (progress.history.rangeSaved || 0).toLocaleString(), ' 条')) : null,
|
|
552
643
|
h('div', null, h('dt', null, '预计剩余'), h('dd', null, progress.history.originalOnly ? '原创总数未知,无法估算' : typeof progress.history.estimatedRemaining === 'number' ? progress.history.estimatedRemaining.toLocaleString() + ' 条' : progress.history.status === 'complete' ? '可见范围已缓存' : '暂时无法估算')),
|
|
553
644
|
h('div', null, h('dt', null, '最近保存'), h('dd', null, progress.lastSavedAt ? date(progress.lastSavedAt) : '尚未保存'))) : null,
|
|
@@ -557,10 +648,10 @@
|
|
|
557
648
|
!target ? h('form', { className: 'st-history-pace', onSubmit: event => { event.preventDefault(); void run('/settings/history', { historyGapSeconds: value.historyGapSeconds }, '历史缓存速度已保存'); } },
|
|
558
649
|
h(SettingRow, { label: '缓存速度', htmlFor: 'st-history-pace', description: '每页读取后的等待秒数,不影响新动态频率' }, h('input', { id: 'st-history-pace', type: 'number', min: 60, max: 86400, required: true, 'aria-label': '历史缓存每页等待秒数', value: value.historyGapSeconds, disabled: busy, onChange: event => setValue({ ...value, historyGapSeconds: Number(event.target.value) }) })),
|
|
559
650
|
h('button', { type: 'submit', disabled: busy }, '保存缓存速度')) : null,
|
|
560
|
-
h('label', { className: 'st-original-only' }, h('span', null, '仅缓存原创(不含转发)'), h('input', { type: 'checkbox', role: 'switch', checked: originalOnly, disabled: busy, onChange: event => setOriginalOnly(event.target.checked) })),
|
|
651
|
+
context.platform !== 'douyin' ? h('label', { className: 'st-original-only' }, h('span', null, '仅缓存原创(不含转发)'), h('input', { type: 'checkbox', role: 'switch', checked: originalOnly, disabled: busy, onChange: event => setOriginalOnly(event.target.checked) })) : null,
|
|
561
652
|
paused ? h('p', { className: 'st-caption' }, '监控开关已暂停,安排的缓存会在恢复后执行。') : null,
|
|
562
653
|
h('div', { className: 'st-actions', style: { marginTop: 14 } },
|
|
563
|
-
!complete && !scheduled ? h('button', { className: 'st-primary', disabled: busy || !owners.length, onClick: () => run(target?.capture_mode === 'pending' ? '/targets/start' : '/history/range', { uid: target?.uid, mode: 'history', scope: target ? 'target' : 'platform', from: range.from, to: range.to, originalOnly, enableTarget: !!target }, paused ? '已安排缓存,恢复监控开关后执行' : '缓存任务已更新') }, target?.capture_mode === 'pending' ? '确认并开始记录' : !unchanged ? '应用筛选并缓存' : started ? '继续缓存' : '开始缓存') : null,
|
|
654
|
+
!complete && !scheduled && !problems.length ? h('button', { className: 'st-primary', disabled: busy || !owners.length, onClick: () => run(target?.capture_mode === 'pending' ? '/targets/start' : '/history/range', { uid: target?.uid, mode: 'history', scope: target ? 'target' : 'platform', from: range.from, to: range.to, originalOnly, enableTarget: !!target }, paused ? '已安排缓存,恢复监控开关后执行' : '缓存任务已更新') }, target?.capture_mode === 'pending' ? '确认并开始记录' : !unchanged ? '应用筛选并缓存' : started ? '继续缓存' : '开始缓存') : null,
|
|
564
655
|
complete ? h('span', { className: 'st-caption' }, '所选可见消息已缓存;更改筛选后可重新缓存。') : null,
|
|
565
656
|
history.some(job => job.status === 'queued') ? h('button', { className: scheduled ? 'st-primary' : '', disabled: busy, onClick: () => run('/history/pause', { uid: target?.uid, scope: target ? 'target' : 'platform' }, '以前消息的缓存已暂停,进度保留') }, '暂停缓存') : null))) : null));
|
|
566
657
|
}
|
|
@@ -586,7 +677,7 @@
|
|
|
586
677
|
const progress = progressFor(state, target.uid);
|
|
587
678
|
const field = (label, value) => h('div', null, h('dt', null, label), h('dd', null, value));
|
|
588
679
|
return h('section', { className: 'st-author-progress', 'aria-label': target.name + '保存进度' },
|
|
589
|
-
h('dl', { className: 'st-save-facts st-reader-facts' }, field('消息总数(含转发)', totalText(progress.history)), field('本地已存', progress.saved.toLocaleString() + ' 条'), field('最近保存', progress.lastSavedAt ? date(progress.lastSavedAt) : '尚未保存')));
|
|
680
|
+
h('dl', { className: 'st-save-facts st-reader-facts' }, field(target.platform === 'douyin' ? '作品总数(视频与图集)' : '消息总数(含转发)', totalText(progress.history)), field('本地已存', progress.saved.toLocaleString() + ' 条'), field('最近保存', progress.lastSavedAt ? date(progress.lastSavedAt) : '尚未保存')));
|
|
590
681
|
}
|
|
591
682
|
|
|
592
683
|
function AuthorList({ state, act, busy, navigate, openSettings }) {
|
|
@@ -606,7 +697,7 @@
|
|
|
606
697
|
['新动态', '历史缓存', '详细设置'].map(label => h('th', { key: label, scope: 'col' }, label)))),
|
|
607
698
|
h('tbody', null, rows.map(({ target, progress }) => h('tr', { key: target.uid, className: 'st-author-overview' },
|
|
608
699
|
h('td', null, h('button', { className: 'st-author-open', onClick: () => navigate('archive', { uid: target.uid }) }, h(AuthorAvatar, { target, size: 'small' }),
|
|
609
|
-
h('span', null, h('strong', null, target.name), h('span', { className: 'st-meta-tag st-uid-tag' }, 'UID ' + (target.external_uid || target.uid))))),
|
|
700
|
+
h('span', null, h('strong', null, target.name), h('span', { className: 'st-meta-tag st-uid-tag', title: 'UID ' + (target.external_uid || target.uid) }, 'UID ' + shortUid(target.external_uid || target.uid))))),
|
|
610
701
|
h('td', null, h('span', { className: 'st-meta-tag st-platform-badge' }, platformNames[target.platform || 'weibo'] || target.platform)),
|
|
611
702
|
h('td', null, totalText(progress.history)), h('td', null, progress.saved.toLocaleString() + ' 条'),
|
|
612
703
|
h('td', null, h(TaskControl, { state, target, kind: 'updates', act, busy, openSettings })),
|
|
@@ -634,8 +725,27 @@
|
|
|
634
725
|
h('p', { className: 'st-quote-preview' }, post.quote.text.slice(0, 180))),
|
|
635
726
|
h('div', { className: 'st-quote-body' }, post.quote.article ? h('a', { href: post.quote.url, target: '_blank', rel: 'noreferrer' }, post.quote.text.split('\n\n')[0] + ' ↗') : post.quote.text, h(ImageGallery, { post, images: post.quote.images, quoted: true })));
|
|
636
727
|
}
|
|
728
|
+
function VideoContent({ post, act, busy }) {
|
|
729
|
+
const video = post.video, labels = { not_requested: '未开启', queued: '待处理', running: '处理中', done: '已完成', failed: '失败', needs_setup: '需配置环境' };
|
|
730
|
+
const media = type => BASE + '/video?id=' + encodeURIComponent(post.id) + '&type=' + type;
|
|
731
|
+
return h('section', { className: 'st-video-content', 'aria-label': '抖音作品资料' },
|
|
732
|
+
h('div', { className: 'st-actions st-caption' }, [['video', '视频'], ['audio', '音频'], ['transcription', '转写']].map(([key, title]) => h('span', { key }, title + ':' + (labels[video.stages[key]] || video.stages[key])))),
|
|
733
|
+
video.videoAvailable ? h('video', { controls: true, preload: 'none', src: media('video'), style: { width: '100%', maxHeight: 400, marginTop: 12 } }) : null,
|
|
734
|
+
video.audioAvailable ? h('audio', { controls: true, preload: 'none', src: media('audio'), style: { maxWidth: '100%', marginTop: 12 } }) : null,
|
|
735
|
+
video.summary.status !== 'disabled' ? h('details', { open: video.summary.status === 'available', className: 'st-quote' }, h('summary', null, '平台 AI 解析 · ' + (video.summary.status === 'available' ? '已获取' : video.summary.status === 'unconfirmed' ? '状态未确认' : '本次未获取')),
|
|
736
|
+
h('p', { className: 'st-caption' }, '平台生成的归纳,不是作者逐字原话;数字与引用请核对视频。'), h('p', { className: 'st-text' }, video.summary.abstract || '本次没有返回摘要'),
|
|
737
|
+
(video.summary.chapters || []).map((chapter, index) => h('div', { key: index }, h('strong', null, Math.floor(chapter.atMs / 60000) + ':' + String(Math.floor(chapter.atMs / 1000) % 60).padStart(2, '0') + ' ' + chapter.title), h('p', null, chapter.text)))) : null,
|
|
738
|
+
h('details', null, h('summary', null, '本地文字稿 · ' + (video.transcript ? '已生成' : labels[video.stages.transcription] || '尚未生成')), h('p', { className: 'st-caption' }, '本地语音识别稿,可能有错字;不是平台 AI 摘要。'), h('div', { className: 'st-text' }, video.transcript || '尚无文字稿')),
|
|
739
|
+
video.error ? h('p', { className: 'st-inline-error' }, video.error) : null,
|
|
740
|
+
['failed', 'needs_setup'].some(status => Object.values(video.stages).includes(status)) ? h('button', { disabled: busy, onClick: () => act('/video/retry', { id: post.id, platform: 'douyin' }) }, '重试媒体处理') : null,
|
|
741
|
+
h('div', { className: 'st-actions' }, video.videoAvailable ? h('a', { href: media('video') + '&download=1', download: true }, '下载本地视频') : null, video.audioAvailable ? h('a', { href: media('audio') + '&download=1', download: true }, '下载本地音频') : null));
|
|
742
|
+
}
|
|
637
743
|
function FrequencyNote({ state, platform, uid = '', navigate }) {
|
|
638
744
|
const source = state.platforms?.find(item => item.id === platform);
|
|
745
|
+
if (platform === 'douyin') {
|
|
746
|
+
const config = state.douyin?.overrides?.[uid] || state.douyin?.config || { times: ['20:00'], repeat: 'daily' };
|
|
747
|
+
return h('button', { className: 'st-frequency-note', 'aria-label': '抖音定时检查', onClick: navigate }, (uid && state.douyin?.overrides?.[uid] ? '独立 · ' : '') + (config.repeat === 'weekdays' ? '工作日 ' : '每天 ') + config.times.join('、') + '(北京时间)');
|
|
748
|
+
}
|
|
639
749
|
const target = state.targets.find(item => item.uid === uid), schedule = target?.policy?.schedule || source?.schedule;
|
|
640
750
|
const progress = state.progress?.find(row => row.uid === uid);
|
|
641
751
|
const rules = schedule?.enabled ? '北京时间:' + schedule.windows.map(window => repeatText(window) + ' ' + window.start + '–' + window.end + ' ' + intervalText(window.intervalMinutes * 60000)).join(';') + ';其他时间 ' + intervalText(schedule.fallbackMinutes * 60000) : '固定频率';
|
|
@@ -859,7 +969,7 @@
|
|
|
859
969
|
return h('div', { key: kind, className: 'st-stream-row' }, h('button', { className: 'st-stream-status' + (['监控中', '缓存中', '补查中'].includes(status) ? ' is-running' : ''), title: effective.reason, disabled: preview, onClick: () => {
|
|
860
970
|
const status = streamStatus(state, item, kind);
|
|
861
971
|
if (status === '等待连接') return navigate('settings', { platform: platform.id, section: 'account' });
|
|
862
|
-
if (
|
|
972
|
+
if (['需处理', '有内容未读完整'].includes(status)) return navigate('tasks', { uid: item.uid });
|
|
863
973
|
openSettings({ scope: 'target', uid: item.uid, platform: platform.id, tab: kind === 'history' ? 'history' : 'frequency' });
|
|
864
974
|
} }, h('span', null, label), h('b', null, preview ? '待设置' : status)),
|
|
865
975
|
h('button', { role: 'switch', 'aria-label': item.name + (kind === 'history' ? '历史缓存' : '新动态') + '开关', 'aria-checked': item.enabled !== false && job?.status === 'queued', className: 'st-monitor-switch st-job-switch' + (item.enabled === false || job?.status !== 'queued' ? ' off' : ''),
|
|
@@ -883,7 +993,7 @@
|
|
|
883
993
|
h('button', { className: 'st-node-gear', 'aria-label': '设置' + platform.name + '平台', disabled: preview || busy, onClick: () => openSettings({ scope: 'platform', platform: platform.id }) }, h(GearIcon)),
|
|
884
994
|
h('button', { type: 'button', className: 'st-platform-select', onClick: () => fold('source-' + platform.id, !!folds['source-' + platform.id]), 'aria-expanded': !folds['source-' + platform.id],
|
|
885
995
|
'aria-label': '展开或收起' + platform.name + '博主' },
|
|
886
|
-
h('span', { className: 'st-node-icon' }, platform.id === 'weibo' ? h('img', { className: 'st-weibo-logo', src: BASE + '/assets/weibo.ico', alt: '微博' }) : platform.id === 'xueqiu' ? h('img', { className: 'st-platform-logo', src: BASE + '/assets/xueqiu.png', alt: '雪球' }) : platform.id === 'x' ? '𝕏' : platform.name.slice(0, 1)), h('strong', null, platform.name),
|
|
996
|
+
h('span', { className: 'st-node-icon' }, platform.id === 'weibo' ? h('img', { className: 'st-weibo-logo', src: BASE + '/assets/weibo.ico', alt: '微博' }) : platform.id === 'douyin' ? h('img', { className: 'st-platform-logo', src: BASE + '/assets/douyin.ico', alt: '抖音' }) : platform.id === 'xueqiu' ? h('img', { className: 'st-platform-logo', src: BASE + '/assets/xueqiu.png', alt: '雪球' }) : platform.id === 'x' ? '𝕏' : platform.name.slice(0, 1)), h('strong', null, platform.name),
|
|
887
997
|
h('small', null, platform.targets.length, ' 位 · ', platform.count.toLocaleString(), ' 条已存'),
|
|
888
998
|
h('span', { className: 'st-platform-fold-mark', 'aria-hidden': true }, folds['source-' + platform.id] ? '›' : '⌄')),
|
|
889
999
|
h('button', { className: 'st-platform-tag st-platform-repair' + (!platformReady(state, platform.id) ? ' warn' : ''), onClick: () => navigate('settings', { platform: platform.id, section: 'account' }), 'aria-label': '查看' + platform.name + '平台' }, h('i'), statusText(platform.id)), toggle(platform.name, state.platforms?.find(source => source.id === platform.id)?.enabled, 'platform', { platform: platform.id }, state.platforms?.find(source => source.id === platform.id)?.enabled === false ? '平台已关闭' : '平台已开启')),
|
|
@@ -940,8 +1050,8 @@
|
|
|
940
1050
|
h('p', null, waitUntil > Date.now() ? platformNames[target.platform] + '要求暂缓访问,冷却至 ' + date(waitUntil) + ' 后可读取资料。' : '先确认昵称、头像和平台显示的消息总数,此步骤不会缓存正文。'),
|
|
941
1051
|
h('button', { disabled: busy || waitUntil > Date.now(), onClick: () => act('/targets/profile', { uid: target.uid, platform: target.platform, interactive: true }) }, '重新读取'))
|
|
942
1052
|
: h(React.Fragment, null,
|
|
943
|
-
h('p', { className: 'st-profile-total' }, '平台显示 ', h('strong', null, target.profile_total == null ? '总数暂不可用' : Number(target.profile_total).toLocaleString() + (target.platform === 'weibo' ? ' 条微博' : ' 条消息')),
|
|
944
|
-
h('small', { className: 'st-caption' }, ' · 包含原创与转发,实际可保存数量以账号可访问范围为准')),
|
|
1053
|
+
h('p', { className: 'st-profile-total' }, '平台显示 ', h('strong', null, target.profile_total == null ? '总数暂不可用' : Number(target.profile_total).toLocaleString() + (target.platform === 'douyin' ? ' 个作品' : target.platform === 'weibo' ? ' 条微博' : ' 条消息')),
|
|
1054
|
+
h('small', { className: 'st-caption' }, target.platform === 'douyin' ? ' · 包含视频与图集,实际保存范围以当前账号可见内容为准' : ' · 包含原创与转发,实际可保存数量以账号可访问范围为准')),
|
|
945
1055
|
h('p', { className: 'st-caption', style: { overflowWrap: 'anywhere' } }, '保存到:', state.storage?.documentsDirectory, '/', target.platform || 'weibo', '/', target.external_uid || target.uid, ' ', h('button', { className: 'st-link-button', onClick: () => navigate('settings', { section: 'workspace' }) }, '更改保存位置')),
|
|
946
1056
|
h('h3', null, '从什么时候开始保存?'),
|
|
947
1057
|
h('div', { className: 'st-capture-options' },
|
|
@@ -1003,7 +1113,7 @@
|
|
|
1003
1113
|
React.useEffect(() => { if (!active) return; const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, [active]);
|
|
1004
1114
|
const labels = { read_author_index: '查看博主资料索引', search_posts: '检索本地消息', read_posts: '读取消息原文', list_status: '查看监控状态', add_monitor: '添加博主', control_monitor: '调整监控开关', set_frequency: '修改监控频率', create_summary_task: '创建总结任务', control_summary_task: '调整总结任务', run_summary_task: '执行总结任务', validate_sources: '核对回答来源' };
|
|
1005
1115
|
const phases = { requesting: '连接模型', thinking: '模型正在思考', answering: '正在生成回答', preparing_tools: '准备查询与操作' };
|
|
1006
|
-
const status = { completed: '已完成', failed: '执行失败', interrupted: '执行中断', cancelled: '已停止', awaiting_confirmation: '等待你确认', executing: '正在应用变更', running: '正在工作' };
|
|
1116
|
+
const status = { partial: '回答未完', completed: '已完成', failed: '执行失败', interrupted: '执行中断', cancelled: '已停止', awaiting_confirmation: '等待你确认', executing: '正在应用变更', running: '正在工作' };
|
|
1007
1117
|
const seconds = Math.max(0, Math.floor(((active ? now : run.finished_at || run.created_at) - run.created_at) / 1000));
|
|
1008
1118
|
const row = (step, index) => {
|
|
1009
1119
|
const running = step.status === 'running' && active;
|
|
@@ -1070,7 +1180,7 @@
|
|
|
1070
1180
|
const title = data?.sessions.find(session => session.id === sessionId)?.title || '新对话';
|
|
1071
1181
|
const compose = h('form', { className: 'st-chat-compose', onSubmit: event => { event.preventDefault(); submit(); } },
|
|
1072
1182
|
h('textarea', { ref: composer, 'aria-label': '告诉 Social Trace 你想做什么', 'aria-describedby': 'st-chat-keyboard-help', onCompositionStart: () => { composing.current = true; }, onCompositionEnd: () => { composing.current = false; }, onKeyDown: event => { if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.repeat && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229 && !composing.current) { event.preventDefault(); submit(); } }, placeholder: '想了解谁的动态,或安排什么任务?', value: input, maxLength: 4000, onChange: event => changeInput(event.target.value) }),
|
|
1073
|
-
h('div', { className: 'st-chat-compose-footer' }, h('span', { className: 'st-caption' }, !data ? '正在连接模型服务…' : data.available ? 'DSH 默认模型 · 本地资料' : '请先在 DSH 配置默认模型'), h('button', { className: 'st-primary st-chat-send', 'aria-label': '发送', disabled: !canSend }, '↑')));
|
|
1183
|
+
h('div', { className: 'st-chat-compose-footer' }, h('span', { className: 'st-caption' }, !data ? '正在连接模型服务…' : data.available ? 'DSH 默认模型 · 本地资料' : data.runtime?.error || '请先在 DSH 配置默认模型'), h('button', { className: 'st-primary st-chat-send', 'aria-label': '发送', disabled: !canSend }, '↑')));
|
|
1074
1184
|
const keyboardHelp = h('p', { id: 'st-chat-keyboard-help', className: 'st-chat-disclosure' }, 'Enter 发送 · Shift + Enter 换行。修改监控前需你确认。');
|
|
1075
1185
|
return h('section', { className: 'st-chat' + (!data?.runs.length ? ' is-empty' : ''), 'aria-label': 'Social Trace 业务 Chat' },
|
|
1076
1186
|
h('header', { className: 'st-chat-toolbar', 'aria-label': '对话标题栏' }, h('div', { className: 'st-chat-title' }, navIcon('chat'), h('h1', { title: title }, title)), h('div', { className: 'st-chat-header-actions' },
|
|
@@ -1084,13 +1194,14 @@
|
|
|
1084
1194
|
h(ChatProgress, { run, pending, cancel: () => request('/chat/cancel', { id: run.id }) }),
|
|
1085
1195
|
run.error ? h('p', { className: 'st-chat-run-error', role: 'alert' }, run.error, run.answer ? '(已保留生成内容,供你核对)' : '') : null,
|
|
1086
1196
|
run.answer ? h(ChatMarkdown, { text: run.answer, sources: run.sources, streaming: run.status === 'running' }) : null,
|
|
1197
|
+
run.status === 'partial' ? h('div', { className: 'st-caption st-chat-partial', role: 'status' }, h('p', null, '回答暂未写完,以上正文已保留;可以从中断处继续,来源核对尚未完成。'), run.id === latest?.id ? h('button', { disabled: pending || !!data.busy, onClick: async () => { const draft = input; followLatest.current = true; await request('/chat/message', { message: '继续刚才未完成的回答,从中断处接着写', sessionId }); if (alive.current && draft) { setInput(draft); drafts.current.set(sessionId, draft); } } }, '继续回答') : null) : null,
|
|
1087
1198
|
run.action && run.status === 'awaiting_confirmation' ? h('div', { className: 'st-chat-confirm', 'aria-label': '待确认的业务变更' }, h('p', null, run.action.label), run.action.actions ? h('ol', null, run.action.actions.map((action, index) => h('li', { key: index }, action.label))) : null,
|
|
1088
1199
|
h('div', { className: 'st-actions' }, h('button', { className: 'st-primary', disabled: pending, onClick: () => request('/chat/confirm', { id: run.id }) }, '确认执行'),
|
|
1089
1200
|
h('button', { disabled: pending, onClick: () => request('/chat/cancel', { id: run.id }) }, '取消变更'))) : null,
|
|
1090
|
-
run.sources.length ? h('details', { className: 'st-chat-source-list' }, h('summary', null, '参考来源 · ', run.sources.length, ' 条'), h('div', { className: 'st-chat-sources' }, run.sources.map(source => /^https:\/\/(?:www\.)?(?:weibo\.com|xueqiu\.com)\//.test(source.url)
|
|
1201
|
+
run.sources.length ? h('details', { className: 'st-chat-source-list' }, h('summary', null, '参考来源 · ', run.sources.length, ' 条'), h('div', { className: 'st-chat-sources' }, run.sources.map(source => /^https:\/\/(?:www\.)?(?:weibo\.com|xueqiu\.com|douyin\.com)\//.test(source.url)
|
|
1091
1202
|
? h('a', { key: source.id, href: source.url, target: '_blank', rel: 'noreferrer' }, '[' + source.sourceId + '] ' + source.author + ' · ' + date(source.publishedAt))
|
|
1092
1203
|
: h('span', { key: source.id }, '[' + source.sourceId + '] ' + source.author)))) : null,
|
|
1093
|
-
run.sources.some(source => source.truncated) ? h('small', null, '
|
|
1204
|
+
run.sources.some(source => source.truncated) ? h('small', null, '部分较长内容按节选分析,参考来源中保留了原文链接。') : null,
|
|
1094
1205
|
null)), h('div', { ref: bottom })),
|
|
1095
1206
|
data?.busy && !data.runs.some(run => run.id === data.busy) ? h('p', { role: 'status' }, '后台正在处理另一项任务,请稍候。') : null,
|
|
1096
1207
|
!data?.runs.length ? compose : null,
|
|
@@ -1154,7 +1265,7 @@
|
|
|
1154
1265
|
const navigate = (next, options = {}) => {
|
|
1155
1266
|
if (next === 'tasks') {
|
|
1156
1267
|
const owner = state?.targets.find(item => item.uid === (options.uid || uid));
|
|
1157
|
-
const problem = state?.jobs.find(job => job.uid === owner?.uid && job.status === 'needs_review');
|
|
1268
|
+
const problem = state?.jobs.find(job => job.uid === owner?.uid && (job.status === 'needs_review' || job.contentIssue));
|
|
1158
1269
|
next = 'archive'; options = { ...options, uid: owner?.uid || '', view: problem?.kind === 'updates' ? 'frequency' : 'history' };
|
|
1159
1270
|
}
|
|
1160
1271
|
if (next === 'settings' && options.section === 'frequency') {
|
|
@@ -1210,7 +1321,7 @@
|
|
|
1210
1321
|
.then(result => { if (active) { setPosts(result.items); setPostsLoading(false); } })
|
|
1211
1322
|
.catch(err => { if (active) { setError(err.message); setErrorSection('service'); setPostsLoading(false); } });
|
|
1212
1323
|
return () => { active = false; };
|
|
1213
|
-
}, [tab, uid, readerTab, query, postKind, offset, focusedPost, state?.count, latestSavedAt, state?.targets.find(item => item.uid === uid)?.capture_mode]);
|
|
1324
|
+
}, [tab, uid, readerTab, query, postKind, offset, focusedPost, state?.count, latestSavedAt, state?.douyin?.revision, state?.targets.find(item => item.uid === uid)?.capture_mode]);
|
|
1214
1325
|
const act = async (path, body = {}) => {
|
|
1215
1326
|
stateEpoch.current++;
|
|
1216
1327
|
setBusy(true); setError(''); setSavedMessage('');
|
|
@@ -1223,7 +1334,8 @@
|
|
|
1223
1334
|
if (added) navigate('archive', { uid: added.uid, platform: added.platform });
|
|
1224
1335
|
setSavedMessage('已添加博主,正在读取昵称与头像;监控尚未开启');
|
|
1225
1336
|
}
|
|
1226
|
-
if (
|
|
1337
|
+
if (path === '/jobs/skip-content') setSavedMessage((state.targets.find(item => item.uid === state.jobs.find(job => job.id === body.id)?.uid)?.name || '所选博主') + ':已保留片段和链接,继续缓存其他内容');
|
|
1338
|
+
else if (/^\/jobs\//.test(path)) {
|
|
1227
1339
|
const job = state.jobs.find(item => path.includes(item.id));
|
|
1228
1340
|
setSavedMessage((state.targets.find(item => item.uid === job?.uid)?.name || '所选博主') + ':' + (job?.kind === 'history' ? '历史缓存' : '更新检查') + (path.endsWith('/pause') ? '已暂停,已保存内容和断点保留' : '已开启;账号未就绪或上级开关暂停时会等待'));
|
|
1229
1341
|
}
|
|
@@ -1364,7 +1476,7 @@
|
|
|
1364
1476
|
!showPreview ? button('+ 添加博主', () => navigate('settings', { section: 'add-target' })) : null,
|
|
1365
1477
|
(showPreview || ['blocked', 'partial'].includes(effective.kind) || state.enabled === false || effectiveSource?.enabled === false || effective.section === 'archive') ? button(showPreview ? '开始配置 →' : effective.kind === 'partial' ? '查看异常' : effective.kind === 'blocked' ? effective.section === 'tasks' ? '检查任务' : effectiveAccount?.status === 'needs_login' ? '登录' + effectiveName : effectiveAccount?.status === 'needs_verification' ? '完成验证' : '恢复连接' : state.enabled === false ? '开启总控' : effectiveSource?.enabled === false ? '开启平台监控' : effective.section === 'archive' ? '选择监控对象' : '查看博主',
|
|
1366
1478
|
() => showPreview ? navigate('settings', { section: 'account' }) : state.enabled === false ? act('/monitoring', { scope: 'all', enabled: true })
|
|
1367
|
-
: effectiveSource?.enabled === false ? act('/monitoring', { scope: 'platform', platform: effectivePlatform, enabled: true }) : ['blocked', 'partial'].includes(effective.kind) && effective.section !== 'tasks' ? navigate('settings', { platform: effectivePlatform, section: effective.section }) : effective.section === 'archive' ? navigate('archive', { uid: '' }) : navigate('tasks', { uid: state.jobs.find(job => job.status === 'needs_review' && state.targets.find(target => target.uid === job.uid)?.platform === effectivePlatform)?.uid || uid }), 'st-primary') : null)),
|
|
1479
|
+
: effectiveSource?.enabled === false ? act('/monitoring', { scope: 'platform', platform: effectivePlatform, enabled: true }) : ['blocked', 'partial'].includes(effective.kind) && effective.section !== 'tasks' ? navigate('settings', { platform: effectivePlatform, section: effective.section }) : effective.section === 'archive' ? navigate('archive', { uid: '' }) : navigate('tasks', { uid: effective.uid || state.jobs.find(job => job.status === 'needs_review' && state.targets.find(target => target.uid === job.uid)?.platform === effectivePlatform)?.uid || uid }), 'st-primary') : null)),
|
|
1368
1480
|
h(FlowOverview, { key: showPreview ? 'preview' : 'live', state: showPreview ? sample : state,
|
|
1369
1481
|
uid: showPreview ? '' : uid, preview: showPreview,
|
|
1370
1482
|
navigate, download, busy,
|
|
@@ -1383,7 +1495,7 @@
|
|
|
1383
1495
|
context: { scope: 'target', uid: target.uid, platform: target.platform, tab: readerTab }, state, act, busy, error, navigate, openSettings: openNodeSettings, close: () => setReaderTab('content') });
|
|
1384
1496
|
else if (tab === 'archive') content = h(React.Fragment, null,
|
|
1385
1497
|
focusedPost ? h('div', { className: 'st-message-toolbar' }, h('span', { className: 'st-caption' }, '提醒对应的动态'), button('查看这位博主的全部内容', () => navigate('archive', { uid }))) : h(React.Fragment, null,
|
|
1386
|
-
h('div', { className: 'st-feed-filters' }, [['all', '全部'], ['original', '原创 ' + originals], ['repost', '转发 ' + reposts]].map(([key, title]) => h('button', { key, type: 'button', 'aria-pressed': postKind === key,
|
|
1498
|
+
h('div', { className: 'st-feed-filters' }, (target?.platform === 'douyin' ? [['all', '全部'], ['video', '视频 ' + (state.douyin?.counts?.find(row => row.uid === uid)?.videos || 0)], ['images', '图集 ' + (state.douyin?.counts?.find(row => row.uid === uid)?.images || 0)]] : [['all', '全部'], ['original', '原创 ' + originals], ['repost', '转发 ' + reposts]]).map(([key, title]) => h('button', { key, type: 'button', 'aria-pressed': postKind === key,
|
|
1387
1499
|
onClick: () => { setPostKind(key); setOffset(0); } }, title))),
|
|
1388
1500
|
h('div', { className: 'st-toolbar' }, h('input', { 'aria-label': '搜索留存正文', placeholder: '搜索已留存的文字…', value: query,
|
|
1389
1501
|
onChange: event => { setQuery(event.target.value); setOffset(0); } }),
|
|
@@ -1393,9 +1505,10 @@
|
|
|
1393
1505
|
onClick: event => { event.preventDefault(); void download('jsonl'); } }, 'JSONL'))),
|
|
1394
1506
|
postsLoading ? h('div', { className: 'st-empty', role: 'status' }, '正在读取本地档案…') : posts.length ? posts.map(post => h('article', { className: 'st-card', key: post.id },
|
|
1395
1507
|
h('div', { className: 'st-card-head' }, h('div', null, h('h3', null, post.author), h(PostTimes, { publishedAt: post.published_at, detectedAt: post.detected_at, savedAt: post.first_seen_at, kind: post.capture_kind })),
|
|
1396
|
-
h('span', { className: 'st-badge' + (post.complete ? '' : ' warn'), title: '首次保存于 ' + date(post.first_seen_at) }, post.article ? '文章链接' : post.complete ? post.quote ? '转发' : '原创' : '正文待补齐')),
|
|
1508
|
+
h('span', { className: 'st-badge' + (post.complete ? '' : ' warn'), title: '首次保存于 ' + date(post.first_seen_at) }, post.video ? post.video.imagePost ? '图集' : '视频作品' : post.article ? '文章链接' : post.complete ? post.quote ? '转发' : '原创' : '正文待补齐')),
|
|
1397
1509
|
post.images?.length && post.text.trim() === '分享图片' || post.quote && ['转发微博', '快转微博'].includes(post.text.trim()) ? null : h('div', { className: 'st-text' }, post.article ? h('a', { href: post.url, target: '_blank', rel: 'noreferrer' }, post.text.split('\n\n')[0] + ' ↗') : post.text),
|
|
1398
1510
|
h(ImageGallery, { post, images: post.images }), h(QuoteBlock, { post }),
|
|
1511
|
+
post.video ? h(VideoContent, { post, act, busy }) : null,
|
|
1399
1512
|
!post.article ? h('div', { className: 'st-card-head st-caption st-post-footer' }, h('a', { href: post.url, target: '_blank', rel: 'noreferrer' }, '查看原文 ↗')) : null))
|
|
1400
1513
|
: h('div', { className: 'st-empty' }, h('h3', null, focusedPost ? '这条动态暂不可用' : query ? '没有找到匹配内容' : state.targets.length ? '还没有留存内容' : '添加第一位博主'),
|
|
1401
1514
|
h('p', null, focusedPost ? '可以查看这位博主的其他留存内容。' : query ? '试试其他关键词,或清空搜索。' : postKind !== 'all' ? '这个分类暂无消息,可以切换到全部查看。' : state.targets.length ? target && platformReady(state, target.platform || 'weibo') && streamStatus(state, target, 'history') === '未开启' ? '这里只保存开启监控后发布的新动态。需要以前的消息,可在「历史消息缓存」中开启。' : effective.reason : '添加博主后,动态和历史内容会逐步留存在本机。'),
|
|
@@ -1417,11 +1530,11 @@
|
|
|
1417
1530
|
!state.notices.some(item => noticeKind === 'all' || item.kind === noticeKind) ? h('div', { className: 'st-empty' }, unreadNotices(state, noticeKind) ? '最近提醒中没有此类消息,未读数量包含更早的提醒。' : '暂无此类消息') : null,
|
|
1418
1531
|
state.notices.length === 100 ? h('p', { className: 'st-caption' }, '显示最近 100 条提醒,未读数量统计全部提醒;完整正文可在关注与档案查看。') : null);
|
|
1419
1532
|
else content = h('div', { className: 'st-settings-page' },
|
|
1420
|
-
h('div', { className: 'st-platform-tabs', role: 'tablist', 'aria-label': '平台配置' }, [['weibo', '微博'], ['xueqiu', '雪球'], ['x', 'X']].map(([id, title]) => h('button', { key: id, role: 'tab', 'aria-selected': platform === id, onClick: () => { setPlatform(id); setFocusSection(''); setError(''); setSavedMessage(''); } }, title))),
|
|
1421
|
-
!['weibo', 'xueqiu'].includes(platform) ? h('div', { className: 'st-development' }, h('h3', null, platformName + ' · 开发中'), h('p', null, '暂未开放配置')) : h(React.Fragment, null,
|
|
1533
|
+
h('div', { className: 'st-platform-tabs', role: 'tablist', 'aria-label': '平台配置' }, [['weibo', '微博'], ['xueqiu', '雪球'], ['douyin', '抖音'], ['x', 'X']].map(([id, title]) => h('button', { key: id, role: 'tab', 'aria-selected': platform === id, onClick: () => { setPlatform(id); setFocusSection(''); setError(''); setSavedMessage(''); } }, title))),
|
|
1534
|
+
!['weibo', 'xueqiu', 'douyin'].includes(platform) ? h('div', { className: 'st-development' }, h('h3', null, platformName + ' · 开发中'), h('p', null, '暂未开放配置')) : h(React.Fragment, null,
|
|
1422
1535
|
h('section', { id: 'st-account', tabIndex: -1, 'aria-label': platformName + '账号连接', className: 'st-card st-connection' + (focusSection === 'account' ? ' st-focused' : '') },
|
|
1423
1536
|
h(SettingRow, { label: platformName + '账号', description: state.platforms?.find(source => source.id === platform)?.connectionRecovery ? '连接暂时不可用,后台会自动检查恢复,无需反复操作' : loginOpened ? '保持登录窗口打开,完成后确认连接' : hasAuthorization ? account?.status === 'ready' ? '用于读取关注博主的内容' : '授权已保留,可检查连接状态' : '登录后才能监控与缓存消息' },
|
|
1424
|
-
h('div', { className: 'st-account-controls' }, h('span', { className: 'st-connection-state ' + accountState }, state.platforms?.find(source => source.id === platform)?.connectionRecovery ? '自动恢复中' : hasAuthorization ? account?.status === 'ready' ? '已登录' : '授权已保存' : loginOpened ? '等待确认' : labels[accountState] || '未连接'),
|
|
1537
|
+
h('div', { className: 'st-account-controls' }, h('span', { className: 'st-connection-state ' + accountState }, state.platforms?.find(source => source.id === platform)?.connectionRecovery ? '自动恢复中' : hasAuthorization ? account?.status === 'ready' && platform !== 'douyin' ? '已登录' : '授权已保存' : loginOpened ? '等待确认' : labels[accountState] || '未连接'),
|
|
1425
1538
|
!hasAuthorization ? button(loginOpened ? '我已登录,继续' : '登录' + platformName, () => act(loginOpened ? '/verify' : '/login'), 'st-primary') : null,
|
|
1426
1539
|
button('检查连接', () => act('/verify')))),
|
|
1427
1540
|
error && errorSection === 'account' ? h('p', { className: 'st-inline-error', role: 'alert' }, error) : !loginOpened && accountState !== 'ready' && account?.reason && !/请先登录|请在.*登录|请在.*验证/.test(account.reason) ? h('p', { className: 'st-caption' }, account.reason) : null),
|
|
@@ -1436,6 +1549,7 @@
|
|
|
1436
1549
|
state.storage?.mediaError ? h(SettingRow, { label: '图片备份', description: '部分图片尚未保存,正文已保留。可以重试保存图片。' }, button('重试图片备份', () => act('/workspace/sync', { images: true }))) : null,
|
|
1437
1550
|
state.storage?.error ? h(SettingRow, { label: '文档保存', description: '本地文档尚未同步完成,请重试;已采集的内容仍保留在资料库中。' }, button('重试文档同步', () => act('/workspace/sync'))) : null,
|
|
1438
1551
|
h('details', { className: 'st-setting-help' }, h('summary', null, '更换保存位置说明'), h('p', null, '已有内容会复制到新位置,原文件保留。平台授权独立保存。'))),
|
|
1552
|
+
platform === 'douyin' ? h('section', { className: 'st-card' }, h('h2', null, '抖音默认配置'), h('p', { className: 'st-caption' }, '授权已保存后,实际访问情况以作品检查结果为准。'), h(DouyinConfigForm, { state, act, busy })) : null,
|
|
1439
1553
|
h('section', { className: 'st-settings-im', 'aria-label': 'dsh-im 消息推送' }, h('h2', null, '消息推送'), h(IMSummary, { agent: state.agent }), button('配置推送渠道', () => { setAgentTab('settings'); setAgentOpen(true); })),
|
|
1440
1554
|
h('p', { className: 'st-settings-footer' }, '电脑关闭或插件退出时停止采集;重新运行后会补查期间仍可见的动态。')));
|
|
1441
1555
|
|
|
@@ -1465,11 +1579,11 @@
|
|
|
1465
1579
|
h('main', { className: 'st-main' + (tab === 'chat' ? ' st-chat-main' : ''), ref: main },
|
|
1466
1580
|
h('header', { className: 'st-head' }, h('div', { className: 'st-page-title' }, tab === 'archive' && uid ? h('button', { className: 'st-back', 'aria-label': '返回博主列表', title: '返回博主列表', onClick: () => navigate('archive', { uid: '' }) }, h('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, h('path', { d: 'm15 6-6 6 6 6' }))) : null, h('h1', null, titles[tab])),
|
|
1467
1581
|
h('div', { className: 'st-head-tools' },
|
|
1468
|
-
tab === 'overview' ? h('button', { className: 'st-flow-status ' + (showPreview ? 'preview' : effective.kind), title: effective.reason, onClick: () => effective.section === 'tasks' ? navigate('tasks', { uid: state.jobs.find(job => job.status === 'needs_review' && state.targets.find(target => target.uid === job.uid)?.platform === effectivePlatform)?.uid || '' }) : navigate('settings', { platform: effectivePlatform, section: effective.section }) }, h('i'), showPreview ? '未配置 · 效果预览' : effective.kind === 'ready' ? '监控中' : effective.label)
|
|
1582
|
+
tab === 'overview' ? h('button', { className: 'st-flow-status ' + (showPreview ? 'preview' : effective.kind), title: effective.reason, onClick: () => effective.section === 'tasks' ? navigate('tasks', { uid: effective.uid || state.jobs.find(job => job.status === 'needs_review' && state.targets.find(target => target.uid === job.uid)?.platform === effectivePlatform)?.uid || '' }) : navigate('settings', { platform: effectivePlatform, section: effective.section }) }, h('i'), showPreview ? '未配置 · 效果预览' : effective.kind === 'ready' ? '监控中' : effective.label)
|
|
1469
1583
|
: null,
|
|
1470
1584
|
tab === 'archive' ? button('+ 添加博主', () => navigate('settings', { section: 'add-target' }), 'st-primary') : null,
|
|
1471
1585
|
null)),
|
|
1472
|
-
firstUse && ['overview', 'settings'].includes(tab) && ['weibo', 'xueqiu'].includes(platform) ? h('ol', { className: 'st-setup-steps', 'aria-label': '首次使用流程' }, ['确认保存位置', '连接' + platformName, '添加博主', '选择记录方式'].map((text, i) => h('li', { key: text, 'aria-current': (state.storage?.workspaceConfigured === false ? i === 0 : accountState !== 'ready' ? i === 1 : !state.targets.length ? i === 2 : i === 3) ? 'step' : undefined },
|
|
1586
|
+
firstUse && ['overview', 'settings'].includes(tab) && ['weibo', 'xueqiu', 'douyin'].includes(platform) ? h('ol', { className: 'st-setup-steps', 'aria-label': '首次使用流程' }, ['确认保存位置', '连接' + platformName, '添加博主', '选择记录方式'].map((text, i) => h('li', { key: text, 'aria-current': (state.storage?.workspaceConfigured === false ? i === 0 : accountState !== 'ready' ? i === 1 : !state.targets.length ? i === 2 : i === 3) ? 'step' : undefined },
|
|
1473
1587
|
button(text, () => i === 0 ? navigate('settings', { section: 'workspace' }) : i === 1 ? navigate('settings', { section: 'account' }) : i === 2 || !state.targets.length ? navigate('settings', { section: 'add-target' }) : navigate('archive', { uid: state.targets.find(item => item.capture_mode === 'pending')?.uid || '' }), 'st-link-button')))) : null,
|
|
1474
1588
|
savedMessage ? h('div', { className: 'st-saved', role: 'status' }, h('span', null, savedMessage), h('small', null, '4 秒后自动关闭'), h('button', { 'aria-label': '关闭操作提醒', onClick: () => setSavedMessage('') }, '×')) : null,
|
|
1475
1589
|
error && !agentOpen && !nodeSettings && !(tab === 'archive' && uid && readerTab !== 'content') && !adding && (tab !== 'settings' || ['service', 'history'].includes(errorSection)) ? h('div', { className: 'st-error', role: 'alert' }, error, ' ', button(errorSection === 'service' ? '重试连接' : errorSection === 'workspace' ? '查看保存位置' : errorSection === 'frequency' ? '查看频率设置' : errorSection === 'history' ? '检查缓存日期' : '处理' + platformName + '连接', () => errorSection === 'service' ? reload().then(() => setError('')).catch(err => setError(err.message)) : errorSection === 'history' ? navigate('archive', { uid }) : navigate('settings', { platform, section: errorSection })), ' ', button('关闭', () => setError(''))) : null,
|
|
@@ -1477,10 +1591,10 @@
|
|
|
1477
1591
|
' ', button('重试启动后台', () => act('/service/retry'))) : null,
|
|
1478
1592
|
state && tab === 'archive' ? h('section', { className: 'st-identity' }, target ? h(AuthorAvatar, { target }) : null,
|
|
1479
1593
|
h('select', { 'aria-label': '筛选博主', value: uid, onChange: event => navigate(tab, { uid: event.target.value }) }, h('option', { value: '' }, '全部博主'), state.targets.map(item => h('option', { key: item.uid, value: item.uid }, item.name))),
|
|
1480
|
-
target ? h(React.Fragment, null, h('span', { className: 'st-meta-tag st-platform-badge' }, platformNames[target.platform || 'weibo']), h('span', { className: 'st-meta-tag st-uid-tag' }, 'UID ' + (target.external_uid || target.uid))) : null,
|
|
1594
|
+
target ? h(React.Fragment, null, h('span', { className: 'st-meta-tag st-platform-badge' }, platformNames[target.platform || 'weibo']), h('span', { className: 'st-meta-tag st-uid-tag', title: 'UID ' + (target.external_uid || target.uid) }, 'UID ' + shortUid(target.external_uid || target.uid))) : null,
|
|
1481
1595
|
h('div', { className: 'st-identity-actions' },
|
|
1482
1596
|
target ? h('details', { className: 'st-author-more' }, h('summary', null, '更多'), h('div', null,
|
|
1483
|
-
h('a', { href: (target.platform === 'xueqiu' ? 'https://xueqiu.com/u/' : 'https://weibo.com/u/') + (target.external_uid || target.uid), target: '_blank', rel: 'noreferrer' }, platformNames[target.platform || 'weibo'] + '主页 ↗'),
|
|
1597
|
+
h('a', { href: (target.platform === 'douyin' ? 'https://www.douyin.com/user/' : target.platform === 'xueqiu' ? 'https://xueqiu.com/u/' : 'https://weibo.com/u/') + (target.external_uid || target.uid), target: '_blank', rel: 'noreferrer' }, platformNames[target.platform || 'weibo'] + '主页 ↗'),
|
|
1484
1598
|
button('刷新昵称与头像', () => act('/targets/profile', { uid: target.uid, platform: target.platform, interactive: true })))) : null)) : null,
|
|
1485
1599
|
tab === 'archive' && target ? h('div', { className: 'st-reader-tabs', role: 'tablist', 'aria-label': '博主详情' }, [['content', '留存内容'], ['frequency', '新消息监控'], ['history', '历史消息缓存']].map(([key, label]) => h('button', { key, id: 'st-reader-tab-' + key, role: 'tab', 'aria-controls': key === 'content' ? 'st-reader-content' : 'st-node-panel-' + key, 'aria-selected': readerTab === key, onClick: () => { setReaderTab(key); setError(''); setSavedMessage(''); } }, label))) : null,
|
|
1486
1600
|
tab === 'archive' && target && readerTab === 'content' ? h('section', { id: 'st-reader-content', role: 'tabpanel', 'aria-labelledby': 'st-reader-tab-content' },
|