@walkhi/code-relax 0.1.0-beta.10 → 0.1.0-beta.11
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 +22 -58
- package/dist/shared/app-server-events.cjs +15 -3
- package/dist/shared/file-mentions.cjs +35 -0
- 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 +9 -7
- package/dist/src/onboarding.mjs +3 -3
- package/dist/src/platform/windows/desktop-host.mjs +1 -2
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +6 -13
- 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/connector.mjs +84 -12
- package/dist/src/self-relay/lifecycle.mjs +8 -24
- package/dist/src/self-relay/p2p-probe.mjs +1 -1
- package/dist/src/self-relay/server.mjs +67 -12
- package/dist/src/server.mjs +141 -34
- package/dist/src/service-doctor.mjs +1 -1
- package/dist/src/service-install.mjs +1 -4
- package/dist/src/shared-app-server.mjs +30 -28
- 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 +1 -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
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);
|
package/dist/web/approval-ui.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
(function (root) {
|
|
2
|
-
root.createApprovalPanel = function (host, respond, pendingChanged = () => {}) {
|
|
3
|
-
const cards = new Map();
|
|
4
|
-
let connected = false;
|
|
5
|
-
const titles = { accept: '允许本次', acceptForSession: '本会话允许', decline: '拒绝', cancel: '拒绝并停止' };
|
|
6
|
-
function content(text, label, collapsed = false) {
|
|
7
|
-
const full = document.createElement('pre'); full.textContent = text;
|
|
8
|
-
if (!text) full.hidden = true;
|
|
9
|
-
if (!collapsed && text.length <= 180 && text.split('\n').length <= 3) return full;
|
|
10
|
-
const details = document.createElement('details'); details.className = 'approval-details';
|
|
11
|
-
const summary = document.createElement('summary');
|
|
12
|
-
const title = document.createElement('span'); title.className = 'approval-details-title'; title.textContent = label;
|
|
13
|
-
const preview = document.createElement('span'); preview.className = 'approval-preview';
|
|
14
|
-
preview.textContent = text.split('\n').slice(0, 3).join('\n').slice(0, 180) + '…';
|
|
15
|
-
summary.append(title);
|
|
16
|
-
if (!collapsed) summary.append(preview);
|
|
17
|
-
details.append(summary, full);
|
|
18
|
-
return details;
|
|
19
|
-
}
|
|
20
|
-
function paragraph(text, className = '') {
|
|
21
|
-
const p = document.createElement('p'); p.textContent = text; p.className = className; return p;
|
|
22
|
-
}
|
|
23
|
-
function shellPresentation(command) {
|
|
24
|
-
if (typeof command !== 'string') return null;
|
|
25
|
-
const match = command.trim().match(/^(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+([\s\S]*))?$/);
|
|
26
|
-
if (!match) return null;
|
|
27
|
-
const executable = match[1] || match[2] || match[3];
|
|
28
|
-
const name = executable.replaceAll('\\', '/').split('/').at(-1)?.toLowerCase() || '';
|
|
29
|
-
const rest = match[4] || '';
|
|
30
|
-
const shells = [
|
|
31
|
-
{ names: ['powershell', 'powershell.exe', 'pwsh', 'pwsh.exe'], label: 'PowerShell', marker: /(?:^|\s)-(?:command|c)\s+/i },
|
|
32
|
-
{ names: ['cmd', 'cmd.exe'], label: '命令提示符', marker: /(?:^|\s)\/(?:c|k)\s+/i },
|
|
33
|
-
{ names: ['bash', 'bash.exe', 'sh', 'sh.exe'], label: 'Shell', marker: /(?:^|\s)-(?:c|lc)\s+/i }
|
|
34
|
-
];
|
|
35
|
-
const shell = shells.find(candidate => candidate.names.includes(name));
|
|
36
|
-
if (!shell) return null;
|
|
37
|
-
const marker = shell.marker.exec(rest);
|
|
38
|
-
const text = marker ? rest.slice(marker.index + marker[0].length).trim() : rest.trim();
|
|
39
|
-
return { label: shell.label, text };
|
|
40
|
-
}
|
|
41
|
-
function requestDetails(request) {
|
|
42
|
-
const p = request.params, body = document.createElement('div'); body.className = 'approval-body';
|
|
43
|
-
const network = p.networkApprovalContext;
|
|
44
|
-
if (network) body.append(paragraph(`目标:${network.protocol}://${network.host}`));
|
|
45
|
-
if (p.reason && network) body.append(paragraph(p.reason));
|
|
46
|
-
for (const action of p.commandActions || []) {
|
|
47
|
-
if (action.type === 'read') body.append(paragraph(`读取文件:${action.path || action.name}`));
|
|
48
|
-
else if (action.type === 'listFiles') body.append(paragraph(`列出文件:${action.path || p.cwd || '当前目录'}`));
|
|
49
|
-
else if (action.type === 'search') body.append(paragraph(`搜索${action.query ? `“${action.query}”` : '内容'}${action.path ? `:${action.path}` : ''}`));
|
|
50
|
-
}
|
|
51
|
-
const shell = shellPresentation(p.command);
|
|
52
|
-
if (shell) {
|
|
53
|
-
body.append(paragraph(`执行方式:${shell.label}`, 'approval-meta'));
|
|
54
|
-
if (shell.text) body.append(content(shell.text, '要执行的内容'));
|
|
55
|
-
body.append(content(p.command, '完整原始命令', true));
|
|
56
|
-
} else if (p.command) body.append(content(p.command, '完整命令'));
|
|
57
|
-
if (p.cwd) body.append(paragraph(`工作目录:${p.cwd}`, 'approval-meta'));
|
|
58
|
-
if (p.grantRoot) body.append(paragraph(`申请本会话写入目录:${p.grantRoot}`));
|
|
59
|
-
for (const change of request.fileChanges || []) {
|
|
60
|
-
const kind = change.kind?.type || change.kind;
|
|
61
|
-
const move = change.kind?.movePath;
|
|
62
|
-
const label = `${{ add: '新增', delete: '删除', update: '修改' }[kind] || '文件'}:${change.path}${move ? ` → ${move}` : ''}`;
|
|
63
|
-
body.append(change.diff ? content(change.diff, label, true) : paragraph(label));
|
|
64
|
-
}
|
|
65
|
-
if (request.method === CodexServerRequests.file && !request.fileChanges?.length) body.append(paragraph('尚未收到文件修改明细,请先在工作站查看。'));
|
|
66
|
-
const permissions = p.additionalPermissions || p.permissions;
|
|
67
|
-
if (permissions) {
|
|
68
|
-
if (permissions.network?.enabled === true) body.append(paragraph('额外权限:连接互联网'));
|
|
69
|
-
const files = permissions.fileSystem;
|
|
70
|
-
for (const path of files?.read || []) body.append(paragraph(`允许读取:${path}`));
|
|
71
|
-
for (const path of files?.write || []) body.append(paragraph(`允许写入:${path}`));
|
|
72
|
-
for (const entry of files?.entries || []) {
|
|
73
|
-
const path = entry.path?.path || entry.path?.pattern;
|
|
74
|
-
const action = { read: '允许读取', write: '允许写入', deny: '禁止访问' }[entry.access];
|
|
75
|
-
if (path && action) body.append(paragraph(`${action}:${path}`));
|
|
76
|
-
}
|
|
77
|
-
body.append(content(JSON.stringify(permissions, null, 2), '完整权限范围', true));
|
|
78
|
-
}
|
|
79
|
-
return body;
|
|
80
|
-
}
|
|
81
|
-
function decisionDescription(request, decision) {
|
|
82
|
-
if (decision === 'acceptForSession') return request.method === CodexServerRequests.file
|
|
83
|
-
? '本次及本会话内对相同文件的后续修改无需再次询问。' : '本次及会话内匹配的后续操作无需再次询问,不是只允许这一次。';
|
|
84
|
-
if (decision?.applyNetworkPolicyAmendment) {
|
|
85
|
-
const rule = decision.applyNetworkPolicyAmendment.network_policy_amendment;
|
|
86
|
-
return `${rule.action === 'allow' ? '允许' : '禁止'}访问 ${rule.host},并保存为后续网络规则。`;
|
|
87
|
-
}
|
|
88
|
-
return JSON.stringify(decision, null, 2);
|
|
89
|
-
}
|
|
90
|
-
const refresh = () => {
|
|
91
|
-
host.hidden = cards.size === 0;
|
|
92
|
-
for (const card of cards.values()) {
|
|
93
|
-
const status = card.request.status;
|
|
94
|
-
card.status.textContent = !connected ? '连接未恢复,暂不能操作' : card.error || (status === 'submitted' ? '决定已提交,等待工作站确认…' : status === 'unknown' ? '提交结果未确认,请检查工作站' : '等待你处理');
|
|
95
|
-
for (const control of card.form.querySelectorAll('button, input, textarea')) control.disabled = !connected || status !== 'pending' || card.busy;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
const notifyPending = threadId => {
|
|
99
|
-
if (!threadId) return;
|
|
100
|
-
const pending = [...cards.values()].some(card => card.request.params.threadId === threadId
|
|
101
|
-
&& card.request.status === 'pending');
|
|
102
|
-
pendingChanged(threadId, pending);
|
|
103
|
-
};
|
|
104
|
-
function create(request) {
|
|
105
|
-
const element = document.createElement('div'); element.className = 'approval-card';
|
|
106
|
-
const heading = document.createElement('strong');
|
|
107
|
-
const p = request.params;
|
|
108
|
-
heading.textContent = request.method === CodexServerRequests.input ? '需要你的回答'
|
|
109
|
-
: p.networkApprovalContext ? `允许连接 ${p.networkApprovalContext.host}?`
|
|
110
|
-
: p.reason?.trim() || (request.method === CodexServerRequests.file ? '允许修改以下文件?'
|
|
111
|
-
: request.method === CodexServerRequests.command
|
|
112
|
-
? p.kind === 'writeStdin' ? '允许向正在运行的终端发送输入?'
|
|
113
|
-
: '允许执行此命令?'
|
|
114
|
-
: '需要你的批准');
|
|
115
|
-
const status = document.createElement('p'); status.setAttribute('role', 'status');
|
|
116
|
-
const detail = requestDetails(request);
|
|
117
|
-
const form = document.createElement('form');
|
|
118
|
-
const card = { element, status, detail, form, request, fileChangesKey: JSON.stringify(request.fileChanges), busy: false, error: '' };
|
|
119
|
-
const submit = async result => {
|
|
120
|
-
if (!connected || card.busy || card.request.status !== 'pending') return;
|
|
121
|
-
card.busy = true; card.error = '';
|
|
122
|
-
card.request = { ...card.request, status: 'submitted' };
|
|
123
|
-
notifyPending(card.request.params.threadId); refresh();
|
|
124
|
-
try { await respond(card.request, result); }
|
|
125
|
-
catch (error) {
|
|
126
|
-
card.request = { ...card.request, status: 'unknown' };
|
|
127
|
-
card.error = error.message;
|
|
128
|
-
} finally { notifyPending(card.request.params.threadId); refresh(); }
|
|
129
|
-
};
|
|
130
|
-
form.onsubmit = event => event.preventDefault();
|
|
131
|
-
if (request.method === CodexServerRequests.input) {
|
|
132
|
-
const inputs = (p.questions || []).map(q => {
|
|
133
|
-
const label = document.createElement('label'); label.textContent = q.question;
|
|
134
|
-
const input = document.createElement('input'); input.type = q.isSecret ? 'password' : 'text'; input.autocomplete = 'off';
|
|
135
|
-
label.append(input); form.append(label);
|
|
136
|
-
for (const option of q.options || []) {
|
|
137
|
-
const button = document.createElement('button'); button.type = 'button'; button.textContent = option.label;
|
|
138
|
-
button.title = option.description || ''; button.onclick = () => { input.value = option.label; }; form.append(button);
|
|
139
|
-
}
|
|
140
|
-
return { id: q.id, input };
|
|
141
|
-
});
|
|
142
|
-
const button = document.createElement('button'); button.textContent = '提交回答'; form.append(button);
|
|
143
|
-
form.onsubmit = event => { event.preventDefault(); submit({ answers: Object.fromEntries(inputs.map(({ id, input }) => [id, { answers: [input.value] }])) }); };
|
|
144
|
-
} else {
|
|
145
|
-
const choices = CodexServerRequests.decisions(request);
|
|
146
|
-
const actions = document.createElement('div'); actions.className = 'approval-actions';
|
|
147
|
-
for (const decision of choices) {
|
|
148
|
-
const button = document.createElement('button'); button.type = 'button';
|
|
149
|
-
button.textContent = typeof decision === 'string' ? titles[decision] || decision : decision.acceptWithExecpolicyAmendment ? '允许类似命令' : decision.applyNetworkPolicyAmendment ? '保存网络规则' : '应用所示决定';
|
|
150
|
-
button.onclick = () => submit({ decision });
|
|
151
|
-
if (decision === 'acceptForSession' || (typeof decision === 'object' && !decision.acceptWithExecpolicyAmendment)) {
|
|
152
|
-
const option = document.createElement('div'); option.className = 'approval-scope-option';
|
|
153
|
-
option.append(paragraph(decisionDescription(request, decision))); form.append(option);
|
|
154
|
-
}
|
|
155
|
-
actions.append(button);
|
|
156
|
-
}
|
|
157
|
-
if (choices.length) form.append(actions);
|
|
158
|
-
if (!choices.length) {
|
|
159
|
-
const unsupported = document.createElement('p'); unsupported.textContent = '此类请求暂不支持手机处理,请到工作站查看。'; form.append(unsupported);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const type = request.method === CodexServerRequests.input ? '问题'
|
|
163
|
-
: p.networkApprovalContext ? '互联网访问' : request.method === CodexServerRequests.file ? '编辑文件'
|
|
164
|
-
: request.method === CodexServerRequests.command ? '终端' : '请求权限';
|
|
165
|
-
element.append(paragraph(type, 'approval-kind'), heading, status, detail, form); return card;
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
update(requests) {
|
|
169
|
-
const threadId = requests[0]?.params?.threadId || [...cards.values()][0]?.request.params.threadId;
|
|
170
|
-
const active = new Set();
|
|
171
|
-
for (const request of requests) {
|
|
172
|
-
const key = JSON.stringify([request.epoch, request.id]); active.add(key);
|
|
173
|
-
let card = cards.get(key);
|
|
174
|
-
if (!card) { card = create(request); cards.set(key, card); host.append(card.element); }
|
|
175
|
-
const fileChangesKey = JSON.stringify(request.fileChanges);
|
|
176
|
-
if (card.fileChangesKey !== fileChangesKey) {
|
|
177
|
-
card.detail.replaceChildren(...requestDetails(request).children); card.fileChangesKey = fileChangesKey;
|
|
178
|
-
}
|
|
179
|
-
card.request = request;
|
|
180
|
-
}
|
|
181
|
-
for (const [key, card] of cards) if (!active.has(key)) { card.element.remove(); cards.delete(key); }
|
|
182
|
-
notifyPending(threadId); refresh();
|
|
183
|
-
},
|
|
184
|
-
setConnected(value) { connected = value; refresh(); },
|
|
185
|
-
clear() { cards.clear(); host.replaceChildren(); refresh(); }
|
|
186
|
-
};
|
|
187
|
-
};
|
|
188
|
-
})(globalThis);
|
package/dist/web/capabilities.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
(function (root, factory) {
|
|
2
|
-
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
-
else root.CodexCapabilities = factory();
|
|
4
|
-
})(globalThis, function () {
|
|
5
|
-
// Execution semantics belong to a connection/task, never to a client OS.
|
|
6
|
-
const profiles = {
|
|
7
|
-
'lan-shared': { input: 'inline-image', history: 'paged', settings: 'immediate', updates: 'events',
|
|
8
|
-
stop: 'primary', queueOwner: 'codex', archive: true, approvals: true, syncBeforeWrite: true, sharedRecovery: true },
|
|
9
|
-
relay: { input: 'inline-image', history: 'paged', settings: 'immediate', updates: 'events',
|
|
10
|
-
stop: 'primary', queueOwner: 'codex', archive: true, approvals: true, syncBeforeWrite: true, historyCache: true, remoteImages: true, remoteFiles: true, managedConnection: true },
|
|
11
|
-
};
|
|
12
|
-
const unknown = Object.freeze({ input: 'unknown', history: 'unknown', settings: 'unknown', updates: 'unknown',
|
|
13
|
-
stop: 'none', queueOwner: 'none' });
|
|
14
|
-
for (const profile of Object.values(profiles)) Object.freeze(profile);
|
|
15
|
-
Object.freeze(profiles);
|
|
16
|
-
|
|
17
|
-
function describe(mode) { return profiles[mode] || unknown; }
|
|
18
|
-
function has(mode, feature) {
|
|
19
|
-
const p = describe(mode);
|
|
20
|
-
switch (feature) {
|
|
21
|
-
case 'queue': return p.queueOwner !== 'none';
|
|
22
|
-
case 'nativeInput': return ['local-image', 'inline-image'].includes(p.input);
|
|
23
|
-
case 'primaryStop': return p.stop === 'primary';
|
|
24
|
-
case 'immediateSettings': return p.settings === 'immediate';
|
|
25
|
-
case 'pagedHistory': return p.history === 'paged';
|
|
26
|
-
case 'archive': return p.archive === true;
|
|
27
|
-
case 'incrementalEvents': return p.updates === 'events';
|
|
28
|
-
default: return p[feature] === true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Support is stable; availability is recalculated for the current task. */
|
|
33
|
-
function actionState(mode, action, context = {}) {
|
|
34
|
-
const p = describe(mode);
|
|
35
|
-
const supported = action === 'interrupt' ? p.stop !== 'none'
|
|
36
|
-
: action === 'queue' ? has(mode, 'queue')
|
|
37
|
-
: action === 'settings' ? has(mode, 'immediateSettings')
|
|
38
|
-
: action === 'send' && p.input !== 'unknown';
|
|
39
|
-
if (!supported) return { visible: false, enabled: false, reason: '当前连接不支持此操作' };
|
|
40
|
-
if (context.connected === false) return { visible: true, enabled: false, reason: '连接尚未恢复' };
|
|
41
|
-
if (context.writable === false) return { visible: true, enabled: false, reason: context.readOnlyReason || '当前对话为只读' };
|
|
42
|
-
if (context.busy) return { visible: true, enabled: false, reason: '操作正在提交' };
|
|
43
|
-
if ((action === 'interrupt' || action === 'settings') && context.syncing) {
|
|
44
|
-
return { visible: true, enabled: false, reason: '请等待对话同步完成' };
|
|
45
|
-
}
|
|
46
|
-
if (action === 'interrupt' && !context.active) return { visible: true, enabled: false, reason: '当前没有运行中的轮次' };
|
|
47
|
-
return { visible: true, enabled: true, reason: '' };
|
|
48
|
-
}
|
|
49
|
-
return { describe, has, actionState };
|
|
50
|
-
});
|