@wfm-cn/dsh-storage 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +128 -2
- package/cordis.patch.yml +3 -0
- package/lib/backends/database.js +578 -0
- package/lib/backfill.js +331 -0
- package/lib/client/index.js +79 -0
- package/lib/client/panel.js +299 -0
- package/lib/client/toast.js +66 -0
- package/lib/client.js +1 -0
- package/lib/compaction.js +108 -0
- package/lib/config.js +171 -0
- package/lib/ddl.js +323 -0
- package/lib/generated/mysql/browser.js +17 -0
- package/lib/generated/mysql/client.js +34 -0
- package/lib/generated/mysql/commonInputTypes.js +10 -0
- package/lib/generated/mysql/enums.js +10 -0
- package/lib/generated/mysql/internal/class.js +49 -0
- package/lib/generated/mysql/internal/prismaNamespace.js +146 -0
- package/lib/generated/mysql/internal/prismaNamespaceBrowser.js +117 -0
- package/lib/generated/mysql/models/AiChatHistory.js +1 -0
- package/lib/generated/mysql/models/AiMessage.js +1 -0
- package/lib/generated/mysql/models.js +1 -0
- package/lib/generated/postgresql/browser.js +17 -0
- package/lib/generated/postgresql/client.js +34 -0
- package/lib/generated/postgresql/commonInputTypes.js +10 -0
- package/lib/generated/postgresql/enums.js +10 -0
- package/lib/generated/postgresql/internal/class.js +49 -0
- package/lib/generated/postgresql/internal/prismaNamespace.js +131 -0
- package/lib/generated/postgresql/internal/prismaNamespaceBrowser.js +102 -0
- package/lib/generated/postgresql/models/AiChatHistory.js +1 -0
- package/lib/generated/postgresql/models/AiMessage.js +1 -0
- package/lib/generated/postgresql/models.js +1 -0
- package/lib/generated/sqlite/browser.js +17 -0
- package/lib/generated/sqlite/client.js +34 -0
- package/lib/generated/sqlite/commonInputTypes.js +10 -0
- package/lib/generated/sqlite/enums.js +10 -0
- package/lib/generated/sqlite/internal/class.js +49 -0
- package/lib/generated/sqlite/internal/prismaNamespace.js +128 -0
- package/lib/generated/sqlite/internal/prismaNamespaceBrowser.js +99 -0
- package/lib/generated/sqlite/models/AiChatHistory.js +1 -0
- package/lib/generated/sqlite/models/AiMessage.js +1 -0
- package/lib/generated/sqlite/models.js +1 -0
- package/lib/generated/sqlserver/browser.js +17 -0
- package/lib/generated/sqlserver/client.js +34 -0
- package/lib/generated/sqlserver/commonInputTypes.js +10 -0
- package/lib/generated/sqlserver/enums.js +10 -0
- package/lib/generated/sqlserver/internal/class.js +49 -0
- package/lib/generated/sqlserver/internal/prismaNamespace.js +119 -0
- package/lib/generated/sqlserver/internal/prismaNamespaceBrowser.js +90 -0
- package/lib/generated/sqlserver/models/AiChatHistory.js +1 -0
- package/lib/generated/sqlserver/models/AiMessage.js +1 -0
- package/lib/generated/sqlserver/models.js +1 -0
- package/lib/health.js +180 -0
- package/lib/index.js +676 -0
- package/lib/notify.js +56 -0
- package/lib/projector.js +191 -0
- package/lib/status-route.js +172 -0
- package/lib/types/backends/database.d.ts +78 -0
- package/lib/types/backfill.d.ts +67 -0
- package/lib/types/client/index.d.ts +12 -0
- package/lib/types/client/panel.d.ts +79 -0
- package/lib/types/client/toast.d.ts +38 -0
- package/lib/types/compaction.d.ts +3 -0
- package/lib/types/config.d.ts +52 -0
- package/lib/types/ddl.d.ts +49 -0
- package/lib/types/generated/mysql/browser.d.ts +14 -0
- package/lib/types/generated/mysql/client.d.ts +33 -0
- package/lib/types/generated/mysql/commonInputTypes.d.ts +396 -0
- package/lib/types/generated/mysql/enums.d.ts +1 -0
- package/lib/types/generated/mysql/internal/class.d.ts +143 -0
- package/lib/types/generated/mysql/internal/prismaNamespace.d.ts +709 -0
- package/lib/types/generated/mysql/internal/prismaNamespaceBrowser.d.ts +112 -0
- package/lib/types/generated/mysql/models/AiChatHistory.d.ts +1258 -0
- package/lib/types/generated/mysql/models/AiMessage.d.ts +1359 -0
- package/lib/types/generated/mysql/models.d.ts +3 -0
- package/lib/types/generated/postgresql/browser.d.ts +14 -0
- package/lib/types/generated/postgresql/client.d.ts +33 -0
- package/lib/types/generated/postgresql/commonInputTypes.d.ts +392 -0
- package/lib/types/generated/postgresql/enums.d.ts +1 -0
- package/lib/types/generated/postgresql/internal/class.d.ts +143 -0
- package/lib/types/generated/postgresql/internal/prismaNamespace.d.ts +728 -0
- package/lib/types/generated/postgresql/internal/prismaNamespaceBrowser.d.ts +95 -0
- package/lib/types/generated/postgresql/models/AiChatHistory.d.ts +1373 -0
- package/lib/types/generated/postgresql/models/AiMessage.d.ts +1496 -0
- package/lib/types/generated/postgresql/models.d.ts +3 -0
- package/lib/types/generated/sqlite/browser.d.ts +14 -0
- package/lib/types/generated/sqlite/client.d.ts +33 -0
- package/lib/types/generated/sqlite/commonInputTypes.d.ts +373 -0
- package/lib/types/generated/sqlite/enums.d.ts +1 -0
- package/lib/types/generated/sqlite/internal/class.d.ts +143 -0
- package/lib/types/generated/sqlite/internal/prismaNamespace.d.ts +705 -0
- package/lib/types/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +92 -0
- package/lib/types/generated/sqlite/models/AiChatHistory.d.ts +1371 -0
- package/lib/types/generated/sqlite/models/AiMessage.d.ts +1493 -0
- package/lib/types/generated/sqlite/models.d.ts +3 -0
- package/lib/types/generated/sqlserver/browser.d.ts +14 -0
- package/lib/types/generated/sqlserver/client.d.ts +33 -0
- package/lib/types/generated/sqlserver/commonInputTypes.d.ts +333 -0
- package/lib/types/generated/sqlserver/enums.d.ts +1 -0
- package/lib/types/generated/sqlserver/internal/class.d.ts +143 -0
- package/lib/types/generated/sqlserver/internal/prismaNamespace.d.ts +669 -0
- package/lib/types/generated/sqlserver/internal/prismaNamespaceBrowser.d.ts +80 -0
- package/lib/types/generated/sqlserver/models/AiChatHistory.d.ts +1257 -0
- package/lib/types/generated/sqlserver/models/AiMessage.d.ts +1375 -0
- package/lib/types/generated/sqlserver/models.d.ts +3 -0
- package/lib/types/health.d.ts +52 -0
- package/lib/types/index.d.ts +91 -0
- package/lib/types/notify.d.ts +35 -0
- package/lib/types/projector.d.ts +51 -0
- package/lib/types/status-route.d.ts +48 -0
- package/lib/types/types.d.ts +58 -0
- package/lib/types.js +8 -0
- package/package.json +126 -3
- package/preview.png +0 -0
- package/prisma/schema.mysql.prisma +78 -0
- package/prisma/schema.postgresql.prisma +70 -0
- package/prisma/schema.sqlite.prisma +70 -0
- package/prisma/schema.sqlserver.prisma +76 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type SyncConfig } from './config.js';
|
|
2
|
+
export type HealthStatus = {
|
|
3
|
+
state: 'ok' | 'down' | 'disabled' | 'config-error';
|
|
4
|
+
lastOkAt?: number;
|
|
5
|
+
lastError?: string;
|
|
6
|
+
retryCount: number;
|
|
7
|
+
mirroredTotal?: number;
|
|
8
|
+
};
|
|
9
|
+
/** Thrown by the probe when the sync config itself is unusable — no retries until it is fixed. */
|
|
10
|
+
export declare class ConfigError extends Error {
|
|
11
|
+
}
|
|
12
|
+
type TransitionCallback = (to: HealthStatus['state'], from: HealthStatus['state'], status: HealthStatus) => void;
|
|
13
|
+
export interface HealthMonitorOptions {
|
|
14
|
+
/** Re-probe interval while healthy (default 30s). */
|
|
15
|
+
intervalMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* States: 'disabled' (not started) → 'ok'/'down' on probe results, or
|
|
19
|
+
* 'config-error' when the probe reports a ConfigError (retries suspended
|
|
20
|
+
* until an explicit `recheck()`). Same-state results never re-fire
|
|
21
|
+
* transition callbacks.
|
|
22
|
+
*/
|
|
23
|
+
export declare class HealthMonitor {
|
|
24
|
+
private readonly probe;
|
|
25
|
+
private readonly options;
|
|
26
|
+
private state;
|
|
27
|
+
private lastOkAt;
|
|
28
|
+
private lastError;
|
|
29
|
+
private retryCount;
|
|
30
|
+
private running;
|
|
31
|
+
private timer;
|
|
32
|
+
private readonly listeners;
|
|
33
|
+
constructor(probe: () => Promise<void>, options?: HealthMonitorOptions);
|
|
34
|
+
start(): void;
|
|
35
|
+
stop(): void;
|
|
36
|
+
snapshot(): HealthStatus;
|
|
37
|
+
onTransition(cb: TransitionCallback): () => void;
|
|
38
|
+
/** Run one probe cycle immediately (re-arms after a config error). */
|
|
39
|
+
recheck(): Promise<void>;
|
|
40
|
+
private clearTimer;
|
|
41
|
+
private runProbe;
|
|
42
|
+
private onSuccess;
|
|
43
|
+
private onFailure;
|
|
44
|
+
private scheduleRetry;
|
|
45
|
+
private transition;
|
|
46
|
+
}
|
|
47
|
+
/** Probe for HealthMonitor: resolves when the database is live and migrated. */
|
|
48
|
+
export declare function probeDatabase(cfg: SyncConfig): Promise<void>;
|
|
49
|
+
export type ConnectionTestResult = 'ok' | 'no-tables' | `fail:${string}`;
|
|
50
|
+
/** One-shot panel test: never throws — every failure maps to 'fail:<reason>'. */
|
|
51
|
+
export declare function testConnection(cfg: SyncConfig): Promise<ConnectionTestResult>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-storage — mirrors the dsh session event stream into the ai_messages /
|
|
3
|
+
* ai_chat_histories tables (MySQL/PostgreSQL/SQLite/SQL Server).
|
|
4
|
+
*
|
|
5
|
+
* Design: dsh's own session persistence (JSONL/SQLite) stays authoritative.
|
|
6
|
+
* This plugin taps `session/event` (the same seam the built-in persistence
|
|
7
|
+
* coordinator uses) and projects events into MessageRow / SessionRow shaped
|
|
8
|
+
* after the source project's ai_messages / ai_chat_histories tables.
|
|
9
|
+
*
|
|
10
|
+
* A2A task state (task metadata in Redis, workspace archives in GCS) is NOT
|
|
11
|
+
* here — that belongs to the A2A layer, see dsh-a2a's TaskStore backends.
|
|
12
|
+
*
|
|
13
|
+
* Resume caveat (docs/subsystems/persistence.md): constructor seeds do not
|
|
14
|
+
* emit events. On agent resume, only events >= `session.firstLiveSeq` fire
|
|
15
|
+
* on this stream — historical rows must come from the database itself, not
|
|
16
|
+
* from replaying the tap.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-storage
|
|
19
|
+
*/
|
|
20
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
21
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
22
|
+
import { type DatabaseProvider } from './backends/database.js';
|
|
23
|
+
import { type BackfillSummary } from './backfill.js';
|
|
24
|
+
import { type SyncConfig } from './config.js';
|
|
25
|
+
import { type CreateTableResult } from './ddl.js';
|
|
26
|
+
import { type ConnectionTestResult, type HealthStatus } from './health.js';
|
|
27
|
+
import { type StorageToast } from './notify.js';
|
|
28
|
+
export declare const name = "dsh-storage";
|
|
29
|
+
export declare const inject: string[];
|
|
30
|
+
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
31
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
32
|
+
database: Schema<Schemastery.ObjectS<NoInfer<{
|
|
33
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
34
|
+
provider: Schema<"mysql" | "postgresql" | "sqlite" | "sqlserver", "mysql" | "postgresql" | "sqlite" | "sqlserver", "defined">;
|
|
35
|
+
url: Schema<string, string, "defined">;
|
|
36
|
+
timezone: Schema<string, string, "defined">;
|
|
37
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
38
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
39
|
+
provider: Schema<"mysql" | "postgresql" | "sqlite" | "sqlserver", "mysql" | "postgresql" | "sqlite" | "sqlserver", "defined">;
|
|
40
|
+
url: Schema<string, string, "defined">;
|
|
41
|
+
timezone: Schema<string, string, "defined">;
|
|
42
|
+
}>>, "plain">;
|
|
43
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
44
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
45
|
+
database: Schema<Schemastery.ObjectS<NoInfer<{
|
|
46
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
47
|
+
provider: Schema<"mysql" | "postgresql" | "sqlite" | "sqlserver", "mysql" | "postgresql" | "sqlite" | "sqlserver", "defined">;
|
|
48
|
+
url: Schema<string, string, "defined">;
|
|
49
|
+
timezone: Schema<string, string, "defined">;
|
|
50
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
51
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
52
|
+
provider: Schema<"mysql" | "postgresql" | "sqlite" | "sqlserver", "mysql" | "postgresql" | "sqlite" | "sqlserver", "defined">;
|
|
53
|
+
url: Schema<string, string, "defined">;
|
|
54
|
+
timezone: Schema<string, string, "defined">;
|
|
55
|
+
}>>, "plain">;
|
|
56
|
+
}>>, "plain">;
|
|
57
|
+
export interface StoragePluginConfig {
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
database: {
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
provider: DatabaseProvider;
|
|
62
|
+
url: string;
|
|
63
|
+
timezone?: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Explicit write API for a consumer's session-clear flow. */
|
|
67
|
+
export interface DshStorageService {
|
|
68
|
+
archiveSession(sessionId: string): Promise<string | null>;
|
|
69
|
+
}
|
|
70
|
+
/** Status/health RPC for the settings panel (Task 5) and notifications (Task 6). */
|
|
71
|
+
export interface DshStorageStatusService {
|
|
72
|
+
snapshot(): HealthStatus;
|
|
73
|
+
testConnection(cfg: SyncConfig): Promise<ConnectionTestResult>;
|
|
74
|
+
/** Panel 「建表」: provider-appropriate DDL via packaged prisma schemas. */
|
|
75
|
+
createTables(cfg: SyncConfig): Promise<CreateTableResult>;
|
|
76
|
+
/** Panel form load/persist — the file the backend watches (T3). */
|
|
77
|
+
getConfig(): {
|
|
78
|
+
config: SyncConfig;
|
|
79
|
+
error?: 'parse' | 'missing-field';
|
|
80
|
+
};
|
|
81
|
+
saveConfig(cfg: SyncConfig): {
|
|
82
|
+
config: SyncConfig;
|
|
83
|
+
};
|
|
84
|
+
/** Panel 「补传」 (T6): replay gap events from local session logs. */
|
|
85
|
+
backfill(): Promise<BackfillSummary>;
|
|
86
|
+
/** Toast notifications newer than the cursor (client polling, T6). */
|
|
87
|
+
toasts(payload: {
|
|
88
|
+
sinceId?: number;
|
|
89
|
+
}): StorageToast[];
|
|
90
|
+
}
|
|
91
|
+
export declare function apply(ctx: Context, config: StoragePluginConfig): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 同步中断/恢复通知 (Task 6): the host side records toast notifications on
|
|
3
|
+
* HealthMonitor transitions; the browser client (src/client/toast.tsx) polls
|
|
4
|
+
* them through the status bridge and renders a React portal independent of
|
|
5
|
+
* the settings panel. The monitor already guarantees transitions only
|
|
6
|
+
* (same-state results never re-fire), so each record is a real state change.
|
|
7
|
+
*/
|
|
8
|
+
export interface StorageToast {
|
|
9
|
+
id: number;
|
|
10
|
+
kind: 'interrupted' | 'recovered';
|
|
11
|
+
text: string;
|
|
12
|
+
/** Secondary line (recovery backfill hint). */
|
|
13
|
+
hint?: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
}
|
|
16
|
+
/** Minimal monitor surface wireNotifications needs (satisfied by HealthMonitor). */
|
|
17
|
+
export interface ToastCapableMonitor {
|
|
18
|
+
onTransition(cb: (to: string, from: string, status: {
|
|
19
|
+
lastError?: string;
|
|
20
|
+
}) => void): () => void;
|
|
21
|
+
}
|
|
22
|
+
/** 中断 toast: good → bad. */
|
|
23
|
+
export declare function notifyGoodToBad(lastError?: string): StorageToast;
|
|
24
|
+
/** 恢复 toast: bad → good, with a backfill hint for the gap. */
|
|
25
|
+
export declare function notifyBadToGood(): StorageToast;
|
|
26
|
+
/** Records newer than `sinceId` (polling cursor for the client). */
|
|
27
|
+
export declare function drainToasts(sinceId?: number): StorageToast[];
|
|
28
|
+
/** Test/reset hook: drop all history. */
|
|
29
|
+
export declare function clearToasts(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Wire the toast records onto a monitor's transitions. Good→bad strictly
|
|
32
|
+
* means ok → down (a config error was never "good"); bad→good covers
|
|
33
|
+
* down → ok and config-error → ok.
|
|
34
|
+
*/
|
|
35
|
+
export declare function wireNotifications(monitor: ToastCapableMonitor): () => void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects dsh session events onto MessageRow / SessionRow.
|
|
3
|
+
*
|
|
4
|
+
* Event shapes verified against the dsh persistence catalog
|
|
5
|
+
* (https://deepseek-harness.github.io/deepseek-harness/reference/persistence-catalog)
|
|
6
|
+
* and the @deepseek-ai/dsh-llm message types:
|
|
7
|
+
* - envelope: `{ type, seq, time, data }` — payload lives under `data`.
|
|
8
|
+
* - `user/message` and `assistant/message` produce rows; `tool/result` updates
|
|
9
|
+
* the matching assistant row's toolCalls instead of producing a tool row.
|
|
10
|
+
* - token usage (V3 log): `assistant/message.data.usage` is the settled
|
|
11
|
+
* step's accounting; a failed/cancelled attempt commits `assistant/attempt`
|
|
12
|
+
* instead, with the adapter's usage chunk preserved as a raw record in its
|
|
13
|
+
* embedded compact stream (recovered in usageSampleOf). `turn/end` carries
|
|
14
|
+
* none — it only serves as a rollup checkpoint.
|
|
15
|
+
* - compaction summaries enter the surface as a `user/message` with
|
|
16
|
+
* `surfaceOp: replace`, so they flow through the user path unchanged.
|
|
17
|
+
*/
|
|
18
|
+
import type { MessageRow } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Map one session event to zero or one message row. Returns null for events
|
|
21
|
+
* that should not be persisted as standalone rows (log-only events: chunks,
|
|
22
|
+
* turn/step lifecycle, approvals, ...).
|
|
23
|
+
*/
|
|
24
|
+
export declare function projectEvent(_session: any, event: any, sessionId: string): MessageRow | null;
|
|
25
|
+
/** Identify the assistant tool call answered by either supported result shape. */
|
|
26
|
+
export declare function toolResultCallId(event: any): string | undefined;
|
|
27
|
+
/** Update the persisted assistant call identified by a tool result. */
|
|
28
|
+
export declare function mergeToolResult(event: any, rows: MessageRow[]): MessageRow | null;
|
|
29
|
+
export interface UsageSample {
|
|
30
|
+
input: number;
|
|
31
|
+
output: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Extract one model call's usage, keyed by its durable event sequence.
|
|
35
|
+
*
|
|
36
|
+
* V3 session log (dsh-session@0.1.6-alpha.2):
|
|
37
|
+
* - `assistant/message.data.usage` — the settled call's accounting;
|
|
38
|
+
* - `assistant/attempt` — a failed/retried/cancelled call commits no surface
|
|
39
|
+
* message; the adapter's usage chunk survives as a raw `chunk` record in the
|
|
40
|
+
* embedded compact `stream` (deltas are packed, but usage is a raw type).
|
|
41
|
+
*
|
|
42
|
+
* Calls on one `(turn, step)` are additive: a retry is another billed model
|
|
43
|
+
* call, not a progressive snapshot. Only redelivery of the same event sequence
|
|
44
|
+
* is a duplicate. Totals include the cache buckets: cacheRead/cacheWrite are
|
|
45
|
+
* billed in their own buckets (observed cacheReadTokens ≫ inputTokens in dsh
|
|
46
|
+
* session logs — they are not subsets of inputTokens).
|
|
47
|
+
*/
|
|
48
|
+
export declare function usageSampleOf(event: any): {
|
|
49
|
+
key: string;
|
|
50
|
+
sample: UsageSample;
|
|
51
|
+
} | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host→browser bridge for the settings panel: exposes the dshStorageStatus
|
|
3
|
+
* service over the DSH connection generic-RPC channel `/dsh-storage`.
|
|
4
|
+
*
|
|
5
|
+
* Pattern verified against the installed reference plugins (ds-harness-remote
|
|
6
|
+
* `registerControlRoute`, dsh-video-skin settings.section client): the browser
|
|
7
|
+
* client calls `ctx.connection.rpc.call('/dsh-storage', endpoint, payload)`,
|
|
8
|
+
* which POSTs `{type:'client-request', rpcId, method, payload}` to
|
|
9
|
+
* `/<channel>/<endpoint>` and expects `{type:'server-response', rpcId,
|
|
10
|
+
* result:{ok:true,value}|{ok:false,error:{code,message,details}}}`. When the
|
|
11
|
+
* webServer service is available we serve that path with a prefix route
|
|
12
|
+
* (guarded by `connection.requestRejection` for authentication); otherwise we
|
|
13
|
+
* fall back to `connection.rpc.handle` for in-process carriers. Both halves
|
|
14
|
+
* stay optional (`ctx.get`), so profiles without a web carrier simply skip
|
|
15
|
+
* the bridge.
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
+
import type { SyncConfig } from './config.js';
|
|
19
|
+
import type { CreateTableResult } from './ddl.js';
|
|
20
|
+
import type { ConnectionTestResult, HealthStatus } from './health.js';
|
|
21
|
+
import type { BackfillSummary } from './backfill.js';
|
|
22
|
+
import type { StorageToast } from './notify.js';
|
|
23
|
+
/** The surface bridged to the browser panel. */
|
|
24
|
+
export interface StatusService {
|
|
25
|
+
snapshot(): HealthStatus;
|
|
26
|
+
testConnection(cfg: SyncConfig): Promise<ConnectionTestResult>;
|
|
27
|
+
createTables(cfg: SyncConfig): Promise<CreateTableResult>;
|
|
28
|
+
getConfig(): {
|
|
29
|
+
config: SyncConfig;
|
|
30
|
+
error?: 'parse' | 'missing-field';
|
|
31
|
+
};
|
|
32
|
+
saveConfig(cfg: SyncConfig): {
|
|
33
|
+
config: SyncConfig;
|
|
34
|
+
};
|
|
35
|
+
/** Panel 「补传」: replay missing events from local session logs. */
|
|
36
|
+
backfill(): Promise<BackfillSummary>;
|
|
37
|
+
/** Toast notifications newer than the cursor (client polling). */
|
|
38
|
+
toasts(payload: {
|
|
39
|
+
sinceId?: number;
|
|
40
|
+
}): StorageToast[];
|
|
41
|
+
}
|
|
42
|
+
export declare const STATUS_CHANNEL = "/dsh-storage";
|
|
43
|
+
/**
|
|
44
|
+
* Register the /dsh-storage channel. Wire order mirrors ds-harness-remote's
|
|
45
|
+
* control route: an authenticated webServer prefix route when both services
|
|
46
|
+
* allow it, otherwise connection.rpc.handle for logical carriers.
|
|
47
|
+
*/
|
|
48
|
+
export declare function registerStatusRoute(ctx: Context, service: StatusService): (() => void) | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared row types and the backend contract.
|
|
3
|
+
*
|
|
4
|
+
* Row shapes mirror the `ai_messages` / `ai_chat_histories` Prisma models
|
|
5
|
+
* (see prisma/schema.{mysql,postgresql,sqlite,sqlserver}.prisma), matching
|
|
6
|
+
* the source project's tables.
|
|
7
|
+
*/
|
|
8
|
+
/** One row in `ai_messages`. */
|
|
9
|
+
export interface MessageRow {
|
|
10
|
+
id: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
historyId: string | null;
|
|
13
|
+
/** 'user' | 'model' | 'tool' | 'utility_compressor' | plugin-defined */
|
|
14
|
+
type: string;
|
|
15
|
+
content: string;
|
|
16
|
+
thoughts?: unknown;
|
|
17
|
+
model?: string | null;
|
|
18
|
+
tokens?: unknown;
|
|
19
|
+
toolCalls?: unknown;
|
|
20
|
+
agentId?: string | null;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
}
|
|
24
|
+
/** One row in `ai_chat_histories` — a per-session rollup. */
|
|
25
|
+
export interface SessionRow {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
title?: string | null;
|
|
28
|
+
messageCount: number;
|
|
29
|
+
totalTokens: number;
|
|
30
|
+
firstMessageAt?: Date | null;
|
|
31
|
+
lastMessageAt?: Date | null;
|
|
32
|
+
/** Plugin bookkeeping persisted in the row's JSON metadata column. */
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A storage backend receives already-projected rows. Backend errors reject
|
|
37
|
+
* into the plugin's per-session queue, whose catch/log boundary keeps
|
|
38
|
+
* mirroring failures out of the agent loop.
|
|
39
|
+
*/
|
|
40
|
+
export interface StorageBackend {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
init?(): Promise<void>;
|
|
43
|
+
/** Read the stored session row, for seeding the live rollup on resume. */
|
|
44
|
+
readSession?(sessionId: string): Promise<SessionRow | null>;
|
|
45
|
+
/** Max message created_at per session across active rows (backfill gap detection). */
|
|
46
|
+
latestPerSession?(): Promise<Map<string, Date>>;
|
|
47
|
+
/** Read one persisted logical message, including archived rows for redelivery. */
|
|
48
|
+
readMessages(sessionId: string, messageId: string): Promise<MessageRow[]>;
|
|
49
|
+
/** Find the newest active model message containing this exact tool call. */
|
|
50
|
+
readToolCallMessages(sessionId: string, callId: string): Promise<MessageRow[]>;
|
|
51
|
+
upsertMessage(row: MessageRow): Promise<void>;
|
|
52
|
+
upsertSession(row: SessionRow): Promise<void>;
|
|
53
|
+
/** Atomically persist one event's optional message and its session rollup. */
|
|
54
|
+
upsertEvent(message: MessageRow | null, session: SessionRow): Promise<void>;
|
|
55
|
+
/** Move active messages into a queryable history row; null when already empty. */
|
|
56
|
+
archiveSession(sessionId: string): Promise<string | null>;
|
|
57
|
+
close?(): Promise<void>;
|
|
58
|
+
}
|
package/lib/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,129 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wfm-cn/dsh-storage",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Session storage mirror for DeepSeek Harness: projects the session event stream into MySQL/PostgreSQL/SQLite/SQL Server (ai_messages / ai_chat_histories) without replacing dsh's local persistence.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./client": {
|
|
15
|
+
"default": "./lib/client.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib",
|
|
20
|
+
"prisma",
|
|
21
|
+
"cordis.patch.yml",
|
|
22
|
+
"preview.png",
|
|
23
|
+
"CHANGELOG.md"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/2938236819/dsh-storage.git"
|
|
32
|
+
},
|
|
33
|
+
"dsh": {
|
|
34
|
+
"bundle": {
|
|
35
|
+
"patch": "./cordis.patch.yml"
|
|
36
|
+
},
|
|
37
|
+
"client": {
|
|
38
|
+
"inject": [
|
|
39
|
+
"@deepseek-ai/dsh-client-connection",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
41
|
+
],
|
|
42
|
+
"platform": "web"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "node scripts/prisma-generate.mjs && tsc -p tsconfig.build.json && npm run build:client",
|
|
47
|
+
"build:client": "esbuild src/client/index.ts --bundle --format=iife --outfile=lib/client.js --target=es2020 --minify --legal-comments=none",
|
|
48
|
+
"generate": "node scripts/prisma-generate.mjs",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
50
|
+
"test": "vitest run"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@deepseek-ai/schemastery": "^3.18.5",
|
|
54
|
+
"@prisma/client": "7.9.1",
|
|
55
|
+
"@prisma/adapter-mariadb": "7.9.1"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
59
|
+
"@deepseek-ai/dsh-session": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.2.0-rc.1 || >=0.2.1-rc.1 || >=0.2.2-rc.1",
|
|
60
|
+
"@prisma/adapter-libsql": "^7.9.1",
|
|
61
|
+
"@prisma/adapter-mariadb": "^7.9.1",
|
|
62
|
+
"@prisma/adapter-mssql": "^7.9.1",
|
|
63
|
+
"@prisma/adapter-pg": "^7.9.1",
|
|
64
|
+
"@prisma/client": "^7.9.1",
|
|
65
|
+
"react": ">=18.2.0 <19",
|
|
66
|
+
"@deepseek-ai/dsh-client-connection": ">=0.2.0-rc.1 <0.3.0-0",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.2.0-rc.1 <0.3.0-0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@prisma/adapter-libsql": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@prisma/adapter-mariadb": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@prisma/adapter-mssql": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@prisma/adapter-pg": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"@prisma/client": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"react": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@deepseek-ai/dsh-client-connection": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"@deepseek-ai/dsh-client-ui-slots": {
|
|
92
|
+
"optional": true
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"devDependencies": {
|
|
96
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
97
|
+
"@deepseek-ai/dsh-session": "0.2.2-rc.1",
|
|
98
|
+
"@prisma/adapter-libsql": "^7.9.1",
|
|
99
|
+
"@prisma/adapter-mariadb": "^7.9.1",
|
|
100
|
+
"@prisma/adapter-mssql": "^7.9.1",
|
|
101
|
+
"@prisma/adapter-pg": "^7.9.1",
|
|
102
|
+
"@prisma/client": "^7.9.1",
|
|
103
|
+
"@testing-library/dom": "^10.4.2",
|
|
104
|
+
"@testing-library/react": "^16.3.3",
|
|
105
|
+
"@types/node": "^22.0.0",
|
|
106
|
+
"@types/react": "^18.3.31",
|
|
107
|
+
"@types/react-dom": "^18.3.7",
|
|
108
|
+
"esbuild": "^0.28.2",
|
|
109
|
+
"jsdom": "^31.0.0",
|
|
110
|
+
"prisma": "^7.9.1",
|
|
111
|
+
"react": "^18.3.1",
|
|
112
|
+
"react-dom": "^18.3.1",
|
|
113
|
+
"typescript": "^5.6.3",
|
|
114
|
+
"vitest": "^4.1.11"
|
|
115
|
+
},
|
|
116
|
+
"engines": {
|
|
117
|
+
"node": "^20.19 || ^22.12 || >=24.0"
|
|
118
|
+
},
|
|
119
|
+
"pnpm": {
|
|
120
|
+
"onlyBuiltDependencies": [
|
|
121
|
+
"@prisma/engines",
|
|
122
|
+
"esbuild",
|
|
123
|
+
"prisma"
|
|
124
|
+
],
|
|
125
|
+
"patchedDependencies": {
|
|
126
|
+
"@prisma/adapter-mariadb@7.9.1": "patches/@prisma__adapter-mariadb@7.9.1.patch"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
6
129
|
}
|
package/preview.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Prisma schema for the dsh-storage database backend — MySQL variant (Prisma 7).
|
|
2
|
+
//
|
|
3
|
+
// Table layout matches the source project's ai_messages / ai_chat_histories
|
|
4
|
+
// (packages/core/prisma/schema.prisma) so existing data stays compatible:
|
|
5
|
+
// - `type` values come from the dsh session event map
|
|
6
|
+
// ('user' | 'model' | 'tool' | ...) instead of gemini-cli message roles.
|
|
7
|
+
// - `agent_id` distinguishes the main agent from subagent writers.
|
|
8
|
+
// - no user_id: tenancy is carried by session_id, not a column.
|
|
9
|
+
//
|
|
10
|
+
// The client is pre-generated at package build time (`pnpm generate`, output
|
|
11
|
+
// src/generated/mysql, compiled into lib/generated) — consumers do NOT run
|
|
12
|
+
// `prisma generate`. To create/upgrade tables, run `prisma db push` /
|
|
13
|
+
// migrations against this file; the connection URL lives in prisma.config.ts
|
|
14
|
+
// (Prisma 7 dropped datasource.url from the schema).
|
|
15
|
+
|
|
16
|
+
generator client {
|
|
17
|
+
provider = "prisma-client"
|
|
18
|
+
output = "../src/generated/mysql"
|
|
19
|
+
importFileExtension = "js"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
datasource db {
|
|
23
|
+
provider = "mysql"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// User and assistant messages; tool results update the assistant row's tool_calls.
|
|
27
|
+
model AiMessage {
|
|
28
|
+
id String @id @default(cuid()) @db.Char(36)
|
|
29
|
+
sessionId String @map("session_id") @db.VarChar(100)
|
|
30
|
+
historyId String? @map("history_id") @db.Char(36)
|
|
31
|
+
type String @db.VarChar(50)
|
|
32
|
+
content String @db.LongText
|
|
33
|
+
thoughts Json?
|
|
34
|
+
model String? @db.VarChar(100)
|
|
35
|
+
tokens Json?
|
|
36
|
+
toolCalls Json? @map("tool_calls")
|
|
37
|
+
agentId String? @map("agent_id") @db.VarChar(100)
|
|
38
|
+
metadata Json?
|
|
39
|
+
device String? @map("device") @db.VarChar(100)
|
|
40
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0)
|
|
41
|
+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamp(0)
|
|
42
|
+
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
|
|
43
|
+
|
|
44
|
+
// Relations
|
|
45
|
+
history AiChatHistory? @relation(fields: [historyId], references: [id], onDelete: NoAction)
|
|
46
|
+
|
|
47
|
+
@@index([sessionId], map: "idx_session")
|
|
48
|
+
@@index([historyId], map: "idx_history")
|
|
49
|
+
@@index([createdAt], map: "idx_created")
|
|
50
|
+
@@index([deletedAt], map: "idx_deleted")
|
|
51
|
+
@@index([agentId], map: "idx_agent")
|
|
52
|
+
@@index([sessionId, agentId, type, createdAt], map: "idx_session_agent_type_created")
|
|
53
|
+
@@index([sessionId, historyId, deletedAt, type, createdAt, id], map: "idx_tool_call_lookup")
|
|
54
|
+
@@index([sessionId, deletedAt, updatedAt, id], map: "idx_archive_recovery")
|
|
55
|
+
@@map("ai_messages")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Per-session rollup, maintained incrementally by the projector.
|
|
59
|
+
model AiChatHistory {
|
|
60
|
+
id String @id @default(cuid()) @db.Char(36)
|
|
61
|
+
sessionId String @map("session_id") @db.VarChar(100)
|
|
62
|
+
title String? @db.VarChar(255)
|
|
63
|
+
summary String? @db.Text
|
|
64
|
+
messageCount Int @default(0) @map("message_count")
|
|
65
|
+
totalTokens BigInt @default(0) @map("total_tokens")
|
|
66
|
+
firstMessageAt DateTime? @map("first_message_at") @db.DateTime(6)
|
|
67
|
+
lastMessageAt DateTime? @map("last_message_at") @db.DateTime(6)
|
|
68
|
+
metadata Json?
|
|
69
|
+
device String? @map("device") @db.VarChar(100)
|
|
70
|
+
deletedAt DateTime? @map("deleted_at") @db.DateTime(6)
|
|
71
|
+
|
|
72
|
+
// Relations
|
|
73
|
+
messages AiMessage[]
|
|
74
|
+
|
|
75
|
+
@@index([sessionId], map: "idx_chat_history_session")
|
|
76
|
+
@@index([deletedAt], map: "idx_chat_history_deleted")
|
|
77
|
+
@@map("ai_chat_histories")
|
|
78
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Prisma schema for the dsh-storage database backend — PostgreSQL variant (Prisma 7).
|
|
2
|
+
//
|
|
3
|
+
// Same models as schema.mysql.prisma, with PostgreSQL native types
|
|
4
|
+
// (`@db.Text` instead of `@db.LongText`, `@db.Timestamp` instead of
|
|
5
|
+
// `@db.DateTime`). Client pre-generation and table creation work as described
|
|
6
|
+
// in schema.mysql.prisma's header.
|
|
7
|
+
|
|
8
|
+
generator client {
|
|
9
|
+
provider = "prisma-client"
|
|
10
|
+
output = "../src/generated/postgresql"
|
|
11
|
+
importFileExtension = "js"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
datasource db {
|
|
15
|
+
provider = "postgresql"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// User and assistant messages; tool results update the assistant row's tool_calls.
|
|
19
|
+
model AiMessage {
|
|
20
|
+
id String @id @default(cuid()) @db.Char(36)
|
|
21
|
+
sessionId String @map("session_id") @db.VarChar(100)
|
|
22
|
+
historyId String? @map("history_id") @db.Char(36)
|
|
23
|
+
type String @db.VarChar(50)
|
|
24
|
+
content String @db.Text
|
|
25
|
+
thoughts Json?
|
|
26
|
+
model String? @db.VarChar(100)
|
|
27
|
+
tokens Json?
|
|
28
|
+
toolCalls Json? @map("tool_calls")
|
|
29
|
+
agentId String? @map("agent_id") @db.VarChar(100)
|
|
30
|
+
metadata Json?
|
|
31
|
+
device String? @map("device") @db.VarChar(100)
|
|
32
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0)
|
|
33
|
+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamp(0)
|
|
34
|
+
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
|
|
35
|
+
|
|
36
|
+
// Relations
|
|
37
|
+
history AiChatHistory? @relation(fields: [historyId], references: [id], onDelete: NoAction)
|
|
38
|
+
|
|
39
|
+
@@index([sessionId], map: "idx_session")
|
|
40
|
+
@@index([historyId], map: "idx_history")
|
|
41
|
+
@@index([createdAt], map: "idx_created")
|
|
42
|
+
@@index([deletedAt], map: "idx_deleted")
|
|
43
|
+
@@index([agentId], map: "idx_agent")
|
|
44
|
+
@@index([sessionId, agentId, type, createdAt], map: "idx_session_agent_type_created")
|
|
45
|
+
@@index([sessionId, historyId, deletedAt, type, createdAt, id], map: "idx_tool_call_lookup")
|
|
46
|
+
@@index([sessionId, deletedAt, updatedAt, id], map: "idx_archive_recovery")
|
|
47
|
+
@@map("ai_messages")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Per-session rollup, maintained incrementally by the projector.
|
|
51
|
+
model AiChatHistory {
|
|
52
|
+
id String @id @default(cuid()) @db.Char(36)
|
|
53
|
+
sessionId String @map("session_id") @db.VarChar(100)
|
|
54
|
+
title String? @db.VarChar(255)
|
|
55
|
+
summary String? @db.Text
|
|
56
|
+
messageCount Int @default(0) @map("message_count")
|
|
57
|
+
totalTokens BigInt @default(0) @map("total_tokens")
|
|
58
|
+
firstMessageAt DateTime? @map("first_message_at") @db.Timestamp(6)
|
|
59
|
+
lastMessageAt DateTime? @map("last_message_at") @db.Timestamp(6)
|
|
60
|
+
metadata Json?
|
|
61
|
+
device String? @map("device") @db.VarChar(100)
|
|
62
|
+
deletedAt DateTime? @map("deleted_at") @db.Timestamp(6)
|
|
63
|
+
|
|
64
|
+
// Relations
|
|
65
|
+
messages AiMessage[]
|
|
66
|
+
|
|
67
|
+
@@index([sessionId], map: "idx_chat_history_session")
|
|
68
|
+
@@index([deletedAt], map: "idx_chat_history_deleted")
|
|
69
|
+
@@map("ai_chat_histories")
|
|
70
|
+
}
|