Commit 8d5b8ec7 authored by AI-甘富林's avatar AI-甘富林

feat(desktop): 增加每日工作上报

parent 0ab5ca4c
...@@ -10,6 +10,7 @@ import { AppConfigService } from "./services/app-config.js"; ...@@ -10,6 +10,7 @@ import { AppConfigService } from "./services/app-config.js";
import { AuthClient, CreditClient, ModelConfigClient, OpenClawConfigClient, ProfileClient } from "./services/cloud-api.js"; import { AuthClient, CreditClient, ModelConfigClient, OpenClawConfigClient, ProfileClient } from "./services/cloud-api.js";
import { DeviceIdentityService } from "./services/device-identity.js"; import { DeviceIdentityService } from "./services/device-identity.js";
import { DiagnosticsService } from "./services/diagnostics.js"; import { DiagnosticsService } from "./services/diagnostics.js";
import { DailyReportService } from "./services/daily-report-service.js";
import { loadLocalOpenClawGatewayConfig, resolveEffectiveGatewayUrl } from "./services/openclaw-local-config.js"; import { loadLocalOpenClawGatewayConfig, resolveEffectiveGatewayUrl } from "./services/openclaw-local-config.js";
import { SecretManager } from "./services/secrets.js"; import { SecretManager } from "./services/secrets.js";
import { startSmokeCloudApiServer } from "./services/smoke-cloud-api.js"; import { startSmokeCloudApiServer } from "./services/smoke-cloud-api.js";
...@@ -515,6 +516,12 @@ async function bootstrap(): Promise<void> { ...@@ -515,6 +516,12 @@ async function bootstrap(): Promise<void> {
const creditClient = new CreditClient(configService, secretManager); const creditClient = new CreditClient(configService, secretManager);
const skillClient = new SkillClient(skillStore); const skillClient = new SkillClient(skillStore);
const modelConfigClient = new ModelConfigClient(configService, secretManager); const modelConfigClient = new ModelConfigClient(configService, secretManager);
const dailyReportService = new DailyReportService({
userDataPath: systemSummary.userDataPath,
configService,
secretManager
});
await dailyReportService.start();
const runtimeCloudSupervisor = new RuntimeCloudSupervisor({ const runtimeCloudSupervisor = new RuntimeCloudSupervisor({
appVersion: app.getVersion(), appVersion: app.getVersion(),
configService, configService,
...@@ -522,6 +529,9 @@ async function bootstrap(): Promise<void> { ...@@ -522,6 +529,9 @@ async function bootstrap(): Promise<void> {
runtimeManager, runtimeManager,
secretManager secretManager
}); });
runtimeCloudSupervisor.onActivity((event) => {
dailyReportService.handleActivity(event);
});
if (resolveRequestedRuntimeMode(config.runtimeMode) !== "external-gateway" && (await secretManager.getApiKey())) { if (resolveRequestedRuntimeMode(config.runtimeMode) !== "external-gateway" && (await secretManager.getApiKey())) {
await runtimeManager.start(); await runtimeManager.start();
...@@ -551,6 +561,7 @@ async function bootstrap(): Promise<void> { ...@@ -551,6 +561,7 @@ async function bootstrap(): Promise<void> {
modelConfigClient, modelConfigClient,
runtimeCloudClient, runtimeCloudClient,
runtimeCloudSupervisor, runtimeCloudSupervisor,
dailyReportService,
runtimeSkillBridge, runtimeSkillBridge,
systemSummary, systemSummary,
localOpenClawConfig localOpenClawConfig
...@@ -566,6 +577,7 @@ async function bootstrap(): Promise<void> { ...@@ -566,6 +577,7 @@ async function bootstrap(): Promise<void> {
event.preventDefault(); event.preventDefault();
void (async () => { void (async () => {
await runtimeCloudSupervisor.stop("app-before-quit"); await runtimeCloudSupervisor.stop("app-before-quit");
await dailyReportService.stop();
await runtimeManager.stop(); await runtimeManager.stop();
await runtimeSkillBridge.clearManagedSkills().catch(() => undefined); await runtimeSkillBridge.clearManagedSkills().catch(() => undefined);
if (stopSmokeCloudApiServer) { if (stopSmokeCloudApiServer) {
...@@ -604,3 +616,5 @@ void bootstrap().catch(async (error) => { ...@@ -604,3 +616,5 @@ void bootstrap().catch(async (error) => {
} }
app.quit(); app.quit();
}); });
...@@ -19,6 +19,7 @@ import type { RuntimeManager } from "@qjclaw/runtime-manager"; ...@@ -19,6 +19,7 @@ import type { RuntimeManager } from "@qjclaw/runtime-manager";
import type { AppConfigService } from "./services/app-config.js"; import type { AppConfigService } from "./services/app-config.js";
import type { AuthClient, CreditClient, ModelConfigClient, OpenClawConfigClient, ProfileClient } from "./services/cloud-api.js"; import type { AuthClient, CreditClient, ModelConfigClient, OpenClawConfigClient, ProfileClient } from "./services/cloud-api.js";
import type { DiagnosticsService } from "./services/diagnostics.js"; import type { DiagnosticsService } from "./services/diagnostics.js";
import type { DailyReportService } from "./services/daily-report-service.js";
import type { SkillClient } from "./services/skill-client.js"; import type { SkillClient } from "./services/skill-client.js";
import type { SkillStoreService } from "./services/skill-store.js"; import type { SkillStoreService } from "./services/skill-store.js";
import { resolveEffectiveGatewayUrl, type LocalOpenClawGatewayConfig } from "./services/openclaw-local-config.js"; import { resolveEffectiveGatewayUrl, type LocalOpenClawGatewayConfig } from "./services/openclaw-local-config.js";
...@@ -40,6 +41,7 @@ interface MainServices { ...@@ -40,6 +41,7 @@ interface MainServices {
modelConfigClient: ModelConfigClient; modelConfigClient: ModelConfigClient;
runtimeCloudClient: OpenClawConfigClient; runtimeCloudClient: OpenClawConfigClient;
runtimeCloudSupervisor: RuntimeCloudSupervisor; runtimeCloudSupervisor: RuntimeCloudSupervisor;
dailyReportService: DailyReportService;
runtimeSkillBridge: RuntimeSkillBridgeService; runtimeSkillBridge: RuntimeSkillBridgeService;
appVersion: string; appVersion: string;
systemSummary: SystemSummary; systemSummary: SystemSummary;
...@@ -199,6 +201,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi { ...@@ -199,6 +201,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
modelConfigClient, modelConfigClient,
runtimeCloudClient, runtimeCloudClient,
runtimeCloudSupervisor, runtimeCloudSupervisor,
dailyReportService,
runtimeSkillBridge, runtimeSkillBridge,
systemSummary, systemSummary,
localOpenClawConfig localOpenClawConfig
...@@ -456,6 +459,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi { ...@@ -456,6 +459,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
await reconfigureGatewayClient(config, input.gatewayToken); await reconfigureGatewayClient(config, input.gatewayToken);
await syncRuntimeCloudSupervisor("config-save"); await syncRuntimeCloudSupervisor("config-save");
void dailyReportService.runDueCheck().catch(() => undefined);
return getEffectiveConfig(); return getEffectiveConfig();
}); });
...@@ -667,6 +671,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi { ...@@ -667,6 +671,7 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
await reconfigureGatewayClient(config, input.gatewayToken); await reconfigureGatewayClient(config, input.gatewayToken);
await syncRuntimeCloudSupervisor("config-save"); await syncRuntimeCloudSupervisor("config-save");
void dailyReportService.runDueCheck().catch(() => undefined);
return getEffectiveConfig(); return getEffectiveConfig();
} }
}, },
...@@ -738,3 +743,4 @@ export function registerDesktopIpc(services: MainServices): DesktopApi { ...@@ -738,3 +743,4 @@ export function registerDesktopIpc(services: MainServices): DesktopApi {
} }
...@@ -4,6 +4,8 @@ import type { ...@@ -4,6 +4,8 @@ import type {
AuthSessionSummary, AuthSessionSummary,
CreditSummary, CreditSummary,
ModelCapability, ModelCapability,
OpenClawDailyReportPayload,
OpenClawDailyReportResponse,
ModelConfigFallbackMode, ModelConfigFallbackMode,
ModelConfigSummary, ModelConfigSummary,
ModelRecommendation, ModelRecommendation,
...@@ -778,6 +780,64 @@ export class CreditClient { ...@@ -778,6 +780,64 @@ export class CreditClient {
} }
} }
export class OpenClawDailyReportClient {
private readonly configService: AppConfigService;
private readonly secretManager: SecretManager;
private readonly httpClient = new HttpJsonClient();
constructor(configService: AppConfigService, secretManager: SecretManager) {
this.configService = configService;
this.secretManager = secretManager;
}
async submit(payload: Omit<OpenClawDailyReportPayload, "api_key">): Promise<OpenClawDailyReportResponse> {
const config = await this.configService.load();
const baseUrl = config.runtimeCloudApiBaseUrl.trim().replace(/\/$/, "");
const apiKey = (await this.secretManager.getApiKey())?.trim();
if (!baseUrl) {
throw new Error("OpenClaw 运行时云端地址未配置。");
}
if (!apiKey) {
throw new Error("请先绑定 OpenClaw employee API Key。");
}
const url = new URL(`${baseUrl}/openclaw-daily-report`);
const body = await this.httpClient.request(url, {
method: "POST",
body: {
api_key: apiKey,
...payload
}
});
let response: {
ok?: boolean;
summary_id?: string;
employee_id?: string;
summary_date?: string;
};
try {
response = JSON.parse(body) as {
ok?: boolean;
summary_id?: string;
employee_id?: string;
summary_date?: string;
};
} catch {
throw new Error("OpenClaw 日报接口返回了无效 JSON。");
}
return {
ok: response.ok !== false,
summaryId: response.summary_id,
employeeId: response.employee_id,
summaryDate: response.summary_date ?? payload.summary_date
};
}
}
export class ModelConfigClient { export class ModelConfigClient {
private readonly api: ProductCloudApiClient; private readonly api: ProductCloudApiClient;
...@@ -792,3 +852,4 @@ export class ModelConfigClient { ...@@ -792,3 +852,4 @@ export class ModelConfigClient {
This diff is collapsed.
...@@ -23,6 +23,41 @@ interface RuntimeCloudEvent { ...@@ -23,6 +23,41 @@ interface RuntimeCloudEvent {
occurred_at: string; occurred_at: string;
} }
export type RuntimeCloudActivityEvent =
| {
type: "message_received";
occurredAt: string;
sessionId: string;
prompt: string;
skillId?: string;
}
| {
type: "message_sent";
occurredAt: string;
sessionId: string;
replyContent: string;
modelId?: string;
skillId?: string;
}
| {
type: "error";
occurredAt: string;
errorType: string;
message: string;
modelId?: string;
sessionId?: string;
}
| {
type: "heartbeat";
occurredAt: string;
ok: boolean;
status?: string;
heartbeatAt?: string;
billing?: RuntimeHeartbeatBillingSummary;
};
type RuntimeCloudActivityListener = (event: RuntimeCloudActivityEvent) => Promise<void> | void;
interface OpenClawHeartbeatResponse { interface OpenClawHeartbeatResponse {
ok?: boolean; ok?: boolean;
status?: string; status?: string;
...@@ -206,6 +241,7 @@ export class RuntimeCloudSupervisor { ...@@ -206,6 +241,7 @@ export class RuntimeCloudSupervisor {
private readonly telemetry: RuntimeTelemetryStatus; private readonly telemetry: RuntimeTelemetryStatus;
private readonly activeConversationIds = new Set<string>(); private readonly activeConversationIds = new Set<string>();
private readonly queue: RuntimeCloudEvent[] = []; private readonly queue: RuntimeCloudEvent[] = [];
private readonly activityListeners = new Set<RuntimeCloudActivityListener>();
private heartbeatTimer?: NodeJS.Timeout; private heartbeatTimer?: NodeJS.Timeout;
private configSyncTimer?: NodeJS.Timeout; private configSyncTimer?: NodeJS.Timeout;
private eventFlushTimer?: NodeJS.Timeout; private eventFlushTimer?: NodeJS.Timeout;
...@@ -249,6 +285,13 @@ export class RuntimeCloudSupervisor { ...@@ -249,6 +285,13 @@ export class RuntimeCloudSupervisor {
}; };
} }
onActivity(listener: RuntimeCloudActivityListener): () => void {
this.activityListeners.add(listener);
return () => {
this.activityListeners.delete(listener);
};
}
async start(): Promise<RuntimeTelemetryStatus> { async start(): Promise<RuntimeTelemetryStatus> {
if (this.telemetry.state === "running") { if (this.telemetry.state === "running") {
return this.getStatus(); return this.getStatus();
...@@ -329,6 +372,14 @@ export class RuntimeCloudSupervisor { ...@@ -329,6 +372,14 @@ export class RuntimeCloudSupervisor {
} }
noteMessageReceived(sessionId: string, prompt: string, skillId?: string): void { noteMessageReceived(sessionId: string, prompt: string, skillId?: string): void {
this.emitActivity({
type: "message_received",
occurredAt: new Date().toISOString(),
sessionId,
prompt,
skillId
});
if (this.telemetry.state !== "running") { if (this.telemetry.state !== "running") {
return; return;
} }
...@@ -344,6 +395,15 @@ export class RuntimeCloudSupervisor { ...@@ -344,6 +395,15 @@ export class RuntimeCloudSupervisor {
} }
noteMessageSent(sessionId: string, replyContent: string, modelId?: string, skillId?: string): void { noteMessageSent(sessionId: string, replyContent: string, modelId?: string, skillId?: string): void {
this.emitActivity({
type: "message_sent",
occurredAt: new Date().toISOString(),
sessionId,
replyContent,
modelId,
skillId
});
if (this.telemetry.state !== "running") { if (this.telemetry.state !== "running") {
return; return;
} }
...@@ -360,6 +420,14 @@ export class RuntimeCloudSupervisor { ...@@ -360,6 +420,14 @@ export class RuntimeCloudSupervisor {
noteError(errorType: string, message: string, options?: { emitEvent?: boolean; modelId?: string; sessionId?: string }): void { noteError(errorType: string, message: string, options?: { emitEvent?: boolean; modelId?: string; sessionId?: string }): void {
this.telemetry.errorCount += 1; this.telemetry.errorCount += 1;
this.telemetry.lastError = message; this.telemetry.lastError = message;
this.emitActivity({
type: "error",
occurredAt: new Date().toISOString(),
errorType,
message,
modelId: options?.modelId,
sessionId: options?.sessionId
});
if (options?.emitEvent === false || this.telemetry.state !== "running") { if (options?.emitEvent === false || this.telemetry.state !== "running") {
return; return;
} }
...@@ -379,6 +447,16 @@ export class RuntimeCloudSupervisor { ...@@ -379,6 +447,16 @@ export class RuntimeCloudSupervisor {
this.telemetry.activeConversationCount = this.activeConversationIds.size; this.telemetry.activeConversationCount = this.activeConversationIds.size;
} }
private emitActivity(event: RuntimeCloudActivityEvent): void {
for (const listener of this.activityListeners) {
try {
Promise.resolve(listener(event)).catch(() => undefined);
} catch {
// Keep runtime telemetry usable even if side observers fail.
}
}
}
private enqueueEvent(eventType: RuntimeCloudEventType, data?: Record<string, unknown>, conversationId?: string): void { private enqueueEvent(eventType: RuntimeCloudEventType, data?: Record<string, unknown>, conversationId?: string): void {
if (this.queue.length >= MAX_EVENT_QUEUE_SIZE) { if (this.queue.length >= MAX_EVENT_QUEUE_SIZE) {
this.queue.shift(); this.queue.shift();
...@@ -423,6 +501,14 @@ export class RuntimeCloudSupervisor { ...@@ -423,6 +501,14 @@ export class RuntimeCloudSupervisor {
this.telemetry.lastHeartbeatError = undefined; this.telemetry.lastHeartbeatError = undefined;
this.telemetry.lastError = undefined; this.telemetry.lastError = undefined;
this.telemetry.heartbeatSuccessCount += 1; this.telemetry.heartbeatSuccessCount += 1;
this.emitActivity({
type: "heartbeat",
occurredAt: new Date().toISOString(),
ok: response.ok,
status: response.status,
heartbeatAt: response.heartbeatAt,
billing: response.billing
});
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
this.telemetry.lastHeartbeatAt = new Date().toISOString(); this.telemetry.lastHeartbeatAt = new Date().toISOString();
...@@ -552,3 +638,6 @@ export class RuntimeCloudSupervisor { ...@@ -552,3 +638,6 @@ export class RuntimeCloudSupervisor {
} }
} }
} }
...@@ -255,6 +255,22 @@ export async function startSmokeCloudApiServer(baseUrl: string, token: string, r ...@@ -255,6 +255,22 @@ export async function startSmokeCloudApiServer(baseUrl: string, token: string, r
return; return;
} }
if (req.method === "POST" && requestUrl.pathname === "/openclaw-daily-report") {
const body = await readJsonBody();
const apiKey = typeof body.api_key === "string" ? body.api_key : "";
if (apiKey !== runtimeApiKey) {
sendJson(401, { message: "Invalid api_key or employee not found" });
return;
}
sendJson(200, {
ok: true,
summary_id: "summary-" + Date.now(),
employee_id: "employee-smoke",
summary_date: typeof body.summary_date === "string" ? body.summary_date : new Date().toISOString().slice(0, 10)
});
return;
}
if (req.method === "GET" && requestUrl.pathname === "/openai/v1/models") { if (req.method === "GET" && requestUrl.pathname === "/openai/v1/models") {
if (bearerToken !== providerToken) { if (bearerToken !== providerToken) {
sendJson(401, { message: "Invalid provider token." }); sendJson(401, { message: "Invalid provider token." });
...@@ -412,3 +428,5 @@ export async function startSmokeCloudApiServer(baseUrl: string, token: string, r ...@@ -412,3 +428,5 @@ export async function startSmokeCloudApiServer(baseUrl: string, token: string, r
}); });
}; };
} }
# OpenClaw Daily Report 方案
## 1. 目标
为 OpenClaw 增加一个“每日自动汇总上报”能力:
- 每天自动整理当天的对话与工作情况
- 通过文档中定义的 `POST /openclaw-daily-report` 接口上报云端
- 不影响现有聊天、心跳、配置同步等主流程
- 方案适合测试阶段快速验证,也适合正式环境长期运行
## 2. 对接的云端 API
使用 `docs/云端API接入文档.txt` 中定义的接口:
- `POST /openclaw-daily-report`
### 请求字段
- `api_key`
- `summary_date`
- `total_conversations`
- `total_messages`
- `total_tokens`
- `total_errors`
- `highlights`
- `issues`
- `raw_summary_text`
- `active_channels`
### 返回字段
- `ok`
- `summary_id`
- `employee_id`
- `summary_date`
## 3. 数据来源
日报内容从现有桌面端数据里生成,主要来源如下:
- `packages/gateway-client/src/index.ts`
- 读取会话列表
- 读取会话消息
- `apps/desktop/src/main/ipc.ts`
- 现有聊天、会话、配置的主进程入口
- `apps/desktop/src/main/services/runtime-cloud-supervisor.ts`
- 现有主进程定时器、云端状态、运行时 telemetry 的管理模式
- `apps/desktop/src/main/services/cloud-api.ts`
- 云端请求封装与 `api_key` 读取方式
- `apps/desktop/src/main/services/diagnostics.ts`
- 结构化报告的组织方式,可参考其输出风格
## 4. 总体设计
### 4.1 单独做成定时任务
日报能力不挂到聊天发送链路,也不挂到心跳或配置同步链路里,而是单独做成一个主进程侧服务,例如:
- `DailyReportService`
这个服务只做三件事:
1. 到点检查是否需要发送日报
2. 生成日报 payload
3. 调用 `/openclaw-daily-report`
### 4.2 为什么要独立出来
这样可以保证:
- 日报失败不会影响聊天
- 日报失败不会影响心跳
- 日报失败不会影响配置同步
- 日报逻辑更容易测试和维护
## 5. 报表内容设计
建议日报保持“简洁 + 有要点”,避免发送过长原文。
### 5.1 建议 payload 结构
```ts
interface OpenClawDailyReportPayload {
api_key: string;
summary_date: string;
total_conversations: number;
total_messages: number;
total_tokens: number;
total_errors: number;
highlights: string[];
issues: string[];
raw_summary_text: string;
active_channels: Array<{
type: string;
name: string;
}>;
}
```
### 5.2 内容建议
- `summary_date`
- 使用本地日期,格式 `YYYY-MM-DD`
- `total_conversations`
- 当天会话数
- `total_messages`
- 当天消息总数
- `total_tokens`
- 如果当前链路能拿到 token,就统计累计值
- `total_errors`
- 当天错误数
- `highlights`
- 3~5 条关键摘要
- `issues`
- 异常、失败、恢复情况
- `raw_summary_text`
- 一段简短的人类可读总结
- `active_channels`
- 当天活跃的渠道列表
## 6. 调度策略
### 6.1 独立调度器
建议在主进程里单独维护一个定时器:
- 采用 `setInterval`
- 每分钟检查一次当前时间
- 如果当前时间已经到设定时间,且今天还没发过,就发送一次
### 6.2 时间配置
时间做成可配置项,便于测试和正式环境切换。
建议使用环境变量:
- `DAILY_REPORT_HOUR`
规则:
- 正式环境默认 `20`,即晚上 8 点
- 测试阶段可以改成当前小时或附近小时,方便快速触发
- 更改配置后,在下次启动时生效
### 6.3 去重机制
为了避免重复发送:
- 记录 `lastSummaryDate`
- 如果今天已经发过,就跳过
- 重启后也能继续判断,避免重复补发
## 7. 预期改动文件
### 7.1 核心文件
- `apps/desktop/src/main/services/cloud-api.ts`
- 增加 `POST /openclaw-daily-report` 的请求方法
- `apps/desktop/src/main/services/runtime-cloud-supervisor.ts`
- 增加日报生成与调度逻辑,或抽出一个新的主进程服务
- `apps/desktop/src/main/index.ts`
- 在应用启动时拉起日报服务
- `packages/shared-types/src/index.ts`
- 如有需要,补充日报请求/响应类型
### 7.2 可选文件
- `apps/desktop/src/main/ipc.ts`
- 如果需要手动触发测试,可增加一个 IPC 入口
## 8. 实现步骤
### Step 1:接入日报 API
`apps/desktop/src/main/services/cloud-api.ts` 中增加一个专门请求 `POST /openclaw-daily-report` 的方法。
要求:
- 复用现有 HTTP 请求封装
- 复用现有 `api_key` 获取方式
- 返回值结构化处理
### Step 2:生成日报内容
在主进程里读取当天会话数据,计算:
- 会话数
- 消息数
- token 数
- 错误数
- 亮点
- 问题
- 简短文本总结
- 活跃渠道
### Step 3:增加独立定时任务
新增一个主进程服务,例如 `DailyReportService`
- 启动时检查是否需要补发前一天的日报
- 每分钟检查一次时间
- 到点后发送日报
- 发送成功后记录 `lastSummaryDate`
### Step 4:保持主流程隔离
日报失败时:
- 只记录日志
- 不抛到聊天逻辑
- 不影响心跳和配置同步
## 9. 测试计划
### 9.1 代码级检查
```bash
corepack pnpm typecheck
corepack pnpm build
```
### 9.2 单元级检查
验证以下内容:
- 日报 payload 组装正确
- `summary_date` 格式正确
- 统计字段计算正确
- `lastSummaryDate` 能防止重复发送
- 请求失败不会影响聊天或 heartbeat
### 9.3 集成检查
1. 启动桌面端
2. 绑定 employee `api_key`
3. 设置 `DAILY_REPORT_HOUR` 为测试阶段可快速触发的时间
4. 创建几条会话和消息
5. 等定时器触发或手动触发日报
6. 确认 `/openclaw-daily-report` 收到正确 payload
7. 确认返回里有 `ok``summary_id``employee_id``summary_date`
### 9.4 回归检查
- 心跳是否仍正常执行
- 配置同步是否仍正常执行
- 聊天发送是否仍正常执行
- 重启后是否不会对同一天重复上报
### 9.5 失败路径检查
- 模拟接口返回 4xx / 5xx
- 确认不会影响主业务
- 确认日志里能看到失败原因
- 确认没有活动时的策略符合产品预期(跳过或发最小摘要)
## 10. 约定和注意事项
- 请求字段名必须严格按文档来,不要改成事件式字段名
- `raw_summary_text` 要简洁,不要过长
- `summary_date` 使用本地日期字符串 `YYYY-MM-DD`
- 日报功能应保持独立,不能影响现有聊天链路
- 文档后续若要补充细节,可直接在这里扩展
...@@ -738,4 +738,47 @@ ...@@ -738,4 +738,47 @@
} }
] ]
} }
}
工作日报上报
POST
/openclaw-daily-report
每天定时上报当日工作汇总,包含对话数、消息数、Token 用量、关键成果与异常。同一员工同一天重复上报会更新(upsert)。建议在每天 23:55 或次日凌晨上报。
请求示例
{
"api_key": "your_48char_hex_api_key",
"summary_date": "2026-03-23",
"total_conversations": 45,
"total_messages": 312,
"total_tokens": 186400,
"total_errors": 2,
"highlights": [
"处理了 3 个高优先级客诉",
"新增 12 个客户画像标签"
],
"issues": [
"飞书渠道 15:20 出现短暂连接中断"
],
"raw_summary_text": "今日共处理 45 个对话,主要集中在售后咨询和产品咨询。下午飞书渠道出现短暂中断,已自动恢复。",
"active_channels": [
{
"type": "feishu",
"name": "飞书客服"
},
{
"type": "wechat",
"name": "微信公众号"
}
]
}
响应示例
{
"ok": true,
"summary_id": "uuid",
"employee_id": "uuid",
"summary_date": "2026-03-23"
} }
\ No newline at end of file
export const IPC_CHANNELS = { export const IPC_CHANNELS = {
workspaceGetSummary: "workspace:get-summary", workspaceGetSummary: "workspace:get-summary",
gatewayStatus: "gateway:status", gatewayStatus: "gateway:status",
gatewayConnect: "gateway:connect", gatewayConnect: "gateway:connect",
...@@ -55,6 +55,7 @@ export type RuntimeCloudEventType = "startup" | "shutdown" | "message_sent" | "m ...@@ -55,6 +55,7 @@ export type RuntimeCloudEventType = "startup" | "shutdown" | "message_sent" | "m
export type PluginStatus = "included" | "extension" | "unavailable"; export type PluginStatus = "included" | "extension" | "unavailable";
export type ChatLaunchState = "unbound" | "starting" | "ready" | "error"; export type ChatLaunchState = "unbound" | "starting" | "ready" | "error";
export type SkillDownloadState = "pending" | "downloading" | "ready" | "failed" | "removed"; export type SkillDownloadState = "pending" | "downloading" | "ready" | "failed" | "removed";
export type DailyReportDeliveryState = "draft" | "sent" | "failed";
export interface GatewayStatus { export interface GatewayStatus {
state: GatewayState; state: GatewayState;
...@@ -163,6 +164,31 @@ export interface RuntimeHeartbeatBillingSummary { ...@@ -163,6 +164,31 @@ export interface RuntimeHeartbeatBillingSummary {
balanceAfter?: number; balanceAfter?: number;
} }
export interface OpenClawDailyReportChannelSummary {
type: string;
name: string;
}
export interface OpenClawDailyReportPayload {
api_key: string;
summary_date: string;
total_conversations: number;
total_messages: number;
total_tokens: number;
total_errors: number;
highlights: string[];
issues: string[];
raw_summary_text: string;
active_channels: OpenClawDailyReportChannelSummary[];
}
export interface OpenClawDailyReportResponse {
ok: boolean;
summaryId?: string;
employeeId?: string;
summaryDate: string;
}
export interface RuntimeTelemetryStatus { export interface RuntimeTelemetryStatus {
state: RuntimeTelemetryState; state: RuntimeTelemetryState;
startedAt?: string; startedAt?: string;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment