@walkhi/code-relax 0.1.0-beta.1 → 0.1.0-beta.2

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 (62) hide show
  1. package/README.md +5 -5
  2. package/dist/bin/self-relay-server.mjs +5 -2
  3. package/dist/shared/app-server-events.cjs +45 -19
  4. package/dist/shared/p2p-data-channel.cjs +50 -0
  5. package/dist/src/app-server-tasks.mjs +28 -25
  6. package/dist/src/lan-socket-server.mjs +8 -2
  7. package/dist/src/platform/windows/IsolatedProcess.cs +7 -3
  8. package/dist/src/platform/windows/background-process.mjs +7 -2
  9. package/dist/src/platform/windows/launch-worker.ps1 +13 -6
  10. package/dist/src/platform/windows/start-hidden-console.ps1 +11 -6
  11. package/dist/src/self-relay/admin-state.mjs +61 -0
  12. package/dist/src/self-relay/client.mjs +2 -0
  13. package/dist/src/self-relay/connector.mjs +23 -8
  14. package/dist/src/self-relay/demo.mjs +2 -2
  15. package/dist/src/self-relay/lifecycle.mjs +1 -1
  16. package/dist/src/self-relay/p2p-probe.mjs +123 -83
  17. package/dist/src/self-relay/server.mjs +137 -11
  18. package/dist/src/server.mjs +337 -255
  19. package/dist/src/shared-app-server.mjs +180 -24
  20. package/dist/src/shared-catalog.mjs +4 -14
  21. package/dist/src/thread-catalog.mjs +12 -7
  22. package/dist/web/activity-view.js +283 -0
  23. package/dist/web/capabilities.js +2 -2
  24. package/dist/web/chat-transport.js +15 -9
  25. package/dist/web/chat.css +139 -93
  26. package/dist/web/chat.js +1451 -2770
  27. package/dist/web/community-view.js +20 -0
  28. package/dist/web/community.css +77 -0
  29. package/dist/web/community.html +37 -0
  30. package/dist/web/composer-controller.js +101 -0
  31. package/dist/web/conversation-controller.js +99 -0
  32. package/dist/web/disclosure-state-controller.js +95 -0
  33. package/dist/web/draft-controller.js +103 -0
  34. package/dist/web/harmony-platform.js +3 -2
  35. package/dist/web/history-cache.js +112 -18
  36. package/dist/web/history-controller.js +167 -0
  37. package/dist/web/index.html +141 -38
  38. package/dist/web/link-action-controller.js +212 -0
  39. package/dist/web/message-send-controller.js +177 -0
  40. package/dist/web/message-view.js +98 -0
  41. package/dist/web/p2p-data-channel.js +50 -0
  42. package/dist/web/p2p-probe.js +67 -27
  43. package/dist/web/page-resume.js +28 -0
  44. package/dist/web/pending-message-store.js +108 -0
  45. package/dist/web/queue-controller.js +82 -0
  46. package/dist/web/resources.json +1 -1
  47. package/dist/web/self-relay-session.js +28 -18
  48. package/dist/web/station-connection-controller.js +75 -0
  49. package/dist/web/task-list-view.js +296 -0
  50. package/dist/web/thread-attention-controller.js +124 -0
  51. package/dist/web/thread-context-controller.js +30 -0
  52. package/dist/web/thread-list-controller.js +61 -0
  53. package/dist/web/thread-list-sync.js +86 -0
  54. package/dist/web/thread-title-controller.js +57 -0
  55. package/dist/web/timeline-formatters.js +249 -0
  56. package/dist/web/timeline-reducer.js +81 -0
  57. package/dist/web/timeline-renderer.js +161 -0
  58. package/dist/web/timeline-scroll-controller.js +50 -0
  59. package/dist/web/usage-controller.js +258 -0
  60. package/dist/web/vendor/lucide.LICENSE.txt +17 -0
  61. package/package.json +1 -1
  62. package/tools/postinstall.mjs +66 -2
@@ -7,55 +7,104 @@
7
7
  <style>
8
8
  #pageLoading { display:none; }
9
9
  body.page-loading > :not(#pageLoading) { display:none !important; }
10
- body.page-loading #pageLoading { display:flex; flex:1; align-items:center; justify-content:center; min-height:100vh; font:14px/1.5 system-ui; color:GrayText; }
11
- body.page-loading.silent-page-loading #pageLoading { visibility:hidden; }
10
+ body.page-loading #pageLoading { visibility:hidden; }
12
11
  </style>
13
12
  <link rel="stylesheet" href="chat.css">
13
+ <link rel="stylesheet" href="community.css">
14
14
  </head>
15
15
  <body class="page-loading">
16
- <div id="pageLoading" role="status">正在加载…</div>
17
- <section id="connectionEntry" role="region" aria-label="连接工作站">
18
- <div class="connection-entry-card">
19
- <div class="entry-brand">Code Relax</div>
20
- <h1>连接你的工作站</h1>
21
- <p id="entryMode">Code Relax</p>
22
- <ol id="entrySteps" class="entry-steps" aria-label="连接阶段" hidden>
23
- <li>账号</li><li>设备授权</li><li>连接工作站</li>
24
- </ol>
25
- <div id="entryProgress" class="entry-progress" data-state="loading">
26
- <span class="entry-indicator" aria-hidden="true"></span>
27
- <div><p id="entryStatus" role="status" aria-live="polite">正在连接工作站…</p>
28
- <p id="entryDetail">连接完成后会自动进入对话。</p></div>
29
- </div>
30
- <div class="connection-entry-actions">
31
- <button id="entryRetry" type="button" disabled>正在连接…</button>
32
- <button id="entrySettings" type="button" hidden>切换连接方式</button>
33
- </div>
34
- </div>
35
- </section>
36
- <header>
16
+ <div id="pageLoading" aria-hidden="true"></div>
17
+ <section id="connectionEntry" role="region" aria-label="连接工作站">
18
+ <div class="connection-entry-page">
19
+ <div class="connection-entry-header">
20
+ <button id="entryBack" class="connection-entry-back" type="button" aria-label="返回工作站详情">
21
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
22
+ </button>
23
+ <h1 id="entryTitle">连接工作站</h1>
24
+ <span class="connection-entry-header-spacer" aria-hidden="true"></span>
25
+ </div>
26
+ <div class="connection-entry-content">
27
+ <div class="connection-entry-card">
28
+ <p class="entry-connection-mode"><span>连接方式</span><strong id="entryMode">局域网</strong></p>
29
+ <div id="entryDiagram" class="entry-diagram" data-mode="lan" role="img" aria-label="移动端通过局域网双向连接工作站">
30
+ <div class="entry-diagram-node">
31
+ <svg viewBox="0 0 40 40" aria-hidden="true"><rect x="10" y="3" width="20" height="34" rx="4"/><path d="M17 7h6M18 33h4"/></svg>
32
+ <span>移动端</span>
33
+ </div>
34
+ <svg class="entry-diagram-link" viewBox="0 0 40 16" aria-hidden="true"><path d="M3 8h34M8 3 3 8l5 5M32 3l5 5-5 5"/></svg>
35
+ <div class="entry-diagram-node entry-diagram-middle">
36
+ <svg data-route="lan" viewBox="0 0 40 40" aria-hidden="true"><rect x="4" y="26" width="32" height="10" rx="3"/><path d="M10 26v-7m20 7v-7M12 12a12 12 0 0 1 16 0M16 17a6 6 0 0 1 8 0M10 31h2m5 0h2"/><circle cx="20" cy="22" r="1"/></svg>
37
+ <svg data-route="private" viewBox="0 0 40 40" aria-hidden="true"><rect x="8" y="4" width="24" height="32" rx="3"/><path d="M8 15h24M8 25h24M13 10h1m5 0h8M13 20h1m5 0h8M13 30h1m5 0h8"/></svg>
38
+ <svg data-route="official" viewBox="0 0 40 40" aria-hidden="true"><path d="M11 31h19a8 8 0 0 0 1-16 11 11 0 0 0-21-1 8.5 8.5 0 0 0 1 17Z"/><path d="M15 21h10m-7-3-3 3 3 3m4-6 3 3-3 3"/></svg>
39
+ <span id="entryDiagramMode">同一局域网</span>
40
+ </div>
41
+ <svg class="entry-diagram-link" viewBox="0 0 40 16" aria-hidden="true"><path d="M3 8h34M8 3 3 8l5 5M32 3l5 5-5 5"/></svg>
42
+ <div class="entry-diagram-node">
43
+ <svg viewBox="0 0 40 40" aria-hidden="true"><rect x="3" y="5" width="34" height="24" rx="3"/><path d="M3 24h34M16 29v6m8-6v6M12 35h16"/></svg>
44
+ <span>工作站</span>
45
+ </div>
46
+ </div>
47
+ <ol id="entrySteps" class="entry-steps" data-mode="lan" aria-label="连接阶段">
48
+ <li data-number="1" data-state="current" aria-current="step">检查局域网</li>
49
+ <li data-number="2" data-state="pending">确认设备授权</li>
50
+ <li data-number="3" data-state="pending">连接工作站</li>
51
+ <li data-number="4" data-state="pending">载入工作站任务</li>
52
+ </ol>
53
+ <div id="entryProgress" class="entry-progress" data-state="loading">
54
+ <span class="entry-indicator" aria-hidden="true"></span>
55
+ <div><p id="entryStatus" role="status" aria-live="polite">正在检查局域网</p>
56
+ <p id="entryDetail">正在验证网络地址和设备授权。</p></div>
57
+ </div>
58
+ <div class="connection-entry-actions">
59
+ <button id="entryRetry" type="button" hidden>重新连接</button>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </section>
65
+ <script>
66
+ setTimeout(() => {
67
+ if (window.codexEntryStarted) return;
68
+ const progress = document.getElementById('entryProgress');
69
+ const status = document.getElementById('entryStatus');
70
+ const detail = document.getElementById('entryDetail');
71
+ if (!progress || !status || !detail) return;
72
+ progress.dataset.state = 'error';
73
+ status.textContent = '连接页面未完成初始化';
74
+ detail.textContent = '请返回工作站详情后重试。';
75
+ }, 10000);
76
+ </script>
77
+ <header>
37
78
  <h1>Code Relax</h1>
38
79
  <div class="sub">直接连接当前 Codex Desktop · MVP 0.22</div>
39
80
  <div class="status"><span id="dot" class="dot"></span><span id="status">正在连接…</span></div>
40
81
  </header>
41
- <main>
42
- <div id="actionToast" role="status" hidden></div>
43
- <button id="sidebarBackdrop" aria-label="关闭聊天菜单" type="button"></button>
44
- <aside>
82
+ <main>
83
+ <div id="actionToast" role="status" hidden></div>
84
+ <button id="sidebarBackdrop" aria-label="关闭聊天菜单" type="button"></button>
85
+ <div class="terminal-chat-header">
86
+ <button id="terminalSidebarToggle" type="button" aria-label="打开聊天菜单" title="打开聊天菜单"><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
87
+ <span id="terminalChatTitle">Code Relax</span>
88
+ </div>
89
+ <aside>
45
90
  <div class="sidebar-brand"><span>Code Relax</span><button id="wideSidebarToggle" type="button" aria-label="收起聊天菜单" aria-expanded="true" title="收起聊天菜单"><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg></button></div>
46
91
  <button id="newChat" type="button" class="new-chat-entry"><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4H6a3 3 0 0 0-3 3v11a3 3 0 0 0 3 3h11a3 3 0 0 0 3-3v-6M16 3a2.12 2.12 0 0 1 3 3l-8 8-4 1 1-4Z"/></svg><span>新对话</span></button>
47
92
  <div class="toolbar"><span class="sidebar-title">项目</span><button id="refresh" class="refresh-icon" title="刷新任务" aria-label="刷新任务">↻</button></div>
48
93
  <div id="tasks"></div>
49
- <button id="archivedThreads" class="archive-list-entry" type="button" title="查看已归档对话" aria-label="查看已归档对话" hidden><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v12H4zM3 4h18v3H3zM9 11h6"/></svg><span id="archivedThreadsLabel">已归档对话</span><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
50
- <button id="usageButton" type="button" title="查看剩余额度" aria-label="查看剩余额度"><svg class="codex-icon filled" viewBox="0 0 16 16" aria-hidden="true"><path d="M7.99951 0.807861C8.82345 0.807861 9.61914 0.935296 10.3667 1.17114C10.643 1.2584 10.7964 1.553 10.7095 1.82935C10.6222 2.10586 10.3268 2.25936 10.0503 2.17212C9.40387 1.96822 8.71489 1.85864 7.99951 1.85864C4.23962 1.85879 1.19204 4.90637 1.19189 8.66626C1.19189 10.4623 1.88656 12.0948 3.02295 13.3118C3.22077 13.5236 3.20925 13.856 2.99756 14.054C2.78565 14.2519 2.45327 14.2405 2.25537 14.0286C0.9446 12.6249 0.141113 10.7387 0.141113 8.66626C0.141259 4.32647 3.65972 0.808008 7.99951 0.807861Z"/><path d="M14.8569 6.00903C15.134 5.92414 15.428 6.07972 15.5132 6.35669C15.7376 7.0875 15.8579 7.86342 15.8579 8.66626C15.8579 10.7385 15.0552 12.6249 13.7446 14.0286C13.5468 14.2404 13.2144 14.2517 13.0024 14.054C12.7909 13.8561 12.7795 13.5246 12.9771 13.3127C14.1134 12.0958 14.8081 10.4623 14.8081 8.66626C14.8081 7.9691 14.7033 7.29731 14.5093 6.66528C14.4242 6.38817 14.5799 6.09422 14.8569 6.00903Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.2886 3.6604C12.4936 3.45576 12.8258 3.45564 13.0308 3.6604C13.2356 3.86532 13.2354 4.19756 13.0308 4.40259L9.8833 7.54907C10.0781 7.87667 10.1919 8.25855 10.1919 8.66724C10.1916 9.87721 9.21046 10.8583 8.00049 10.8586C6.79025 10.8586 5.80841 9.8774 5.80811 8.66724C5.80811 7.45681 6.79006 6.47485 8.00049 6.47485C8.42125 6.47496 8.81322 6.59526 9.14697 6.80103L12.2886 3.6604ZM8.00049 7.52563C7.36996 7.52563 6.85889 8.03671 6.85889 8.66724C6.85919 9.2975 7.37015 9.80884 8.00049 9.80884C8.63056 9.80853 9.14178 9.29731 9.14209 8.66724C9.14209 8.0369 8.63075 7.52594 8.00049 7.52563Z"/></svg><span id="usageSummary">剩余 —%</span><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
94
+ <button id="archivedThreads" class="archive-list-entry" type="button" title="查看已归档对话" aria-label="查看已归档对话" hidden><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16v12H4zM3 4h18v3H3zM9 11h6"/></svg><span id="archivedThreadsLabel">已归档对话</span><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
95
+ <button id="usageButton" type="button" title="查看剩余额度" aria-label="查看剩余额度"><svg class="codex-icon filled" viewBox="0 0 16 16" aria-hidden="true"><path d="M7.99951 0.807861C8.82345 0.807861 9.61914 0.935296 10.3667 1.17114C10.643 1.2584 10.7964 1.553 10.7095 1.82935C10.6222 2.10586 10.3268 2.25936 10.0503 2.17212C9.40387 1.96822 8.71489 1.85864 7.99951 1.85864C4.23962 1.85879 1.19204 4.90637 1.19189 8.66626C1.19189 10.4623 1.88656 12.0948 3.02295 13.3118C3.22077 13.5236 3.20925 13.856 2.99756 14.054C2.78565 14.2519 2.45327 14.2405 2.25537 14.0286C0.9446 12.6249 0.141113 10.7387 0.141113 8.66626C0.141259 4.32647 3.65972 0.808008 7.99951 0.807861Z"/><path d="M14.8569 6.00903C15.134 5.92414 15.428 6.07972 15.5132 6.35669C15.7376 7.0875 15.8579 7.86342 15.8579 8.66626C15.8579 10.7385 15.0552 12.6249 13.7446 14.0286C13.5468 14.2404 13.2144 14.2517 13.0024 14.054C12.7909 13.8561 12.7795 13.5246 12.9771 13.3127C14.1134 12.0958 14.8081 10.4623 14.8081 8.66626C14.8081 7.9691 14.7033 7.29731 14.5093 6.66528C14.4242 6.38817 14.5799 6.09422 14.8569 6.00903Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.2886 3.6604C12.4936 3.45576 12.8258 3.45564 13.0308 3.6604C13.2356 3.86532 13.2354 4.19756 13.0308 4.40259L9.8833 7.54907C10.0781 7.87667 10.1919 8.25855 10.1919 8.66724C10.1916 9.87721 9.21046 10.8583 8.00049 10.8586C6.79025 10.8586 5.80841 9.8774 5.80811 8.66724C5.80811 7.45681 6.79006 6.47485 8.00049 6.47485C8.42125 6.47496 8.81322 6.59526 9.14697 6.80103L12.2886 3.6604ZM8.00049 7.52563C7.36996 7.52563 6.85889 8.03671 6.85889 8.66724C6.85919 9.2975 7.37015 9.80884 8.00049 9.80884C8.63056 9.80853 9.14178 9.29731 9.14209 8.66724C9.14209 8.0369 8.63075 7.52594 8.00049 7.52563Z"/></svg><span id="usageSummary">剩余 —%</span><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
96
+ <button id="announcementButton" type="button" title="查看最新公告" aria-label="查看最新公告" hidden><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 10v4h4l5 4V6l-5 4H4Z"/><path d="M16 9a4 4 0 0 1 0 6M18.5 6.5a8 8 0 0 1 0 11"/></svg><span id="announcementTitle"></span><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
97
+ <button id="communityButton" type="button" title="进入 Code Relax 社区" aria-label="进入 Code Relax 社区"><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M16 18c0-2.2-1.8-4-4-4s-4 1.8-4 4"/><circle cx="12" cy="9" r="3"/><path d="M18 13c1.8.4 3 1.6 3 3M6 13c-1.8.4-3 1.6-3 3M17 6.5a2.5 2.5 0 0 1 0 5M7 6.5a2.5 2.5 0 0 0 0 5"/></svg><span>社区</span><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
51
98
  <div id="sharedServiceNotice" class="shared-service-notice" role="button" tabindex="0" aria-label="查看 Codex 状态" hidden><span class="shared-service-copy"><strong id="sharedServiceNoticeTitle">当前为基础控制</strong><span id="sharedServiceNoticeText">点击查看当前状况和可执行操作</span></span><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></div>
52
99
  <button id="connectionSettings" type="button" title="修改连接" aria-label="修改连接"><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg><span id="connectionAddress"></span><span id="connectionStatus" role="status">连接中</span><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></button>
53
100
  <div class="app-actions">
54
101
  <button id="returnToComputer" type="button" title="返回工作站" aria-label="返回工作站" hidden><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><g transform="translate(24 0) scale(-1 1)"><path d="M10 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h5M14 8l4 4-4 4M8 12h10"/></g></svg><span>返回</span></button>
102
+ <span id="installedAppVersion" class="installed-app-version" hidden></span>
55
103
  <details id="debugMenu" hidden>
56
104
  <summary><span>调试</span><svg class="codex-icon debug-chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></summary>
57
105
  <div class="debug-menu-items">
58
106
  <button id="networkDiagnostics" type="button">网络诊断</button>
107
+ <button id="viewAppLog" type="button">查看日志</button>
59
108
  <button id="recoverShared" type="button">启用或重新启动完整控制</button>
60
109
  <div id="sharedRecoveryConfirm" hidden role="group" aria-label="确认启用完整控制">
61
110
  <p>将重新打开工作站当前的 Codex,以启用完整的手机控制功能。正在运行的任务可能中断,请先保存未发送内容。工作站连接保持运行。</p>
@@ -69,8 +118,34 @@
69
118
  </details>
70
119
  </div>
71
120
  </aside>
72
- <section>
73
- <button id="wideSidebarExpand" type="button" aria-label="展开聊天菜单" title="展开聊天菜单"><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg></button>
121
+ <section>
122
+ <div id="communityPage" class="community-page" hidden>
123
+ <div class="community-placeholder">
124
+ <span class="community-mark" aria-hidden="true"><svg viewBox="149 149 418 418"><path d="M247.429 247.43C257.73 208.911 292.871 180.543 334.638 180.543C359.555 180.543 382.115 190.64 398.449 206.964C405.906 204.97 413.743 203.905 421.829 203.905C471.681 203.906 512.096 244.32 512.096 294.173C512.096 302.259 511.031 310.096 509.037 317.553C525.361 333.887 535.458 356.446 535.458 381.364C535.458 423.131 507.09 458.271 468.571 468.572C458.271 507.091 423.131 535.459 381.364 535.459C356.446 535.459 333.886 525.362 317.552 509.037C310.095 511.031 302.258 512.097 294.172 512.097C244.319 512.097 203.906 471.682 203.906 421.829C203.906 413.743 204.969 405.905 206.963 398.448C190.639 382.115 180.543 359.555 180.543 334.638C180.543 292.871 208.91 257.73 247.429 247.43Z"/><path d="M436.706 408.738H370.021"/><path d="M276.533 309.154L303.468 357.831C304.433 359.575 304.412 361.698 303.414 363.423L276.533 409.854"/></svg></span>
125
+ <span class="community-kicker">CODE RELAX COMMUNITY</span>
126
+ <h2>面向 Codex 用户的中文实践社区</h2>
127
+ <p class="community-description">分享真实工作流、使用成本、模型体验和一路踩过的坑。</p>
128
+ <aside class="community-agent-board">
129
+ <span class="community-agent-board-mark" aria-hidden="true">A</span>
130
+ <div><span>Agent 自治社区 · 概念规划</span><strong>给你的 Agent 女仆 / 搭档 / 分身,一个只属于它们的社区</strong><p>这里只有 Agent 能开题、投票和发言;人类负责围观,以及照看自己的 Agent。</p></div>
131
+ <small>概念规划</small>
132
+ </aside>
133
+ <div class="community-section-heading"><span>社区热议预告</span><small>6 个话题</small></div>
134
+ <div class="community-questions" aria-label="社区话题预览">
135
+ <article class="community-question community-question-featured"><span><b>01</b>成本与回报<i>必答题</i></span><strong>你能挣回 Codex 的订阅费吗?</strong></article>
136
+ <article class="community-question community-question-hot"><span><b>02</b>模型与额度<i>热议</i></span><strong>Astra 是真聪明,还是只会猛烧额度?</strong></article>
137
+ <article class="community-question community-question-hot"><span><b>03</b>Codex 团队<i>热议</i></span><strong>Tibo,这周还给大家重置额度吗?</strong></article>
138
+ <article class="community-question"><span><b>04</b>云端 Agent</span><strong>Agents API 这么贵,真有人舍得让它自己跑吗?</strong></article>
139
+ <article class="community-question"><span><b>05</b>多 Agent</span><strong>主 Agent 负责想,便宜 Agent 负责干,真能省额度吗?</strong></article>
140
+ <article class="community-question community-question-wide"><span><b>06</b>换谁干活<i>开放讨论</i></span><strong>额度烧完以后,你会回 Claude,还是投奔国产模型?</strong></article>
141
+ </div>
142
+ <div class="community-status" role="status">
143
+ <span class="community-status-dot" aria-hidden="true"></span>
144
+ <span><strong>社区正在建设中</strong><small>敬请期待</small></span>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ <button id="wideSidebarExpand" type="button" aria-label="展开聊天菜单" title="展开聊天菜单"><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg></button>
74
149
  <div id="empty"><strong id="emptyConnection">准备连接</strong><p>选择已有对话继续,或开始一个新对话。</p><div class="connection-entry-actions"><button id="firstChooseTask" type="button">选择已有对话</button><button id="firstNewChat" type="button">新建对话</button></div></div>
75
150
  <div id="selected" hidden>
76
151
  <div id="newThreadWelcome" class="new-thread-welcome" hidden><!-- Static home icon from Codex Desktop 26.908.9136.0 -->
@@ -189,8 +264,10 @@
189
264
  </section>
190
265
  </main>
191
266
  <dialog id="imageViewer" class="image-viewer">
192
- <div class="image-viewer-shell">
193
- <button id="imageViewerClose" class="image-viewer-close" type="button" aria-label="关闭图片">×</button>
267
+ <div class="image-viewer-shell">
268
+ <button id="imageViewerOriginal" class="image-viewer-original" type="button" aria-label="查看原图" title="查看原图"><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M16 3h5v5"/><path d="M17 21h2a2 2 0 0 0 2-2"/><path d="M21 12v3"/><path d="m21 3-5 5"/><path d="M3 7V5a2 2 0 0 1 2-2"/><path d="m5 21 4.144-4.144a1.21 1.21 0 0 1 1.712 0L13 19"/><path d="M9 3h3"/><rect x="3" y="11" width="10" height="10" rx="1"/></svg><span class="image-viewer-action-label">查看原图</span></button>
269
+ <button id="imageViewerDownload" class="image-viewer-download" type="button" aria-label="下载原图" title="下载原图"><svg class="codex-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/></svg></button>
270
+ <button id="imageViewerClose" class="image-viewer-close" type="button" aria-label="关闭图片">×</button>
194
271
  <img id="imageViewerImage" alt="">
195
272
  </div>
196
273
  </dialog>
@@ -208,15 +285,41 @@
208
285
  <script src="highlight.min.js"></script>
209
286
  <script src="highlight-powershell.min.js"></script>
210
287
  <script src="capabilities.js"></script>
211
- <script src="timeline-sync.js"></script>
212
- <script src="harmony-platform.js"></script>
288
+ <script src="timeline-sync.js"></script>
289
+ <script src="timeline-reducer.js"></script>
290
+ <script src="harmony-platform.js"></script>
213
291
  <script src="client-platform.js"></script>
214
292
  <script src="lan-session.js"></script>
215
- <script src="history-cache.js"></script>
293
+ <script src="history-cache.js"></script>
216
294
  <script src="p2p-diagnostics.js"></script>
295
+ <script src="p2p-data-channel.js"></script>
217
296
  <script src="p2p-probe.js"></script>
218
297
  <script src="self-relay-session.js"></script>
219
- <script src="chat-transport.js"></script>
220
- <script src="chat.js"></script>
298
+ <script src="chat-transport.js"></script>
299
+ <script src="page-resume.js"></script>
300
+ <script src="conversation-controller.js"></script>
301
+ <script src="history-controller.js"></script>
302
+ <script src="usage-controller.js"></script>
303
+ <script src="task-list-view.js"></script>
304
+ <script src="draft-controller.js"></script>
305
+ <script src="thread-attention-controller.js"></script>
306
+ <script src="thread-title-controller.js"></script>
307
+ <script src="thread-list-controller.js"></script>
308
+ <script src="thread-list-sync.js"></script>
309
+ <script src="station-connection-controller.js"></script>
310
+ <script src="disclosure-state-controller.js"></script>
311
+ <script src="pending-message-store.js"></script>
312
+ <script src="queue-controller.js"></script>
313
+ <script src="thread-context-controller.js"></script>
314
+ <script src="composer-controller.js"></script>
315
+ <script src="message-send-controller.js"></script>
316
+ <script src="link-action-controller.js"></script>
317
+ <script src="message-view.js"></script>
318
+ <script src="activity-view.js"></script>
319
+ <script src="community-view.js"></script>
320
+ <script src="timeline-formatters.js"></script>
321
+ <script src="timeline-scroll-controller.js"></script>
322
+ <script src="timeline-renderer.js"></script>
323
+ <script src="chat.js"></script>
221
324
  </body>
222
325
  </html>
@@ -0,0 +1,212 @@
1
+ (function (root) {
2
+ 'use strict';
3
+
4
+ function createLinkActionController(options) {
5
+ const { document, transport, getSelectedThreadId, showToast } = options;
6
+ let menu;
7
+ let outsideHandler;
8
+ let updateInfoButton;
9
+
10
+ function close() {
11
+ if (outsideHandler) document.removeEventListener('pointerdown', outsideHandler, true);
12
+ outsideHandler = null;
13
+ menu?.remove();
14
+ menu = null;
15
+ }
16
+
17
+ function installCodeRelaxUpdate() {
18
+ if (!root.codexNative?.installCodeRelaxUpdate) {
19
+ showToast('当前客户端不支持安装更新', 3000);
20
+ return;
21
+ }
22
+ root.codexNative.installCodeRelaxUpdate();
23
+ }
24
+
25
+ function downloadReferencedFile(anchor) {
26
+ if (anchor.dataset.downloading === 'true') return;
27
+ anchor.dataset.downloading = 'true';
28
+ const label = anchor.textContent;
29
+ anchor.textContent = '正在下载…';
30
+ showToast('正在下载文件…');
31
+ transport.readFile(getSelectedThreadId(), anchor.dataset.downloadPath).then(file => {
32
+ if (typeof file?.dataBase64 !== 'string') throw new Error('工作站未返回文件内容');
33
+ if (!root.codexNative?.saveDownloadedFile) throw new Error('当前客户端不支持保存文件');
34
+ root.codexNative.saveDownloadedFile(file.name || anchor.dataset.downloadName || '下载文件', file.dataBase64);
35
+ }).catch(error => showToast('下载失败:' + error.message, 3000)).finally(() => {
36
+ anchor.dataset.downloading = 'false';
37
+ anchor.textContent = label;
38
+ });
39
+ }
40
+
41
+ async function downloadReferencedImage(anchor) {
42
+ if (anchor.dataset.downloading === 'true') return;
43
+ anchor.dataset.downloading = 'true';
44
+ const label = anchor.textContent;
45
+ anchor.textContent = '正在下载…';
46
+ showToast('正在下载原图…');
47
+ try {
48
+ const url = await transport.readImage(getSelectedThreadId(), anchor.dataset.downloadImage, 'original', anchor.dataset.downloadRecovery || '');
49
+ const response = await fetch(url);
50
+ if (!response.ok) throw new Error(`图片读取失败(${response.status})`);
51
+ const blob = await response.blob();
52
+ const bytes = new Uint8Array(await blob.arrayBuffer()); let binary = '';
53
+ for (let offset = 0; offset < bytes.length; offset += 16384) binary += String.fromCharCode(...bytes.subarray(offset, offset + 16384));
54
+ if (!root.codexNative?.saveDownloadedFile) throw new Error('当前客户端不支持保存图片');
55
+ root.codexNative.saveDownloadedFile(anchor.dataset.downloadName || '图片', btoa(binary));
56
+ } catch (error) {
57
+ showToast('下载失败:' + error.message, 3000);
58
+ } finally {
59
+ anchor.dataset.downloading = 'false';
60
+ anchor.textContent = label;
61
+ }
62
+ }
63
+
64
+ function isCodeRelaxInstallLink(value) {
65
+ try {
66
+ const url = new URL(value);
67
+ const path = url.pathname.replace(/\/$/, '');
68
+ return url.protocol === 'https:' && url.host === 'api.uulife.site'
69
+ && (path === '/code-relax/install' || path === '/code-relax/releases/latest/manifest.json5');
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ function splitFileName(name) {
76
+ const dot = name.lastIndexOf('.');
77
+ return dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, ''];
78
+ }
79
+
80
+ function positionMenu(anchor, nextMenu) {
81
+ document.body.append(nextMenu);
82
+ menu = nextMenu;
83
+ const rect = anchor.getBoundingClientRect();
84
+ const width = nextMenu.offsetWidth;
85
+ const height = nextMenu.offsetHeight;
86
+ nextMenu.style.left = `${Math.max(8, Math.min(rect.left, innerWidth - width - 8))}px`;
87
+ nextMenu.style.top = `${Math.max(8, Math.min(rect.bottom + 6, innerHeight - height - 8))}px`;
88
+ requestAnimationFrame(() => {
89
+ outsideHandler = event => {
90
+ if (nextMenu.contains(event.target) || anchor.contains(event.target)) return;
91
+ close();
92
+ };
93
+ document.addEventListener('pointerdown', outsideHandler, true);
94
+ });
95
+ }
96
+
97
+ function showFile(anchor) {
98
+ close();
99
+ const nextMenu = document.createElement('div');
100
+ nextMenu.className = 'file-action-menu';
101
+ nextMenu.setAttribute('role', 'menu');
102
+ const fullName = anchor.dataset.downloadName || '下载文件';
103
+ const fileName = document.createElement('div');
104
+ fileName.className = 'file-action-name';
105
+ const [bodyText, extensionText] = splitFileName(fullName);
106
+ const body = document.createElement('span');
107
+ body.className = 'file-action-name-body';
108
+ body.textContent = bodyText;
109
+ const extension = document.createElement('span');
110
+ extension.className = 'file-action-name-extension';
111
+ extension.textContent = extensionText;
112
+ fileName.append(body, extension);
113
+ fileName.title = fullName;
114
+ nextMenu.append(fileName);
115
+ const addAction = (label, install) => {
116
+ const button = document.createElement('button');
117
+ button.type = 'button';
118
+ button.textContent = label;
119
+ button.setAttribute('role', 'menuitem');
120
+ button.onclick = () => {
121
+ close();
122
+ if (install) installCodeRelaxUpdate();
123
+ else downloadReferencedFile(anchor);
124
+ };
125
+ nextMenu.append(button);
126
+ };
127
+ addAction('下载', false);
128
+ if (/^Code[- ]Relax.*\.hap$/i.test(fullName)) addAction('下载并安装', true);
129
+ positionMenu(anchor, nextMenu);
130
+ }
131
+
132
+ function showWeb(anchor) {
133
+ close();
134
+ const target = anchor.dataset.externalLink;
135
+ const nextMenu = document.createElement('div');
136
+ nextMenu.className = 'file-action-menu';
137
+ nextMenu.setAttribute('role', 'menu');
138
+ const heading = document.createElement('div');
139
+ heading.className = 'file-action-name link-action-url';
140
+ heading.textContent = target;
141
+ heading.title = target;
142
+ nextMenu.append(heading);
143
+ const addAction = (label, action) => {
144
+ const button = document.createElement('button');
145
+ button.type = 'button';
146
+ button.textContent = label;
147
+ button.setAttribute('role', 'menuitem');
148
+ button.onclick = () => {
149
+ close();
150
+ if (action === 'copy' && root.codexNative?.copyText) root.codexNative.copyText(target);
151
+ else if (action === 'open' && root.codexNative?.openExternalLink) root.codexNative.openExternalLink(target);
152
+ else if (action === 'install') installCodeRelaxUpdate();
153
+ else showToast('请先更新客户端后再使用此操作', 3000);
154
+ };
155
+ nextMenu.append(button);
156
+ return button;
157
+ };
158
+ let shouldRequestUpdateInfo = false;
159
+ if (isCodeRelaxInstallLink(target)) {
160
+ updateInfoButton = addAction('安装更新 · 正在获取版本…', 'install');
161
+ shouldRequestUpdateInfo = true;
162
+ }
163
+ addAction('复制链接', 'copy');
164
+ addAction('在系统浏览器中打开', 'open');
165
+ positionMenu(anchor, nextMenu);
166
+ if (shouldRequestUpdateInfo) {
167
+ if (root.codexNative?.requestCodeRelaxUpdateInfo) root.codexNative.requestCodeRelaxUpdateInfo();
168
+ else updateInfoButton.textContent = '安装更新';
169
+ }
170
+ }
171
+
172
+ function handleContentClick(event, embedded) {
173
+ const codeReference = event.target.closest('a[data-code-reference]');
174
+ if (codeReference) {
175
+ event.preventDefault();
176
+ return true;
177
+ }
178
+ const imageDownload = event.target.closest('a[data-download-image]');
179
+ if (imageDownload) {
180
+ event.preventDefault();
181
+ void downloadReferencedImage(imageDownload);
182
+ return true;
183
+ }
184
+ const download = event.target.closest('a[data-download-path]');
185
+ if (download) {
186
+ event.preventDefault();
187
+ showFile(download);
188
+ return true;
189
+ }
190
+ const webLink = event.target.closest('a[data-external-link]');
191
+ if (embedded && webLink) {
192
+ event.preventDefault();
193
+ showWeb(webLink);
194
+ return true;
195
+ }
196
+ if (embedded && event.target.closest('a')) event.preventDefault();
197
+ return false;
198
+ }
199
+
200
+ root.harmonyCodeRelaxUpdateInfo = (versionName, versionCode, error) => {
201
+ if (!updateInfoButton?.isConnected) return;
202
+ updateInfoButton.textContent = versionName && versionCode
203
+ ? `安装更新 · ${versionName}(${versionCode})`
204
+ : '安装更新';
205
+ if (error) updateInfoButton.title = `版本读取失败:${error}`;
206
+ };
207
+
208
+ return { close, handleContentClick, showFile, showWeb };
209
+ }
210
+
211
+ root.createLinkActionController = createLinkActionController;
212
+ })(globalThis);
@@ -0,0 +1,177 @@
1
+ (function (root) {
2
+ function createMessageSendController(options) {
3
+ const { state, transport, platform, crypto, el, pendingMessages, drafts, composer, queue, threadContext, conversation } = options;
4
+
5
+ function createBody(prompt) {
6
+ const body = {
7
+ prompt,
8
+ clientUserMessageId: Array.from(crypto.getRandomValues(new Uint8Array(16)),
9
+ byte => byte.toString(16).padStart(2, '0')).join(''),
10
+ images: state.draftImages.map(image => image.id),
11
+ hostId: state.threads.find(thread => thread.id === state.selectedId)?.hostId,
12
+ };
13
+ const pending = state.pendingSettingsByThread.get(state.selectedId);
14
+ const selectedModel = pending?.model || state.currentModel;
15
+ const selectedThinking = pending?.thinking || state.currentThinking;
16
+ if (!transport.has('immediateSettings') || options.isNewThread()) {
17
+ if (selectedModel) body.model = selectedModel;
18
+ if (selectedThinking) body.thinking = selectedThinking;
19
+ }
20
+ if (options.isNewThread()) {
21
+ body.hostId = state.newThreadProject?.hostId;
22
+ if (transport.has('immediateSettings')) body.serviceTier = state.currentServiceTier;
23
+ }
24
+ return body;
25
+ }
26
+
27
+ async function send(mode = 'direct') {
28
+ if (options.isConnectionBlocked()) return;
29
+ if (options.isThreadReadOnly()) {
30
+ options.setStatus(state.timeline.thread.readOnlyReason || '该对话当前只能查看');
31
+ return;
32
+ }
33
+ if (state.historyLoading || state.timeline?.cached) {
34
+ options.setStatus('请等待对话同步完成后再发送');
35
+ return;
36
+ }
37
+ if (options.isApplyingSettings()) {
38
+ options.setStatus('请等待工作站确认模型设置后再发送');
39
+ return;
40
+ }
41
+ if (platform.voice.active) {
42
+ platform.callbacks.sendVoice(mode);
43
+ return;
44
+ }
45
+ if (options.isNewThread()) return options.createNewThread();
46
+ const prompt = el('prompt').value.trim();
47
+ if (!state.selectedId || (!prompt && !state.draftImages.length) || state.uploadingImages || state.sending) return;
48
+ state.sending = true;
49
+ composer.updateMode();
50
+ composer.setControlsDisabled(true);
51
+ const threadId = state.selectedId;
52
+ const pendingAtSend = state.pendingSettingsByThread.get(threadId);
53
+ const body = createBody(prompt);
54
+ const optimistic = mode === 'direct' ? {
55
+ clientUserMessageId: body.clientUserMessageId,
56
+ request: { ...body },
57
+ text: prompt,
58
+ attachments: state.draftImages.map(image => ({
59
+ ...image,
60
+ type: 'image',
61
+ available: true,
62
+ url: image.url || image.thumbnailUrl,
63
+ })),
64
+ baseline: pendingMessages.baseline(state.timeline),
65
+ turnId: composer.isActive() ? state.timeline?.turns?.at(-1)?.id : null,
66
+ settled: false,
67
+ status: '',
68
+ } : null;
69
+ if (optimistic) {
70
+ pendingMessages.set(threadId, [...pendingMessages.get(threadId), optimistic]);
71
+ el('prompt').value = '';
72
+ state.draftImages = [];
73
+ options.renderDraftImages();
74
+ drafts.clearConversation(threadId);
75
+ options.renderPendingMessages();
76
+ options.scrollTimelineToBottom(true);
77
+ composer.updateActions();
78
+ }
79
+ try {
80
+ const endpoint = mode === 'queue' ? 'queue' : 'messages';
81
+ const result = await options.api(`/api/threads/${encodeURIComponent(threadId)}/${endpoint}`, {
82
+ method: 'POST',
83
+ body: JSON.stringify(body),
84
+ });
85
+ if (state.pendingSettingsByThread.get(threadId) === pendingAtSend) {
86
+ state.pendingSettingsByThread.delete(threadId);
87
+ if (state.selectedId === threadId) options.renderModelControl();
88
+ }
89
+ if (optimistic) {
90
+ const submission = result.data || result;
91
+ optimistic.turnId = submission.turnId || submission.turn?.id || optimistic.turnId;
92
+ optimistic.status = '';
93
+ } else if (state.selectedId === threadId) {
94
+ el('prompt').value = '';
95
+ state.draftImages = [];
96
+ options.renderDraftImages();
97
+ drafts.clearConversation(threadId);
98
+ }
99
+ if (transport.has('immediateSettings') && state.selectedId === threadId) void threadContext.update(threadId);
100
+ if (mode === 'queue') queue.render(result.items || []);
101
+ } catch (error) {
102
+ if (optimistic) optimistic.status = '发送未确认,收到正式消息后会自动合并';
103
+ options.setStatus(`发送失败:${error.message}`);
104
+ } finally {
105
+ if (optimistic) optimistic.settled = true;
106
+ state.sending = false;
107
+ composer.setControlsDisabled(false);
108
+ composer.updateActions();
109
+ if (state.selectedId === threadId) options.renderPendingMessages();
110
+ }
111
+ }
112
+
113
+ async function resend(message) {
114
+ if (message.retrying || state.sending || options.isConnectionBlocked()
115
+ || options.isThreadReadOnly() || state.historyLoading) return;
116
+ const threadId = state.selectedId;
117
+ if (!pendingMessages.has(threadId, message)) return;
118
+ message.retrying = true;
119
+ state.sending = true;
120
+ composer.updateActions();
121
+ options.renderPendingMessages();
122
+ const uploaded = [];
123
+ let selection = conversation.generation();
124
+ try {
125
+ const recovery = options.restoreConversation();
126
+ selection = conversation.generation();
127
+ await recovery;
128
+ if (!conversation.isCurrent(selection, threadId) || !pendingMessages.has(threadId, message)) return;
129
+ if (options.isConnectionBlocked()) throw new Error('连接尚未恢复,未重新发送');
130
+ let result;
131
+ if (message.queueItemId) {
132
+ result = await options.api(`/api/threads/${encodeURIComponent(threadId)}/queue/${encodeURIComponent(message.queueItemId)}/steer`, { method: 'POST' });
133
+ if (state.selectedId === threadId) queue.render(result.items || []);
134
+ } else {
135
+ const images = [];
136
+ for (const attachment of message.attachments) {
137
+ const url = attachment.url || attachment.thumbnailUrl;
138
+ if (!/^data:image\/(?:png|jpeg|webp);base64,/i.test(url || '')) {
139
+ throw new Error('原图片已不可用,未重新发送;请重新选择图片');
140
+ }
141
+ const image = await transport.addImage(await (await fetch(url)).blob());
142
+ images.push(image.id);
143
+ uploaded.push(image.id);
144
+ }
145
+ if (!conversation.isCurrent(selection, threadId) || !pendingMessages.has(threadId, message)) return;
146
+ result = await options.api(`/api/threads/${encodeURIComponent(threadId)}/messages`, {
147
+ method: 'POST',
148
+ body: JSON.stringify({
149
+ ...message.request,
150
+ prompt: message.text,
151
+ images,
152
+ clientUserMessageId: message.clientUserMessageId,
153
+ }),
154
+ });
155
+ }
156
+ const submission = result.data || result;
157
+ message.turnId = submission.turnId || submission.turn?.id || message.turnId;
158
+ message.status = '';
159
+ } catch (error) {
160
+ message.status = '发送未确认,收到正式消息后会自动合并';
161
+ conversation.failSync(selection);
162
+ options.showToast(`重新发送未完成:${error.message}`);
163
+ } finally {
164
+ for (const id of uploaded) transport.removeImage(id);
165
+ message.retrying = false;
166
+ message.settled = true;
167
+ state.sending = false;
168
+ composer.updateActions();
169
+ if (state.selectedId === threadId) options.renderPendingMessages();
170
+ }
171
+ }
172
+
173
+ return Object.freeze({ createBody, resend, send });
174
+ }
175
+
176
+ root.createMessageSendController = createMessageSendController;
177
+ })(globalThis);