feat: tighten desktop presence loop and refine admin-web UX
Desktop Client Build / Resolve Build Metadata (push) Successful in 43s
Frontend CI / Frontend (push) Successful in 3m46s
Backend CI / Backend (push) Failing after 7m52s
Desktop Client Build / Build Desktop Client (push) Successful in 27m13s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 59s
Desktop Client Build / Resolve Build Metadata (push) Successful in 43s
Frontend CI / Frontend (push) Successful in 3m46s
Backend CI / Backend (push) Failing after 7m52s
Desktop Client Build / Build Desktop Client (push) Successful in 27m13s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 59s
- Shrink desktop presence TTL to 30s and heartbeat to 15s so unexpected client exits surface within one TTL even if the explicit offline call is missed; keep migration default aligned. - Trust a known presence miss in resolveDesktopClientOnline instead of falling back to the recent-heartbeat heuristic, so a still-cached LastSeenAt cannot mask an offline client. - Release the runtime session before clearing renderer desktop sessions on shutdown to avoid leaving stale leases behind. - Auto-refresh the MediaView account list every 5s and revamp card layout (inline platform badge, status tags, UID row, meta box). - Let the brand-question AI wizard accept multiple seed topics via a tag-style input; candidates from each topic are merged and deduped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1403,6 +1403,9 @@ const enUS = {
|
||||
ai: {
|
||||
note: 'AI expansion consumes 1 AI point. Failed generations are refunded automatically.',
|
||||
seedTopic: 'Seed topic',
|
||||
seedTopicPlaceholder: 'Enter a topic and press Enter; multiple topics supported',
|
||||
seedTopicHint:
|
||||
'Use Enter, commas, or semicolons to add multiple topics. Candidates are merged and deduplicated automatically.',
|
||||
generate: 'Generate with AI',
|
||||
waitingTitle: 'AI is generating candidate questions. Please wait...',
|
||||
waitingStages: {
|
||||
|
||||
@@ -1331,6 +1331,8 @@ const zhCN = {
|
||||
ai: {
|
||||
note: "AI 扩展会消耗 1 个 AI 点;生成失败时系统会自动退还。",
|
||||
seedTopic: "扩展主题",
|
||||
seedTopicPlaceholder: "输入主题后按回车,可添加多个",
|
||||
seedTopicHint: "支持回车、逗号或分号分隔多个主题;系统会逐个扩展并自动合并去重。",
|
||||
generate: "AI 生成候选",
|
||||
waitingTitle: "AI 正在生成候选问题,请稍后…",
|
||||
waitingStages: {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
BulbOutlined,
|
||||
LeftOutlined,
|
||||
RobotOutlined,
|
||||
SaveOutlined,
|
||||
@@ -70,9 +69,11 @@ const expansionForm = reactive({
|
||||
})
|
||||
|
||||
const aiForm = reactive({
|
||||
seed_topic: '',
|
||||
seed_topics: [] as string[],
|
||||
})
|
||||
|
||||
const aiTopicTokenSeparators = [',', ',', ';', ';', '\n']
|
||||
|
||||
const batchForm = reactive({
|
||||
text: '',
|
||||
})
|
||||
@@ -241,10 +242,17 @@ const previewMutations = {
|
||||
},
|
||||
}),
|
||||
ai: useMutation({
|
||||
mutationFn: () =>
|
||||
brandsApi.distillQuestions(brandId.value, {
|
||||
seed_topic: aiForm.seed_topic.trim(),
|
||||
}),
|
||||
mutationFn: async () => {
|
||||
const topics = normalizedAiSeedTopics.value
|
||||
const results = await Promise.all(
|
||||
topics.map((topic) =>
|
||||
brandsApi.distillQuestions(brandId.value, {
|
||||
seed_topic: topic,
|
||||
}),
|
||||
),
|
||||
)
|
||||
return mergeQuestionCandidateResults(results)
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
candidateRows.value = toCandidateRows(result.candidates)
|
||||
if (result.cache_hit) {
|
||||
@@ -333,6 +341,81 @@ function splitLines(value: string | string[]): string[] {
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function normalizedTopicParts(value: string): string[] {
|
||||
return value
|
||||
.split(/\r?\n|,|,|;|;/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const normalizedAiSeedTopics = computed(() => {
|
||||
const seen = new Set<string>()
|
||||
const topics: string[] = []
|
||||
for (const rawTopic of aiForm.seed_topics) {
|
||||
for (const topic of normalizedTopicParts(rawTopic)) {
|
||||
const key = topic.toLowerCase()
|
||||
if (seen.has(key)) {
|
||||
continue
|
||||
}
|
||||
seen.add(key)
|
||||
topics.push(topic)
|
||||
}
|
||||
}
|
||||
return topics
|
||||
})
|
||||
|
||||
function updateAiSeedTopics(value: string[]): void {
|
||||
aiForm.seed_topics.splice(0, aiForm.seed_topics.length, ...dedupeTopicParts(value))
|
||||
}
|
||||
|
||||
function dedupeTopicParts(values: string[]): string[] {
|
||||
const seen = new Set<string>()
|
||||
const topics: string[] = []
|
||||
for (const value of values) {
|
||||
for (const topic of normalizedTopicParts(value)) {
|
||||
const key = topic.toLowerCase()
|
||||
if (seen.has(key)) {
|
||||
continue
|
||||
}
|
||||
seen.add(key)
|
||||
topics.push(topic)
|
||||
}
|
||||
}
|
||||
return topics
|
||||
}
|
||||
|
||||
function mergeQuestionCandidateResults(
|
||||
results: Awaited<ReturnType<typeof brandsApi.distillQuestions>>[],
|
||||
) {
|
||||
const seen = new Set<string>()
|
||||
const candidates: QuestionCandidate[] = []
|
||||
let aiPointsCharged = 0
|
||||
let cacheHit = false
|
||||
let truncated = false
|
||||
|
||||
for (const result of results) {
|
||||
aiPointsCharged += result.ai_points_charged ?? 0
|
||||
cacheHit = cacheHit || Boolean(result.cache_hit)
|
||||
truncated = truncated || Boolean(result.truncated)
|
||||
|
||||
for (const candidate of result.candidates) {
|
||||
const key = normalizeQuestionKey(candidate.text)
|
||||
if (!key || seen.has(key)) {
|
||||
continue
|
||||
}
|
||||
seen.add(key)
|
||||
candidates.push(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
candidates,
|
||||
ai_points_charged: aiPointsCharged || undefined,
|
||||
cache_hit: cacheHit,
|
||||
truncated,
|
||||
}
|
||||
}
|
||||
|
||||
type ColumnKey = 'region' | 'prefix' | 'core' | 'industry' | 'suffix'
|
||||
|
||||
function handleInput(column: ColumnKey, index: number) {
|
||||
@@ -356,7 +439,9 @@ function handleEnter(column: ColumnKey, index: number) {
|
||||
arr.splice(index + 1, 0, '')
|
||||
}
|
||||
setTimeout(() => {
|
||||
const inputs = document.querySelectorAll(`.word-column[data-col="${column}"] .word-column__input`) as NodeListOf<HTMLInputElement>
|
||||
const inputs = document.querySelectorAll(
|
||||
`.word-column[data-col="${column}"] .word-column__input`,
|
||||
) as NodeListOf<HTMLInputElement>
|
||||
if (inputs[index + 1]) {
|
||||
inputs[index + 1].focus()
|
||||
}
|
||||
@@ -369,7 +454,9 @@ function handleBackspace(column: ColumnKey, index: number, event: KeyboardEvent)
|
||||
event.preventDefault()
|
||||
arr.splice(index, 1)
|
||||
setTimeout(() => {
|
||||
const inputs = document.querySelectorAll(`.word-column[data-col="${column}"] .word-column__input`) as NodeListOf<HTMLInputElement>
|
||||
const inputs = document.querySelectorAll(
|
||||
`.word-column[data-col="${column}"] .word-column__input`,
|
||||
) as NodeListOf<HTMLInputElement>
|
||||
const target = inputs[index - 1 >= 0 ? index - 1 : 0]
|
||||
if (target) {
|
||||
target.focus()
|
||||
@@ -552,7 +639,7 @@ async function generateCandidates(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
if (!aiForm.seed_topic.trim()) {
|
||||
if (!normalizedAiSeedTopics.value.length) {
|
||||
pageNotice.value = t('brands.questions.errors.emptyInput')
|
||||
return
|
||||
}
|
||||
@@ -835,18 +922,20 @@ function backToBrands(): void {
|
||||
</div>
|
||||
|
||||
<div v-else-if="createMode === 'ai'" class="wizard-card">
|
||||
<div class="wizard-ai-banner">
|
||||
<div class="wizard-ai-banner__info">
|
||||
<RobotOutlined class="wizard-ai-banner__icon" />
|
||||
<div class="wizard-ai-banner__text">
|
||||
<h4>{{ t('brands.questions.ai.generate') }}</h4>
|
||||
<p>{{ t('brands.questions.ai.note') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-item field-item--compact">
|
||||
<label>{{ t('brands.questions.ai.seedTopic') }}</label>
|
||||
<a-input v-model:value="aiForm.seed_topic" />
|
||||
<a-select
|
||||
class="seed-topic-select"
|
||||
mode="tags"
|
||||
:value="aiForm.seed_topics"
|
||||
:placeholder="t('brands.questions.ai.seedTopicPlaceholder')"
|
||||
:token-separators="aiTopicTokenSeparators"
|
||||
:options="[]"
|
||||
@change="(value: string[]) => updateAiSeedTopics(value)"
|
||||
/>
|
||||
<p class="field-item__hint">
|
||||
{{ t('brands.questions.ai.seedTopicHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1167,6 +1256,39 @@ function backToBrands(): void {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.field-item__hint {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.seed-topic-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.seed-topic-select :deep(.ant-select-selector) {
|
||||
min-height: 52px;
|
||||
padding: 8px 12px;
|
||||
border-color: #d9d9d9 !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.seed-topic-select :deep(.ant-select-selection-overflow) {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.seed-topic-select :deep(.ant-select-selection-item) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
min-height: 28px;
|
||||
color: #1e293b;
|
||||
background: #f1f5f9;
|
||||
border-color: #dbe3ee;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tool-header-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1359,41 +1481,6 @@ function backToBrands(): void {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.wizard-ai-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, rgba(22, 119, 255, 0.04), rgba(22, 119, 255, 0.08));
|
||||
border: 1px solid rgba(22, 119, 255, 0.15);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(22, 119, 255, 0.05);
|
||||
}
|
||||
|
||||
.wizard-ai-banner__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.wizard-ai-banner__icon {
|
||||
font-size: 28px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.wizard-ai-banner__text h4 {
|
||||
margin: 0 0 4px;
|
||||
color: #1f2f4d;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wizard-ai-banner__text p {
|
||||
margin: 0;
|
||||
color: #6b7a99;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.candidate-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
|
||||
@@ -67,6 +67,9 @@ const platformsQuery = useQuery({
|
||||
const accountsQuery = useQuery({
|
||||
queryKey: ['tenant', 'desktop-accounts', 'media-view'],
|
||||
queryFn: () => tenantAccountsApi.list(),
|
||||
staleTime: 0,
|
||||
refetchInterval: 5_000,
|
||||
refetchOnWindowFocus: true,
|
||||
})
|
||||
|
||||
const loading = computed(() => platformsQuery.isPending.value || accountsQuery.isPending.value)
|
||||
@@ -374,57 +377,47 @@ async function refreshAll(): Promise<void> {
|
||||
</span>
|
||||
|
||||
<div class="media-card__identity-copy">
|
||||
<h3>{{ account.displayName }}</h3>
|
||||
<p>{{ account.platformLabel }}</p>
|
||||
<h3>
|
||||
{{ account.displayName }}
|
||||
<img
|
||||
v-if="account.platformLogoUrl"
|
||||
:src="account.platformLogoUrl"
|
||||
:alt="account.platformLabel"
|
||||
class="media-card__inline-platform-icon"
|
||||
/>
|
||||
<span v-else class="media-card__inline-platform-text" :style="{ color: account.platformAccent }">{{ account.platformShortName }}</span>
|
||||
</h3>
|
||||
<p>
|
||||
<span class="media-card__platform-name">{{ account.platformLabel }}</span>
|
||||
<span class="media-card__uid-divider">|</span>
|
||||
<span class="media-card__uid">UID: {{ account.platformUid }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media-card__platform-badge" :style="{ color: account.platformAccent }">
|
||||
<img
|
||||
v-if="account.platformLogoUrl"
|
||||
:src="account.platformLogoUrl"
|
||||
:alt="account.platformLabel"
|
||||
/>
|
||||
<span v-else>{{ account.platformShortName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="media-card__meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">平台 UID</span>
|
||||
<span class="meta-value">{{ account.platformUid }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">授权状态</span>
|
||||
<a-tag :color="healthColor(account.health)">
|
||||
{{ healthLabel(account.health) }}
|
||||
<div class="media-card__tags">
|
||||
<a-tag :color="healthColor(account.health)">
|
||||
{{ healthLabel(account.health) }}
|
||||
</a-tag>
|
||||
<a-tag :color="publishStateColor(account.publishState)">
|
||||
{{ publishStateLabel(account.publishState) }}
|
||||
</a-tag>
|
||||
<a-tooltip :title="clientStatusText(account)" placement="top">
|
||||
<a-tag :color="clientStatusColor(account)" class="client-status-tag">
|
||||
{{ clientStatusText(account) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="media-card__meta-box">
|
||||
<div class="meta-box-item">
|
||||
<span class="meta-label">最近校验</span>
|
||||
<span class="meta-value">
|
||||
{{ account.verifiedAt ? formatDateTime(account.verifiedAt) : '--' }}
|
||||
</span>
|
||||
<span class="meta-value">{{ account.verifiedAt ? formatDateTime(account.verifiedAt) : '--' }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">客户端状态</span>
|
||||
<a-tooltip :title="clientStatusText(account)" placement="topLeft">
|
||||
<a-tag :color="clientStatusColor(account)" class="client-status-tag">
|
||||
{{ clientStatusText(account) }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<div class="meta-box-item">
|
||||
<span class="meta-label">最近心跳</span>
|
||||
<span class="meta-value">
|
||||
{{ account.clientLastSeenAt ? formatDateTime(account.clientLastSeenAt) : '--' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">发布状态</span>
|
||||
<a-tag :color="publishStateColor(account.publishState)">
|
||||
{{ publishStateLabel(account.publishState) }}
|
||||
</a-tag>
|
||||
<span class="meta-value">{{ account.clientLastSeenAt ? formatDateTime(account.clientLastSeenAt) : '--' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -564,43 +557,47 @@ async function refreshAll(): Promise<void> {
|
||||
}
|
||||
|
||||
.media-card {
|
||||
border: 1px solid #e6edf5;
|
||||
border-radius: 16px;
|
||||
padding: 18px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(31, 92, 255, 0.06), transparent 28%),
|
||||
linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
gap: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.media-card:hover {
|
||||
box-shadow: 0 12px 28px -6px rgba(0, 0, 0, 0.05);
|
||||
border-color: transparent;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.media-card__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.media-card__identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.media-card__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.media-card__avatar img {
|
||||
@@ -615,68 +612,93 @@ async function refreshAll(): Promise<void> {
|
||||
|
||||
.media-card__identity-copy h3 {
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
font-size: 15px;
|
||||
color: #1e293b;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.media-card__inline-platform-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.media-card__inline-platform-text {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
background: #f8fafc;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.media-card__identity-copy p {
|
||||
margin: 4px 0 0;
|
||||
color: #8c8c8c;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.media-card__platform-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.media-card__uid-divider {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.media-card__uid {
|
||||
color: #94a3b8;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.media-card__platform-badge {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 12px;
|
||||
background: #f7f9fc;
|
||||
border: 1px solid #e6edf5;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
.media-card__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.media-card__platform-badge img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
.media-card__tags :deep(.ant-tag) {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.media-card__meta {
|
||||
.media-card__meta-box {
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
.meta-box-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: #1a1a1a;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.client-status-tag {
|
||||
max-width: 160px;
|
||||
max-width: 140px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1061,8 +1061,8 @@ if (!hasSingleInstanceLock) {
|
||||
|
||||
void Promise.race([
|
||||
(async () => {
|
||||
await clearRendererDesktopSessions()
|
||||
await releaseRuntimeSession()
|
||||
await clearRendererDesktopSessions()
|
||||
})(),
|
||||
new Promise<void>((resolve) => setTimeout(resolve, 1500)),
|
||||
]).finally(() => {
|
||||
|
||||
@@ -145,7 +145,7 @@ type MonitorExecutionPhase =
|
||||
| 'PARSING'
|
||||
| 'POSTING'
|
||||
|
||||
const heartbeatIntervalMs = 25_000
|
||||
const heartbeatIntervalMs = 15_000
|
||||
const pullIntervalMs = 60_000
|
||||
const leaseExtendIntervalMs = 60_000
|
||||
const maxActivityItems = 60
|
||||
|
||||
Reference in New Issue
Block a user