chatgpt-pro-research 0.2.0 → 0.2.3

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.
Files changed (3) hide show
  1. package/lib/client.js +132 -3
  2. package/lib/index.js +104 -18
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -34,8 +34,135 @@ var import_react = require("react");
34
34
  var RESEARCH_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-40 -76 200 200"><title>Anyvia R</title><defs><mask id="R-mono-black-standard-gap" maskUnits="userSpaceOnUse" x="-100" y="-100" width="400" height="400"><rect x="-100" y="-100" width="400" height="400" fill="white"/><path d="M77.18032292691355 52.74975779269961 A3 3 0 0 1 79.72446721538282 48.16 L108.02474889607315 48.16 A3 3 0 0 1 110.56889318454242 49.57024220730038 L139.21290427606598 95.41024220730039 A3 3 0 0 1 136.6687599875967 100 L108.36847830690638 100 A3 3 0 0 1 105.82433401843711 98.58975779269961 Z" transform="translate(-30 -12)" fill="black" stroke="black" stroke-width="6" stroke-linejoin="round"/></mask></defs><g mask="url(#R-mono-black-standard-gap)"><path d="M3.46 -40 H67 A39 39 0 0 1 67 38 H36.81354211962983 V84.54 Q36.81354211962983 88 33.35354211962983 88 H3.46 Q0 88 0 84.54 V-36.54 Q0 -40 3.46 -40 Z M36.81354211962983 -14 V12 H67 A13 13 0 0 0 67 -14 Z" fill="currentColor" fill-rule="evenodd"/></g><path d="M77.18032292691355 52.74975779269961 A3 3 0 0 1 79.72446721538282 48.16 L108.02474889607315 48.16 A3 3 0 0 1 110.56889318454242 49.57024220730038 L139.21290427606598 95.41024220730039 A3 3 0 0 1 136.6687599875967 100 L108.36847830690638 100 A3 3 0 0 1 105.82433401843711 98.58975779269961 Z" transform="translate(-30 -12)" fill="currentColor"/></svg>';
35
35
  var DESIGN_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-22.599456503942804 -56 200 200"><title>Anyvia X</title><defs><mask id="X-mono-black-standard-gap" maskUnits="userSpaceOnUse" x="-100" y="-100" width="400" height="400"><rect x="-100" y="-100" width="400" height="400" fill="white"/><path d="M77.18032292691355 52.74975779269961 A3 3 0 0 1 79.72446721538282 48.16 L108.02474889607315 48.16 A3 3 0 0 1 110.56889318454242 49.57024220730038 L139.21290427606598 95.41024220730039 A3 3 0 0 1 136.6687599875967 100 L108.36847830690638 100 A3 3 0 0 1 105.82433401843711 98.58975779269961 Z" transform="translate(-12 0)" fill="black" stroke="black" stroke-width="6" stroke-linejoin="round"/></mask></defs><g mask="url(#X-mono-black-standard-gap)"><path d="M89.67202319894069 -10.373520654246889 A3.46 3.46 0 0 1 92.60626961164192 -12 L127.85608887910462 -12 A3.46 3.46 0 0 1 130.79033529180586 -6.706479345753113 L65.12906379317373 98.37352065424689 A3.46 3.46 0 0 1 62.19481738047249 100 L26.94499811300981 100 A3.46 3.46 0 0 1 24.010751700308575 94.70647934575311 Z" fill="currentColor"/><path d="M28.027942559241307 -6.706479345753113 A3.46 3.46 0 0 1 30.96218897194254 -12 L58.17762652153973 -12 A3.46 3.46 0 0 1 61.111872934240964 -10.373520654246889 L91.78046072595076 38.70647934575311 A3.46 3.46 0 0 1 88.84621431324952 44 L61.63077676365233 44 A3.46 3.46 0 0 1 58.69653035095109 42.37352065424689 Z" fill="currentColor"/></g><path d="M77.18032292691355 52.74975779269961 A3 3 0 0 1 79.72446721538282 48.16 L108.02474889607315 48.16 A3 3 0 0 1 110.56889318454242 49.57024220730038 L139.21290427606598 95.41024220730039 A3 3 0 0 1 136.6687599875967 100 L108.36847830690638 100 A3 3 0 0 1 105.82433401843711 98.58975779269961 Z" transform="translate(-12 0)" fill="currentColor"/></svg>';
36
36
 
37
+ // client/sidebar-status.ts
38
+ var POLL_MS = 5e3;
39
+ function countTopics(topics) {
40
+ const counts = { active: 0, completed: 0, failed: 0 };
41
+ for (const topic of topics) {
42
+ switch (topic.status) {
43
+ case "queued":
44
+ case "uploading":
45
+ case "submitted":
46
+ case "waiting":
47
+ case "answered":
48
+ case "adjudicating":
49
+ counts.active += 1;
50
+ break;
51
+ case "completed":
52
+ counts.completed += 1;
53
+ break;
54
+ case "failed":
55
+ case "stalled":
56
+ counts.failed += 1;
57
+ break;
58
+ default:
59
+ break;
60
+ }
61
+ }
62
+ return counts;
63
+ }
64
+ function badgeText(counts) {
65
+ const parts = [];
66
+ if (counts.active > 0) parts.push(`${counts.active} \u8FDB\u884C`);
67
+ if (counts.completed > 0) parts.push(`${counts.completed} \u5B8C\u6210`);
68
+ if (counts.failed > 0) parts.push(`${counts.failed} \u5931\u8D25`);
69
+ return parts.join(" \xB7 ");
70
+ }
71
+ function mountSidebarStatus(options) {
72
+ if (typeof document === "undefined") return () => {
73
+ };
74
+ let badge;
75
+ let text = "";
76
+ let disposed = false;
77
+ let polling = false;
78
+ let attachTimer;
79
+ let observer;
80
+ const rowFor = () => {
81
+ const buttons = document.querySelectorAll("button[aria-label]");
82
+ for (const button of buttons) {
83
+ if (button.getAttribute("aria-label") === options.label) return button;
84
+ }
85
+ return void 0;
86
+ };
87
+ const applyText = (next) => {
88
+ text = next;
89
+ if (badge !== void 0 && badge.textContent !== text) badge.textContent = text;
90
+ ensureMounted();
91
+ };
92
+ const ensureMounted = () => {
93
+ if (disposed) return;
94
+ if (text === "") {
95
+ badge?.remove();
96
+ badge = void 0;
97
+ return;
98
+ }
99
+ const row = rowFor();
100
+ if (row === void 0) return;
101
+ const title = row.querySelector('[class*="panelTitle"]');
102
+ if (title === null) {
103
+ badge?.remove();
104
+ badge = void 0;
105
+ return;
106
+ }
107
+ if (badge !== void 0 && badge.parentElement === row) {
108
+ if (badge.textContent !== text) badge.textContent = text;
109
+ return;
110
+ }
111
+ badge = document.createElement("span");
112
+ badge.dataset.dshProBadge = options.panelId;
113
+ badge.textContent = text;
114
+ badge.setAttribute("aria-hidden", "true");
115
+ badge.style.cssText = [
116
+ "margin-left:auto",
117
+ "display:inline-flex",
118
+ "align-items:center",
119
+ "flex:0 0 auto",
120
+ "font-size:10px",
121
+ "line-height:1",
122
+ "opacity:.62",
123
+ "white-space:nowrap",
124
+ "font-variant-numeric:tabular-nums",
125
+ "pointer-events:none"
126
+ ].join(";");
127
+ row.appendChild(badge);
128
+ };
129
+ const refresh = async () => {
130
+ if (disposed || polling) return;
131
+ polling = true;
132
+ try {
133
+ const response = await fetch(`${options.routes}/topics`, { headers: { accept: "application/json" } });
134
+ if (!response.ok) return;
135
+ const data = await response.json();
136
+ applyText(badgeText(countTopics(data.topics ?? [])));
137
+ } catch {
138
+ } finally {
139
+ polling = false;
140
+ }
141
+ };
142
+ observer = new MutationObserver(() => {
143
+ if (attachTimer !== void 0) return;
144
+ attachTimer = window.setTimeout(() => {
145
+ attachTimer = void 0;
146
+ ensureMounted();
147
+ }, 100);
148
+ });
149
+ observer.observe(document.body, { childList: true, subtree: true });
150
+ const timer = window.setInterval(() => {
151
+ void refresh();
152
+ }, POLL_MS);
153
+ void refresh();
154
+ return () => {
155
+ disposed = true;
156
+ window.clearInterval(timer);
157
+ if (attachTimer !== void 0) window.clearTimeout(attachTimer);
158
+ observer?.disconnect();
159
+ badge?.remove();
160
+ badge = void 0;
161
+ };
162
+ }
163
+
37
164
  // client/panel.tsx
38
- var POLL_MS = 3e3;
165
+ var POLL_MS2 = 3e3;
39
166
  var config = {
40
167
  routes: "/chatgpt-pro-research",
41
168
  panelId: "chatgpt-pro-research",
@@ -126,7 +253,7 @@ function Panel() {
126
253
  void loadList();
127
254
  const timer = setInterval(() => {
128
255
  void loadList();
129
- }, POLL_MS);
256
+ }, POLL_MS2);
130
257
  return () => clearInterval(timer);
131
258
  }, [loadList]);
132
259
  (0, import_react.useEffect)(() => {
@@ -134,7 +261,7 @@ function Panel() {
134
261
  void loadDetail(selected);
135
262
  const timer = setInterval(() => {
136
263
  void loadDetail(selected);
137
- }, POLL_MS);
264
+ }, POLL_MS2);
138
265
  return () => clearInterval(timer);
139
266
  }, [selected, loadDetail, topics]);
140
267
  const act = (0, import_react.useCallback)(async (action, conclusion) => {
@@ -315,6 +442,8 @@ function Icon({ kind }) {
315
442
  function apply(ctx) {
316
443
  ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: config.panelId }, () => (0, import_react.createElement)(Panel)));
317
444
  ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({ name: "sidebar.panellist", id: config.panelId, order: 21, label: () => config.label }, () => (0, import_react.createElement)(Icon, { kind: config.icon })));
445
+ const disposeBadge = mountSidebarStatus({ label: config.label, routes: config.routes, panelId: config.panelId });
446
+ if (typeof ctx.effect === "function") ctx.effect(() => disposeBadge, `${config.panelId}: sidebar status badge`);
318
447
  }
319
448
 
320
449
  // client/index.tsx
package/lib/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/plugin.ts
4
4
  import { homedir } from "node:os";
5
- import { dirname as dirname2, extname, isAbsolute, join as join3, resolve as resolve2, sep } from "node:path";
5
+ import { dirname as dirname2, extname, isAbsolute, join as join3, resolve as resolve3, sep } from "node:path";
6
6
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import z from "@deepseek-ai/schemastery";
@@ -597,7 +597,7 @@ var ResearchEngine = class {
597
597
  if (this.active !== void 0) {
598
598
  await this.active.promise;
599
599
  } else {
600
- await new Promise((resolve3) => setTimeout(resolve3, 0));
600
+ await new Promise((resolve4) => setTimeout(resolve4, 0));
601
601
  }
602
602
  }
603
603
  }
@@ -733,7 +733,9 @@ var ResearchEngine = class {
733
733
  try {
734
734
  return await this.options.transport.fetch({ submissionId });
735
735
  } catch (error) {
736
- if (polled.answer !== void 0) return { answer: polled.answer };
736
+ if (polled.answer !== void 0) {
737
+ return { answer: polled.answer.startsWith("image:") ? "" : polled.answer };
738
+ }
737
739
  throw error;
738
740
  }
739
741
  }
@@ -838,14 +840,16 @@ ${result.summary}
838
840
  if (topic.status === "completed" || topic.status === "cancelled") return;
839
841
  if (error instanceof TransportError) {
840
842
  if (error.failure === "worker") {
841
- await this.transition(topic, "failed");
843
+ await this.transition(topic, "failed", { stopReason: error.message });
842
844
  return;
843
845
  }
844
846
  const reason = error.failure === "login" || error.failure === "upload" || error.failure === "ui" ? error.failure : "timeout";
845
- await this.transition(topic, "stalled", { stallReason: reason });
847
+ await this.transition(topic, "stalled", { stallReason: reason, stopReason: error.message });
846
848
  return;
847
849
  }
848
- await this.transition(topic, "failed");
850
+ await this.transition(topic, "failed", {
851
+ stopReason: error instanceof Error ? error.message : String(error)
852
+ });
849
853
  }
850
854
  async transition(topic, next, extra) {
851
855
  assertTransition(topic.status, next);
@@ -901,7 +905,15 @@ var SELECTORS = {
901
905
  '[data-markdown-text-style="assistant-message"]',
902
906
  '[data-message-author-role="assistant"]'
903
907
  ].join(", "),
904
- fileInput: 'input[type="file"]',
908
+ // The composer has several file inputs (camera, photos, videos). The general
909
+ // attachment input is the one without an `accept` filter; the camera input
910
+ // rejects multiple files.
911
+ fileInput: [
912
+ 'input[type="file"][aria-label="\u9644\u52A0\u6587\u4EF6"]',
913
+ 'input[type="file"][aria-label*="Attach" i]:not([capture])',
914
+ 'form input[type="file"][multiple]:not([accept]):not([capture])',
915
+ 'input[type="file"][multiple]:not([accept]):not([capture])'
916
+ ].join(", "),
905
917
  modelPicker: [
906
918
  'button[aria-label*="\u9009\u62E9 ChatGPT \u6A21\u578B"]',
907
919
  'button[aria-label*="\u6A21\u578B"]',
@@ -922,6 +934,16 @@ function advanceCompletion(previous, text, streaming) {
922
934
  return { lastText: text, stable: 1, answered: false };
923
935
  }
924
936
 
937
+ // ../transport/src/assets.ts
938
+ function parseDataUrlImage(source) {
939
+ const match = /^data:([^;,]+);base64,(.*)$/s.exec(source);
940
+ if (match === null) return void 0;
941
+ const mime = match[1];
942
+ const base64 = match[2];
943
+ if (mime === void 0 || base64 === void 0) return void 0;
944
+ return { mime, base64: base64.replace(/\s/g, "") };
945
+ }
946
+
925
947
  // ../transport/src/chatgpt.ts
926
948
  function sha256(value) {
927
949
  return createHash3("sha256").update(value, "utf8").digest("hex");
@@ -947,6 +969,7 @@ async function firstVisible(page, selector, timeoutMs) {
947
969
  }
948
970
  var ChatGptWebTransport = class {
949
971
  context;
972
+ launching;
950
973
  submissions = /* @__PURE__ */ new Map();
951
974
  options;
952
975
  constructor(options) {
@@ -957,6 +980,15 @@ var ChatGptWebTransport = class {
957
980
  }
958
981
  async ensureContext() {
959
982
  if (this.context !== void 0) return this.context;
983
+ if (this.launching !== void 0) return this.launching;
984
+ this.launching = this.launchContext();
985
+ try {
986
+ return await this.launching;
987
+ } finally {
988
+ this.launching = void 0;
989
+ }
990
+ }
991
+ async launchContext() {
960
992
  await mkdir2(this.options.profileDir, { recursive: true });
961
993
  const base = {
962
994
  headless: this.options.headless ?? false,
@@ -964,7 +996,12 @@ var ChatGptWebTransport = class {
964
996
  args: ["--disable-blink-features=AutomationControlled"]
965
997
  };
966
998
  try {
967
- this.context = await chromium.launchPersistentContext(this.options.profileDir, { ...base, channel: "chrome" });
999
+ const context = await chromium.launchPersistentContext(this.options.profileDir, { ...base, channel: "chrome" });
1000
+ context.on("close", () => {
1001
+ if (this.context === context) this.context = void 0;
1002
+ });
1003
+ this.context = context;
1004
+ return context;
968
1005
  } catch (error) {
969
1006
  throw new TransportError(
970
1007
  `cannot launch Chrome with profile ${this.options.profileDir}; install Google Chrome or pass browser.headless=false plus a Playwright browser. ${error instanceof Error ? error.message : String(error)}`,
@@ -972,7 +1009,6 @@ var ChatGptWebTransport = class {
972
1009
  { cause: error }
973
1010
  );
974
1011
  }
975
- return this.context;
976
1012
  }
977
1013
  async probe() {
978
1014
  const context = await this.ensureContext();
@@ -1116,13 +1152,24 @@ ${attachments.join("\n")}`);
1116
1152
  const images = [];
1117
1153
  for (const [index, source] of sources.slice(0, 8).entries()) {
1118
1154
  try {
1155
+ if (source.startsWith("data:")) {
1156
+ const parsed = parseDataUrlImage(source);
1157
+ if (parsed === void 0) continue;
1158
+ if (Buffer.from(parsed.base64, "base64").byteLength < 2e4) continue;
1159
+ images.push({
1160
+ name: `image-${String(index + 1).padStart(2, "0")}.${extensionFor(parsed.mime)}`,
1161
+ mime: parsed.mime,
1162
+ base64: parsed.base64
1163
+ });
1164
+ continue;
1165
+ }
1119
1166
  if (source.startsWith("blob:")) {
1120
1167
  const dataUrl = await page.evaluate(async (src) => {
1121
1168
  const response = await fetch(src);
1122
1169
  const blob = await response.blob();
1123
- return await new Promise((resolve3, reject) => {
1170
+ return await new Promise((resolve4, reject) => {
1124
1171
  const reader = new FileReader();
1125
- reader.onload = () => resolve3(String(reader.result));
1172
+ reader.onload = () => resolve4(String(reader.result));
1126
1173
  reader.onerror = () => reject(reader.error);
1127
1174
  reader.readAsDataURL(blob);
1128
1175
  });
@@ -1213,8 +1260,8 @@ ${attachments.join("\n")}`);
1213
1260
  async richAnswerText(page) {
1214
1261
  const text = await this.lastAssistantText(page);
1215
1262
  if (text !== "") return text;
1216
- const images = await page.evaluate(() => [...document.querySelectorAll("main img")].filter((image) => image.naturalWidth >= 300).map((image) => image.src));
1217
- return images.length > 0 ? `image:${images.join("|")}` : "";
1263
+ const images = await page.evaluate(() => [...document.querySelectorAll("main img")].filter((image) => image.naturalWidth >= 300).map((image) => `${image.naturalWidth}x${image.naturalHeight}`));
1264
+ return images.length > 0 ? `image:${images.length}:${images.join(",")}` : "";
1218
1265
  }
1219
1266
  async lastAssistantText(page) {
1220
1267
  const messages = page.locator(SELECTORS.assistantMessage);
@@ -1224,6 +1271,41 @@ ${attachments.join("\n")}`);
1224
1271
  }
1225
1272
  };
1226
1273
 
1274
+ // ../transport/src/shared.ts
1275
+ import { resolve as resolve2 } from "node:path";
1276
+ var REGISTRY = /* @__PURE__ */ Symbol.for("dsh-pro.transport.shared.v1");
1277
+ function registry() {
1278
+ const holder = globalThis;
1279
+ const existing = holder[REGISTRY];
1280
+ if (existing instanceof Map) return existing;
1281
+ const created = /* @__PURE__ */ new Map();
1282
+ holder[REGISTRY] = created;
1283
+ return created;
1284
+ }
1285
+ function acquireSharedTransport(options, create = () => new ChatGptWebTransport(options)) {
1286
+ const key = resolve2(options.profileDir);
1287
+ const slots = registry();
1288
+ let slot = slots.get(key);
1289
+ if (slot === void 0) {
1290
+ slot = { transport: create(), refs: 0 };
1291
+ slots.set(key, slot);
1292
+ }
1293
+ slot.refs += 1;
1294
+ const target = slot;
1295
+ let released = false;
1296
+ return {
1297
+ transport: target.transport,
1298
+ release: () => {
1299
+ if (released) return;
1300
+ released = true;
1301
+ target.refs -= 1;
1302
+ if (target.refs > 0) return;
1303
+ if (slots.get(key) === target) slots.delete(key);
1304
+ void target.transport.close();
1305
+ }
1306
+ };
1307
+ }
1308
+
1227
1309
  // src/plugin.ts
1228
1310
  var Config = z.object({
1229
1311
  rounds: z.number().step(1).min(1).max(10).default(3).description("\u6BCF\u4E3B\u9898\u9ED8\u8BA4\u8F6E\u6B21\u4E0A\u9650\uFF081\u201310\uFF09\u3002"),
@@ -1331,6 +1413,7 @@ var ProResearchService = class {
1331
1413
  stores = /* @__PURE__ */ new Map();
1332
1414
  topicSessions = /* @__PURE__ */ new Map();
1333
1415
  transport;
1416
+ shared;
1334
1417
  rootFor(cwd) {
1335
1418
  return cwd.trim() === "" ? process.cwd() : cwd;
1336
1419
  }
@@ -1354,10 +1437,11 @@ var ProResearchService = class {
1354
1437
  }
1355
1438
  transportFor() {
1356
1439
  if (this.transport !== void 0) return this.transport;
1357
- this.transport = new ChatGptWebTransport({
1440
+ this.shared = acquireSharedTransport({
1358
1441
  profileDir: this.config.browser.profileDir,
1359
1442
  headless: this.config.browser.headless
1360
1443
  });
1444
+ this.transport = this.shared.transport;
1361
1445
  return this.transport;
1362
1446
  }
1363
1447
  judge() {
@@ -1532,8 +1616,8 @@ ${input.answer.slice(0, 3e4)}`
1532
1616
  if (entry === void 0) throw new Error(`unknown topic: ${id}`);
1533
1617
  const store = this.storeAt(entry.root, entry.mode);
1534
1618
  const dir = await store.dirOf(id);
1535
- const artifactsRoot = resolve2(dir, "artifacts");
1536
- const target = resolve2(dir, relativePath);
1619
+ const artifactsRoot = resolve3(dir, "artifacts");
1620
+ const target = resolve3(dir, relativePath);
1537
1621
  if (target !== artifactsRoot && !target.startsWith(`${artifactsRoot}${sep}`)) {
1538
1622
  throw new Error("artifact path escapes the topic");
1539
1623
  }
@@ -1611,7 +1695,7 @@ ${input.answer.slice(0, 3e4)}`
1611
1695
  withSources(cwd, refs) {
1612
1696
  return refs.map((ref) => ({
1613
1697
  ...ref,
1614
- source: isAbsolute(ref.path) ? ref.path : resolve2(cwd, ref.path)
1698
+ source: isAbsolute(ref.path) ? ref.path : resolve3(cwd, ref.path)
1615
1699
  }));
1616
1700
  }
1617
1701
  withDesignCount(brief, designs) {
@@ -1686,7 +1770,9 @@ ${input.answer.slice(0, 3e4)}`
1686
1770
  };
1687
1771
  }
1688
1772
  dispose() {
1689
- void this.transport?.close();
1773
+ this.shared?.release();
1774
+ this.shared = void 0;
1775
+ this.transport = void 0;
1690
1776
  }
1691
1777
  };
1692
1778
  function registerSkill(ctx, spec2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatgpt-pro-research",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "ChatGPT Pro Research: unattended multi-round research through a real ChatGPT Pro web session, with local records and a DSH sidebar.",
5
5
  "private": false,
6
6
  "type": "module",