feat: IM平台列 + deviceID标识 + 会话持久化完善
- agent-96: chat_messages 加 platform 列(迁移0004) + IM主人私聊落主会话web(带platform) + 前端按平台过滤契约 + message_persisted 带 platform(复用 BroadcastPersisted) - agent-97: chat_messages 加 device_id 列(迁移0005) + history.Message.DeviceID + Web发消息带deviceId(localStorage getClientId) + IM persistToMain 带 deviceID + 前端气泡展示 SourceLabel + 修主会话基础2个bug(fn scanMessage语法/DDL缺列) - design/16 §4.4(平台) + §4.5(deviceID) 已入档 - 实机: POST /chat 带 deviceId 读回 user/assistant 均带 deviceId+platform; 后端 build/vet/test 绿 + 前端 build 绿 - 红线: 身份靠 actor 元数据; 禁止 shell 读 .env(JWT冒烟由主会话跑)
This commit is contained in:
1 parent
1bf60c1ae4
commit
37e98ec584
26 files changed
+828
-116
No files matched your search
@@ -116,6 +116,19 @@ function statusLabel(status: ToolStatus): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** Small muted source label for a persisted message (design/16 §4.5): shows the
|
||||
* concrete client the message came from, e.g. "来自 web:chrome:a1b2c3". Rendered
|
||||
* only when a deviceId is present, so legacy/unlabelled messages stay clean. The
|
||||
* Web client defaults to this label; other clients decide themselves. */
|
||||
function SourceLabel({ deviceId }: { deviceId?: string }): React.ReactNode {
|
||||
if (!deviceId) return null;
|
||||
return (
|
||||
<div className="text-[10px] leading-tight text-[var(--mu-text-lo)]">
|
||||
来自 {deviceId}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** A single message bubble. Per design/07 §1 the bubble shape is driven by the
|
||||
* message's own `mode`: the same role renders differently under different modes,
|
||||
* and a single session may mix shapes. Falls back to `chat` when mode is absent
|
||||
@@ -188,6 +201,7 @@ function AssistantParaBubble({
|
||||
{full && message.mood ? (
|
||||
<div className="mb-1 text-xs text-[var(--mu-text-lo)]">心情 · {message.mood}</div>
|
||||
) : null}
|
||||
{full ? <SourceLabel deviceId={message.deviceId} /> : null}
|
||||
{full ? <WakePath active={active} /> : null}
|
||||
{full ? <ThinkingBlock text={message.thinking} streaming={message.streaming} /> : null}
|
||||
{full ? message.tools?.map((t, i) => <ToolCard key={i} tool={t} />) : null}
|
||||
@@ -208,7 +222,7 @@ function UserParaBubble({
|
||||
full: boolean;
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<div className="mu-rise-in flex justify-end">
|
||||
<div className="mu-rise-in flex flex-col items-end gap-0.5">
|
||||
<div className="max-w-[80%] rounded-[var(--mu-radius-lg)] bg-[var(--mu-accent)] px-4 py-2.5 text-sm text-white shadow-[var(--mu-shadow-1)]">
|
||||
{full && message.image ? (
|
||||
<img
|
||||
@@ -219,6 +233,7 @@ function UserParaBubble({
|
||||
) : null}
|
||||
<span className="whitespace-pre-wrap">{renderContent(text)}</span>
|
||||
</div>
|
||||
{full ? <SourceLabel deviceId={message.deviceId} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -281,7 +296,7 @@ function UserBubble({ message, mode }: { message: ChatMessage; mode: Mode }): Re
|
||||
<span className="whitespace-pre-wrap">{renderContent(message.content)}</span>
|
||||
) : null;
|
||||
return (
|
||||
<div className="mu-rise-in flex justify-end">
|
||||
<div className="mu-rise-in flex flex-col items-end gap-0.5">
|
||||
<div className="max-w-[80%] rounded-[var(--mu-radius-lg)] bg-[var(--mu-accent)] px-4 py-2.5 text-sm text-white shadow-[var(--mu-shadow-1)]">
|
||||
{message.image ? (
|
||||
<img
|
||||
@@ -292,6 +307,7 @@ function UserBubble({ message, mode }: { message: ChatMessage; mode: Mode }): Re
|
||||
) : null}
|
||||
{body}
|
||||
</div>
|
||||
<SourceLabel deviceId={message.deviceId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -316,6 +332,7 @@ function AssistantBubble({
|
||||
{message.mood ? (
|
||||
<div className="mb-1 text-xs text-[var(--mu-text-lo)]">心情 · {message.mood}</div>
|
||||
) : null}
|
||||
<SourceLabel deviceId={message.deviceId} />
|
||||
<WakePath active={active} />
|
||||
<ThinkingBlock text={message.thinking} streaming={message.streaming} />
|
||||
{message.tools?.map((t, i) => <ToolCard key={i} tool={t} />)}
|
||||
|
||||
Reference in new issue
Block a user