chore(task-board): disable feature during refactor
This commit is contained in:
6
.planning/2026-08-21-disable-task-board/findings.md
Normal file
6
.planning/2026-08-21-disable-task-board/findings.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 临时禁用任务看板发现
|
||||||
|
|
||||||
|
- 页面入口位于 `public/index.html` 的 `task-board-open`,当前会话跟踪入口为 `task-tracking-control`。
|
||||||
|
- 服务端自动状态链路有两层:`dispatchTaskBoardLifecycle()` 与 `enqueueTaskStatusClassification()`。
|
||||||
|
- 任务 MCP 动态定义由 `taskBoardMcpToolDefinitionsForSession()` 返回;功能关闭时应返回空数组。
|
||||||
|
- 最小改法是使用显式常量门禁,不删除实现,便于重构后重新开放。
|
||||||
9
.planning/2026-08-21-disable-task-board/progress.md
Normal file
9
.planning/2026-08-21-disable-task-board/progress.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 临时禁用任务看板进度
|
||||||
|
|
||||||
|
- 2026-08-21:codebase-memory 索引 ready;已定位页面入口、任务跟踪控件、生命周期 Hook、分类 Hook 和任务 MCP 定义入口。
|
||||||
|
- 2026-08-21:计划审查补充了关闭态真实 MCP、旧 WebSocket 客户端、Hook no-op、历史数据不变和重启失败处理的验收标准。
|
||||||
|
- 2026-08-21:默认关闭门禁已覆盖页面入口、跟踪控件、任务 MCP、WebSocket、生命周期与自动分类;关闭态专项真实跑完一轮 Codex App 消息后历史 taskTracking 完全不变,退出码 0。
|
||||||
|
- 2026-08-21:语法、service、lifecycle、integration 均退出 0;完整回归首次因旧 usage feature 静态字面量断言退出 1,已做最小契约修正。
|
||||||
|
- 2026-08-21:完整回归修正后退出 0,`git diff --check` 退出 0;进入重启会话门禁检查。
|
||||||
|
- 2026-08-21:重启门禁确认只有当前对话运行;`ccweb` 已重启并在线,PM2 环境确认 `CC_WEB_TASK_BOARD_ENABLED=0`。
|
||||||
|
- 2026-08-21:CentOS 7 baseline 单文件包重新构建成功;六项语法检查、MCP initialize 和 tar 内容校验均退出 0,准备提交并推送全部修改。
|
||||||
36
.planning/2026-08-21-disable-task-board/task_plan.md
Normal file
36
.planning/2026-08-21-disable-task-board/task_plan.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 临时禁用任务看板
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在任务看板重构期间,通过明确的功能门禁禁用页面入口、任务跟踪控件、任务 MCP 暴露和服务端自动 Hook;保留现有实现与数据。完成后重启 ccweb、重新构建 baseline 发布包并推送。
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
- [x] 1. 定位并固定页面入口与 Hook 门禁点
|
||||||
|
- [x] 2. 写入临时功能门禁与回归契约
|
||||||
|
- [x] 3. 运行最小验证
|
||||||
|
- [x] 4. 检查会话门禁并重启 ccweb
|
||||||
|
- [x] 5. 重新构建并验证 CentOS 7 baseline 包
|
||||||
|
- [x] 6. 提交全部修改并推送 origin/main
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 不删除看板实现、历史状态或配置。
|
||||||
|
- 页面不显示看板按钮与加入看板控件。
|
||||||
|
- 服务端不执行任务生命周期和自动分类 Hook。
|
||||||
|
- 模型不再看到任务状态 MCP 工具。
|
||||||
|
- 重启前必须确认除当前对话外没有 running 会话。
|
||||||
|
|
||||||
|
## 完成标准
|
||||||
|
|
||||||
|
- 默认页面不显示任务看板按钮,当前会话不显示加入/移出看板控件。
|
||||||
|
- 默认真实 MCP `tools/list` 不包含任何 `ccweb_task_*`;过期客户端直接调用更新也被拒绝。
|
||||||
|
- 默认 WebSocket 看板查询与写入被拒绝,新建会话不能通过遗留参数加入看板。
|
||||||
|
- 用户消息、轮次完成和运行事件不会进入生命周期或自动分类 Hook。
|
||||||
|
- 隔离测试前后既有 session 的 `taskTracking` 内容与版本不变。
|
||||||
|
- 若重启门禁不满足则停止在重启前;满足后执行 `pm2 restart ccweb --update-env`。
|
||||||
|
- 发布包固定为 `dist-exe/cc-web-bun-linux-x64-baseline.tar.gz`,完成语法、MCP initialize、tar 校验后提交并推送 `origin/main`。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
- 完整回归首次退出 1:认证 feature 对象新增 `taskBoard` 后,usage statistics 旧静态断言仍匹配单行旧字面量;已更新为分别检查两个 feature 字段。
|
||||||
Binary file not shown.
@@ -305,6 +305,7 @@
|
|||||||
let usageDashboardRequestSeq = 0;
|
let usageDashboardRequestSeq = 0;
|
||||||
const taskBoardViewState = {
|
const taskBoardViewState = {
|
||||||
open: false,
|
open: false,
|
||||||
|
enabled: false,
|
||||||
savedFocus: null,
|
savedFocus: null,
|
||||||
};
|
};
|
||||||
let taskBoardController = null;
|
let taskBoardController = null;
|
||||||
@@ -4587,6 +4588,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTaskBoardFeatureEnabled(enabled) {
|
||||||
|
taskBoardViewState.enabled = enabled === true;
|
||||||
|
if (taskBoardOpen) taskBoardOpen.hidden = !taskBoardViewState.enabled;
|
||||||
|
if (taskTrackingControl) taskTrackingControl.hidden = true;
|
||||||
|
if (!taskBoardViewState.enabled) {
|
||||||
|
if (taskBoardViewState.open) closeTaskBoard({ restoreFocus: false });
|
||||||
|
taskTrackingController?.setState({ sessionId: '', disabled: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setUsageDashboardState(state, message = '') {
|
function setUsageDashboardState(state, message = '') {
|
||||||
const loading = state === 'loading';
|
const loading = state === 'loading';
|
||||||
usageDashboardState.loading = loading;
|
usageDashboardState.loading = loading;
|
||||||
@@ -5417,7 +5428,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openTaskBoard() {
|
function openTaskBoard() {
|
||||||
if (!taskBoardPanel || !taskBoardRoot || !window.CcwebTaskBoard) return;
|
if (!taskBoardViewState.enabled || !taskBoardPanel || !taskBoardRoot || !window.CcwebTaskBoard) return;
|
||||||
if (usageDashboardState.open) closeUsageDashboard({ restoreFocus: false });
|
if (usageDashboardState.open) closeUsageDashboard({ restoreFocus: false });
|
||||||
if (advancedSessionSearchState.open) closeAdvancedSessionSearch({ restoreFocus: false });
|
if (advancedSessionSearchState.open) closeAdvancedSessionSearch({ restoreFocus: false });
|
||||||
taskBoardViewState.savedFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
taskBoardViewState.savedFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||||
@@ -5444,6 +5455,11 @@
|
|||||||
|
|
||||||
function syncTaskTrackingControl(snapshot = null) {
|
function syncTaskTrackingControl(snapshot = null) {
|
||||||
if (!taskTrackingControl || !window.CcwebTaskBoard) return;
|
if (!taskTrackingControl || !window.CcwebTaskBoard) return;
|
||||||
|
if (!taskBoardViewState.enabled) {
|
||||||
|
taskTrackingControl.hidden = true;
|
||||||
|
taskTrackingController?.setState({ sessionId: '', disabled: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sessionId = snapshot?.sessionId || currentSessionId || '';
|
const sessionId = snapshot?.sessionId || currentSessionId || '';
|
||||||
const tracking = snapshot?.taskTracking || {};
|
const tracking = snapshot?.taskTracking || {};
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
@@ -6969,6 +6985,7 @@
|
|||||||
loginOverlay.hidden = true;
|
loginOverlay.hidden = true;
|
||||||
app.hidden = false;
|
app.hidden = false;
|
||||||
setUsageDashboardFeatureEnabled(msg.features?.usageStatistics === true);
|
setUsageDashboardFeatureEnabled(msg.features?.usageStatistics === true);
|
||||||
|
setTaskBoardFeatureEnabled(msg.features?.taskBoard === true);
|
||||||
const flushedSessionSwitch = flushPendingSessionSwitch();
|
const flushedSessionSwitch = flushPendingSessionSwitch();
|
||||||
const flushedSessionResume = flushedSessionSwitch ? false : flushPendingSessionResume();
|
const flushedSessionResume = flushedSessionSwitch ? false : flushPendingSessionResume();
|
||||||
if (!flushedSessionSwitch &&
|
if (!flushedSessionSwitch &&
|
||||||
@@ -6996,6 +7013,7 @@
|
|||||||
authToken = null;
|
authToken = null;
|
||||||
wsAuthenticated = false;
|
wsAuthenticated = false;
|
||||||
setUsageDashboardFeatureEnabled(false);
|
setUsageDashboardFeatureEnabled(false);
|
||||||
|
setTaskBoardFeatureEnabled(false);
|
||||||
localStorage.removeItem('cc-web-token');
|
localStorage.removeItem('cc-web-token');
|
||||||
document.dispatchEvent(new CustomEvent('cc-web-auth-failed'));
|
document.dispatchEvent(new CustomEvent('cc-web-auth-failed'));
|
||||||
loginOverlay.hidden = false;
|
loginOverlay.hidden = false;
|
||||||
@@ -11515,6 +11533,8 @@
|
|||||||
setUsageDashboardFeatureEnabled(false);
|
setUsageDashboardFeatureEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTaskBoardFeatureEnabled(false);
|
||||||
|
|
||||||
if (taskBoardOpen && taskBoardPanel && taskBoardRoot) {
|
if (taskBoardOpen && taskBoardPanel && taskBoardRoot) {
|
||||||
taskBoardOpen.addEventListener('click', openTaskBoard);
|
taskBoardOpen.addEventListener('click', openTaskBoard);
|
||||||
taskBoardClose?.addEventListener('click', () => closeTaskBoard());
|
taskBoardClose?.addEventListener('click', () => closeTaskBoard());
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
<path d="M5 21v-6"></path><path d="M12 21V9"></path><path d="M19 21V3"></path>
|
<path d="M5 21v-6"></path><path d="M12 21V9"></path><path d="M19 21V3"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="task-board-open" class="usage-dashboard-open" type="button" title="任务看板" aria-label="打开任务看板" aria-controls="task-board-panel" aria-expanded="false">
|
<button id="task-board-open" class="usage-dashboard-open" type="button" title="任务看板" aria-label="打开任务看板" aria-controls="task-board-panel" aria-expanded="false" hidden>
|
||||||
<span aria-hidden="true">▦</span>
|
<span aria-hidden="true">▦</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -813,6 +813,13 @@ function assertTaskBoardIntegrationContract() {
|
|||||||
/@media \(prefers-reduced-motion: reduce\)[\s\S]*?\.ccweb-task-board__card\[data-runtime-state='running'\]::after\s*\{[^}]*animation:\s*none;/.test(taskBoardStyleSource),
|
/@media \(prefers-reduced-motion: reduce\)[\s\S]*?\.ccweb-task-board__card\[data-runtime-state='running'\]::after\s*\{[^}]*animation:\s*none;/.test(taskBoardStyleSource),
|
||||||
'Wasteland edge tracer should retain a static reduced-motion treatment'
|
'Wasteland edge tracer should retain a static reduced-motion treatment'
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
serverSource.includes("const TASK_BOARD_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.CC_WEB_TASK_BOARD_ENABLED || ''));"),
|
||||||
|
'Task board must have an explicit opt-in runtime gate during refactor'
|
||||||
|
);
|
||||||
|
assert(indexSource.includes('id="task-board-open"') && /id="task-board-open"[^>]*hidden/.test(indexSource), 'Task board page entry should be hidden by default');
|
||||||
|
assert(frontendSource.includes('setTaskBoardFeatureEnabled(msg.features?.taskBoard === true)'), 'Frontend task board entry should follow the server feature gate');
|
||||||
|
assert(serverSource.includes("return mcpToolError('task_board_disabled'"), 'Direct task update calls must fail closed while disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertFrontendGenerationControlsContract() {
|
function assertFrontendGenerationControlsContract() {
|
||||||
@@ -4243,6 +4250,7 @@ async function runCodexAppRuntimeImageSteerRegression() {
|
|||||||
CC_WEB_CONFIG_DIR: configDir,
|
CC_WEB_CONFIG_DIR: configDir,
|
||||||
CC_WEB_SESSIONS_DIR: sessionsDir,
|
CC_WEB_SESSIONS_DIR: sessionsDir,
|
||||||
CC_WEB_LOGS_DIR: logsDir,
|
CC_WEB_LOGS_DIR: logsDir,
|
||||||
|
CC_WEB_TASK_BOARD_ENABLED: '1',
|
||||||
HOME: homeDir,
|
HOME: homeDir,
|
||||||
CLAUDE_PATH: MOCK_CLAUDE,
|
CLAUDE_PATH: MOCK_CLAUDE,
|
||||||
CODEX_PATH: MOCK_CODEX_APP_SERVER,
|
CODEX_PATH: MOCK_CODEX_APP_SERVER,
|
||||||
@@ -4377,6 +4385,7 @@ async function runCodexAppStaleRunningRegression(options = {}) {
|
|||||||
CC_WEB_CONFIG_DIR: configDir,
|
CC_WEB_CONFIG_DIR: configDir,
|
||||||
CC_WEB_SESSIONS_DIR: sessionsDir,
|
CC_WEB_SESSIONS_DIR: sessionsDir,
|
||||||
CC_WEB_LOGS_DIR: logsDir,
|
CC_WEB_LOGS_DIR: logsDir,
|
||||||
|
CC_WEB_TASK_BOARD_ENABLED: '1',
|
||||||
HOME: homeDir,
|
HOME: homeDir,
|
||||||
CLAUDE_PATH: MOCK_CLAUDE,
|
CLAUDE_PATH: MOCK_CLAUDE,
|
||||||
CODEX_PATH: MOCK_CODEX_APP_SERVER,
|
CODEX_PATH: MOCK_CODEX_APP_SERVER,
|
||||||
@@ -5370,7 +5379,8 @@ function assertUsageStatisticsContract() {
|
|||||||
assert(serverSource.includes("case 'usage_stats_query':")
|
assert(serverSource.includes("case 'usage_stats_query':")
|
||||||
&& serverSource.includes("type: 'usage_stats_result'")
|
&& serverSource.includes("type: 'usage_stats_result'")
|
||||||
&& serverSource.includes("type: 'usage_stats_error'")
|
&& serverSource.includes("type: 'usage_stats_error'")
|
||||||
&& serverSource.includes('features: { usageStatistics: USAGE_STATISTICS_ENABLED }'),
|
&& serverSource.includes('usageStatistics: USAGE_STATISTICS_ENABLED')
|
||||||
|
&& serverSource.includes('taskBoard: TASK_BOARD_ENABLED'),
|
||||||
'Server should expose the versioned usage query protocol and authenticated feature flag');
|
'Server should expose the versioned usage query protocol and authenticated feature flag');
|
||||||
assert(!queryHandler.includes('sendSessionList') && !queryHandler.includes('broadcastSessionList'),
|
assert(!queryHandler.includes('sendSessionList') && !queryHandler.includes('broadcastSessionList'),
|
||||||
'Usage queries must not refresh or replace session_list');
|
'Usage queries must not refresh or replace session_list');
|
||||||
@@ -6325,6 +6335,7 @@ async function main() {
|
|||||||
CC_WEB_CONFIG_DIR: configDir,
|
CC_WEB_CONFIG_DIR: configDir,
|
||||||
CC_WEB_SESSIONS_DIR: sessionsDir,
|
CC_WEB_SESSIONS_DIR: sessionsDir,
|
||||||
CC_WEB_LOGS_DIR: logsDir,
|
CC_WEB_LOGS_DIR: logsDir,
|
||||||
|
CC_WEB_TASK_BOARD_ENABLED: '1',
|
||||||
HOME: homeDir,
|
HOME: homeDir,
|
||||||
CLAUDE_PATH: MOCK_CLAUDE,
|
CLAUDE_PATH: MOCK_CLAUDE,
|
||||||
CODEX_PATH: MOCK_CODEX_APP_SERVER,
|
CODEX_PATH: MOCK_CODEX_APP_SERVER,
|
||||||
|
|||||||
194
scripts/task-board-disabled-unit.js
Normal file
194
scripts/task-board-disabled-unit.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const http = require('node:http');
|
||||||
|
const net = require('node:net');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawn } = require('node:child_process');
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
const SERVER = path.join(ROOT, 'server.js');
|
||||||
|
|
||||||
|
function freePort() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = net.createServer();
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
const port = server.address().port;
|
||||||
|
server.close(() => resolve(port));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForPort(port) {
|
||||||
|
const started = Date.now();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const check = () => {
|
||||||
|
const socket = net.createConnection({ host: '127.0.0.1', port });
|
||||||
|
socket.once('connect', () => { socket.destroy(); resolve(); });
|
||||||
|
socket.once('error', () => {
|
||||||
|
if (Date.now() - started > 8_000) reject(new Error('等待关闭态测试服务超时'));
|
||||||
|
else setTimeout(check, 40);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function callMcp(port, token, sourceSessionId, method, params = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const body = JSON.stringify({ jsonrpc: '2.0', id: `disabled-${Date.now()}`, method, params });
|
||||||
|
const request = http.request({
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port,
|
||||||
|
path: `/api/internal/mcp/stream?sourceSessionId=${encodeURIComponent(sourceSessionId)}`,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'content-length': Buffer.byteLength(body),
|
||||||
|
'x-cc-web-mcp-token': token,
|
||||||
|
},
|
||||||
|
}, (response) => {
|
||||||
|
let text = '';
|
||||||
|
response.setEncoding('utf8');
|
||||||
|
response.on('data', (chunk) => { text += chunk; });
|
||||||
|
response.on('end', () => resolve({ status: response.statusCode, body: JSON.parse(text || '{}') }));
|
||||||
|
});
|
||||||
|
request.once('error', reject);
|
||||||
|
request.end(body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect(port, password) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
|
||||||
|
const messages = [];
|
||||||
|
ws.on('open', () => ws.send(JSON.stringify({ type: 'auth', password })));
|
||||||
|
ws.on('message', (raw) => {
|
||||||
|
const message = JSON.parse(String(raw));
|
||||||
|
messages.push(message);
|
||||||
|
if (message.type === 'auth_result') {
|
||||||
|
if (message.success) resolve({ ws, messages, auth: message });
|
||||||
|
else reject(new Error('关闭态测试认证失败'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ws.once('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function next(messages, predicate) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const started = Date.now();
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const index = messages.findIndex(predicate);
|
||||||
|
if (index >= 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
resolve(messages.splice(index, 1)[0]);
|
||||||
|
} else if (Date.now() - started > 5_000) {
|
||||||
|
clearInterval(timer);
|
||||||
|
reject(new Error(`关闭态 WS 响应超时: ${messages.map((item) => item.type).join(',')}`));
|
||||||
|
}
|
||||||
|
}, 25);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-task-board-disabled-'));
|
||||||
|
const configDir = path.join(root, 'config');
|
||||||
|
const sessionsDir = path.join(root, 'sessions');
|
||||||
|
const logsDir = path.join(root, 'logs');
|
||||||
|
[configDir, sessionsDir, logsDir].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
|
||||||
|
const sessionId = 'disabled-existing-task';
|
||||||
|
const sessionPath = path.join(sessionsDir, `${sessionId}.json`);
|
||||||
|
const before = {
|
||||||
|
id: sessionId,
|
||||||
|
title: '重构前历史任务',
|
||||||
|
agent: 'codexapp',
|
||||||
|
cwd: root,
|
||||||
|
created: '2026-08-21T00:00:00.000Z',
|
||||||
|
updated: '2026-08-21T00:00:00.000Z',
|
||||||
|
messages: [],
|
||||||
|
taskTracking: {
|
||||||
|
enabled: true,
|
||||||
|
statusId: 'in_progress',
|
||||||
|
source: 'user',
|
||||||
|
version: 7,
|
||||||
|
archivedAt: '2026-08-20T23:59:00.000Z',
|
||||||
|
archivedBy: 'user',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fs.writeFileSync(sessionPath, JSON.stringify(before, null, 2));
|
||||||
|
const port = await freePort();
|
||||||
|
const password = 'Disabled!234';
|
||||||
|
const token = 'DisabledMcp!234';
|
||||||
|
const child = spawn(process.execPath, [SERVER], {
|
||||||
|
cwd: ROOT,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PORT: String(port),
|
||||||
|
CC_WEB_PASSWORD: password,
|
||||||
|
CC_WEB_INTERNAL_MCP_TOKEN: token,
|
||||||
|
CC_WEB_CONFIG_DIR: configDir,
|
||||||
|
CC_WEB_SESSIONS_DIR: sessionsDir,
|
||||||
|
CC_WEB_LOGS_DIR: logsDir,
|
||||||
|
CC_WEB_TASK_BOARD_ENABLED: '0',
|
||||||
|
HOME: root,
|
||||||
|
CODEX_PATH: path.join(ROOT, 'scripts', 'mock-codex-app-server.js'),
|
||||||
|
},
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await waitForPort(port);
|
||||||
|
const html = await (await fetch(`http://127.0.0.1:${port}/`)).text();
|
||||||
|
assert.match(html, /id="task-board-open"[^>]*hidden/);
|
||||||
|
assert.match(html, /id="task-tracking-control"[^>]*hidden/);
|
||||||
|
|
||||||
|
const tools = await callMcp(port, token, sessionId, 'tools/list');
|
||||||
|
assert.equal(tools.status, 200);
|
||||||
|
assert.deepEqual(
|
||||||
|
(tools.body.result?.tools || []).map((tool) => tool.name).filter((name) => name.startsWith('ccweb_task_')),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const update = await callMcp(port, token, sessionId, 'tools/call', {
|
||||||
|
name: 'ccweb_task_update',
|
||||||
|
arguments: { statusId: 'completed' },
|
||||||
|
});
|
||||||
|
assert.equal(update.body.result?.structuredContent?.code, 'task_board_disabled');
|
||||||
|
|
||||||
|
const client = await connect(port, password);
|
||||||
|
client.ws.send(JSON.stringify({ type: 'task_board_query', requestId: 'disabled-query' }));
|
||||||
|
const query = await next(client.messages, (message) => message.type === 'task_board_result' && message.requestId === 'disabled-query');
|
||||||
|
assert.equal(query.code, 'task_board_disabled');
|
||||||
|
client.ws.send(JSON.stringify({ type: 'new_session', requestId: 'disabled-new', taskTrackingEnabled: true }));
|
||||||
|
const created = await next(client.messages, (message) => message.type === 'session_info' && message.requestId === 'disabled-new');
|
||||||
|
assert.equal(created.taskTracking.enabled, false);
|
||||||
|
|
||||||
|
client.ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
text: '关闭态普通轮次不得触发任务状态 Hook',
|
||||||
|
sessionId,
|
||||||
|
mode: 'yolo',
|
||||||
|
agent: 'codexapp',
|
||||||
|
}));
|
||||||
|
await next(client.messages, (message) => message.type === 'done' && message.sessionId === sessionId);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
client.ws.close();
|
||||||
|
|
||||||
|
assert.deepEqual(JSON.parse(fs.readFileSync(sessionPath, 'utf8')).taskTracking, before.taskTracking);
|
||||||
|
assert.equal(client.auth.features.taskBoard, false);
|
||||||
|
console.log('Task board disabled checks passed.');
|
||||||
|
} finally {
|
||||||
|
if (child.exitCode === null && child.signalCode === null) {
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
await new Promise((resolve) => child.once('exit', resolve));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
@@ -344,6 +344,7 @@ async function main() {
|
|||||||
CC_WEB_CONFIG_DIR: configDir,
|
CC_WEB_CONFIG_DIR: configDir,
|
||||||
CC_WEB_SESSIONS_DIR: sessionsDir,
|
CC_WEB_SESSIONS_DIR: sessionsDir,
|
||||||
CC_WEB_LOGS_DIR: logsDir,
|
CC_WEB_LOGS_DIR: logsDir,
|
||||||
|
CC_WEB_TASK_BOARD_ENABLED: '1',
|
||||||
HOME: homeDir,
|
HOME: homeDir,
|
||||||
CLAUDE_PATH: MOCK_CLAUDE,
|
CLAUDE_PATH: MOCK_CLAUDE,
|
||||||
CODEX_PATH: MOCK_CODEX_APP_SERVER,
|
CODEX_PATH: MOCK_CODEX_APP_SERVER,
|
||||||
|
|||||||
34
server.js
34
server.js
@@ -173,6 +173,7 @@ const CODEX_APP_CCWEB_MCP_TRANSPORT = normalizeCodexAppCcwebMcpTransport(process
|
|||||||
const CODEX_APP_WORKER_DISABLED = /^(0|false|no|off)$/i.test(String(process.env.CC_WEB_CODEX_APP_WORKER || ''));
|
const CODEX_APP_WORKER_DISABLED = /^(0|false|no|off)$/i.test(String(process.env.CC_WEB_CODEX_APP_WORKER || ''));
|
||||||
const CODEX_APP_WORKER_ENABLED = !CODEX_APP_WORKER_DISABLED;
|
const CODEX_APP_WORKER_ENABLED = !CODEX_APP_WORKER_DISABLED;
|
||||||
const USAGE_STATISTICS_ENABLED = !/^(0|false|no|off)$/i.test(String(process.env.CC_WEB_USAGE_STATISTICS || ''));
|
const USAGE_STATISTICS_ENABLED = !/^(0|false|no|off)$/i.test(String(process.env.CC_WEB_USAGE_STATISTICS || ''));
|
||||||
|
const TASK_BOARD_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.CC_WEB_TASK_BOARD_ENABLED || ''));
|
||||||
const CODEX_APP_PROCESS_ENV_STRIP_KEYS = [
|
const CODEX_APP_PROCESS_ENV_STRIP_KEYS = [
|
||||||
'CC_WEB_MCP_URL',
|
'CC_WEB_MCP_URL',
|
||||||
'CC_WEB_MCP_TOKEN',
|
'CC_WEB_MCP_TOKEN',
|
||||||
@@ -2780,6 +2781,7 @@ function listComposerMcpItems(options = {}) {
|
|||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const push = (item) => {
|
const push = (item) => {
|
||||||
if (!item?.server && !item?.name) return;
|
if (!item?.server && !item?.name) return;
|
||||||
|
if (!TASK_BOARD_ENABLED && String(item.name || '').startsWith('ccweb_task_')) return;
|
||||||
const key = `${item.kind || ''}:${item.server || ''}:${item.name || item.label || item.insertion || ''}`;
|
const key = `${item.kind || ''}:${item.server || ''}:${item.name || item.label || item.insertion || ''}`;
|
||||||
if (seen.has(key)) return;
|
if (seen.has(key)) return;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
@@ -4574,7 +4576,7 @@ function createProductionTaskBoardService() {
|
|||||||
|
|
||||||
taskBoardService = createProductionTaskBoardService();
|
taskBoardService = createProductionTaskBoardService();
|
||||||
taskBoardMcpHandlers = createTaskBoardMcpHandlers(taskBoardService);
|
taskBoardMcpHandlers = createTaskBoardMcpHandlers(taskBoardService);
|
||||||
taskBoardLifecycle = createTaskBoardLifecycle(taskBoardService, {
|
taskBoardLifecycle = TASK_BOARD_ENABLED ? createTaskBoardLifecycle(taskBoardService, {
|
||||||
isTrackingEnabled(sessionId) {
|
isTrackingEnabled(sessionId) {
|
||||||
try {
|
try {
|
||||||
return taskBoardService.getTask(sessionId).taskTracking.enabled;
|
return taskBoardService.getTask(sessionId).taskTracking.enabled;
|
||||||
@@ -4585,8 +4587,8 @@ taskBoardLifecycle = createTaskBoardLifecycle(taskBoardService, {
|
|||||||
logger(level, event, data) {
|
logger(level, event, data) {
|
||||||
plog(String(level || '').toUpperCase() === 'ERROR' ? 'ERROR' : 'WARN', event, data);
|
plog(String(level || '').toUpperCase() === 'ERROR' ? 'ERROR' : 'WARN', event, data);
|
||||||
},
|
},
|
||||||
});
|
}) : null;
|
||||||
taskStatusClassifier = createTaskStatusClassifier({
|
taskStatusClassifier = TASK_BOARD_ENABLED ? createTaskStatusClassifier({
|
||||||
taskBoardService,
|
taskBoardService,
|
||||||
loadSession,
|
loadSession,
|
||||||
resolveRuntime: resolveTaskStatusClassifierRuntime,
|
resolveRuntime: resolveTaskStatusClassifierRuntime,
|
||||||
@@ -4600,10 +4602,10 @@ taskStatusClassifier = createTaskStatusClassifier({
|
|||||||
});
|
});
|
||||||
broadcastSessionList();
|
broadcastSessionList();
|
||||||
},
|
},
|
||||||
});
|
}) : null;
|
||||||
|
|
||||||
function enqueueTaskStatusClassification(sessionId, event = {}) {
|
function enqueueTaskStatusClassification(sessionId, event = {}) {
|
||||||
if (!taskStatusClassifier || !sessionId) return Promise.resolve(null);
|
if (!TASK_BOARD_ENABLED || !taskStatusClassifier || !sessionId) return Promise.resolve(null);
|
||||||
return taskStatusClassifier.enqueue(sessionId, event).catch((error) => {
|
return taskStatusClassifier.enqueue(sessionId, event).catch((error) => {
|
||||||
plog('WARN', 'task_status_classification_enqueue_failed', {
|
plog('WARN', 'task_status_classification_enqueue_failed', {
|
||||||
sessionId: String(sessionId).slice(0, 8),
|
sessionId: String(sessionId).slice(0, 8),
|
||||||
@@ -4615,7 +4617,7 @@ function enqueueTaskStatusClassification(sessionId, event = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function taskBoardMcpToolDefinitionsForSession(sessionId) {
|
function taskBoardMcpToolDefinitionsForSession(sessionId) {
|
||||||
if (!taskBoardService || !sessionId) return [];
|
if (!TASK_BOARD_ENABLED || !taskBoardService || !sessionId) return [];
|
||||||
try {
|
try {
|
||||||
return createTaskBoardMcpToolDefinitions(taskBoardService, {
|
return createTaskBoardMcpToolDefinitions(taskBoardService, {
|
||||||
sourceSessionId: sanitizeId(sessionId),
|
sourceSessionId: sanitizeId(sessionId),
|
||||||
@@ -5320,7 +5322,7 @@ function broadcastTaskBoardEvent(eventName, subject = null, extra = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dispatchTaskBoardLifecycle(sessionId, event = {}) {
|
function dispatchTaskBoardLifecycle(sessionId, event = {}) {
|
||||||
if (!taskBoardLifecycle || !sessionId || event.trackingEnabled === false) return Promise.resolve(null);
|
if (!TASK_BOARD_ENABLED || !taskBoardLifecycle || !sessionId || event.trackingEnabled === false) return Promise.resolve(null);
|
||||||
return taskBoardLifecycle.handleEvent(sessionId, event).then((result) => {
|
return taskBoardLifecycle.handleEvent(sessionId, event).then((result) => {
|
||||||
if (result?.changed && result.task) {
|
if (result?.changed && result.task) {
|
||||||
broadcastTaskBoardEvent(`lifecycle:${event.type || result.eventType || 'updated'}`, result.task, {
|
broadcastTaskBoardEvent(`lifecycle:${event.type || result.eventType || 'updated'}`, result.task, {
|
||||||
@@ -5349,6 +5351,14 @@ function sendTaskBoardWsResult(ws, request, payload = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTaskBoardWsMessage(ws, message = {}) {
|
function handleTaskBoardWsMessage(ws, message = {}) {
|
||||||
|
if (!TASK_BOARD_ENABLED) {
|
||||||
|
sendTaskBoardWsResult(ws, message, {
|
||||||
|
ok: false,
|
||||||
|
code: 'task_board_disabled',
|
||||||
|
message: '任务看板正在重构,暂不可用。',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const actor = { source: 'user', id: 'ccweb-ui' };
|
const actor = { source: 'user', id: 'ccweb-ui' };
|
||||||
try {
|
try {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
@@ -6577,6 +6587,9 @@ function callInternalMcpTool(tool, args, sourceSessionId, sourceHopCount) {
|
|||||||
case 'ccweb_prompt_user':
|
case 'ccweb_prompt_user':
|
||||||
return createCcwebPromptUser(args, sourceSessionId);
|
return createCcwebPromptUser(args, sourceSessionId);
|
||||||
case 'ccweb_task_update': {
|
case 'ccweb_task_update': {
|
||||||
|
if (!TASK_BOARD_ENABLED) {
|
||||||
|
return mcpToolError('task_board_disabled', '任务看板正在重构,暂不可用。');
|
||||||
|
}
|
||||||
const result = taskBoardMcpHandlers.ccweb_task_update(args, {
|
const result = taskBoardMcpHandlers.ccweb_task_update(args, {
|
||||||
sourceSessionId,
|
sourceSessionId,
|
||||||
actorId: `mcp:${String(sourceSessionId || '').slice(0, 32)}`,
|
actorId: `mcp:${String(sourceSessionId || '').slice(0, 32)}`,
|
||||||
@@ -7717,7 +7730,10 @@ wss.on('connection', (ws, req) => {
|
|||||||
token: authToken,
|
token: authToken,
|
||||||
mustChangePassword: !!authConfig.mustChange,
|
mustChangePassword: !!authConfig.mustChange,
|
||||||
frontendAssetVersion: computeFrontendAssetVersion(),
|
frontendAssetVersion: computeFrontendAssetVersion(),
|
||||||
features: { usageStatistics: USAGE_STATISTICS_ENABLED },
|
features: {
|
||||||
|
usageStatistics: USAGE_STATISTICS_ENABLED,
|
||||||
|
taskBoard: TASK_BOARD_ENABLED,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
sendSessionList(ws);
|
sendSessionList(ws);
|
||||||
} else {
|
} else {
|
||||||
@@ -9232,7 +9248,7 @@ function handleNewSession(ws, msg) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
let { session } = result;
|
let { session } = result;
|
||||||
if (request.taskTrackingEnabled === true) {
|
if (TASK_BOARD_ENABLED && request.taskTrackingEnabled === true) {
|
||||||
try {
|
try {
|
||||||
const trackedTask = taskBoardService.setTracking(session.id, true, {
|
const trackedTask = taskBoardService.setTracking(session.id, true, {
|
||||||
source: 'user',
|
source: 'user',
|
||||||
|
|||||||
Reference in New Issue
Block a user