|
|
@@ -11,6 +11,7 @@ import {
|
|
|
Me,
|
|
|
Task,
|
|
|
UploadTicket,
|
|
|
+ UsageSession,
|
|
|
} from './models';
|
|
|
|
|
|
/**
|
|
|
@@ -556,6 +557,30 @@ function mapAgent(raw: Record<string, unknown>): Agent {
|
|
|
wecomBotId: channels.wecomBotId,
|
|
|
}
|
|
|
: undefined,
|
|
|
+ /**
|
|
|
+ * 🔴 `usage` 此前**完全没有映射** —— 云函数返回了、映射层丢掉了,
|
|
|
+ * 造成「Token 用量」永远空白(实测:库里 totalTokens=2238055,页面不显示)。
|
|
|
+ *
|
|
|
+ * 键名兼容:容器发 `inputTokens`/`outputTokens`,
|
|
|
+ * 契约旧字段是 `promptTokens`/`completionTokens`,两者都收。
|
|
|
+ */
|
|
|
+ usage: (() => {
|
|
|
+ const u = parseJsonLoose<Record<string, unknown>>(raw['usage']);
|
|
|
+ if (!u) return undefined;
|
|
|
+ const num = (x: unknown): number | undefined =>
|
|
|
+ typeof x === 'number' && Number.isFinite(x) ? x : undefined;
|
|
|
+ const usage = {
|
|
|
+ totalTokens: num(u['totalTokens']),
|
|
|
+ promptTokens: num(u['promptTokens']) ?? num(u['inputTokens']),
|
|
|
+ completionTokens: num(u['completionTokens']) ?? num(u['outputTokens']),
|
|
|
+ updatedAt: u['updatedAt'] ? String(u['updatedAt']) : undefined,
|
|
|
+ raw: u,
|
|
|
+ };
|
|
|
+ // 三项全无 → 视为未上报(避免 UI 显示 0)
|
|
|
+ return usage.totalTokens == null && usage.promptTokens == null && usage.completionTokens == null
|
|
|
+ ? undefined
|
|
|
+ : usage;
|
|
|
+ })(),
|
|
|
owner: user
|
|
|
? {
|
|
|
id: String(user['objectId'] ?? ''),
|
|
|
@@ -564,6 +589,19 @@ function mapAgent(raw: Record<string, unknown>): Agent {
|
|
|
mobile: user['mobile'] ? String(user['mobile']) : undefined,
|
|
|
}
|
|
|
: undefined,
|
|
|
+ // 容器标识(排障反查来源用;云函数读的是 hostname/containerRef)
|
|
|
+ containerRef: raw['containerRef'] ? String(raw['containerRef']) : undefined,
|
|
|
+ // Agent token 健康灯:expired 时余额等依赖 token 的字段会降级
|
|
|
+ sessionStatus: (() => {
|
|
|
+ const s = String(raw['sessionStatus'] ?? '').toLowerCase();
|
|
|
+ return s === 'ok' || s === 'expired' ? (s as 'ok' | 'expired') : 'unknown';
|
|
|
+ })(),
|
|
|
+ // 会话级用量明细(可选,缺失不影响总量展示)
|
|
|
+ usageSessions: (() => {
|
|
|
+ const list = parseJsonLoose<unknown[]>(raw['usageSessions']);
|
|
|
+ if (!Array.isArray(list) || list.length === 0) return undefined;
|
|
|
+ return list as UsageSession[];
|
|
|
+ })(),
|
|
|
companyId: (raw['company'] as { objectId?: string } | undefined)?.objectId,
|
|
|
// 「是否已接入 FMODE」= channels.matrix 为真(或已有 matrixConfig.userId)
|
|
|
matrixReady: !!channels?.matrix || !!channels?.matrixConfig?.userId,
|
|
|
@@ -584,6 +622,17 @@ function mapDeliverable(raw: Record<string, unknown>): Deliverable {
|
|
|
type: String(a['type'] ?? ''),
|
|
|
}))
|
|
|
: [];
|
|
|
+ /*
|
|
|
+ * 🔴 派生展示字段:`filename` / `fileType` / `size` 此前**从未赋值**
|
|
|
+ * (只在 demo-data 里有),真实数据下右栏渲染成「· 」,形如「仅有 · 的文档」。
|
|
|
+ *
|
|
|
+ * 契约未提供独立字段 → 从首个 artifact 推导;无 artifact 时给明确占位,
|
|
|
+ * 让 UI 能显示「暂无附件」而不是一串空分隔符。
|
|
|
+ */
|
|
|
+ const first = artifacts[0];
|
|
|
+ const filename = first?.name || undefined;
|
|
|
+ const rawType = first?.type || filename?.split('.').pop() || '';
|
|
|
+ const fileType = rawType ? rawType.replace(/^\./, '').toUpperCase() : undefined;
|
|
|
return {
|
|
|
id: String(raw['objectId'] ?? raw['id'] ?? ''),
|
|
|
agentId: String(raw['agentId'] ?? ''),
|
|
|
@@ -594,6 +643,10 @@ function mapDeliverable(raw: Record<string, unknown>): Deliverable {
|
|
|
artifacts,
|
|
|
tags: Array.isArray(raw['tags']) ? (raw['tags'] as string[]).map(String) : [],
|
|
|
deliveredAt: String(raw['deliveredAt'] ?? ''),
|
|
|
+ filename,
|
|
|
+ fileType,
|
|
|
+ // 契约未返回文件大小 → 保持空(UI 不该显示 0 B)
|
|
|
+ size: raw['size'] ? String(raw['size']) : undefined,
|
|
|
// 契约 §2.2:status 取值为推断 —— 含 fail/error 子串即失败
|
|
|
state: /fail|error/i.test(status) ? 'failed' : 'delivered',
|
|
|
};
|