@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.
- package/README.md +44 -54
- package/dist/bin/codex-remote.mjs +34 -60
- package/dist/bin/migrate-relay-db.mjs +104 -0
- package/dist/bin/self-relay-server.mjs +17 -10
- package/dist/shared/app-server-events.cjs +15 -3
- package/dist/shared/file-mentions.cjs +35 -0
- package/dist/shared/server-requests.cjs +86 -58
- package/dist/skills/code-relax/SKILL.md +1 -1
- package/dist/src/desktop-launcher.mjs +6 -11
- package/dist/src/file-upload-session.mjs +124 -0
- package/dist/src/lan-socket-server.mjs +51 -8
- package/dist/src/managed-app-server.mjs +21 -10
- package/dist/src/onboarding.mjs +3 -3
- package/dist/src/platform/windows/codex-runtime.mjs +138 -4
- package/dist/src/platform/windows/desktop-host.mjs +1 -2
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +6 -13
- package/dist/src/platform/windows/resident-start.js +8 -0
- package/dist/src/platform/windows/resident-startup.ps1 +6 -3
- package/dist/src/platform/windows/service-host.mjs +0 -9
- package/dist/src/self-relay/admin-page.mjs +1 -1
- package/dist/src/self-relay/admin-state.mjs +8 -6
- package/dist/src/self-relay/connector.mjs +84 -12
- package/dist/src/self-relay/email-accounts.mjs +125 -0
- package/dist/src/self-relay/lifecycle.mjs +52 -100
- package/dist/src/self-relay/mysql-state.mjs +104 -0
- package/dist/src/self-relay/p2p-probe.mjs +1 -1
- package/dist/src/self-relay/server.mjs +155 -38
- package/dist/src/server.mjs +736 -610
- package/dist/src/service-doctor.mjs +1 -1
- package/dist/src/service-install.mjs +1 -4
- package/dist/src/shared-app-server.mjs +274 -272
- package/dist/src/shared-catalog.mjs +1 -11
- package/dist/src/shared-recovery.mjs +5 -5
- package/dist/src/skill-install.mjs +0 -13
- package/package.json +2 -1
- package/dist/src/debug-ui.mjs +0 -131
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
- package/dist/web/activity-view.js +0 -283
- package/dist/web/approval-ui.js +0 -188
- package/dist/web/capabilities.js +0 -50
- package/dist/web/chat-transport.js +0 -196
- package/dist/web/chat.css +0 -778
- package/dist/web/chat.js +0 -3815
- package/dist/web/client-platform.js +0 -11
- package/dist/web/community.css +0 -82
- package/dist/web/community.html +0 -36
- package/dist/web/composer-controller.js +0 -97
- package/dist/web/conversation-controller.js +0 -99
- package/dist/web/disclosure-state-controller.js +0 -95
- package/dist/web/draft-controller.js +0 -103
- package/dist/web/harmony-platform.js +0 -52
- package/dist/web/highlight-powershell.min.js +0 -39
- package/dist/web/highlight.min.js +0 -1262
- package/dist/web/history-cache.js +0 -240
- package/dist/web/history-controller.js +0 -167
- package/dist/web/index.html +0 -288
- package/dist/web/lan-session.js +0 -122
- package/dist/web/link-action-controller.js +0 -212
- package/dist/web/message-send-controller.js +0 -177
- package/dist/web/message-view.js +0 -98
- package/dist/web/p2p-data-channel.js +0 -50
- package/dist/web/p2p-diagnostics.js +0 -28
- package/dist/web/p2p-probe.js +0 -123
- package/dist/web/page-resume.js +0 -28
- package/dist/web/pending-message-store.js +0 -108
- package/dist/web/queue-controller.js +0 -82
- package/dist/web/resources.json +0 -1
- package/dist/web/self-relay-session.js +0 -129
- package/dist/web/server-requests.js +0 -67
- package/dist/web/station-connection-controller.js +0 -75
- package/dist/web/task-list-view.js +0 -289
- package/dist/web/thread-attention-controller.js +0 -124
- package/dist/web/thread-context-controller.js +0 -30
- package/dist/web/thread-list-controller.js +0 -61
- package/dist/web/thread-list-sync.js +0 -86
- package/dist/web/thread-title-controller.js +0 -57
- package/dist/web/timeline-formatters.js +0 -249
- package/dist/web/timeline-reducer.js +0 -77
- package/dist/web/timeline-renderer.js +0 -161
- package/dist/web/timeline-scroll-controller.js +0 -50
- package/dist/web/timeline-sync.js +0 -67
- package/dist/web/usage-controller.js +0 -258
- package/dist/web/vendor/README.md +0 -14
- package/dist/web/vendor/highlight-powershell.min.js +0 -39
- package/dist/web/vendor/highlight.LICENSE.txt +0 -29
- package/dist/web/vendor/highlight.min.js +0 -1262
- 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
|
-
|
|
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
|
|
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'))
|
|
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 ||
|
|
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.
|
|
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",
|
package/dist/src/debug-ui.mjs
DELETED
|
@@ -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);
|