paseo-plugin-zcode-provider 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/LICENSE +21 -0
- package/NOTICE.md +29 -0
- package/README.md +112 -2
- package/client/account.tsx +827 -0
- package/client/diagnostics.tsx +379 -0
- package/client/runtime.tsx +296 -0
- package/client/settings.tsx +84 -0
- package/client/value.tsx +20 -0
- package/icon.svg +1 -0
- package/index.client.tsx +22 -0
- package/index.server.ts +39 -0
- package/package.json +77 -4
- package/paseo-plugin.json +6 -0
- package/server/account.ts +781 -0
- package/server/attachments.ts +150 -0
- package/server/build-info.ts +2 -0
- package/server/conversation.ts +319 -0
- package/server/diagnostics.ts +252 -0
- package/server/discovery/discover.ts +225 -0
- package/server/discovery/manifest.ts +36 -0
- package/server/discovery/types.ts +30 -0
- package/server/errors.ts +36 -0
- package/server/host/bridge.ts +490 -0
- package/server/host/schemas.ts +119 -0
- package/server/logger.ts +19 -0
- package/server/mapping.ts +270 -0
- package/server/models.ts +68 -0
- package/server/persistence.ts +146 -0
- package/server/presentation.ts +470 -0
- package/server/provider-status.ts +81 -0
- package/server/provider.ts +838 -0
- package/server/runtime/managed.ts +501 -0
- package/server/runtime/pins.ts +72 -0
- package/server/runtime-setup.ts +77 -0
- package/server/session-types.ts +49 -0
- package/server/session.ts +866 -0
- package/server/status.ts +89 -0
- package/server/timeline.ts +18 -0
- package/server/usage.ts +362 -0
- package/server/vendor/zcode/LICENSE +202 -0
- package/server/vendor/zcode/NOTICE.md +70 -0
- package/server/vendor/zcode/THIRD-PARTY-NOTICES.md +37 -0
- package/server/vendor/zcode/packages/model-option-map/src/compiler.ts +92 -0
- package/server/vendor/zcode/packages/model-option-map/src/evaluator.ts +214 -0
- package/server/vendor/zcode/packages/model-option-map/src/index.ts +5 -0
- package/server/vendor/zcode/packages/model-option-map/src/merge-patch.ts +97 -0
- package/server/vendor/zcode/packages/model-option-map/src/option-maps.ts +42 -0
- package/server/vendor/zcode/packages/model-option-map/src/parser.ts +240 -0
- package/server/vendor/zcode/packages/model-option-map/src/tokenizer.ts +141 -0
- package/server/vendor/zcode/packages/model-option-map/src/types.ts +36 -0
- package/server/vendor/zcode/packages/rpc/src/buffer.ts +88 -0
- package/server/vendor/zcode/packages/rpc/src/channelClient.ts +275 -0
- package/server/vendor/zcode/packages/rpc/src/channelServer.ts +283 -0
- package/server/vendor/zcode/packages/rpc/src/channels.shared.ts +66 -0
- package/server/vendor/zcode/packages/rpc/src/channels.ts +4 -0
- package/server/vendor/zcode/packages/rpc/src/delayedChannel.ts +17 -0
- package/server/vendor/zcode/packages/rpc/src/foundation.ts +260 -0
- package/server/vendor/zcode/packages/rpc/src/protocol.ts +397 -0
- package/server/vendor/zcode/packages/rpc/src/proxy-channel.ts +213 -0
- package/server/vendor/zcode/packages/rpc/src/serialization.ts +291 -0
- package/server/vendor/zcode/packages/shared/src/account-provider-state.ts +13 -0
- package/server/vendor/zcode/packages/shared/src/background-bash-output.ts +24 -0
- package/server/vendor/zcode/packages/shared/src/background-task-controls.ts +404 -0
- package/server/vendor/zcode/packages/shared/src/bash-output-display.ts +11 -0
- package/server/vendor/zcode/packages/shared/src/bots.ts +558 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/backend.ts +80 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/command-metadata.ts +170 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/commands.ts +588 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/result.ts +130 -0
- package/server/vendor/zcode/packages/shared/src/browser-use/snapshot.ts +77 -0
- package/server/vendor/zcode/packages/shared/src/command-types.ts +96 -0
- package/server/vendor/zcode/packages/shared/src/config-schema.ts +8 -0
- package/server/vendor/zcode/packages/shared/src/cuaAccessibilitySettings.ts +41 -0
- package/server/vendor/zcode/packages/shared/src/database-startup.ts +178 -0
- package/server/vendor/zcode/packages/shared/src/env.ts +63 -0
- package/server/vendor/zcode/packages/shared/src/execution-output-preview.ts +14 -0
- package/server/vendor/zcode/packages/shared/src/localTtft.ts +192 -0
- package/server/vendor/zcode/packages/shared/src/model-config.ts +123 -0
- package/server/vendor/zcode/packages/shared/src/model-execution.ts +23 -0
- package/server/vendor/zcode/packages/shared/src/model-provider-family.ts +171 -0
- package/server/vendor/zcode/packages/shared/src/model-provider-types.ts +81 -0
- package/server/vendor/zcode/packages/shared/src/model-selection.ts +60 -0
- package/server/vendor/zcode/packages/shared/src/oauth.ts +203 -0
- package/server/vendor/zcode/packages/shared/src/official-mcp-auth.ts +272 -0
- package/server/vendor/zcode/packages/shared/src/official-mcp-tool-error.ts +68 -0
- package/server/vendor/zcode/packages/shared/src/process-diagnostic.ts +42 -0
- package/server/vendor/zcode/packages/shared/src/protocol.ts +369 -0
- package/server/vendor/zcode/packages/shared/src/provider-family-connection-selection.ts +32 -0
- package/server/vendor/zcode/packages/shared/src/remoteAssetInstallMode.ts +11 -0
- package/server/vendor/zcode/packages/shared/src/remoteResourcePackages.ts +91 -0
- package/server/vendor/zcode/packages/shared/src/runtimeEnv.ts +315 -0
- package/server/vendor/zcode/packages/shared/src/settings-source.ts +10 -0
- package/server/vendor/zcode/packages/shared/src/usage-quota.ts +74 -0
- package/server/vendor/zcode/packages/shared/src/usage-stats.ts +462 -0
- package/server/vendor/zcode/packages/shared/src/validationAppSettings.ts +562 -0
- package/server/vendor/zcode/packages/shared/src/workspacePurpose.ts +2 -0
- package/server/vendor/zcode/packages/shared/src/wslUserValidation.ts +33 -0
- package/server/vendor/zcode/packages/shared/src/zcode-agent-policy.ts +25 -0
- package/server/vendor/zcode/packages/shared/src/zcode-media-policy.ts +10 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol/index.ts +3720 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-legacy-types.ts +529 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/apply.ts +218 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/attachment-ref.ts +14 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/command.ts +482 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/core.ts +100 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/create-workflow-display.ts +156 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/cuaPermission.ts +41 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/delta.ts +147 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/input-intent.ts +69 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/rows.ts +432 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/session-config.ts +43 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/sessions-index-workflow-activity.ts +195 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/sessions-index.ts +85 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/shared-context-import.ts +38 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/shared-context-ref.ts +10 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/snapshot.ts +509 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/submission.ts +5 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/toolDisplay.ts +264 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/transport.ts +1152 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-assembler.ts +574 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-binary.ts +65 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-codec.ts +240 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire-fault.ts +39 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/wire.ts +159 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-artifacts.ts +291 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-observation-display.ts +281 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-row-meta.ts +163 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-run-settings-command.ts +62 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-runs-delta.ts +517 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workflow-runs.ts +512 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workspace-config.ts +81 -0
- package/server/vendor/zcode/packages/shared/src/zcode-protocol-v4/workspace-hook-review.ts +210 -0
- package/server/vendor/zcode/packages/shared/src/zcode-task-mode-schema.ts +6 -0
- package/server/vendor/zcode/packages/shared/src/zcode-task-types-core.ts +1187 -0
- package/server/vendor/zcode/packages/shared/src/zcodeEndpoint.ts +296 -0
- package/server/vendor/zcode/provenance.json +405 -0
- package/shared/account.ts +201 -0
- package/shared/diagnostics.ts +52 -0
- package/shared/runtime-setup.ts +73 -0
- package/vendor/acp-registry/LICENSE +176 -0
- package/vendor/paseo/LICENSE +211 -0
|
@@ -0,0 +1,1152 @@
|
|
|
1
|
+
import { localTtftFactsSchema } from "../localTtft.js";
|
|
2
|
+
/* eslint-disable max-lines -- 三个 topic 的 logical/physical/candidate schema 必须共享同一通用传输声明,避免跨文件分叉。 */
|
|
3
|
+
// 传输外壳:连接握手 / 订阅 / 帧信封。
|
|
4
|
+
// 阶段为类型占位(后半接通道层时启用),数据形状已按规范定稿。
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { APP_USAGE_RANGES, appUsageSnapshotSchema } from "../usage-stats.js";
|
|
7
|
+
import { zcodeWorkspaceRefSchema } from "../zcode-protocol-legacy-types.js";
|
|
8
|
+
import {
|
|
9
|
+
PROTOCOL_V4_LIMITS,
|
|
10
|
+
V4_WIRE_PROTOCOL_VERSION,
|
|
11
|
+
conversationRowTargetSchema,
|
|
12
|
+
timestampSchema,
|
|
13
|
+
} from "./core.js";
|
|
14
|
+
import { conversationDeltaSchema } from "./delta.js";
|
|
15
|
+
import { conversationRowSchema, toolCallRowSchema } from "./rows.js";
|
|
16
|
+
import { sessionsIndexDeltaSchema, sessionsIndexSnapshotSchema } from "./sessions-index.js";
|
|
17
|
+
import { WORKFLOW_RUN_STOP_REASONS } from "./workflow-observation-display.js";
|
|
18
|
+
import { conversationSnapshotSchema } from "./snapshot.js";
|
|
19
|
+
import { workspaceConfigDeltaSchema, workspaceConfigSnapshotSchema } from "./workspace-config.js";
|
|
20
|
+
import { createTopicWireFrameSchema, topicWireFrameCandidateSchema } from "./wire.js";
|
|
21
|
+
|
|
22
|
+
// ── 连接与握手 ──
|
|
23
|
+
export const hostCapabilitiesSchema = z.object({
|
|
24
|
+
nativeDialogs: z.boolean(),
|
|
25
|
+
localTerminal: z.boolean(),
|
|
26
|
+
// ws binary(relay 链路探测用)。
|
|
27
|
+
binaryFrames: z.boolean(),
|
|
28
|
+
compression: z.enum(["none", "permessage-deflate"]),
|
|
29
|
+
// Wire-compatible:旧 Host 缺失等价于 false;调用方必须用 === true 判断。
|
|
30
|
+
workspaceHookReview: z.boolean().optional(),
|
|
31
|
+
independentPlanState: z.boolean().optional(),
|
|
32
|
+
/**
|
|
33
|
+
* 本 Host 会发 `workflowRun.*` 键级增量(delta.ts 的两条 op),因而 `workflowRuns` 的
|
|
34
|
+
* actors / nodes 可以到 1024 而不是旧界的 256。没有这个位的消费者收到的仍是整键
|
|
35
|
+
* `state.updated`,并且先经 `clampWorkflowRunsForLegacy` 裁到旧界。
|
|
36
|
+
*/
|
|
37
|
+
workflowRunDeltas: z.boolean().optional(),
|
|
38
|
+
});
|
|
39
|
+
export type HostCapabilities = z.infer<typeof hostCapabilitiesSchema>;
|
|
40
|
+
|
|
41
|
+
export const helloMessageSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
kind: z.literal("hello"),
|
|
44
|
+
protocolVersion: z.literal(V4_WIRE_PROTOCOL_VERSION),
|
|
45
|
+
connectionId: z.string(),
|
|
46
|
+
clientMode: z.enum(["desktop-continuous", "web-remote-replayable"]),
|
|
47
|
+
deliveryProfile: z.enum(["continuous", "replayable"]),
|
|
48
|
+
// 首次时钟校准。
|
|
49
|
+
serverTime: timestampSchema,
|
|
50
|
+
capabilities: hostCapabilitiesSchema,
|
|
51
|
+
// 此处只留位。
|
|
52
|
+
auth: z.object({ userId: z.string().optional() }),
|
|
53
|
+
})
|
|
54
|
+
.strict()
|
|
55
|
+
.superRefine((hello, context) => {
|
|
56
|
+
const expectedProfile = hello.clientMode === "desktop-continuous" ? "continuous" : "replayable";
|
|
57
|
+
if (hello.deliveryProfile !== expectedProfile) {
|
|
58
|
+
context.addIssue({
|
|
59
|
+
code: "custom",
|
|
60
|
+
message: "trusted clientMode and deliveryProfile must match",
|
|
61
|
+
path: ["deliveryProfile"],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
export type HelloMessage = z.infer<typeof helloMessageSchema>;
|
|
66
|
+
|
|
67
|
+
// clientMode 连接级注册一次,不进 command 信封。
|
|
68
|
+
export const clientHelloSchema = z
|
|
69
|
+
.object({
|
|
70
|
+
kind: z.literal("clientHello"),
|
|
71
|
+
protocolVersion: z.literal(V4_WIRE_PROTOCOL_VERSION),
|
|
72
|
+
clientId: z.string(),
|
|
73
|
+
clientKind: z.enum(["desktop", "web", "mobileRemote", "mobileApp"]).optional(),
|
|
74
|
+
appVersion: z.string(),
|
|
75
|
+
// 缺失代表旧客户端,不具备 Settings-centered review UI。
|
|
76
|
+
capabilities: z
|
|
77
|
+
.object({
|
|
78
|
+
workspaceHookReviewUi: z.boolean().optional(),
|
|
79
|
+
/**
|
|
80
|
+
* 本客户端认得 `workflowRun.*` 增量。⚠ 声明规则是**单向**的:客户端只有在 Host 的
|
|
81
|
+
* hello 里见到 `workflowRunDeltas === true` 时才能带上这个键——这个 capabilities 对象
|
|
82
|
+
* 是 `.strict()` 的,老 Host 见到不认识的键会整条 clientHello 解析失败、连接握不上手。
|
|
83
|
+
*/
|
|
84
|
+
workflowRunDeltas: z.boolean().optional(),
|
|
85
|
+
})
|
|
86
|
+
.strict()
|
|
87
|
+
.optional(),
|
|
88
|
+
})
|
|
89
|
+
.strict();
|
|
90
|
+
export type ClientHello = z.infer<typeof clientHelloSchema>;
|
|
91
|
+
|
|
92
|
+
export function hostSupportsWorkspaceHookReview(capabilities: HostCapabilities): boolean {
|
|
93
|
+
return capabilities.workspaceHookReview === true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function clientSupportsWorkspaceHookReview(clientHello: ClientHello): boolean {
|
|
97
|
+
return clientHello.capabilities?.workspaceHookReviewUi === true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function hostSupportsWorkflowRunDeltas(capabilities: HostCapabilities): boolean {
|
|
101
|
+
return capabilities.workflowRunDeltas === true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function clientSupportsWorkflowRunDeltas(clientHello: ClientHello): boolean {
|
|
105
|
+
return clientHello.capabilities?.workflowRunDeltas === true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── §3.1 订阅 ──
|
|
109
|
+
export const subscribeParamsSchema = z
|
|
110
|
+
.object({
|
|
111
|
+
// "conversation/<sessionId>" | "sessions-index/<workspaceId>" | ...
|
|
112
|
+
topic: z.string(),
|
|
113
|
+
// 水位不变量:仅当客户端真持有该时刻一致状态才允许带。
|
|
114
|
+
base: z.object({ logEpoch: z.string(), seq: z.number() }).optional(),
|
|
115
|
+
// QoS hint,只影响调度,不影响语义。
|
|
116
|
+
visibility: z.enum(["foreground", "background"]).optional(),
|
|
117
|
+
})
|
|
118
|
+
.strict();
|
|
119
|
+
export type SubscribeParams = z.infer<typeof subscribeParamsSchema>;
|
|
120
|
+
|
|
121
|
+
export const subscribeAckSchema = z.object({
|
|
122
|
+
subscriptionId: z.string(),
|
|
123
|
+
// resume = base 有效,从 base.seq 续传增量;否则 snapshot。
|
|
124
|
+
mode: z.enum(["snapshot", "resume"]),
|
|
125
|
+
logEpoch: z.string(),
|
|
126
|
+
});
|
|
127
|
+
export type SubscribeAck = z.infer<typeof subscribeAckSchema>;
|
|
128
|
+
|
|
129
|
+
const openTimingMsSchema = z.number().int().nonnegative().optional();
|
|
130
|
+
const openTimingCountSchema = z.number().int().nonnegative().optional();
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Desktop session 首次打开的低频诊断 timing;只挂在 conversation subscribe ACK,
|
|
134
|
+
* 不进入 snapshot、delta、sessions-index 或 workspace-config。
|
|
135
|
+
*/
|
|
136
|
+
export const conversationOpenTimingSchema = z
|
|
137
|
+
.object({
|
|
138
|
+
version: z.literal(1),
|
|
139
|
+
hostPrepareMs: openTimingMsSchema,
|
|
140
|
+
providerRegistrySyncMs: openTimingMsSchema,
|
|
141
|
+
taskMetaReadMs: openTimingMsSchema,
|
|
142
|
+
cliRequestMs: openTimingMsSchema,
|
|
143
|
+
cliBootstrapMs: openTimingMsSchema,
|
|
144
|
+
cliSessionRestoreMs: openTimingMsSchema,
|
|
145
|
+
initialFrameEncodeMs: openTimingMsSchema,
|
|
146
|
+
cliProcessState: z.enum(["spawned", "reused"]).optional(),
|
|
147
|
+
sessionRuntimeState: z.enum(["cold", "warm"]).optional(),
|
|
148
|
+
snapshotRowCount: openTimingCountSchema,
|
|
149
|
+
})
|
|
150
|
+
.strict();
|
|
151
|
+
export type ConversationOpenTiming = z.infer<typeof conversationOpenTimingSchema>;
|
|
152
|
+
|
|
153
|
+
const conversationSubscribeAckSchema = subscribeAckSchema.extend({
|
|
154
|
+
openTiming: conversationOpenTimingSchema.optional(),
|
|
155
|
+
});
|
|
156
|
+
export type ConversationSubscribeAck = z.infer<typeof conversationSubscribeAckSchema>;
|
|
157
|
+
|
|
158
|
+
// ── 帧信封(泛型帧用工厂构造具体 topic 的 schema)──
|
|
159
|
+
export function createTopicFrameSchema<S extends z.ZodTypeAny, D extends z.ZodTypeAny>(
|
|
160
|
+
snapshotSchema: S,
|
|
161
|
+
deltaSchema: D,
|
|
162
|
+
) {
|
|
163
|
+
return z.object({
|
|
164
|
+
topic: z.string(),
|
|
165
|
+
// 代际标识,防旧流交错。
|
|
166
|
+
subscriptionId: z.string(),
|
|
167
|
+
// 区间记账 (fromSeq, toSeq];snapshot 帧 fromSeq 固定为 0。
|
|
168
|
+
fromSeq: z.number(),
|
|
169
|
+
toSeq: z.number(),
|
|
170
|
+
// CLI 时钟,供 clockOffset 估计。
|
|
171
|
+
sentAt: timestampSchema,
|
|
172
|
+
payload: z.discriminatedUnion("kind", [
|
|
173
|
+
z.object({ kind: z.literal("snapshot"), snapshot: snapshotSchema }),
|
|
174
|
+
z.object({ kind: z.literal("deltas"), deltas: z.array(deltaSchema) }),
|
|
175
|
+
]),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface TopicFrame<S, D> {
|
|
180
|
+
topic: string;
|
|
181
|
+
subscriptionId: string;
|
|
182
|
+
fromSeq: number;
|
|
183
|
+
toSeq: number;
|
|
184
|
+
sentAt: Timestamp;
|
|
185
|
+
payload: { kind: "snapshot"; snapshot: S } | { kind: "deltas"; deltas: D[] };
|
|
186
|
+
}
|
|
187
|
+
type Timestamp = z.infer<typeof timestampSchema>;
|
|
188
|
+
|
|
189
|
+
// conversation topic 的具体帧 schema(五件套实例化)。
|
|
190
|
+
// 传输外壳/黄金测试用它做帧合法性校验;host 通道层 复用。
|
|
191
|
+
export const conversationTopicFrameSchema = createTopicFrameSchema(
|
|
192
|
+
conversationSnapshotSchema,
|
|
193
|
+
conversationDeltaSchema,
|
|
194
|
+
)
|
|
195
|
+
.extend({
|
|
196
|
+
ttft: localTtftFactsSchema.optional(),
|
|
197
|
+
ttftRelated: z.array(localTtftFactsSchema).max(16).optional(),
|
|
198
|
+
})
|
|
199
|
+
.superRefine((frame, context) => {
|
|
200
|
+
if (!frame.topic.startsWith("conversation/") || frame.topic.length === "conversation/".length) {
|
|
201
|
+
context.addIssue({ code: "custom", message: "invalid conversation topic", path: ["topic"] });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
export type ConversationTopicFrame = z.infer<typeof conversationTopicFrameSchema>;
|
|
205
|
+
export const conversationTopicWireFrameSchema = createTopicWireFrameSchema(
|
|
206
|
+
conversationTopicFrameSchema,
|
|
207
|
+
).superRefine((wire, context) => {
|
|
208
|
+
if (!wire.topic.startsWith("conversation/") || wire.topic.length === "conversation/".length) {
|
|
209
|
+
context.addIssue({ code: "custom", message: "invalid conversation topic", path: ["topic"] });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
export type ConversationTopicWireFrame = z.infer<typeof conversationTopicWireFrameSchema>;
|
|
213
|
+
export const conversationTopicWireCandidateSchema = topicWireFrameCandidateSchema.superRefine(
|
|
214
|
+
(wire, context) => {
|
|
215
|
+
if (!wire.topic.startsWith("conversation/") || wire.topic.length === "conversation/".length) {
|
|
216
|
+
context.addIssue({ code: "custom", message: "invalid conversation topic", path: ["topic"] });
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
export type ConversationTopicWireCandidate = z.infer<typeof conversationTopicWireCandidateSchema>;
|
|
221
|
+
|
|
222
|
+
// sessions-index topic 的具体帧 schema(五件套实例化;侧栏列表活性数据源)。
|
|
223
|
+
export const sessionsIndexTopicFrameSchema = createTopicFrameSchema(
|
|
224
|
+
sessionsIndexSnapshotSchema,
|
|
225
|
+
sessionsIndexDeltaSchema,
|
|
226
|
+
).superRefine((frame, context) => {
|
|
227
|
+
if (
|
|
228
|
+
!frame.topic.startsWith("sessions-index/") ||
|
|
229
|
+
frame.topic.length === "sessions-index/".length
|
|
230
|
+
) {
|
|
231
|
+
context.addIssue({ code: "custom", message: "invalid sessions-index topic", path: ["topic"] });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
export type SessionsIndexTopicFrame = z.infer<typeof sessionsIndexTopicFrameSchema>;
|
|
235
|
+
export const sessionsIndexTopicWireFrameSchema = createTopicWireFrameSchema(
|
|
236
|
+
sessionsIndexTopicFrameSchema,
|
|
237
|
+
).superRefine((wire, context) => {
|
|
238
|
+
if (!wire.topic.startsWith("sessions-index/") || wire.topic.length === "sessions-index/".length) {
|
|
239
|
+
context.addIssue({ code: "custom", message: "invalid sessions-index topic", path: ["topic"] });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
export type SessionsIndexTopicWireFrame = z.infer<typeof sessionsIndexTopicWireFrameSchema>;
|
|
243
|
+
export const sessionsIndexTopicWireCandidateSchema = topicWireFrameCandidateSchema.superRefine(
|
|
244
|
+
(wire, context) => {
|
|
245
|
+
if (
|
|
246
|
+
!wire.topic.startsWith("sessions-index/") ||
|
|
247
|
+
wire.topic.length === "sessions-index/".length
|
|
248
|
+
) {
|
|
249
|
+
context.addIssue({
|
|
250
|
+
code: "custom",
|
|
251
|
+
message: "invalid sessions-index topic",
|
|
252
|
+
path: ["topic"],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
export type SessionsIndexTopicWireCandidate = z.infer<typeof sessionsIndexTopicWireCandidateSchema>;
|
|
258
|
+
|
|
259
|
+
// workspace-config topic 的具体帧 schema(additive 变更;配置目录活性数据源)。
|
|
260
|
+
export const workspaceConfigTopicFrameSchema = createTopicFrameSchema(
|
|
261
|
+
workspaceConfigSnapshotSchema,
|
|
262
|
+
workspaceConfigDeltaSchema,
|
|
263
|
+
).superRefine((frame, context) => {
|
|
264
|
+
if (
|
|
265
|
+
!frame.topic.startsWith("workspace-config/") ||
|
|
266
|
+
frame.topic.length === "workspace-config/".length
|
|
267
|
+
) {
|
|
268
|
+
context.addIssue({
|
|
269
|
+
code: "custom",
|
|
270
|
+
message: "invalid workspace-config topic",
|
|
271
|
+
path: ["topic"],
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
export type WorkspaceConfigTopicFrame = z.infer<typeof workspaceConfigTopicFrameSchema>;
|
|
276
|
+
export const workspaceConfigTopicWireFrameSchema = createTopicWireFrameSchema(
|
|
277
|
+
workspaceConfigTopicFrameSchema,
|
|
278
|
+
).superRefine((wire, context) => {
|
|
279
|
+
if (
|
|
280
|
+
!wire.topic.startsWith("workspace-config/") ||
|
|
281
|
+
wire.topic.length === "workspace-config/".length
|
|
282
|
+
) {
|
|
283
|
+
context.addIssue({
|
|
284
|
+
code: "custom",
|
|
285
|
+
message: "invalid workspace-config topic",
|
|
286
|
+
path: ["topic"],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
export type WorkspaceConfigTopicWireFrame = z.infer<typeof workspaceConfigTopicWireFrameSchema>;
|
|
291
|
+
export const workspaceConfigTopicWireCandidateSchema = topicWireFrameCandidateSchema.superRefine(
|
|
292
|
+
(wire, context) => {
|
|
293
|
+
if (
|
|
294
|
+
!wire.topic.startsWith("workspace-config/") ||
|
|
295
|
+
wire.topic.length === "workspace-config/".length
|
|
296
|
+
) {
|
|
297
|
+
context.addIssue({
|
|
298
|
+
code: "custom",
|
|
299
|
+
message: "invalid workspace-config topic",
|
|
300
|
+
path: ["topic"],
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
export type WorkspaceConfigTopicWireCandidate = z.infer<
|
|
306
|
+
typeof workspaceConfigTopicWireCandidateSchema
|
|
307
|
+
>;
|
|
308
|
+
|
|
309
|
+
/** 三个生产 topic 的公共 logical / physical 路由面。 */
|
|
310
|
+
export const routedTopicFrameSchema = z.union([
|
|
311
|
+
conversationTopicFrameSchema,
|
|
312
|
+
sessionsIndexTopicFrameSchema,
|
|
313
|
+
workspaceConfigTopicFrameSchema,
|
|
314
|
+
]);
|
|
315
|
+
export type RoutedTopicFrame = z.infer<typeof routedTopicFrameSchema>;
|
|
316
|
+
export const routedTopicWireFrameSchema = z.union([
|
|
317
|
+
conversationTopicWireFrameSchema,
|
|
318
|
+
sessionsIndexTopicWireFrameSchema,
|
|
319
|
+
workspaceConfigTopicWireFrameSchema,
|
|
320
|
+
]);
|
|
321
|
+
export type RoutedTopicWireFrame = z.infer<typeof routedTopicWireFrameSchema>;
|
|
322
|
+
export const routedTopicWireCandidateSchema = z.union([
|
|
323
|
+
conversationTopicWireCandidateSchema,
|
|
324
|
+
sessionsIndexTopicWireCandidateSchema,
|
|
325
|
+
workspaceConfigTopicWireCandidateSchema,
|
|
326
|
+
]);
|
|
327
|
+
export type RoutedTopicWireCandidate = z.infer<typeof routedTopicWireCandidateSchema>;
|
|
328
|
+
|
|
329
|
+
// ── v4 RPC 出入口(host 通道层)──
|
|
330
|
+
// 载体复用现有 JSON-RPC(stdio NDJSON / socket),方法名带 v4/ 前缀与旧协议并存;
|
|
331
|
+
// 旧 session/* 方法删除后,这里就是唯一协议面。
|
|
332
|
+
export const V4_METHODS = {
|
|
333
|
+
connectionFlow: "v4/connection/flow",
|
|
334
|
+
controllerSubscribe: "v4/controller/subscribe",
|
|
335
|
+
controllerResync: "v4/controller/resync",
|
|
336
|
+
controllerUnsubscribe: "v4/controller/unsubscribe",
|
|
337
|
+
conversationSubscribe: "v4/conversation/subscribe",
|
|
338
|
+
conversationResync: "v4/conversation/resync",
|
|
339
|
+
conversationUnsubscribe: "v4/conversation/unsubscribe",
|
|
340
|
+
// 行分页 query(rows/range):只读、无状态、超时重发安全。
|
|
341
|
+
conversationRowsRange: "v4/conversation/rowsRange",
|
|
342
|
+
// 当前有效分支的终态 ExitPlanMode 目录;只读、无状态、超时重发安全。
|
|
343
|
+
conversationPlans: "v4/conversation/plans",
|
|
344
|
+
conversationFileChanges: "v4/conversation/fileChanges",
|
|
345
|
+
backgroundBashOutput: "v4/conversation/backgroundBashOutput",
|
|
346
|
+
conversationFileRewindPreview: "v4/conversation/fileRewindPreview",
|
|
347
|
+
// workflow run 的事件日志分页(详情页审计面):只读、无状态、超时重发安全。
|
|
348
|
+
// 新方法天然偏斜安全——旧桌面根本不会调用它。
|
|
349
|
+
conversationWorkflowRunEvents: "v4/conversation/workflowRunEvents",
|
|
350
|
+
conversationWorkflowRuns: "v4/conversation/workflowRuns",
|
|
351
|
+
// workflow run 的用户面产物。三条同族:
|
|
352
|
+
// 只读、无状态、超时重发安全;schema 在 workflow-artifacts.ts(那边还有术语消歧)。
|
|
353
|
+
// Artifacts 产物清单(冷恢复与中枢详情的 durable 读法)
|
|
354
|
+
// ArtifactData 预置看板的条目分页(cursor = journal sequence)
|
|
355
|
+
// ArtifactRead 内容产物的字节,≤ 512 KiB 一块,形状逐字照 attachmentRead
|
|
356
|
+
conversationWorkflowRunArtifacts: "v4/conversation/workflowRunArtifacts",
|
|
357
|
+
conversationWorkflowRunArtifactData: "v4/conversation/workflowRunArtifactData",
|
|
358
|
+
conversationWorkflowRunArtifactRead: "v4/conversation/workflowRunArtifactRead",
|
|
359
|
+
// workflow run 的工作区 transcript。两条同族,
|
|
360
|
+
// 照产物的 ①/③ 拆法:Workspace 是轻行清单(不带正文),NodeResult 是一个节点的有界正文。
|
|
361
|
+
conversationWorkflowRunWorkspace: "v4/conversation/workflowRunWorkspace",
|
|
362
|
+
conversationWorkflowRunNodeResult: "v4/conversation/workflowRunNodeResult",
|
|
363
|
+
// usage query(模式同 rows/range:只读、无状态、超时重发安全)。
|
|
364
|
+
// usage 事实源在 CLI 的 session 库(model_usage/turn_usage 聚合),host 侧无副本,
|
|
365
|
+
// 故收敛为 v4 query 而非 host 直连;旧词 usage/stats、session/usage 就此消费清零。
|
|
366
|
+
usageStats: "v4/usage/stats",
|
|
367
|
+
conversationUsage: "v4/conversation/usage",
|
|
368
|
+
// 附件事务:禁止 full-data RPC。每个 chunk 的 decoded bytes <=512KiB,
|
|
369
|
+
// renderer->host Channel 与 host->CLI NDJSON 都必须逐 request 证明 <=1MiB。
|
|
370
|
+
attachmentBegin: "v4/attachment/begin",
|
|
371
|
+
attachmentChunk: "v4/attachment/chunk",
|
|
372
|
+
attachmentCommit: "v4/attachment/commit",
|
|
373
|
+
attachmentAbort: "v4/attachment/abort",
|
|
374
|
+
// 已发送图片预览:只读、按 session row 授权,响应仍按 512KiB 分块。
|
|
375
|
+
attachmentRead: "v4/attachment/read",
|
|
376
|
+
// Share 读取 userInput 附件:允许 text/plain 等非媒体类型,仍按 row/index 授权并分块返回。
|
|
377
|
+
conversationAttachmentRead: "v4/conversation/attachmentRead",
|
|
378
|
+
// Share 预检 userInput 附件元数据:只做 row/index 授权和 stat,不读取完整文件。
|
|
379
|
+
conversationAttachmentStat: "v4/conversation/attachmentStat",
|
|
380
|
+
// Desktop local 已发送视频:只返回经过同一 row/index 授权的本地播放源。
|
|
381
|
+
attachmentPreviewSource: "v4/attachment/previewSource",
|
|
382
|
+
commandsQuery: "v4/commands/query",
|
|
383
|
+
command: "v4/command",
|
|
384
|
+
} as const;
|
|
385
|
+
export type V4Method = (typeof V4_METHODS)[keyof typeof V4_METHODS];
|
|
386
|
+
|
|
387
|
+
/** 按任务授权的有界输出查询,不接受任意路径或调用方扩大的读取预算。 */
|
|
388
|
+
export const v4BackgroundBashOutputParamsSchema = z.strictObject({
|
|
389
|
+
sessionId: z.string().min(1),
|
|
390
|
+
workId: z.string().min(1),
|
|
391
|
+
});
|
|
392
|
+
export type V4BackgroundBashOutputParams = z.infer<typeof v4BackgroundBashOutputParamsSchema>;
|
|
393
|
+
|
|
394
|
+
export const v4ConnectionFlowStateSchema = z.enum(["saturated", "drained", "closed"]);
|
|
395
|
+
export type V4ConnectionFlowState = z.infer<typeof v4ConnectionFlowStateSchema>;
|
|
396
|
+
|
|
397
|
+
export const v4ConnectionFlowParamsSchema = z
|
|
398
|
+
.object({
|
|
399
|
+
connectionId: z.string().min(1),
|
|
400
|
+
state: v4ConnectionFlowStateSchema,
|
|
401
|
+
})
|
|
402
|
+
.strict();
|
|
403
|
+
export type V4ConnectionFlowParams = z.infer<typeof v4ConnectionFlowParamsSchema>;
|
|
404
|
+
|
|
405
|
+
export const v4ConnectionFlowResultSchema = z.object({}).strict();
|
|
406
|
+
|
|
407
|
+
export const V4_NOTIFICATIONS = {
|
|
408
|
+
// 下行帧(snapshot / deltas),params = ConversationTopicFrame。
|
|
409
|
+
conversationFrame: "v4/conversation/frame",
|
|
410
|
+
// 仅 live ingest 的无正文事实;不进入 topic snapshot/recovery。
|
|
411
|
+
conversationTelemetryFact: "v4/telemetry/event",
|
|
412
|
+
localTtftFacts: "v4/telemetry/local-ttft",
|
|
413
|
+
// 仅当前进程 live ToolCallResult 产生;历史与 replayable 链路不得补造。
|
|
414
|
+
cuaPermissionObservation: "v4/cua/permission-observation",
|
|
415
|
+
} as const;
|
|
416
|
+
|
|
417
|
+
/** 3.3.6 SSH 历史任务归属证明与 sessions-index 冷种子共享同一有界窗口。 */
|
|
418
|
+
export const MAX_LEGACY_TASK_IDS_PER_SUBSCRIBE = 200;
|
|
419
|
+
|
|
420
|
+
// subscribe 请求 = 通用 SubscribeParams + connectionId(重订阅替换按
|
|
421
|
+
// (connectionId, topic) 判定;stdio 单管道场景由 host 为每个下游客户端分配)。
|
|
422
|
+
export const v4ConversationSubscribeParamsSchema = subscribeParamsSchema.extend({
|
|
423
|
+
connectionId: z.string(),
|
|
424
|
+
clientMode: z.enum(["desktop-continuous", "web-remote-replayable"]),
|
|
425
|
+
// 当前可信 attachment 的 workspace;cold resume 优先使用它恢复身份,live 不消费。
|
|
426
|
+
workspace: zcodeWorkspaceRefSchema.optional(),
|
|
427
|
+
/** host 从当前 remote workspace 的 tasks-index 精确读取的旧任务归属 allowlist。 */
|
|
428
|
+
legacyTaskIds: z.array(z.string().min(1)).max(MAX_LEGACY_TASK_IDS_PER_SUBSCRIBE).optional(),
|
|
429
|
+
// 仅供 host→CLI cold resume 使用;live conversation 不消费该 hint。
|
|
430
|
+
resumeThoughtLevel: z.string().trim().min(1).optional(),
|
|
431
|
+
/**
|
|
432
|
+
* 这条订阅收不收 `workflowRun.*` 键级增量。与 `clientMode` 同族:由**可信 host** 从该连接的
|
|
433
|
+
* clientHello 注入,面向 UI 的 subscribe 选不了它——一个客户端能不能认得增量是连接的事实,
|
|
434
|
+
* 不是某一次订阅可以自选的口味。缺席 = 按旧消费者处理(整键 patch + 旧界裁剪)。
|
|
435
|
+
*/
|
|
436
|
+
workflowRunDeltas: z.boolean().optional(),
|
|
437
|
+
});
|
|
438
|
+
export type V4ConversationSubscribeParams = z.infer<typeof v4ConversationSubscribeParamsSchema>;
|
|
439
|
+
|
|
440
|
+
// 公共 subscribe response 严格只含 ACK。initial snapshot/resume 由 server 的
|
|
441
|
+
// request-scoped post-response outbox 在 response line 之后作为 owned notification 发出。
|
|
442
|
+
export const v4ConversationSubscribeResultSchema = z
|
|
443
|
+
.object({
|
|
444
|
+
ack: conversationSubscribeAckSchema,
|
|
445
|
+
})
|
|
446
|
+
.strict();
|
|
447
|
+
export type V4ConversationSubscribeResult = z.infer<typeof v4ConversationSubscribeResultSchema>;
|
|
448
|
+
|
|
449
|
+
// sessions-index 与 conversation 共用同一 subscribe RPC;公共 response 同样 ACK-only。
|
|
450
|
+
export const v4SessionsIndexSubscribeResultSchema = z
|
|
451
|
+
.object({
|
|
452
|
+
ack: subscribeAckSchema,
|
|
453
|
+
})
|
|
454
|
+
.strict();
|
|
455
|
+
export type V4SessionsIndexSubscribeResult = z.infer<typeof v4SessionsIndexSubscribeResultSchema>;
|
|
456
|
+
|
|
457
|
+
// workspace-config 也只回 ACK;initial frame 走同一 post-response notification 时序。
|
|
458
|
+
export const v4WorkspaceConfigSubscribeResultSchema = z
|
|
459
|
+
.object({
|
|
460
|
+
ack: subscribeAckSchema,
|
|
461
|
+
})
|
|
462
|
+
.strict();
|
|
463
|
+
export type V4WorkspaceConfigSubscribeResult = z.infer<
|
|
464
|
+
typeof v4WorkspaceConfigSubscribeResultSchema
|
|
465
|
+
>;
|
|
466
|
+
|
|
467
|
+
// 活跃订阅的 same-sub 恢复。topic/connection/profile 必须由 host owned registry
|
|
468
|
+
// 反查,客户端只能声明自己已确认的水位与是否强制 snapshot。
|
|
469
|
+
export const conversationResyncParamsSchema = z
|
|
470
|
+
.object({
|
|
471
|
+
subscriptionId: z.string().trim().min(1).max(1024),
|
|
472
|
+
base: z
|
|
473
|
+
.object({
|
|
474
|
+
logEpoch: z.string().trim().min(1).max(1024),
|
|
475
|
+
seq: z.number().int().nonnegative(),
|
|
476
|
+
})
|
|
477
|
+
.strict()
|
|
478
|
+
.nullable(),
|
|
479
|
+
forceSnapshot: z.boolean().optional(),
|
|
480
|
+
})
|
|
481
|
+
.strict();
|
|
482
|
+
export type ConversationResyncParams = z.infer<typeof conversationResyncParamsSchema>;
|
|
483
|
+
|
|
484
|
+
// CLI-facing 形状:topic/connectionId 只能由 connection facade 从 owned registry 注入。
|
|
485
|
+
export const v4ConversationResyncParamsSchema = conversationResyncParamsSchema
|
|
486
|
+
.extend({
|
|
487
|
+
topic: z.string().trim().min(1).max(2048),
|
|
488
|
+
connectionId: z.string().trim().min(1).max(1024),
|
|
489
|
+
})
|
|
490
|
+
.strict();
|
|
491
|
+
export type V4ConversationResyncParams = z.infer<typeof v4ConversationResyncParamsSchema>;
|
|
492
|
+
|
|
493
|
+
export const v4ConversationResyncResultSchema = z
|
|
494
|
+
.object({
|
|
495
|
+
ack: subscribeAckSchema,
|
|
496
|
+
})
|
|
497
|
+
.strict();
|
|
498
|
+
export type V4ConversationResyncResult = z.infer<typeof v4ConversationResyncResultSchema>;
|
|
499
|
+
|
|
500
|
+
export const v4ConversationUnsubscribeParamsSchema = z
|
|
501
|
+
.object({
|
|
502
|
+
topic: z.string().trim().min(1).max(2048),
|
|
503
|
+
subscriptionId: z.string().trim().min(1).max(1024),
|
|
504
|
+
connectionId: z.string().trim().min(1).max(1024),
|
|
505
|
+
})
|
|
506
|
+
.strict();
|
|
507
|
+
export type V4ConversationUnsubscribeParams = z.infer<typeof v4ConversationUnsubscribeParamsSchema>;
|
|
508
|
+
|
|
509
|
+
// ── rows/range(游标制行分页,loadOlder)──
|
|
510
|
+
// 无 index 语义:全序 = rowId 升序;客户端按 rowId 键控合并。
|
|
511
|
+
export const v4ConversationRowsRangeParamsSchema = z.object({
|
|
512
|
+
sessionId: z.string(),
|
|
513
|
+
/** Host attachment injects this trusted value; renderer callers omit it. */
|
|
514
|
+
clientMode: z.enum(["desktop-continuous", "web-remote-replayable"]).optional(),
|
|
515
|
+
// 取 rowId < beforeRowId 的行;缺省 = 从当前尾部向前。
|
|
516
|
+
beforeRowId: z.number().optional(),
|
|
517
|
+
limit: z.number().min(1).max(PROTOCOL_V4_LIMITS.rowsRangeMaxLimit),
|
|
518
|
+
});
|
|
519
|
+
export type V4ConversationRowsRangeParams = z.infer<typeof v4ConversationRowsRangeParamsSchema>;
|
|
520
|
+
|
|
521
|
+
export const v4ConversationRowsRangeResultSchema = z.object({
|
|
522
|
+
// rowId 升序。
|
|
523
|
+
rows: z.array(conversationRowSchema),
|
|
524
|
+
// 服务端取值时的水位/纪元;与 fileChanges 等只读查询共用,避免跨 revision 拼接发布数据。
|
|
525
|
+
atSeq: z.number(),
|
|
526
|
+
atRevision: z.number().int().nonnegative(),
|
|
527
|
+
atLogEpoch: z.string(),
|
|
528
|
+
// beforeRowId 方向是否还有更早的行。
|
|
529
|
+
hasMore: z.boolean(),
|
|
530
|
+
});
|
|
531
|
+
export type V4ConversationRowsRangeResult = z.infer<typeof v4ConversationRowsRangeResultSchema>;
|
|
532
|
+
|
|
533
|
+
// ── conversation plans directory ──
|
|
534
|
+
// 目录来自 CLI 完整有效 projection,不能用 renderer 的有界 tail window 推导。
|
|
535
|
+
export const v4ConversationPlansParamsSchema = z
|
|
536
|
+
.object({
|
|
537
|
+
sessionId: z.string().min(1),
|
|
538
|
+
})
|
|
539
|
+
.strict();
|
|
540
|
+
export type V4ConversationPlansParams = z.infer<typeof v4ConversationPlansParamsSchema>;
|
|
541
|
+
|
|
542
|
+
export const v4ConversationPlansResultSchema = z
|
|
543
|
+
.object({
|
|
544
|
+
// 当前有效分支的终态 ExitPlanMode,rowId 降序(最新优先)。
|
|
545
|
+
plans: z.array(toolCallRowSchema),
|
|
546
|
+
atSeq: z.number().int().nonnegative(),
|
|
547
|
+
atLogEpoch: z.string().min(1),
|
|
548
|
+
})
|
|
549
|
+
.strict();
|
|
550
|
+
export type V4ConversationPlansResult = z.infer<typeof v4ConversationPlansResultSchema>;
|
|
551
|
+
|
|
552
|
+
const readonlyDiffHunkSchema = z
|
|
553
|
+
.object({
|
|
554
|
+
oldStart: z.number(),
|
|
555
|
+
oldLines: z.number(),
|
|
556
|
+
newStart: z.number(),
|
|
557
|
+
newLines: z.number(),
|
|
558
|
+
lines: z.array(z.string()),
|
|
559
|
+
})
|
|
560
|
+
.strict();
|
|
561
|
+
|
|
562
|
+
export const v4ConversationFileChangesParamsSchema = z
|
|
563
|
+
.object({
|
|
564
|
+
sessionId: z.string().min(1),
|
|
565
|
+
target: conversationRowTargetSchema,
|
|
566
|
+
baseRevision: z.number().int().nonnegative(),
|
|
567
|
+
baseLogEpoch: z.string().trim().min(1),
|
|
568
|
+
})
|
|
569
|
+
.strict();
|
|
570
|
+
export type V4ConversationFileChangesParams = z.infer<typeof v4ConversationFileChangesParamsSchema>;
|
|
571
|
+
|
|
572
|
+
export const v4ConversationFileChangesResultSchema = z
|
|
573
|
+
.object({
|
|
574
|
+
files: z.number().int().nonnegative(),
|
|
575
|
+
additions: z.number().int().nonnegative(),
|
|
576
|
+
deletions: z.number().int().nonnegative(),
|
|
577
|
+
state: z.enum(["active", "reverted"]).optional(),
|
|
578
|
+
items: z.array(
|
|
579
|
+
z
|
|
580
|
+
.object({
|
|
581
|
+
path: z.string().min(1),
|
|
582
|
+
additions: z.number().int().nonnegative(),
|
|
583
|
+
deletions: z.number().int().nonnegative(),
|
|
584
|
+
writeCount: z.number().int().nonnegative(),
|
|
585
|
+
toolNames: z.array(z.string()),
|
|
586
|
+
patches: z.array(readonlyDiffHunkSchema),
|
|
587
|
+
})
|
|
588
|
+
.strict(),
|
|
589
|
+
),
|
|
590
|
+
})
|
|
591
|
+
.strict();
|
|
592
|
+
export type V4ConversationFileChangesResult = z.infer<typeof v4ConversationFileChangesResultSchema>;
|
|
593
|
+
|
|
594
|
+
// ── workflow run 事件日志──
|
|
595
|
+
// 形态与 rows/range、plans 同族:只读、无状态、超时重发安全。cursor = journal sequence
|
|
596
|
+
// (`appendEvent` 单调分配),在 workflowRuns[].lastEventSequence 抬升时重取。
|
|
597
|
+
// 刻意**不是** v4 command:command 的 ACK 结果是 commandResultSchema 那个封闭的「变更结果」
|
|
598
|
+
// 判别联合,把一页只读事件塞进去等于把读放进写的词汇表,还要白背 baseRevision/幂等那套机制。
|
|
599
|
+
export const v4ConversationWorkflowRunEventsParamsSchema = z
|
|
600
|
+
.object({
|
|
601
|
+
sessionId: z.string().min(1),
|
|
602
|
+
runId: z.string().min(1),
|
|
603
|
+
/** 只取 sequence 严格大于该值的事件;缺省从头取。 */
|
|
604
|
+
afterSequence: z.number().int().nonnegative().optional(),
|
|
605
|
+
limit: z.number().int().positive().max(500).optional(),
|
|
606
|
+
})
|
|
607
|
+
.strict();
|
|
608
|
+
export type V4ConversationWorkflowRunEventsParams = z.infer<
|
|
609
|
+
typeof v4ConversationWorkflowRunEventsParamsSchema
|
|
610
|
+
>;
|
|
611
|
+
|
|
612
|
+
export const v4ConversationWorkflowRunEventsResultSchema = z
|
|
613
|
+
.object({
|
|
614
|
+
events: z.array(
|
|
615
|
+
z
|
|
616
|
+
.object({
|
|
617
|
+
sequence: z.number().int().nonnegative(),
|
|
618
|
+
type: z.string().min(1).max(64),
|
|
619
|
+
// 载荷已在 CLI 侧经 boundDynamicWorkflowRunEventPayload 有界化(同一次序列化
|
|
620
|
+
// 也喂给 workflowRuns 投影)。这里不再复述引擎的事件形状:读端按种类解释。
|
|
621
|
+
payload: z.record(z.string(), z.unknown()),
|
|
622
|
+
truncated: z.boolean().optional(),
|
|
623
|
+
})
|
|
624
|
+
.strict(),
|
|
625
|
+
),
|
|
626
|
+
/** 本页取满 limit 且后面仍有事件。 */
|
|
627
|
+
hasMore: z.boolean(),
|
|
628
|
+
})
|
|
629
|
+
.strict();
|
|
630
|
+
// **刻意不带 `atSeq` / `atLogEpoch`**,尽管同族的 rows/range 与 plans 都带。
|
|
631
|
+
//
|
|
632
|
+
// 那两个字段在同族里是**陈旧读防护**:它们读的是 conversation projection,而 projection 的
|
|
633
|
+
// `rowId` 只在一个 log epoch 内有意义(重建 / fork / rewind 会重新编号),所以读端的契约是
|
|
634
|
+
// "atLogEpoch ≠ store 当前 epoch → 整个结果丢弃"。
|
|
635
|
+
//
|
|
636
|
+
// 本 query 读的是 **journal**(`dwf_event`),它与 conversation log 无关,且 cursor 永不失效:
|
|
637
|
+
// journal 的 JournalStorePort 契约要求 sequence 只追加、
|
|
638
|
+
// 跨 resume 从既有最大值继续、既不重置也不复用,既有条目的编号不变。也就是说一个
|
|
639
|
+
// `(runId, sequence)` cursor 永远有效——没有任何"陈旧"可供防护。
|
|
640
|
+
//
|
|
641
|
+
// 更要紧的是:带上 `atLogEpoch` 不是无害的对称。按同族的读端契约,一次与 run 完全无关的
|
|
642
|
+
// 会话 rewind(epoch 变化)会让详情页把**合法的** journal 页整页丢掉。宁可少一个字段,
|
|
643
|
+
// 也不要携带一个在这里定义不出正确语义的字段。
|
|
644
|
+
export type V4ConversationWorkflowRunEventsResult = z.infer<
|
|
645
|
+
typeof v4ConversationWorkflowRunEventsResultSchema
|
|
646
|
+
>;
|
|
647
|
+
|
|
648
|
+
// ── dwf run 枚举 ──
|
|
649
|
+
// journal-backed 的重启后发现面:`workflowRuns` 投影是 memory-only(冷合并归类),
|
|
650
|
+
// 重启后为空——工具卡 join 与 Resume 按钮的可用性只能从 dwf_run 行还原。与
|
|
651
|
+
// workflowRunEvents 同族(只读、无状态、超时重发安全),同样刻意不是 v4 command,
|
|
652
|
+
// 同样不带 atSeq/atLogEpoch(读 journal,与 conversation log 无关,见上一个 query 的论证)。
|
|
653
|
+
// 新方法天然偏斜安全:旧桌面永远不会调用它。
|
|
654
|
+
export const v4ConversationWorkflowRunsParamsSchema = z
|
|
655
|
+
.object({
|
|
656
|
+
sessionId: z.string().min(1),
|
|
657
|
+
/** 返回条数上限;缺省与钳制在 CLI 侧(枚举面有界,绝不无界扫库)。 */
|
|
658
|
+
limit: z.number().int().positive().max(64).optional(),
|
|
659
|
+
})
|
|
660
|
+
.strict();
|
|
661
|
+
export type V4ConversationWorkflowRunsParams = z.infer<
|
|
662
|
+
typeof v4ConversationWorkflowRunsParamsSchema
|
|
663
|
+
>;
|
|
664
|
+
|
|
665
|
+
export const v4ConversationWorkflowRunSummarySchema = z
|
|
666
|
+
.object({
|
|
667
|
+
runId: z.string().min(1),
|
|
668
|
+
/** 发起 run 的 CreateWorkflow 工具调用 id(工具卡 → 详情页/Resume 的关联键);老 run 缺席。 */
|
|
669
|
+
toolCallId: z.string().min(1).optional(),
|
|
670
|
+
/**
|
|
671
|
+
* 展示标签,服务端读时派生(`name` → 脚本首行 → runId)。optional 是偏斜安全:
|
|
672
|
+
* 老 CLI 不发这个键,读侧回落 runId——少一个标签是退化,不是错误。
|
|
673
|
+
* 上限与派生侧的 80 字符对齐后留一倍余量(用户起的 `name` 不受派生上限约束)。
|
|
674
|
+
*/
|
|
675
|
+
label: z.string().min(1).max(160).optional(),
|
|
676
|
+
/** 最后更新时间(epoch 毫秒,journal 的 `dwf_run.time_updated`)。缺席即不显示时间。 */
|
|
677
|
+
updatedAt: z.number().int().nonnegative().optional(),
|
|
678
|
+
// 与 workflowRuns 投影同一套五值词汇;
|
|
679
|
+
// 此查询使用独立的 schema,新增查询状态不改变投影侧的状态键。
|
|
680
|
+
status: z.enum(["completed", "errored", "pending", "running", "stopped"]),
|
|
681
|
+
/** `status === "stopped"` 才在场。词表与观察面共用一份——这里曾各抄一遍,于是引擎多出
|
|
682
|
+
* `superseded` 时这份 strict schema 把整页 run 目录拒掉(桌面端表现为
|
|
683
|
+
* 「读取 run 摘要失败」,任务列表计数与目录页一起空白)。 */
|
|
684
|
+
stopReason: z.enum(WORKFLOW_RUN_STOP_REASONS).optional(),
|
|
685
|
+
// lineage:修订出来的 run 带前驱,
|
|
686
|
+
// 被替代的 run 带后继(只随 `stopReason: "superseded"`)。两者 optional:老 CLI 不发。
|
|
687
|
+
resumedFrom: z.string().min(1).optional(),
|
|
688
|
+
supersededBy: z.string().min(1).optional(),
|
|
689
|
+
/** errored / stopped(provider|interrupted) 的结构化失败编码(`ProviderStop` / `Interrupted` …)。 */
|
|
690
|
+
failureCode: z.string().min(1).max(64).optional(),
|
|
691
|
+
failureMessage: z.string().max(2048).optional(),
|
|
692
|
+
/** 是否可恢复。CLI 按 resume 门的同一个谓词算好——UI 绝不自行推导(两处谓词会漂移)。 */
|
|
693
|
+
resumable: z.boolean(),
|
|
694
|
+
})
|
|
695
|
+
.strict();
|
|
696
|
+
export type V4ConversationWorkflowRunSummary = z.infer<
|
|
697
|
+
typeof v4ConversationWorkflowRunSummarySchema
|
|
698
|
+
>;
|
|
699
|
+
|
|
700
|
+
export const v4ConversationWorkflowRunsResultSchema = z
|
|
701
|
+
.object({
|
|
702
|
+
/** 最近更新在前(排序在存储层)。 */
|
|
703
|
+
runs: z.array(v4ConversationWorkflowRunSummarySchema),
|
|
704
|
+
})
|
|
705
|
+
.strict();
|
|
706
|
+
export type V4ConversationWorkflowRunsResult = z.infer<
|
|
707
|
+
typeof v4ConversationWorkflowRunsResultSchema
|
|
708
|
+
>;
|
|
709
|
+
|
|
710
|
+
export const v4ConversationFileRewindPreviewParamsSchema = z
|
|
711
|
+
.object({
|
|
712
|
+
sessionId: z.string().min(1),
|
|
713
|
+
target: conversationRowTargetSchema,
|
|
714
|
+
baseRevision: z.number().int().nonnegative(),
|
|
715
|
+
baseLogEpoch: z.string().trim().min(1),
|
|
716
|
+
})
|
|
717
|
+
.strict();
|
|
718
|
+
export type V4ConversationFileRewindPreviewParams = z.infer<
|
|
719
|
+
typeof v4ConversationFileRewindPreviewParamsSchema
|
|
720
|
+
>;
|
|
721
|
+
|
|
722
|
+
const v4WorkspaceFileRewindSafeFileSchema = z
|
|
723
|
+
.object({
|
|
724
|
+
action: z.enum(["restore", "delete"]),
|
|
725
|
+
operationCount: z.number().int().nonnegative(),
|
|
726
|
+
path: z.string().min(1),
|
|
727
|
+
toolNames: z.array(z.string()),
|
|
728
|
+
})
|
|
729
|
+
.strict();
|
|
730
|
+
|
|
731
|
+
const v4WorkspaceFileRewindUnsafeFileSchema = z
|
|
732
|
+
.object({
|
|
733
|
+
currentHash: z.string().optional(),
|
|
734
|
+
expectedHash: z.string().optional(),
|
|
735
|
+
message: z.string().optional(),
|
|
736
|
+
operationCount: z.number().int().nonnegative(),
|
|
737
|
+
path: z.string().min(1),
|
|
738
|
+
reason: z.enum([
|
|
739
|
+
"checkpoint_missing",
|
|
740
|
+
"checkpoint_unreadable",
|
|
741
|
+
"external_modified",
|
|
742
|
+
"file_read_failed",
|
|
743
|
+
"unsupported_checkpoint",
|
|
744
|
+
]),
|
|
745
|
+
toolNames: z.array(z.string()),
|
|
746
|
+
})
|
|
747
|
+
.strict();
|
|
748
|
+
|
|
749
|
+
const v4WorkspaceFileRewindIgnoredFileSchema = z
|
|
750
|
+
.object({
|
|
751
|
+
operationCount: z.number().int().nonnegative(),
|
|
752
|
+
path: z.string().min(1),
|
|
753
|
+
reason: z.literal("bash_ignored"),
|
|
754
|
+
toolNames: z.array(z.string()),
|
|
755
|
+
})
|
|
756
|
+
.strict();
|
|
757
|
+
|
|
758
|
+
export const v4ConversationFileRewindPreviewResultSchema = z
|
|
759
|
+
.object({
|
|
760
|
+
canApply: z.boolean(),
|
|
761
|
+
ignoredFiles: z.array(v4WorkspaceFileRewindIgnoredFileSchema),
|
|
762
|
+
safeFiles: z.array(v4WorkspaceFileRewindSafeFileSchema),
|
|
763
|
+
unsafeFiles: z.array(v4WorkspaceFileRewindUnsafeFileSchema),
|
|
764
|
+
})
|
|
765
|
+
.strict();
|
|
766
|
+
export type V4ConversationFileRewindPreviewResult = z.infer<
|
|
767
|
+
typeof v4ConversationFileRewindPreviewResultSchema
|
|
768
|
+
>;
|
|
769
|
+
|
|
770
|
+
// ── usage query──
|
|
771
|
+
// app 级用量聚合:range/timeZone 入参与旧 usage/stats 同形(消费者语义不变),
|
|
772
|
+
// 结果 = AppUsageSnapshot(形状归属中性模块 usage-stats.ts,不与旧词表文件耦合)。
|
|
773
|
+
export const v4UsageStatsParamsSchema = z
|
|
774
|
+
.object({
|
|
775
|
+
range: z.enum(APP_USAGE_RANGES),
|
|
776
|
+
timeZone: z.string().optional(),
|
|
777
|
+
})
|
|
778
|
+
.strict();
|
|
779
|
+
export type V4UsageStatsParams = z.infer<typeof v4UsageStatsParamsSchema>;
|
|
780
|
+
export const v4UsageStatsResultSchema = appUsageSnapshotSchema;
|
|
781
|
+
export type V4UsageStatsResult = z.infer<typeof v4UsageStatsResultSchema>;
|
|
782
|
+
|
|
783
|
+
// 会话级 token 用量(旧 session/usage 的 v4 名字空间落位:会话是协议一等概念,
|
|
784
|
+
// task 是 UI 投影概念不进协议词表)。字段与旧 result 同形,旧 schema 随词一起死。
|
|
785
|
+
export const v4ConversationUsageParamsSchema = z
|
|
786
|
+
.object({
|
|
787
|
+
sessionId: z.string().min(1),
|
|
788
|
+
})
|
|
789
|
+
.strict();
|
|
790
|
+
export type V4ConversationUsageParams = z.infer<typeof v4ConversationUsageParamsSchema>;
|
|
791
|
+
export const v4ConversationUsageResultSchema = z
|
|
792
|
+
.object({
|
|
793
|
+
sessionId: z.string().min(1),
|
|
794
|
+
totalTokens: z.number().int().nonnegative(),
|
|
795
|
+
inputTokens: z.number().int().nonnegative(),
|
|
796
|
+
outputTokens: z.number().int().nonnegative(),
|
|
797
|
+
reasoningTokens: z.number().int().nonnegative(),
|
|
798
|
+
cacheCreationTokens: z.number().int().nonnegative(),
|
|
799
|
+
cacheReadTokens: z.number().int().nonnegative(),
|
|
800
|
+
modelRequestCount: z.number().int().nonnegative(),
|
|
801
|
+
modelErrorCount: z.number().int().nonnegative(),
|
|
802
|
+
inputBaselineBySource: z.record(z.string(), z.number().int().nonnegative()),
|
|
803
|
+
})
|
|
804
|
+
.strict();
|
|
805
|
+
export type V4ConversationUsageResult = z.infer<typeof v4ConversationUsageResultSchema>;
|
|
806
|
+
|
|
807
|
+
// ── 附件上行事务 ──
|
|
808
|
+
// UI 高层仍用 put(input)->ref;这份 full-data schema 只描述 renderer 内部调用,绝不作为
|
|
809
|
+
// production RPC method。wire 只能用 begin/chunk/commit/abort。
|
|
810
|
+
export const v4AttachmentPutParamsSchema = z
|
|
811
|
+
.object({
|
|
812
|
+
sessionId: z.string().min(1),
|
|
813
|
+
fileName: z.string().min(1),
|
|
814
|
+
mime: z.string().min(1),
|
|
815
|
+
// base64(不带 data: 前缀);解码后字节数 ≤ PROTOCOL_V4_LIMITS.attachmentMaxBytes。
|
|
816
|
+
dataBase64: z.string().min(1),
|
|
817
|
+
})
|
|
818
|
+
.strict();
|
|
819
|
+
export type V4AttachmentPutParams = z.infer<typeof v4AttachmentPutParamsSchema>;
|
|
820
|
+
export const v4AttachmentPutResultSchema = z.object({
|
|
821
|
+
ref: z.string().min(1),
|
|
822
|
+
});
|
|
823
|
+
export type V4AttachmentPutResult = z.infer<typeof v4AttachmentPutResultSchema>;
|
|
824
|
+
|
|
825
|
+
const v4AttachmentUploadIdSchema = z
|
|
826
|
+
.string()
|
|
827
|
+
.min(1)
|
|
828
|
+
.max(128)
|
|
829
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
830
|
+
const v4AttachmentChecksumSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
831
|
+
|
|
832
|
+
export const v4AttachmentBeginParamsSchema = z
|
|
833
|
+
.object({
|
|
834
|
+
connectionId: z.string().min(1),
|
|
835
|
+
uploadId: v4AttachmentUploadIdSchema,
|
|
836
|
+
sessionId: z.string().min(1),
|
|
837
|
+
fileName: z
|
|
838
|
+
.string()
|
|
839
|
+
.min(1)
|
|
840
|
+
.max(255)
|
|
841
|
+
.regex(/^[^\0\r\n]+$/),
|
|
842
|
+
mime: z
|
|
843
|
+
.string()
|
|
844
|
+
.min(3)
|
|
845
|
+
.max(255)
|
|
846
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/),
|
|
847
|
+
totalBytes: z.number().int().min(0).max(PROTOCOL_V4_LIMITS.attachmentMaxBytes),
|
|
848
|
+
totalChunks: z.number().int().min(0).max(PROTOCOL_V4_LIMITS.attachmentUploadMaxChunks),
|
|
849
|
+
checksum: v4AttachmentChecksumSchema,
|
|
850
|
+
})
|
|
851
|
+
.strict()
|
|
852
|
+
.superRefine((value, context) => {
|
|
853
|
+
if ((value.totalBytes === 0) !== (value.totalChunks === 0)) {
|
|
854
|
+
context.addIssue({
|
|
855
|
+
code: "custom",
|
|
856
|
+
message: "zero-byte upload must declare zero chunks",
|
|
857
|
+
path: ["totalChunks"],
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
export type V4AttachmentBeginParams = z.infer<typeof v4AttachmentBeginParamsSchema>;
|
|
862
|
+
|
|
863
|
+
export const v4AttachmentBeginResultSchema = z.discriminatedUnion("state", [
|
|
864
|
+
z
|
|
865
|
+
.object({
|
|
866
|
+
uploadId: v4AttachmentUploadIdSchema,
|
|
867
|
+
state: z.literal("staging"),
|
|
868
|
+
nextChunkIndex: z.number().int().nonnegative(),
|
|
869
|
+
})
|
|
870
|
+
.strict(),
|
|
871
|
+
z
|
|
872
|
+
.object({
|
|
873
|
+
uploadId: v4AttachmentUploadIdSchema,
|
|
874
|
+
state: z.literal("committed"),
|
|
875
|
+
nextChunkIndex: z.number().int().nonnegative(),
|
|
876
|
+
ref: z.string().min(1),
|
|
877
|
+
})
|
|
878
|
+
.strict(),
|
|
879
|
+
]);
|
|
880
|
+
export type V4AttachmentBeginResult = z.infer<typeof v4AttachmentBeginResultSchema>;
|
|
881
|
+
|
|
882
|
+
function decodedBase64ByteLength(value: string): number | null {
|
|
883
|
+
if (value.length === 0) return 0;
|
|
884
|
+
if (value.length % 4 !== 0) return null;
|
|
885
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
886
|
+
const contentLength = value.length - padding;
|
|
887
|
+
for (let index = 0; index < contentLength; index += 1) {
|
|
888
|
+
const code = value.charCodeAt(index);
|
|
889
|
+
const valid =
|
|
890
|
+
(code >= 65 && code <= 90) ||
|
|
891
|
+
(code >= 97 && code <= 122) ||
|
|
892
|
+
(code >= 48 && code <= 57) ||
|
|
893
|
+
code === 43 ||
|
|
894
|
+
code === 47;
|
|
895
|
+
if (!valid) return null;
|
|
896
|
+
}
|
|
897
|
+
for (let index = contentLength; index < value.length; index += 1) {
|
|
898
|
+
if (value.charCodeAt(index) !== 61) return null;
|
|
899
|
+
}
|
|
900
|
+
return (value.length / 4) * 3 - padding;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export const v4AttachmentChunkParamsSchema = z
|
|
904
|
+
.object({
|
|
905
|
+
connectionId: z.string().min(1),
|
|
906
|
+
uploadId: v4AttachmentUploadIdSchema,
|
|
907
|
+
sessionId: z.string().min(1),
|
|
908
|
+
chunkIndex: z.number().int().nonnegative(),
|
|
909
|
+
dataBase64: z.string(),
|
|
910
|
+
})
|
|
911
|
+
.strict()
|
|
912
|
+
.superRefine((value, context) => {
|
|
913
|
+
const decodedBytes = decodedBase64ByteLength(value.dataBase64);
|
|
914
|
+
if (decodedBytes === null) {
|
|
915
|
+
context.addIssue({ code: "custom", message: "invalid base64", path: ["dataBase64"] });
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (decodedBytes > PROTOCOL_V4_LIMITS.attachmentChunkMaxBytes) {
|
|
919
|
+
context.addIssue({
|
|
920
|
+
code: "too_big",
|
|
921
|
+
maximum: PROTOCOL_V4_LIMITS.attachmentChunkMaxBytes,
|
|
922
|
+
origin: "string",
|
|
923
|
+
inclusive: true,
|
|
924
|
+
message: "attachment chunk exceeds decoded byte limit",
|
|
925
|
+
path: ["dataBase64"],
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
export type V4AttachmentChunkParams = z.infer<typeof v4AttachmentChunkParamsSchema>;
|
|
930
|
+
|
|
931
|
+
export const v4AttachmentChunkResultSchema = z
|
|
932
|
+
.object({
|
|
933
|
+
uploadId: v4AttachmentUploadIdSchema,
|
|
934
|
+
nextChunkIndex: z.number().int().nonnegative(),
|
|
935
|
+
})
|
|
936
|
+
.strict();
|
|
937
|
+
export type V4AttachmentChunkResult = z.infer<typeof v4AttachmentChunkResultSchema>;
|
|
938
|
+
|
|
939
|
+
const v4AttachmentTerminalParamsSchema = z
|
|
940
|
+
.object({
|
|
941
|
+
connectionId: z.string().min(1),
|
|
942
|
+
uploadId: v4AttachmentUploadIdSchema,
|
|
943
|
+
sessionId: z.string().min(1),
|
|
944
|
+
})
|
|
945
|
+
.strict();
|
|
946
|
+
export const v4AttachmentCommitParamsSchema = v4AttachmentTerminalParamsSchema;
|
|
947
|
+
export type V4AttachmentCommitParams = z.infer<typeof v4AttachmentCommitParamsSchema>;
|
|
948
|
+
export const v4AttachmentCommitResultSchema = v4AttachmentPutResultSchema.strict();
|
|
949
|
+
export type V4AttachmentCommitResult = z.infer<typeof v4AttachmentCommitResultSchema>;
|
|
950
|
+
export const v4AttachmentAbortParamsSchema = v4AttachmentTerminalParamsSchema;
|
|
951
|
+
export type V4AttachmentAbortParams = z.infer<typeof v4AttachmentAbortParamsSchema>;
|
|
952
|
+
export const v4AttachmentAbortResultSchema = z.object({}).strict();
|
|
953
|
+
|
|
954
|
+
/** 已发送 image/video/PDF 预览 query;ref 必须由 CLI 对当前 session projection 再授权。 */
|
|
955
|
+
export const v4AttachmentReadParamsSchema = z
|
|
956
|
+
.object({
|
|
957
|
+
sessionId: z.string().min(1),
|
|
958
|
+
ref: z.string().min(1),
|
|
959
|
+
// 新 renderer 用稳定 row 身份 + 附件序号消除同一路径跨轮歧义;两者必须成对出现。
|
|
960
|
+
target: conversationRowTargetSchema.optional(),
|
|
961
|
+
attachmentIndex: z.number().int().nonnegative().optional(),
|
|
962
|
+
offset: z.number().int().nonnegative(),
|
|
963
|
+
limit: z.number().int().positive().max(PROTOCOL_V4_LIMITS.attachmentChunkMaxBytes),
|
|
964
|
+
})
|
|
965
|
+
.strict()
|
|
966
|
+
.superRefine((value, context) => {
|
|
967
|
+
if ((value.target === undefined) === (value.attachmentIndex === undefined)) return;
|
|
968
|
+
context.addIssue({
|
|
969
|
+
code: "custom",
|
|
970
|
+
message: "target and attachmentIndex must be provided together",
|
|
971
|
+
path: value.target === undefined ? ["target"] : ["attachmentIndex"],
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
export type V4AttachmentReadParams = z.infer<typeof v4AttachmentReadParamsSchema>;
|
|
975
|
+
|
|
976
|
+
/** Desktop local 已发送视频 source query;远端与 Web 必须返回 chunked。PDF 始终走 chunked。 */
|
|
977
|
+
export const v4AttachmentPreviewSourceParamsSchema = z
|
|
978
|
+
.object({
|
|
979
|
+
sessionId: z.string().min(1),
|
|
980
|
+
ref: z.string().min(1),
|
|
981
|
+
target: conversationRowTargetSchema.optional(),
|
|
982
|
+
attachmentIndex: z.number().int().nonnegative().optional(),
|
|
983
|
+
clientMode: z.enum(["desktop-continuous", "web-remote-replayable"]),
|
|
984
|
+
})
|
|
985
|
+
.strict()
|
|
986
|
+
.superRefine((value, context) => {
|
|
987
|
+
if ((value.target === undefined) === (value.attachmentIndex === undefined)) return;
|
|
988
|
+
context.addIssue({
|
|
989
|
+
code: "custom",
|
|
990
|
+
message: "target and attachmentIndex must be provided together",
|
|
991
|
+
path: value.target === undefined ? ["target"] : ["attachmentIndex"],
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
export type V4AttachmentPreviewSourceParams = z.infer<typeof v4AttachmentPreviewSourceParamsSchema>;
|
|
995
|
+
|
|
996
|
+
export const v4AttachmentPreviewSourceResultSchema = z.union([
|
|
997
|
+
z
|
|
998
|
+
.object({
|
|
999
|
+
kind: z.literal("local_path"),
|
|
1000
|
+
path: z.string().min(1),
|
|
1001
|
+
mediaType: z.string().refine((value) => value.startsWith("video/"), {
|
|
1002
|
+
message: "local attachment preview only supports video media types",
|
|
1003
|
+
}),
|
|
1004
|
+
})
|
|
1005
|
+
.strict(),
|
|
1006
|
+
z.object({ kind: z.literal("chunked") }).strict(),
|
|
1007
|
+
]);
|
|
1008
|
+
export type V4AttachmentPreviewSourceResult = z.infer<typeof v4AttachmentPreviewSourceResultSchema>;
|
|
1009
|
+
|
|
1010
|
+
export const v4AttachmentReadResultSchema = z
|
|
1011
|
+
.object({
|
|
1012
|
+
dataBase64: z.string(),
|
|
1013
|
+
mediaType: z
|
|
1014
|
+
.string()
|
|
1015
|
+
.refine(
|
|
1016
|
+
(value) =>
|
|
1017
|
+
value.startsWith("image/") ||
|
|
1018
|
+
value.startsWith("video/") ||
|
|
1019
|
+
value.split(";", 1)[0]?.trim().toLowerCase() === "application/pdf",
|
|
1020
|
+
"attachment preview only supports image/video/pdf media types",
|
|
1021
|
+
),
|
|
1022
|
+
totalBytes: z.number().int().nonnegative().max(PROTOCOL_V4_LIMITS.attachmentPreviewMaxBytes),
|
|
1023
|
+
nextOffset: z.number().int().positive().nullable(),
|
|
1024
|
+
})
|
|
1025
|
+
.strict()
|
|
1026
|
+
.superRefine((value, context) => {
|
|
1027
|
+
if (
|
|
1028
|
+
value.mediaType.startsWith("image/") &&
|
|
1029
|
+
value.totalBytes > PROTOCOL_V4_LIMITS.attachmentMaxBytes
|
|
1030
|
+
) {
|
|
1031
|
+
context.addIssue({
|
|
1032
|
+
code: "too_big",
|
|
1033
|
+
maximum: PROTOCOL_V4_LIMITS.attachmentMaxBytes,
|
|
1034
|
+
origin: "number",
|
|
1035
|
+
inclusive: true,
|
|
1036
|
+
message: "image preview exceeds total byte limit",
|
|
1037
|
+
path: ["totalBytes"],
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
const decodedBytes = decodedBase64ByteLength(value.dataBase64);
|
|
1041
|
+
if (decodedBytes === null) {
|
|
1042
|
+
context.addIssue({ code: "custom", message: "invalid base64", path: ["dataBase64"] });
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (decodedBytes > PROTOCOL_V4_LIMITS.attachmentChunkMaxBytes) {
|
|
1046
|
+
context.addIssue({
|
|
1047
|
+
code: "too_big",
|
|
1048
|
+
maximum: PROTOCOL_V4_LIMITS.attachmentChunkMaxBytes,
|
|
1049
|
+
origin: "string",
|
|
1050
|
+
inclusive: true,
|
|
1051
|
+
message: "attachment read chunk exceeds decoded byte limit",
|
|
1052
|
+
path: ["dataBase64"],
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
if (value.nextOffset !== null && value.nextOffset > value.totalBytes) {
|
|
1056
|
+
context.addIssue({
|
|
1057
|
+
code: "custom",
|
|
1058
|
+
message: "nextOffset exceeds totalBytes",
|
|
1059
|
+
path: ["nextOffset"],
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
export type V4AttachmentReadResult = z.infer<typeof v4AttachmentReadResultSchema>;
|
|
1064
|
+
|
|
1065
|
+
/** Share 读取用户输入附件,允许任意已授权 MIME,不改变媒体预览 read 的语义。 */
|
|
1066
|
+
export const v4ConversationAttachmentReadParamsSchema = z
|
|
1067
|
+
.object({
|
|
1068
|
+
sessionId: z.string().min(1),
|
|
1069
|
+
ref: z.string().min(1),
|
|
1070
|
+
target: conversationRowTargetSchema,
|
|
1071
|
+
attachmentIndex: z.number().int().nonnegative(),
|
|
1072
|
+
offset: z.number().int().nonnegative(),
|
|
1073
|
+
limit: z.number().int().positive().max(PROTOCOL_V4_LIMITS.attachmentChunkMaxBytes),
|
|
1074
|
+
})
|
|
1075
|
+
.strict();
|
|
1076
|
+
export type V4ConversationAttachmentReadParams = z.infer<
|
|
1077
|
+
typeof v4ConversationAttachmentReadParamsSchema
|
|
1078
|
+
>;
|
|
1079
|
+
|
|
1080
|
+
export const v4ConversationAttachmentReadResultSchema = z
|
|
1081
|
+
.object({
|
|
1082
|
+
dataBase64: z.string(),
|
|
1083
|
+
mediaType: z.string().min(1),
|
|
1084
|
+
totalBytes: z.number().int().nonnegative().max(PROTOCOL_V4_LIMITS.attachmentPreviewMaxBytes),
|
|
1085
|
+
nextOffset: z.number().int().positive().nullable(),
|
|
1086
|
+
})
|
|
1087
|
+
.strict()
|
|
1088
|
+
.superRefine((value, context) => {
|
|
1089
|
+
const decodedBytes = decodedBase64ByteLength(value.dataBase64);
|
|
1090
|
+
if (decodedBytes === null) {
|
|
1091
|
+
context.addIssue({ code: "custom", message: "invalid base64", path: ["dataBase64"] });
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (decodedBytes > PROTOCOL_V4_LIMITS.attachmentChunkMaxBytes) {
|
|
1095
|
+
context.addIssue({
|
|
1096
|
+
code: "too_big",
|
|
1097
|
+
maximum: PROTOCOL_V4_LIMITS.attachmentChunkMaxBytes,
|
|
1098
|
+
origin: "string",
|
|
1099
|
+
inclusive: true,
|
|
1100
|
+
message: "attachment read chunk exceeds decoded byte limit",
|
|
1101
|
+
path: ["dataBase64"],
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
if (value.nextOffset !== null && value.nextOffset > value.totalBytes) {
|
|
1105
|
+
context.addIssue({
|
|
1106
|
+
code: "custom",
|
|
1107
|
+
message: "nextOffset exceeds totalBytes",
|
|
1108
|
+
path: ["nextOffset"],
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
export type V4ConversationAttachmentReadResult = z.infer<
|
|
1113
|
+
typeof v4ConversationAttachmentReadResultSchema
|
|
1114
|
+
>;
|
|
1115
|
+
|
|
1116
|
+
/** Share 选择阶段的 metadata-only 附件检查。 */
|
|
1117
|
+
export const v4ConversationAttachmentStatParamsSchema = z
|
|
1118
|
+
.object({
|
|
1119
|
+
sessionId: z.string().min(1),
|
|
1120
|
+
ref: z.string().min(1),
|
|
1121
|
+
target: conversationRowTargetSchema,
|
|
1122
|
+
attachmentIndex: z.number().int().nonnegative(),
|
|
1123
|
+
})
|
|
1124
|
+
.strict();
|
|
1125
|
+
export type V4ConversationAttachmentStatParams = z.infer<
|
|
1126
|
+
typeof v4ConversationAttachmentStatParamsSchema
|
|
1127
|
+
>;
|
|
1128
|
+
|
|
1129
|
+
export const v4ConversationAttachmentStatResultSchema = z
|
|
1130
|
+
.object({
|
|
1131
|
+
mediaType: z.string().min(1),
|
|
1132
|
+
// stat 是 metadata-only 探测,必须能表达超过传输上限的真实大小,否则
|
|
1133
|
+
// 「已知容量超限」无法在选择阶段作为阻断项呈现(见 attachmentStatMaxBytes 注释)。
|
|
1134
|
+
totalBytes: z.number().int().nonnegative().max(PROTOCOL_V4_LIMITS.attachmentStatMaxBytes),
|
|
1135
|
+
mtimeMs: z.number().finite().optional(),
|
|
1136
|
+
})
|
|
1137
|
+
.strict();
|
|
1138
|
+
export type V4ConversationAttachmentStatResult = z.infer<
|
|
1139
|
+
typeof v4ConversationAttachmentStatResultSchema
|
|
1140
|
+
>;
|
|
1141
|
+
|
|
1142
|
+
/** conversation topic key 构造(与 parseConversationTopic 对偶)。 */
|
|
1143
|
+
export function conversationTopic(sessionId: string): string {
|
|
1144
|
+
return `conversation/${sessionId}`;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** conversation topic key 解析("conversation/<sessionId>")。 */
|
|
1148
|
+
export function parseConversationTopic(topic: string): string | null {
|
|
1149
|
+
if (!topic.startsWith("conversation/")) return null;
|
|
1150
|
+
const sessionId = topic.slice("conversation/".length);
|
|
1151
|
+
return sessionId.length > 0 ? sessionId : null;
|
|
1152
|
+
}
|