principles-disciple 1.7.4 → 1.7.5
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/dist/commands/focus.js +30 -155
- package/dist/constants/diagnostician.d.ts +16 -0
- package/dist/constants/diagnostician.js +60 -0
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +1 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +7 -0
- package/dist/core/evolution-engine.js +1 -1
- package/dist/core/focus-history.d.ts +92 -0
- package/dist/core/focus-history.js +490 -0
- package/dist/core/init.js +2 -2
- package/dist/core/profile.js +1 -1
- package/dist/hooks/gate.js +3 -3
- package/dist/hooks/prompt.js +73 -22
- package/dist/hooks/subagent.js +1 -2
- package/dist/http/principles-console-route.d.ts +7 -0
- package/dist/http/principles-console-route.js +243 -1
- package/dist/index.js +0 -2
- package/dist/service/central-database.d.ts +104 -0
- package/dist/service/central-database.js +648 -0
- package/dist/service/evolution-worker.js +3 -3
- package/dist/tools/deep-reflect.js +1 -2
- package/dist/utils/subagent-probe.d.ts +11 -0
- package/dist/utils/subagent-probe.js +46 -1
- package/package.json +2 -1
- package/templates/langs/en/core/AGENTS.md +1 -1
- package/templates/langs/en/core/TOOLS.md +1 -1
- package/templates/langs/zh/core/AGENTS.md +1 -1
- package/templates/langs/zh/core/TOOLS.md +1 -1
- package/{agents/auditor.md → templates/langs/zh/skills/pd-auditor/SKILL.md} +3 -3
- package/{agents/diagnostician.md → templates/langs/zh/skills/pd-diagnostician/SKILL.md} +39 -29
- package/{agents/explorer.md → templates/langs/zh/skills/pd-explorer/SKILL.md} +3 -3
- package/{agents/implementer.md → templates/langs/zh/skills/pd-implementer/SKILL.md} +3 -3
- package/{agents/planner.md → templates/langs/zh/skills/pd-planner/SKILL.md} +3 -3
- package/{agents/reporter.md → templates/langs/zh/skills/pd-reporter/SKILL.md} +3 -3
- package/{agents/reviewer.md → templates/langs/zh/skills/pd-reviewer/SKILL.md} +3 -3
- package/dist/core/agent-loader.d.ts +0 -44
- package/dist/core/agent-loader.js +0 -147
- package/dist/tools/agent-spawn.d.ts +0 -54
- package/dist/tools/agent-spawn.js +0 -456
package/dist/hooks/prompt.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from 'path';
|
|
|
3
3
|
import { clearInjectedProbationIds, getSession, resetFriction, setInjectedProbationIds } from '../core/session-tracker.js';
|
|
4
4
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
5
5
|
import { defaultContextConfig } from '../types.js';
|
|
6
|
-
import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection } from '../core/focus-history.js';
|
|
6
|
+
import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
|
|
7
7
|
import { empathyObserverManager } from '../service/empathy-observer-manager.js';
|
|
8
8
|
import { PathResolver } from '../core/path-resolver.js';
|
|
9
9
|
/**
|
|
@@ -130,6 +130,8 @@ function resolveEvolutionTask(inProgressTask, messages, maxContextMessages = 4,
|
|
|
130
130
|
const preview = typeof inProgressTask.trigger_text_preview === 'string' && inProgressTask.trigger_text_preview.trim()
|
|
131
131
|
? inProgressTask.trigger_text_preview.trim()
|
|
132
132
|
: 'N/A';
|
|
133
|
+
const sessionId = typeof inProgressTask.session_id === 'string' ? inProgressTask.session_id.trim() : '';
|
|
134
|
+
const agentId = typeof inProgressTask.agent_id === 'string' ? inProgressTask.agent_id.trim() : '';
|
|
133
135
|
const conversationContext = includeConversationContext
|
|
134
136
|
? extractRecentConversationContext(messages, maxContextMessages, maxCharsPerMsg)
|
|
135
137
|
: '';
|
|
@@ -142,11 +144,27 @@ function resolveEvolutionTask(inProgressTask, messages, maxContextMessages = 4,
|
|
|
142
144
|
`;
|
|
143
145
|
taskDescription += `**Trigger Text**: "${preview}"
|
|
144
146
|
`;
|
|
147
|
+
if (sessionId) {
|
|
148
|
+
taskDescription += `**Session ID**: ${sessionId}
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
if (agentId) {
|
|
152
|
+
taskDescription += `**Agent ID**: ${agentId}
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
145
155
|
if (conversationContext) {
|
|
146
156
|
taskDescription += `
|
|
147
157
|
---
|
|
148
158
|
**Recent Conversation Context**:
|
|
149
159
|
${conversationContext}`;
|
|
160
|
+
}
|
|
161
|
+
else if (!sessionId) {
|
|
162
|
+
taskDescription += `
|
|
163
|
+
---
|
|
164
|
+
**Note**: 对话上下文不可用。请主动收集证据:
|
|
165
|
+
1. 从 Reason 字段提取关键词,搜索相关代码
|
|
166
|
+
2. 读取 .state/logs/events.jsonl 最近日志
|
|
167
|
+
3. 基于 Reason 中的文件路径定位问题`;
|
|
150
168
|
}
|
|
151
169
|
taskDescription += `
|
|
152
170
|
|
|
@@ -227,7 +245,6 @@ export function resolveModelFromConfig(modelConfig, logger) {
|
|
|
227
245
|
}
|
|
228
246
|
// 闁哄秶鍘х槐?3: 闁轰焦澹嗙划宥夊冀閻撳海纭€闁挎稑鐗呯粭澶愬绩椤栨稑鐦柨娑樿嫰瑜板倿宕欐ウ娆惧妳闁告稑顭槐?
|
|
229
247
|
if (Array.isArray(modelConfig)) {
|
|
230
|
-
console.warn(`[PD:Prompt] Array model config not supported. Expected "provider/model" string or { primary: "..." } object.`);
|
|
231
248
|
logger?.warn(`[PD:Prompt] Array model config not supported. Expected "provider/model" string or { primary: "..." } object.`);
|
|
232
249
|
return null;
|
|
233
250
|
}
|
|
@@ -388,8 +405,7 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
|
388
405
|
- Use the current session for the normal user reply.
|
|
389
406
|
- Use sessions_send for cross-session messaging.
|
|
390
407
|
- Use agents_list / sessions_list / sessions_spawn for peer-agent or peer-session orchestration.
|
|
391
|
-
- Use
|
|
392
|
-
- Use pd_run_worker only for Principles Disciple internal workers such as diagnostician/explorer.
|
|
408
|
+
- Use sessions_spawn with pd-diagnostician/pd-explorer/etc skills for internal worker tasks.
|
|
393
409
|
|
|
394
410
|
## 妫e啯鎯?INTERNAL SYSTEM LAYOUT
|
|
395
411
|
- Your core plugin logic is rooted at: ${PathResolver.getExtensionRoot() || 'EXTENSION_ROOT (unresolved)'}
|
|
@@ -407,7 +423,7 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
|
407
423
|
trustContext += `Hygiene: ${hygiene.persistenceCount} persists today\n`;
|
|
408
424
|
// Stage-based restrictions
|
|
409
425
|
if (safeStage === 1) {
|
|
410
|
-
trustContext += `ACTION CONSTRAINT: You are in READ-ONLY MODE. You MUST use
|
|
426
|
+
trustContext += `ACTION CONSTRAINT: You are in READ-ONLY MODE. You MUST use sessions_spawn with the pd-diagnostician skill to recover trust before writing files.\n`;
|
|
411
427
|
}
|
|
412
428
|
else if (safeStage === 2) {
|
|
413
429
|
trustContext += `ACTION CONSTRAINT: LIMITED MODE. You are restricted to a maximum of 50 lines per edit.\n`;
|
|
@@ -444,13 +460,30 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
|
|
|
444
460
|
evolutionDirective = `<evolution_task priority="high">
|
|
445
461
|
TASK: ${escapedTask}
|
|
446
462
|
|
|
447
|
-
REQUIRED ACTION:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
463
|
+
REQUIRED ACTION (两阶段回复):
|
|
464
|
+
|
|
465
|
+
**阶段 1 - 立即回复用户:**
|
|
466
|
+
简要告知用户你检测到的问题,例如:
|
|
467
|
+
"我注意到刚才的操作遇到了一些问题(简述问题),我已安排诊断代理在后台分析根因。分析完成后会继续告诉你结果。"
|
|
468
|
+
|
|
469
|
+
然后调用:
|
|
470
|
+
sessions_spawn(task="使用 pd-diagnostician skill 执行诊断任务。\n\n任务: ${escapedTask}")
|
|
471
|
+
|
|
472
|
+
**阶段 2 - 收到子代理结果后:**
|
|
473
|
+
当收到诊断代理的完成消息后,用以下格式回复用户:
|
|
474
|
+
|
|
475
|
+
"**诊断完成** 🔍
|
|
476
|
+
|
|
477
|
+
基于后台分析结果:
|
|
478
|
+
- **根因**: [一句话总结]
|
|
479
|
+
- **分类**: [People/Design/Assumption/Tooling]
|
|
480
|
+
- **建议**: [可执行的改进建议]
|
|
481
|
+
|
|
482
|
+
相关原则已记录到 PRINCIPLES.md。"
|
|
483
|
+
|
|
484
|
+
IMPORTANT:
|
|
452
485
|
- Do NOT reply with "[EVOLUTION_ACK]".
|
|
453
|
-
-
|
|
486
|
+
- 子代理结果会作为新消息到达,届时再进行阶段2的回复。
|
|
454
487
|
</evolution_task>\n`;
|
|
455
488
|
}
|
|
456
489
|
else {
|
|
@@ -459,8 +492,7 @@ TASK: ${escapedTask}
|
|
|
459
492
|
|
|
460
493
|
REQUIRED ACTION:
|
|
461
494
|
- Start diagnostics immediately by calling:
|
|
462
|
-
|
|
463
|
-
- Treat pd_run_worker as an internal Principles Disciple worker, not a peer-session messaging tool.
|
|
495
|
+
sessions_spawn(task="使用 pd-diagnostician skill 执行诊断任务。\n\n任务: ${escapedTask}")
|
|
464
496
|
- Do NOT reply with "[EVOLUTION_ACK]".
|
|
465
497
|
</evolution_task>\n`;
|
|
466
498
|
}
|
|
@@ -581,34 +613,53 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
|
|
|
581
613
|
let workingMemoryContent = '';
|
|
582
614
|
if (!isMinimalMode && contextConfig.projectFocus !== 'off') {
|
|
583
615
|
const focusPath = wctx.resolve('CURRENT_FOCUS');
|
|
584
|
-
|
|
616
|
+
const extensionRoot = PathResolver.getExtensionRoot();
|
|
617
|
+
// 🔒 安全读取:自动验证格式,损坏时从模板恢复
|
|
618
|
+
const { content: currentFocus, recovered, validationErrors } = safeReadCurrentFocus(focusPath, extensionRoot || '', logger);
|
|
619
|
+
if (recovered) {
|
|
620
|
+
logger?.info?.(`[PD:Prompt] CURRENT_FOCUS.md was recovered from template`);
|
|
621
|
+
}
|
|
622
|
+
if (validationErrors.length > 0) {
|
|
623
|
+
logger?.warn?.(`[PD:Prompt] CURRENT_FOCUS validation errors: ${validationErrors.join(', ')}`);
|
|
624
|
+
}
|
|
625
|
+
if (currentFocus.trim()) {
|
|
585
626
|
try {
|
|
586
|
-
|
|
587
|
-
|
|
627
|
+
// 🚀 自动压缩门禁:检查文件大小,超过阈值自动压缩
|
|
628
|
+
const stateDir = wctx.stateDir;
|
|
629
|
+
const compressResult = autoCompressFocus(focusPath, workspaceDir, stateDir);
|
|
630
|
+
if (compressResult.compressed) {
|
|
631
|
+
logger?.info?.(`[PD:Prompt] Auto-compressed CURRENT_FOCUS: ${compressResult.oldLines} → ${compressResult.newLines} lines. Milestones archived: ${compressResult.milestonesArchived}`);
|
|
632
|
+
}
|
|
633
|
+
else if (compressResult.reason === 'Rate limited (24h interval)') {
|
|
634
|
+
logger?.debug?.(`[PD:Prompt] Auto-compress skipped: ${compressResult.reason}`);
|
|
635
|
+
}
|
|
636
|
+
// 重新读取(可能被压缩更新了)
|
|
637
|
+
const finalContent = fs.readFileSync(focusPath, 'utf8').trim();
|
|
638
|
+
if (finalContent) {
|
|
588
639
|
// 解析工作记忆部分(用于独立注入)
|
|
589
|
-
const workingMemorySnapshot = parseWorkingMemorySection(
|
|
640
|
+
const workingMemorySnapshot = parseWorkingMemorySection(finalContent);
|
|
590
641
|
if (workingMemorySnapshot) {
|
|
591
642
|
workingMemoryContent = workingMemoryToInjection(workingMemorySnapshot);
|
|
592
643
|
}
|
|
593
644
|
if (contextConfig.projectFocus === 'summary') {
|
|
594
645
|
// Summary mode: intelligent extraction prioritizing key sections
|
|
595
|
-
projectContextContent = extractSummary(
|
|
646
|
+
projectContextContent = extractSummary(finalContent, 30);
|
|
596
647
|
}
|
|
597
648
|
else {
|
|
598
649
|
// Full mode: current version + recent history (3 versions)
|
|
599
650
|
const historyVersions = getHistoryVersions(focusPath, 3);
|
|
600
651
|
if (historyVersions.length > 0) {
|
|
601
|
-
const historySections = historyVersions.map((v, i) => `\n---\n\n
|
|
602
|
-
projectContextContent = `${
|
|
652
|
+
const historySections = historyVersions.map((v, i) => `\n---\n\n**历史版本 v${historyVersions.length - i}**\n\n${v}`).join('');
|
|
653
|
+
projectContextContent = `${finalContent}${historySections}`;
|
|
603
654
|
}
|
|
604
655
|
else {
|
|
605
|
-
projectContextContent =
|
|
656
|
+
projectContextContent = finalContent;
|
|
606
657
|
}
|
|
607
658
|
}
|
|
608
659
|
}
|
|
609
660
|
}
|
|
610
661
|
catch (e) {
|
|
611
|
-
logger?.error(`[PD:Prompt] Failed to
|
|
662
|
+
logger?.error(`[PD:Prompt] Failed to process CURRENT_FOCUS: ${String(e)}`);
|
|
612
663
|
}
|
|
613
664
|
}
|
|
614
665
|
}
|
package/dist/hooks/subagent.js
CHANGED
|
@@ -3,7 +3,6 @@ import { writePainFlag } from '../core/pain.js';
|
|
|
3
3
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
4
4
|
import { empathyObserverManager } from '../service/empathy-observer-manager.js';
|
|
5
5
|
import { acquireQueueLock } from '../service/evolution-worker.js';
|
|
6
|
-
import { isSubagentRuntimeAvailable } from '../utils/subagent-probe.js';
|
|
7
6
|
const COMPLETION_RETRY_DELAY_MS = 250;
|
|
8
7
|
const COMPLETION_MAX_RETRIES = 3;
|
|
9
8
|
const COMPLETION_RETRY_TTL_MS = 60 * 60 * 1000; // 1 hour TTL for retry entries
|
|
@@ -252,7 +251,7 @@ export async function handleSubagentEnded(event, ctx) {
|
|
|
252
251
|
}
|
|
253
252
|
}
|
|
254
253
|
// Read diagnostician output and create principle with generalized pattern
|
|
255
|
-
if (completedTaskId &&
|
|
254
|
+
if (completedTaskId && ctx.api?.runtime?.subagent) {
|
|
256
255
|
try {
|
|
257
256
|
const messages = await ctx.api?.runtime?.subagent?.getSessionMessages?.({
|
|
258
257
|
sessionKey: targetSessionKey,
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { OpenClawPluginApi, OpenClawPluginHttpRouteParams } from '../openclaw-sdk.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create routes for Principles Console.
|
|
4
|
+
* Returns an array of routes:
|
|
5
|
+
* 1. Static files route (no auth required for HTML/CSS/JS)
|
|
6
|
+
* 2. API route (gateway auth required)
|
|
7
|
+
*/
|
|
8
|
+
export declare function createPrinciplesConsoleRoutes(api: OpenClawPluginApi): OpenClawPluginHttpRouteParams[];
|
|
2
9
|
export declare function createPrinciplesConsoleRoute(api: OpenClawPluginApi): OpenClawPluginHttpRouteParams;
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { ControlUiQueryService } from '../service/control-ui-query-service.js';
|
|
4
4
|
import { getEvolutionQueryService } from '../service/evolution-query-service.js';
|
|
5
5
|
import { TrajectoryRegistry } from '../core/trajectory.js';
|
|
6
|
+
import { getCentralDatabase } from '../service/central-database.js';
|
|
6
7
|
const ROUTE_PREFIX = '/plugins/principles';
|
|
7
8
|
const API_PREFIX = `${ROUTE_PREFIX}/api`;
|
|
8
9
|
const ASSETS_PREFIX = `${ROUTE_PREFIX}/assets`;
|
|
@@ -82,6 +83,11 @@ function createService(api) {
|
|
|
82
83
|
return new ControlUiQueryService(workspaceDir);
|
|
83
84
|
}
|
|
84
85
|
function handleApiRoute(api, pathname, req, res) {
|
|
86
|
+
// Check authentication for API routes
|
|
87
|
+
if (!validateGatewayAuth(req)) {
|
|
88
|
+
json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
85
91
|
const service = createService(api);
|
|
86
92
|
const url = new URL(req.url || pathname, 'http://127.0.0.1');
|
|
87
93
|
const method = (req.method || 'GET').toUpperCase();
|
|
@@ -103,6 +109,153 @@ function handleApiRoute(api, pathname, req, res) {
|
|
|
103
109
|
if (pathname === `${API_PREFIX}/overview` && method === 'GET') {
|
|
104
110
|
return done(() => service.getOverview());
|
|
105
111
|
}
|
|
112
|
+
if (pathname === `${API_PREFIX}/central/overview` && method === 'GET') {
|
|
113
|
+
return done(() => {
|
|
114
|
+
const centralDb = getCentralDatabase();
|
|
115
|
+
const stats = centralDb.getOverviewStats();
|
|
116
|
+
const trend = centralDb.getDailyTrend(14);
|
|
117
|
+
const regressions = centralDb.getTopRegressions(5);
|
|
118
|
+
const thinkingStats = centralDb.getThinkingModelStats();
|
|
119
|
+
const workspaces = centralDb.getWorkspaces();
|
|
120
|
+
return {
|
|
121
|
+
workspaceDir: 'central',
|
|
122
|
+
generatedAt: new Date().toISOString(),
|
|
123
|
+
dataFreshness: workspaces.length > 0 ? (workspaces[0].lastSync ?? null) : null,
|
|
124
|
+
dataSource: 'central_aggregated_db',
|
|
125
|
+
runtimeControlPlaneSource: 'all_workspaces',
|
|
126
|
+
summary: {
|
|
127
|
+
repeatErrorRate: stats.totalToolCalls > 0
|
|
128
|
+
? stats.totalFailures / stats.totalToolCalls
|
|
129
|
+
: 0,
|
|
130
|
+
userCorrectionRate: stats.totalToolCalls > 0
|
|
131
|
+
? stats.totalCorrections / stats.totalToolCalls
|
|
132
|
+
: 0,
|
|
133
|
+
pendingSamples: stats.pendingSamples,
|
|
134
|
+
approvedSamples: stats.approvedSamples,
|
|
135
|
+
thinkingCoverageRate: stats.totalToolCalls > 0
|
|
136
|
+
? stats.totalThinkingEvents / stats.totalToolCalls
|
|
137
|
+
: 0,
|
|
138
|
+
painEvents: stats.totalPainEvents,
|
|
139
|
+
principleEventCount: 0,
|
|
140
|
+
gateBlocks: 0,
|
|
141
|
+
taskOutcomes: 0,
|
|
142
|
+
},
|
|
143
|
+
dailyTrend: trend,
|
|
144
|
+
topRegressions: regressions,
|
|
145
|
+
sampleQueue: {
|
|
146
|
+
counters: {
|
|
147
|
+
pending: stats.pendingSamples,
|
|
148
|
+
approved: stats.approvedSamples,
|
|
149
|
+
rejected: stats.rejectedSamples,
|
|
150
|
+
},
|
|
151
|
+
preview: [],
|
|
152
|
+
},
|
|
153
|
+
thinkingSummary: {
|
|
154
|
+
activeModels: thinkingStats.activeModels,
|
|
155
|
+
dormantModels: thinkingStats.totalModels - thinkingStats.activeModels,
|
|
156
|
+
effectiveModels: thinkingStats.models.filter(m => m.coverageRate > 0.1).length,
|
|
157
|
+
coverageRate: stats.totalToolCalls > 0
|
|
158
|
+
? stats.totalThinkingEvents / stats.totalToolCalls
|
|
159
|
+
: 0,
|
|
160
|
+
},
|
|
161
|
+
centralInfo: {
|
|
162
|
+
workspaceCount: stats.workspaceCount,
|
|
163
|
+
enabledWorkspaceCount: stats.enabledWorkspaceCount,
|
|
164
|
+
workspaces: stats.workspaceNames,
|
|
165
|
+
enabledWorkspaces: stats.enabledWorkspaceNames,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (pathname === `${API_PREFIX}/central/sync` && method === 'POST') {
|
|
171
|
+
return done(() => {
|
|
172
|
+
const centralDb = getCentralDatabase();
|
|
173
|
+
const results = centralDb.syncEnabled();
|
|
174
|
+
const summary = {};
|
|
175
|
+
results.forEach((count, name) => {
|
|
176
|
+
summary[name] = count;
|
|
177
|
+
});
|
|
178
|
+
return { synced: summary, timestamp: new Date().toISOString() };
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (pathname === `${API_PREFIX}/central/workspaces` && method === 'GET') {
|
|
182
|
+
return done(() => {
|
|
183
|
+
const centralDb = getCentralDatabase();
|
|
184
|
+
const configs = centralDb.getWorkspaceConfigs();
|
|
185
|
+
const workspaces = centralDb.getWorkspaces();
|
|
186
|
+
return {
|
|
187
|
+
configs,
|
|
188
|
+
workspaces: workspaces.map(ws => ({
|
|
189
|
+
name: ws.name,
|
|
190
|
+
path: ws.path,
|
|
191
|
+
lastSync: ws.lastSync,
|
|
192
|
+
config: configs.find(c => c.workspaceName === ws.name) ?? null,
|
|
193
|
+
})),
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const workspaceConfigMatch = pathname.match(/^\/plugins\/principles\/api\/central\/workspaces\/([^/]+)$/);
|
|
198
|
+
if (workspaceConfigMatch && method === 'GET') {
|
|
199
|
+
return done(() => {
|
|
200
|
+
const centralDb = getCentralDatabase();
|
|
201
|
+
const workspaceName = decodeURIComponent(workspaceConfigMatch[1]);
|
|
202
|
+
const configs = centralDb.getWorkspaceConfigs();
|
|
203
|
+
const config = configs.find(c => c.workspaceName === workspaceName);
|
|
204
|
+
return config ?? { workspaceName, enabled: true, displayName: workspaceName, syncEnabled: true };
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (workspaceConfigMatch && method === 'PATCH') {
|
|
208
|
+
return (async () => {
|
|
209
|
+
try {
|
|
210
|
+
const body = await readJsonBody(req);
|
|
211
|
+
const centralDb = getCentralDatabase();
|
|
212
|
+
const workspaceName = decodeURIComponent(workspaceConfigMatch[1]);
|
|
213
|
+
centralDb.updateWorkspaceConfig(workspaceName, {
|
|
214
|
+
enabled: body.enabled,
|
|
215
|
+
displayName: body.displayName,
|
|
216
|
+
syncEnabled: body.syncEnabled,
|
|
217
|
+
});
|
|
218
|
+
const configs = centralDb.getWorkspaceConfigs();
|
|
219
|
+
json(res, 200, configs.find(c => c.workspaceName === workspaceName));
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof Error && error.message === 'invalid_json') {
|
|
224
|
+
json(res, 400, { error: 'bad_request', message: 'Request body must be valid JSON.' });
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
api.logger.warn(`[PD:ControlUI] Workspace config update failed: ${String(error)}`);
|
|
228
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
})();
|
|
232
|
+
}
|
|
233
|
+
if (pathname === `${API_PREFIX}/central/workspaces` && method === 'POST') {
|
|
234
|
+
return (async () => {
|
|
235
|
+
try {
|
|
236
|
+
const body = await readJsonBody(req);
|
|
237
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
238
|
+
const workspacePath = typeof body.path === 'string' ? body.path : '';
|
|
239
|
+
if (!name || !workspacePath) {
|
|
240
|
+
json(res, 400, { error: 'bad_request', message: 'name and path are required.' });
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
const centralDb = getCentralDatabase();
|
|
244
|
+
centralDb.addCustomWorkspace(name, workspacePath);
|
|
245
|
+
json(res, 201, { success: true, workspace: name });
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
if (error instanceof Error && error.message === 'invalid_json') {
|
|
250
|
+
json(res, 400, { error: 'bad_request', message: 'Request body must be valid JSON.' });
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
api.logger.warn(`[PD:ControlUI] Add workspace failed: ${String(error)}`);
|
|
254
|
+
json(res, 500, { error: 'internal_error', message: String(error) });
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
})();
|
|
258
|
+
}
|
|
106
259
|
if (pathname === `${API_PREFIX}/samples` && method === 'GET') {
|
|
107
260
|
return done(() => service.listSamples({
|
|
108
261
|
status: url.searchParams.get('status') ?? undefined,
|
|
@@ -275,10 +428,93 @@ function handleApiRoute(api, pathname, req, res) {
|
|
|
275
428
|
json(res, 404, { error: 'not_found', message: 'Unknown Principles Console API route.' });
|
|
276
429
|
return true;
|
|
277
430
|
}
|
|
431
|
+
function getGatewayToken() {
|
|
432
|
+
try {
|
|
433
|
+
const configPath = path.join(process.env.HOME || '', '.openclaw', 'openclaw.json');
|
|
434
|
+
if (!fs.existsSync(configPath))
|
|
435
|
+
return null;
|
|
436
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
437
|
+
return config?.gateway?.auth?.token || null;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function validateGatewayAuth(req) {
|
|
444
|
+
const gatewayToken = getGatewayToken();
|
|
445
|
+
if (!gatewayToken) {
|
|
446
|
+
// No token configured, allow all requests
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
const authHeader = req.headers?.['authorization'] || '';
|
|
450
|
+
const tokenMatch = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
451
|
+
const providedToken = tokenMatch?.[1];
|
|
452
|
+
return providedToken === gatewayToken;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Create routes for Principles Console.
|
|
456
|
+
* Returns an array of routes:
|
|
457
|
+
* 1. Static files route (no auth required for HTML/CSS/JS)
|
|
458
|
+
* 2. API route (gateway auth required)
|
|
459
|
+
*/
|
|
460
|
+
export function createPrinciplesConsoleRoutes(api) {
|
|
461
|
+
// Route 1: Static files (HTML, CSS, JS) - no auth check
|
|
462
|
+
const staticRoute = {
|
|
463
|
+
path: ROUTE_PREFIX,
|
|
464
|
+
auth: 'plugin',
|
|
465
|
+
match: 'prefix',
|
|
466
|
+
async handler(req, res) {
|
|
467
|
+
const url = new URL(req.url || ROUTE_PREFIX, 'http://127.0.0.1');
|
|
468
|
+
const pathname = url.pathname;
|
|
469
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
470
|
+
// Skip API routes - they'll be handled by the API route
|
|
471
|
+
if (pathname.startsWith(API_PREFIX)) {
|
|
472
|
+
return false; // Let the API route handle this
|
|
473
|
+
}
|
|
474
|
+
// Serve assets
|
|
475
|
+
if (pathname.startsWith(ASSETS_PREFIX)) {
|
|
476
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
477
|
+
text(res, 405, 'Method Not Allowed');
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
const assetPath = safeStaticPath(api.rootDir, pathname);
|
|
481
|
+
if (!assetPath || !serveFile(res, assetPath)) {
|
|
482
|
+
text(res, 404, 'Asset Not Found');
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
// Serve index.html for the main route
|
|
487
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
488
|
+
text(res, 405, 'Method Not Allowed');
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
const indexPath = path.join(api.rootDir, 'dist', 'web', 'index.html');
|
|
492
|
+
if (!serveFile(res, indexPath)) {
|
|
493
|
+
text(res, 503, 'Principles Console UI is not built yet.');
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
// Route 2: API endpoints - gateway auth required
|
|
499
|
+
const apiRoute = {
|
|
500
|
+
path: API_PREFIX,
|
|
501
|
+
auth: 'gateway',
|
|
502
|
+
match: 'prefix',
|
|
503
|
+
async handler(req, res) {
|
|
504
|
+
const url = new URL(req.url || API_PREFIX, 'http://127.0.0.1');
|
|
505
|
+
const pathname = url.pathname;
|
|
506
|
+
return handleApiRoute(api, pathname, req, res);
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
return [staticRoute, apiRoute];
|
|
510
|
+
}
|
|
511
|
+
// Legacy export for backwards compatibility
|
|
278
512
|
export function createPrinciplesConsoleRoute(api) {
|
|
513
|
+
const routes = createPrinciplesConsoleRoutes(api);
|
|
514
|
+
// Return the combined behavior - this will be called from index.ts
|
|
279
515
|
return {
|
|
280
516
|
path: ROUTE_PREFIX,
|
|
281
|
-
auth: '
|
|
517
|
+
auth: 'plugin',
|
|
282
518
|
match: 'prefix',
|
|
283
519
|
async handler(req, res) {
|
|
284
520
|
const url = new URL(req.url || ROUTE_PREFIX, 'http://127.0.0.1');
|
|
@@ -287,9 +523,15 @@ export function createPrinciplesConsoleRoute(api) {
|
|
|
287
523
|
if (!pathname.startsWith(ROUTE_PREFIX)) {
|
|
288
524
|
return false;
|
|
289
525
|
}
|
|
526
|
+
// For API routes, check auth manually
|
|
290
527
|
if (pathname.startsWith(API_PREFIX)) {
|
|
528
|
+
if (!validateGatewayAuth(req)) {
|
|
529
|
+
json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
291
532
|
return handleApiRoute(api, pathname, req, res);
|
|
292
533
|
}
|
|
534
|
+
// Static files - no auth required
|
|
293
535
|
if (pathname.startsWith(ASSETS_PREFIX)) {
|
|
294
536
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
295
537
|
text(res, 405, 'Method Not Allowed');
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,6 @@ import { ensureWorkspaceTemplates } from './core/init.js';
|
|
|
26
26
|
import { migrateDirectoryStructure } from './core/migration.js';
|
|
27
27
|
import { SystemLogger } from './core/system-logger.js';
|
|
28
28
|
import { createDeepReflectTool } from './tools/deep-reflect.js';
|
|
29
|
-
import { createAgentSpawnTool } from './tools/agent-spawn.js';
|
|
30
29
|
import { PathResolver } from './core/path-resolver.js';
|
|
31
30
|
import { createPrinciplesConsoleRoute } from './http/principles-console-route.js';
|
|
32
31
|
// Track initialization to avoid repeated calls
|
|
@@ -476,7 +475,6 @@ const plugin = {
|
|
|
476
475
|
}
|
|
477
476
|
});
|
|
478
477
|
api.registerTool(createDeepReflectTool(api));
|
|
479
|
-
api.registerTool(createAgentSpawnTool(api));
|
|
480
478
|
}
|
|
481
479
|
};
|
|
482
480
|
export default plugin;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export interface WorkspaceInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
lastSync: string | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Central database that aggregates data from all agent workspaces.
|
|
8
|
+
* Stored in ~/.openclaw/.central/ (NOT in memory/ which is for embeddings)
|
|
9
|
+
*/
|
|
10
|
+
export declare class CentralDatabase {
|
|
11
|
+
private readonly dbPath;
|
|
12
|
+
private readonly db;
|
|
13
|
+
private readonly workspaces;
|
|
14
|
+
constructor();
|
|
15
|
+
dispose(): void;
|
|
16
|
+
private tableExists;
|
|
17
|
+
private initSchema;
|
|
18
|
+
private discoverWorkspaces;
|
|
19
|
+
/**
|
|
20
|
+
* Sync data from a single workspace into the central database
|
|
21
|
+
*/
|
|
22
|
+
syncWorkspace(workspaceName: string): number;
|
|
23
|
+
syncEnabled(): Map<string, number>;
|
|
24
|
+
/**
|
|
25
|
+
* Sync all workspaces (legacy method - syncs all regardless of config)
|
|
26
|
+
*/
|
|
27
|
+
syncAll(): Map<string, number>;
|
|
28
|
+
private getEnabledWorkspaceFilter;
|
|
29
|
+
/**
|
|
30
|
+
* Get aggregated overview stats (only from enabled workspaces)
|
|
31
|
+
*/
|
|
32
|
+
getOverviewStats(): {
|
|
33
|
+
totalSessions: number;
|
|
34
|
+
totalToolCalls: number;
|
|
35
|
+
totalFailures: number;
|
|
36
|
+
totalPainEvents: number;
|
|
37
|
+
totalCorrections: number;
|
|
38
|
+
totalThinkingEvents: number;
|
|
39
|
+
totalSamples: number;
|
|
40
|
+
pendingSamples: number;
|
|
41
|
+
approvedSamples: number;
|
|
42
|
+
rejectedSamples: number;
|
|
43
|
+
workspaceCount: number;
|
|
44
|
+
enabledWorkspaceCount: number;
|
|
45
|
+
workspaceNames: string[];
|
|
46
|
+
enabledWorkspaceNames: string[];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Get daily trend data
|
|
50
|
+
*/
|
|
51
|
+
getDailyTrend(days?: number): Array<{
|
|
52
|
+
day: string;
|
|
53
|
+
toolCalls: number;
|
|
54
|
+
failures: number;
|
|
55
|
+
userCorrections: number;
|
|
56
|
+
thinkingTurns: number;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* Get top regressions
|
|
60
|
+
*/
|
|
61
|
+
getTopRegressions(limit?: number): Array<{
|
|
62
|
+
toolName: string;
|
|
63
|
+
errorType: string;
|
|
64
|
+
occurrences: number;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Get thinking model stats
|
|
68
|
+
*/
|
|
69
|
+
getThinkingModelStats(): {
|
|
70
|
+
totalModels: number;
|
|
71
|
+
activeModels: number;
|
|
72
|
+
models: Array<{
|
|
73
|
+
modelId: string;
|
|
74
|
+
hits: number;
|
|
75
|
+
coverageRate: number;
|
|
76
|
+
}>;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Get workspace list
|
|
80
|
+
*/
|
|
81
|
+
getWorkspaces(): WorkspaceInfo[];
|
|
82
|
+
getWorkspaceConfigs(): Array<{
|
|
83
|
+
workspaceName: string;
|
|
84
|
+
enabled: boolean;
|
|
85
|
+
displayName: string | null;
|
|
86
|
+
syncEnabled: boolean;
|
|
87
|
+
}>;
|
|
88
|
+
updateWorkspaceConfig(workspaceName: string, updates: {
|
|
89
|
+
enabled?: boolean;
|
|
90
|
+
displayName?: string | null;
|
|
91
|
+
syncEnabled?: boolean;
|
|
92
|
+
}): void;
|
|
93
|
+
isWorkspaceEnabled(workspaceName: string): boolean;
|
|
94
|
+
getEnabledWorkspaces(): WorkspaceInfo[];
|
|
95
|
+
addCustomWorkspace(name: string, workspacePath: string): void;
|
|
96
|
+
removeWorkspace(workspaceName: string): void;
|
|
97
|
+
getGlobalConfig(key: string): string | null;
|
|
98
|
+
setGlobalConfig(key: string, value: string): void;
|
|
99
|
+
/**
|
|
100
|
+
* Clear all aggregated data (for testing/reset)
|
|
101
|
+
*/
|
|
102
|
+
clearAll(): void;
|
|
103
|
+
}
|
|
104
|
+
export declare function getCentralDatabase(): CentralDatabase;
|