dsh-codex-connect 0.1.0-alpha.4.19 → 0.1.0-alpha.4.21

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/lib/client.js CHANGED
@@ -11,8 +11,35 @@ window.__ModuleLoader__.load({
11
11
  /** Node-free settings contract shared by the Host plugin and browser card. */
12
12
  /** Stable Harness settings namespace owned by this plugin. */
13
13
  const OPENAI_CODEX_SETTINGS_NAMESPACE = "llm-openai-codex";
14
+ /** Suggested local HTTP proxy shown by the settings UI; it is never enabled by default. */
15
+ const DEFAULT_OPENAI_CODEX_PROXY_URL = "http://127.0.0.1:7890";
16
+ /**
17
+ * Normalize the credential-free HTTP proxy URL accepted by Codex Connect.
18
+ * Paths, query strings, fragments, and embedded credentials are rejected so
19
+ * the value remains an origin rather than an opaque request target.
20
+ */
21
+ function normalizeOpenAICodexProxyUrl(value) {
22
+ if (typeof value !== "string" || value.trim().length === 0) return void 0;
23
+ try {
24
+ const parsed = new URL(value.trim());
25
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
26
+ if (parsed.username !== "" || parsed.password !== "") return void 0;
27
+ if (parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") return void 0;
28
+ if (parsed.hostname.length === 0) return void 0;
29
+ if (parsed.port !== "" && (!/^\d+$/u.test(parsed.port) || Number(parsed.port) < 1 || Number(parsed.port) > 65535)) return void 0;
30
+ return parsed.origin;
31
+ } catch {
32
+ return;
33
+ }
34
+ }
35
+ /** Whether a value is a supported, canonical HTTP(S) proxy origin. */
36
+ function isValidOpenAICodexProxyUrl(value) {
37
+ return normalizeOpenAICodexProxyUrl(value) !== void 0;
38
+ }
14
39
  Object.freeze({
15
40
  models: void 0,
41
+ enableProxy: false,
42
+ proxyUrl: DEFAULT_OPENAI_CODEX_PROXY_URL,
16
43
  enableSearch: false,
17
44
  enableImageTool: false,
18
45
  enableImageGeneration: false,
@@ -28,6 +55,8 @@ window.__ModuleLoader__.load({
28
55
  function decodeOpenAICodexSettings(value) {
29
56
  if (!isRecord$2(value)) return void 0;
30
57
  const models = value["models"];
58
+ const enableProxy = value["enableProxy"];
59
+ const proxyUrl = value["proxyUrl"];
31
60
  const enableSearch = value["enableSearch"];
32
61
  const enableImageTool = value["enableImageTool"];
33
62
  const enableImageGeneration = value["enableImageGeneration"];
@@ -36,6 +65,8 @@ window.__ModuleLoader__.load({
36
65
  const searchContextSize = value["searchContextSize"];
37
66
  const searchMaxOutputTokens = value["searchMaxOutputTokens"];
38
67
  if (models !== void 0 && (!Array.isArray(models) || models.some((model) => typeof model !== "string"))) return void 0;
68
+ if (enableProxy !== void 0 && typeof enableProxy !== "boolean") return void 0;
69
+ if (proxyUrl !== void 0 && (typeof proxyUrl !== "string" || !isValidOpenAICodexProxyUrl(proxyUrl))) return void 0;
39
70
  if (typeof enableSearch !== "boolean" || typeof enableImageTool !== "boolean") return void 0;
40
71
  if (enableImageGeneration !== void 0 && typeof enableImageGeneration !== "boolean") return void 0;
41
72
  if (typeof searchModel !== "string" || searchModel.trim().length === 0) return void 0;
@@ -44,6 +75,8 @@ window.__ModuleLoader__.load({
44
75
  if (typeof searchMaxOutputTokens !== "number" || !Number.isInteger(searchMaxOutputTokens) || searchMaxOutputTokens < 1) return void 0;
45
76
  return {
46
77
  models: models === void 0 ? void 0 : [...new Set(models)],
78
+ enableProxy: enableProxy ?? false,
79
+ proxyUrl: proxyUrl === void 0 ? DEFAULT_OPENAI_CODEX_PROXY_URL : normalizeOpenAICodexProxyUrl(proxyUrl),
47
80
  enableSearch,
48
81
  enableImageTool,
49
82
  enableImageGeneration: enableImageGeneration ?? false,
@@ -87,6 +120,13 @@ window.__ModuleLoader__.load({
87
120
  return catalog;
88
121
  }
89
122
  //#endregion
123
+ //#region src/proxy-paths.ts
124
+ /** Node-free route constants shared by the Host and browser plugin halves. */
125
+ /** Detect bounded local/environment proxy candidates without changing settings. */
126
+ const OPENAI_CODEX_PROXY_DETECT_PATH = "/plugins/dsh-openai-codex/proxy/detect";
127
+ /** Test one manually entered proxy origin without changing settings. */
128
+ const OPENAI_CODEX_PROXY_TEST_PATH = "/plugins/dsh-openai-codex/proxy/test";
129
+ //#endregion
90
130
  //#region src/client/OpenAICodexConfiguration.tsx
91
131
  /** Staged optional-capability editor inside the OpenAI Codex plugin card. */
92
132
  const sectionStyle$1 = {
@@ -223,6 +263,8 @@ window.__ModuleLoader__.load({
223
263
  };
224
264
  const CONFIG_FIELDS = [
225
265
  "models",
266
+ "enableProxy",
267
+ "proxyUrl",
226
268
  "enableSearch",
227
269
  "enableImageTool",
228
270
  "enableImageGeneration",
@@ -250,6 +292,10 @@ window.__ModuleLoader__.load({
250
292
  const [feedback, setFeedback] = (0, react.useState)("idle");
251
293
  const [modelCatalog, setModelCatalog] = (0, react.useState)();
252
294
  const [modelCatalogError, setModelCatalogError] = (0, react.useState)(false);
295
+ const [proxyDetection, setProxyDetection] = (0, react.useState)({ status: "idle" });
296
+ const [manualProxy, setManualProxy] = (0, react.useState)(false);
297
+ const [manualProbe, setManualProbe] = (0, react.useState)();
298
+ const [manualProbeBusy, setManualProbeBusy] = (0, react.useState)(false);
253
299
  (0, react.useEffect)(() => {
254
300
  if (scope === void 0) return;
255
301
  const controller = new AbortController();
@@ -291,10 +337,73 @@ window.__ModuleLoader__.load({
291
337
  setDraft(scope?.getSnapshot().value);
292
338
  setDirty(false);
293
339
  setFeedback("idle");
340
+ setProxyDetection({ status: "idle" });
341
+ setManualProbe(void 0);
342
+ };
343
+ const detectProxy = async () => {
344
+ setProxyDetection({ status: "detecting" });
345
+ try {
346
+ const response = await fetch(OPENAI_CODEX_PROXY_DETECT_PATH, {
347
+ method: "POST",
348
+ credentials: "same-origin",
349
+ headers: { accept: "application/json" }
350
+ });
351
+ if (!response.ok) throw new Error(`proxy detection failed: ${String(response.status)}`);
352
+ const value = await response.json();
353
+ if (!Array.isArray(value.candidates) || !Array.isArray(value.results)) throw new Error("proxy detection response was invalid");
354
+ setProxyDetection(value.candidates.length > 0 ? {
355
+ status: "candidate",
356
+ candidates: value.candidates
357
+ } : {
358
+ status: "failed",
359
+ results: value.results
360
+ });
361
+ } catch {
362
+ setProxyDetection({
363
+ status: "failed",
364
+ results: []
365
+ });
366
+ }
367
+ };
368
+ const useProxy = (proxyUrl) => {
369
+ if (draft === void 0 || !isValidOpenAICodexProxyUrl(proxyUrl)) return;
370
+ setDraft({
371
+ ...draft,
372
+ proxyUrl,
373
+ enableProxy: true
374
+ });
375
+ setDirty(true);
376
+ setFeedback("idle");
377
+ setManualProxy(true);
378
+ setManualProbe(void 0);
379
+ setProxyDetection({ status: "idle" });
380
+ };
381
+ const testManualProxy = async () => {
382
+ if (draft === void 0 || !isValidOpenAICodexProxyUrl(draft.proxyUrl)) return;
383
+ setManualProbeBusy(true);
384
+ try {
385
+ const path = `${OPENAI_CODEX_PROXY_TEST_PATH}?proxyUrl=${encodeURIComponent(draft.proxyUrl)}`;
386
+ const response = await fetch(path, {
387
+ method: "POST",
388
+ credentials: "same-origin",
389
+ headers: { accept: "application/json" }
390
+ });
391
+ if (!response.ok) throw new Error(`proxy test failed: ${String(response.status)}`);
392
+ setManualProbe(await response.json());
393
+ } catch {
394
+ setManualProbe({
395
+ proxyUrl: draft.proxyUrl,
396
+ reachable: false,
397
+ classification: "connect-failure"
398
+ });
399
+ } finally {
400
+ setManualProbeBusy(false);
401
+ }
294
402
  };
295
403
  const validModel = draft !== void 0 && draft.searchModel.trim().length > 0;
296
404
  const validTokens = draft !== void 0 && Number.isInteger(draft.searchMaxOutputTokens) && draft.searchMaxOutputTokens > 0;
297
- const valid = validModel && validTokens;
405
+ const validProxy = draft !== void 0 && isValidOpenAICodexProxyUrl(draft.proxyUrl);
406
+ const valid = validModel && validTokens && validProxy;
298
407
  const save = async () => {
299
408
  if (scope === void 0 || draft === void 0 || !snapshot.writable || !valid) return;
300
409
  const desired = {
@@ -398,6 +507,135 @@ window.__ModuleLoader__.load({
398
507
  }, model.id);
399
508
  })
400
509
  }),
510
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
511
+ style: { paddingTop: 4 },
512
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
513
+ style: headingStyle,
514
+ children: t("networkHeading")
515
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
516
+ style: {
517
+ ...bodyStyle$2,
518
+ marginTop: 4
519
+ },
520
+ children: t("networkIntro")
521
+ })]
522
+ }),
523
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
524
+ style: bodyStyle$2,
525
+ role: "status",
526
+ children: draft.enableProxy ? t("customProxyActive", { proxyUrl: draft.proxyUrl }) : t("directConnection")
527
+ }),
528
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
529
+ style: buttonsStyle,
530
+ children: [
531
+ !draft.enableProxy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
532
+ type: "button",
533
+ style: buttonStyle$2,
534
+ onClick: () => {
535
+ detectProxy();
536
+ },
537
+ disabled: proxyDetection.status === "detecting",
538
+ children: proxyDetection.status === "detecting" ? t("detectingProxy") : t("detectProxy")
539
+ }) : null,
540
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
541
+ type: "button",
542
+ style: buttonStyle$2,
543
+ onClick: () => {
544
+ setManualProxy(true);
545
+ },
546
+ children: t("configureProxyManually")
547
+ }),
548
+ draft.enableProxy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
549
+ type: "button",
550
+ style: buttonStyle$2,
551
+ onClick: () => {
552
+ update("enableProxy", false);
553
+ },
554
+ children: t("disableProxy")
555
+ }) : null
556
+ ]
557
+ }),
558
+ proxyDetection.status === "candidate" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
559
+ style: fieldsetStyle,
560
+ role: "status",
561
+ children: [
562
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
563
+ style: bodyStyle$2,
564
+ children: t("proxyCandidatesFound")
565
+ }),
566
+ proxyDetection.candidates.map((candidate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
567
+ style: actionsStyle,
568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
569
+ style: modelIdStyle,
570
+ children: candidate.proxyUrl
571
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
572
+ type: "button",
573
+ style: primaryButtonStyle$2,
574
+ onClick: () => {
575
+ useProxy(candidate.proxyUrl);
576
+ },
577
+ children: t("useThisProxy")
578
+ })]
579
+ }, candidate.proxyUrl)),
580
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
581
+ type: "button",
582
+ style: buttonStyle$2,
583
+ onClick: () => {
584
+ setProxyDetection({ status: "idle" });
585
+ },
586
+ children: t("keepDirectConnection")
587
+ })
588
+ ]
589
+ }) : null,
590
+ proxyDetection.status === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
591
+ style: errorStyle$2,
592
+ role: "alert",
593
+ children: t("proxyDetectionFailed")
594
+ }) : null,
595
+ manualProxy ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
596
+ style: fieldsetStyle,
597
+ children: [
598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
599
+ style: formFieldStyle,
600
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
601
+ style: labelStyle,
602
+ children: t("proxyAddress")
603
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
604
+ style: controlStyle,
605
+ value: draft.proxyUrl,
606
+ "aria-invalid": !isValidOpenAICodexProxyUrl(draft.proxyUrl),
607
+ onChange: (event) => {
608
+ update("proxyUrl", event.currentTarget.value);
609
+ }
610
+ })]
611
+ }),
612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
613
+ style: buttonsStyle,
614
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
615
+ type: "button",
616
+ style: buttonStyle$2,
617
+ disabled: !isValidOpenAICodexProxyUrl(draft.proxyUrl) || manualProbeBusy,
618
+ onClick: () => {
619
+ testManualProxy();
620
+ },
621
+ children: manualProbeBusy ? t("testingProxy") : t("testProxy")
622
+ }), !draft.enableProxy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
623
+ type: "button",
624
+ style: primaryButtonStyle$2,
625
+ disabled: !isValidOpenAICodexProxyUrl(draft.proxyUrl),
626
+ onClick: () => {
627
+ useProxy(draft.proxyUrl);
628
+ },
629
+ children: t("useThisProxy")
630
+ }) : null]
631
+ }),
632
+ manualProbe === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
633
+ style: manualProbe.reachable ? successStyle : errorStyle$2,
634
+ role: "status",
635
+ children: manualProbe.reachable ? t("proxyTestSucceeded", { status: manualProbe.status ?? manualProbe.classification }) : t("proxyTestFailed", { reason: manualProbe.classification })
636
+ })
637
+ ]
638
+ }) : null,
401
639
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
402
640
  style: { paddingTop: 4 },
403
641
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
@@ -575,6 +813,11 @@ window.__ModuleLoader__.load({
575
813
  role: "alert",
576
814
  children: t("invalidSearchTokens")
577
815
  }) : null,
816
+ !validProxy && draft !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
817
+ style: errorStyle$2,
818
+ role: "alert",
819
+ children: t("invalidProxyUrl")
820
+ }) : null,
578
821
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
579
822
  style: bodyStyle$2,
580
823
  children: t("routingNote")
@@ -621,7 +864,8 @@ window.__ModuleLoader__.load({
621
864
  "search-stability",
622
865
  "image-generation",
623
866
  "oauth-history",
624
- "model-visibility"
867
+ "model-visibility",
868
+ "proxy-connection"
625
869
  ];
626
870
  function isHighlightKind(value) {
627
871
  return typeof value === "string" && HIGHLIGHT_KINDS.includes(value);
@@ -1125,7 +1369,8 @@ window.__ModuleLoader__.load({
1125
1369
  "search-stability": "updateHighlightSearchStability",
1126
1370
  "image-generation": "updateHighlightImageGeneration",
1127
1371
  "oauth-history": "updateHighlightOauthHistory",
1128
- "model-visibility": "updateHighlightModelVisibility"
1372
+ "model-visibility": "updateHighlightModelVisibility",
1373
+ "proxy-connection": "updateHighlightProxyConnection"
1129
1374
  };
1130
1375
  const compatibilityTitleKeys = {
1131
1376
  compatible: "compatibilityCompatibleTitle",
@@ -2567,6 +2812,24 @@ window.__ModuleLoader__.load({
2567
2812
  modelCatalogFailed: "Available models could not be loaded.",
2568
2813
  capabilitiesHeading: "Optional capabilities",
2569
2814
  capabilitiesIntro: "Choose which extra Codex capabilities this dsh profile may register.",
2815
+ networkHeading: "Network connection",
2816
+ networkIntro: "Direct connection is the default. Proxy detection only tests a bounded set of local and environment candidates; nothing is saved until you choose Use this proxy and save.",
2817
+ directConnection: "Direct connection",
2818
+ customProxyActive: "Custom proxy selected: {proxyUrl}",
2819
+ detectProxy: "Detect proxy",
2820
+ detectingProxy: "Detecting…",
2821
+ configureProxyManually: "Configure manually",
2822
+ disableProxy: "Disable proxy",
2823
+ proxyCandidatesFound: "Working proxy candidates found. Choose one to stage it for activation.",
2824
+ keepDirectConnection: "Keep direct connection",
2825
+ useThisProxy: "Use this proxy",
2826
+ proxyDetectionFailed: "No working proxy candidate was found. Check DNS, connection refused, timeout, TLS, or proxy authentication errors, then try again.",
2827
+ proxyAddress: "Custom proxy address",
2828
+ testProxy: "Test proxy",
2829
+ testingProxy: "Testing…",
2830
+ proxyTestSucceeded: "Proxy reached the Codex endpoint (HTTP {status}).",
2831
+ proxyTestFailed: "Proxy test failed: {reason}.",
2832
+ invalidProxyUrl: "Enter an HTTP(S) proxy origin without credentials or a path.",
2570
2833
  enableSearch: "Enable Codex search provider",
2571
2834
  enableSearchHelp: "Makes OpenAI Codex available as a search provider. It does not select the global search route.",
2572
2835
  searchModel: "Search model",
@@ -2606,12 +2869,18 @@ window.__ModuleLoader__.load({
2606
2869
  unknownResult: "The image result could not be displayed safely.",
2607
2870
  imageDetails: "Image details",
2608
2871
  imageDetail: "{name}: {format} · {width} × {height} · {size}",
2872
+ originalImageDetail: "Original {name}: {format} · {width} × {height} · {size}",
2873
+ previewImageDetail: "Conversation preview: {format} · {width} × {height} · {size}",
2609
2874
  download: "Download",
2610
2875
  downloadNamed: "Download {name}",
2876
+ downloadOriginal: "Download original",
2877
+ downloadOriginalNamed: "Download original {name}",
2878
+ downloadPreview: "Download preview",
2879
+ downloadPreviewNamed: "Download preview {name}",
2611
2880
  downloading: "Downloading…",
2612
2881
  downloadFailed: "Download failed. Retry",
2613
2882
  image: "Generated image",
2614
- open: "Open original image",
2883
+ open: "Open image preview",
2615
2884
  openNamed: "Open {name}",
2616
2885
  loading: "Loading image",
2617
2886
  loadFailed: "Image could not be loaded. Retry",
@@ -2675,6 +2944,7 @@ window.__ModuleLoader__.load({
2675
2944
  updateHighlightImageGeneration: "Added optional GPT Image generation in the Codex Connect settings and a preview card in the conversation.",
2676
2945
  updateHighlightOauthHistory: "Added safer session-history migration and a sign-in fallback when the account needs to reconnect.",
2677
2946
  updateHighlightModelVisibility: "You can now choose which Codex models appear in model selectors.",
2947
+ updateHighlightProxyConnection: "Added opt-in Codex proxy detection with explicit confirmation and a visible direct-connection fallback.",
2678
2948
  viewTechnicalDetails: "View technical details",
2679
2949
  hideTechnicalDetails: "Hide technical details",
2680
2950
  technicalDetailsHeading: "Technical changes",
@@ -2751,6 +3021,24 @@ window.__ModuleLoader__.load({
2751
3021
  modelCatalogFailed: "无法加载可用模型。",
2752
3022
  capabilitiesHeading: "可选能力",
2753
3023
  capabilitiesIntro: "选择允许此 dsh profile 注册哪些额外的 Codex 能力。",
3024
+ networkHeading: "网络连接",
3025
+ networkIntro: "默认使用直连。代理检测只测试有限的本机和环境变量候选地址;只有你选择“使用此代理”并保存后才会写入设置。",
3026
+ directConnection: "直连",
3027
+ customProxyActive: "已选择自定义代理:{proxyUrl}",
3028
+ detectProxy: "检测代理",
3029
+ detectingProxy: "正在检测…",
3030
+ configureProxyManually: "手动配置",
3031
+ disableProxy: "停用代理",
3032
+ proxyCandidatesFound: "找到了可用的代理候选地址。请选择一个,将其暂存以便启用。",
3033
+ keepDirectConnection: "保持直连",
3034
+ useThisProxy: "使用此代理",
3035
+ proxyDetectionFailed: "没有找到可用代理。请检查 DNS、连接被拒绝、超时、TLS 或代理认证错误后重试。",
3036
+ proxyAddress: "自定义代理地址",
3037
+ testProxy: "测试代理",
3038
+ testingProxy: "正在测试…",
3039
+ proxyTestSucceeded: "代理已到达 Codex 端点(HTTP {status})。",
3040
+ proxyTestFailed: "代理测试失败:{reason}。",
3041
+ invalidProxyUrl: "请输入不含凭据或路径的 HTTP(S) 代理 origin。",
2754
3042
  enableSearch: "启用 Codex 搜索提供方",
2755
3043
  enableSearchHelp: "让 OpenAI Codex 可被选作搜索提供方,但不会自动改动全局搜索路由。",
2756
3044
  searchModel: "搜索模型",
@@ -2790,12 +3078,18 @@ window.__ModuleLoader__.load({
2790
3078
  unknownResult: "无法安全显示这次图片结果。",
2791
3079
  imageDetails: "图片详情",
2792
3080
  imageDetail: "{name}:{format} · {width} × {height} · {size}",
3081
+ originalImageDetail: "原图 {name}:{format} · {width} × {height} · {size}",
3082
+ previewImageDetail: "对话预览:{format} · {width} × {height} · {size}",
2793
3083
  download: "下载",
2794
3084
  downloadNamed: "下载 {name}",
3085
+ downloadOriginal: "下载原图",
3086
+ downloadOriginalNamed: "下载原图 {name}",
3087
+ downloadPreview: "下载预览图",
3088
+ downloadPreviewNamed: "下载预览图 {name}",
2795
3089
  downloading: "正在下载…",
2796
3090
  downloadFailed: "下载失败,重试",
2797
3091
  image: "生成的图片",
2798
- open: "打开原图",
3092
+ open: "打开图片预览",
2799
3093
  openNamed: "打开 {name}",
2800
3094
  loading: "正在加载图片",
2801
3095
  loadFailed: "图片加载失败,重试",
@@ -2859,6 +3153,7 @@ window.__ModuleLoader__.load({
2859
3153
  updateHighlightImageGeneration: "在 Codex Connect 设置中加入可选的 GPT Image 生图,并在对话里展示生成结果卡片。",
2860
3154
  updateHighlightOauthHistory: "增加更安全的会话历史迁移,并在账号需要重新登录时提供备用登录路径。",
2861
3155
  updateHighlightModelVisibility: "现在可以选择哪些 Codex 模型显示在模型选择菜单中。",
3156
+ updateHighlightProxyConnection: "增加可选的 Codex 代理检测,启用前需要明确确认,并始终保留直连回退入口。",
2862
3157
  viewTechnicalDetails: "查看完整技术说明",
2863
3158
  hideTechnicalDetails: "收起技术说明",
2864
3159
  technicalDetailsHeading: "技术变更",
@@ -2882,6 +3177,38 @@ window.__ModuleLoader__.load({
2882
3177
  dismissUpdate: "稍后提醒"
2883
3178
  };
2884
3179
  //#endregion
3180
+ //#region src/image-assets-contract.ts
3181
+ /** Same-origin endpoint serving one session-owned original generated image. */
3182
+ const OPENAI_CODEX_ORIGINAL_IMAGE_PATH = "/plugins/dsh-codex-connect/images/original";
3183
+ /** Opaque identifier format for one plugin-owned original image. */
3184
+ const OPENAI_CODEX_IMAGE_ASSET_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
3185
+ function positiveSafeInteger$1(value) {
3186
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
3187
+ }
3188
+ /** Decode session-log metadata without trusting an asset id, filename, or media type. */
3189
+ function decodeOpenAICodexOriginalImageRef(value) {
3190
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
3191
+ const candidate = value;
3192
+ if (typeof candidate.assetId !== "string" || !OPENAI_CODEX_IMAGE_ASSET_ID_PATTERN.test(candidate.assetId) || candidate.mediaType !== "image/png" && candidate.mediaType !== "image/jpeg" && candidate.mediaType !== "image/webp" || !positiveSafeInteger$1(candidate.width) || !positiveSafeInteger$1(candidate.height) || !positiveSafeInteger$1(candidate.bytes) || candidate.bytes > 50331648 || typeof candidate.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(candidate.name) || typeof candidate.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(candidate.sha256)) return void 0;
3193
+ return {
3194
+ assetId: candidate.assetId,
3195
+ mediaType: candidate.mediaType,
3196
+ width: candidate.width,
3197
+ height: candidate.height,
3198
+ bytes: candidate.bytes,
3199
+ name: candidate.name,
3200
+ sha256: candidate.sha256
3201
+ };
3202
+ }
3203
+ /** Build a same-origin URL without allowing either opaque id to become a path segment. */
3204
+ function openAICodexOriginalImageUrl(sessionId, assetId) {
3205
+ const query = new URLSearchParams({
3206
+ sessionId,
3207
+ assetId
3208
+ });
3209
+ return `${OPENAI_CODEX_ORIGINAL_IMAGE_PATH}?${query.toString()}`;
3210
+ }
3211
+ //#endregion
2885
3212
  //#region src/image-presentation.ts
2886
3213
  /** Stable metadata marker for generated image result views. */
2887
3214
  const IMAGE_PRESENTATION_KIND = "codex-connect-images";
@@ -2909,10 +3236,32 @@ window.__ModuleLoader__.load({
2909
3236
  if (typeof value !== "object" || value === null) return void 0;
2910
3237
  const candidate = value;
2911
3238
  if (candidate.kind !== "codex-connect-images" || typeof candidate.prompt !== "string" || candidate.prompt.length < 1 || candidate.prompt.length > 32e3 || !Array.isArray(candidate.images) || candidate.images.length < 1 || candidate.images.length > 4) return void 0;
2912
- const images = candidate.images.map(imageRef);
2913
- if (images.some((image) => image === void 0)) return void 0;
3239
+ if (candidate.schemaVersion === void 0) {
3240
+ const previews = candidate.images.map(imageRef);
3241
+ if (previews.some((image) => image === void 0)) return void 0;
3242
+ return {
3243
+ kind: IMAGE_PRESENTATION_KIND,
3244
+ schemaVersion: 1,
3245
+ prompt: candidate.prompt,
3246
+ images: previews.map((preview) => ({ preview }))
3247
+ };
3248
+ }
3249
+ if (candidate.schemaVersion !== 1) return void 0;
3250
+ const images = [];
3251
+ for (const value of candidate.images) {
3252
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
3253
+ const entry = value;
3254
+ const preview = imageRef(entry.preview);
3255
+ const original = decodeOpenAICodexOriginalImageRef(entry.original);
3256
+ if (preview === void 0 || original === void 0) return void 0;
3257
+ images.push({
3258
+ preview,
3259
+ original
3260
+ });
3261
+ }
2914
3262
  return {
2915
3263
  kind: IMAGE_PRESENTATION_KIND,
3264
+ schemaVersion: 1,
2916
3265
  prompt: candidate.prompt,
2917
3266
  images
2918
3267
  };
@@ -3508,7 +3857,19 @@ window.__ModuleLoader__.load({
3508
3857
  children: t("actionFailed")
3509
3858
  }) : null;
3510
3859
  }
3511
- function DownloadButton({ image, label, load, t }) {
3860
+ function triggerDownload(url, name) {
3861
+ const anchor = document.createElement("a");
3862
+ anchor.href = url;
3863
+ anchor.download = name;
3864
+ anchor.rel = "noopener";
3865
+ document.body.append(anchor);
3866
+ try {
3867
+ anchor.click();
3868
+ } finally {
3869
+ anchor.remove();
3870
+ }
3871
+ }
3872
+ function DownloadButton({ label, onDownload, t }) {
3512
3873
  const [state, setState] = (0, react.useState)("idle");
3513
3874
  const alive = (0, react.useRef)(true);
3514
3875
  (0, react.useEffect)(() => () => {
@@ -3518,17 +3879,7 @@ window.__ModuleLoader__.load({
3518
3879
  if (state === "pending") return;
3519
3880
  setState("pending");
3520
3881
  try {
3521
- const url = await load(image);
3522
- const anchor = document.createElement("a");
3523
- anchor.href = url;
3524
- anchor.download = image.name ?? t("image");
3525
- anchor.rel = "noopener";
3526
- document.body.append(anchor);
3527
- try {
3528
- anchor.click();
3529
- } finally {
3530
- anchor.remove();
3531
- }
3882
+ await onDownload();
3532
3883
  if (alive.current) setState("idle");
3533
3884
  } catch {
3534
3885
  if (alive.current) setState("failed");
@@ -3552,6 +3903,22 @@ window.__ModuleLoader__.load({
3552
3903
  children: status
3553
3904
  })] });
3554
3905
  }
3906
+ async function downloadOriginal(sessionId, original) {
3907
+ const response = await fetch(openAICodexOriginalImageUrl(sessionId, original.assetId), {
3908
+ method: "GET",
3909
+ headers: { accept: original.mediaType },
3910
+ credentials: "same-origin"
3911
+ });
3912
+ if (!response.ok) throw new Error("Original image download failed");
3913
+ const blob = await response.blob();
3914
+ if (blob.size !== original.bytes) throw new Error("Original image download was incomplete");
3915
+ const url = URL.createObjectURL(blob);
3916
+ try {
3917
+ triggerDownload(url, original.name);
3918
+ } finally {
3919
+ URL.revokeObjectURL(url);
3920
+ }
3921
+ }
3555
3922
  function PromptPanel({ prompt, t }) {
3556
3923
  const [copyState, setCopyState] = (0, react.useState)("idle");
3557
3924
  const [hovered, setHovered] = (0, react.useState)(false);
@@ -3868,7 +4235,7 @@ window.__ModuleLoader__.load({
3868
4235
  style: header,
3869
4236
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("completed") })
3870
4237
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodexImageGallery, {
3871
- images: decoded.images.map((attachment) => ({ attachment })),
4238
+ images: decoded.images.map((image) => ({ attachment: image.preview })),
3872
4239
  load,
3873
4240
  align: "start",
3874
4241
  labels: galleryLabels
@@ -3913,12 +4280,21 @@ window.__ModuleLoader__.load({
3913
4280
  flexWrap: "wrap",
3914
4281
  gap: 8
3915
4282
  },
3916
- children: decoded.images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadButton, {
3917
- image,
3918
- load,
3919
- t,
3920
- label: decoded.images.length === 1 ? t("download") : t("downloadNamed", { name: image.name ?? String(index + 1) })
3921
- }, image.attachmentId))
4283
+ children: decoded.images.flatMap((image, index) => {
4284
+ const suffix = image.preview.name ?? String(index + 1);
4285
+ const exactOriginal = image.original;
4286
+ return [...exactOriginal === void 0 ? [] : [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadButton, {
4287
+ onDownload: () => downloadOriginal(sessionId, exactOriginal),
4288
+ t,
4289
+ label: decoded.images.length === 1 ? t("downloadOriginal") : t("downloadOriginalNamed", { name: exactOriginal.name })
4290
+ }, `${image.preview.attachmentId}:original`)], /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DownloadButton, {
4291
+ onDownload: async () => {
4292
+ triggerDownload(await load(image.preview), image.preview.name ?? t("image"));
4293
+ },
4294
+ t,
4295
+ label: image.original === void 0 ? decoded.images.length === 1 ? t("download") : t("downloadNamed", { name: suffix }) : decoded.images.length === 1 ? t("downloadPreview") : t("downloadPreviewNamed", { name: suffix })
4296
+ }, `${image.preview.attachmentId}:preview`)];
4297
+ })
3922
4298
  }),
3923
4299
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
3924
4300
  style: {
@@ -3934,20 +4310,37 @@ window.__ModuleLoader__.load({
3934
4310
  gap: 4,
3935
4311
  marginTop: 6
3936
4312
  },
3937
- children: decoded.images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageDetail", {
3938
- name: image.name ?? String(index + 1),
3939
- format: formatMediaType(image.mediaType),
3940
- width: image.width,
3941
- height: image.height,
3942
- size: formatBytes(image.bytes)
3943
- }) }, image.attachmentId))
4313
+ children: decoded.images.flatMap((image, index) => {
4314
+ const preview = image.preview;
4315
+ const original = image.original;
4316
+ const name = preview.name ?? String(index + 1);
4317
+ if (original === void 0) return [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageDetail", {
4318
+ name,
4319
+ format: formatMediaType(preview.mediaType),
4320
+ width: preview.width,
4321
+ height: preview.height,
4322
+ size: formatBytes(preview.bytes)
4323
+ }) }, preview.attachmentId)];
4324
+ return [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("originalImageDetail", {
4325
+ name: original.name,
4326
+ format: formatMediaType(original.mediaType),
4327
+ width: original.width,
4328
+ height: original.height,
4329
+ size: formatBytes(original.bytes)
4330
+ }) }, `${preview.attachmentId}:original`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("previewImageDetail", {
4331
+ format: formatMediaType(preview.mediaType),
4332
+ width: preview.width,
4333
+ height: preview.height,
4334
+ size: formatBytes(preview.bytes)
4335
+ }) }, `${preview.attachmentId}:preview`)];
4336
+ })
3944
4337
  })] })
3945
4338
  ] })
3946
4339
  });
3947
4340
  }
3948
4341
  //#endregion
3949
4342
  //#region src/version.ts
3950
- const CODEX_CONNECT_VERSION = "0.1.0-alpha.4.19";
4343
+ const CODEX_CONNECT_VERSION = "0.1.0-alpha.4.21";
3951
4344
  //#endregion
3952
4345
  //#region src/client/index.tsx
3953
4346
  /** Stable browser-plugin name. */