principles-disciple 1.7.4 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/commands/evolution-status.js +32 -44
  2. package/dist/commands/focus.js +30 -155
  3. package/dist/config/defaults/runtime.d.ts +40 -0
  4. package/dist/config/defaults/runtime.js +44 -0
  5. package/dist/config/errors.d.ts +84 -0
  6. package/dist/config/errors.js +94 -0
  7. package/dist/config/index.d.ts +7 -0
  8. package/dist/config/index.js +7 -0
  9. package/dist/constants/diagnostician.d.ts +12 -0
  10. package/dist/constants/diagnostician.js +56 -0
  11. package/dist/constants/tools.d.ts +2 -2
  12. package/dist/constants/tools.js +1 -1
  13. package/dist/core/config.d.ts +12 -0
  14. package/dist/core/config.js +7 -0
  15. package/dist/core/control-ui-db.d.ts +27 -0
  16. package/dist/core/control-ui-db.js +18 -0
  17. package/dist/core/evolution-engine.js +1 -1
  18. package/dist/core/focus-history.d.ts +92 -0
  19. package/dist/core/focus-history.js +490 -0
  20. package/dist/core/init.js +2 -2
  21. package/dist/core/path-resolver.js +2 -1
  22. package/dist/core/profile.js +1 -1
  23. package/dist/core/trajectory.d.ts +60 -0
  24. package/dist/core/trajectory.js +72 -2
  25. package/dist/hooks/bash-risk.d.ts +57 -0
  26. package/dist/hooks/bash-risk.js +137 -0
  27. package/dist/hooks/edit-verification.d.ts +62 -0
  28. package/dist/hooks/edit-verification.js +256 -0
  29. package/dist/hooks/gate-block-helper.d.ts +44 -0
  30. package/dist/hooks/gate-block-helper.js +119 -0
  31. package/dist/hooks/gate.d.ts +18 -0
  32. package/dist/hooks/gate.js +63 -752
  33. package/dist/hooks/gfi-gate.d.ts +40 -0
  34. package/dist/hooks/gfi-gate.js +112 -0
  35. package/dist/hooks/progressive-trust-gate.d.ts +79 -0
  36. package/dist/hooks/progressive-trust-gate.js +242 -0
  37. package/dist/hooks/prompt.js +83 -28
  38. package/dist/hooks/subagent.js +1 -2
  39. package/dist/hooks/thinking-checkpoint.d.ts +37 -0
  40. package/dist/hooks/thinking-checkpoint.js +51 -0
  41. package/dist/http/principles-console-route.d.ts +7 -0
  42. package/dist/http/principles-console-route.js +255 -3
  43. package/dist/index.js +0 -2
  44. package/dist/service/central-database.d.ts +104 -0
  45. package/dist/service/central-database.js +649 -0
  46. package/dist/service/control-ui-query-service.d.ts +1 -1
  47. package/dist/service/control-ui-query-service.js +3 -3
  48. package/dist/service/evolution-query-service.d.ts +1 -1
  49. package/dist/service/evolution-query-service.js +5 -5
  50. package/dist/service/evolution-worker.d.ts +10 -0
  51. package/dist/service/evolution-worker.js +10 -6
  52. package/dist/service/phase3-input-filter.d.ts +57 -0
  53. package/dist/service/phase3-input-filter.js +93 -3
  54. package/dist/service/runtime-summary-service.d.ts +34 -0
  55. package/dist/service/runtime-summary-service.js +93 -1
  56. package/dist/tools/deep-reflect.js +1 -2
  57. package/dist/types/event-types.d.ts +2 -0
  58. package/dist/types/runtime-summary.d.ts +54 -0
  59. package/dist/types/runtime-summary.js +1 -0
  60. package/dist/utils/subagent-probe.d.ts +11 -0
  61. package/dist/utils/subagent-probe.js +46 -1
  62. package/openclaw.plugin.json +1 -1
  63. package/package.json +2 -1
  64. package/templates/langs/en/core/AGENTS.md +1 -1
  65. package/templates/langs/en/core/TOOLS.md +1 -1
  66. package/templates/langs/zh/core/AGENTS.md +1 -1
  67. package/templates/langs/zh/core/TOOLS.md +1 -1
  68. package/{agents/auditor.md → templates/langs/zh/skills/pd-auditor/SKILL.md} +3 -3
  69. package/{agents/diagnostician.md → templates/langs/zh/skills/pd-diagnostician/SKILL.md} +39 -29
  70. package/{agents/explorer.md → templates/langs/zh/skills/pd-explorer/SKILL.md} +3 -3
  71. package/{agents/implementer.md → templates/langs/zh/skills/pd-implementer/SKILL.md} +3 -3
  72. package/{agents/planner.md → templates/langs/zh/skills/pd-planner/SKILL.md} +3 -3
  73. package/{agents/reporter.md → templates/langs/zh/skills/pd-reporter/SKILL.md} +3 -3
  74. package/{agents/reviewer.md → templates/langs/zh/skills/pd-reviewer/SKILL.md} +3 -3
  75. package/dist/core/agent-loader.d.ts +0 -44
  76. package/dist/core/agent-loader.js +0 -147
  77. package/dist/tools/agent-spawn.d.ts +0 -54
  78. package/dist/tools/agent-spawn.js +0 -456
@@ -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 subagents to inspect already-dispatched internal workers such as diagnostician/explorer.
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 the internal pd_run_worker diagnostician worker to recover trust before writing files. Do not use it for peer-session messaging.\n`;
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`;
@@ -421,7 +437,11 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
421
437
  prependContext += `<system_override:runtime_constraints>\n${trustContext.trim()}\n</system_override:runtime_constraints>\n`;
422
438
  }
423
439
  // 闁崇儤鍔忛弲鏌ュ煛?3. Evolution Directive (always on, highest priority) - stays in prependContext 闁崇儤鍔忛弲鏌ュ煛?
424
- let evolutionDirective = '';
440
+ // NOTE: active evolution task prompt is injected from EVOLUTION_QUEUE for active tasks
441
+ // NOT used for Phase 3 eligibility decisions
442
+ // EVOLUTION_DIRECTIVE.json is a compatibility-only display artifact
443
+ // Phase 3 eligibility uses only queue and trust (see phase3-input-filter.ts)
444
+ let activeEvolutionTaskPrompt = '';
425
445
  const queuePath = wctx.resolve('EVOLUTION_QUEUE');
426
446
  if (fs.existsSync(queuePath)) {
427
447
  try {
@@ -441,32 +461,48 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
441
461
  const escapedTask = JSON.stringify(resolvedTask);
442
462
  logger?.info(`[PD:Prompt] Injecting EVOLUTION TASK for: ${inProgressTask.id}`);
443
463
  if (trigger === 'user') {
444
- evolutionDirective = `<evolution_task priority="high">
464
+ activeEvolutionTaskPrompt = `<evolution_task priority="high">
445
465
  TASK: ${escapedTask}
446
466
 
447
- REQUIRED ACTION:
448
- - First respond to the user's current request normally.
449
- - After finishing the user-facing response, call:
450
- pd_run_worker agentType="diagnostician" task=${escapedTask} runInBackground=true
451
- - Treat pd_run_worker as an internal Principles Disciple worker, not a peer-session messaging tool.
467
+ REQUIRED ACTION (两阶段回复):
468
+
469
+ **阶段 1 - 立即回复用户:**
470
+ 简要告知用户你检测到的问题,例如:
471
+ "我注意到刚才的操作遇到了一些问题(简述问题),我已安排诊断代理在后台分析根因。分析完成后会继续告诉你结果。"
472
+
473
+ 然后调用:
474
+ sessions_spawn(task="使用 pd-diagnostician skill 执行诊断任务。\n\n任务: ${escapedTask}")
475
+
476
+ **阶段 2 - 收到子代理结果后:**
477
+ 当收到诊断代理的完成消息后,用以下格式回复用户:
478
+
479
+ "**诊断完成** 🔍
480
+
481
+ 基于后台分析结果:
482
+ - **根因**: [一句话总结]
483
+ - **分类**: [People/Design/Assumption/Tooling]
484
+ - **建议**: [可执行的改进建议]
485
+
486
+ 相关原则已记录到 PRINCIPLES.md。"
487
+
488
+ IMPORTANT:
452
489
  - Do NOT reply with "[EVOLUTION_ACK]".
453
- - Do NOT let this task interrupt the current user interaction.
490
+ - 子代理结果会作为新消息到达,届时再进行阶段2的回复。
454
491
  </evolution_task>\n`;
455
492
  }
456
493
  else {
457
- evolutionDirective = `<evolution_task priority="critical">
494
+ activeEvolutionTaskPrompt = `<evolution_task priority="critical">
458
495
  TASK: ${escapedTask}
459
496
 
460
497
  REQUIRED ACTION:
461
498
  - Start diagnostics immediately by calling:
462
- pd_run_worker agentType="diagnostician" task=${escapedTask} runInBackground=true
463
- - Treat pd_run_worker as an internal Principles Disciple worker, not a peer-session messaging tool.
499
+ sessions_spawn(task="使用 pd-diagnostician skill 执行诊断任务。\n\n任务: ${escapedTask}")
464
500
  - Do NOT reply with "[EVOLUTION_ACK]".
465
501
  </evolution_task>\n`;
466
502
  }
467
503
  break;
468
504
  }
469
- if (!evolutionDirective && inProgressTasks.length > 0) {
505
+ if (!activeEvolutionTaskPrompt && inProgressTasks.length > 0) {
470
506
  logger?.warn('[PD:Prompt] Skipping evolution task injection because task payload is invalid.');
471
507
  }
472
508
  }
@@ -475,8 +511,8 @@ REQUIRED ACTION:
475
511
  }
476
512
  }
477
513
  // Inject queue-derived evolution task at the front of prependContext
478
- if (evolutionDirective) {
479
- prependContext = evolutionDirective + prependContext;
514
+ if (activeEvolutionTaskPrompt) {
515
+ prependContext = activeEvolutionTaskPrompt + prependContext;
480
516
  }
481
517
  // 鈺愨晲鈺?4. Empathy Observer Spawn (async sidecar) 鈺愨晲鈺?
482
518
  // Skip if this is a subagent session or if the message indicates agent-to-agent communication
@@ -581,34 +617,53 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
581
617
  let workingMemoryContent = '';
582
618
  if (!isMinimalMode && contextConfig.projectFocus !== 'off') {
583
619
  const focusPath = wctx.resolve('CURRENT_FOCUS');
584
- if (fs.existsSync(focusPath)) {
620
+ const extensionRoot = PathResolver.getExtensionRoot();
621
+ // 🔒 安全读取:自动验证格式,损坏时从模板恢复
622
+ const { content: currentFocus, recovered, validationErrors } = safeReadCurrentFocus(focusPath, extensionRoot || '', logger);
623
+ if (recovered) {
624
+ logger?.info?.(`[PD:Prompt] CURRENT_FOCUS.md was recovered from template`);
625
+ }
626
+ if (validationErrors.length > 0) {
627
+ logger?.warn?.(`[PD:Prompt] CURRENT_FOCUS validation errors: ${validationErrors.join(', ')}`);
628
+ }
629
+ if (currentFocus.trim()) {
585
630
  try {
586
- const currentFocus = fs.readFileSync(focusPath, 'utf8').trim();
587
- if (currentFocus) {
631
+ // 🚀 自动压缩门禁:检查文件大小,超过阈值自动压缩
632
+ const stateDir = wctx.stateDir;
633
+ const compressResult = autoCompressFocus(focusPath, workspaceDir, stateDir);
634
+ if (compressResult.compressed) {
635
+ logger?.info?.(`[PD:Prompt] Auto-compressed CURRENT_FOCUS: ${compressResult.oldLines} → ${compressResult.newLines} lines. Milestones archived: ${compressResult.milestonesArchived}`);
636
+ }
637
+ else if (compressResult.reason === 'Rate limited (24h interval)') {
638
+ logger?.debug?.(`[PD:Prompt] Auto-compress skipped: ${compressResult.reason}`);
639
+ }
640
+ // 重新读取(可能被压缩更新了)
641
+ const finalContent = fs.readFileSync(focusPath, 'utf8').trim();
642
+ if (finalContent) {
588
643
  // 解析工作记忆部分(用于独立注入)
589
- const workingMemorySnapshot = parseWorkingMemorySection(currentFocus);
644
+ const workingMemorySnapshot = parseWorkingMemorySection(finalContent);
590
645
  if (workingMemorySnapshot) {
591
646
  workingMemoryContent = workingMemoryToInjection(workingMemorySnapshot);
592
647
  }
593
648
  if (contextConfig.projectFocus === 'summary') {
594
649
  // Summary mode: intelligent extraction prioritizing key sections
595
- projectContextContent = extractSummary(currentFocus, 30);
650
+ projectContextContent = extractSummary(finalContent, 30);
596
651
  }
597
652
  else {
598
653
  // Full mode: current version + recent history (3 versions)
599
654
  const historyVersions = getHistoryVersions(focusPath, 3);
600
655
  if (historyVersions.length > 0) {
601
- const historySections = historyVersions.map((v, i) => `\n---\n\n**闁告ê妫楄ぐ鍫曟偋閸喐鎷?v${historyVersions.length - i}**\n\n${v}`).join('');
602
- projectContextContent = `${currentFocus}${historySections}`;
656
+ const historySections = historyVersions.map((v, i) => `\n---\n\n**历史版本 v${historyVersions.length - i}**\n\n${v}`).join('');
657
+ projectContextContent = `${finalContent}${historySections}`;
603
658
  }
604
659
  else {
605
- projectContextContent = currentFocus;
660
+ projectContextContent = finalContent;
606
661
  }
607
662
  }
608
663
  }
609
664
  }
610
665
  catch (e) {
611
- logger?.error(`[PD:Prompt] Failed to read CURRENT_FOCUS: ${String(e)}`);
666
+ logger?.error(`[PD:Prompt] Failed to process CURRENT_FOCUS: ${String(e)}`);
612
667
  }
613
668
  }
614
669
  }
@@ -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 && isSubagentRuntimeAvailable(ctx.api?.runtime?.subagent)) {
254
+ if (completedTaskId && ctx.api?.runtime?.subagent) {
256
255
  try {
257
256
  const messages = await ctx.api?.runtime?.subagent?.getSessionMessages?.({
258
257
  sessionKey: targetSessionKey,
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Thinking Checkpoint Module
3
+ *
4
+ * Enforces P-10 deep reflection requirement for high-risk tool operations.
5
+ *
6
+ * **Responsibilities:**
7
+ * - Check if high-risk tools have recent deep thinking (T-01 through T-10)
8
+ * - Block high-risk operations without preceding deep reflection
9
+ * - Configurable time window for thinking validity (default 5 minutes)
10
+ * - Provide clear guidance on required action (deep_reflect tool usage)
11
+ *
12
+ * **Configuration:**
13
+ * - Thinking checkpoint settings from profile.thinking_checkpoint
14
+ * - Window duration for thinking validity
15
+ * - High-risk tool list
16
+ */
17
+ import type { PluginHookBeforeToolCallEvent, PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
18
+ export interface ThinkingCheckpointConfig {
19
+ enabled?: boolean;
20
+ window_ms?: number;
21
+ high_risk_tools?: string[];
22
+ }
23
+ /**
24
+ * Checks if a tool call requires a recent deep thinking checkpoint.
25
+ *
26
+ * This enforces P-10 (Thinking OS Checkpoint) - high-risk operations must
27
+ * be preceded by deep reflection within the configured time window.
28
+ *
29
+ * @param event - The before_tool_call event
30
+ * @param config - Thinking checkpoint configuration from profile
31
+ * @param sessionId - Current session ID
32
+ * @param logger - Optional logger for info messages
33
+ * @returns Block result if thinking required, undefined otherwise
34
+ */
35
+ export declare function checkThinkingCheckpoint(event: PluginHookBeforeToolCallEvent, config: ThinkingCheckpointConfig, sessionId: string | undefined, logger?: {
36
+ info?: (message: string) => void;
37
+ }): PluginHookBeforeToolCallResult | undefined;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Thinking Checkpoint Module
3
+ *
4
+ * Enforces P-10 deep reflection requirement for high-risk tool operations.
5
+ *
6
+ * **Responsibilities:**
7
+ * - Check if high-risk tools have recent deep thinking (T-01 through T-10)
8
+ * - Block high-risk operations without preceding deep reflection
9
+ * - Configurable time window for thinking validity (default 5 minutes)
10
+ * - Provide clear guidance on required action (deep_reflect tool usage)
11
+ *
12
+ * **Configuration:**
13
+ * - Thinking checkpoint settings from profile.thinking_checkpoint
14
+ * - Window duration for thinking validity
15
+ * - High-risk tool list
16
+ */
17
+ import { hasRecentThinking } from '../core/session-tracker.js';
18
+ import { THINKING_CHECKPOINT_WINDOW_MS, THINKING_CHECKPOINT_DEFAULT_HIGH_RISK_TOOLS } from '../config/index.js';
19
+ /**
20
+ * Checks if a tool call requires a recent deep thinking checkpoint.
21
+ *
22
+ * This enforces P-10 (Thinking OS Checkpoint) - high-risk operations must
23
+ * be preceded by deep reflection within the configured time window.
24
+ *
25
+ * @param event - The before_tool_call event
26
+ * @param config - Thinking checkpoint configuration from profile
27
+ * @param sessionId - Current session ID
28
+ * @param logger - Optional logger for info messages
29
+ * @returns Block result if thinking required, undefined otherwise
30
+ */
31
+ export function checkThinkingCheckpoint(event, config, sessionId, logger) {
32
+ const enabled = config.enabled ?? false;
33
+ const windowMs = config.window_ms ?? THINKING_CHECKPOINT_WINDOW_MS;
34
+ const highRiskTools = config.high_risk_tools ?? [...THINKING_CHECKPOINT_DEFAULT_HIGH_RISK_TOOLS];
35
+ if (!enabled || !sessionId) {
36
+ return undefined;
37
+ }
38
+ const isHighRisk = highRiskTools.includes(event.toolName);
39
+ if (!isHighRisk) {
40
+ return undefined;
41
+ }
42
+ const hasThinking = hasRecentThinking(sessionId, windowMs);
43
+ if (!hasThinking) {
44
+ logger?.info?.(`[PD:THINKING_GATE] High-risk tool "${event.toolName}" called without recent deep thinking`);
45
+ return {
46
+ block: true,
47
+ blockReason: `[Thinking OS Checkpoint] 高风险操作 "${event.toolName}" 需要先进行深度思考。\n\n请先使用 deep_reflect 工具分析当前情况,然后再尝试此操作。\n\n这是强制性检查点,目的是确保决策质量。\n\n提示:调用 deep_reflect 后,${Math.round(windowMs / 60000)}分钟内的操作将自动放行。\n\n可在PROFILE.json中设置 thinking_checkpoint.enabled: false 来禁用此检查。`,
48
+ };
49
+ }
50
+ return undefined;
51
+ }
@@ -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();
@@ -100,8 +106,164 @@ function handleApiRoute(api, pathname, req, res) {
100
106
  service.dispose();
101
107
  }
102
108
  };
109
+ // Helper to parse and clamp days parameter
110
+ const parseDays = (param) => {
111
+ const value = param ? Number(param) : 30;
112
+ if (!Number.isFinite(value) || value < 1)
113
+ return 30;
114
+ return Math.min(365, Math.max(1, Math.floor(value)));
115
+ };
103
116
  if (pathname === `${API_PREFIX}/overview` && method === 'GET') {
104
- return done(() => service.getOverview());
117
+ const days = parseDays(url.searchParams.get('days'));
118
+ return done(() => service.getOverview(days));
119
+ }
120
+ if (pathname === `${API_PREFIX}/central/overview` && method === 'GET') {
121
+ const days = parseDays(url.searchParams.get('days'));
122
+ return done(() => {
123
+ const centralDb = getCentralDatabase();
124
+ const stats = centralDb.getOverviewStats();
125
+ const trend = centralDb.getDailyTrend(days);
126
+ const regressions = centralDb.getTopRegressions(5);
127
+ const thinkingStats = centralDb.getThinkingModelStats();
128
+ const workspaces = centralDb.getWorkspaces();
129
+ return {
130
+ workspaceDir: 'central',
131
+ generatedAt: new Date().toISOString(),
132
+ dataFreshness: workspaces.length > 0 ? (workspaces[0].lastSync ?? null) : null,
133
+ dataSource: 'central_aggregated_db',
134
+ runtimeControlPlaneSource: 'all_workspaces',
135
+ summary: {
136
+ repeatErrorRate: stats.totalToolCalls > 0
137
+ ? stats.totalFailures / stats.totalToolCalls
138
+ : 0,
139
+ userCorrectionRate: stats.totalToolCalls > 0
140
+ ? stats.totalCorrections / stats.totalToolCalls
141
+ : 0,
142
+ pendingSamples: stats.pendingSamples,
143
+ approvedSamples: stats.approvedSamples,
144
+ thinkingCoverageRate: stats.totalToolCalls > 0
145
+ ? stats.totalThinkingEvents / stats.totalToolCalls
146
+ : 0,
147
+ painEvents: stats.totalPainEvents,
148
+ principleEventCount: 0,
149
+ gateBlocks: 0,
150
+ taskOutcomes: 0,
151
+ },
152
+ dailyTrend: trend,
153
+ topRegressions: regressions,
154
+ sampleQueue: {
155
+ counters: {
156
+ pending: stats.pendingSamples,
157
+ approved: stats.approvedSamples,
158
+ rejected: stats.rejectedSamples,
159
+ },
160
+ preview: [],
161
+ },
162
+ thinkingSummary: {
163
+ activeModels: thinkingStats.activeModels,
164
+ dormantModels: thinkingStats.totalModels - thinkingStats.activeModels,
165
+ effectiveModels: thinkingStats.models.filter(m => m.coverageRate > 0.1).length,
166
+ coverageRate: stats.totalToolCalls > 0
167
+ ? stats.totalThinkingEvents / stats.totalToolCalls
168
+ : 0,
169
+ },
170
+ centralInfo: {
171
+ workspaceCount: stats.workspaceCount,
172
+ enabledWorkspaceCount: stats.enabledWorkspaceCount,
173
+ workspaces: stats.workspaceNames,
174
+ enabledWorkspaces: stats.enabledWorkspaceNames,
175
+ },
176
+ };
177
+ });
178
+ }
179
+ if (pathname === `${API_PREFIX}/central/sync` && method === 'POST') {
180
+ return done(() => {
181
+ const centralDb = getCentralDatabase();
182
+ const results = centralDb.syncEnabled();
183
+ const summary = {};
184
+ results.forEach((count, name) => {
185
+ summary[name] = count;
186
+ });
187
+ return { synced: summary, timestamp: new Date().toISOString() };
188
+ });
189
+ }
190
+ if (pathname === `${API_PREFIX}/central/workspaces` && method === 'GET') {
191
+ return done(() => {
192
+ const centralDb = getCentralDatabase();
193
+ const configs = centralDb.getWorkspaceConfigs();
194
+ const workspaces = centralDb.getWorkspaces();
195
+ return {
196
+ configs,
197
+ workspaces: workspaces.map(ws => ({
198
+ name: ws.name,
199
+ path: ws.path,
200
+ lastSync: ws.lastSync,
201
+ config: configs.find(c => c.workspaceName === ws.name) ?? null,
202
+ })),
203
+ };
204
+ });
205
+ }
206
+ const workspaceConfigMatch = pathname.match(/^\/plugins\/principles\/api\/central\/workspaces\/([^/]+)$/);
207
+ if (workspaceConfigMatch && method === 'GET') {
208
+ return done(() => {
209
+ const centralDb = getCentralDatabase();
210
+ const workspaceName = decodeURIComponent(workspaceConfigMatch[1]);
211
+ const configs = centralDb.getWorkspaceConfigs();
212
+ const config = configs.find(c => c.workspaceName === workspaceName);
213
+ return config ?? { workspaceName, enabled: true, displayName: workspaceName, syncEnabled: true };
214
+ });
215
+ }
216
+ if (workspaceConfigMatch && method === 'PATCH') {
217
+ return (async () => {
218
+ try {
219
+ const body = await readJsonBody(req);
220
+ const centralDb = getCentralDatabase();
221
+ const workspaceName = decodeURIComponent(workspaceConfigMatch[1]);
222
+ centralDb.updateWorkspaceConfig(workspaceName, {
223
+ enabled: body.enabled,
224
+ displayName: body.displayName,
225
+ syncEnabled: body.syncEnabled,
226
+ });
227
+ const configs = centralDb.getWorkspaceConfigs();
228
+ json(res, 200, configs.find(c => c.workspaceName === workspaceName));
229
+ return true;
230
+ }
231
+ catch (error) {
232
+ if (error instanceof Error && error.message === 'invalid_json') {
233
+ json(res, 400, { error: 'bad_request', message: 'Request body must be valid JSON.' });
234
+ return true;
235
+ }
236
+ api.logger.warn(`[PD:ControlUI] Workspace config update failed: ${String(error)}`);
237
+ json(res, 500, { error: 'internal_error', message: String(error) });
238
+ return true;
239
+ }
240
+ })();
241
+ }
242
+ if (pathname === `${API_PREFIX}/central/workspaces` && method === 'POST') {
243
+ return (async () => {
244
+ try {
245
+ const body = await readJsonBody(req);
246
+ const name = typeof body.name === 'string' ? body.name : '';
247
+ const workspacePath = typeof body.path === 'string' ? body.path : '';
248
+ if (!name || !workspacePath) {
249
+ json(res, 400, { error: 'bad_request', message: 'name and path are required.' });
250
+ return true;
251
+ }
252
+ const centralDb = getCentralDatabase();
253
+ centralDb.addCustomWorkspace(name, workspacePath);
254
+ json(res, 201, { success: true, workspace: name });
255
+ return true;
256
+ }
257
+ catch (error) {
258
+ if (error instanceof Error && error.message === 'invalid_json') {
259
+ json(res, 400, { error: 'bad_request', message: 'Request body must be valid JSON.' });
260
+ return true;
261
+ }
262
+ api.logger.warn(`[PD:ControlUI] Add workspace failed: ${String(error)}`);
263
+ json(res, 500, { error: 'internal_error', message: String(error) });
264
+ return true;
265
+ }
266
+ })();
105
267
  }
106
268
  if (pathname === `${API_PREFIX}/samples` && method === 'GET') {
107
269
  return done(() => service.listSamples({
@@ -217,9 +379,10 @@ function handleApiRoute(api, pathname, req, res) {
217
379
  });
218
380
  }
219
381
  if (pathname === `${API_PREFIX}/evolution/stats` && method === 'GET') {
382
+ const days = parseDays(url.searchParams.get('days'));
220
383
  return done(() => {
221
384
  const evoService = evolutionService();
222
- return evoService.getStats();
385
+ return evoService.getStats(days);
223
386
  });
224
387
  }
225
388
  const evolutionTraceMatch = pathname.match(/^\/plugins\/principles\/api\/evolution\/trace\/([^/]+)$/);
@@ -275,10 +438,93 @@ function handleApiRoute(api, pathname, req, res) {
275
438
  json(res, 404, { error: 'not_found', message: 'Unknown Principles Console API route.' });
276
439
  return true;
277
440
  }
441
+ function getGatewayToken() {
442
+ try {
443
+ const configPath = path.join(process.env.HOME || '', '.openclaw', 'openclaw.json');
444
+ if (!fs.existsSync(configPath))
445
+ return null;
446
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
447
+ return config?.gateway?.auth?.token || null;
448
+ }
449
+ catch {
450
+ return null;
451
+ }
452
+ }
453
+ function validateGatewayAuth(req) {
454
+ const gatewayToken = getGatewayToken();
455
+ if (!gatewayToken) {
456
+ // No token configured, allow all requests
457
+ return true;
458
+ }
459
+ const authHeader = req.headers?.['authorization'] || '';
460
+ const tokenMatch = authHeader.match(/^Bearer\s+(.+)$/i);
461
+ const providedToken = tokenMatch?.[1];
462
+ return providedToken === gatewayToken;
463
+ }
464
+ /**
465
+ * Create routes for Principles Console.
466
+ * Returns an array of routes:
467
+ * 1. Static files route (no auth required for HTML/CSS/JS)
468
+ * 2. API route (gateway auth required)
469
+ */
470
+ export function createPrinciplesConsoleRoutes(api) {
471
+ // Route 1: Static files (HTML, CSS, JS) - no auth check
472
+ const staticRoute = {
473
+ path: ROUTE_PREFIX,
474
+ auth: 'plugin',
475
+ match: 'prefix',
476
+ async handler(req, res) {
477
+ const url = new URL(req.url || ROUTE_PREFIX, 'http://127.0.0.1');
478
+ const pathname = url.pathname;
479
+ const method = (req.method || 'GET').toUpperCase();
480
+ // Skip API routes - they'll be handled by the API route
481
+ if (pathname.startsWith(API_PREFIX)) {
482
+ return false; // Let the API route handle this
483
+ }
484
+ // Serve assets
485
+ if (pathname.startsWith(ASSETS_PREFIX)) {
486
+ if (method !== 'GET' && method !== 'HEAD') {
487
+ text(res, 405, 'Method Not Allowed');
488
+ return true;
489
+ }
490
+ const assetPath = safeStaticPath(api.rootDir, pathname);
491
+ if (!assetPath || !serveFile(res, assetPath)) {
492
+ text(res, 404, 'Asset Not Found');
493
+ }
494
+ return true;
495
+ }
496
+ // Serve index.html for the main route
497
+ if (method !== 'GET' && method !== 'HEAD') {
498
+ text(res, 405, 'Method Not Allowed');
499
+ return true;
500
+ }
501
+ const indexPath = path.join(api.rootDir, 'dist', 'web', 'index.html');
502
+ if (!serveFile(res, indexPath)) {
503
+ text(res, 503, 'Principles Console UI is not built yet.');
504
+ }
505
+ return true;
506
+ },
507
+ };
508
+ // Route 2: API endpoints - gateway auth required
509
+ const apiRoute = {
510
+ path: API_PREFIX,
511
+ auth: 'gateway',
512
+ match: 'prefix',
513
+ async handler(req, res) {
514
+ const url = new URL(req.url || API_PREFIX, 'http://127.0.0.1');
515
+ const pathname = url.pathname;
516
+ return handleApiRoute(api, pathname, req, res);
517
+ },
518
+ };
519
+ return [staticRoute, apiRoute];
520
+ }
521
+ // Legacy export for backwards compatibility
278
522
  export function createPrinciplesConsoleRoute(api) {
523
+ const routes = createPrinciplesConsoleRoutes(api);
524
+ // Return the combined behavior - this will be called from index.ts
279
525
  return {
280
526
  path: ROUTE_PREFIX,
281
- auth: 'gateway',
527
+ auth: 'plugin',
282
528
  match: 'prefix',
283
529
  async handler(req, res) {
284
530
  const url = new URL(req.url || ROUTE_PREFIX, 'http://127.0.0.1');
@@ -287,9 +533,15 @@ export function createPrinciplesConsoleRoute(api) {
287
533
  if (!pathname.startsWith(ROUTE_PREFIX)) {
288
534
  return false;
289
535
  }
536
+ // For API routes, check auth manually
290
537
  if (pathname.startsWith(API_PREFIX)) {
538
+ if (!validateGatewayAuth(req)) {
539
+ json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
540
+ return true;
541
+ }
291
542
  return handleApiRoute(api, pathname, req, res);
292
543
  }
544
+ // Static files - no auth required
293
545
  if (pathname.startsWith(ASSETS_PREFIX)) {
294
546
  if (method !== 'GET' && method !== 'HEAD') {
295
547
  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;