gpt-connector 0.1.0 → 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/dist/src/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { GptConnector } from "./connector.js";
3
+ import { ConsultJobStore } from "./consult-job-store.js";
3
4
  import { ConnectorError } from "./errors.js";
5
+ import { packageVersion } from "./version.js";
4
6
  function parseArgs(args) {
5
7
  const [command, ...rest] = args;
6
8
  const values = new Map();
@@ -10,10 +12,16 @@ function parseArgs(args) {
10
12
  throw new Error(`不明な引数: ${current}`);
11
13
  const key = current.slice(2);
12
14
  const next = rest[index + 1];
13
- if (next === undefined || next.startsWith("--"))
15
+ const existing = values.get(key);
16
+ if (next === undefined || next.startsWith("--")) {
17
+ if (existing !== undefined)
18
+ throw new Error(`重複したflag: --${key}`);
14
19
  values.set(key, true);
20
+ }
15
21
  else {
16
- values.set(key, next);
22
+ if (existing === true)
23
+ throw new Error(`値とflagを混在できません: --${key}`);
24
+ values.set(key, [...(existing ?? []), next]);
17
25
  index += 1;
18
26
  }
19
27
  }
@@ -21,15 +29,65 @@ function parseArgs(args) {
21
29
  }
22
30
  function stringArg(values, name) {
23
31
  const value = values.get(name);
24
- return typeof value === "string" ? value : undefined;
32
+ if (value === undefined)
33
+ return undefined;
34
+ if (value === true || value.length !== 1)
35
+ throw new Error(`--${name}は1つの値が必要です。`);
36
+ return value[0];
37
+ }
38
+ function stringArgs(values, name) {
39
+ const value = values.get(name);
40
+ if (value === undefined)
41
+ return undefined;
42
+ if (value === true)
43
+ throw new Error(`--${name}には値が必要です。`);
44
+ return [...value];
45
+ }
46
+ function flagArg(values, name) {
47
+ const value = values.get(name);
48
+ if (value === undefined)
49
+ return false;
50
+ if (value !== true)
51
+ throw new Error(`--${name}は値なしflagです。`);
52
+ return true;
53
+ }
54
+ function writeJson(value) {
55
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
25
56
  }
26
57
  async function main() {
27
58
  const { command, values } = parseArgs(process.argv.slice(2));
59
+ if (command === "--version" || command === "version") {
60
+ process.stdout.write(`${packageVersion}\n`);
61
+ return;
62
+ }
28
63
  const endpoint = stringArg(values, "endpoint") ?? "http://127.0.0.1:9223";
29
- const connector = await GptConnector.connect({ endpoint });
64
+ const stateDirectory = stringArg(values, "state-directory") ??
65
+ process.env.GPT_CONNECTOR_STATE_DIR;
66
+ if (command === "sessions") {
67
+ const slug = stringArg(values, "slug");
68
+ if (slug === undefined)
69
+ throw new Error("sessionsには--slugが必要です。");
70
+ const store = new ConsultJobStore({ stateDirectory, readOnly: true });
71
+ await store.initialize();
72
+ try {
73
+ writeJson(store.get(slug));
74
+ }
75
+ finally {
76
+ store.close();
77
+ }
78
+ return;
79
+ }
80
+ if (command === "doctor" || command === "diagnostics") {
81
+ const diagnostics = await GptConnector.doctor({ endpoint, stateDirectory });
82
+ writeJson(diagnostics);
83
+ if (diagnostics.overall !== "ready")
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ const connector = await GptConnector.connect({ endpoint, stateDirectory });
30
88
  try {
31
89
  if (command === "models") {
32
- process.stdout.write(`${JSON.stringify(await connector.models(), null, 2)}\n`);
90
+ writeJson(await connector.models());
33
91
  return;
34
92
  }
35
93
  if (command === "chat") {
@@ -42,10 +100,35 @@ async function main() {
42
100
  effort: stringArg(values, "effort"),
43
101
  keepOpen: false,
44
102
  });
45
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
103
+ writeJson(result);
104
+ return;
105
+ }
106
+ if (command === "consult") {
107
+ const prompt = stringArg(values, "prompt");
108
+ const slug = stringArg(values, "slug");
109
+ if (prompt === undefined || slug === undefined) {
110
+ throw new Error("consultには--promptと--slugが必要です。");
111
+ }
112
+ writeJson(await connector.consult({
113
+ prompt,
114
+ slug,
115
+ files: stringArgs(values, "file"),
116
+ workspaceRoot: stringArg(values, "workspace-root"),
117
+ model: stringArg(values, "model"),
118
+ effort: stringArg(values, "effort"),
119
+ keepOpen: flagArg(values, "keep-open"),
120
+ dryRun: flagArg(values, "dry-run"),
121
+ }));
122
+ return;
123
+ }
124
+ if (command === "close") {
125
+ const sessionId = stringArg(values, "session-id");
126
+ if (sessionId === undefined)
127
+ throw new Error("closeには--session-idが必要です。");
128
+ writeJson(await connector.closeSession({ sessionId }));
46
129
  return;
47
130
  }
48
- throw new Error("usage: gpt-connector models | chat --prompt <text> [--model <id>] [--effort <id>]");
131
+ throw new Error("usage: gpt-connector --version | models | doctor | chat --prompt <text> | consult --prompt <text> --slug <id> [--workspace-root <abs> --file <spec> ...] [--model <id> --effort <id>] [--dry-run] | sessions --slug <id> | close --session-id <uuid>");
49
132
  }
50
133
  finally {
51
134
  connector.close();
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAO7C,SAAS,SAAS,CAAC,IAAuB;IACxC,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;IAChD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAE,CAAC;QAC7B,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;QACpE,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAC7B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;aAClE,CAAC;YACJ,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACtB,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC;AAED,SAAS,SAAS,CAAC,MAA0C,EAAE,IAAY;IACzE,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvD,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,IAAI,uBAAuB,CAAC;IAC1E,MAAM,SAAS,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;IAE3D,IAAI,CAAC;QACH,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,SAAS,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YAC/E,OAAO;QACT,CAAC;QAED,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YAC3C,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;YAClE,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC;gBAClC,MAAM;gBACN,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC;gBACjC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC;gBACnC,QAAQ,EAAE,KAAK;aAChB,CAAC,CAAC;YACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QAED,MAAM,IAAI,KAAK,CAAC,mFAAmF,CAAC,CAAC;IACvG,CAAC;YAAS,CAAC;QACT,SAAS,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,IAAI,KAAK,YAAY,cAAc,EAAE,CAAC;QACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;IAC5F,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;IACjG,CAAC;IACD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,20 @@
1
- import { type ChatInput, type ChatResult, type CloseInput, type CloseResult, type ModelCatalog } from "./contract.js";
1
+ import { type ChatInput, type ChatResult, type CloseInput, type CloseResult, type ConnectorDiagnostics, type ConsultDryRunResult, type ConsultInput, type ConsultSnapshot, type ModelCatalog, type SessionsInput } from "./contract.js";
2
2
  export interface ConnectorOptions {
3
3
  readonly endpoint?: string;
4
4
  readonly operationTimeoutMs?: number;
5
5
  readonly pollIntervalMs?: number;
6
+ readonly stateDirectory?: string;
6
7
  }
7
8
  export declare class GptConnector {
8
9
  #private;
9
10
  private constructor();
10
11
  static connect(options?: ConnectorOptions): Promise<GptConnector>;
12
+ static doctor(options?: ConnectorOptions): Promise<ConnectorDiagnostics>;
11
13
  models(): Promise<ModelCatalog>;
14
+ diagnostics(): Promise<ConnectorDiagnostics>;
12
15
  chat(input: ChatInput): Promise<ChatResult>;
16
+ consult(input: ConsultInput): Promise<ConsultSnapshot | ConsultDryRunResult>;
17
+ sessions(input: SessionsInput): ConsultSnapshot;
13
18
  closeSession(input: CloseInput): Promise<CloseResult>;
14
19
  close(): void;
15
20
  shutdown(): Promise<void>;
@@ -1,13 +1,17 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { setTimeout as delay } from "node:timers/promises";
2
3
  import { z } from "zod";
4
+ import { attachmentLimits, prepareAttachmentFiles, } from "./attachment-files.js";
3
5
  import { discoverRuntimeAssets, listLoadedAssetUrls } from "./asset-discovery.js";
4
6
  import { CdpClient, discoverChatGptTarget } from "./cdp.js";
5
- import { chatInputSchema, closeInputSchema, } from "./contract.js";
7
+ import { chatInputSchema, closeInputSchema, consultInputSchema, sessionsInputSchema, } from "./contract.js";
8
+ import { ConsultJobStore } from "./consult-job-store.js";
6
9
  import { ConnectorError, connectorErrorCodes, } from "./errors.js";
7
10
  import { validateModelSelection } from "./model-catalog.js";
8
11
  import { bridgeBuildId, createBridgeBootstrapExpression, createBridgeCallExpression, } from "./page-bridge.js";
9
12
  import { evaluateByValue } from "./runtime-evaluate.js";
10
13
  import { SessionRegistry } from "./session-registry.js";
14
+ import { packageVersion } from "./version.js";
11
15
  const operationStartSchema = z.object({ operationId: z.string().uuid() });
12
16
  const operationErrorSchema = z.object({
13
17
  code: z.string(),
@@ -37,14 +41,42 @@ const chatResultSchema = z.object({
37
41
  resolvedEffort: z.string().nullable(),
38
42
  sessionId: z.string().uuid().optional(),
39
43
  });
44
+ const attachmentSummarySchema = z.object({
45
+ count: z.number().int().nonnegative(),
46
+ names: z.array(z.string()),
47
+ mimeTypes: z.array(z.string().nullable()),
48
+ readBack: z.literal("confirmed"),
49
+ retention: z.literal("unknown"),
50
+ cleanup: z.enum(["not_supported", "failed", "deleted"]),
51
+ });
52
+ const bridgeChatResultSchema = chatResultSchema.extend({
53
+ attachments: attachmentSummarySchema,
54
+ });
55
+ const uploadHandleSchema = z.object({ uploadHandle: z.string().uuid() });
56
+ const uploadChunkResultSchema = z.object({ receivedBytes: z.number().int().nonnegative() });
57
+ const uploadResultSchema = z.object({
58
+ uploadHandle: z.string().uuid(),
59
+ name: z.string(),
60
+ size: z.number().int().positive(),
61
+ mimeType: z.string(),
62
+ });
63
+ const uploadChunkBytes = 256 * 1024;
40
64
  const closeResultSchema = z.object({ archived: z.literal(true) });
65
+ const pageDiagnosticsSchema = z.object({
66
+ sessionCount: z.number().int().nonnegative(),
67
+ operationCount: z.number().int().nonnegative(),
68
+ uploadCount: z.number().int().nonnegative(),
69
+ bufferedUploadBytes: z.number().int().nonnegative(),
70
+ });
41
71
  export class GptConnector {
42
72
  #client;
43
73
  #sessions = new SessionRegistry();
74
+ #jobs;
44
75
  #operationTimeoutMs;
45
76
  #pollIntervalMs;
46
77
  constructor(client, options) {
47
78
  this.#client = client;
79
+ this.#jobs = new ConsultJobStore({ stateDirectory: options.stateDirectory });
48
80
  this.#operationTimeoutMs = options.operationTimeoutMs ?? 180_000;
49
81
  this.#pollIntervalMs = options.pollIntervalMs ?? 250;
50
82
  }
@@ -55,28 +87,214 @@ export class GptConnector {
55
87
  const connector = new GptConnector(client, options);
56
88
  try {
57
89
  await client.call("Runtime.enable");
90
+ await connector.#jobs.initialize();
58
91
  await connector.#bootstrap();
59
92
  return connector;
60
93
  }
61
94
  catch (error) {
62
- client.close();
95
+ try {
96
+ connector.#jobs.close();
97
+ }
98
+ finally {
99
+ client.close();
100
+ }
63
101
  throw error;
64
102
  }
65
103
  }
104
+ static async doctor(options = {}) {
105
+ let connector = null;
106
+ try {
107
+ connector = await GptConnector.connect(options);
108
+ return await connector.diagnostics();
109
+ }
110
+ catch (error) {
111
+ const code = error instanceof ConnectorError ? error.code : null;
112
+ return {
113
+ schema: "gpt-connector.diagnostics.v1",
114
+ packageVersion,
115
+ overall: "not_ready",
116
+ reasonCode: diagnosticReason(code),
117
+ cdpConnected: false,
118
+ officialOrigin: null,
119
+ authenticated: code === "AUTH_REQUIRED" ? false : null,
120
+ bridgeBuildId,
121
+ sessionCount: null,
122
+ operationCount: null,
123
+ uploadCount: null,
124
+ bufferedUploadBytes: null,
125
+ jobCount: null,
126
+ activeJobCount: null,
127
+ terminalJobCount: null,
128
+ };
129
+ }
130
+ finally {
131
+ connector?.close();
132
+ }
133
+ }
66
134
  async models() {
67
135
  const result = await this.#runOperation("startModels", []);
68
136
  return modelCatalogSchema.parse(result);
69
137
  }
138
+ async diagnostics() {
139
+ const page = pageDiagnosticsSchema.parse(await evaluateByValue(this.#client, createBridgeCallExpression("diagnostics", []), false));
140
+ const jobs = this.#jobs.diagnostics();
141
+ return {
142
+ schema: "gpt-connector.diagnostics.v1",
143
+ packageVersion,
144
+ overall: "ready",
145
+ reasonCode: "ready",
146
+ cdpConnected: true,
147
+ officialOrigin: true,
148
+ authenticated: true,
149
+ bridgeBuildId,
150
+ ...page,
151
+ ...jobs,
152
+ };
153
+ }
70
154
  async chat(input) {
71
155
  const parsed = chatInputSchema.parse(input);
156
+ const result = await this.#chatParsed(parsed, []);
157
+ return chatResultSchema.parse(result);
158
+ }
159
+ async consult(input) {
160
+ let parsed;
161
+ try {
162
+ parsed = consultInputSchema.parse(input);
163
+ }
164
+ catch {
165
+ throw new ConnectorError("INVALID_INPUT", "consult inputが公開schemaに一致しません。");
166
+ }
72
167
  const catalog = await this.models();
73
168
  validateModelSelection(catalog, parsed.model, parsed.effort);
169
+ const prepared = parsed.files === undefined
170
+ ? { files: [], totalBytes: 0 }
171
+ : await prepareAttachmentFiles({
172
+ workspaceRoot: parsed.workspaceRoot,
173
+ specs: parsed.files,
174
+ });
175
+ const fileMetadata = prepared.files.map((file) => ({
176
+ relativePath: file.relativePath,
177
+ name: file.name,
178
+ bytes: file.bytes,
179
+ mimeType: file.mimeType,
180
+ sha256: file.sha256,
181
+ }));
182
+ if (parsed.dryRun) {
183
+ for (const file of prepared.files)
184
+ file.content.fill(0);
185
+ return {
186
+ dryRun: true,
187
+ slug: parsed.slug,
188
+ files: fileMetadata,
189
+ totalBytes: prepared.totalBytes,
190
+ requestedModel: parsed.model ?? null,
191
+ requestedEffort: parsed.effort ?? null,
192
+ limits: attachmentLimits,
193
+ uploadWouldRun: false,
194
+ conversationWouldRun: false,
195
+ };
196
+ }
197
+ const fingerprint = consultFingerprint({
198
+ prompt: parsed.prompt,
199
+ files: fileMetadata,
200
+ model: parsed.model ?? null,
201
+ effort: parsed.effort ?? null,
202
+ keepOpen: parsed.keepOpen,
203
+ });
204
+ const reservation = await this.#jobs.reserve(parsed.slug, fingerprint);
205
+ if (!reservation.created) {
206
+ for (const file of prepared.files)
207
+ file.content.fill(0);
208
+ return reservation.snapshot;
209
+ }
210
+ return this.#runConsultJob(parsed, prepared.files);
211
+ }
212
+ sessions(input) {
213
+ let parsed;
214
+ try {
215
+ parsed = sessionsInputSchema.parse(input);
216
+ }
217
+ catch {
218
+ throw new ConnectorError("INVALID_INPUT", "sessions inputが公開schemaに一致しません。");
219
+ }
220
+ return this.#jobs.get(parsed.slug);
221
+ }
222
+ async #runConsultJob(parsed, files) {
223
+ let uploadHandles = [];
224
+ try {
225
+ await this.#jobs.transition(parsed.slug, "uploading");
226
+ uploadHandles = await this.#uploadAttachments(files);
227
+ await this.#jobs.transition(parsed.slug, "submitted");
228
+ await this.#jobs.transition(parsed.slug, "running");
229
+ const result = await this.#chatParsed({
230
+ prompt: parsed.prompt,
231
+ model: parsed.model,
232
+ effort: parsed.effort,
233
+ keepOpen: parsed.keepOpen,
234
+ }, uploadHandles, true);
235
+ return this.#jobs.transition(parsed.slug, "succeeded", {
236
+ result: {
237
+ text: result.text,
238
+ status: result.status,
239
+ endTurn: true,
240
+ resolvedModel: result.resolvedModel,
241
+ resolvedEffort: result.resolvedEffort,
242
+ ...(result.sessionId === undefined ? {} : { sessionId: result.sessionId }),
243
+ attachments: result.attachments,
244
+ archived: parsed.keepOpen !== true,
245
+ },
246
+ error: null,
247
+ });
248
+ }
249
+ catch (error) {
250
+ let jobError = error;
251
+ let cleanup = "not_supported";
252
+ for (const uploadHandle of uploadHandles) {
253
+ try {
254
+ await this.#discardUpload(uploadHandle);
255
+ }
256
+ catch {
257
+ // job errorをcleanup failureへ昇格し、成功や元errorの握り潰しを防ぐ。
258
+ jobError = new ConnectorError("UPLOAD_FAILED", "consult失敗後にpage側upload handleを完全破棄できませんでした。");
259
+ cleanup = "failed";
260
+ break;
261
+ }
262
+ }
263
+ const connectorError = jobError instanceof ConnectorError
264
+ ? jobError
265
+ : new ConnectorError("CHAT_FAILED", "consult jobが失敗しました。");
266
+ const partialCount = typeof connectorError.details?.uploadedCount === "number"
267
+ ? connectorError.details.uploadedCount
268
+ : uploadHandles.length;
269
+ const partialCleanup = connectorError.details?.cleanup === "failed"
270
+ ? "failed"
271
+ : cleanup;
272
+ return this.#jobs.transition(parsed.slug, "failed", {
273
+ result: null,
274
+ error: {
275
+ code: connectorError.code,
276
+ message: connectorError.message,
277
+ retry: retryFor(connectorError.code),
278
+ ...(partialCount > 0 ? {
279
+ partialUpload: { count: partialCount, cleanup: partialCleanup },
280
+ } : {}),
281
+ },
282
+ });
283
+ }
284
+ }
285
+ async #chatParsed(parsed, attachmentHandles, selectionValidated = false) {
286
+ if (!selectionValidated) {
287
+ const catalog = await this.models();
288
+ validateModelSelection(catalog, parsed.model, parsed.effort);
289
+ }
74
290
  const existingSession = parsed.sessionId;
75
291
  if (existingSession !== undefined)
76
292
  this.#sessions.acquire(existingSession);
77
293
  try {
78
- const raw = await this.#runOperation("startChat", [parsed]);
79
- const result = chatResultSchema.parse(raw);
294
+ const raw = await this.#runOperation("startChat", [
295
+ { ...parsed, attachmentHandles },
296
+ ]);
297
+ const result = bridgeChatResultSchema.parse(raw);
80
298
  if (existingSession !== undefined) {
81
299
  if (parsed.keepOpen)
82
300
  this.#sessions.release(existingSession);
@@ -101,6 +319,87 @@ export class GptConnector {
101
319
  throw error;
102
320
  }
103
321
  }
322
+ async #uploadAttachments(files) {
323
+ const uploadHandles = [];
324
+ try {
325
+ for (const file of files) {
326
+ uploadHandles.push(await this.#transferAttachment(file));
327
+ }
328
+ return uploadHandles;
329
+ }
330
+ catch (error) {
331
+ const cleanupFailures = [];
332
+ for (const uploadHandle of uploadHandles) {
333
+ try {
334
+ await this.#discardUpload(uploadHandle);
335
+ }
336
+ catch (cleanupError) {
337
+ cleanupFailures.push(cleanupError instanceof ConnectorError ? cleanupError.code : "RUNTIME_DRIFT");
338
+ }
339
+ }
340
+ if (cleanupFailures.length > 0) {
341
+ throw new ConnectorError("UPLOAD_FAILED", "添付upload失敗後にpage側handleを完全破棄できませんでした。", {
342
+ cleanupFailureCount: cleanupFailures.length,
343
+ uploadedCount: uploadHandles.length,
344
+ cleanup: "failed",
345
+ originalCode: error instanceof ConnectorError ? error.code : "UPLOAD_FAILED",
346
+ });
347
+ }
348
+ if (uploadHandles.length > 0) {
349
+ const original = error instanceof ConnectorError
350
+ ? error
351
+ : new ConnectorError("UPLOAD_FAILED", "添付uploadが途中で失敗しました。");
352
+ throw new ConnectorError(original.code, original.message, {
353
+ uploadedCount: uploadHandles.length,
354
+ cleanup: "not_supported",
355
+ });
356
+ }
357
+ throw error;
358
+ }
359
+ finally {
360
+ for (const file of files)
361
+ file.content.fill(0);
362
+ }
363
+ }
364
+ async #transferAttachment(file) {
365
+ const created = uploadHandleSchema.parse(await evaluateByValue(this.#client, createBridgeCallExpression("createUpload", [{
366
+ name: file.name,
367
+ mimeType: file.mimeType,
368
+ size: file.bytes,
369
+ sha256: file.sha256,
370
+ }]), false));
371
+ try {
372
+ for (let offset = 0; offset < file.content.byteLength; offset += uploadChunkBytes) {
373
+ const length = Math.min(uploadChunkBytes, file.content.byteLength - offset);
374
+ const base64Chunk = Buffer.from(file.content.buffer, file.content.byteOffset + offset, length).toString("base64");
375
+ const appended = uploadChunkResultSchema.parse(await evaluateByValue(this.#client, createBridgeCallExpression("appendUploadChunk", [
376
+ created.uploadHandle,
377
+ base64Chunk,
378
+ ]), false));
379
+ if (appended.receivedBytes !== offset + length) {
380
+ throw new ConnectorError("UPLOAD_FAILED", "page側の添付byte受信数が一致しませんでした。");
381
+ }
382
+ }
383
+ const result = uploadResultSchema.parse(await this.#runOperation("startUpload", [{
384
+ uploadHandle: created.uploadHandle,
385
+ timeoutMs: Math.min(this.#operationTimeoutMs, 120_000),
386
+ }]));
387
+ if (result.uploadHandle !== created.uploadHandle ||
388
+ result.name !== file.name ||
389
+ result.size !== file.bytes ||
390
+ result.mimeType !== file.mimeType) {
391
+ throw new ConnectorError("RUNTIME_DRIFT", "upload完了metadataが送信fileと一致しませんでした。");
392
+ }
393
+ return created.uploadHandle;
394
+ }
395
+ catch (error) {
396
+ await this.#discardUpload(created.uploadHandle);
397
+ throw error;
398
+ }
399
+ }
400
+ async #discardUpload(uploadHandle) {
401
+ await evaluateByValue(this.#client, createBridgeCallExpression("discardUpload", [uploadHandle]), false);
402
+ }
104
403
  async closeSession(input) {
105
404
  const parsed = closeInputSchema.parse(input);
106
405
  this.#sessions.acquire(parsed.sessionId);
@@ -117,7 +416,12 @@ export class GptConnector {
117
416
  }
118
417
  }
119
418
  close() {
120
- this.#client.close();
419
+ try {
420
+ this.#jobs.close();
421
+ }
422
+ finally {
423
+ this.#client.close();
424
+ }
121
425
  }
122
426
  async shutdown() {
123
427
  const failures = [];
@@ -129,7 +433,12 @@ export class GptConnector {
129
433
  failures.push(error instanceof ConnectorError ? error.code : "CHAT_FAILED");
130
434
  }
131
435
  }
132
- this.#client.close();
436
+ try {
437
+ this.#jobs.close();
438
+ }
439
+ finally {
440
+ this.#client.close();
441
+ }
133
442
  if (failures.length > 0) {
134
443
  throw new ConnectorError("ARCHIVE_FAILED", "shutdown時に一部sessionをarchiveできませんでした。", { failureCount: failures.length });
135
444
  }
@@ -175,7 +484,7 @@ export class GptConnector {
175
484
  return;
176
485
  const urls = await listLoadedAssetUrls(this.#client);
177
486
  const assets = await discoverRuntimeAssets(urls);
178
- const summary = await evaluateByValue(this.#client, createBridgeBootstrapExpression(assets.coreUrl, assets.conversationUrl));
487
+ const summary = await evaluateByValue(this.#client, createBridgeBootstrapExpression(assets.coreUrl, assets.conversationUrl, assets.uploadUrl));
179
488
  const parsed = z
180
489
  .object({
181
490
  version: z.literal(1),
@@ -187,6 +496,7 @@ export class GptConnector {
187
496
  throw new ConnectorError("RUNTIME_DRIFT", "page bridgeを初期化できませんでした。", {
188
497
  coreFingerprint: assets.coreFingerprint,
189
498
  conversationFingerprint: assets.conversationFingerprint,
499
+ uploadFingerprint: assets.uploadFingerprint,
190
500
  });
191
501
  }
192
502
  }
@@ -206,9 +516,7 @@ export class GptConnector {
206
516
  }
207
517
  await delay(this.#pollIntervalMs);
208
518
  }
209
- throw new ConnectorError("CHAT_FAILED", "ChatGPT runtime operationがtimeoutしました。", {
210
- method,
211
- });
519
+ throw new ConnectorError(method === "startUpload" ? "UPLOAD_TIMEOUT" : "CHAT_FAILED", "ChatGPT runtime operationがtimeoutしました。", { method });
212
520
  }
213
521
  async #consumeOperation(operationId) {
214
522
  await evaluateByValue(this.#client, createBridgeCallExpression("poll", [operationId, true]), false);
@@ -219,4 +527,54 @@ function toConnectorErrorCode(value) {
219
527
  ? value
220
528
  : "CHAT_FAILED";
221
529
  }
530
+ function diagnosticReason(code) {
531
+ if (code === "AUTH_REQUIRED")
532
+ return "auth_required";
533
+ if (code === "CDP_UNAVAILABLE")
534
+ return "cdp_unavailable";
535
+ if (code === "RUNTIME_DRIFT")
536
+ return "runtime_drift";
537
+ if (code === "JOB_RECOVERY_UNAVAILABLE")
538
+ return "state_unavailable";
539
+ return "connector_error";
540
+ }
541
+ function consultFingerprint(input) {
542
+ return createHash("sha256")
543
+ .update(JSON.stringify({
544
+ promptSha256: createHash("sha256").update(input.prompt).digest("hex"),
545
+ files: input.files.map((file) => ({
546
+ relativePath: file.relativePath,
547
+ bytes: file.bytes,
548
+ sha256: file.sha256,
549
+ })),
550
+ model: input.model,
551
+ effort: input.effort,
552
+ keepOpen: input.keepOpen,
553
+ }))
554
+ .digest("hex");
555
+ }
556
+ function retryFor(code) {
557
+ if (code === "AUTH_REQUIRED")
558
+ return "after_auth";
559
+ if (code === "RUNTIME_DRIFT")
560
+ return "after_runtime_update";
561
+ if (code === "INVALID_INPUT" ||
562
+ code === "FILE_NOT_FOUND" ||
563
+ code === "FILE_OUTSIDE_ROOT" ||
564
+ code === "SENSITIVE_FILE_BLOCKED" ||
565
+ code === "FILE_TYPE_NOT_SUPPORTED" ||
566
+ code === "FILE_EMPTY" ||
567
+ code === "FILE_LIMIT_EXCEEDED" ||
568
+ code === "MODEL_NOT_AVAILABLE" ||
569
+ code === "EFFORT_NOT_SUPPORTED")
570
+ return "after_input_change";
571
+ if (code === "UPLOAD_TIMEOUT" ||
572
+ code === "UPLOAD_FAILED" ||
573
+ code === "CHAT_FAILED" ||
574
+ code === "STREAM_INCOMPLETE" ||
575
+ code === "ATTACHMENT_READBACK_FAILED" ||
576
+ code === "JOB_RECOVERY_UNAVAILABLE")
577
+ return "status_first";
578
+ return "never";
579
+ }
222
580
  //# sourceMappingURL=connector.js.map