Archived
human-tool / ledger / loop-engine / release / task / interaction 六模块与 app 层(desk 路由、engines、consumers、 data-order-landing、case-stamp、reset-runs、seed-activities、容器与 server 装配),web 的 desk-task (ViewRegion / chat-bubbles / review-face)与 app.css、ui-kit 图标;契约 card / manifest; 迁移 0034_task_seq 与迁移日志;api-report 快照;CLAUDE.md 快照、debt、活动开发规范、HTML 报告规范同步。 逐条口径见 CLAUDE.md 快照(2026-08-27 下午至晚)。另一会话做的,一直留在工作树没入库;与同批另两条提交互相依赖。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
/**
|
|
* human-tool 存储 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。
|
|
*/
|
|
import { and, asc, eq, isNull, sql } from 'drizzle-orm';
|
|
import type { ChatCard, StaffId, TaskId, WorkOrder } from '@contracts';
|
|
import { chatCardSchema, workOrderSchema } from '@contracts';
|
|
import type { Db } from '@shared/db.ts';
|
|
import type { CardStore } from '../../port.ts';
|
|
import { chatCardTable, workOrderTable } from './human-tool-schema.ts';
|
|
|
|
export function createCardStore(db: Db): CardStore {
|
|
return {
|
|
async saveWorkOrder(taskId, order, recipientId) {
|
|
await db.insert(workOrderTable).values({
|
|
id: order.id,
|
|
taskId,
|
|
recipientId,
|
|
mode: order.mode,
|
|
title: order.title,
|
|
payload: order,
|
|
at: new Date(order.at),
|
|
done: order.done ?? null,
|
|
});
|
|
},
|
|
|
|
async getWorkOrder(id) {
|
|
const rows = await db.select().from(workOrderTable).where(eq(workOrderTable.id, id)).limit(1);
|
|
const row = rows[0];
|
|
if (row === undefined) return undefined;
|
|
const order = workOrderSchema.parse({
|
|
...(row.payload as Record<string, unknown>),
|
|
...(row.done === null ? {} : { done: row.done }),
|
|
});
|
|
return { taskId: row.taskId as TaskId, order, recipientId: row.recipientId as StaffId };
|
|
},
|
|
|
|
async listWorkOrders(taskId, openOnly) {
|
|
const conditions = [eq(workOrderTable.taskId, taskId)];
|
|
if (openOnly) conditions.push(isNull(workOrderTable.done));
|
|
const rows = await db
|
|
.select()
|
|
.from(workOrderTable)
|
|
.where(and(...conditions))
|
|
.orderBy(asc(workOrderTable.at));
|
|
return rows.map((row) =>
|
|
workOrderSchema.parse({
|
|
...(row.payload as Record<string, unknown>),
|
|
...(row.done === null ? {} : { done: row.done }),
|
|
}),
|
|
);
|
|
},
|
|
|
|
async markWorkOrderDone(id, done) {
|
|
await db.update(workOrderTable).set({ done }).where(eq(workOrderTable.id, id));
|
|
},
|
|
|
|
/*
|
|
* 局部改卡面:整卡存在 `payload` 里,所以读回来合并再写回。
|
|
* 只对**未回传**的卡开放(调用方保证)—— 已答的卡是留痕,只增不改。
|
|
*/
|
|
async patchWorkOrder(id, patch) {
|
|
const rows = await db.select().from(workOrderTable).where(eq(workOrderTable.id, id)).limit(1);
|
|
const row = rows[0];
|
|
if (row === undefined) return;
|
|
await db
|
|
.update(workOrderTable)
|
|
.set({ payload: { ...(row.payload as Record<string, unknown>), ...patch } })
|
|
.where(eq(workOrderTable.id, id));
|
|
},
|
|
|
|
async saveChatCard(taskId, card, recipientId) {
|
|
await db.insert(chatCardTable).values({
|
|
id: card.id,
|
|
taskId,
|
|
recipientId,
|
|
kind: card.k,
|
|
payload: card,
|
|
at: new Date(card.at),
|
|
});
|
|
},
|
|
|
|
async getChatCard(id) {
|
|
const rows = await db.select().from(chatCardTable).where(eq(chatCardTable.id, id)).limit(1);
|
|
const row = rows[0];
|
|
if (row === undefined) return undefined;
|
|
return {
|
|
taskId: row.taskId as TaskId,
|
|
card: chatCardSchema.parse(row.payload),
|
|
recipientId: row.recipientId as StaffId,
|
|
};
|
|
},
|
|
|
|
async listChatCards(taskId) {
|
|
const rows = await db
|
|
.select()
|
|
.from(chatCardTable)
|
|
.where(eq(chatCardTable.taskId, taskId))
|
|
.orderBy(asc(chatCardTable.at));
|
|
return rows.map((row) => chatCardSchema.parse(row.payload));
|
|
},
|
|
|
|
async listOpenChatCards(taskId) {
|
|
const rows = await db
|
|
.select()
|
|
.from(chatCardTable)
|
|
.where(and(eq(chatCardTable.taskId, taskId), isNull(chatCardTable.supersededBy)))
|
|
.orderBy(asc(chatCardTable.at));
|
|
return rows.map((row) => chatCardSchema.parse(row.payload));
|
|
},
|
|
|
|
async updateChatCard(card: ChatCard) {
|
|
await db.update(chatCardTable).set({ payload: card }).where(eq(chatCardTable.id, card.id));
|
|
},
|
|
|
|
async closeChatCard(id, supersededBy, reason) {
|
|
await db.update(chatCardTable).set({ supersededBy, rejectedReason: reason }).where(eq(chatCardTable.id, id));
|
|
},
|
|
|
|
/** 调人次数 = 发出的工单 + 对话卡 —— 打扰率与预算的计数面(OS 8.3) */
|
|
async humanCallCount(taskId) {
|
|
const orders = await db
|
|
.select({ n: sql<number>`count(*)::int` })
|
|
.from(workOrderTable)
|
|
.where(eq(workOrderTable.taskId, taskId));
|
|
const cards = await db
|
|
.select({ n: sql<number>`count(*)::int` })
|
|
.from(chatCardTable)
|
|
.where(eq(chatCardTable.taskId, taskId));
|
|
return (orders[0]?.n ?? 0) + (cards[0]?.n ?? 0);
|
|
},
|
|
|
|
async openCardsOf(staffId) {
|
|
const orders = await db
|
|
.select({ id: workOrderTable.id, title: workOrderTable.title })
|
|
.from(workOrderTable)
|
|
.where(and(eq(workOrderTable.recipientId, staffId), isNull(workOrderTable.done)));
|
|
const cards = await db
|
|
.select({ id: chatCardTable.id, kind: chatCardTable.kind })
|
|
.from(chatCardTable)
|
|
.where(and(eq(chatCardTable.recipientId, staffId), isNull(chatCardTable.supersededBy)));
|
|
return [
|
|
...orders.map((o) => ({ cardId: o.id, title: o.title })),
|
|
...cards.map((c) => ({ cardId: c.id, title: `${c.kind} 卡` })),
|
|
];
|
|
},
|
|
};
|
|
}
|
|
|
|
export type { WorkOrder };
|