Skip to content
3 changes: 2 additions & 1 deletion src/public/parts/shells/chat/public/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Root: `{userDict}/shells/chat/entities/{entityHash}/` — bookmarks, folders, al

## HTTP

Thin wrappers: `endpoints/shared.mjs` → `chatClientFromReq` → operator client. Shapes: `public/llms.txt`.
- **Backend**: thin wrappers `src/endpoints/shared.mjs` → `chatClientFromReq` → operator client. Shapes: `public/llms.txt`.
- **Frontend**: named functions only in `public/src/endpoints/*.mjs`. Private `chatFetch` / `groupFetch` stay inside `endpoints/` — UI must not import `groupClient`. UI / shared / providers must not `fetch` shell REST — only `endpoints/**` and Litterbox in `share.mjs` may call raw `fetch`. Global whoami / getdetails / EVFS → `/scripts/endpoints/`. HTML templates → `renderTemplate` / `mountTemplate` / `withTemplates`.

`GET …/groups/:id/state` → `{ meta, viewer, federation }`. Frontend flatten must **not** let `viewer.roles` (held role IDs) overwrite `meta.roles` (role definition map) — write held roles into `myRoles`.

Expand Down
37 changes: 14 additions & 23 deletions src/public/parts/shells/chat/public/emoji-packs/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { initTranslations, geti18n } from '/scripts/i18n/index.mjs'
import { discoverEmojiPackOffers } from '/scripts/features/emoji/discover.mjs'
import { showEmojiPackPreview } from '/scripts/components/emojiPackPreview.mjs'
import { escapeHtml } from '/scripts/lib/escapeHtml.mjs'
import { handleError } from '/scripts/features/errorHandlers.mjs'
import { showToastI18n } from '/scripts/features/toast.mjs'
import { joinGroup } from '../src/endpoints/groupCore.mjs'
import { postRelationshipFollow } from '../src/endpoints/social.mjs'

applyTheme()
await initTranslations()
Expand All @@ -15,9 +18,6 @@ const statusEl = document.getElementById('emoji-packs-status')
const gridEl = document.getElementById('emoji-packs-grid')
const emptyEl = document.getElementById('emoji-packs-empty')

const CHAT_API = '/api/parts/shells:chat'
const SOCIAL_API = '/api/parts/shells:social'

/**
* @param {HTMLElement} actions 操作区
* @param {{ i18nKey: string, fallback: string, className: string, onClick: () => void | Promise<void> }} options 按钮选项
Expand All @@ -29,10 +29,13 @@ function addActionButton(actions, { i18nKey, fallback, className, onClick }) {
button.className = className
button.dataset.i18n = i18nKey
button.textContent = geti18n(i18nKey) || fallback
button.addEventListener('click', () => {
void Promise.resolve(onClick()).catch(error => {
showToastI18n('error', 'chat.emoji.previewActionFailed', { error: error.message || String(error) })
})
button.addEventListener('click', async () => {
try {
await onClick()
}
catch (error) {
handleError('chat.emoji.previewActionFailed')(error)
}
})
actions.appendChild(button)
return button
Expand Down Expand Up @@ -91,35 +94,23 @@ function renderOfferCard(offer) {

if (sourceKind === 'group' && sourceId) {
/** @returns {Promise<void>} 加入来源群 */
const joinGroup = async () => {
const r = await fetch(`${CHAT_API}/groups/${encodeURIComponent(sourceId)}/join`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: '{}',
})
if (!r.ok) throw new Error(await r.text() || r.statusText)
const joinSourceGroup = async () => {
await joinGroup(sourceId)
window.location.href = `/parts/shells:chat/hub/#group:${encodeURIComponent(sourceId)}:default`
}
addActionButton(actions, {
i18nKey: 'chat.emojiPacks.joinGroup',
fallback: 'Join',
className: 'btn btn-primary btn-sm',
onClick: joinGroup,
onClick: joinSourceGroup,
})
}
else if (sourceKind === 'entity' && sourceId) {
/** @type {HTMLButtonElement} */
let followBtn
/** @returns {Promise<void>} 关注作者 */
const followAuthor = async () => {
const r = await fetch(`${SOCIAL_API}/relationships/follow`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ entityHash: sourceId, follow: true }),
})
if (!r.ok) throw new Error(await r.text() || r.statusText)
await postRelationshipFollow(sourceId, true)
showToastI18n('success', 'chat.emoji.followSuccess')
followBtn.disabled = true
followBtn.dataset.i18n = 'chat.emoji.alreadyFollowing'
Expand Down
4 changes: 3 additions & 1 deletion src/public/parts/shells/chat/public/hub/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ Deeper UI (profile card, module layout, unread/inbox/aliases, cabinet bind perms

- CSS: page-local, no `hub-` prefix. Ready-gate: `HUB_GATE` / `fount:hub-*`. Layout: `body[data-layout-pane]` / `body[data-surface]`. Mobile (`≤768px`): `body[data-layout-pane=nav|main]` via `hubPane.mjs`.
- **`fount.user.send`**: Hub bootstrap registers `globalThis.fount.user.send(string | chatLogEntry)` → current channel. Normalize in `shared/fountUserSend.mjs` (Deno-pure — no `/scripts/*` imports there).
- Errors: `handleUIError` (toast + `console.error` + Sentry). Background: `toError` + console + Sentry, no toast.
- Errors: `handleError('chat.hub.…')` → `.catch` closure (toast + console + Sentry) for fount faults. User mistakes: `showToastI18n`. Impl: `/scripts/features/errorHandlers.mjs`.
- Floating promises: call directly; no need for `void`. Use `return void sideEffect()` only when the side effect's return value is not `undefined`.
- Prefer `renderTemplate` / `mountTemplate`. Modals: `openDialogFromTemplate` (`modal-box` only). Cross-shell shared modules: `withTemplates`, never bare `usingTemplates`. Prefer DaisyUI; context menus via `/scripts/components/positionContextMenu.mjs`; prompts via `/scripts/features/promptDialog.mjs`.
- **HTTP**: named functions in `../src/endpoints/*.mjs` only — no UI `fetch` of shell REST (`share.mjs` Litterbox is the sole non-endpoint exception). Global whoami/getdetails/EVFS → `/scripts/endpoints/`.
- State: `core/state.mjs` — import exported bindings; heavy modules use call-site `await import()`.
- No hardcoded user-visible strings; `data-i18n` / `setElementI18n` + `zh-CN.json`.
- **@-mention autocomplete**: on `<textarea>` use only `aria-controls` / `aria-activedescendant`; do not add `role="combobox"` / `aria-expanded`.
Expand Down
12 changes: 5 additions & 7 deletions src/public/parts/shells/chat/public/hub/banners.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import { isHex64 } from 'https://esm.sh/@steve02081504/fount-p2p/core/hexIds'

import { renderTemplateAsHtmlString } from '../../../../scripts/features/template.mjs'
import { getDagTips } from '../src/endpoints/groupCore.mjs'
import { handleError } from '/scripts/features/errorHandlers.mjs'

import { escapeHtml } from '/scripts/lib/escapeHtml.mjs'
import { refreshBoundBanners } from './core/bindings.mjs'
Expand Down Expand Up @@ -55,11 +57,7 @@ export async function refreshDagForkBanner() {
store.federation.dagTips = []
return
}
const response = await fetch(
`/api/parts/shells:chat/groups/${encodeURIComponent(store.context.currentGroupId)}/dag/tips`,
{ credentials: 'include' },
)
const data = await response.json()
const data = await getDagTips(store.context.currentGroupId)
const tips = Array.isArray(data.tips) ? data.tips : []
store.federation.dagTips = tips
const governanceFork = !!data.governanceFork || !!store.context.currentState?.governanceFork
Expand Down Expand Up @@ -168,6 +166,6 @@ export function updateStatusBanners() {
refreshGshBufferBanner()
refreshQuarantineBanner()
refreshLocalViewBanner()
void refreshChannelPinsBar()
void refreshDagForkBanner().then(() => refreshLocalViewBanner())
refreshChannelPinsBar().catch(handleError('chat.hub.operationFailed'))
refreshDagForkBanner().then(refreshLocalViewBanner).catch(handleError('chat.hub.operationFailed'))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

统一两处 MJS 异步错误边界。

这两处变更继续使用 Promise 链。项目要求异步代码统一使用 async/await,并在异步边界集中处理错误。

  • src/public/parts/shells/chat/public/hub/banners.mjs#L170-L170: 将 refreshLocalViewBanner() 放入带 try/catch 的异步辅助函数,并等待 refreshDagForkBanner()
  • src/public/parts/shells/chat/public/hub/messages/messageRefresh.mjs#L353-L355: 用 try/catchawait import(...)await fetchMemberReadMarkers(...) 替换 .then(...).catch(...)

As per coding guidelines,MJS 异步代码必须统一使用 async/await
As per path instructions,异步失败必须通过统一的 handleError(...) 处理。

📍 Affects 2 files
  • src/public/parts/shells/chat/public/hub/banners.mjs#L170-L170 (this comment)
  • src/public/parts/shells/chat/public/hub/messages/messageRefresh.mjs#L353-L355
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/public/parts/shells/chat/public/hub/banners.mjs` at line 170, Replace the
Promise chain at src/public/parts/shells/chat/public/hub/banners.mjs:170-170
with an async helper using try/catch, await refreshDagForkBanner(), then await
refreshLocalViewBanner(), and route failures through
handleError('chat.hub.operationFailed'). Also replace the .then(...).catch(...)
chain at
src/public/parts/shells/chat/public/hub/messages/messageRefresh.mjs:353-355 with
try/catch using await import(...) and await fetchMemberReadMarkers(...),
handling errors through the existing handleError(...) mechanism.

Sources: Coding guidelines, Path instructions

}
13 changes: 6 additions & 7 deletions src/public/parts/shells/chat/public/hub/call.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { geti18n, setElementI18n } from '../../../../scripts/i18n/index.mjs'
import { buildChatCallWsUrl } from '../shared/avRelayClient.mjs'
import { displayProfileAvatar } from '../shared/hashAvatar.mjs'
import { resolveDisplayName } from '../shared/nameResolve.mjs'
import { getCallStatus } from '../src/endpoints/groupChannel.mjs'
import { iconifyImg, iconifyUrl } from '../src/lib/emojiSvg.mjs'
import { handleError } from '/scripts/features/errorHandlers.mjs'

import { joinCodecsAvRoom, leaveCodecsAvRoom } from './codecsAv.mjs'
import { store } from './core/state.mjs'
Expand Down Expand Up @@ -517,15 +519,12 @@ export async function refreshCallStatusBadge() {
return
}
try {
const res = await fetch(
`/api/parts/shells:chat/groups/${encodeURIComponent(groupId)}/channels/${encodeURIComponent(channelId)}/call-status`,
{ credentials: 'include' },
)
if (!res.ok) return
const data = await res.json()
const data = await getCallStatus(groupId, channelId)
updateCallBadge(data.active ? data.peerCount || 0 : 0)
}
catch { /* ignore */ }
catch (error) {
handleError('chat.hub.operationFailed')(error)
}
}

/**
Expand Down
12 changes: 6 additions & 6 deletions src/public/parts/shells/chat/public/hub/channelContextMenu.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@
* 【职责】侧栏频道项右键菜单:重命名、删除、类型切换与打开线程等频道级操作入口。
* 【原理】`showChannelContextMenu` 在频道行旁弹出定位菜单并绑定一次性点击处理;删除/切换频道后由 `selectChannel`/`loadMessages` 重建主栏消息视图。
* 【数据结构】store(core/state)及本模块函数入参/返回值;详见 JSDoc。
* 【关联】打开频道时可能触发 `updateHash`(由 `sidebar.selectChannel` 完成);../../../../scripts/i18n、../../../../scripts/template、../../../../scripts/toast、../src/api/groupCore、groupChannel、core/state、sidebar。
* 【关联】打开频道时可能触发 `updateHash`(由 `sidebar.selectChannel` 完成);../../../../scripts/i18n、../../../../scripts/template、../../../../scripts/toast、../src/endpoints/groupCore、groupChannel、core/state、sidebar。
*/
import { renderTemplate } from '../../../../scripts/features/template.mjs'
import { showToastI18n } from '../../../../scripts/features/toast.mjs'
import { confirmI18n } from '../../../../scripts/i18n/index.mjs'
import {
downloadChannelArchiveJson,
exportChannelArchiveJson,
} from '../src/api/channelArchive.mjs'
} from '../src/endpoints/channelArchive.mjs'
import {
deleteChannel,
setDefaultChannel,
updateChannel,
} from '../src/api/groupChannel.mjs'
import { getGroupState } from '../src/api/groupCore.mjs'
import { handleUIError } from '../src/ui/errors.mjs'
} from '../src/endpoints/groupChannel.mjs'
import { getGroupState } from '../src/endpoints/groupCore.mjs'
import { handleError } from '/scripts/features/errorHandlers.mjs'

import { bindDismissOnDocumentInteraction } from '/scripts/components/contextMenuDismiss.mjs'
import { positionContextMenu } from '/scripts/components/positionContextMenu.mjs'
Expand Down Expand Up @@ -139,7 +139,7 @@ export async function showChannelContextMenu(event, channelId) {
showToastI18n('success', 'chat.hub.channel.context.exportOk')
}
catch (error) {
handleUIError(error, 'chat.hub.channel.context.exportFailed')
handleError('chat.hub.channel.context.exportFailed')(error)
}
})

Expand Down
5 changes: 2 additions & 3 deletions src/public/parts/shells/chat/public/hub/charCard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
renderTemplateAsHtmlString,
usingTemplates,
} from '../../../../scripts/features/template.mjs'
import { getPartDetails } from '/scripts/endpoints/parts.mjs'
import { escapeHtml } from '/scripts/lib/escapeHtml.mjs'
import { createEntityProfileCardElement } from '../shared/entityProfileCard.mjs'
import { displayProfileAvatar } from '../shared/hashAvatar.mjs'
Expand All @@ -36,9 +37,7 @@ let charInfoCardRenderGeneration = 0
*/
export async function getCharDetails(name) {
try {
const resp = await fetch(`/api/getdetails/chars/${encodeURIComponent(name)}`, { credentials: 'include' })
if (!resp.ok) return null
return await resp.json()
return await getPartDetails(`chars/${name}`)
}
catch {
return null
Expand Down
62 changes: 35 additions & 27 deletions src/public/parts/shells/chat/public/hub/chatConfig.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
* 【职责】群组/频道聊天配置面板:挂载到设置浮层或内嵌区,编辑频道与生成相关选项。
* 【原理】`mountChatConfigPanel` 将配置表单模板注入指定容器(常由 `chat.openGroupSettingsModal` 调用);配置变更可能触发重新生成或刷新消息;本模块只负责表单 UI 与保存回调。
* 【数据结构】store(core/state)及本模块函数入参/返回值;详见 JSDoc。
* 【关联】../../../../scripts/parts、../../../../scripts/template、../../../../scripts/toast、../src/api/groupCore、groupClient、groupChannel、core/domUtils、core/overlayModal、core/state。
* 【关联】../../../../scripts/parts、../../../../scripts/template、../../../../scripts/toast、../src/endpoints/groupCore、groupChannel、core/domUtils、core/overlayModal、core/state。
*/
import { getPartList } from '../../../../scripts/endpoints/parts.mjs'
import { handleError } from '/scripts/features/errorHandlers.mjs'
import { mountTemplate, renderTemplateAsHtmlString } from '../../../../scripts/features/template.mjs'
import { showToastI18n } from '../../../../scripts/features/toast.mjs'
import { triggerChannelReply } from '../src/api/groupChannel.mjs'
import { groupRequest } from '../src/api/groupClient.mjs'
import { getGroupChatConfig } from '../src/api/groupCore.mjs'
import { triggerChannelReply } from '../src/endpoints/groupChannel.mjs'
import {
addGroupPlugin,
getGroupChatConfig,
listGroupPlugins,
removeGroupChar,
removeGroupPlugin,
setGroupCharFrequency,
setGroupPersona,
setGroupWorld,
} from '../src/endpoints/groupCore.mjs'

import { showOverlayNotice } from './core/overlayModal.mjs'
import { store } from './core/state.mjs'
Expand Down Expand Up @@ -57,11 +65,11 @@ export async function mountChatConfigPanel(groupId, channelId = 'default', optio
getPartList('worlds').catch(() => []),
getPartList('personas').catch(() => []),
getPartList('plugins').catch(() => []),
groupRequest(groupId, 'plugins', 'GET').catch(() => []),
listGroupPlugins(groupId),
])

const charlist = Array.isArray(initial?.charlist) ? initial.charlist : []
const pluginlist = Array.isArray(activePlugins) ? activePlugins : Array.isArray(initial?.pluginlist) ? initial.pluginlist : []
const pluginlist = activePlugins
const freqMap = initial?.frequency_data || {}
const worldname = initial?.worldname || ''
const personaname = initial?.personaname || ''
Expand All @@ -80,7 +88,7 @@ export async function mountChatConfigPanel(groupId, channelId = 'default', optio
document.getElementById('character-chat-persona')?.addEventListener('change', async (changeEvent) => {
const v = changeEvent.target.value || null
try {
await groupRequest(groupId, 'persona', 'PUT', { personaname: v })
await setGroupPersona(groupId, v)
const { invalidateUserProfileCache } = await import('./presence.mjs')
const { refreshViewerHubPresentation } = await import('./init.mjs')
const { renderMemberList } = await import('./sidebar/index.mjs')
Expand All @@ -91,20 +99,20 @@ export async function mountChatConfigPanel(groupId, channelId = 'default', optio
await renderMemberList(store.context.currentState)
showOverlayNotice('success', '', 'chat.hub.config.saved')
}
catch (err) {
showOverlayNotice('error', err.message)
catch (error) {
handleError('chat.hub.config.saveFailed')(error)
}
})

if (canEditWorldPlugins) {
document.getElementById('character-chat-world')?.addEventListener('change', async (changeEvent) => {
const v = changeEvent.target.value || null
try {
await groupRequest(groupId, 'world', 'PUT', { worldname: v, channelId })
await setGroupWorld(groupId, v, channelId)
showOverlayNotice('success', '', 'chat.hub.config.saved')
}
catch (err) {
showOverlayNotice('error', err.message)
catch (error) {
handleError('chat.hub.config.saveFailed')(error)
}
})

Expand All @@ -113,12 +121,12 @@ export async function mountChatConfigPanel(groupId, channelId = 'default', optio
const pluginname = sel?.value
if (!pluginname) return
try {
await groupRequest(groupId, 'plugin', 'POST', { pluginname })
await addGroupPlugin(groupId, pluginname)
await mountChatConfigPanel(groupId, channelId, options)
showOverlayNotice('success', '', 'chat.hub.config.saved')
}
catch (err) {
showOverlayNotice('error', err.message)
catch (error) {
handleError('chat.hub.config.saveFailed')(error)
}
})

Expand All @@ -127,12 +135,12 @@ export async function mountChatConfigPanel(groupId, channelId = 'default', optio
const pluginname = removePluginButton.dataset.plugin
if (!pluginname) return
try {
await groupRequest(groupId, `plugin/${encodeURIComponent(pluginname)}`, 'DELETE')
await removeGroupPlugin(groupId, pluginname)
await mountChatConfigPanel(groupId, channelId, options)
showOverlayNotice('success', '', 'chat.hub.config.saved')
}
catch (err) {
showOverlayNotice('error', err.message)
catch (error) {
handleError('chat.hub.config.saveFailed')(error)
}
})
})
Expand All @@ -145,10 +153,10 @@ export async function mountChatConfigPanel(groupId, channelId = 'default', optio
if (!charname) return
const frequency = Number(inputEvent.target.value) / 100
try {
await groupRequest(groupId, `char/${encodeURIComponent(charname)}/frequency`, 'PUT', { frequency })
await setGroupCharFrequency(groupId, charname, frequency)
}
catch (err) {
showToastI18n('error', 'chat.hub.config.saveFailed', { error: err.message })
catch (error) {
handleError('chat.hub.config.saveFailed')(error)
}
})
})
Expand All @@ -161,8 +169,8 @@ export async function mountChatConfigPanel(groupId, channelId = 'default', optio
await triggerChannelReply(groupId, channelId, charname)
showOverlayNotice('success', '', 'chat.hub.config.saved')
}
catch (err) {
showOverlayNotice('error', err.message)
catch (error) {
handleError('chat.hub.config.saveFailed')(error)
}
})
})
Expand All @@ -172,12 +180,12 @@ export async function mountChatConfigPanel(groupId, channelId = 'default', optio
const charname = removeCharButton.dataset.char
if (!charname) return
try {
await groupRequest(groupId, `char/${encodeURIComponent(charname)}`, 'DELETE')
await removeGroupChar(groupId, charname)
await mountChatConfigPanel(groupId, channelId, options)
showOverlayNotice('success', '', 'chat.hub.config.saved')
}
catch (err) {
showOverlayNotice('error', err.message)
catch (error) {
handleError('chat.hub.config.saveFailed')(error)
}
})
})
Expand Down
Loading
Loading