@walkhi/code-relax 0.1.0-beta.10 → 0.1.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +44 -54
  2. package/dist/bin/codex-remote.mjs +34 -60
  3. package/dist/bin/migrate-relay-db.mjs +104 -0
  4. package/dist/bin/self-relay-server.mjs +17 -10
  5. package/dist/shared/app-server-events.cjs +15 -3
  6. package/dist/shared/file-mentions.cjs +35 -0
  7. package/dist/shared/server-requests.cjs +86 -58
  8. package/dist/skills/code-relax/SKILL.md +1 -1
  9. package/dist/src/desktop-launcher.mjs +6 -11
  10. package/dist/src/file-upload-session.mjs +124 -0
  11. package/dist/src/lan-socket-server.mjs +51 -8
  12. package/dist/src/managed-app-server.mjs +21 -10
  13. package/dist/src/onboarding.mjs +3 -3
  14. package/dist/src/platform/windows/codex-runtime.mjs +138 -4
  15. package/dist/src/platform/windows/desktop-host.mjs +1 -2
  16. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
  17. package/dist/src/platform/windows/desktop-shortcuts.ps1 +6 -13
  18. package/dist/src/platform/windows/resident-start.js +8 -0
  19. package/dist/src/platform/windows/resident-startup.ps1 +6 -3
  20. package/dist/src/platform/windows/service-host.mjs +0 -9
  21. package/dist/src/self-relay/admin-page.mjs +1 -1
  22. package/dist/src/self-relay/admin-state.mjs +8 -6
  23. package/dist/src/self-relay/connector.mjs +84 -12
  24. package/dist/src/self-relay/email-accounts.mjs +125 -0
  25. package/dist/src/self-relay/lifecycle.mjs +52 -100
  26. package/dist/src/self-relay/mysql-state.mjs +104 -0
  27. package/dist/src/self-relay/p2p-probe.mjs +1 -1
  28. package/dist/src/self-relay/server.mjs +155 -38
  29. package/dist/src/server.mjs +736 -610
  30. package/dist/src/service-doctor.mjs +1 -1
  31. package/dist/src/service-install.mjs +1 -4
  32. package/dist/src/shared-app-server.mjs +274 -272
  33. package/dist/src/shared-catalog.mjs +1 -11
  34. package/dist/src/shared-recovery.mjs +5 -5
  35. package/dist/src/skill-install.mjs +0 -13
  36. package/package.json +2 -1
  37. package/dist/src/debug-ui.mjs +0 -131
  38. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
  39. package/dist/web/activity-view.js +0 -283
  40. package/dist/web/approval-ui.js +0 -188
  41. package/dist/web/capabilities.js +0 -50
  42. package/dist/web/chat-transport.js +0 -196
  43. package/dist/web/chat.css +0 -778
  44. package/dist/web/chat.js +0 -3815
  45. package/dist/web/client-platform.js +0 -11
  46. package/dist/web/community.css +0 -82
  47. package/dist/web/community.html +0 -36
  48. package/dist/web/composer-controller.js +0 -97
  49. package/dist/web/conversation-controller.js +0 -99
  50. package/dist/web/disclosure-state-controller.js +0 -95
  51. package/dist/web/draft-controller.js +0 -103
  52. package/dist/web/harmony-platform.js +0 -52
  53. package/dist/web/highlight-powershell.min.js +0 -39
  54. package/dist/web/highlight.min.js +0 -1262
  55. package/dist/web/history-cache.js +0 -240
  56. package/dist/web/history-controller.js +0 -167
  57. package/dist/web/index.html +0 -288
  58. package/dist/web/lan-session.js +0 -122
  59. package/dist/web/link-action-controller.js +0 -212
  60. package/dist/web/message-send-controller.js +0 -177
  61. package/dist/web/message-view.js +0 -98
  62. package/dist/web/p2p-data-channel.js +0 -50
  63. package/dist/web/p2p-diagnostics.js +0 -28
  64. package/dist/web/p2p-probe.js +0 -123
  65. package/dist/web/page-resume.js +0 -28
  66. package/dist/web/pending-message-store.js +0 -108
  67. package/dist/web/queue-controller.js +0 -82
  68. package/dist/web/resources.json +0 -1
  69. package/dist/web/self-relay-session.js +0 -129
  70. package/dist/web/server-requests.js +0 -67
  71. package/dist/web/station-connection-controller.js +0 -75
  72. package/dist/web/task-list-view.js +0 -289
  73. package/dist/web/thread-attention-controller.js +0 -124
  74. package/dist/web/thread-context-controller.js +0 -30
  75. package/dist/web/thread-list-controller.js +0 -61
  76. package/dist/web/thread-list-sync.js +0 -86
  77. package/dist/web/thread-title-controller.js +0 -57
  78. package/dist/web/timeline-formatters.js +0 -249
  79. package/dist/web/timeline-reducer.js +0 -77
  80. package/dist/web/timeline-renderer.js +0 -161
  81. package/dist/web/timeline-scroll-controller.js +0 -50
  82. package/dist/web/timeline-sync.js +0 -67
  83. package/dist/web/usage-controller.js +0 -258
  84. package/dist/web/vendor/README.md +0 -14
  85. package/dist/web/vendor/highlight-powershell.min.js +0 -39
  86. package/dist/web/vendor/highlight.LICENSE.txt +0 -29
  87. package/dist/web/vendor/highlight.min.js +0 -1262
  88. package/dist/web/vendor/lucide.LICENSE.txt +0 -17
@@ -1,17 +1,7 @@
1
1
  import events from '../shared/app-server-events.cjs';
2
2
 
3
3
  export function sharedThreadProjectId(thread, projects) {
4
- if (thread.projectId) return thread.projectId;
5
- const local = thread.environments?.find(environment => environment.environmentId === 'local');
6
- if (local && Array.isArray(local.runtimeWorkspaceRoots)) {
7
- for (const root of local.runtimeWorkspaceRoots) {
8
- const projectId = events.threadProjectId({ cwd: root }, projects);
9
- if (projectId) return projectId;
10
- }
11
- return null;
12
- }
13
- // Unloaded Desktop history omits environments and often has no persisted project ID.
14
- return events.threadProjectId({ cwd: thread.cwd }, projects);
4
+ return events.catalogThreadProjectId(thread, projects);
15
5
  }
16
6
 
17
7
  // Mobile catalog order comes entirely from app-server authority, independent of Desktop UI state.
@@ -18,9 +18,9 @@ const active = value => value && ['scheduled', 'running'].includes(value.status)
18
18
  const recoverySteps = () => [
19
19
  ['checking', '检查启动条件'], ['stopping-desktop', '关闭当前 Codex'],
20
20
  ['starting-app-server', '确认 Codex 内核'], ['starting-desktop', '重新打开 Codex'],
21
- ['checking-execution', '确认手机控制就绪'],
21
+ ['checking-execution', '确认移动端控制就绪'],
22
22
  ].map(([id, label]) => ({ id, label, status: 'pending' }));
23
- export const recoveryWarning = 'Codex Desktop 需要重新打开,才能接入同一个 Codex 内核。手机连接和 Codex 内核会保持运行;旧 Desktop 实例中的任务可能中断,未发送内容请先保存。切换不会清空项目、对话历史、设置或设备授权。';
23
+ export const recoveryWarning = 'Codex Desktop 需要重新打开,才能接入同一个 Codex 内核。移动端连接和 Codex 内核会保持运行;旧 Desktop 实例中的任务可能中断,未发送内容请先保存。切换不会清空项目、对话历史、设置或设备授权。';
24
24
 
25
25
  export async function recoveryStatus(stateRoot, host = desktopHost) {
26
26
  const record = readRecord(recordPath(stateRoot));
@@ -42,7 +42,7 @@ export async function planRecovery(stateRoot, host, available = portAvailable) {
42
42
  const info = await host.inspect();
43
43
  const action = await host.sharedServiceUrl() ? 'restart' : 'switch';
44
44
  if (!info.desktop) throw new Error('未找到 Codex Desktop;CLI 用户无需执行 Desktop 接入。');
45
- const record = readRecord(path.join(stateRoot, 'managed-app-server.json')) || readRecord(path.join(stateRoot, 'shared-desktop.json'));
45
+ const record = readRecord(path.join(stateRoot, 'managed-app-server.json'));
46
46
  const actual = record ? await host.process(record.pid) : null;
47
47
  if (actual && (!sameProcess(record, actual) || !actual.command?.includes('app-server') || !actual.command.includes('ws://127.0.0.1:45839'))) {
48
48
  throw new Error('Codex 内核进程身份与记录不符,未结束任何进程。');
@@ -84,7 +84,7 @@ export async function runRecovery(stateRoot, operationId, dependencies = {}) {
84
84
  const host = dependencies.host || desktopHost;
85
85
  const service = dependencies.service || lifecycle({ stateRoot, host: serviceHost });
86
86
  const available = dependencies.available || portAvailable;
87
- const start = dependencies.launch || (options => launchDesktop('shared', options));
87
+ const start = dependencies.launch || launchDesktop;
88
88
  let record = readRecord(recordPath(stateRoot));
89
89
  if (record?.operationId !== operationId || readRecord(lockPath(stateRoot))?.operationId !== operationId) throw new Error('恢复实例已失效。');
90
90
  record.steps ||= recoverySteps();
@@ -113,7 +113,7 @@ export async function runRecovery(stateRoot, operationId, dependencies = {}) {
113
113
  return health.running && health.mode === 'lan-shared' && health.execution?.ready === true;
114
114
  }, 'Codex 内核未就绪;Bridge 和中继管理连接仍保留。');
115
115
  record = { ...record, status: 'completed', stage: 'ready', steps: record.steps.map(step => ({ ...step, status: 'completed' })),
116
- message: 'Codex Desktop 已接入 Codex 内核;手机控制保持就绪,原任务未自动重发。' };
116
+ message: 'Codex Desktop 已接入 Codex 内核;移动端控制保持就绪,原任务未自动重发。' };
117
117
  } catch (error) { record = { ...record, status: 'failed', error: error.message,
118
118
  steps: record.steps.map(step => ({ ...step, status: step.status === 'running' ? 'failed' : step.status })),
119
119
  message: 'Codex Desktop 未完成接入,已停止后续步骤。' }; }
@@ -6,11 +6,8 @@ import { readRecord, writeRecord } from './service-lifecycle.mjs';
6
6
 
7
7
  const owner = '@walkhi/code-relax';
8
8
  const markerName = '.code-relax-install.json';
9
- const legacyOwner = '@closedhi/codex-remote';
10
- const legacyMarkerName = '.codex-remote-install.json';
11
9
  const digest = value => createHash('sha256').update(value).digest('hex');
12
10
  export const skillDirectory = (home = os.homedir()) => path.join(home, '.agents/skills/code-relax');
13
- const legacySkillDirectory = (home = os.homedir()) => path.join(home, '.agents/skills/codex-remote');
14
11
 
15
12
  // Refuse junctions/symlinks along managed paths before writing or removing files.
16
13
  export function assertPlainPath(target) {
@@ -33,15 +30,6 @@ export function inspectSkill(target = skillDirectory()) {
33
30
  return { installed: true, managed, target };
34
31
  }
35
32
 
36
- export function removeLegacyManagedSkill(target = legacySkillDirectory()) {
37
- const marker = readRecord(path.join(target, legacyMarkerName));
38
- const file = path.join(target, 'SKILL.md');
39
- if (marker?.owner !== legacyOwner || !fs.existsSync(file) || marker.sha256 !== digest(fs.readFileSync(file))
40
- || !fs.readdirSync(target).every(name => ['SKILL.md', legacyMarkerName].includes(name))) return false;
41
- fs.unlinkSync(file); fs.unlinkSync(path.join(target, legacyMarkerName)); fs.rmdirSync(target);
42
- return true;
43
- }
44
-
45
33
  export function installSkill(source, target = skillDirectory()) {
46
34
  const state = inspectSkill(target);
47
35
  if (state.installed && !state.managed) throw new Error(`同名 Skill 非本包管理或已被修改,请保留并手动处理:${target}`);
@@ -49,7 +37,6 @@ export function installSkill(source, target = skillDirectory()) {
49
37
  fs.mkdirSync(target, { recursive: true });
50
38
  fs.writeFileSync(path.join(target, 'SKILL.md'), content);
51
39
  writeRecord(path.join(target, markerName), { owner, sha256: digest(content) });
52
- if (path.resolve(target) === path.resolve(skillDirectory())) removeLegacyManagedSkill();
53
40
  return { installed: true, target, message: 'Skill 已安装/更新;若 Codex 尚未显示,请重新打开 Codex。' };
54
41
  }
55
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.10",
3
+ "version": "0.1.0-beta.12",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -11,6 +11,7 @@
11
11
  "prepack": "npm run build"
12
12
  },
13
13
  "dependencies": {
14
+ "mysql2": "^3.24.4",
14
15
  "node-datachannel": "0.33.4",
15
16
  "qrcode": "1.5.4",
16
17
  "sharp": "0.35.4",
@@ -1,131 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import http from 'node:http';
4
- import { fileURLToPath } from 'node:url';
5
- import { readRecord, writeRecord } from './service-lifecycle.mjs';
6
- import { controlRequest } from './service-control.mjs';
7
-
8
- const script = fileURLToPath(import.meta.url);
9
- const bridgeRoot = path.resolve(path.dirname(script), '..');
10
- export const debugRecord = stateRoot => path.join(stateRoot, 'debug-ui-session.json');
11
-
12
- function enabledRecord(stateRoot) {
13
- const record = readRecord(debugRecord(stateRoot));
14
- return record?.enabled === true && Number.isInteger(record.port) && record.port > 0 && record.port < 65536
15
- && typeof record.sourceRoot === 'string' ? record : null;
16
- }
17
-
18
- export function debugPort(stateRoot) {
19
- return enabledRecord(stateRoot)?.port || null;
20
- }
21
-
22
- export async function activeDebugPort(stateRoot, expectedPort) {
23
- const port = debugPort(stateRoot);
24
- return expectedPort === undefined || port === expectedPort ? port : null;
25
- }
26
-
27
- function assetsFor(root) {
28
- const web = path.join(root, 'web');
29
- const packaged = path.join(web, 'resources.json');
30
- const manifest = fs.existsSync(packaged)
31
- ? JSON.parse(fs.readFileSync(packaged, 'utf8')).map(name => ({ name, source: path.join(web, name) }))
32
- : JSON.parse(fs.readFileSync(path.join(root, '../packages/web-resources.json'), 'utf8')).common
33
- .map(item => ({ name: item.name, source: path.resolve(root, '..', item.source) }));
34
- const assets = new Map(manifest.map(item => ['/' + item.name, item.source]));
35
- assets.set('/vendor/markdown-it.umd.min.js', path.join(web, 'vendor/markdown-it.umd.min.js'));
36
- return { web, assets };
37
- }
38
-
39
- function send(response, status, mime, bytes) {
40
- response.writeHead(status, {
41
- ...(mime ? { 'Content-Type': mime } : {}),
42
- 'Cache-Control': 'no-store',
43
- 'X-Content-Type-Options': 'nosniff',
44
- 'Referrer-Policy': 'no-referrer',
45
- 'Content-Security-Policy': "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:",
46
- });
47
- response.end(bytes);
48
- }
49
-
50
- export function serveDebugResource(request, response, stateRoot, bridgePort) {
51
- const record = enabledRecord(stateRoot);
52
- if (!record || record.port !== bridgePort) return false;
53
- if (request.method !== 'GET' && request.method !== 'HEAD') return false;
54
- const url = new URL(request.url, 'http://localhost');
55
- if (url.pathname.startsWith('/api/')) return false;
56
- try {
57
- const { web, assets } = assetsFor(record.sourceRoot);
58
- let bytes, mime;
59
- if (url.pathname === '/' || url.pathname === '/index.html') {
60
- bytes = fs.readFileSync(path.join(web, 'index.html'), 'utf8')
61
- .replace('<body class="page-loading">', url.searchParams.get('client') === 'harmony'
62
- ? '<body class="page-loading silent-page-loading">' : '<body class="page-loading">')
63
- .replace('<script', '<script src="debug-config.js"></script>\n<script');
64
- mime = 'text/html; charset=utf-8';
65
- } else if (url.pathname === '/debug-config.js') {
66
- bytes = 'if(globalThis.codexNative){globalThis.codexLanConfig=JSON.parse(globalThis.codexNative.lanConnection());}else{globalThis.codexLanConfig={address:location.origin};}';
67
- mime = 'text/javascript; charset=utf-8';
68
- } else if (assets.has(url.pathname)) {
69
- bytes = fs.readFileSync(assets.get(url.pathname));
70
- mime = url.pathname.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8';
71
- } else {
72
- send(response, 404); return true;
73
- }
74
- send(response, 200, mime, request.method === 'HEAD' ? undefined : bytes);
75
- } catch {
76
- send(response, 500, 'text/plain; charset=utf-8', 'Debug resource unavailable');
77
- }
78
- return true;
79
- }
80
-
81
- // Small harness for the same request handler used by Bridge.
82
- export function createDebugServer(root, bridgePort) {
83
- const stateRoot = fs.mkdtempSync(path.join(path.dirname(root), '.debug-state-'));
84
- writeRecord(debugRecord(stateRoot), { enabled: true, port: bridgePort, sourceRoot: root });
85
- const server = http.createServer((request, response) => {
86
- if (!serveDebugResource(request, response, stateRoot, bridgePort)) response.writeHead(404).end();
87
- });
88
- server.once('close', () => fs.rmSync(stateRoot, { recursive: true, force: true }));
89
- return server;
90
- }
91
-
92
- async function bridgeSession(stateRoot) {
93
- const session = readRecord(path.join(stateRoot, 'bridge-session.json'));
94
- if (!session?.port) throw new Error('Bridge 未运行,请先执行 relax start。');
95
- try { await controlRequest(session); }
96
- catch { throw new Error('Bridge 未运行,请先执行 relax start。'); }
97
- return session;
98
- }
99
-
100
- async function stopLegacyWorker(record) {
101
- if (!record?.controlPort || !record?.controlSecret || !record?.instanceId) return;
102
- try { await controlRequest(record, 'POST'); }
103
- catch (error) { if (error.code !== 'ECONNREFUSED') throw error; }
104
- }
105
-
106
- export async function debugUi(command, stateRoot) {
107
- const recordPath = debugRecord(stateRoot);
108
- const previous = readRecord(recordPath);
109
- if (command === 'dev-stop') {
110
- await stopLegacyWorker(previous);
111
- fs.rmSync(recordPath, { force: true });
112
- return { stopped: true, message: 'Bridge 调试页面已关闭;Bridge 继续运行。' };
113
- }
114
- let session;
115
- try { session = await bridgeSession(stateRoot); }
116
- catch (error) {
117
- if (command === 'dev-status') return { running: false, integrated: true, port: null, sourceRoot: null };
118
- throw error;
119
- }
120
- const current = enabledRecord(stateRoot);
121
- if (command === 'dev-status') return {
122
- running: Boolean(current && current.port === session.port),
123
- integrated: true,
124
- port: current?.port === session.port ? session.port : null,
125
- sourceRoot: current?.port === session.port ? current.sourceRoot : null,
126
- };
127
- await stopLegacyWorker(previous);
128
- const value = { enabled: true, integrated: true, port: session.port, sourceRoot: bridgeRoot };
129
- writeRecord(recordPath, value);
130
- return { running: true, ...value };
131
- }
@@ -1,5 +0,0 @@
1
- param([string]$NodePath, [string]$Entry, [ValidateSet('shared-start')][string]$Action)
2
- $ErrorActionPreference = 'Stop'
3
- [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
- & $NodePath $Entry $Action
5
- if ($LASTEXITCODE -ne 0) { Write-Host '启动未完成,请根据上方提示处理后重试。' }
@@ -1,283 +0,0 @@
1
- (function (root) {
2
- 'use strict';
3
-
4
- function createActivityView(options) {
5
- const {
6
- document, disclosures, entryDisclosureKey, activityIcon, disclosureChevronElement,
7
- codeBlockIcon, copyText, showToast, compactActivityElement,
8
- activityPreview, commandStatusText, diffLineCounts, formatDuration, statusText, toolStatusText,
9
- } = options;
10
-
11
- function copyButton(value, label) {
12
- const button = document.createElement('button');
13
- button.type = 'button';
14
- button.className = 'activity-copy';
15
- button.title = label;
16
- button.setAttribute('aria-label', label);
17
- button.append(codeBlockIcon('copy'));
18
- button.onclick = async event => {
19
- event.preventDefault();
20
- event.stopPropagation();
21
- if (button.dataset.copyState === 'copied') return;
22
- try {
23
- await copyText(value);
24
- button.dataset.copyState = 'copied';
25
- button.title = '已复制';
26
- button.setAttribute('aria-label', '已复制');
27
- button.replaceChildren(codeBlockIcon('check'));
28
- setTimeout(() => {
29
- if (!button.isConnected) return;
30
- delete button.dataset.copyState;
31
- button.title = label;
32
- button.setAttribute('aria-label', label);
33
- button.replaceChildren(codeBlockIcon('copy'));
34
- }, 1500);
35
- } catch {
36
- showToast('复制失败,可长按选择文字。', 3000);
37
- }
38
- };
39
- return button;
40
- }
41
-
42
- function command(entry) {
43
- const entryStatus = statusText(entry.status);
44
- const commandText = Array.isArray(entry.command) ? entry.command.join(' ') : String(entry.command || '');
45
- const output = String(entry.output || '');
46
- const preview = activityPreview(commandText);
47
- const details = document.createElement('details');
48
- details.className = 'activity command-activity';
49
- const summary = document.createElement('summary');
50
- const label = document.createElement('span');
51
- label.className = 'activity-title';
52
- label.textContent = `${commandStatusText(entry.status)}${preview ? ` ${preview}` : ''}`;
53
- summary.append(activityIcon('command'), label, disclosureChevronElement());
54
- const shell = document.createElement('div');
55
- shell.className = 'command-shell';
56
- if (commandText) {
57
- const row = document.createElement('div');
58
- row.className = 'command-shell-command';
59
- const prompt = document.createElement('span');
60
- prompt.className = 'command-shell-prompt';
61
- prompt.textContent = '$';
62
- const code = document.createElement('code');
63
- code.textContent = commandText;
64
- row.append(prompt, code, copyButton(commandText, '复制命令'));
65
- shell.append(row);
66
- }
67
- const outputArea = document.createElement('div');
68
- outputArea.className = 'command-shell-output';
69
- const outputText = document.createElement('pre');
70
- outputText.textContent = output || (entryStatus === 'inProgress' ? '' : '无输出');
71
- if (!output) outputText.classList.add('empty');
72
- outputArea.append(outputText);
73
- if (output) outputArea.append(copyButton(output, '复制输出'));
74
- shell.append(outputArea);
75
- const footer = document.createElement('div');
76
- footer.className = 'command-shell-footer';
77
- const cwd = document.createElement('span');
78
- cwd.className = 'command-shell-cwd';
79
- cwd.textContent = entry.cwd ? `目录:${entry.cwd}` : '';
80
- cwd.title = entry.cwd || '';
81
- const status = document.createElement('span');
82
- status.className = 'command-shell-status';
83
- const duration = Number(entry.durationMs);
84
- const durationText = Number.isFinite(duration) && duration >= 0 ? ` · ${formatDuration(duration)}` : '';
85
- if (entryStatus === 'inProgress') status.textContent = `运行中${durationText}`;
86
- else if (entryStatus === 'interrupted') status.textContent = `已停止${durationText}`;
87
- else if (entry.exitCode === 0) status.textContent = `成功${durationText}`;
88
- else if (entry.exitCode != null) status.textContent = `退出码 ${entry.exitCode}${durationText}`;
89
- else status.textContent = `${entryStatus === 'failed' ? '失败' : '已完成'}${durationText}`;
90
- footer.append(cwd, status);
91
- shell.append(footer);
92
- details.append(summary, shell);
93
- return disclosures.bind(details, entryDisclosureKey(entry));
94
- }
95
-
96
- const fieldLabels = {
97
- cmd: '命令', command: '命令', cwd: '目录', workdir: '目录', path: '路径', file: '文件',
98
- query: '查询', url: '地址', prompt: '内容', message: '消息', target: '目标', pattern: '匹配',
99
- server: '服务', tool: '工具', status: '状态', language: '语言', timeout: '超时',
100
- };
101
-
102
- function fieldLabel(value) {
103
- const key = String(value || '字段');
104
- return fieldLabels[key] || key.replaceAll('_', ' ');
105
- }
106
-
107
- function parsedStructuredText(value) {
108
- const text = String(value || '').trim();
109
- if (!text || !['{', '['].includes(text[0])) return null;
110
- try { return JSON.parse(text); } catch { return null; }
111
- }
112
-
113
- function appendReadableValue(container, value, depth = 0) {
114
- if (typeof value === 'string') {
115
- const structured = parsedStructuredText(value);
116
- if (structured != null) { appendReadableValue(container, structured, depth); return; }
117
- const node = document.createElement(value.includes('\n') ? 'pre' : 'span');
118
- node.className = value.includes('\n') ? 'tool-detail-text' : 'tool-detail-value';
119
- node.textContent = value || '空';
120
- container.append(node);
121
- return;
122
- }
123
- if (value == null || typeof value !== 'object') {
124
- const node = document.createElement('span');
125
- node.className = 'tool-detail-value';
126
- node.textContent = value == null ? '无' : String(value);
127
- container.append(node);
128
- return;
129
- }
130
- if (depth >= 3) {
131
- const node = document.createElement('span');
132
- node.className = 'tool-detail-value muted';
133
- node.textContent = Array.isArray(value) ? `${value.length} 项` : `${Object.keys(value).length} 个字段`;
134
- container.append(node);
135
- return;
136
- }
137
- if (Array.isArray(value)) {
138
- if (!value.length) {
139
- const node = document.createElement('span');
140
- node.className = 'tool-detail-value muted';
141
- node.textContent = '无';
142
- container.append(node);
143
- return;
144
- }
145
- const list = document.createElement('ul');
146
- list.className = 'tool-detail-list';
147
- for (const item of value.slice(0, 20)) {
148
- const row = document.createElement('li');
149
- appendReadableValue(row, item, depth + 1);
150
- list.append(row);
151
- }
152
- if (value.length > 20) {
153
- const more = document.createElement('li');
154
- more.className = 'muted';
155
- more.textContent = `另有 ${value.length - 20} 项`;
156
- list.append(more);
157
- }
158
- container.append(list);
159
- return;
160
- }
161
- const entries = Object.entries(value);
162
- if (!entries.length) {
163
- const node = document.createElement('span');
164
- node.className = 'tool-detail-value muted';
165
- node.textContent = '无';
166
- container.append(node);
167
- return;
168
- }
169
- const fields = document.createElement('dl');
170
- fields.className = 'tool-detail-fields';
171
- for (const [key, fieldValue] of entries.slice(0, 24)) {
172
- const term = document.createElement('dt');
173
- term.textContent = fieldLabel(key);
174
- const description = document.createElement('dd');
175
- appendReadableValue(description, fieldValue, depth + 1);
176
- fields.append(term, description);
177
- }
178
- if (entries.length > 24) {
179
- const description = document.createElement('dd');
180
- description.className = 'muted';
181
- description.textContent = `另有 ${entries.length - 24} 个字段`;
182
- fields.append(description);
183
- }
184
- container.append(fields);
185
- }
186
-
187
- function detailSection(title, value, className = '') {
188
- const section = document.createElement('section');
189
- section.className = `tool-detail-section ${className}`.trim();
190
- const heading = document.createElement('div');
191
- heading.className = 'tool-detail-heading';
192
- heading.textContent = title;
193
- section.append(heading);
194
- appendReadableValue(section, value);
195
- return section;
196
- }
197
-
198
- function tool(entry) {
199
- const source = [entry.server, entry.tool].filter(Boolean).join('/');
200
- const title = `${toolStatusText(entry.status)}${source ? ` ${source}` : '工具'}`;
201
- const argumentsValue = entry.arguments && typeof entry.arguments === 'object' ? entry.arguments : {};
202
- const resources = Array.isArray(entry.resources) ? entry.resources : [];
203
- const hasDetails = Object.keys(argumentsValue).length > 0 || entry.resultText || entry.errorText || resources.length || entry.mediaCount;
204
- if (!hasDetails) return compactActivityElement(title, 'tool');
205
- const details = document.createElement('details');
206
- details.className = 'activity tool-activity';
207
- const summary = document.createElement('summary');
208
- const label = document.createElement('span');
209
- label.className = 'activity-title';
210
- label.textContent = title;
211
- summary.append(activityIcon('tool'), label, disclosureChevronElement());
212
- const body = document.createElement('div');
213
- body.className = 'tool-detail';
214
- if (Object.keys(argumentsValue).length) body.append(detailSection('参数', argumentsValue));
215
- if (entry.resultText) body.append(detailSection('结果', entry.resultText));
216
- if (entry.errorText) body.append(detailSection('错误', entry.errorText, 'error'));
217
- if (resources.length) body.append(detailSection('资源', resources));
218
- if (entry.mediaCount) body.append(detailSection('媒体', `返回了 ${entry.mediaCount} 个媒体结果`));
219
- const duration = Number(entry.durationMs);
220
- if (Number.isFinite(duration) && duration >= 0) {
221
- const footer = document.createElement('div');
222
- footer.className = 'tool-detail-footer';
223
- footer.textContent = formatDuration(duration);
224
- body.append(footer);
225
- }
226
- details.append(summary, body);
227
- return disclosures.bind(details, entryDisclosureKey(entry));
228
- }
229
-
230
- function activity(title, text, status, iconKind = null, disclosureKey = '') {
231
- const details = document.createElement('details');
232
- details.className = 'activity';
233
- const summary = document.createElement('summary');
234
- if (iconKind) summary.append(activityIcon(iconKind));
235
- const label = document.createElement('span');
236
- label.className = 'activity-title';
237
- if (typeof title === 'string') label.textContent = title;
238
- else label.append(title);
239
- summary.append(label, disclosureChevronElement());
240
- const content = document.createElement('pre');
241
- content.textContent = text || '无详细信息';
242
- details.append(summary, content);
243
- return disclosures.bind(details, disclosureKey);
244
- }
245
-
246
- function fileChange(entry) {
247
- const entryStatus = statusText(entry.status);
248
- const changes = Array.isArray(entry.changes) ? entry.changes : [];
249
- if (!changes.length) {
250
- const title = entryStatus === 'completed' || entryStatus === 'failed' ? '已编辑文件' : '正在编辑文件';
251
- return activity(title, '无详细信息', entry.status, 'edit');
252
- }
253
- const fragment = document.createDocumentFragment();
254
- for (const [index, change] of changes.entries()) {
255
- const title = document.createDocumentFragment();
256
- const label = document.createElement('span');
257
- const action = entryStatus === 'completed' || entryStatus === 'failed' ? '已编辑' : '正在编辑';
258
- const filename = String(change.path || '文件').split(/[\\/]/).filter(Boolean).at(-1) || '文件';
259
- label.textContent = `${action} ${filename}`;
260
- title.append(label);
261
- const counts = diffLineCounts(typeof change.diff === 'string' ? change.diff : change.diff?.text);
262
- if (counts.added || counts.removed) {
263
- const added = document.createElement('span');
264
- added.className = 'diff-add';
265
- added.textContent = `+${counts.added}`;
266
- const removed = document.createElement('span');
267
- removed.className = 'diff-remove';
268
- removed.textContent = `-${counts.removed}`;
269
- title.append(added, removed);
270
- }
271
- const diff = typeof change.diff === 'string' ? change.diff : change.diff?.text || '';
272
- const detail = [change.path ? `路径:${change.path}` : '', diff].filter(Boolean).join('\n\n');
273
- fragment.append(activity(title, detail || '已记录文件修改', entry.status, 'edit',
274
- entryDisclosureKey(entry, `file:${index}:${change.path || ''}`)));
275
- }
276
- return fragment;
277
- }
278
-
279
- return { activity, command, fileChange, tool };
280
- }
281
-
282
- root.createActivityView = createActivityView;
283
- })(globalThis);