chatccc 0.2.259 → 0.2.260

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.
@@ -16,7 +16,7 @@ import { handleAgentTeamRequest } from "./agent-team/http/board-routes.js";
16
16
  import { AGENT_TEAM_PAGE_HTML } from "./agent-team/web/agent-team-page.js";
17
17
  export { AGENT_TEAM_PAGE_HTML } from "./agent-team/web/agent-team-page.js";
18
18
  import { buildWebUiUrl, createInternalRestartEnv, openWebUiInDefaultBrowser, } from "./startup-lifecycle.js";
19
- import { getClaudeSdkInstalledVersion, getLastInstallProgress, installClaudeSdk, isClaudeSdkInstalled, isInstallRunning, } from "./claude-sdk-installer.js";
19
+ import { engineManager } from "./engines/engine-specs.js";
20
20
  const PROJECT_ROOT = CHATCCC_PACKAGE_ROOT;
21
21
  const USER_DATA_DIR = join(homedir(), ".chatccc");
22
22
  const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
@@ -511,6 +511,34 @@ export function unflattenConfig(flat) {
511
511
  result.ccc = result.ccc || {};
512
512
  result.ccc.defaultAgent = val === true || val === "true";
513
513
  }
514
+ else if (key === "CHATCCC_DSH_API_KEY") {
515
+ result.dsh = result.dsh || {};
516
+ result.dsh.apiKey = val;
517
+ }
518
+ else if (key === "CHATCCC_DSH_BASE_URL") {
519
+ result.dsh = result.dsh || {};
520
+ result.dsh.baseUrl = val;
521
+ }
522
+ else if (key === "CHATCCC_DSH_MODEL") {
523
+ result.dsh = result.dsh || {};
524
+ result.dsh.model = val;
525
+ }
526
+ else if (key === "CHATCCC_DSH_PROVIDER") {
527
+ result.dsh = result.dsh || {};
528
+ result.dsh.provider = val;
529
+ }
530
+ else if (key === "CHATCCC_DSH_MAX_TOKENS") {
531
+ result.dsh = result.dsh || {};
532
+ result.dsh.maxTokens = parseInt(String(val), 10) || 49152;
533
+ }
534
+ else if (key === "CHATCCC_DSH_ENABLED") {
535
+ result.dsh = result.dsh || {};
536
+ result.dsh.enabled = val === true || val === "true";
537
+ }
538
+ else if (key === "CHATCCC_DSH_DEFAULT_AGENT") {
539
+ result.dsh = result.dsh || {};
540
+ result.dsh.defaultAgent = val === true || val === "true";
541
+ }
514
542
  }
515
543
  return result;
516
544
  }
@@ -607,33 +635,20 @@ async function handleForgetIlink(_req, res) {
607
635
  jsonReply(res, 500, { ok: false, error: err.message });
608
636
  }
609
637
  }
610
- async function handleClaudeSdkStatus(_req, res) {
611
- const progress = getLastInstallProgress();
612
- jsonReply(res, 200, {
613
- installed: isClaudeSdkInstalled(),
614
- version: getClaudeSdkInstalledVersion(),
615
- running: isInstallRunning(),
616
- // 展开到顶层:前端轮询 JS 读顶层 phase/message/percent/error(历史契约),
617
- // 若只放在 progress 嵌套里,前端 s.phase 恒为 undefined → 进度条永不渲染。
618
- ...progress,
619
- // 保留嵌套结构,兼容其它调用方。
620
- progress,
621
- });
622
- }
623
- async function handleClaudeSdkInstall(_req, res) {
624
- if (isInstallRunning()) {
625
- jsonReply(res, 200, { ok: true, alreadyRunning: true });
626
- return;
638
+ async function handleEngineStatus(engineId, res) {
639
+ try {
640
+ jsonReply(res, 200, { ok: true, ...(await engineManager.getStatus(engineId)) });
641
+ }
642
+ catch (err) {
643
+ jsonReply(res, err.message.startsWith("Unknown engine:") ? 404 : 500, { ok: false, error: err.message });
627
644
  }
645
+ }
646
+ async function handleEngineInstall(engineId, res) {
628
647
  try {
629
- // 后台执行,不阻塞响应;前端通过 /api/claude-sdk/status 轮询进度
630
- installClaudeSdk().catch((err) => {
631
- console.error(`[WEB-UI] Claude SDK 安装失败: ${err.message}`);
632
- });
633
- jsonReply(res, 200, { ok: true });
648
+ jsonReply(res, 202, { ok: true, job: await engineManager.startInstall(engineId) });
634
649
  }
635
650
  catch (err) {
636
- jsonReply(res, 500, { ok: false, error: err.message });
651
+ jsonReply(res, err.message.startsWith("Unknown engine:") ? 404 : 500, { ok: false, error: err.message });
637
652
  }
638
653
  }
639
654
  // ---------------------------------------------------------------------------
@@ -682,7 +697,12 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
682
697
  .btn-danger:hover{background:#dc2626}
683
698
  .btn-success{background:#16a34a;color:#fff}
684
699
  .btn-success:hover{background:#15803d}
685
- .btn:disabled{opacity:.5;cursor:not-allowed}
700
+ .btn:disabled{opacity:.5;cursor:not-allowed}
701
+ .engine-status{font-size:13px;color:#64748b;margin-bottom:8px}
702
+ .engine-steps{display:grid;gap:5px;margin:8px 0 10px}
703
+ .engine-step{display:grid;grid-template-columns:18px 1fr auto;gap:7px;align-items:center;padding:5px 7px;border-radius:7px;background:#f8fafc;font-size:12px;color:#64748b}
704
+ .engine-step.running{background:#eff6ff;color:#1d4ed8}.engine-step.completed{color:#15803d}.engine-step.failed{background:#fef2f2;color:#dc2626}
705
+ .engine-step-icon{font-weight:700;text-align:center}.engine-step-percent{font-variant-numeric:tabular-nums;color:inherit}
686
706
  .btn-group{display:flex;gap:8px;flex-wrap:wrap}
687
707
  .agent-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:16px}
688
708
  .agent-card{min-width:0}
@@ -913,12 +933,41 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
913
933
  <div class="hint">压缩阈值自动 = 窗口 × 80%(超出即把较早消息压缩为摘要)。⚠️ 超过模型/服务端实际上限时请求会被 API 直接拒绝(context length exceeded);实际窗口以模型与所用服务端为准(如 litellm 代理的 max_input_tokens)。</div>
914
934
  </div>
915
935
  </fieldset>
916
- </div>
917
-
918
- <!-- Claude 卡片 -->
936
+ </div>
937
+
938
+ <!-- DeepSeek Harness 卡片 -->
939
+ <div class="agent-card" id="agent-card-dsh">
940
+ <div class="agent-card-header">
941
+ <input type="checkbox" class="agent-toggle" id="agent-enable-dsh" onchange="onInstallableAgentToggle('dsh', this)">
942
+ <div class="meta">
943
+ <div class="name">DeepSeek Harness</div>
944
+ <div class="desc">DeepSeek 官方 Agent Harness<br>按需安装,独立于 CCC Agent</div>
945
+ </div>
946
+ </div>
947
+ <label class="agent-default-row">
948
+ <input type="checkbox" id="agent-default-dsh" onchange="onDefaultAgentToggle('dsh', this.checked)">
949
+ 设为默认 Agent
950
+ </label>
951
+ <fieldset class="agent-body" id="agent-body-dsh" disabled>
952
+ <div class="form-group"><label>API Key</label><input type="password" id="field-CHATCCC_DSH_API_KEY" placeholder="DeepSeek API Key"></div>
953
+ <div class="form-group"><label>Base URL</label><input type="text" id="field-CHATCCC_DSH_BASE_URL" placeholder="https://api.deepseek.com/v1"></div>
954
+ <div class="form-group"><label>模型</label><input type="text" id="field-CHATCCC_DSH_MODEL" placeholder="deepseek-v4-flash"></div>
955
+ <div class="form-group"><label>Provider 路由</label><input type="text" id="field-CHATCCC_DSH_PROVIDER" placeholder="deepseek-official"></div>
956
+ <div class="form-group"><label>单次最大输出 Tokens</label><input type="number" id="field-CHATCCC_DSH_MAX_TOKENS" min="1" placeholder="49152"></div>
957
+ <div class="form-group" style="border-top:1px solid #e2e8f0;padding-top:12px;margin-top:4px">
958
+ <label>DeepSeek Harness 引擎</label>
959
+ <div id="dsh-engine-status" class="engine-status">检测中...</div>
960
+ <div id="dsh-engine-steps" class="engine-steps"></div>
961
+ <button type="button" class="btn btn-outline" id="dsh-engine-install-btn" onclick="installEngine('dsh')">安装并启用</button>
962
+ <div class="hint" style="margin-top:6px">一次点击自动完成环境检查、下载、校验、Runtime 握手和原子切换;页面刷新后仍可恢复进度。</div>
963
+ </div>
964
+ </fieldset>
965
+ </div>
966
+
967
+ <!-- Claude 卡片 -->
919
968
  <div class="agent-card" id="agent-card-claude">
920
969
  <div class="agent-card-header">
921
- <input type="checkbox" class="agent-toggle" id="agent-enable-claude" onchange="onClaudeToggle(this)">
970
+ <input type="checkbox" class="agent-toggle" id="agent-enable-claude" onchange="onInstallableAgentToggle('claude', this)">
922
971
  <div class="meta">
923
972
  <div class="name">Claude Code</div>
924
973
  <div class="desc">Anthropic Claude Code CLI<br>模型、effort 均为选填</div>
@@ -951,14 +1000,9 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
951
1000
  </div>
952
1001
  <div class="form-group" style="border-top:1px solid #e2e8f0;padding-top:12px;margin-top:4px">
953
1002
  <label>Claude Code 引擎(Agent SDK)</label>
954
- <div id="claude-engine-status" style="font-size:13px;color:#64748b;margin-bottom:8px">检测中...</div>
955
- <div id="claude-engine-progress-wrap" style="display:none;margin-bottom:8px">
956
- <div style="background:#e2e8f0;border-radius:6px;height:8px;overflow:hidden">
957
- <div id="claude-engine-progress-bar" style="width:0%;height:100%;background:#3b82f6;transition:width .3s"></div>
958
- </div>
959
- <div id="claude-engine-progress-text" style="font-size:12px;color:#64748b;margin-top:4px"></div>
960
- </div>
961
- <button class="btn btn-outline" id="claude-engine-install-btn" onclick="installClaudeEngine()">安装 Claude Code SDK</button>
1003
+ <div id="claude-engine-status" class="engine-status">检测中...</div>
1004
+ <div id="claude-engine-steps" class="engine-steps"></div>
1005
+ <button type="button" class="btn btn-outline" id="claude-engine-install-btn" onclick="installEngine('claude')">安装并启用</button>
962
1006
  <div class="hint" style="margin-top:6px">ChatCCC 通过 Claude Agent SDK 调用 Claude Code;SDK 引擎按需下载到本机(仅启用 Claude Code 时需要),安装期间请保持网络畅通。</div>
963
1007
  </div>
964
1008
  </fieldset>
@@ -1136,7 +1180,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
1136
1180
  </div>
1137
1181
  </details>
1138
1182
 
1139
- <details class="card config-section" id="dash-claude">
1183
+ <details class="card config-section" id="dash-claude">
1140
1184
  <summary>Claude Agent</summary>
1141
1185
  <div class="section-detail">
1142
1186
  <div class="config-row"><span class="key">模型</span><span class="val" id="cfg-ANTHROPIC_MODEL">-</span></div>
@@ -1149,7 +1193,24 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
1149
1193
  <div class="hint" style="margin-top:6px;line-height:1.6">生效范围:保存后下一条消息或下个新会话生效,当前生成不中断。</div>
1150
1194
  <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('claude')">编辑</button>
1151
1195
  </div>
1152
- </details>
1196
+ </details>
1197
+
1198
+ <details class="card config-section" id="dash-dsh">
1199
+ <summary>DeepSeek Harness</summary>
1200
+ <div class="section-detail">
1201
+ <div class="config-row"><span class="key">API Key</span><span class="val" id="cfg-DSH_API_KEY">-</span></div>
1202
+ <div class="config-row"><span class="key">Base URL</span><span class="val" id="cfg-DSH_BASE_URL">-</span></div>
1203
+ <div class="config-row"><span class="key">模型</span><span class="val" id="cfg-DSH_MODEL">-</span></div>
1204
+ <div class="config-row"><span class="key">Provider</span><span class="val" id="cfg-DSH_PROVIDER">-</span></div>
1205
+ <div class="config-row"><span class="key">最大输出 Tokens</span><span class="val" id="cfg-DSH_MAX_TOKENS">-</span></div>
1206
+ <div id="dsh-dashboard-engine-status" class="engine-status">检测中...</div>
1207
+ <div id="dsh-dashboard-engine-steps" class="engine-steps"></div>
1208
+ <button type="button" class="btn btn-outline" id="dsh-dashboard-engine-install-btn" onclick="installEngine('dsh')">安装或升级引擎</button>
1209
+ <label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-dsh" onchange="setDashboardDefaultAgent('dsh', this.checked)"> 设为默认 Agent</label>
1210
+ <div class="hint" style="margin-top:6px;line-height:1.6">安装过程不调用付费模型;保存后下一条消息或下个新会话生效。</div>
1211
+ <button class="btn btn-outline" style="margin-top:8px" onclick="editSection('dsh')">编辑</button>
1212
+ </div>
1213
+ </details>
1153
1214
 
1154
1215
  <details class="card config-section" id="dash-cursor">
1155
1216
  <summary>Cursor Agent</summary>
@@ -1220,8 +1281,8 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
1220
1281
  <script>
1221
1282
  let state = {
1222
1283
  view: 'loading',
1223
- // 四个 Agent 各自的启用开关;初始全 false,renderStep2() 会按已存在 config 自动打开
1224
- agentsEnabled: { claude: false, cursor: false, codex: false, ccc: false },
1284
+ // 五个 Agent 各自的启用开关;初始全 false,renderStep2() 会按已存在 config 自动打开
1285
+ agentsEnabled: { claude: false, cursor: false, codex: false, ccc: false, dsh: false },
1225
1286
  defaultAgent: 'claude',
1226
1287
  wizardStep: 1,
1227
1288
  config: {},
@@ -1238,7 +1299,8 @@ const AGENT_FIELDS = {
1238
1299
  claude: ['CHATCCC_ANTHROPIC_MODEL','CHATCCC_ANTHROPIC_SUBAGENT_MODEL','CHATCCC_ANTHROPIC_EFFORT','CHATCCC_ANTHROPIC_API_KEY','CHATCCC_ANTHROPIC_BASE_URL','CHATCCC_ANTHROPIC_MAX_TURN'],
1239
1300
  cursor: ['CHATCCC_CURSOR_PATH','CHATCCC_CURSOR_MODEL','CHATCCC_CURSOR_ALTERNATIVE_MODEL','CHATCCC_CURSOR_AVATAR_BATTERY_MODE','CHATCCC_CURSOR_ON_DEMAND_MONTHLY_BUDGET'],
1240
1301
  codex: ['CHATCCC_CODEX_PATH','CHATCCC_CODEX_MODEL','CHATCCC_CODEX_ALTERNATIVE_MODEL','CHATCCC_CODEX_EFFORT','CHATCCC_CODEX_FAST_MODE'],
1241
- ccc: ['CHATCCC_CCC_API_KEY','CHATCCC_CCC_BASE_URL','CHATCCC_CCC_MODEL','CHATCCC_CCC_SUB_MODEL','CHATCCC_CCC_ALTERNATIVE_MODEL','CHATCCC_CCC_EFFORT','CHATCCC_CCC_PROVIDER','CHATCCC_CCC_CONTEXT_WINDOW']
1302
+ ccc: ['CHATCCC_CCC_API_KEY','CHATCCC_CCC_BASE_URL','CHATCCC_CCC_MODEL','CHATCCC_CCC_SUB_MODEL','CHATCCC_CCC_ALTERNATIVE_MODEL','CHATCCC_CCC_EFFORT','CHATCCC_CCC_PROVIDER','CHATCCC_CCC_CONTEXT_WINDOW'],
1303
+ dsh: ['CHATCCC_DSH_API_KEY','CHATCCC_DSH_BASE_URL','CHATCCC_DSH_MODEL','CHATCCC_DSH_PROVIDER','CHATCCC_DSH_MAX_TOKENS']
1242
1304
  };
1243
1305
  const FEISHU_FIELDS = ['CHATCCC_APP_ID','CHATCCC_APP_SECRET'];
1244
1306
  const WEB_UI_FIELDS = ['CHATCCC_WEB_UI_OPEN_ON_START'];
@@ -1392,28 +1454,31 @@ function onAgentToggle(agent, enabled) {
1392
1454
  updateStep2NextBtn();
1393
1455
  }
1394
1456
 
1395
- function firstEnabledAgent() {
1457
+ function firstEnabledAgent() {
1396
1458
  if (state.agentsEnabled.claude) return 'claude';
1397
1459
  if (state.agentsEnabled.cursor) return 'cursor';
1398
1460
  if (state.agentsEnabled.codex) return 'codex';
1399
- if (state.agentsEnabled.ccc) return 'ccc';
1461
+ if (state.agentsEnabled.ccc) return 'ccc';
1462
+ if (state.agentsEnabled.dsh) return 'dsh';
1400
1463
  return null;
1401
1464
  }
1402
1465
 
1403
- function resolveDefaultAgentFromConfig(c, claudeOn, cursorOn, codexOn, cccOn) {
1466
+ function resolveDefaultAgentFromConfig(c, claudeOn, cursorOn, codexOn, cccOn, dshOn) {
1404
1467
  if (claudeOn && c.claude && c.claude.defaultAgent === true) return 'claude';
1405
1468
  if (cursorOn && c.cursor && c.cursor.defaultAgent === true) return 'cursor';
1406
1469
  if (codexOn && c.codex && c.codex.defaultAgent === true) return 'codex';
1407
- if (cccOn && c.ccc && c.ccc.defaultAgent === true) return 'ccc';
1470
+ if (cccOn && c.ccc && c.ccc.defaultAgent === true) return 'ccc';
1471
+ if (dshOn && c.dsh && c.dsh.defaultAgent === true) return 'dsh';
1408
1472
  if (claudeOn) return 'claude';
1409
1473
  if (cursorOn) return 'cursor';
1410
1474
  if (codexOn) return 'codex';
1411
- if (cccOn) return 'ccc';
1475
+ if (cccOn) return 'ccc';
1476
+ if (dshOn) return 'dsh';
1412
1477
  return 'claude';
1413
1478
  }
1414
1479
 
1415
1480
  function updateDefaultAgentToggles() {
1416
- ['claude','cursor','codex','ccc'].forEach(function(agent){
1481
+ ['claude','cursor','codex','ccc','dsh'].forEach(function(agent){
1417
1482
  var el = document.getElementById('agent-default-' + agent);
1418
1483
  if (el) {
1419
1484
  el.checked = state.defaultAgent === agent;
@@ -1445,7 +1510,8 @@ function updateStep2NextBtn() {
1445
1510
  if (state.agentsEnabled.claude) validCount++;
1446
1511
  if (state.agentsEnabled.cursor) validCount++;
1447
1512
  if (state.agentsEnabled.codex) validCount++;
1448
- if (state.agentsEnabled.ccc) validCount++;
1513
+ if (state.agentsEnabled.ccc) validCount++;
1514
+ if (state.agentsEnabled.dsh) validCount++;
1449
1515
  if (validCount > 0 && !state.defaultAgent) {
1450
1516
  state.defaultAgent = firstEnabledAgent();
1451
1517
  updateDefaultAgentToggles();
@@ -1587,7 +1653,8 @@ var CLAUDE_FALLBACK_KEYS = ['model','subagentModel','effort','maxTurn'];
1587
1653
  var CURSOR_FALLBACK_KEYS = ['path','command','model','alternativeModel'];
1588
1654
  var CODEX_FALLBACK_KEYS = ['path','command','model','alternativeModel','effort','fastMode'];
1589
1655
  // 旧版 CCC 配置没有 enabled;只有 API Key 才表示用户实际完成了配置。
1590
- var CCC_FALLBACK_KEYS = ['DEEPSEEK_API_KEY'];
1656
+ var CCC_FALLBACK_KEYS = ['DEEPSEEK_API_KEY'];
1657
+ var DSH_FALLBACK_KEYS = ['apiKey'];
1591
1658
 
1592
1659
  function renderStep2() {
1593
1660
  var c = state.config || {};
@@ -1620,7 +1687,7 @@ function renderStep2() {
1620
1687
  }
1621
1688
  var codexFastModeEl = document.getElementById('field-CHATCCC_CODEX_FAST_MODE');
1622
1689
  if (codexFastModeEl) codexFastModeEl.checked = !!(c.codex && c.codex.fastMode === true);
1623
- if (c.ccc) {
1690
+ if (c.ccc) {
1624
1691
  prefillNested('field-CHATCCC_CCC_API_KEY', c.ccc.DEEPSEEK_API_KEY);
1625
1692
  prefillNested('field-CHATCCC_CCC_BASE_URL', c.ccc.DEEPSEEK_BASE_URL);
1626
1693
  prefillNested('field-CHATCCC_CCC_PROVIDER', c.ccc.provider);
@@ -1628,27 +1695,37 @@ function renderStep2() {
1628
1695
  prefillNested('field-CHATCCC_CCC_SUB_MODEL', c.ccc.subModel);
1629
1696
  prefillNested('field-CHATCCC_CCC_ALTERNATIVE_MODEL', c.ccc.alternativeModel);
1630
1697
  prefillNested('field-CHATCCC_CCC_EFFORT', c.ccc.effort);
1631
- prefillContextWindow('field-', c.ccc.contextWindow);
1632
- }
1698
+ prefillContextWindow('field-', c.ccc.contextWindow);
1699
+ }
1700
+ if (c.dsh) {
1701
+ prefillNested('field-CHATCCC_DSH_API_KEY', c.dsh.apiKey);
1702
+ prefillNested('field-CHATCCC_DSH_BASE_URL', c.dsh.baseUrl);
1703
+ prefillNested('field-CHATCCC_DSH_MODEL', c.dsh.model);
1704
+ prefillNested('field-CHATCCC_DSH_PROVIDER', c.dsh.provider);
1705
+ prefillNested('field-CHATCCC_DSH_MAX_TOKENS', c.dsh.maxTokens || 49152);
1706
+ }
1633
1707
 
1634
1708
  // 按已有 config 决定每个 Agent 默认是否开启:优先 enabled 字段,缺省时按"任一字段非空"
1635
1709
  var claudeOn = isAgentEnabled(c.claude, CLAUDE_FALLBACK_KEYS);
1636
1710
  var cursorOn = isAgentEnabled(c.cursor, CURSOR_FALLBACK_KEYS);
1637
1711
  var codexOn = isAgentEnabled(c.codex, CODEX_FALLBACK_KEYS);
1638
- var cccOn = isAgentEnabled(c.ccc, CCC_FALLBACK_KEYS);
1639
- // 全新用户:四个 Agent 均无启用/配置痕迹时,只默认勾选 DeepCCC(ccc),其余不勾
1640
- if (!claudeOn && !cursorOn && !codexOn && !cccOn) {
1712
+ var cccOn = isAgentEnabled(c.ccc, CCC_FALLBACK_KEYS);
1713
+ var dshOn = isAgentEnabled(c.dsh, DSH_FALLBACK_KEYS);
1714
+ // 全新用户:五个 Agent 均无启用/配置痕迹时,只默认勾选 DeepCCC(ccc),其余不勾
1715
+ if (!claudeOn && !cursorOn && !codexOn && !cccOn && !dshOn) {
1641
1716
  cccOn = true;
1642
1717
  }
1643
- state.defaultAgent = resolveDefaultAgentFromConfig(c, claudeOn, cursorOn, codexOn, cccOn);
1718
+ state.defaultAgent = resolveDefaultAgentFromConfig(c, claudeOn, cursorOn, codexOn, cccOn, dshOn);
1644
1719
  document.getElementById('agent-enable-claude').checked = claudeOn;
1645
1720
  document.getElementById('agent-enable-cursor').checked = cursorOn;
1646
1721
  document.getElementById('agent-enable-codex').checked = codexOn;
1647
- document.getElementById('agent-enable-ccc').checked = cccOn;
1722
+ document.getElementById('agent-enable-ccc').checked = cccOn;
1723
+ document.getElementById('agent-enable-dsh').checked = dshOn;
1648
1724
  onAgentToggle('claude', claudeOn);
1649
1725
  onAgentToggle('cursor', cursorOn);
1650
1726
  onAgentToggle('codex', codexOn);
1651
- onAgentToggle('ccc', cccOn);
1727
+ onAgentToggle('ccc', cccOn);
1728
+ onAgentToggle('dsh', dshOn);
1652
1729
  updateDefaultAgentToggles();
1653
1730
 
1654
1731
  // Cursor path placeholder/hint:把已探测到的路径显示为占位
@@ -1672,7 +1749,7 @@ function renderStep2() {
1672
1749
  * 收集"待落地到 config.json 的扁平 vars"。
1673
1750
  *
1674
1751
  * - 飞书字段始终收集
1675
- * - 四个 Agent 的 enabled 状态都显式下发,让 config.json 持久化用户的最新开关偏好
1752
+ * - 五个 Agent 的 enabled 状态都显式下发,让 config.json 持久化用户的最新开关偏好
1676
1753
  * - Agent 字段仅在该 Agent 开关启用时收集;未启用的 Agent 不下发其它字段,
1677
1754
  * 服务端 deepMerge 会保留 config.json 中已有值(避免关闭开关时误清空旧配置)
1678
1755
  */
@@ -1698,14 +1775,16 @@ function collectAllFields() {
1698
1775
  vars.CHATCCC_CLAUDE_ENABLED = !!state.agentsEnabled.claude;
1699
1776
  vars.CHATCCC_CURSOR_ENABLED = !!state.agentsEnabled.cursor;
1700
1777
  vars.CHATCCC_CODEX_ENABLED = !!state.agentsEnabled.codex;
1701
- vars.CHATCCC_CCC_ENABLED = !!state.agentsEnabled.ccc;
1778
+ vars.CHATCCC_CCC_ENABLED = !!state.agentsEnabled.ccc;
1779
+ vars.CHATCCC_DSH_ENABLED = !!state.agentsEnabled.dsh;
1702
1780
  if (!state.defaultAgent || !state.agentsEnabled[state.defaultAgent]) {
1703
1781
  state.defaultAgent = firstEnabledAgent();
1704
1782
  }
1705
1783
  vars.CHATCCC_CLAUDE_DEFAULT_AGENT = state.defaultAgent === 'claude';
1706
1784
  vars.CHATCCC_CURSOR_DEFAULT_AGENT = state.defaultAgent === 'cursor';
1707
1785
  vars.CHATCCC_CODEX_DEFAULT_AGENT = state.defaultAgent === 'codex';
1708
- vars.CHATCCC_CCC_DEFAULT_AGENT = state.defaultAgent === 'ccc';
1786
+ vars.CHATCCC_CCC_DEFAULT_AGENT = state.defaultAgent === 'ccc';
1787
+ vars.CHATCCC_DSH_DEFAULT_AGENT = state.defaultAgent === 'dsh';
1709
1788
  if (state.agentsEnabled.claude) {
1710
1789
  AGENT_FIELDS.claude.forEach(function(key){
1711
1790
  var el = document.getElementById('field-' + key);
@@ -1726,7 +1805,7 @@ function collectAllFields() {
1726
1805
  else if (el.value.trim()) vars[key] = el.value.trim();
1727
1806
  });
1728
1807
  }
1729
- if (state.agentsEnabled.ccc) {
1808
+ if (state.agentsEnabled.ccc) {
1730
1809
  AGENT_FIELDS.ccc.forEach(function(key){
1731
1810
  if (key === 'CHATCCC_CCC_CONTEXT_WINDOW') {
1732
1811
  var cw = contextWindowToTokensValue('field-');
@@ -1736,7 +1815,13 @@ function collectAllFields() {
1736
1815
  var el = document.getElementById('field-' + key);
1737
1816
  if (el && el.value.trim()) vars[key] = el.value.trim();
1738
1817
  });
1739
- }
1818
+ }
1819
+ if (state.agentsEnabled.dsh) {
1820
+ AGENT_FIELDS.dsh.forEach(function(key){
1821
+ var el = document.getElementById('field-' + key);
1822
+ if (el && el.value.trim()) vars[key] = el.value.trim();
1823
+ });
1824
+ }
1740
1825
  return vars;
1741
1826
  }
1742
1827
 
@@ -1776,7 +1861,8 @@ function renderStep3() {
1776
1861
  if (state.agentsEnabled.claude) enabledList.push('claude');
1777
1862
  if (state.agentsEnabled.cursor) enabledList.push('cursor');
1778
1863
  if (state.agentsEnabled.codex) enabledList.push('codex');
1779
- if (state.agentsEnabled.ccc) enabledList.push('ccc');
1864
+ if (state.agentsEnabled.ccc) enabledList.push('ccc');
1865
+ if (state.agentsEnabled.dsh) enabledList.push('dsh');
1780
1866
  if (enabledList.length === 0) {
1781
1867
  lines.push('<div style="color:#ef4444">未启用任何 AI Agent</div>');
1782
1868
  } else {
@@ -1807,7 +1893,7 @@ function renderStep3() {
1807
1893
  lines.push('<div class="config-row"><span class="key">备选模型</span><span class="val">' + (vars.CHATCCC_CODEX_ALTERNATIVE_MODEL || '(留空)') + '</span></div>');
1808
1894
  lines.push('<div class="config-row"><span class="key">Effort</span><span class="val">' + (vars.CHATCCC_CODEX_EFFORT || '(留空)') + '</span></div>');
1809
1895
  lines.push('<div class="config-row"><span class="key">Fast 模式</span><span class="val">' + (vars.CHATCCC_CODEX_FAST_MODE ? '已启用' : '已禁用') + '</span></div>');
1810
- } else if (t === 'ccc') {
1896
+ } else if (t === 'ccc') {
1811
1897
  lines.push('<h4 style="margin:10px 0 4px;color:#334155">CCC Agent</h4>');
1812
1898
  lines.push('<div class="config-row"><span class="key">API Key</span><span class="val">' + (vars.CHATCCC_CCC_API_KEY ? '***已设置***' : '(留空)') + '</span></div>');
1813
1899
  lines.push('<div class="config-row"><span class="key">Base URL</span><span class="val">' + (vars.CHATCCC_CCC_BASE_URL || '(留空)') + '</span></div>');
@@ -1815,7 +1901,12 @@ function renderStep3() {
1815
1901
  lines.push('<div class="config-row"><span class="key">子模型</span><span class="val">' + (vars.CHATCCC_CCC_SUB_MODEL || '(留空,跟随主模型)') + '</span></div>');
1816
1902
  lines.push('<div class="config-row"><span class="key">备选模型</span><span class="val">' + (vars.CHATCCC_CCC_ALTERNATIVE_MODEL || '(留空)') + '</span></div>');
1817
1903
  lines.push('<div class="config-row"><span class="key">Effort</span><span class="val">' + (vars.CHATCCC_CCC_EFFORT || '(留空)') + '</span></div>');
1818
- lines.push('<div class="config-row"><span class="key">上下文窗口</span><span class="val">' + contextWindowTokensLabel(vars.CHATCCC_CCC_CONTEXT_WINDOW || 1048576) + '</span></div>');
1904
+ lines.push('<div class="config-row"><span class="key">上下文窗口</span><span class="val">' + contextWindowTokensLabel(vars.CHATCCC_CCC_CONTEXT_WINDOW || 1048576) + '</span></div>');
1905
+ } else if (t === 'dsh') {
1906
+ lines.push('<h4 style="margin:10px 0 4px;color:#334155">DeepSeek Harness</h4>');
1907
+ lines.push('<div class="config-row"><span class="key">API Key</span><span class="val">' + (vars.CHATCCC_DSH_API_KEY ? '***已设置***' : '(留空)') + '</span></div>');
1908
+ lines.push('<div class="config-row"><span class="key">Base URL</span><span class="val">' + (vars.CHATCCC_DSH_BASE_URL || '(默认)') + '</span></div>');
1909
+ lines.push('<div class="config-row"><span class="key">模型</span><span class="val">' + (vars.CHATCCC_DSH_MODEL || 'deepseek-v4-flash') + '</span></div>');
1819
1910
  }
1820
1911
  });
1821
1912
  document.getElementById('review-content').innerHTML = lines.join('');
@@ -1843,18 +1934,21 @@ async function setDashboardDefaultAgent(agent, enabled) {
1843
1934
  CHATCCC_CLAUDE_DEFAULT_AGENT: agent === 'claude',
1844
1935
  CHATCCC_CURSOR_DEFAULT_AGENT: agent === 'cursor',
1845
1936
  CHATCCC_CODEX_DEFAULT_AGENT: agent === 'codex',
1846
- CHATCCC_CCC_DEFAULT_AGENT: agent === 'ccc'
1937
+ CHATCCC_CCC_DEFAULT_AGENT: agent === 'ccc',
1938
+ CHATCCC_DSH_DEFAULT_AGENT: agent === 'dsh'
1847
1939
  };
1848
1940
  var result = await api('/api/config', 'POST', { vars: vars });
1849
1941
  if (result.ok) {
1850
1942
  state.config.claude = state.config.claude || {};
1851
1943
  state.config.cursor = state.config.cursor || {};
1852
1944
  state.config.codex = state.config.codex || {};
1853
- state.config.ccc = state.config.ccc || {};
1945
+ state.config.ccc = state.config.ccc || {};
1946
+ state.config.dsh = state.config.dsh || {};
1854
1947
  state.config.claude.defaultAgent = agent === 'claude';
1855
1948
  state.config.cursor.defaultAgent = agent === 'cursor';
1856
1949
  state.config.codex.defaultAgent = agent === 'codex';
1857
- state.config.ccc.defaultAgent = agent === 'ccc';
1950
+ state.config.ccc.defaultAgent = agent === 'ccc';
1951
+ state.config.dsh.defaultAgent = agent === 'dsh';
1858
1952
  updateDefaultAgentToggles();
1859
1953
  toastConfigApplyResult(result, '默认 Agent 已更新');
1860
1954
  } else {
@@ -1971,9 +2065,10 @@ function updateDashboardUI() {
1971
2065
  var claudeOn = isAgentEnabled(c.claude, CLAUDE_FALLBACK_KEYS);
1972
2066
  var cursorOn = isAgentEnabled(c.cursor, CURSOR_FALLBACK_KEYS);
1973
2067
  var codexOn = isAgentEnabled(c.codex, CODEX_FALLBACK_KEYS);
1974
- var cccOn = isAgentEnabled(c.ccc, CCC_FALLBACK_KEYS);
1975
- state.agentsEnabled = { claude: claudeOn, cursor: cursorOn, codex: codexOn, ccc: cccOn };
1976
- state.defaultAgent = resolveDefaultAgentFromConfig(c, claudeOn, cursorOn, codexOn, cccOn);
2068
+ var cccOn = isAgentEnabled(c.ccc, CCC_FALLBACK_KEYS);
2069
+ var dshOn = isAgentEnabled(c.dsh, DSH_FALLBACK_KEYS);
2070
+ state.agentsEnabled = { claude: claudeOn, cursor: cursorOn, codex: codexOn, ccc: cccOn, dsh: dshOn };
2071
+ state.defaultAgent = resolveDefaultAgentFromConfig(c, claudeOn, cursorOn, codexOn, cccOn, dshOn);
1977
2072
 
1978
2073
  // 微信 iLink 平台开关:同步复选框和标签
1979
2074
  var ilinkEnabled = c.platforms && c.platforms.ilink ? c.platforms.ilink.enabled !== false : true;
@@ -2005,11 +2100,12 @@ function updateDashboardUI() {
2005
2100
  document.getElementById('dash-claude').style.display = claudeOn ? '' : 'none';
2006
2101
  document.getElementById('dash-cursor').style.display = cursorOn ? '' : 'none';
2007
2102
  document.getElementById('dash-codex').style.display = codexOn ? '' : 'none';
2008
- document.getElementById('dash-ccc').style.display = cccOn ? '' : 'none';
2103
+ document.getElementById('dash-ccc').style.display = cccOn ? '' : 'none';
2104
+ document.getElementById('dash-dsh').style.display = dshOn ? '' : 'none';
2009
2105
  updateDefaultAgentToggles();
2010
- // 四个都未启用时给一个空态提示,引导用户去配置向导启用
2106
+ // 五个都未启用时给一个空态提示,引导用户去配置向导启用
2011
2107
  var emptyHint = document.getElementById('dash-no-agent-hint');
2012
- if (emptyHint) emptyHint.style.display = (!claudeOn && !cursorOn && !codexOn && !cccOn) ? '' : 'none';
2108
+ if (emptyHint) emptyHint.style.display = (!claudeOn && !cursorOn && !codexOn && !cccOn && !dshOn) ? '' : 'none';
2013
2109
 
2014
2110
  document.getElementById('cfg-ANTHROPIC_MODEL').textContent = (c.claude && c.claude.model) || '(留空)';
2015
2111
  document.getElementById('cfg-ANTHROPIC_SUBAGENT_MODEL').textContent = (c.claude && c.claude.subagentModel) || '(留空)';
@@ -2035,7 +2131,13 @@ function updateDashboardUI() {
2035
2131
  document.getElementById('cfg-CCC_PROVIDER').textContent = (c.ccc && c.ccc.provider) ? c.ccc.provider : '(跟随 DeepCCC 内核配置)';
2036
2132
  document.getElementById('cfg-CCC_MODEL').textContent = (c.ccc && c.ccc.model) || '(留空)';
2037
2133
  document.getElementById('cfg-CCC_SUB_MODEL').textContent = (c.ccc && c.ccc.subModel) || '(留空,跟随主模型)';
2038
- document.getElementById('cfg-CCC_ALTERNATIVE_MODEL').textContent = (c.ccc && c.ccc.alternativeModel) || '(留空)';
2134
+ document.getElementById('cfg-CCC_ALTERNATIVE_MODEL').textContent = (c.ccc && c.ccc.alternativeModel) || '(留空)';
2135
+ document.getElementById('cfg-DSH_API_KEY').textContent = (c.dsh && c.dsh.apiKey) ? '***已设置***' : '(留空)';
2136
+ document.getElementById('cfg-DSH_BASE_URL').textContent = (c.dsh && c.dsh.baseUrl) || 'https://api.deepseek.com/v1';
2137
+ document.getElementById('cfg-DSH_MODEL').textContent = (c.dsh && c.dsh.model) || 'deepseek-v4-flash';
2138
+ document.getElementById('cfg-DSH_PROVIDER').textContent = (c.dsh && c.dsh.provider) || 'deepseek-official';
2139
+ document.getElementById('cfg-DSH_MAX_TOKENS').textContent = String((c.dsh && c.dsh.maxTokens) || 49152);
2140
+ engineRefreshStatus('dsh');
2039
2141
  }
2040
2142
 
2041
2143
  function pollStatus() {
@@ -2091,7 +2193,7 @@ function editSection(section) {
2091
2193
  else if (section === 'chromeDevtools') fields = CHROME_DEVTOOLS_FIELDS;
2092
2194
  else fields = AGENT_FIELDS[section] || [];
2093
2195
 
2094
- var titleMap = { feishu: '飞书', webUi: 'Web UI', chromeDevtools: 'Chrome CDP', claude: 'Claude Agent', cursor: 'Cursor Agent', codex: 'Codex Agent', ccc: 'CCC Agent' };
2196
+ var titleMap = { feishu: '飞书', webUi: 'Web UI', chromeDevtools: 'Chrome CDP', claude: 'Claude Agent', cursor: 'Cursor Agent', codex: 'Codex Agent', ccc: 'CCC Agent', dsh: 'DeepSeek Harness' };
2095
2197
  document.getElementById('edit-modal-title').textContent = '编辑 ' + (titleMap[section] || section);
2096
2198
 
2097
2199
  document.getElementById('edit-modal-effect').textContent = configEffectHint(section);
@@ -2112,7 +2214,8 @@ function editSection(section) {
2112
2214
  'CHATCCC_CODEX_FAST_MODE': 'Fast 模式',
2113
2215
  'CHATCCC_CCC_API_KEY': 'API Key', 'CHATCCC_CCC_BASE_URL': 'Base URL',
2114
2216
  'CHATCCC_CCC_PROVIDER': 'API 协议(选填)',
2115
- 'CHATCCC_CCC_MODEL': '模型', 'CHATCCC_CCC_SUB_MODEL': '子模型', 'CHATCCC_CCC_ALTERNATIVE_MODEL': '备选模型', 'CHATCCC_CCC_EFFORT': 'Effort', 'CHATCCC_CCC_CONTEXT_WINDOW': '上下文窗口'
2217
+ 'CHATCCC_CCC_MODEL': '模型', 'CHATCCC_CCC_SUB_MODEL': '子模型', 'CHATCCC_CCC_ALTERNATIVE_MODEL': '备选模型', 'CHATCCC_CCC_EFFORT': 'Effort', 'CHATCCC_CCC_CONTEXT_WINDOW': '上下文窗口',
2218
+ 'CHATCCC_DSH_API_KEY': 'API Key', 'CHATCCC_DSH_BASE_URL': 'Base URL', 'CHATCCC_DSH_MODEL': '模型', 'CHATCCC_DSH_PROVIDER': 'Provider 路由', 'CHATCCC_DSH_MAX_TOKENS': '单次最大输出 Tokens'
2116
2219
  };
2117
2220
  var hintMap = {
2118
2221
  'CHATCCC_WEB_UI_OPEN_ON_START': '关闭后可继续手动访问 http://localhost:<端口>/;/restart、/update 和 Web UI 重启无论此项为何值都不会自动打开。',
@@ -2160,7 +2263,7 @@ function editSection(section) {
2160
2263
  else if (key === 'CHATCCC_CODEX_ALTERNATIVE_MODEL') val = state.config.codex.alternativeModel || '';
2161
2264
  else if (key === 'CHATCCC_CODEX_EFFORT') val = state.config.codex.effort || '';
2162
2265
  else if (key === 'CHATCCC_CODEX_FAST_MODE') val = state.config.codex.fastMode === true ? 'true' : 'false';
2163
- } else if (section === 'ccc' && state.config.ccc) {
2266
+ } else if (section === 'ccc' && state.config.ccc) {
2164
2267
  if (key === 'CHATCCC_CCC_API_KEY') val = state.config.ccc.DEEPSEEK_API_KEY || '';
2165
2268
  else if (key === 'CHATCCC_CCC_BASE_URL') val = state.config.ccc.DEEPSEEK_BASE_URL || '';
2166
2269
  else if (key === 'CHATCCC_CCC_PROVIDER') val = state.config.ccc.provider || '';
@@ -2168,8 +2271,14 @@ function editSection(section) {
2168
2271
  else if (key === 'CHATCCC_CCC_SUB_MODEL') val = state.config.ccc.subModel || '';
2169
2272
  else if (key === 'CHATCCC_CCC_ALTERNATIVE_MODEL') val = state.config.ccc.alternativeModel || '';
2170
2273
  else if (key === 'CHATCCC_CCC_EFFORT') val = state.config.ccc.effort || '';
2171
- else if (key === 'CHATCCC_CCC_CONTEXT_WINDOW') val = state.config.ccc.contextWindow || '1048576';
2172
- }
2274
+ else if (key === 'CHATCCC_CCC_CONTEXT_WINDOW') val = state.config.ccc.contextWindow || '1048576';
2275
+ } else if (section === 'dsh' && state.config.dsh) {
2276
+ if (key === 'CHATCCC_DSH_API_KEY') val = state.config.dsh.apiKey || '';
2277
+ else if (key === 'CHATCCC_DSH_BASE_URL') val = state.config.dsh.baseUrl || '';
2278
+ else if (key === 'CHATCCC_DSH_MODEL') val = state.config.dsh.model || '';
2279
+ else if (key === 'CHATCCC_DSH_PROVIDER') val = state.config.dsh.provider || '';
2280
+ else if (key === 'CHATCCC_DSH_MAX_TOKENS') val = state.config.dsh.maxTokens || '49152';
2281
+ }
2173
2282
  }
2174
2283
  var isSecret = key.includes('SECRET') || key.includes('API_KEY');
2175
2284
  if (key === 'CHATCCC_WEB_UI_OPEN_ON_START' || key === 'CHATCCC_CHROME_DEVTOOLS_ENABLED' || key === 'CHATCCC_CODEX_FAST_MODE') {
@@ -2319,124 +2428,95 @@ function validateCli(tool) {
2319
2428
  });
2320
2429
  }
2321
2430
 
2322
- // ---- Claude Code 引擎(Agent SDK)按需安装 ----
2323
- var claudeEnginePollTimer = null;
2324
- var claudeEngineState = null;
2325
-
2326
- // Claude Code 开关:从关闭 → 打开时弹窗确认(SDK 必装才能使用)。
2327
- // 页面初始化(按已有 config 设置开关)直接调 onAgentToggle,不经由此函数,不会误弹窗。
2328
- function onClaudeToggle(el) {
2329
- var enabled = el.checked;
2330
- if (!enabled) {
2331
- onAgentToggle('claude', false);
2332
- return;
2333
- }
2334
- // 实时查询后端安装状态(不依赖可能过期的 claudeEngineState 缓存),
2335
- // 已安装/正在安装时直接打开开关,不重复安装。
2336
- api('/api/claude-sdk/status', 'GET').then(function(s){
2337
- if (!s || !s.phase) { el.checked = false; return; }
2338
- claudeEngineState = s;
2339
- var sdkReady = s.installed === true || s.phase === 'done' ||
2340
- s.phase === 'downloading' || s.phase === 'installing' || s.phase === 'detecting';
2341
- if (sdkReady) {
2342
- // 已安装 / 正在安装 / 正在检测:直接打开开关,不再触发安装
2343
- onAgentToggle('claude', true);
2344
- return;
2345
- }
2346
- if (!confirm('Claude Code 必须先安装 Claude Code SDK 才能使用(必装)。是否立即安装?')) {
2347
- el.checked = false; // 用户取消 → 回滚开关为关闭
2348
- return;
2349
- }
2350
- onAgentToggle('claude', true);
2351
- installClaudeEngine();
2352
- }).catch(function(){
2353
- el.checked = false; // 状态查询失败 → 回滚开关,避免误判
2354
- });
2355
- }
2356
-
2357
- function claudeEngineEl(id) { return document.getElementById(id); }
2358
-
2359
- function claudeEngineRenderStatus(s) {
2360
- var el = claudeEngineEl('claude-engine-status');
2361
- if (!el) return;
2362
- var phase = s.phase;
2363
- var text = s.message || '';
2364
- var color = '#64748b';
2365
- if (phase === 'done') color = '#16a34a';
2366
- else if (phase === 'error') color = '#ef4444';
2367
- else if (phase === 'downloading' || phase === 'installing') color = '#3b82f6';
2368
- if (!text && phase === 'idle') {
2369
- // 初始/空闲状态给出明确文案,而不是“未知状态”
2370
- text = s.installed ? ('已安装 v' + (s.version || '未知版本')) : '未安装';
2371
- }
2372
- el.innerHTML = '<span style="color:' + color + '">' + (text ? text : '未知状态') + '</span>';
2373
- if (s.error) el.innerHTML += '<br><span style="color:#ef4444;font-size:12px">' + s.error + '</span>';
2374
- }
2375
-
2376
- function claudeEngineRenderProgress(p) {
2377
- var wrap = claudeEngineEl('claude-engine-progress-wrap');
2378
- if (!wrap) return;
2379
- if (p && (p.phase === 'downloading' || p.phase === 'installing' || p.phase === 'detecting')) {
2380
- wrap.style.display = 'block';
2381
- claudeEngineEl('claude-engine-progress-bar').style.width = (p.percent || 0) + '%';
2382
- claudeEngineEl('claude-engine-progress-text').textContent = p.message || '';
2383
- } else {
2384
- wrap.style.display = 'none';
2385
- }
2386
- }
2387
-
2388
- function claudeEngineRefreshStatus() {
2389
- api('/api/claude-sdk/status', 'GET').then(function(s){
2390
- if (!s || !s.phase) return;
2391
- claudeEngineState = s;
2392
- claudeEngineRenderStatus(s);
2393
- claudeEngineRenderProgress(s);
2394
- var btn = claudeEngineEl('claude-engine-install-btn');
2395
- if (btn) {
2396
- if (s.phase === 'done' || s.installed) {
2397
- btn.textContent = '重新安装 Claude Code SDK';
2398
- btn.disabled = false;
2399
- } else if (s.phase === 'downloading' || s.phase === 'installing' || s.phase === 'detecting') {
2400
- btn.textContent = '安装中…';
2401
- btn.disabled = true;
2402
- } else {
2403
- btn.textContent = '安装 Claude Code SDK';
2404
- btn.disabled = false;
2405
- }
2406
- }
2407
- if (s.phase === 'downloading' || s.phase === 'installing' || s.phase === 'detecting') {
2408
- claudeEnginePollTimer = setTimeout(claudeEngineRefreshStatus, 600);
2409
- } else if (claudeEnginePollTimer) {
2410
- clearTimeout(claudeEnginePollTimer);
2411
- claudeEnginePollTimer = null;
2412
- }
2413
- }).catch(function(){
2414
- // 网络/服务异常时停止轮询,避免无限重试
2415
- if (claudeEnginePollTimer) { clearTimeout(claudeEnginePollTimer); claudeEnginePollTimer = null; }
2416
- });
2417
- }
2418
-
2419
- function installClaudeEngine() {
2420
- var btn = claudeEngineEl('claude-engine-install-btn');
2421
- if (btn) btn.disabled = true;
2422
- api('/api/claude-sdk/install', 'POST').then(function(r){
2423
- if (r.ok) {
2424
- claudeEngineRefreshStatus();
2425
- } else {
2426
- claudeEngineRenderStatus({ phase: 'error', message: '启动安装失败', error: r.error || '' });
2427
- if (btn) btn.disabled = false;
2428
- }
2429
- }).catch(function(e){
2430
- claudeEngineRenderStatus({ phase: 'error', message: '请求失败', error: String(e) });
2431
- if (btn) btn.disabled = false;
2432
- });
2433
- }
2431
+ // ---- 通用 Agent 引擎:一次点击,后端自动执行全部原子安装步骤 ----
2432
+ var enginePollTimers = {};
2433
+ function escapeHtml(value) {
2434
+ return String(value == null ? '' : value).replace(/[&<>"']/g, function(char){
2435
+ return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[char];
2436
+ });
2437
+ }
2438
+
2439
+ function onInstallableAgentToggle(engineId, el) {
2440
+ if (!el.checked) { onAgentToggle(engineId, false); return; }
2441
+ onAgentToggle(engineId, true);
2442
+ api('/api/engines/' + encodeURIComponent(engineId) + '/status', 'GET').then(function(status){
2443
+ renderEngineStatus(engineId, status);
2444
+ if (!status.installed && !status.running) installEngine(engineId);
2445
+ else if (status.running) scheduleEnginePoll(engineId);
2446
+ }).catch(function(error){
2447
+ el.checked = false;
2448
+ onAgentToggle(engineId, false);
2449
+ toast('引擎状态查询失败: ' + String(error), 'error');
2450
+ });
2451
+ }
2452
+
2453
+ function renderEngineStatus(engineId, status) {
2454
+ var ids = [engineId + '-engine-status', engineId + '-dashboard-engine-status'];
2455
+ var job = status.job;
2456
+ var state = job && job.state;
2457
+ var text = status.installed ? ('已安装 v' + (status.version || '未知版本')) : '未安装';
2458
+ var color = status.installed ? '#15803d' : '#64748b';
2459
+ if (state === 'running') { text = '安装中 ' + (job.percent || 0) + '%'; color = '#1d4ed8'; }
2460
+ if (state === 'failed') { text = '安装失败'; color = '#dc2626'; }
2461
+ ids.forEach(function(id){
2462
+ var el = document.getElementById(id);
2463
+ if (!el) return;
2464
+ el.innerHTML = '<span style="color:' + color + '">' + text + '</span>' +
2465
+ (job && job.error ? '<br><span style="color:#dc2626;font-size:12px">' + escapeHtml(job.error) + '</span>' : '');
2466
+ });
2467
+ [engineId + '-engine-steps', engineId + '-dashboard-engine-steps'].forEach(function(id){
2468
+ var root = document.getElementById(id);
2469
+ if (!root) return;
2470
+ root.innerHTML = job && job.steps ? job.steps.map(function(step){
2471
+ var icon = step.state === 'completed' ? '✓' : step.state === 'failed' ? '!' : step.state === 'running' ? '●' : '○';
2472
+ return '<div class="engine-step ' + step.state + '"><span class="engine-step-icon">' + icon + '</span><span>' +
2473
+ escapeHtml(step.label + (step.message && step.message !== '等待中' ? ' · ' + step.message : '')) +
2474
+ '</span><span class="engine-step-percent">' + (step.state === 'running' ? step.percent + '%' : '') + '</span></div>';
2475
+ }).join('') : '';
2476
+ });
2477
+ [engineId + '-engine-install-btn', engineId + '-dashboard-engine-install-btn'].forEach(function(id){
2478
+ var button = document.getElementById(id);
2479
+ if (!button) return;
2480
+ button.disabled = state === 'running';
2481
+ button.textContent = state === 'running' ? '自动安装中…' : state === 'failed' ? '重试' : status.installed ? '原子升级/重新安装' : '安装并启用';
2482
+ });
2483
+ }
2484
+
2485
+ function scheduleEnginePoll(engineId) {
2486
+ if (enginePollTimers[engineId]) clearTimeout(enginePollTimers[engineId]);
2487
+ enginePollTimers[engineId] = setTimeout(function(){ engineRefreshStatus(engineId); }, 800);
2488
+ }
2489
+
2490
+ function engineRefreshStatus(engineId) {
2491
+ return api('/api/engines/' + encodeURIComponent(engineId) + '/status', 'GET').then(function(status){
2492
+ renderEngineStatus(engineId, status);
2493
+ if (status.running || (status.job && status.job.state === 'running')) scheduleEnginePoll(engineId);
2494
+ return status;
2495
+ });
2496
+ }
2497
+
2498
+ function installEngine(engineId) {
2499
+ var toggle = document.getElementById('agent-enable-' + engineId);
2500
+ if (toggle && !toggle.checked) {
2501
+ toggle.checked = true;
2502
+ onAgentToggle(engineId, true);
2503
+ }
2504
+ return api('/api/engines/' + encodeURIComponent(engineId) + '/install', 'POST').then(function(result){
2505
+ if (!result.ok) throw new Error(result.error || '启动安装失败');
2506
+ renderEngineStatus(engineId, { installed: false, running: true, job: result.job });
2507
+ scheduleEnginePoll(engineId);
2508
+ return result;
2509
+ }).catch(function(error){
2510
+ renderEngineStatus(engineId, { installed: false, running: false, job: { state: 'failed', error: String(error), steps: [] } });
2511
+ toast('引擎安装失败: ' + String(error), 'error');
2512
+ });
2513
+ }
2434
2514
 
2435
2515
  // ---- Start ----
2436
2516
  init();
2437
2517
 
2438
- // 进入 dashboard/向导后初始化 Claude 引擎状态(页面元素此时已存在)
2439
- setTimeout(claudeEngineRefreshStatus, 300);
2518
+ // 页面刷新后从持久化任务文件恢复每一步的安装进度。
2519
+ setTimeout(function(){ engineRefreshStatus('claude'); engineRefreshStatus('dsh'); }, 300);
2440
2520
  </script>
2441
2521
  </body>
2442
2522
  </html>`;
@@ -2470,15 +2550,21 @@ async function handleRequest(req, res) {
2470
2550
  return handleValidate(req, res);
2471
2551
  if (url === "/api/ilink/forget" && method === "POST")
2472
2552
  return handleForgetIlink(req, res);
2473
- if (url === "/api/claude-sdk/status" && method === "GET")
2474
- return handleClaudeSdkStatus(req, res);
2475
- if (url === "/api/claude-sdk/install" && method === "POST")
2476
- return handleClaudeSdkInstall(req, res);
2553
+ const engineStatusMatch = pathname.match(/^\/api\/engines\/([^/]+)\/status$/);
2554
+ if (engineStatusMatch && method === "GET")
2555
+ return handleEngineStatus(decodeURIComponent(engineStatusMatch[1]), res);
2556
+ const engineInstallMatch = pathname.match(/^\/api\/engines\/([^/]+)\/install$/);
2557
+ if (engineInstallMatch && method === "POST")
2558
+ return handleEngineInstall(decodeURIComponent(engineInstallMatch[1]), res);
2477
2559
  if (method === "GET" && (pathname === "/agent-team" || pathname === "/agent-team/")) {
2478
2560
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2479
2561
  res.end(AGENT_TEAM_PAGE_HTML);
2480
2562
  return;
2481
2563
  }
2564
+ if (pathname.startsWith("/api/")) {
2565
+ jsonReply(res, 404, { error: "Not found" });
2566
+ return;
2567
+ }
2482
2568
  // Serve HTML page for all other GET requests
2483
2569
  if (method === "GET") {
2484
2570
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });