deepccc 0.2.2 → 0.2.4
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/README.md +19 -0
- package/dist/web-page.d.ts +1 -1
- package/dist/web-page.js +27 -18
- package/dist/web-runtime.d.ts +8 -1
- package/dist/web-runtime.js +111 -43
- package/dist/web-server.d.ts +1 -1
- package/dist/web-server.js +17 -0
- package/docs/deepccc-api-settings.png +0 -0
- package/docs/deepccc-inline-approval.png +0 -0
- package/docs/deepccc-web-ui.png +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,25 @@ npm run build
|
|
|
36
36
|
npm run dev
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Web UI 预览
|
|
40
|
+
|
|
41
|
+
以下画面来自 DeepCCC 对本项目真实开发需求的 Agent 调用。截图仅将用户名、组织名、
|
|
42
|
+
内部域名和绝对路径替换为公开示例,任务内容、模型配置、Agent 回复和审批流程均来自
|
|
43
|
+
实际运行结果。
|
|
44
|
+
|
|
45
|
+
多会话可以并行处理不同任务,每个会话分别选择 model 和 effort,并实时保留 Agent
|
|
46
|
+
回复与工具过程:
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
命中需要确认的命令时,审批卡会直接出现在当前会话时间线中,不打断到弹窗:
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
单一 API 配置作为新会话默认值,模型与 effort 仍可在每个会话中单独覆盖:
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
39
58
|
## 核心能力
|
|
40
59
|
|
|
41
60
|
- Web-first:多会话、持久化历史、实时流式过程、停止和恢复
|
package/dist/web-page.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const DEEPCCC_WEB_PAGE = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>DeepCCC</title>\n<style>\n:root{color-scheme:light;--bg:#f5f7fb;--panel:#fff;--panel2:#fff;--soft:#f8f9fc;--line:#e3e7ef;--text:#182033;--muted:#737d91;--accent:#6758e8;--accent-soft:#eeeaff;--accent2:#23825d;--danger:#c2415a;--warning:#b7791f;--shadow:0 8px 28px rgba(26,35,58,.08)}\n*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Microsoft YaHei\",sans-serif;overflow:hidden}button,input,select,textarea{font:inherit}button{cursor:pointer}.app{height:100%;display:grid;grid-template-columns:286px minmax(0,1fr)}\n.sidebar{min-width:0;background:var(--panel);border-right:1px solid var(--line);display:flex;flex-direction:column}.brand{height:70px;display:flex;align-items:center;gap:12px;padding:0 18px;border-bottom:1px solid var(--line)}.mark{width:36px;height:36px;border-radius:12px;display:grid;place-items:center;background:var(--accent);color:#fff;box-shadow:0 9px 25px rgba(103,88,232,.25);font-weight:900}.brand-copy strong{display:block;font-size:16px;letter-spacing:.01em}.brand-copy span{font-size:10px;color:var(--muted);letter-spacing:.13em}.sidebar-actions{display:flex;gap:8px;padding:14px}.primary,.ghost,.icon{border:1px solid var(--line);border-radius:10px;color:var(--text);background:var(--panel);font-weight:700}.primary{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:0 8px 22px rgba(103,88,232,.22)}.sidebar-actions .primary{flex:1;padding:10px}.icon{width:40px;padding:0;font-size:17px}.ghost{padding:9px 12px}.primary:hover,.ghost:hover,.icon:hover{filter:brightness(1.03)}\n.session-list{flex:1;overflow:auto;padding:0 10px 14px}.session-empty{padding:38px 18px;color:var(--muted);text-align:center}.session-item{width:100%;display:block;padding:12px;border:1px solid transparent;border-radius:12px;background:transparent;color:var(--text);text-align:left;margin:3px 0}.session-item:hover{background:var(--panel2)}.session-item.active{background:linear-gradient(135deg,rgba(124,105,241,.2),rgba(93,74,207,.08));border-color:rgba(139,124,246,.35)}.session-title{display:flex;align-items:center;gap:8px;font-weight:750}.status-dot{width:7px;height:7px;border-radius:50%;background:#626a78}.status-dot.running{background:var(--accent2);box-shadow:0 0 0 4px rgba(94,228,177,.1)}.session-meta{margin-top:5px;color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-foot{padding:12px 16px;border-top:1px solid var(--line);color:var(--muted);font-size:10px;display:flex;justify-content:space-between}\n.main{min-width:0;height:100%;display:flex;flex-direction:column}.topbar{height:70px;flex:none;display:flex;align-items:center;justify-content:space-between;padding:0 22px;border-bottom:1px solid var(--line);background:rgba(12,14,19,.7);backdrop-filter:blur(18px)}.session-heading{min-width:0}.session-heading h1{font-size:16px;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.session-heading p{font-size:11px;color:var(--muted);margin:3px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.top-actions{display:flex;gap:8px}.top-actions button{height:38px}.content{flex:1;min-height:0;overflow-y:auto;scroll-behavior:smooth}.welcome{height:100%;display:grid;place-items:center;padding:30px}.welcome-card{max-width:650px;text-align:center}.welcome-mark{width:72px;height:72px;margin:0 auto 20px;border-radius:24px;display:grid;place-items:center;background:linear-gradient(135deg,#7764eb,#b45fd7);font-size:29px;box-shadow:0 20px 50px rgba(119,100,235,.28)}.welcome h2{font-size:28px;margin:0 0 10px;letter-spacing:-.02em}.welcome p{color:var(--muted);margin:0 auto;max-width:480px}.hint-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:28px}.hint{padding:14px;border:1px solid var(--line);border-radius:13px;background:rgba(24,28,37,.65);font-size:12px;color:var(--muted)}\n.thread{width:min(900px,calc(100% - 36px));margin:0 auto;padding:28px 0 150px}.empty-thread{margin:80px auto 0;max-width:520px;padding:28px;border:1px dashed #353c4a;border-radius:18px;background:rgba(20,23,31,.55);text-align:center}.empty-thread strong{display:block;font-size:18px;margin-bottom:7px}.empty-thread span{color:var(--muted);font-size:12px}.message{display:grid;grid-template-columns:30px minmax(0,1fr);gap:12px;margin:0 0 22px}.avatar{width:30px;height:30px;border-radius:10px;display:grid;place-items:center;background:var(--soft);font-size:11px;font-weight:900}.message.user .avatar{background:#304058}.message.assistant .avatar{background:linear-gradient(135deg,#7363e8,#9a61dc)}.bubble{min-width:0;padding:2px 0;color:#dfe3eb}.bubble .role{font-size:11px;color:var(--muted);font-weight:800;margin-bottom:7px}.markdown{font-size:14px;line-height:1.75;overflow-wrap:anywhere}.markdown p{margin:.35em 0}.markdown pre{padding:14px;border:1px solid var(--line);border-radius:11px;background:#0a0c11;overflow:auto;font:12px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace}.markdown code{padding:2px 5px;border-radius:5px;background:var(--soft);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.markdown pre code{padding:0;background:transparent}.tool-card{margin:8px 0;border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:hidden}.tool-card summary{padding:9px 11px;color:#bbc2d0;font-size:12px;font-weight:700}.tool-card pre{margin:0;border:0;border-top:1px solid var(--line);border-radius:0;max-height:260px}.run-state{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px;margin:8px 0 20px}.pulse{width:8px;height:8px;border-radius:50%;background:var(--accent2);animation:pulse 1.4s infinite}@keyframes pulse{50%{opacity:.35;transform:scale(.72)}}.workspace-warning{width:min(900px,calc(100% - 36px));margin:14px auto 0;padding:9px 12px;border:1px solid rgba(242,184,75,.3);background:rgba(242,184,75,.08);border-radius:10px;color:#e8c879;font-size:11px}\n.composer-shell{position:absolute;left:286px;right:0;bottom:0;padding:0 20px 18px;background:linear-gradient(transparent,var(--bg) 32%)}.composer{width:min(900px,100%);margin:auto;border:1px solid #343b49;border-radius:17px;background:rgba(25,29,38,.96);box-shadow:var(--shadow);overflow:hidden}.composer textarea{width:100%;min-height:68px;max-height:180px;resize:none;border:0;outline:0;padding:15px 16px 8px;background:transparent;color:var(--text)}.composer textarea::placeholder{color:#737b8b}.composer-bar{display:flex;align-items:center;gap:8px;padding:7px 9px}.composer-bar input,.composer-bar select{min-width:0;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--muted);padding:7px 9px;font-size:11px}.composer-bar input{width:180px}.composer-bar select{width:104px}.send{margin-left:auto;width:36px;height:36px;border:0;border-radius:10px;background:var(--accent);color:#fff;font-size:17px}.stop{margin-left:auto;height:36px;border:1px solid rgba(255,111,137,.4);border-radius:10px;background:rgba(255,111,137,.12);color:#ff91a5;padding:0 13px;font-weight:700}.composer-disabled{opacity:.5;pointer-events:none}\n.modal-backdrop{position:fixed;inset:0;z-index:30;display:grid;place-items:center;padding:20px;background:rgba(4,5,8,.66);backdrop-filter:blur(7px)}.modal-backdrop.hidden{display:none}.modal{width:min(560px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid #353c4a;border-radius:18px;background:#171a22;box-shadow:0 30px 100px rgba(0,0,0,.55);padding:22px}.modal h2{margin:0 0 5px;font-size:20px}.modal>.subtitle{margin:0 0 20px;color:var(--muted);font-size:12px}.field{display:grid;gap:7px;margin:13px 0}.field label{font-size:11px;color:#b7becb;font-weight:800}.field input,.field select{width:100%;border:1px solid var(--line);border-radius:10px;background:#10131a;color:var(--text);padding:10px 11px;outline:none}.field input:focus,.field select:focus{border-color:var(--accent)}.row{display:grid;grid-template-columns:1fr 1fr;gap:10px}.check{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px}.modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.danger{border:1px solid rgba(255,111,137,.35);background:rgba(255,111,137,.1);color:#ff849b;border-radius:10px;padding:9px 12px;font-weight:700}.approval{width:min(620px,100%)}.approval-badge{display:inline-flex;padding:5px 8px;border-radius:999px;background:rgba(242,184,75,.13);color:#f1c565;font-size:10px;font-weight:900}.approval pre{white-space:pre-wrap;overflow-wrap:anywhere;padding:13px;border:1px solid var(--line);border-radius:10px;background:#0d0f15;color:#dce0e9}.approval-actions{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.toast{position:fixed;right:20px;top:20px;z-index:50;max-width:420px;padding:11px 14px;border-radius:10px;background:#283143;border:1px solid #3b465b;box-shadow:var(--shadow)}.toast.error{background:#602536;border-color:#913a52}.hidden{display:none!important}\n.topbar{background:rgba(255,255,255,.86)}.welcome-mark{background:var(--accent);color:#fff}.hint,.empty-thread{background:var(--panel);border-color:var(--line)}.session-item.active{background:var(--accent-soft);border-color:#d8d1ff}.status-dot{background:#a3aabd}.status-dot.running{background:var(--accent2);box-shadow:0 0 0 4px rgba(35,130,93,.1)}.message.user .avatar{background:#e8edf6}.message.assistant .avatar{background:var(--accent);color:#fff}.bubble{color:var(--text)}.markdown pre,.tool-card pre{background:var(--soft);color:var(--text)}.tool-card summary{color:var(--muted)}.composer{border-color:var(--line);background:rgba(255,255,255,.98)}.composer textarea::placeholder{color:#9aa3b4}.composer-bar input,.composer-bar select{background:var(--soft)}.modal-backdrop{background:rgba(24,32,51,.36)}.modal{border-color:var(--line);background:var(--panel);box-shadow:0 28px 90px rgba(35,43,65,.24)}.field label{color:var(--text)}.field input,.field select{background:var(--soft);color:var(--text)}.danger{border-color:#f3c4ce;background:#fff1f3;color:var(--danger)}.toast{background:#253047;color:#fff}.toast.error{background:#9f2942}.approval-card{margin:16px 0;padding:14px;border:1px solid #ead49b;border-radius:13px;background:#fffaf0;box-shadow:0 5px 18px rgba(94,70,15,.06)}.approval-card.resolved{border-color:var(--line);background:var(--soft)}.approval-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}.approval-head strong{font-size:12px}.approval-status{padding:3px 7px;border-radius:999px;background:#fff0c2;color:#8a6110;font-size:10px;font-weight:800}.approval-card.resolved .approval-status{background:#e5f5ee;color:var(--accent2)}.approval-detail{color:var(--muted);font-size:11px;margin-bottom:8px}.approval-command{margin:0;padding:10px;border:1px solid var(--line);border-radius:9px;background:#fff;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}.approval-inline-actions{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px}.approval-inline-actions button{padding:7px 10px;font-size:11px}\n.sidebar{height:100%;overflow:hidden}.main{min-height:0!important;overflow:hidden}\n@media(max-width:760px){.app{grid-template-columns:78px minmax(0,1fr)}.brand{padding:0;justify-content:center}.brand-copy,.sidebar-actions .primary,.session-item>div:not(.session-title),.session-title span:last-child,.sidebar-foot{display:none}.sidebar-actions{justify-content:center;padding:12px 8px}.session-item{padding:11px;display:grid;place-items:center}.composer-shell{left:78px}.hint-grid{grid-template-columns:1fr}.topbar{padding:0 14px}.thread{width:calc(100% - 24px)}.row{grid-template-columns:1fr}.approval-inline-actions{display:grid;grid-template-columns:1fr 1fr}}\n</style>\n</head>\n<body>\n<div class=\"app\">\n <aside class=\"sidebar\">\n <div class=\"brand\"><div class=\"mark\">D</div><div class=\"brand-copy\"><strong>DeepCCC</strong><span>LOCAL AGENT</span></div></div>\n <div class=\"sidebar-actions\"><button id=\"new-session\" class=\"primary\">\uFF0B \u65B0\u5EFA\u4F1A\u8BDD</button><button id=\"open-settings\" class=\"icon\" title=\"API \u8BBE\u7F6E\">\u2699</button></div>\n <div id=\"session-list\" class=\"session-list\"></div>\n <div class=\"sidebar-foot\"><span>127.0.0.1</span><span id=\"server-port\">28080</span></div>\n </aside>\n <main class=\"main\">\n <header class=\"topbar\"><div class=\"session-heading\"><h1 id=\"session-title\">DeepCCC</h1><p id=\"session-cwd\">\u9009\u62E9\u6216\u521B\u5EFA\u4E00\u4E2A\u4F1A\u8BDD</p></div><div class=\"top-actions\"><button id=\"rename-session\" class=\"ghost hidden\">\u91CD\u547D\u540D</button><button id=\"delete-session\" class=\"danger hidden\">\u5220\u9664</button></div></header>\n <div id=\"warning\" class=\"workspace-warning hidden\"></div>\n <section id=\"content\" class=\"content\"><div class=\"welcome\"><div class=\"welcome-card\"><div class=\"welcome-mark\">\u2726</div><h2>\u628A\u5DE5\u4F5C\u4EA4\u7ED9 DeepCCC</h2><p>\u5728\u6D4F\u89C8\u5668\u91CC\u7BA1\u7406\u591A\u4E2A\u672C\u5730 Coding Agent \u4F1A\u8BDD\uFF0C\u5207\u6362\u6A21\u578B\u4E0E\u63A8\u7406\u5F3A\u5EA6\uFF0C\u5E76\u5728\u4F1A\u8BDD\u5185\u5BA1\u6279\u64CD\u4F5C\u3002</p><div class=\"hint-grid\"><div class=\"hint\">\u591A\u4F1A\u8BDD\u5E76\u884C\u8FD0\u884C</div><div class=\"hint\">\u672C\u5730\u6587\u4EF6\u4E0E\u5386\u53F2</div><div class=\"hint\">API \u81EA\u7531\u914D\u7F6E</div></div></div></div></section>\n </main>\n</div>\n<div id=\"composer-shell\" class=\"composer-shell hidden\"><div class=\"composer\"><textarea id=\"prompt\" placeholder=\"\u63CF\u8FF0\u4F60\u60F3\u5B8C\u6210\u7684\u4EFB\u52A1\u2026\"></textarea><div class=\"composer-bar\"><input id=\"session-model\" list=\"recent-models\" aria-label=\"\u6A21\u578B\"><datalist id=\"recent-models\"></datalist><select id=\"session-effort\" aria-label=\"\u63A8\u7406\u5F3A\u5EA6\"><option value=\"\">\u9ED8\u8BA4 effort</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select><button id=\"send\" class=\"send\" title=\"\u53D1\u9001\">\u2191</button><button id=\"stop\" class=\"stop hidden\">\u25A0 \u505C\u6B62</button></div></div></div>\n\n<div id=\"new-modal\" class=\"modal-backdrop hidden\"><form id=\"new-form\" class=\"modal\"><h2>\u65B0\u5EFA Agent \u4F1A\u8BDD</h2><p class=\"subtitle\">\u6BCF\u4E2A\u4F1A\u8BDD\u62E5\u6709\u72EC\u7ACB\u4E0A\u4E0B\u6587\uFF1B\u540C\u76EE\u5F55\u4F1A\u8BDD\u53EF\u4EE5\u5E76\u53D1\uFF0C\u4F46\u6587\u4EF6\u4FEE\u6539\u53EF\u80FD\u51B2\u7A81\u3002</p><div class=\"field\"><label>\u5DE5\u4F5C\u76EE\u5F55</label><input id=\"new-cwd\" required></div><div class=\"field\"><label>\u4F1A\u8BDD\u540D\u79F0\uFF08\u53EF\u9009\uFF09</label><input id=\"new-title\" placeholder=\"\u9ED8\u8BA4\u4F7F\u7528\u76EE\u5F55\u540D\"></div><div class=\"row\"><div class=\"field\"><label>\u6A21\u578B</label><input id=\"new-model\" list=\"recent-models\"></div><div class=\"field\"><label>Effort</label><select id=\"new-effort\"><option value=\"\">\u9ED8\u8BA4</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"new-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\" type=\"submit\">\u521B\u5EFA\u4F1A\u8BDD</button></div></form></div>\n\n<div id=\"settings-modal\" class=\"modal-backdrop hidden\"><form id=\"settings-form\" class=\"modal\"><h2>API \u4E0E Web \u8BBE\u7F6E</h2><p class=\"subtitle\">\u5355\u4E00 API \u914D\u7F6E\u4F5C\u4E3A\u6240\u6709\u65B0\u4F1A\u8BDD\u7684\u9ED8\u8BA4\u503C\uFF1B\u4F1A\u8BDD\u53EF\u5355\u72EC\u8986\u76D6\u6A21\u578B\u548C effort\u3002</p><div class=\"row\"><div class=\"field\"><label>API \u534F\u8BAE</label><select id=\"cfg-provider\"><option value=\"openai\">OpenAI-compatible</option><option value=\"anthropic\">Anthropic Messages</option></select></div><div class=\"field\"><label>API Key</label><input id=\"cfg-api-key\" type=\"password\" placeholder=\"\u7559\u7A7A\u4FDD\u7559\u5F53\u524D Key\"></div></div><div class=\"field\"><label>Base URL</label><input id=\"cfg-base-url\" required></div><div class=\"row\"><div class=\"field\"><label>\u9ED8\u8BA4\u6A21\u578B</label><input id=\"cfg-model\" required></div><div class=\"field\"><label>\u5B50\u6A21\u578B\uFF08\u53EF\u9009\uFF09</label><input id=\"cfg-sub-model\"></div></div><div class=\"row\"><div class=\"field\"><label>\u9ED8\u8BA4 Effort</label><select id=\"cfg-effort\"><option value=\"\">\u4E0D\u4F20</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div><div class=\"field\"><label>Context Window</label><input id=\"cfg-context\" type=\"number\" min=\"1\"></div></div><div class=\"row\"><div class=\"field\"><label>Web \u7AEF\u53E3</label><input id=\"cfg-port\" type=\"number\" min=\"1\" max=\"65535\"></div><div class=\"field\"><label>Max Output Tokens\uFF08\u53EF\u9009\uFF09</label><input id=\"cfg-max-output\" type=\"number\" min=\"1\"></div></div><label class=\"check\"><input id=\"cfg-streaming\" type=\"checkbox\">\u542F\u7528\u6D41\u5F0F\u8BF7\u6C42</label><label class=\"check\"><input id=\"cfg-open\" type=\"checkbox\">\u8FD0\u884C deepccc web \u65F6\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668</label><div id=\"key-state\" class=\"subtitle\"></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"settings-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\" type=\"submit\">\u4FDD\u5B58\u8BBE\u7F6E</button></div></form></div>\n\n<div id=\"rename-modal\" class=\"modal-backdrop hidden\"><form id=\"rename-form\" class=\"modal\"><h2>\u91CD\u547D\u540D\u4F1A\u8BDD</h2><div class=\"field\"><label>\u540D\u79F0</label><input id=\"rename-title\" required maxlength=\"120\"></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"rename-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\">\u4FDD\u5B58</button></div></form></div>\n\n<div id=\"toast\" class=\"toast hidden\"></div>\n<script>\n(function(){\n var state={sessions:[],selectedId:null,detail:null,config:null,eventSource:null,reloadTimer:null};\n var $=function(id){return document.getElementById(id);};\n async function api(path,options){var response=await fetch(path,Object.assign({headers:{'content-type':'application/json; charset=utf-8'}},options||{}));var data=await response.json().catch(function(){return {error:'\u54CD\u5E94\u4E0D\u662F\u6709\u6548 JSON'};});if(!response.ok)throw new Error(data.error||('HTTP '+response.status));return data;}\n function toast(message,error){var el=$('toast');el.textContent=message;el.className='toast'+(error?' error':'');clearTimeout(toast.timer);toast.timer=setTimeout(function(){el.className='toast hidden';},3500);}\n function openModal(id){$(id).classList.remove('hidden');}function closeModal(id){$(id).classList.add('hidden');}\n function escapeHtml(value){return String(value||'').replace(/[&<>\"']/g,function(ch){return {'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[ch];});}\n function markdown(value){var text=escapeHtml(value);var blocks=[];var tick=String.fromCharCode(96);var fence=tick+tick+tick;var pattern=new RegExp(fence+'([a-z0-9_-]*)\\\\n([\\\\s\\\\S]*?)'+fence,'gi');text=text.replace(pattern,function(_,lang,code){blocks.push('<pre><code data-lang=\"'+lang+'\">'+code+'</code></pre>');return '@@BLOCK'+(blocks.length-1)+'@@';});text=text.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>').replace(new RegExp(tick+'([^'+tick+']+)'+tick,'g'),'<code>$1</code>').replace(/\\n/g,'<br>');text=text.replace(/@@BLOCK(\\d+)@@/g,function(_,index){return blocks[Number(index)];});return text;}\n function statusText(status){return status==='running'?'\u6267\u884C\u4E2D':'\u7A7A\u95F2';}\n function renderSessions(){var root=$('session-list');root.innerHTML='';if(!state.sessions.length){root.innerHTML='<div class=\"session-empty\">\u8FD8\u6CA1\u6709\u4F1A\u8BDD<br>\u70B9\u51FB\u4E0A\u65B9\u6309\u94AE\u5F00\u59CB</div>';return;}state.sessions.forEach(function(session){var button=document.createElement('button');button.className='session-item'+(session.sessionId===state.selectedId?' active':'');button.innerHTML='<div class=\"session-title\"><i class=\"status-dot '+session.status+'\"></i><span>'+escapeHtml(session.title)+'</span></div><div class=\"session-meta\">'+escapeHtml(session.model||'\u9ED8\u8BA4\u6A21\u578B')+' \u00B7 '+escapeHtml(session.effort||'\u9ED8\u8BA4 effort')+'</div><div class=\"session-meta\">'+escapeHtml(session.cwd)+'</div>';button.addEventListener('click',function(){selectSession(session.sessionId);});root.appendChild(button);});}\n function messageHtml(role,content){return '<article class=\"message '+role+'\"><div class=\"avatar\">'+(role==='user'?'\u4F60':'D')+'</div><div class=\"bubble\"><div class=\"role\">'+(role==='user'?'YOU':'DEEPCCC')+'</div><div class=\"markdown\">'+markdown(content)+'</div></div></article>';}\n function toolHtml(event){var data=event.data||{};var item=data.type==='agent'?data.data:data;return '<details class=\"tool-card\"><summary>'+escapeHtml(item.type==='tool_use'?'\u8C03\u7528\u5DE5\u5177 \u00B7 '+(item.name||'tool'):'\u5DE5\u5177\u7ED3\u679C \u00B7 '+(item.name||item.tool_use_id||'tool'))+'</summary><pre>'+escapeHtml(JSON.stringify(item.type==='tool_use'?item.input:item.content,null,2))+'</pre></details>';}\n function approvalAnswerLabel(answer){var labels={allow:'\u5DF2\u5141\u8BB8\u4E00\u6B21','allow-session':'\u672C\u4F1A\u8BDD\u5DF2\u5141\u8BB8','allow-always':'\u5DF2\u6C38\u4E45\u5141\u8BB8',deny:'\u5DF2\u62D2\u7EDD'};return labels[answer]||'\u7B49\u5F85\u51B3\u5B9A';}\n function approvalHtml(approval){var resolved=approval.status==='resolved';var html='<section class=\"approval-card'+(resolved?' resolved':'')+'\"><div class=\"approval-head\"><strong>\u26A0 \u64CD\u4F5C\u5BA1\u6279</strong><span class=\"approval-status\">'+escapeHtml(resolved?approvalAnswerLabel(approval.answer):'\u7B49\u5F85\u6279\u51C6')+'</span></div><div class=\"approval-detail\">'+escapeHtml(approval.detail||('\u5DE5\u5177\uFF1A'+approval.tool))+'</div><pre class=\"approval-command\">'+escapeHtml(approval.action)+'</pre>';if(!resolved)html+='<div class=\"approval-inline-actions\"><button class=\"danger\" data-approval-answer=\"deny\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u62D2\u7EDD</button><button class=\"ghost\" data-approval-answer=\"allow\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u5141\u8BB8\u4E00\u6B21</button><button class=\"ghost\" data-approval-answer=\"allow-session\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u672C\u4F1A\u8BDD\u5141\u8BB8</button><button class=\"primary\" data-approval-answer=\"allow-always\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u6C38\u4E45\u5141\u8BB8</button></div>';return html+'</section>';}\n function renderDetail(){var detail=state.detail;if(!detail)return;$('session-title').textContent=detail.title;$('session-cwd').textContent=detail.cwd+' \u00B7 '+statusText(detail.status);$('rename-session').classList.remove('hidden');$('delete-session').classList.remove('hidden');$('composer-shell').classList.remove('hidden');$('session-model').value=detail.model||'';$('session-effort').value=detail.effort||'';var html='<div class=\"thread\">';if(!(detail.messages||[]).length&&detail.status!=='running')html+='<div class=\"empty-thread\"><strong>\u51C6\u5907\u5F00\u59CB\u8FD9\u4E2A\u4F1A\u8BDD</strong><span>\u5728\u4E0B\u65B9\u8F93\u5165\u4EFB\u52A1\u3002DeepCCC \u4F1A\u8BFB\u53D6\u5F53\u524D\u9879\u76EE\u6307\u4EE4\u3001\u8C03\u7528\u5DE5\u5177\u5E76\u6301\u7EED\u4FDD\u5B58\u4E0A\u4E0B\u6587\u3002</span></div>';(detail.messages||[]).forEach(function(message){html+=messageHtml(message.role,message.content);});if(detail.status==='running'){var live=(detail.events||[]).slice().reverse().find(function(event){return event.type==='agent'&&event.data&&event.data.type==='text';});var current=(detail.events||[]).filter(function(event){return event.type==='agent'&&event.data&&(event.data.type==='tool_use'||event.data.type==='tool_result');});current.forEach(function(event){html+=toolHtml(event);});if(live)html+=messageHtml('assistant',live.data.accumulated||live.data.text||'');}var approvals=(detail.approvals||[]).slice();if(detail.pendingApproval&&!approvals.some(function(item){return item.approvalId===detail.pendingApproval.approvalId;}))approvals.push(Object.assign({status:'pending'},detail.pendingApproval));approvals.forEach(function(approval){html+=approvalHtml(approval);});if(detail.status==='running')html+='<div class=\"run-state\"><i class=\"pulse\"></i>Agent \u6B63\u5728\u5DE5\u4F5C</div>';html+='</div>';$('content').innerHTML=html;$('send').classList.toggle('hidden',detail.status==='running');$('stop').classList.toggle('hidden',detail.status!=='running');$('prompt').disabled=detail.status==='running';$('session-model').disabled=detail.status==='running';$('session-effort').disabled=detail.status==='running';renderWarning();requestAnimationFrame(function(){$('content').scrollTop=$('content').scrollHeight;});}\n function renderWarning(){if(!state.detail)return;var conflicts=state.sessions.filter(function(item){return item.sessionId!==state.detail.sessionId&&item.status==='running'&&item.cwd===state.detail.cwd;});var el=$('warning');if(conflicts.length){el.textContent='\u26A0 \u5F53\u524D\u76EE\u5F55\u8FD8\u6709 '+conflicts.length+' \u4E2A Agent \u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u3002\u591A\u4E2A\u4F1A\u8BDD\u76F4\u63A5\u4FEE\u6539\u540C\u4E00\u76EE\u5F55\uFF0C\u53EF\u80FD\u4EA7\u751F\u8986\u76D6\u6216\u51B2\u7A81\u3002';el.classList.remove('hidden');}else el.classList.add('hidden');}\n async function loadSessions(){var result=await api('/api/sessions');state.sessions=result.sessions||[];renderSessions();if(state.selectedId&&!state.sessions.some(function(item){return item.sessionId===state.selectedId;}))state.selectedId=null;}\n async function loadDetail(){if(!state.selectedId)return;var result=await api('/api/sessions/'+encodeURIComponent(state.selectedId));state.detail=result.session;renderDetail();await loadSessions();}\n function scheduleDetail(){clearTimeout(state.reloadTimer);state.reloadTimer=setTimeout(function(){loadDetail().catch(function(error){toast(error.message,true);});},120);}\n async function selectSession(id){state.selectedId=id;renderSessions();if(state.eventSource)state.eventSource.close();state.eventSource=new EventSource('/api/sessions/'+encodeURIComponent(id)+'/events');state.eventSource.onmessage=scheduleDetail;await loadDetail();}\n async function loadConfig(){var result=await api('/api/config');state.config=result.config;$('cfg-provider').value=state.config.provider;$('cfg-base-url').value=state.config.baseURL;$('cfg-model').value=state.config.model;$('cfg-sub-model').value=state.config.subModel||'';$('cfg-effort').value=state.config.effort||'';$('cfg-context').value=state.config.contextWindow;$('cfg-max-output').value=state.config.maxOutputTokens||'';$('cfg-streaming').checked=state.config.streaming!==false;$('cfg-port').value=state.config.web.port;$('cfg-open').checked=state.config.web.openOnStart!==false;$('server-port').textContent=state.config.web.port;$('key-state').textContent=state.config.apiKeyConfigured?'\u5F53\u524D API Key\uFF1A'+state.config.apiKeyMask:'\u5C1A\u672A\u914D\u7F6E API Key';$('new-cwd').value=state.config.defaultCwd||'';$('new-model').value=state.config.model;var models=[state.config.model].concat(state.sessions.map(function(item){return item.model;})).filter(Boolean).filter(function(value,index,array){return array.indexOf(value)===index;});$('recent-models').innerHTML=models.map(function(model){return '<option value=\"'+escapeHtml(model)+'\">';}).join('');}\n $('new-session').addEventListener('click',function(){openModal('new-modal');setTimeout(function(){$('new-cwd').focus();},0);});$('open-settings').addEventListener('click',function(){loadConfig().then(function(){openModal('settings-modal');});});document.querySelectorAll('[data-close]').forEach(function(button){button.addEventListener('click',function(){closeModal(button.dataset.close);});});\n $('new-form').addEventListener('submit',async function(event){event.preventDefault();try{var result=await api('/api/sessions',{method:'POST',body:JSON.stringify({cwd:$('new-cwd').value,title:$('new-title').value,model:$('new-model').value,effort:$('new-effort').value})});closeModal('new-modal');$('new-title').value='';await loadSessions();await selectSession(result.session.sessionId);}catch(error){toast(error.message,true);}});\n $('settings-form').addEventListener('submit',async function(event){event.preventDefault();var body={provider:$('cfg-provider').value,baseURL:$('cfg-base-url').value,model:$('cfg-model').value,subModel:$('cfg-sub-model').value,effort:$('cfg-effort').value,contextWindow:Number($('cfg-context').value),streaming:$('cfg-streaming').checked,web:{port:Number($('cfg-port').value),openOnStart:$('cfg-open').checked}};if($('cfg-max-output').value)body.maxOutputTokens=Number($('cfg-max-output').value);if($('cfg-api-key').value)body.apiKey=$('cfg-api-key').value;try{await api('/api/config',{method:'PUT',body:JSON.stringify(body)});$('cfg-api-key').value='';closeModal('settings-modal');toast('\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u65B0\u914D\u7F6E\u4ECE\u4E0B\u4E00\u6B21\u65B0\u5EFA\u4F1A\u8BDD\u751F\u6548');await loadConfig();}catch(error){toast(error.message,true);}});\n async function send(){if(!state.selectedId||!$('prompt').value.trim())return;try{if($('session-model').value!==state.detail.model||$('session-effort').value!==state.detail.effort){await api('/api/sessions/'+encodeURIComponent(state.selectedId),{method:'PATCH',body:JSON.stringify({model:$('session-model').value,effort:$('session-effort').value})});}var text=$('prompt').value;$('prompt').value='';await api('/api/sessions/'+encodeURIComponent(state.selectedId)+'/messages',{method:'POST',body:JSON.stringify({text:text})});await loadDetail();}catch(error){toast(error.message,true);}}\n $('send').addEventListener('click',send);$('prompt').addEventListener('keydown',function(event){if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send();}});$('stop').addEventListener('click',async function(){try{await api('/api/sessions/'+encodeURIComponent(state.selectedId)+'/stop',{method:'POST',body:'{}'});toast('\u5DF2\u8BF7\u6C42\u505C\u6B62');}catch(error){toast(error.message,true);}});\n $('rename-session').addEventListener('click',function(){$('rename-title').value=state.detail.title;openModal('rename-modal');});$('rename-form').addEventListener('submit',async function(event){event.preventDefault();try{await api('/api/sessions/'+encodeURIComponent(state.selectedId),{method:'PATCH',body:JSON.stringify({title:$('rename-title').value})});closeModal('rename-modal');await loadDetail();}catch(error){toast(error.message,true);}});$('delete-session').addEventListener('click',async function(){if(!confirm('\u5220\u9664\u8FD9\u4E2A\u4F1A\u8BDD\u53CA\u5176\u672C\u5730\u5386\u53F2\uFF1F\u4E0D\u4F1A\u5220\u9664\u5DE5\u4F5C\u76EE\u5F55\u4E2D\u7684\u6587\u4EF6\u3002'))return;try{await api('/api/sessions/'+encodeURIComponent(state.selectedId),{method:'DELETE'});if(state.eventSource)state.eventSource.close();state.selectedId=null;state.detail=null;$('composer-shell').classList.add('hidden');$('rename-session').classList.add('hidden');$('delete-session').classList.add('hidden');$('session-title').textContent='DeepCCC';$('session-cwd').textContent='\u9009\u62E9\u6216\u521B\u5EFA\u4E00\u4E2A\u4F1A\u8BDD';$('content').innerHTML='<div class=\"welcome\"><div class=\"welcome-card\"><div class=\"welcome-mark\">\u2726</div><h2>\u4F1A\u8BDD\u5DF2\u5220\u9664</h2><p>\u4ECE\u5DE6\u4FA7\u9009\u62E9\u5176\u4ED6\u4F1A\u8BDD\u6216\u521B\u5EFA\u4E00\u4E2A\u65B0\u7684\u4EFB\u52A1\u3002</p></div></div>';await loadSessions();}catch(error){toast(error.message,true);}});\n $('content').addEventListener('click',async function(event){var button=event.target.closest('[data-approval-answer]');if(!button)return;button.disabled=true;try{await api('/api/approvals/'+encodeURIComponent(button.dataset.approvalId),{method:'POST',body:JSON.stringify({answer:button.dataset.approvalAnswer})});await loadDetail();}catch(error){button.disabled=false;toast(error.message,true);}});\n Promise.all([loadSessions(),loadConfig()]).then(function(){if(state.sessions[0])selectSession(state.sessions[0].sessionId);}).catch(function(error){toast(error.message,true);});\n})();\n</script>\n</body></html>";
|
|
1
|
+
export declare const DEEPCCC_WEB_PAGE = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>DeepCCC</title>\n<style>\n:root{color-scheme:light;--bg:#f5f7fb;--panel:#fff;--panel2:#fff;--soft:#f8f9fc;--line:#e3e7ef;--text:#182033;--muted:#737d91;--accent:#6758e8;--accent-soft:#eeeaff;--accent2:#23825d;--danger:#c2415a;--warning:#b7791f;--shadow:0 8px 28px rgba(26,35,58,.08)}\n*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Microsoft YaHei\",sans-serif;overflow:hidden}button,input,select,textarea{font:inherit}button{cursor:pointer}.app{height:100%;display:grid;grid-template-columns:286px minmax(0,1fr)}\n.sidebar{min-width:0;background:var(--panel);border-right:1px solid var(--line);display:flex;flex-direction:column}.brand{height:70px;display:flex;align-items:center;gap:12px;padding:0 18px;border-bottom:1px solid var(--line)}.mark{width:36px;height:36px;border-radius:12px;display:grid;place-items:center;background:var(--accent);color:#fff;box-shadow:0 9px 25px rgba(103,88,232,.25);font-weight:900}.brand-copy strong{display:block;font-size:16px;letter-spacing:.01em}.brand-copy span{font-size:10px;color:var(--muted);letter-spacing:.13em}.sidebar-actions{display:flex;gap:8px;padding:14px}.primary,.ghost,.icon{border:1px solid var(--line);border-radius:10px;color:var(--text);background:var(--panel);font-weight:700}.primary{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:0 8px 22px rgba(103,88,232,.22)}.sidebar-actions .primary{flex:1;padding:10px}.icon{width:40px;padding:0;font-size:17px}.ghost{padding:9px 12px}.primary:hover,.ghost:hover,.icon:hover{filter:brightness(1.03)}\n.session-list{flex:1;overflow:auto;padding:0 10px 14px}.session-empty{padding:38px 18px;color:var(--muted);text-align:center}.session-item{width:100%;display:block;padding:12px;border:1px solid transparent;border-radius:12px;background:transparent;color:var(--text);text-align:left;margin:3px 0}.session-item:hover{background:var(--panel2)}.session-item.active{background:linear-gradient(135deg,rgba(124,105,241,.2),rgba(93,74,207,.08));border-color:rgba(139,124,246,.35)}.session-title{display:flex;align-items:center;gap:8px;font-weight:750}.status-dot{width:7px;height:7px;border-radius:50%;background:#626a78}.status-dot.running{background:var(--accent2);box-shadow:0 0 0 4px rgba(94,228,177,.1)}.session-meta{margin-top:5px;color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-foot{padding:12px 16px;border-top:1px solid var(--line);color:var(--muted);font-size:10px;display:flex;justify-content:space-between}\n.main{min-width:0;height:100%;display:flex;flex-direction:column}.topbar{height:70px;flex:none;display:flex;align-items:center;justify-content:space-between;padding:0 22px;border-bottom:1px solid var(--line);background:rgba(12,14,19,.7);backdrop-filter:blur(18px)}.session-heading{min-width:0}.session-heading h1{font-size:16px;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.session-heading p{font-size:11px;color:var(--muted);margin:3px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.top-actions{display:flex;gap:8px}.top-actions button{height:38px}.content{flex:1;min-height:0;overflow-y:auto;scroll-behavior:smooth}.welcome{height:100%;display:grid;place-items:center;padding:30px}.welcome-card{max-width:650px;text-align:center}.welcome-mark{width:72px;height:72px;margin:0 auto 20px;border-radius:24px;display:grid;place-items:center;background:linear-gradient(135deg,#7764eb,#b45fd7);font-size:29px;box-shadow:0 20px 50px rgba(119,100,235,.28)}.welcome h2{font-size:28px;margin:0 0 10px;letter-spacing:-.02em}.welcome p{color:var(--muted);margin:0 auto;max-width:480px}.hint-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:28px}.hint{padding:14px;border:1px solid var(--line);border-radius:13px;background:rgba(24,28,37,.65);font-size:12px;color:var(--muted)}\n.thread{width:min(900px,calc(100% - 36px));margin:0 auto;padding:28px 0 150px}.empty-thread{margin:80px auto 0;max-width:520px;padding:28px;border:1px dashed #353c4a;border-radius:18px;background:rgba(20,23,31,.55);text-align:center}.empty-thread strong{display:block;font-size:18px;margin-bottom:7px}.empty-thread span{color:var(--muted);font-size:12px}.message{display:grid;grid-template-columns:30px minmax(0,1fr);gap:12px;margin:0 0 22px}.avatar{width:30px;height:30px;border-radius:10px;display:grid;place-items:center;background:var(--soft);font-size:11px;font-weight:900}.message.user .avatar{background:#304058}.message.assistant .avatar{background:linear-gradient(135deg,#7363e8,#9a61dc)}.bubble{min-width:0;padding:2px 0;color:#dfe3eb}.bubble .role{font-size:11px;color:var(--muted);font-weight:800;margin-bottom:7px}.markdown{font-size:14px;line-height:1.75;overflow-wrap:anywhere}.markdown p{margin:.35em 0}.markdown pre{padding:14px;border:1px solid var(--line);border-radius:11px;background:#0a0c11;overflow:auto;font:12px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace}.markdown code{padding:2px 5px;border-radius:5px;background:var(--soft);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.markdown pre code{padding:0;background:transparent}.tool-card{margin:8px 0;border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:hidden}.tool-card summary{padding:9px 11px;color:#bbc2d0;font-size:12px;font-weight:700}.tool-card pre{margin:0;border:0;border-top:1px solid var(--line);border-radius:0;max-height:260px}.run-state{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px;margin:8px 0 20px}.pulse{width:8px;height:8px;border-radius:50%;background:var(--accent2);animation:pulse 1.4s infinite}@keyframes pulse{50%{opacity:.35;transform:scale(.72)}}.workspace-warning{width:min(900px,calc(100% - 36px));margin:14px auto 0;padding:9px 12px;border:1px solid rgba(242,184,75,.3);background:rgba(242,184,75,.08);border-radius:10px;color:#e8c879;font-size:11px}\n.composer-shell{position:absolute;left:286px;right:0;bottom:0;padding:0 20px 18px;background:linear-gradient(transparent,var(--bg) 32%)}.composer{width:min(900px,100%);margin:auto;border:1px solid #343b49;border-radius:17px;background:rgba(25,29,38,.96);box-shadow:var(--shadow);overflow:hidden}.composer textarea{width:100%;min-height:68px;max-height:180px;resize:none;border:0;outline:0;padding:15px 16px 8px;background:transparent;color:var(--text)}.composer textarea::placeholder{color:#737b8b}.composer-bar{display:flex;align-items:center;gap:8px;padding:7px 9px}.composer-bar input,.composer-bar select{min-width:0;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--muted);padding:7px 9px;font-size:11px}.composer-bar input{width:180px}.composer-bar select{width:104px}.send{margin-left:auto;width:36px;height:36px;border:0;border-radius:10px;background:var(--accent);color:#fff;font-size:17px}.stop{margin-left:auto;height:36px;border:1px solid rgba(255,111,137,.4);border-radius:10px;background:rgba(255,111,137,.12);color:#ff91a5;padding:0 13px;font-weight:700}.composer-disabled{opacity:.5;pointer-events:none}\n.modal-backdrop{position:fixed;inset:0;z-index:30;display:grid;place-items:center;padding:20px;background:rgba(4,5,8,.66);backdrop-filter:blur(7px)}.modal-backdrop.hidden{display:none}.modal{width:min(560px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid #353c4a;border-radius:18px;background:#171a22;box-shadow:0 30px 100px rgba(0,0,0,.55);padding:22px}.modal h2{margin:0 0 5px;font-size:20px}.modal>.subtitle{margin:0 0 20px;color:var(--muted);font-size:12px}.field{display:grid;gap:7px;margin:13px 0}.field label{font-size:11px;color:#b7becb;font-weight:800}.field input,.field select{width:100%;border:1px solid var(--line);border-radius:10px;background:#10131a;color:var(--text);padding:10px 11px;outline:none}.field input:focus,.field select:focus{border-color:var(--accent)}.row{display:grid;grid-template-columns:1fr 1fr;gap:10px}.check{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px}.modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.danger{border:1px solid rgba(255,111,137,.35);background:rgba(255,111,137,.1);color:#ff849b;border-radius:10px;padding:9px 12px;font-weight:700}.approval{width:min(620px,100%)}.approval-badge{display:inline-flex;padding:5px 8px;border-radius:999px;background:rgba(242,184,75,.13);color:#f1c565;font-size:10px;font-weight:900}.approval pre{white-space:pre-wrap;overflow-wrap:anywhere;padding:13px;border:1px solid var(--line);border-radius:10px;background:#0d0f15;color:#dce0e9}.approval-actions{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.toast{position:fixed;right:20px;top:20px;z-index:50;max-width:420px;padding:11px 14px;border-radius:10px;background:#283143;border:1px solid #3b465b;box-shadow:var(--shadow)}.toast.error{background:#602536;border-color:#913a52}.hidden{display:none!important}\n.topbar{background:rgba(255,255,255,.86)}.welcome-mark{background:var(--accent);color:#fff}.hint,.empty-thread{background:var(--panel);border-color:var(--line)}.session-item.active{background:var(--accent-soft);border-color:#d8d1ff}.status-dot{background:#a3aabd}.status-dot.running{background:var(--accent2);box-shadow:0 0 0 4px rgba(35,130,93,.1)}.message.user .avatar{background:#e8edf6}.message.assistant .avatar{background:var(--accent);color:#fff}.bubble{color:var(--text)}.markdown pre,.tool-card pre{background:var(--soft);color:var(--text)}.tool-card summary{color:var(--muted)}.composer{border-color:var(--line);background:rgba(255,255,255,.98)}.composer textarea::placeholder{color:#9aa3b4}.composer-bar input,.composer-bar select{background:var(--soft)}.modal-backdrop{background:rgba(24,32,51,.36)}.modal{border-color:var(--line);background:var(--panel);box-shadow:0 28px 90px rgba(35,43,65,.24)}.field label{color:var(--text)}.field input,.field select{background:var(--soft);color:var(--text)}.danger{border-color:#f3c4ce;background:#fff1f3;color:var(--danger)}.toast{background:#253047;color:#fff}.toast.error{background:#9f2942}.approval-card{margin:16px 0;padding:14px;border:1px solid #ead49b;border-radius:13px;background:#fffaf0;box-shadow:0 5px 18px rgba(94,70,15,.06)}.approval-card.resolved{border-color:var(--line);background:var(--soft)}.approval-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}.approval-head strong{font-size:12px}.approval-status{padding:3px 7px;border-radius:999px;background:#fff0c2;color:#8a6110;font-size:10px;font-weight:800}.approval-card.resolved .approval-status{background:#e5f5ee;color:var(--accent2)}.approval-detail{color:var(--muted);font-size:11px;margin-bottom:8px}.approval-command{margin:0;padding:10px;border:1px solid var(--line);border-radius:9px;background:#fff;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}.approval-inline-actions{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px}.approval-inline-actions button{padding:7px 10px;font-size:11px}\n.sidebar{height:100%;overflow:hidden}.main{min-height:0!important;overflow:hidden}\n@media(max-width:760px){.app{grid-template-columns:78px minmax(0,1fr)}.brand{padding:0;justify-content:center}.brand-copy,.sidebar-actions .primary,.session-item>div:not(.session-title),.session-title span:last-child,.sidebar-foot{display:none}.sidebar-actions{justify-content:center;padding:12px 8px}.session-item{padding:11px;display:grid;place-items:center}.composer-shell{left:78px}.hint-grid{grid-template-columns:1fr}.topbar{padding:0 14px}.thread{width:calc(100% - 24px)}.row{grid-template-columns:1fr}.approval-inline-actions{display:grid;grid-template-columns:1fr 1fr}}\n</style>\n</head>\n<body>\n<div class=\"app\">\n <aside class=\"sidebar\">\n <div class=\"brand\"><div class=\"mark\">D</div><div class=\"brand-copy\"><strong>DeepCCC</strong><span>LOCAL AGENT</span></div></div>\n <div class=\"sidebar-actions\"><button id=\"new-session\" class=\"primary\">\uFF0B \u65B0\u5EFA\u4F1A\u8BDD</button><button id=\"open-settings\" class=\"icon\" title=\"API \u8BBE\u7F6E\">\u2699</button></div>\n <div id=\"session-list\" class=\"session-list\"></div>\n <div class=\"sidebar-foot\"><span>127.0.0.1</span><span id=\"server-port\">28080</span></div>\n </aside>\n <main class=\"main\">\n <header class=\"topbar\"><div class=\"session-heading\"><h1 id=\"session-title\">DeepCCC</h1><p id=\"session-cwd\">\u9009\u62E9\u6216\u521B\u5EFA\u4E00\u4E2A\u4F1A\u8BDD</p></div><div class=\"top-actions\"><button id=\"rename-session\" class=\"ghost hidden\">\u91CD\u547D\u540D</button><button id=\"delete-session\" class=\"danger hidden\">\u5220\u9664</button></div></header>\n <div id=\"warning\" class=\"workspace-warning hidden\"></div>\n <section id=\"content\" class=\"content\"><div class=\"welcome\"><div class=\"welcome-card\"><div class=\"welcome-mark\">\u2726</div><h2>\u628A\u5DE5\u4F5C\u4EA4\u7ED9 DeepCCC</h2><p>\u5728\u6D4F\u89C8\u5668\u91CC\u7BA1\u7406\u591A\u4E2A\u672C\u5730 Coding Agent \u4F1A\u8BDD\uFF0C\u5207\u6362\u6A21\u578B\u4E0E\u63A8\u7406\u5F3A\u5EA6\uFF0C\u5E76\u5728\u4F1A\u8BDD\u5185\u5BA1\u6279\u64CD\u4F5C\u3002</p><div class=\"hint-grid\"><div class=\"hint\">\u591A\u4F1A\u8BDD\u5E76\u884C\u8FD0\u884C</div><div class=\"hint\">\u672C\u5730\u6587\u4EF6\u4E0E\u5386\u53F2</div><div class=\"hint\">API \u81EA\u7531\u914D\u7F6E</div></div></div></div></section>\n </main>\n</div>\n<div id=\"composer-shell\" class=\"composer-shell hidden\"><div class=\"composer\"><textarea id=\"prompt\" placeholder=\"\u63CF\u8FF0\u4F60\u60F3\u5B8C\u6210\u7684\u4EFB\u52A1\u2026\"></textarea><div class=\"composer-bar\"><input id=\"session-model\" list=\"recent-models\" aria-label=\"\u6A21\u578B\"><input id=\"session-sub-model\" list=\"recent-models\" aria-label=\"\u5B50\u6A21\u578B\" placeholder=\"\u9ED8\u8BA4\u5B50\u6A21\u578B\"><datalist id=\"recent-models\"></datalist><select id=\"session-effort\" aria-label=\"\u63A8\u7406\u5F3A\u5EA6\"><option value=\"\">\u9ED8\u8BA4 effort</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select><button id=\"send\" class=\"send\" title=\"\u53D1\u9001\">\u2191</button><button id=\"stop\" class=\"stop hidden\">\u25A0 \u505C\u6B62</button></div></div></div>\n\n<div id=\"new-modal\" class=\"modal-backdrop hidden\"><form id=\"new-form\" class=\"modal\"><h2>\u65B0\u5EFA Agent \u4F1A\u8BDD</h2><p class=\"subtitle\">\u6BCF\u4E2A\u4F1A\u8BDD\u62E5\u6709\u72EC\u7ACB\u4E0A\u4E0B\u6587\uFF1B\u540C\u76EE\u5F55\u4F1A\u8BDD\u53EF\u4EE5\u5E76\u53D1\uFF0C\u4F46\u6587\u4EF6\u4FEE\u6539\u53EF\u80FD\u51B2\u7A81\u3002</p><div class=\"field\"><label>\u5DE5\u4F5C\u76EE\u5F55</label><input id=\"new-cwd\" required></div><div class=\"field\"><label>\u4F1A\u8BDD\u540D\u79F0\uFF08\u53EF\u9009\uFF09</label><input id=\"new-title\" placeholder=\"\u9ED8\u8BA4\u4F7F\u7528\u76EE\u5F55\u540D\"></div><div class=\"row\"><div class=\"field\"><label>\u6A21\u578B</label><input id=\"new-model\" list=\"recent-models\"></div><div class=\"field\"><label>\u5B50\u6A21\u578B\uFF08\u53EF\u9009\uFF09</label><input id=\"new-sub-model\" list=\"recent-models\"></div></div><div class=\"field\"><label>Effort</label><select id=\"new-effort\"><option value=\"\">\u9ED8\u8BA4</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"new-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\" type=\"submit\">\u521B\u5EFA\u4F1A\u8BDD</button></div></form></div>\n\n<div id=\"settings-modal\" class=\"modal-backdrop hidden\"><form id=\"settings-form\" class=\"modal\"><h2>API \u4E0E Web \u8BBE\u7F6E</h2><p class=\"subtitle\">\u5355\u4E00 API \u914D\u7F6E\u4F5C\u4E3A\u6240\u6709\u65B0\u4F1A\u8BDD\u7684\u9ED8\u8BA4\u503C\uFF1B\u4F1A\u8BDD\u53EF\u5355\u72EC\u8986\u76D6\u6A21\u578B\u548C effort\u3002</p><div class=\"row\"><div class=\"field\"><label>API \u534F\u8BAE</label><select id=\"cfg-provider\"><option value=\"openai\">OpenAI-compatible</option><option value=\"anthropic\">Anthropic Messages</option></select></div><div class=\"field\"><label>API Key</label><input id=\"cfg-api-key\" type=\"password\" placeholder=\"\u7559\u7A7A\u4FDD\u7559\u5F53\u524D Key\"></div></div><div class=\"field\"><label>Base URL</label><input id=\"cfg-base-url\" required></div><div class=\"row\"><div class=\"field\"><label>\u9ED8\u8BA4\u6A21\u578B</label><input id=\"cfg-model\" required></div><div class=\"field\"><label>\u5B50\u6A21\u578B\uFF08\u53EF\u9009\uFF09</label><input id=\"cfg-sub-model\"></div></div><div class=\"row\"><div class=\"field\"><label>\u9ED8\u8BA4 Effort</label><select id=\"cfg-effort\"><option value=\"\">\u4E0D\u4F20</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div><div class=\"field\"><label>Context Window</label><input id=\"cfg-context\" type=\"number\" min=\"1\"></div></div><div class=\"row\"><div class=\"field\"><label>Web \u7AEF\u53E3</label><input id=\"cfg-port\" type=\"number\" min=\"1\" max=\"65535\"></div><div class=\"field\"><label>Max Output Tokens\uFF08\u53EF\u9009\uFF09</label><input id=\"cfg-max-output\" type=\"number\" min=\"1\"></div></div><label class=\"check\"><input id=\"cfg-streaming\" type=\"checkbox\">\u542F\u7528\u6D41\u5F0F\u8BF7\u6C42</label><label class=\"check\"><input id=\"cfg-open\" type=\"checkbox\">\u8FD0\u884C deepccc web \u65F6\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668</label><div id=\"key-state\" class=\"subtitle\"></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"settings-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\" type=\"submit\">\u4FDD\u5B58\u8BBE\u7F6E</button></div></form></div>\n\n<div id=\"rename-modal\" class=\"modal-backdrop hidden\"><form id=\"rename-form\" class=\"modal\"><h2>\u91CD\u547D\u540D\u4F1A\u8BDD</h2><div class=\"field\"><label>\u540D\u79F0</label><input id=\"rename-title\" required maxlength=\"120\"></div><div class=\"modal-actions\"><button type=\"button\" data-close=\"rename-modal\" class=\"ghost\">\u53D6\u6D88</button><button class=\"primary\">\u4FDD\u5B58</button></div></form></div>\n\n<div id=\"toast\" class=\"toast hidden\"></div>\n<script>\n(function(){\n var state={sessions:[],selectedId:null,detail:null,config:null,eventSource:null,globalEventSource:null,reloadTimer:null,sessionsReloadTimer:null,detailRequestToken:0,configDrafts:{},promptDrafts:{}};\n var $=function(id){return document.getElementById(id);};\n async function api(path,options){var response=await fetch(path,Object.assign({headers:{'content-type':'application/json; charset=utf-8'}},options||{}));var data=await response.json().catch(function(){return {error:'\u54CD\u5E94\u4E0D\u662F\u6709\u6548 JSON'};});if(!response.ok)throw new Error(data.error||('HTTP '+response.status));return data;}\n function toast(message,error){var el=$('toast');el.textContent=message;el.className='toast'+(error?' error':'');clearTimeout(toast.timer);toast.timer=setTimeout(function(){el.className='toast hidden';},3500);}\n function openModal(id){$(id).classList.remove('hidden');}function closeModal(id){$(id).classList.add('hidden');}\n function escapeHtml(value){return String(value||'').replace(/[&<>\"']/g,function(ch){return {'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[ch];});}\n function markdown(value){var text=escapeHtml(value);var blocks=[];var tick=String.fromCharCode(96);var fence=tick+tick+tick;var pattern=new RegExp(fence+'([a-z0-9_-]*)\\\\n([\\\\s\\\\S]*?)'+fence,'gi');text=text.replace(pattern,function(_,lang,code){blocks.push('<pre><code data-lang=\"'+lang+'\">'+code+'</code></pre>');return '@@BLOCK'+(blocks.length-1)+'@@';});text=text.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>').replace(new RegExp(tick+'([^'+tick+']+)'+tick,'g'),'<code>$1</code>').replace(/\\n/g,'<br>');text=text.replace(/@@BLOCK(\\d+)@@/g,function(_,index){return blocks[Number(index)];});return text;}\n function statusText(status){return status==='running'?'\u6267\u884C\u4E2D':'\u7A7A\u95F2';}\n function renderSessions(){var root=$('session-list');root.innerHTML='';if(!state.sessions.length){root.innerHTML='<div class=\"session-empty\">\u8FD8\u6CA1\u6709\u4F1A\u8BDD<br>\u70B9\u51FB\u4E0A\u65B9\u6309\u94AE\u5F00\u59CB</div>';return;}state.sessions.forEach(function(session){var button=document.createElement('button');button.className='session-item'+(session.sessionId===state.selectedId?' active':'');button.innerHTML='<div class=\"session-title\"><i class=\"status-dot '+session.status+'\"></i><span>'+escapeHtml(session.title)+'</span></div><div class=\"session-meta\">'+escapeHtml(session.model||'\u9ED8\u8BA4\u6A21\u578B')+' \u00B7 '+escapeHtml(session.effort||'\u9ED8\u8BA4 effort')+'</div><div class=\"session-meta\">'+escapeHtml(session.cwd)+'</div>';button.addEventListener('click',function(){selectSession(session.sessionId).catch(function(error){toast(error.message,true);});});root.appendChild(button);});}\n function messageHtml(role,content){return '<article class=\"message '+role+'\"><div class=\"avatar\">'+(role==='user'?'\u4F60':'D')+'</div><div class=\"bubble\"><div class=\"role\">'+(role==='user'?'YOU':'DEEPCCC')+'</div><div class=\"markdown\">'+markdown(content)+'</div></div></article>';}\n function toolHtml(event){var data=event.data||{};var item=data.type==='agent'?data.data:data;return '<details class=\"tool-card\"><summary>'+escapeHtml(item.type==='tool_use'?'\u8C03\u7528\u5DE5\u5177 \u00B7 '+(item.name||'tool'):'\u5DE5\u5177\u7ED3\u679C \u00B7 '+(item.name||item.tool_use_id||'tool'))+'</summary><pre>'+escapeHtml(JSON.stringify(item.type==='tool_use'?item.input:item.content,null,2))+'</pre></details>';}\n function approvalAnswerLabel(answer){var labels={allow:'\u5DF2\u5141\u8BB8\u4E00\u6B21','allow-session':'\u672C\u4F1A\u8BDD\u5DF2\u5141\u8BB8','allow-always':'\u5DF2\u6C38\u4E45\u5141\u8BB8',deny:'\u5DF2\u62D2\u7EDD'};return labels[answer]||'\u7B49\u5F85\u51B3\u5B9A';}\n function approvalHtml(approval){var resolved=approval.status==='resolved';var html='<section class=\"approval-card'+(resolved?' resolved':'')+'\"><div class=\"approval-head\"><strong>\u26A0 \u64CD\u4F5C\u5BA1\u6279</strong><span class=\"approval-status\">'+escapeHtml(resolved?approvalAnswerLabel(approval.answer):'\u7B49\u5F85\u6279\u51C6')+'</span></div><div class=\"approval-detail\">'+escapeHtml(approval.detail||('\u5DE5\u5177\uFF1A'+approval.tool))+'</div><pre class=\"approval-command\">'+escapeHtml(approval.action)+'</pre>';if(!resolved)html+='<div class=\"approval-inline-actions\"><button class=\"danger\" data-approval-answer=\"deny\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u62D2\u7EDD</button><button class=\"ghost\" data-approval-answer=\"allow\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u5141\u8BB8\u4E00\u6B21</button><button class=\"ghost\" data-approval-answer=\"allow-session\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u672C\u4F1A\u8BDD\u5141\u8BB8</button><button class=\"primary\" data-approval-answer=\"allow-always\" data-approval-id=\"'+escapeHtml(approval.approvalId)+'\">\u6C38\u4E45\u5141\u8BB8</button></div>';return html+'</section>';}\n function sessionDraft(detail){return state.configDrafts[detail.sessionId]||{model:detail.model||'',subModel:detail.subModel||'',effort:detail.effort||''};}\n function rememberDraft(){if(!state.selectedId)return;state.configDrafts[state.selectedId]={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};}\n function rememberPromptDraft(){if(!state.selectedId)return;state.promptDrafts[state.selectedId]=$('prompt').value;}\n function clearSelection(message){if(state.eventSource)state.eventSource.close();state.eventSource=null;state.detailRequestToken++;state.selectedId=null;state.detail=null;$('composer-shell').classList.add('hidden');$('rename-session').classList.add('hidden');$('delete-session').classList.add('hidden');$('warning').classList.add('hidden');$('session-title').textContent='DeepCCC';$('session-cwd').textContent='\u9009\u62E9\u6216\u521B\u5EFA\u4E00\u4E2A\u4F1A\u8BDD';$('content').innerHTML='<div class=\"welcome\"><div class=\"welcome-card\"><div class=\"welcome-mark\">\u2726</div><h2>'+escapeHtml(message||'\u9009\u62E9\u4E00\u4E2A\u4F1A\u8BDD')+'</h2><p>\u4ECE\u5DE6\u4FA7\u9009\u62E9\u5176\u4ED6\u4F1A\u8BDD\u6216\u521B\u5EFA\u4E00\u4E2A\u65B0\u7684\u4EFB\u52A1\u3002</p></div></div>';}\n function renderDetail(){var detail=state.detail;if(!detail)return;var draft=sessionDraft(detail);$('session-title').textContent=detail.title;$('session-cwd').textContent=detail.cwd+' \u00B7 '+statusText(detail.status);$('rename-session').classList.remove('hidden');$('delete-session').classList.remove('hidden');$('composer-shell').classList.remove('hidden');$('prompt').value=state.promptDrafts[detail.sessionId]||'';$('session-model').value=draft.model;$('session-sub-model').value=draft.subModel;$('session-effort').value=draft.effort;var html='<div class=\"thread\">';if(!(detail.messages||[]).length&&detail.status!=='running')html+='<div class=\"empty-thread\"><strong>\u51C6\u5907\u5F00\u59CB\u8FD9\u4E2A\u4F1A\u8BDD</strong><span>\u5728\u4E0B\u65B9\u8F93\u5165\u4EFB\u52A1\u3002DeepCCC \u4F1A\u8BFB\u53D6\u5F53\u524D\u9879\u76EE\u6307\u4EE4\u3001\u8C03\u7528\u5DE5\u5177\u5E76\u6301\u7EED\u4FDD\u5B58\u4E0A\u4E0B\u6587\u3002</span></div>';(detail.messages||[]).forEach(function(message){html+=messageHtml(message.role,message.content);});if(detail.status==='running'){var live=(detail.events||[]).slice().reverse().find(function(event){return event.type==='agent'&&event.data&&event.data.type==='text';});var current=(detail.events||[]).filter(function(event){return event.type==='agent'&&event.data&&(event.data.type==='tool_use'||event.data.type==='tool_result');});current.forEach(function(event){html+=toolHtml(event);});if(live)html+=messageHtml('assistant',live.data.accumulated||live.data.text||'');}var approvals=(detail.approvals||[]).slice();if(detail.pendingApproval&&!approvals.some(function(item){return item.approvalId===detail.pendingApproval.approvalId;}))approvals.push(Object.assign({status:'pending'},detail.pendingApproval));approvals.forEach(function(approval){html+=approvalHtml(approval);});if(detail.status==='running')html+='<div class=\"run-state\"><i class=\"pulse\"></i>Agent \u6B63\u5728\u5DE5\u4F5C</div>';html+='</div>';$('content').innerHTML=html;$('send').classList.toggle('hidden',detail.status==='running');$('stop').classList.toggle('hidden',detail.status!=='running');$('prompt').disabled=detail.status==='running';$('session-model').disabled=detail.status==='running';$('session-sub-model').disabled=detail.status==='running';$('session-effort').disabled=detail.status==='running';renderWarning();requestAnimationFrame(function(){$('content').scrollTop=$('content').scrollHeight;});}\n function cwdKey(value){var raw=String(value||'');var normalized=raw.replace(/\\\\/g,'/').replace(new RegExp('/+$'),'');var windowsStyle=new RegExp('^[a-z]:/','i').test(normalized)||raw.indexOf(String.fromCharCode(92))>=0;return windowsStyle?normalized.toLowerCase():normalized;}\n function renderWarning(){if(!state.detail)return;var selectedCwd=cwdKey(state.detail.cwd);var conflicts=state.sessions.filter(function(item){return item.sessionId!==state.detail.sessionId&&item.status==='running'&&cwdKey(item.cwd)===selectedCwd;});var el=$('warning');if(conflicts.length){el.textContent='\u26A0 \u5F53\u524D\u76EE\u5F55\u8FD8\u6709 '+conflicts.length+' \u4E2A Agent \u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u3002\u591A\u4E2A\u4F1A\u8BDD\u76F4\u63A5\u4FEE\u6539\u540C\u4E00\u76EE\u5F55\uFF0C\u53EF\u80FD\u4EA7\u751F\u8986\u76D6\u6216\u51B2\u7A81\u3002';el.classList.remove('hidden');}else el.classList.add('hidden');}\n async function loadSessions(){var result=await api('/api/sessions');state.sessions=result.sessions||[];if(state.selectedId&&!state.sessions.some(function(item){return item.sessionId===state.selectedId;}))clearSelection('\u4F1A\u8BDD\u5DF2\u5728\u5176\u4ED6\u9875\u9762\u5220\u9664');renderSessions();renderWarning();}\n async function loadDetail(id,token){var sessionId=id||state.selectedId;if(!sessionId)return false;var requestToken=token===undefined?++state.detailRequestToken:token;var result=await api('/api/sessions/'+encodeURIComponent(sessionId));if(state.selectedId!==sessionId||requestToken!==state.detailRequestToken)return false;await loadSessions();if(state.selectedId!==sessionId||requestToken!==state.detailRequestToken)return false;state.detail=result.session;renderDetail();return true;}\n function scheduleDetail(id){if(id&&id!==state.selectedId)return;clearTimeout(state.reloadTimer);var token=++state.detailRequestToken;state.reloadTimer=setTimeout(function(){loadDetail(id||state.selectedId,token).catch(function(error){toast(error.message,true);});},120);}\n function scheduleSessions(){clearTimeout(state.sessionsReloadTimer);state.sessionsReloadTimer=setTimeout(function(){loadSessions().catch(function(error){toast(error.message,true);});},80);}\n async function selectSession(id){state.selectedId=id;var token=++state.detailRequestToken;renderSessions();if(state.eventSource)state.eventSource.close();state.eventSource=new EventSource('/api/sessions/'+encodeURIComponent(id)+'/events');state.eventSource.onmessage=function(){scheduleDetail(id);};await loadDetail(id,token);}\n async function loadConfig(){var result=await api('/api/config');state.config=result.config;$('cfg-provider').value=state.config.provider;$('cfg-base-url').value=state.config.baseURL;$('cfg-model').value=state.config.model;$('cfg-sub-model').value=state.config.subModel||'';$('cfg-effort').value=state.config.effort||'';$('cfg-context').value=state.config.contextWindow;$('cfg-max-output').value=state.config.maxOutputTokens||'';$('cfg-streaming').checked=state.config.streaming!==false;$('cfg-port').value=state.config.web.port;$('cfg-open').checked=state.config.web.openOnStart!==false;$('server-port').textContent=state.config.web.port;$('key-state').textContent=state.config.apiKeyConfigured?'\u5F53\u524D API Key\uFF1A'+state.config.apiKeyMask:'\u5C1A\u672A\u914D\u7F6E API Key';$('new-cwd').value=state.config.defaultCwd||'';$('new-model').value=state.config.model;$('new-sub-model').value=state.config.subModel||'';var models=[state.config.model,state.config.subModel].concat(state.sessions.reduce(function(all,item){return all.concat([item.model,item.subModel]);},[])).filter(Boolean).filter(function(value,index,array){return array.indexOf(value)===index;});$('recent-models').innerHTML=models.map(function(model){return '<option value=\"'+escapeHtml(model)+'\">';}).join('');}\n $('new-session').addEventListener('click',function(){openModal('new-modal');setTimeout(function(){$('new-cwd').focus();},0);});$('open-settings').addEventListener('click',function(){loadConfig().then(function(){openModal('settings-modal');}).catch(function(error){toast(error.message,true);});});document.querySelectorAll('[data-close]').forEach(function(button){button.addEventListener('click',function(){closeModal(button.dataset.close);});});\n $('new-form').addEventListener('submit',async function(event){event.preventDefault();try{var result=await api('/api/sessions',{method:'POST',body:JSON.stringify({cwd:$('new-cwd').value,title:$('new-title').value,model:$('new-model').value,subModel:$('new-sub-model').value,effort:$('new-effort').value})});closeModal('new-modal');$('new-title').value='';await loadSessions();await selectSession(result.session.sessionId);}catch(error){toast(error.message,true);}});\n $('settings-form').addEventListener('submit',async function(event){event.preventDefault();var body={provider:$('cfg-provider').value,baseURL:$('cfg-base-url').value,model:$('cfg-model').value,subModel:$('cfg-sub-model').value,effort:$('cfg-effort').value,contextWindow:Number($('cfg-context').value),streaming:$('cfg-streaming').checked,web:{port:Number($('cfg-port').value),openOnStart:$('cfg-open').checked}};if($('cfg-max-output').value)body.maxOutputTokens=Number($('cfg-max-output').value);if($('cfg-api-key').value)body.apiKey=$('cfg-api-key').value;try{await api('/api/config',{method:'PUT',body:JSON.stringify(body)});$('cfg-api-key').value='';closeModal('settings-modal');toast('\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u65B0\u914D\u7F6E\u4ECE\u4E0B\u4E00\u6B21\u65B0\u5EFA\u4F1A\u8BDD\u751F\u6548');await loadConfig();}catch(error){toast(error.message,true);}});\n async function send(){var sessionId=state.selectedId;var text=$('prompt').value;if(!sessionId||!text.trim())return;try{var patch={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};if(patch.model!==state.detail.model||patch.subModel!==state.detail.subModel||patch.effort!==state.detail.effort){var updated=await api('/api/sessions/'+encodeURIComponent(sessionId),{method:'PATCH',body:JSON.stringify(patch)});if(state.selectedId===sessionId){state.detail=Object.assign({},state.detail,updated.session);renderDetail();}await loadSessions();}await api('/api/sessions/'+encodeURIComponent(sessionId)+'/messages',{method:'POST',body:JSON.stringify({text:text})});delete state.configDrafts[sessionId];delete state.promptDrafts[sessionId];if(state.selectedId===sessionId)$('prompt').value='';var token=++state.detailRequestToken;await loadDetail(sessionId,token);}catch(error){toast(error.message,true);}}\n ['session-model','session-sub-model','session-effort'].forEach(function(id){$(id).addEventListener('input',rememberDraft);$(id).addEventListener('change',rememberDraft);});\n $('send').addEventListener('click',send);$('prompt').addEventListener('input',rememberPromptDraft);$('prompt').addEventListener('keydown',function(event){if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send();}});$('stop').addEventListener('click',async function(){try{await api('/api/sessions/'+encodeURIComponent(state.selectedId)+'/stop',{method:'POST',body:'{}'});toast('\u5DF2\u8BF7\u6C42\u505C\u6B62');}catch(error){toast(error.message,true);}});\n $('rename-session').addEventListener('click',function(){$('rename-title').value=state.detail.title;openModal('rename-modal');});$('rename-form').addEventListener('submit',async function(event){event.preventDefault();try{await api('/api/sessions/'+encodeURIComponent(state.selectedId),{method:'PATCH',body:JSON.stringify({title:$('rename-title').value})});closeModal('rename-modal');var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){toast(error.message,true);}});$('delete-session').addEventListener('click',async function(){if(!confirm('\u5220\u9664\u8FD9\u4E2A\u4F1A\u8BDD\u53CA\u5176\u672C\u5730\u5386\u53F2\uFF1F\u4E0D\u4F1A\u5220\u9664\u5DE5\u4F5C\u76EE\u5F55\u4E2D\u7684\u6587\u4EF6\u3002'))return;try{var deletedId=state.selectedId;await api('/api/sessions/'+encodeURIComponent(deletedId),{method:'DELETE'});delete state.configDrafts[deletedId];delete state.promptDrafts[deletedId];clearSelection('\u4F1A\u8BDD\u5DF2\u5220\u9664');await loadSessions();}catch(error){toast(error.message,true);}});\n $('content').addEventListener('click',async function(event){var button=event.target.closest('[data-approval-answer]');if(!button)return;button.disabled=true;try{await api('/api/approvals/'+encodeURIComponent(button.dataset.approvalId),{method:'POST',body:JSON.stringify({answer:button.dataset.approvalAnswer})});var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){button.disabled=false;toast(error.message,true);}});\n function handleGlobalEvent(event){try{var update=JSON.parse(event.data);if(update.type==='session_deleted'){delete state.configDrafts[update.sessionId];delete state.promptDrafts[update.sessionId];}}catch(error){}scheduleSessions();}\n state.globalEventSource=new EventSource('/api/events');state.globalEventSource.onmessage=handleGlobalEvent;\n Promise.all([loadSessions(),loadConfig()]).then(function(){if(state.sessions[0])return selectSession(state.sessions[0].sessionId);}).catch(function(error){toast(error.message,true);});\n})();\n</script>\n</body></html>";
|
package/dist/web-page.js
CHANGED
|
@@ -32,9 +32,9 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
|
|
|
32
32
|
<section id="content" class="content"><div class="welcome"><div class="welcome-card"><div class="welcome-mark">✦</div><h2>把工作交给 DeepCCC</h2><p>在浏览器里管理多个本地 Coding Agent 会话,切换模型与推理强度,并在会话内审批操作。</p><div class="hint-grid"><div class="hint">多会话并行运行</div><div class="hint">本地文件与历史</div><div class="hint">API 自由配置</div></div></div></div></section>
|
|
33
33
|
</main>
|
|
34
34
|
</div>
|
|
35
|
-
<div id="composer-shell" class="composer-shell hidden"><div class="composer"><textarea id="prompt" placeholder="描述你想完成的任务…"></textarea><div class="composer-bar"><input id="session-model" list="recent-models" aria-label="模型"><datalist id="recent-models"></datalist><select id="session-effort" aria-label="推理强度"><option value="">默认 effort</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select><button id="send" class="send" title="发送">↑</button><button id="stop" class="stop hidden">■ 停止</button></div></div></div>
|
|
35
|
+
<div id="composer-shell" class="composer-shell hidden"><div class="composer"><textarea id="prompt" placeholder="描述你想完成的任务…"></textarea><div class="composer-bar"><input id="session-model" list="recent-models" aria-label="模型"><input id="session-sub-model" list="recent-models" aria-label="子模型" placeholder="默认子模型"><datalist id="recent-models"></datalist><select id="session-effort" aria-label="推理强度"><option value="">默认 effort</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select><button id="send" class="send" title="发送">↑</button><button id="stop" class="stop hidden">■ 停止</button></div></div></div>
|
|
36
36
|
|
|
37
|
-
<div id="new-modal" class="modal-backdrop hidden"><form id="new-form" class="modal"><h2>新建 Agent 会话</h2><p class="subtitle">每个会话拥有独立上下文;同目录会话可以并发,但文件修改可能冲突。</p><div class="field"><label>工作目录</label><input id="new-cwd" required></div><div class="field"><label>会话名称(可选)</label><input id="new-title" placeholder="默认使用目录名"></div><div class="row"><div class="field"><label>模型</label><input id="new-model" list="recent-models"></div><div class="field"><label>Effort</label><select id="new-effort"><option value="">默认</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div
|
|
37
|
+
<div id="new-modal" class="modal-backdrop hidden"><form id="new-form" class="modal"><h2>新建 Agent 会话</h2><p class="subtitle">每个会话拥有独立上下文;同目录会话可以并发,但文件修改可能冲突。</p><div class="field"><label>工作目录</label><input id="new-cwd" required></div><div class="field"><label>会话名称(可选)</label><input id="new-title" placeholder="默认使用目录名"></div><div class="row"><div class="field"><label>模型</label><input id="new-model" list="recent-models"></div><div class="field"><label>子模型(可选)</label><input id="new-sub-model" list="recent-models"></div></div><div class="field"><label>Effort</label><select id="new-effort"><option value="">默认</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div><div class="modal-actions"><button type="button" data-close="new-modal" class="ghost">取消</button><button class="primary" type="submit">创建会话</button></div></form></div>
|
|
38
38
|
|
|
39
39
|
<div id="settings-modal" class="modal-backdrop hidden"><form id="settings-form" class="modal"><h2>API 与 Web 设置</h2><p class="subtitle">单一 API 配置作为所有新会话的默认值;会话可单独覆盖模型和 effort。</p><div class="row"><div class="field"><label>API 协议</label><select id="cfg-provider"><option value="openai">OpenAI-compatible</option><option value="anthropic">Anthropic Messages</option></select></div><div class="field"><label>API Key</label><input id="cfg-api-key" type="password" placeholder="留空保留当前 Key"></div></div><div class="field"><label>Base URL</label><input id="cfg-base-url" required></div><div class="row"><div class="field"><label>默认模型</label><input id="cfg-model" required></div><div class="field"><label>子模型(可选)</label><input id="cfg-sub-model"></div></div><div class="row"><div class="field"><label>默认 Effort</label><select id="cfg-effort"><option value="">不传</option><option>none</option><option>minimal</option><option>low</option><option>medium</option><option>high</option><option>xhigh</option><option>max</option></select></div><div class="field"><label>Context Window</label><input id="cfg-context" type="number" min="1"></div></div><div class="row"><div class="field"><label>Web 端口</label><input id="cfg-port" type="number" min="1" max="65535"></div><div class="field"><label>Max Output Tokens(可选)</label><input id="cfg-max-output" type="number" min="1"></div></div><label class="check"><input id="cfg-streaming" type="checkbox">启用流式请求</label><label class="check"><input id="cfg-open" type="checkbox">运行 deepccc web 时自动打开浏览器</label><div id="key-state" class="subtitle"></div><div class="modal-actions"><button type="button" data-close="settings-modal" class="ghost">取消</button><button class="primary" type="submit">保存设置</button></div></form></div>
|
|
40
40
|
|
|
@@ -43,7 +43,7 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
|
|
|
43
43
|
<div id="toast" class="toast hidden"></div>
|
|
44
44
|
<script>
|
|
45
45
|
(function(){
|
|
46
|
-
var state={sessions:[],selectedId:null,detail:null,config:null,eventSource:null,reloadTimer:null};
|
|
46
|
+
var state={sessions:[],selectedId:null,detail:null,config:null,eventSource:null,globalEventSource:null,reloadTimer:null,sessionsReloadTimer:null,detailRequestToken:0,configDrafts:{},promptDrafts:{}};
|
|
47
47
|
var $=function(id){return document.getElementById(id);};
|
|
48
48
|
async function api(path,options){var response=await fetch(path,Object.assign({headers:{'content-type':'application/json; charset=utf-8'}},options||{}));var data=await response.json().catch(function(){return {error:'响应不是有效 JSON'};});if(!response.ok)throw new Error(data.error||('HTTP '+response.status));return data;}
|
|
49
49
|
function toast(message,error){var el=$('toast');el.textContent=message;el.className='toast'+(error?' error':'');clearTimeout(toast.timer);toast.timer=setTimeout(function(){el.className='toast hidden';},3500);}
|
|
@@ -51,26 +51,35 @@ export const DEEPCCC_WEB_PAGE = `<!DOCTYPE html>
|
|
|
51
51
|
function escapeHtml(value){return String(value||'').replace(/[&<>"']/g,function(ch){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch];});}
|
|
52
52
|
function markdown(value){var text=escapeHtml(value);var blocks=[];var tick=String.fromCharCode(96);var fence=tick+tick+tick;var pattern=new RegExp(fence+'([a-z0-9_-]*)\\\\n([\\\\s\\\\S]*?)'+fence,'gi');text=text.replace(pattern,function(_,lang,code){blocks.push('<pre><code data-lang="'+lang+'">'+code+'</code></pre>');return '@@BLOCK'+(blocks.length-1)+'@@';});text=text.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>').replace(new RegExp(tick+'([^'+tick+']+)'+tick,'g'),'<code>$1</code>').replace(/\\n/g,'<br>');text=text.replace(/@@BLOCK(\\d+)@@/g,function(_,index){return blocks[Number(index)];});return text;}
|
|
53
53
|
function statusText(status){return status==='running'?'执行中':'空闲';}
|
|
54
|
-
function renderSessions(){var root=$('session-list');root.innerHTML='';if(!state.sessions.length){root.innerHTML='<div class="session-empty">还没有会话<br>点击上方按钮开始</div>';return;}state.sessions.forEach(function(session){var button=document.createElement('button');button.className='session-item'+(session.sessionId===state.selectedId?' active':'');button.innerHTML='<div class="session-title"><i class="status-dot '+session.status+'"></i><span>'+escapeHtml(session.title)+'</span></div><div class="session-meta">'+escapeHtml(session.model||'默认模型')+' · '+escapeHtml(session.effort||'默认 effort')+'</div><div class="session-meta">'+escapeHtml(session.cwd)+'</div>';button.addEventListener('click',function(){selectSession(session.sessionId);});root.appendChild(button);});}
|
|
54
|
+
function renderSessions(){var root=$('session-list');root.innerHTML='';if(!state.sessions.length){root.innerHTML='<div class="session-empty">还没有会话<br>点击上方按钮开始</div>';return;}state.sessions.forEach(function(session){var button=document.createElement('button');button.className='session-item'+(session.sessionId===state.selectedId?' active':'');button.innerHTML='<div class="session-title"><i class="status-dot '+session.status+'"></i><span>'+escapeHtml(session.title)+'</span></div><div class="session-meta">'+escapeHtml(session.model||'默认模型')+' · '+escapeHtml(session.effort||'默认 effort')+'</div><div class="session-meta">'+escapeHtml(session.cwd)+'</div>';button.addEventListener('click',function(){selectSession(session.sessionId).catch(function(error){toast(error.message,true);});});root.appendChild(button);});}
|
|
55
55
|
function messageHtml(role,content){return '<article class="message '+role+'"><div class="avatar">'+(role==='user'?'你':'D')+'</div><div class="bubble"><div class="role">'+(role==='user'?'YOU':'DEEPCCC')+'</div><div class="markdown">'+markdown(content)+'</div></div></article>';}
|
|
56
56
|
function toolHtml(event){var data=event.data||{};var item=data.type==='agent'?data.data:data;return '<details class="tool-card"><summary>'+escapeHtml(item.type==='tool_use'?'调用工具 · '+(item.name||'tool'):'工具结果 · '+(item.name||item.tool_use_id||'tool'))+'</summary><pre>'+escapeHtml(JSON.stringify(item.type==='tool_use'?item.input:item.content,null,2))+'</pre></details>';}
|
|
57
57
|
function approvalAnswerLabel(answer){var labels={allow:'已允许一次','allow-session':'本会话已允许','allow-always':'已永久允许',deny:'已拒绝'};return labels[answer]||'等待决定';}
|
|
58
58
|
function approvalHtml(approval){var resolved=approval.status==='resolved';var html='<section class="approval-card'+(resolved?' resolved':'')+'"><div class="approval-head"><strong>⚠ 操作审批</strong><span class="approval-status">'+escapeHtml(resolved?approvalAnswerLabel(approval.answer):'等待批准')+'</span></div><div class="approval-detail">'+escapeHtml(approval.detail||('工具:'+approval.tool))+'</div><pre class="approval-command">'+escapeHtml(approval.action)+'</pre>';if(!resolved)html+='<div class="approval-inline-actions"><button class="danger" data-approval-answer="deny" data-approval-id="'+escapeHtml(approval.approvalId)+'">拒绝</button><button class="ghost" data-approval-answer="allow" data-approval-id="'+escapeHtml(approval.approvalId)+'">允许一次</button><button class="ghost" data-approval-answer="allow-session" data-approval-id="'+escapeHtml(approval.approvalId)+'">本会话允许</button><button class="primary" data-approval-answer="allow-always" data-approval-id="'+escapeHtml(approval.approvalId)+'">永久允许</button></div>';return html+'</section>';}
|
|
59
|
-
function
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
function sessionDraft(detail){return state.configDrafts[detail.sessionId]||{model:detail.model||'',subModel:detail.subModel||'',effort:detail.effort||''};}
|
|
60
|
+
function rememberDraft(){if(!state.selectedId)return;state.configDrafts[state.selectedId]={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};}
|
|
61
|
+
function rememberPromptDraft(){if(!state.selectedId)return;state.promptDrafts[state.selectedId]=$('prompt').value;}
|
|
62
|
+
function clearSelection(message){if(state.eventSource)state.eventSource.close();state.eventSource=null;state.detailRequestToken++;state.selectedId=null;state.detail=null;$('composer-shell').classList.add('hidden');$('rename-session').classList.add('hidden');$('delete-session').classList.add('hidden');$('warning').classList.add('hidden');$('session-title').textContent='DeepCCC';$('session-cwd').textContent='选择或创建一个会话';$('content').innerHTML='<div class="welcome"><div class="welcome-card"><div class="welcome-mark">✦</div><h2>'+escapeHtml(message||'选择一个会话')+'</h2><p>从左侧选择其他会话或创建一个新的任务。</p></div></div>';}
|
|
63
|
+
function renderDetail(){var detail=state.detail;if(!detail)return;var draft=sessionDraft(detail);$('session-title').textContent=detail.title;$('session-cwd').textContent=detail.cwd+' · '+statusText(detail.status);$('rename-session').classList.remove('hidden');$('delete-session').classList.remove('hidden');$('composer-shell').classList.remove('hidden');$('prompt').value=state.promptDrafts[detail.sessionId]||'';$('session-model').value=draft.model;$('session-sub-model').value=draft.subModel;$('session-effort').value=draft.effort;var html='<div class="thread">';if(!(detail.messages||[]).length&&detail.status!=='running')html+='<div class="empty-thread"><strong>准备开始这个会话</strong><span>在下方输入任务。DeepCCC 会读取当前项目指令、调用工具并持续保存上下文。</span></div>';(detail.messages||[]).forEach(function(message){html+=messageHtml(message.role,message.content);});if(detail.status==='running'){var live=(detail.events||[]).slice().reverse().find(function(event){return event.type==='agent'&&event.data&&event.data.type==='text';});var current=(detail.events||[]).filter(function(event){return event.type==='agent'&&event.data&&(event.data.type==='tool_use'||event.data.type==='tool_result');});current.forEach(function(event){html+=toolHtml(event);});if(live)html+=messageHtml('assistant',live.data.accumulated||live.data.text||'');}var approvals=(detail.approvals||[]).slice();if(detail.pendingApproval&&!approvals.some(function(item){return item.approvalId===detail.pendingApproval.approvalId;}))approvals.push(Object.assign({status:'pending'},detail.pendingApproval));approvals.forEach(function(approval){html+=approvalHtml(approval);});if(detail.status==='running')html+='<div class="run-state"><i class="pulse"></i>Agent 正在工作</div>';html+='</div>';$('content').innerHTML=html;$('send').classList.toggle('hidden',detail.status==='running');$('stop').classList.toggle('hidden',detail.status!=='running');$('prompt').disabled=detail.status==='running';$('session-model').disabled=detail.status==='running';$('session-sub-model').disabled=detail.status==='running';$('session-effort').disabled=detail.status==='running';renderWarning();requestAnimationFrame(function(){$('content').scrollTop=$('content').scrollHeight;});}
|
|
64
|
+
function cwdKey(value){var raw=String(value||'');var normalized=raw.replace(/\\\\/g,'/').replace(new RegExp('/+$'),'');var windowsStyle=new RegExp('^[a-z]:/','i').test(normalized)||raw.indexOf(String.fromCharCode(92))>=0;return windowsStyle?normalized.toLowerCase():normalized;}
|
|
65
|
+
function renderWarning(){if(!state.detail)return;var selectedCwd=cwdKey(state.detail.cwd);var conflicts=state.sessions.filter(function(item){return item.sessionId!==state.detail.sessionId&&item.status==='running'&&cwdKey(item.cwd)===selectedCwd;});var el=$('warning');if(conflicts.length){el.textContent='⚠ 当前目录还有 '+conflicts.length+' 个 Agent 会话正在运行。多个会话直接修改同一目录,可能产生覆盖或冲突。';el.classList.remove('hidden');}else el.classList.add('hidden');}
|
|
66
|
+
async function loadSessions(){var result=await api('/api/sessions');state.sessions=result.sessions||[];if(state.selectedId&&!state.sessions.some(function(item){return item.sessionId===state.selectedId;}))clearSelection('会话已在其他页面删除');renderSessions();renderWarning();}
|
|
67
|
+
async function loadDetail(id,token){var sessionId=id||state.selectedId;if(!sessionId)return false;var requestToken=token===undefined?++state.detailRequestToken:token;var result=await api('/api/sessions/'+encodeURIComponent(sessionId));if(state.selectedId!==sessionId||requestToken!==state.detailRequestToken)return false;await loadSessions();if(state.selectedId!==sessionId||requestToken!==state.detailRequestToken)return false;state.detail=result.session;renderDetail();return true;}
|
|
68
|
+
function scheduleDetail(id){if(id&&id!==state.selectedId)return;clearTimeout(state.reloadTimer);var token=++state.detailRequestToken;state.reloadTimer=setTimeout(function(){loadDetail(id||state.selectedId,token).catch(function(error){toast(error.message,true);});},120);}
|
|
69
|
+
function scheduleSessions(){clearTimeout(state.sessionsReloadTimer);state.sessionsReloadTimer=setTimeout(function(){loadSessions().catch(function(error){toast(error.message,true);});},80);}
|
|
70
|
+
async function selectSession(id){state.selectedId=id;var token=++state.detailRequestToken;renderSessions();if(state.eventSource)state.eventSource.close();state.eventSource=new EventSource('/api/sessions/'+encodeURIComponent(id)+'/events');state.eventSource.onmessage=function(){scheduleDetail(id);};await loadDetail(id,token);}
|
|
71
|
+
async function loadConfig(){var result=await api('/api/config');state.config=result.config;$('cfg-provider').value=state.config.provider;$('cfg-base-url').value=state.config.baseURL;$('cfg-model').value=state.config.model;$('cfg-sub-model').value=state.config.subModel||'';$('cfg-effort').value=state.config.effort||'';$('cfg-context').value=state.config.contextWindow;$('cfg-max-output').value=state.config.maxOutputTokens||'';$('cfg-streaming').checked=state.config.streaming!==false;$('cfg-port').value=state.config.web.port;$('cfg-open').checked=state.config.web.openOnStart!==false;$('server-port').textContent=state.config.web.port;$('key-state').textContent=state.config.apiKeyConfigured?'当前 API Key:'+state.config.apiKeyMask:'尚未配置 API Key';$('new-cwd').value=state.config.defaultCwd||'';$('new-model').value=state.config.model;$('new-sub-model').value=state.config.subModel||'';var models=[state.config.model,state.config.subModel].concat(state.sessions.reduce(function(all,item){return all.concat([item.model,item.subModel]);},[])).filter(Boolean).filter(function(value,index,array){return array.indexOf(value)===index;});$('recent-models').innerHTML=models.map(function(model){return '<option value="'+escapeHtml(model)+'">';}).join('');}
|
|
72
|
+
$('new-session').addEventListener('click',function(){openModal('new-modal');setTimeout(function(){$('new-cwd').focus();},0);});$('open-settings').addEventListener('click',function(){loadConfig().then(function(){openModal('settings-modal');}).catch(function(error){toast(error.message,true);});});document.querySelectorAll('[data-close]').forEach(function(button){button.addEventListener('click',function(){closeModal(button.dataset.close);});});
|
|
73
|
+
$('new-form').addEventListener('submit',async function(event){event.preventDefault();try{var result=await api('/api/sessions',{method:'POST',body:JSON.stringify({cwd:$('new-cwd').value,title:$('new-title').value,model:$('new-model').value,subModel:$('new-sub-model').value,effort:$('new-effort').value})});closeModal('new-modal');$('new-title').value='';await loadSessions();await selectSession(result.session.sessionId);}catch(error){toast(error.message,true);}});
|
|
68
74
|
$('settings-form').addEventListener('submit',async function(event){event.preventDefault();var body={provider:$('cfg-provider').value,baseURL:$('cfg-base-url').value,model:$('cfg-model').value,subModel:$('cfg-sub-model').value,effort:$('cfg-effort').value,contextWindow:Number($('cfg-context').value),streaming:$('cfg-streaming').checked,web:{port:Number($('cfg-port').value),openOnStart:$('cfg-open').checked}};if($('cfg-max-output').value)body.maxOutputTokens=Number($('cfg-max-output').value);if($('cfg-api-key').value)body.apiKey=$('cfg-api-key').value;try{await api('/api/config',{method:'PUT',body:JSON.stringify(body)});$('cfg-api-key').value='';closeModal('settings-modal');toast('设置已保存,新配置从下一次新建会话生效');await loadConfig();}catch(error){toast(error.message,true);}});
|
|
69
|
-
async function send(){
|
|
70
|
-
|
|
71
|
-
$('
|
|
72
|
-
$('
|
|
73
|
-
|
|
75
|
+
async function send(){var sessionId=state.selectedId;var text=$('prompt').value;if(!sessionId||!text.trim())return;try{var patch={model:$('session-model').value,subModel:$('session-sub-model').value,effort:$('session-effort').value};if(patch.model!==state.detail.model||patch.subModel!==state.detail.subModel||patch.effort!==state.detail.effort){var updated=await api('/api/sessions/'+encodeURIComponent(sessionId),{method:'PATCH',body:JSON.stringify(patch)});if(state.selectedId===sessionId){state.detail=Object.assign({},state.detail,updated.session);renderDetail();}await loadSessions();}await api('/api/sessions/'+encodeURIComponent(sessionId)+'/messages',{method:'POST',body:JSON.stringify({text:text})});delete state.configDrafts[sessionId];delete state.promptDrafts[sessionId];if(state.selectedId===sessionId)$('prompt').value='';var token=++state.detailRequestToken;await loadDetail(sessionId,token);}catch(error){toast(error.message,true);}}
|
|
76
|
+
['session-model','session-sub-model','session-effort'].forEach(function(id){$(id).addEventListener('input',rememberDraft);$(id).addEventListener('change',rememberDraft);});
|
|
77
|
+
$('send').addEventListener('click',send);$('prompt').addEventListener('input',rememberPromptDraft);$('prompt').addEventListener('keydown',function(event){if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send();}});$('stop').addEventListener('click',async function(){try{await api('/api/sessions/'+encodeURIComponent(state.selectedId)+'/stop',{method:'POST',body:'{}'});toast('已请求停止');}catch(error){toast(error.message,true);}});
|
|
78
|
+
$('rename-session').addEventListener('click',function(){$('rename-title').value=state.detail.title;openModal('rename-modal');});$('rename-form').addEventListener('submit',async function(event){event.preventDefault();try{await api('/api/sessions/'+encodeURIComponent(state.selectedId),{method:'PATCH',body:JSON.stringify({title:$('rename-title').value})});closeModal('rename-modal');var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){toast(error.message,true);}});$('delete-session').addEventListener('click',async function(){if(!confirm('删除这个会话及其本地历史?不会删除工作目录中的文件。'))return;try{var deletedId=state.selectedId;await api('/api/sessions/'+encodeURIComponent(deletedId),{method:'DELETE'});delete state.configDrafts[deletedId];delete state.promptDrafts[deletedId];clearSelection('会话已删除');await loadSessions();}catch(error){toast(error.message,true);}});
|
|
79
|
+
$('content').addEventListener('click',async function(event){var button=event.target.closest('[data-approval-answer]');if(!button)return;button.disabled=true;try{await api('/api/approvals/'+encodeURIComponent(button.dataset.approvalId),{method:'POST',body:JSON.stringify({answer:button.dataset.approvalAnswer})});var token=++state.detailRequestToken;await loadDetail(state.selectedId,token);}catch(error){button.disabled=false;toast(error.message,true);}});
|
|
80
|
+
function handleGlobalEvent(event){try{var update=JSON.parse(event.data);if(update.type==='session_deleted'){delete state.configDrafts[update.sessionId];delete state.promptDrafts[update.sessionId];}}catch(error){}scheduleSessions();}
|
|
81
|
+
state.globalEventSource=new EventSource('/api/events');state.globalEventSource.onmessage=handleGlobalEvent;
|
|
82
|
+
Promise.all([loadSessions(),loadConfig()]).then(function(){if(state.sessions[0])return selectSession(state.sessions[0].sessionId);}).catch(function(error){toast(error.message,true);});
|
|
74
83
|
})();
|
|
75
84
|
</script>
|
|
76
85
|
</body></html>`;
|
package/dist/web-runtime.d.ts
CHANGED
|
@@ -13,8 +13,9 @@ export interface WebRuntimeConfig extends ChatSessionConfig {
|
|
|
13
13
|
}
|
|
14
14
|
export interface WebRuntimeEvent {
|
|
15
15
|
eventId: number;
|
|
16
|
+
sessionId: string;
|
|
16
17
|
at: string;
|
|
17
|
-
type: "user" | "agent" | "run_started" | "run_finished" | "approval" | "approval_resolved" | "session_updated";
|
|
18
|
+
type: "user" | "agent" | "run_started" | "run_finished" | "approval" | "approval_resolved" | "session_updated" | "session_deleted";
|
|
18
19
|
data: unknown;
|
|
19
20
|
}
|
|
20
21
|
export interface PendingWebApproval extends PermissionRequest {
|
|
@@ -47,8 +48,11 @@ export declare class DeepCccWebRuntime {
|
|
|
47
48
|
private readonly idFactory;
|
|
48
49
|
private readonly agents;
|
|
49
50
|
private readonly activeRuns;
|
|
51
|
+
private readonly startingRuns;
|
|
52
|
+
private readonly sessionMutations;
|
|
50
53
|
private readonly events;
|
|
51
54
|
private readonly listeners;
|
|
55
|
+
private readonly globalListeners;
|
|
52
56
|
private readonly approvals;
|
|
53
57
|
private nextEventId;
|
|
54
58
|
constructor(options: DeepCccWebRuntimeOptions);
|
|
@@ -93,9 +97,12 @@ export declare class DeepCccWebRuntime {
|
|
|
93
97
|
waitForIdle(sessionId: string): Promise<void>;
|
|
94
98
|
resolveApproval(approvalId: string, answer: PermissionAnswer): Promise<boolean>;
|
|
95
99
|
subscribe(sessionId: string, listener: (event: WebRuntimeEvent) => void): () => void;
|
|
100
|
+
subscribeAll(listener: (event: WebRuntimeEvent) => void): () => void;
|
|
96
101
|
private run;
|
|
97
102
|
private createAgent;
|
|
98
103
|
private requestApproval;
|
|
104
|
+
private finishApproval;
|
|
99
105
|
private emit;
|
|
106
|
+
private notifyListeners;
|
|
100
107
|
private requireSession;
|
|
101
108
|
}
|
package/dist/web-runtime.js
CHANGED
|
@@ -11,8 +11,11 @@ export class DeepCccWebRuntime {
|
|
|
11
11
|
idFactory;
|
|
12
12
|
agents = new Map();
|
|
13
13
|
activeRuns = new Map();
|
|
14
|
+
startingRuns = new Map();
|
|
15
|
+
sessionMutations = new Set();
|
|
14
16
|
events = new Map();
|
|
15
17
|
listeners = new Map();
|
|
18
|
+
globalListeners = new Set();
|
|
16
19
|
approvals = new Map();
|
|
17
20
|
nextEventId = 1;
|
|
18
21
|
constructor(options) {
|
|
@@ -70,46 +73,84 @@ export class DeepCccWebRuntime {
|
|
|
70
73
|
};
|
|
71
74
|
}
|
|
72
75
|
async updateSession(sessionId, patch) {
|
|
73
|
-
if (this.activeRuns.has(sessionId))
|
|
74
|
-
throw new Error("Cannot change session settings while
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
if (this.activeRuns.has(sessionId) || this.startingRuns.has(sessionId) || this.sessionMutations.has(sessionId)) {
|
|
77
|
+
throw new Error("Cannot change session settings while another session operation is running");
|
|
78
|
+
}
|
|
79
|
+
this.sessionMutations.add(sessionId);
|
|
80
|
+
try {
|
|
81
|
+
const updated = await this.store.update(sessionId, patch);
|
|
82
|
+
this.agents.delete(sessionId);
|
|
83
|
+
this.emit(sessionId, "session_updated", updated);
|
|
84
|
+
return updated;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.sessionMutations.delete(sessionId);
|
|
88
|
+
}
|
|
79
89
|
}
|
|
80
90
|
async deleteSession(sessionId) {
|
|
81
|
-
if (this.activeRuns.has(sessionId))
|
|
91
|
+
if (this.activeRuns.has(sessionId) || this.startingRuns.has(sessionId) || this.sessionMutations.has(sessionId)) {
|
|
82
92
|
throw new Error("Stop the running session before deleting it");
|
|
83
|
-
|
|
84
|
-
this.
|
|
85
|
-
|
|
93
|
+
}
|
|
94
|
+
this.sessionMutations.add(sessionId);
|
|
95
|
+
try {
|
|
96
|
+
this.agents.delete(sessionId);
|
|
97
|
+
const deleted = await this.store.delete(sessionId);
|
|
98
|
+
if (deleted) {
|
|
99
|
+
this.emit(sessionId, "session_deleted", { sessionId });
|
|
100
|
+
this.events.delete(sessionId);
|
|
101
|
+
}
|
|
102
|
+
return deleted;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
this.sessionMutations.delete(sessionId);
|
|
106
|
+
}
|
|
86
107
|
}
|
|
87
108
|
async sendMessage(sessionId, text) {
|
|
88
109
|
const prompt = text.trim();
|
|
89
110
|
if (!prompt)
|
|
90
111
|
throw new Error("Message must not be empty");
|
|
91
|
-
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
if (this.activeRuns.has(sessionId) || this.startingRuns.has(sessionId) || this.sessionMutations.has(sessionId)) {
|
|
92
114
|
throw new Error("This session is already running");
|
|
93
|
-
let meta = await this.requireSession(sessionId);
|
|
94
|
-
if (meta.title === "新会话" || meta.title === "New session" || meta.title === meta.cwd.split(/[\\/]/).at(-1)) {
|
|
95
|
-
meta = await this.store.update(sessionId, { title: prompt.replace(/\s+/g, " ").slice(0, 42) });
|
|
96
115
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
this.startingRuns.set(sessionId, controller);
|
|
117
|
+
try {
|
|
118
|
+
let meta = await this.requireSession(sessionId);
|
|
119
|
+
if (meta.title === "新会话" || meta.title === "New session" || meta.title === meta.cwd.split(/[\\/]/).at(-1)) {
|
|
120
|
+
meta = await this.store.update(sessionId, { title: prompt.replace(/\s+/g, " ").slice(0, 42) });
|
|
121
|
+
}
|
|
122
|
+
if (controller.signal.aborted)
|
|
123
|
+
throw new DOMException("The session start was stopped", "AbortError");
|
|
124
|
+
const runId = `run-${this.idFactory()}`;
|
|
125
|
+
this.events.set(sessionId, []);
|
|
126
|
+
this.emit(sessionId, "user", { text: prompt });
|
|
127
|
+
const promise = Promise.resolve()
|
|
128
|
+
.then(() => this.run(meta, prompt, runId, controller.signal))
|
|
129
|
+
.finally(() => {
|
|
130
|
+
this.activeRuns.delete(sessionId);
|
|
131
|
+
this.emit(sessionId, "session_updated", { sessionId, status: "idle" });
|
|
132
|
+
});
|
|
133
|
+
this.activeRuns.set(sessionId, { runId, controller, promise });
|
|
134
|
+
this.emit(sessionId, "run_started", { runId });
|
|
135
|
+
void promise.catch(() => { });
|
|
136
|
+
return { runId };
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
this.startingRuns.delete(sessionId);
|
|
140
|
+
}
|
|
107
141
|
}
|
|
108
142
|
async stopSession(sessionId) {
|
|
109
143
|
const active = this.activeRuns.get(sessionId);
|
|
110
|
-
|
|
144
|
+
const starting = this.startingRuns.get(sessionId);
|
|
145
|
+
if (!active && !starting)
|
|
111
146
|
return false;
|
|
147
|
+
starting?.abort();
|
|
148
|
+
if (!active)
|
|
149
|
+
return true;
|
|
112
150
|
active.controller.abort();
|
|
151
|
+
const pending = [...this.approvals.entries()]
|
|
152
|
+
.filter(([, waiter]) => waiter.approval.sessionId === sessionId);
|
|
153
|
+
await Promise.all(pending.map(([approvalId, waiter]) => this.finishApproval(approvalId, waiter, "deny", { stopped: true }).catch(() => { })));
|
|
113
154
|
return true;
|
|
114
155
|
}
|
|
115
156
|
async waitForIdle(sessionId) {
|
|
@@ -119,11 +160,7 @@ export class DeepCccWebRuntime {
|
|
|
119
160
|
const waiter = this.approvals.get(approvalId);
|
|
120
161
|
if (!waiter)
|
|
121
162
|
return false;
|
|
122
|
-
|
|
123
|
-
this.approvals.delete(approvalId);
|
|
124
|
-
await this.store.resolveApproval(waiter.approval.sessionId, approvalId, answer);
|
|
125
|
-
waiter.resolve(answer);
|
|
126
|
-
this.emit(waiter.approval.sessionId, "approval_resolved", { approvalId, answer });
|
|
163
|
+
await this.finishApproval(approvalId, waiter, answer);
|
|
127
164
|
return true;
|
|
128
165
|
}
|
|
129
166
|
subscribe(sessionId, listener) {
|
|
@@ -136,6 +173,10 @@ export class DeepCccWebRuntime {
|
|
|
136
173
|
this.listeners.delete(sessionId);
|
|
137
174
|
};
|
|
138
175
|
}
|
|
176
|
+
subscribeAll(listener) {
|
|
177
|
+
this.globalListeners.add(listener);
|
|
178
|
+
return () => this.globalListeners.delete(listener);
|
|
179
|
+
}
|
|
139
180
|
async run(meta, prompt, runId, signal) {
|
|
140
181
|
try {
|
|
141
182
|
const agent = this.agents.get(meta.sessionId) ?? this.createAgent(meta);
|
|
@@ -167,33 +208,60 @@ export class DeepCccWebRuntime {
|
|
|
167
208
|
createdAt: this.now().toISOString(),
|
|
168
209
|
};
|
|
169
210
|
return new Promise((resolve) => {
|
|
211
|
+
const ready = this.store.addApproval(sessionId, {
|
|
212
|
+
...approval,
|
|
213
|
+
status: "pending",
|
|
214
|
+
});
|
|
215
|
+
let waiter;
|
|
170
216
|
const timeout = setTimeout(() => {
|
|
171
|
-
this.
|
|
172
|
-
void this.store.resolveApproval(sessionId, approvalId, "deny").finally(() => {
|
|
173
|
-
resolve("deny");
|
|
174
|
-
this.emit(sessionId, "approval_resolved", { approvalId, answer: "deny", timedOut: true });
|
|
175
|
-
});
|
|
217
|
+
void this.finishApproval(approvalId, waiter, "deny", { timedOut: true }).catch(() => { });
|
|
176
218
|
}, this.approvalTimeoutMs);
|
|
177
219
|
timeout.unref?.();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
220
|
+
waiter = { approval, resolve, timeout, ready };
|
|
221
|
+
this.approvals.set(approvalId, waiter);
|
|
222
|
+
void ready.then(() => {
|
|
223
|
+
if (this.approvals.get(approvalId) !== waiter)
|
|
224
|
+
return;
|
|
183
225
|
this.emit(sessionId, "approval", approval);
|
|
184
226
|
}).catch(() => {
|
|
227
|
+
if (this.approvals.get(approvalId) !== waiter)
|
|
228
|
+
return;
|
|
185
229
|
clearTimeout(timeout);
|
|
186
230
|
this.approvals.delete(approvalId);
|
|
187
231
|
resolve("deny");
|
|
188
232
|
});
|
|
189
233
|
});
|
|
190
234
|
}
|
|
235
|
+
async finishApproval(approvalId, waiter, answer, extra = {}) {
|
|
236
|
+
if (this.approvals.get(approvalId) !== waiter)
|
|
237
|
+
return;
|
|
238
|
+
clearTimeout(waiter.timeout);
|
|
239
|
+
this.approvals.delete(approvalId);
|
|
240
|
+
try {
|
|
241
|
+
await waiter.ready;
|
|
242
|
+
await this.store.resolveApproval(waiter.approval.sessionId, approvalId, answer);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
waiter.resolve(answer);
|
|
246
|
+
this.emit(waiter.approval.sessionId, "approval_resolved", { approvalId, answer, ...extra });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
191
249
|
emit(sessionId, type, data) {
|
|
192
|
-
const event = { eventId: this.nextEventId++, at: this.now().toISOString(), type, data };
|
|
250
|
+
const event = { eventId: this.nextEventId++, sessionId, at: this.now().toISOString(), type, data };
|
|
193
251
|
const events = [...(this.events.get(sessionId) ?? []), event].slice(-500);
|
|
194
252
|
this.events.set(sessionId, events);
|
|
195
|
-
|
|
196
|
-
|
|
253
|
+
this.notifyListeners(this.listeners.get(sessionId) ?? [], event);
|
|
254
|
+
if (["run_started", "run_finished", "session_updated", "session_deleted"].includes(type)) {
|
|
255
|
+
this.notifyListeners(this.globalListeners, event);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
notifyListeners(listeners, event) {
|
|
259
|
+
for (const listener of listeners) {
|
|
260
|
+
try {
|
|
261
|
+
listener(event);
|
|
262
|
+
}
|
|
263
|
+
catch { /* a disconnected SSE client must not affect the Agent run */ }
|
|
264
|
+
}
|
|
197
265
|
}
|
|
198
266
|
async requireSession(sessionId) {
|
|
199
267
|
const meta = await this.store.get(sessionId);
|
package/dist/web-server.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface PublicDeepCccConfig {
|
|
|
19
19
|
defaultCwd?: string;
|
|
20
20
|
}
|
|
21
21
|
export interface DeepCccWebRequestHandlerOptions {
|
|
22
|
-
runtime: Pick<DeepCccWebRuntime, "listSessions" | "createSession" | "getSession" | "updateSession" | "deleteSession" | "sendMessage" | "stopSession" | "resolveApproval" | "subscribe">;
|
|
22
|
+
runtime: Pick<DeepCccWebRuntime, "listSessions" | "createSession" | "getSession" | "updateSession" | "deleteSession" | "sendMessage" | "stopSession" | "resolveApproval" | "subscribe" | "subscribeAll">;
|
|
23
23
|
getPublicConfig: () => PublicDeepCccConfig;
|
|
24
24
|
saveConfig: (patch: DeepCccConfigPatch) => Promise<PublicDeepCccConfig> | PublicDeepCccConfig;
|
|
25
25
|
defaultCwd: string;
|
package/dist/web-server.js
CHANGED
|
@@ -84,6 +84,9 @@ export function createDeepCccWebRequestHandler(options) {
|
|
|
84
84
|
if (method === "GET" && path === "/api/sessions") {
|
|
85
85
|
return jsonReply(res, 200, { ok: true, sessions: await options.runtime.listSessions() });
|
|
86
86
|
}
|
|
87
|
+
if (method === "GET" && path === "/api/events") {
|
|
88
|
+
return openGlobalEventStream(req, res, options.runtime);
|
|
89
|
+
}
|
|
87
90
|
if (method === "POST" && path === "/api/sessions") {
|
|
88
91
|
const body = await readJson(req);
|
|
89
92
|
const cwd = body.cwd?.trim() || options.defaultCwd;
|
|
@@ -265,6 +268,20 @@ function openEventStream(req, res, sessionId, runtime) {
|
|
|
265
268
|
heartbeat.unref?.();
|
|
266
269
|
req.on("close", () => { clearInterval(heartbeat); unsubscribe(); });
|
|
267
270
|
}
|
|
271
|
+
function openGlobalEventStream(req, res, runtime) {
|
|
272
|
+
res.writeHead(200, {
|
|
273
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
274
|
+
"cache-control": "no-cache, no-transform",
|
|
275
|
+
connection: "keep-alive",
|
|
276
|
+
"x-accel-buffering": "no",
|
|
277
|
+
});
|
|
278
|
+
res.write(": connected\n\n");
|
|
279
|
+
const send = (event) => res.write(`id: ${event.eventId}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
280
|
+
const unsubscribe = runtime.subscribeAll(send);
|
|
281
|
+
const heartbeat = setInterval(() => res.write(": heartbeat\n\n"), 15_000);
|
|
282
|
+
heartbeat.unref?.();
|
|
283
|
+
req.on("close", () => { clearInterval(heartbeat); unsubscribe(); });
|
|
284
|
+
}
|
|
268
285
|
async function readJson(req) {
|
|
269
286
|
const chunks = [];
|
|
270
287
|
let bytes = 0;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|