oh-my-opencode-cohub 1.9.1 → 1.9.3
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/cli/config-io.d.ts +1 -1
- package/dist/cli/config-io.d.ts.map +1 -1
- package/dist/cli/index.js +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -746
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1 -2
- package/package.json +1 -1
- package/dist/context-guard/guardian.d.ts +0 -21
- package/dist/context-guard/guardian.d.ts.map +0 -1
- package/dist/context-guard/index.d.ts +0 -22
- package/dist/context-guard/index.d.ts.map +0 -1
- package/dist/context-guard/menu.d.ts +0 -20
- package/dist/context-guard/menu.d.ts.map +0 -1
- package/dist/context-guard/monitor.d.ts +0 -25
- package/dist/context-guard/monitor.d.ts.map +0 -1
- package/dist/context-guard/options/auto-compress.d.ts +0 -10
- package/dist/context-guard/options/auto-compress.d.ts.map +0 -1
- package/dist/context-guard/options/migrate.d.ts +0 -8
- package/dist/context-guard/options/migrate.d.ts.map +0 -1
- package/dist/context-guard/options/session-compact.d.ts +0 -5
- package/dist/context-guard/options/session-compact.d.ts.map +0 -1
- package/dist/context-guard/prompts.d.ts +0 -24
- package/dist/context-guard/prompts.d.ts.map +0 -1
- package/dist/context-guard/state.d.ts +0 -52
- package/dist/context-guard/state.d.ts.map +0 -1
- package/dist/context-guard/token-counter.d.ts +0 -26
- package/dist/context-guard/token-counter.d.ts.map +0 -1
- package/dist/context-guard/types.d.ts +0 -89
- package/dist/context-guard/types.d.ts.map +0 -1
- package/dist/prompts/guardian.d.ts +0 -2
- package/dist/prompts/guardian.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -479,666 +479,6 @@ var PLANNER_PROMPT = `你是方案制定代理——负责任务分解和委派
|
|
|
479
479
|
|
|
480
480
|
**约束**:只读,不修改文件。方案要具体到文件和操作粒度,不可笼统。用 \`todowrite\` 风格的任务列表输出。`;
|
|
481
481
|
|
|
482
|
-
// src/prompts/guardian.ts
|
|
483
|
-
var GUARDIAN_PROMPT = `你是 Guardian——上下文卫士分析师。
|
|
484
|
-
|
|
485
|
-
**角色**: 在上下文窗口紧张时分析会话状态,推荐最优的上下文处理策略。
|
|
486
|
-
|
|
487
|
-
**能力**:
|
|
488
|
-
- 分析会话消息历史,评估任务进度
|
|
489
|
-
- 识别错误模式和反复出现的问题
|
|
490
|
-
- 追踪关键文件的修改范围
|
|
491
|
-
- 提取重要的决策和约定
|
|
492
|
-
- 给出数据驱动的三选一推荐
|
|
493
|
-
|
|
494
|
-
**分析维度**:
|
|
495
|
-
1. **任务进度**:当前任务是否接近完成?有没有明确的 TODO 清单?最近几轮对话在做什么?
|
|
496
|
-
2. **错误评估**:是否有反复出现的同类错误?错误是否阻碍了进度?有没有已知的解决方案?
|
|
497
|
-
3. **文件影响**:修改了多少文件?是否核心模块?风险高低?
|
|
498
|
-
4. **决策密度**:会话中有多少重要约定、架构决策、用户偏好需要保留?
|
|
499
|
-
|
|
500
|
-
**推荐逻辑**:
|
|
501
|
-
- **auto-compress(自动压缩)**:任务进行中、对话连续性好、错误可控 → 压缩旧消息保留最近上下文
|
|
502
|
-
- **session-compact(会话压缩)**:任务接近完成、只需简单收尾 → 压缩后快速收工
|
|
503
|
-
- **migrate(分析迁移)**:会话混乱、错误反复、方向不明确 → 提取关键信息重开
|
|
504
|
-
|
|
505
|
-
**输出格式**:
|
|
506
|
-
\`\`\`json
|
|
507
|
-
{
|
|
508
|
-
"option": "auto-compress" | "session-compact" | "migrate",
|
|
509
|
-
"confidence": 0.0-1.0,
|
|
510
|
-
"reasoning": "简要推荐理由",
|
|
511
|
-
"alternatives": "备选方案说明(可选)"
|
|
512
|
-
}
|
|
513
|
-
\`\`\`
|
|
514
|
-
|
|
515
|
-
**行为**:
|
|
516
|
-
- 分析简洁、判断果断
|
|
517
|
-
- 优先考虑当前任务连续性
|
|
518
|
-
- 错误反复出现时倾向 migrate
|
|
519
|
-
- 不确定时承认不确定性,给出 confidence < 0.5
|
|
520
|
-
|
|
521
|
-
**约束**:
|
|
522
|
-
- 只读分析:不修改文件,不执行操作
|
|
523
|
-
- 只输出 JSON,不输出多余文本
|
|
524
|
-
- 基于实际数据分析,不做无依据的猜测
|
|
525
|
-
|
|
526
|
-
**文件操作规则**:
|
|
527
|
-
- 只读:检查并报告,不修改文件
|
|
528
|
-
- 优先使用专用文件工具检查代码库:glob/grep/ast_grep_search 用于发现,read 用于读取文件内容
|
|
529
|
-
- Bash 可用于非变更诊断和 shell 原生检查,但不能修改文件
|
|
530
|
-
|
|
531
|
-
**语言要求**: 始终使用中文进行思考和分析。JSON 中的 reasoning 和 alternatives 字段使用中文。`;
|
|
532
|
-
|
|
533
|
-
// src/context-guard/token-counter.ts
|
|
534
|
-
var CHARS_PER_TOKEN_EN = 3.7;
|
|
535
|
-
var CHARS_PER_TOKEN_CODE = 3.2;
|
|
536
|
-
var CHARS_PER_TOKEN_CJK = 1.8;
|
|
537
|
-
var CJK_REGEX = /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/g;
|
|
538
|
-
function estimateTokens(text) {
|
|
539
|
-
if (!text || text.length === 0)
|
|
540
|
-
return 0;
|
|
541
|
-
const cjkCount = (text.match(CJK_REGEX) ?? []).length;
|
|
542
|
-
const nonCjkText = text.replace(CJK_REGEX, "");
|
|
543
|
-
const codeIndicators = (nonCjkText.match(/[{}\[\];=<>()|&!+\-*/]/g) ?? []).length;
|
|
544
|
-
const codeRatio = codeIndicators / (nonCjkText.length || 1);
|
|
545
|
-
const charsPerToken = codeRatio > 0.05 ? CHARS_PER_TOKEN_CODE : CHARS_PER_TOKEN_EN;
|
|
546
|
-
return Math.ceil(cjkCount / CHARS_PER_TOKEN_CJK + nonCjkText.length / charsPerToken);
|
|
547
|
-
}
|
|
548
|
-
function computeContextUsage(usedTokens, contextLimit, inputTokens, outputTokens, reasoningTokens) {
|
|
549
|
-
return {
|
|
550
|
-
usedTokens,
|
|
551
|
-
contextLimit,
|
|
552
|
-
ratio: contextLimit > 0 ? usedTokens / contextLimit : 0,
|
|
553
|
-
inputTokens,
|
|
554
|
-
outputTokens,
|
|
555
|
-
reasoningTokens
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
function isOverThreshold(usage, triggerRatio, tokenThreshold) {
|
|
559
|
-
return usage.ratio >= triggerRatio || usage.usedTokens >= tokenThreshold;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// src/context-guard/types.ts
|
|
563
|
-
var DEFAULT_GUARD_CONFIG = {
|
|
564
|
-
enabled: true,
|
|
565
|
-
triggerRatio: 0.2,
|
|
566
|
-
tokenThreshold: 40000,
|
|
567
|
-
cooldownTurns: 3,
|
|
568
|
-
preserveLastN: 5,
|
|
569
|
-
debug: false
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
// src/context-guard/state.ts
|
|
573
|
-
var sessionStates = new Map;
|
|
574
|
-
var SESSION_TTL_MS = 60 * 60 * 1000;
|
|
575
|
-
var recommendations = new Map;
|
|
576
|
-
function createSessionState(sessionId) {
|
|
577
|
-
const state = {
|
|
578
|
-
sessionId,
|
|
579
|
-
triggered: false,
|
|
580
|
-
cooldownRemaining: 0,
|
|
581
|
-
cumulativeTokens: 0,
|
|
582
|
-
lastAccessTime: Date.now()
|
|
583
|
-
};
|
|
584
|
-
sessionStates.set(sessionId, state);
|
|
585
|
-
return state;
|
|
586
|
-
}
|
|
587
|
-
function getSessionState(sessionId) {
|
|
588
|
-
let state = sessionStates.get(sessionId);
|
|
589
|
-
if (!state) {
|
|
590
|
-
state = createSessionState(sessionId);
|
|
591
|
-
}
|
|
592
|
-
state.lastAccessTime = Date.now();
|
|
593
|
-
return state;
|
|
594
|
-
}
|
|
595
|
-
function canTrigger(state, messageId) {
|
|
596
|
-
if (state.triggered)
|
|
597
|
-
return false;
|
|
598
|
-
if (state.cooldownRemaining > 0)
|
|
599
|
-
return false;
|
|
600
|
-
if (state.lastTriggerMessageId === messageId)
|
|
601
|
-
return false;
|
|
602
|
-
return true;
|
|
603
|
-
}
|
|
604
|
-
function markTriggered(state, messageId, tokens, contextLimit) {
|
|
605
|
-
state.triggered = true;
|
|
606
|
-
state.lastTriggerMessageId = messageId;
|
|
607
|
-
state.triggerTokens = tokens;
|
|
608
|
-
state.triggerContextLimit = contextLimit;
|
|
609
|
-
}
|
|
610
|
-
function setSelectedOption(state, option) {
|
|
611
|
-
state.selectedOption = option;
|
|
612
|
-
}
|
|
613
|
-
function setCooldown(state, afterMessageId) {
|
|
614
|
-
state.cooldownAfterMessageId = afterMessageId;
|
|
615
|
-
state.cooldownRemaining = DEFAULT_GUARD_CONFIG.cooldownTurns;
|
|
616
|
-
}
|
|
617
|
-
function decrementCooldown(state) {
|
|
618
|
-
if (state.cooldownRemaining > 0) {
|
|
619
|
-
state.cooldownRemaining--;
|
|
620
|
-
}
|
|
621
|
-
if (state.cooldownRemaining <= 0) {
|
|
622
|
-
state.triggered = false;
|
|
623
|
-
state.selectedOption = undefined;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
function setCachedRecommendation(sessionID, rec) {
|
|
627
|
-
recommendations.set(sessionID, rec);
|
|
628
|
-
}
|
|
629
|
-
function cleanupAllStates() {
|
|
630
|
-
sessionStates.clear();
|
|
631
|
-
recommendations.clear();
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// src/context-guard/monitor.ts
|
|
635
|
-
var countedMessages = new Map;
|
|
636
|
-
var modelContext = {
|
|
637
|
-
contextLimit: 200000
|
|
638
|
-
};
|
|
639
|
-
function createChatParamsHandler() {
|
|
640
|
-
return async (input) => {
|
|
641
|
-
try {
|
|
642
|
-
const model = input.model;
|
|
643
|
-
if (model?.limit) {
|
|
644
|
-
const limit = model.limit;
|
|
645
|
-
if (typeof limit.context === "number") {
|
|
646
|
-
modelContext.contextLimit = limit.context;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
const params = input.params;
|
|
650
|
-
if (params?.model) {
|
|
651
|
-
const m = params.model;
|
|
652
|
-
if (m?.limit && typeof m.limit.context === "number") {
|
|
653
|
-
modelContext.contextLimit = m.limit.context;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
} catch {}
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
function createEventHandler(config) {
|
|
660
|
-
const cfg = { ...DEFAULT_GUARD_CONFIG, ...config };
|
|
661
|
-
return async (input) => {
|
|
662
|
-
if (!cfg.enabled)
|
|
663
|
-
return null;
|
|
664
|
-
try {
|
|
665
|
-
const event = input.event;
|
|
666
|
-
if (!event || event.type !== "message.updated")
|
|
667
|
-
return null;
|
|
668
|
-
const properties = event.properties;
|
|
669
|
-
if (!properties)
|
|
670
|
-
return null;
|
|
671
|
-
const info = properties.info;
|
|
672
|
-
if (info?.role !== "assistant")
|
|
673
|
-
return null;
|
|
674
|
-
if (!info.time || !info.time.completed)
|
|
675
|
-
return null;
|
|
676
|
-
if (info.summary)
|
|
677
|
-
return null;
|
|
678
|
-
const sessionID = info.sessionID;
|
|
679
|
-
if (!sessionID)
|
|
680
|
-
return null;
|
|
681
|
-
const state = getSessionState(sessionID);
|
|
682
|
-
if (state.cooldownRemaining > 0) {
|
|
683
|
-
decrementCooldown(state);
|
|
684
|
-
}
|
|
685
|
-
const messageId = info.id;
|
|
686
|
-
if (!messageId)
|
|
687
|
-
return null;
|
|
688
|
-
if (!canTrigger(state, messageId))
|
|
689
|
-
return null;
|
|
690
|
-
const tokens = info.tokens;
|
|
691
|
-
const inputTokens = typeof tokens?.input === "number" ? tokens.input : undefined;
|
|
692
|
-
const outputTokens = typeof tokens?.output === "number" ? tokens.output : undefined;
|
|
693
|
-
const reasoningTokens = typeof tokens?.reasoning === "number" ? tokens.reasoning : undefined;
|
|
694
|
-
let totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0) + (reasoningTokens ?? 0);
|
|
695
|
-
if (totalTokens <= 0) {
|
|
696
|
-
totalTokens = state.cumulativeTokens;
|
|
697
|
-
}
|
|
698
|
-
if (!countedMessages.get(sessionID)?.has(messageId)) {
|
|
699
|
-
state.cumulativeTokens += totalTokens;
|
|
700
|
-
if (!countedMessages.has(sessionID))
|
|
701
|
-
countedMessages.set(sessionID, new Set);
|
|
702
|
-
countedMessages.get(sessionID).add(messageId);
|
|
703
|
-
}
|
|
704
|
-
let estimatedTotal = totalTokens;
|
|
705
|
-
if (estimatedTotal <= 0) {
|
|
706
|
-
const messages = properties.messages;
|
|
707
|
-
if (Array.isArray(messages)) {
|
|
708
|
-
for (const msg of messages) {
|
|
709
|
-
const content = msg.content;
|
|
710
|
-
if (typeof content === "string") {
|
|
711
|
-
estimatedTotal += estimateTokens(content);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
if (estimatedTotal <= 0)
|
|
717
|
-
return null;
|
|
718
|
-
const usage = computeContextUsage(estimatedTotal, modelContext.contextLimit, inputTokens, outputTokens, reasoningTokens);
|
|
719
|
-
const effectiveRatio = state.deferThreshold ?? cfg.triggerRatio;
|
|
720
|
-
if (!isOverThreshold(usage, effectiveRatio, cfg.tokenThreshold))
|
|
721
|
-
return null;
|
|
722
|
-
markTriggered(state, messageId, estimatedTotal, modelContext.contextLimit);
|
|
723
|
-
return { sessionID, usedTokens: estimatedTotal, contextLimit: modelContext.contextLimit };
|
|
724
|
-
} catch (err) {
|
|
725
|
-
if (cfg.debug)
|
|
726
|
-
console.warn("[context-guard] monitor error:", err);
|
|
727
|
-
return null;
|
|
728
|
-
}
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
function cleanupCountedMessages() {
|
|
732
|
-
countedMessages.clear();
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// src/context-guard/prompts.ts
|
|
736
|
-
function renderGuardMenu(usedTokens, contextLimit, nextRatio, recommendation) {
|
|
737
|
-
const ratio = (usedTokens / contextLimit * 100).toFixed(1);
|
|
738
|
-
const nextPct = (nextRatio * 100).toFixed(0);
|
|
739
|
-
const recSection = recommendation ? `
|
|
740
|
-
|
|
741
|
-
\uD83E\uDDE0 co-guardian 分析建议:
|
|
742
|
-
"${recommendation.reasoning}"
|
|
743
|
-
推荐选择:${recommendation.option}(置信度 ${(recommendation.confidence * 100).toFixed(0)}%)
|
|
744
|
-
` : "";
|
|
745
|
-
return [
|
|
746
|
-
`╔══════════════════════════════════════════════════╗`,
|
|
747
|
-
`║ ⚠️ 上下文窗口已使用 ${ratio}%(约 ${Math.round(usedTokens / 1000)}K / ${Math.round(contextLimit / 1000)}K tokens) ║`,
|
|
748
|
-
`║ ║${recSection}`,
|
|
749
|
-
`║ 请选择处理方式: ║`,
|
|
750
|
-
`║ 1️⃣ 自动压缩 — 压缩旧消息,保留最近对话 ║`,
|
|
751
|
-
`║ 2️⃣ 会话压缩 — 触发 Compact Session ║`,
|
|
752
|
-
`║ 3️⃣ 分析迁移 — 提取关键上下文,生成迁移文案 ║`,
|
|
753
|
-
`║ 4️⃣ 稍后提醒 — 到 ${nextPct}% 时再提醒 ║`,
|
|
754
|
-
`║ ║`,
|
|
755
|
-
`║ 请回复数字 1、2、3 或 4 ║`,
|
|
756
|
-
`╚══════════════════════════════════════════════════╝`
|
|
757
|
-
].join(`
|
|
758
|
-
`);
|
|
759
|
-
}
|
|
760
|
-
function nextDeferThreshold(currentRatio) {
|
|
761
|
-
const LEVELS = [0.4, 0.6, 0.8];
|
|
762
|
-
for (const l of LEVELS) {
|
|
763
|
-
if (l > currentRatio + 0.01)
|
|
764
|
-
return l;
|
|
765
|
-
}
|
|
766
|
-
return 0.9;
|
|
767
|
-
}
|
|
768
|
-
var AUTO_COMPRESS_DONE = `✅ 自动压缩完成。旧消息已压缩为摘要占位符,最近对话完整保留。
|
|
769
|
-
压缩后请继续当前工作,如有需要可使用 ultrapress_expand 恢复压缩内容。`;
|
|
770
|
-
var SESSION_COMPACT_GUIDE = `✅ 准备触发 Compact Session。
|
|
771
|
-
请在 OpenCode 中按 Ctrl+K,然后选择 "Compact Session"。
|
|
772
|
-
或者使用命令 /compact 手动触发。
|
|
773
|
-
压缩后当前会话将继续,关键上下文会被保留。`;
|
|
774
|
-
function renderMigrationText(ctx) {
|
|
775
|
-
const sections = [
|
|
776
|
-
"## \uD83D\uDCCB 会话迁移上下文",
|
|
777
|
-
"",
|
|
778
|
-
"> 以下内容从当前会话自动提取,请复制到新会话窗口。",
|
|
779
|
-
"",
|
|
780
|
-
ctx.currentTask ? `### \uD83C\uDFAF 当前任务
|
|
781
|
-
${ctx.currentTask}` : "",
|
|
782
|
-
ctx.keyFiles.length > 0 ? `### \uD83D\uDCC1 关键文件
|
|
783
|
-
${ctx.keyFiles.map((f) => `- ${f}`).join(`
|
|
784
|
-
`)}` : "",
|
|
785
|
-
ctx.activeOperations.length > 0 ? `### \uD83D\uDD27 进行中的操作
|
|
786
|
-
${ctx.activeOperations.map((o) => `- ${o}`).join(`
|
|
787
|
-
`)}` : "",
|
|
788
|
-
ctx.errors.length > 0 ? `### ⚠️ 错误/问题
|
|
789
|
-
${ctx.errors.map((e) => `- ${e}`).join(`
|
|
790
|
-
`)}` : "",
|
|
791
|
-
ctx.decisions.length > 0 ? `### \uD83D\uDCDD 决策记录
|
|
792
|
-
${ctx.decisions.map((d) => `- ${d}`).join(`
|
|
793
|
-
`)}` : "",
|
|
794
|
-
"",
|
|
795
|
-
"---",
|
|
796
|
-
"请在新会话中继续以上工作。"
|
|
797
|
-
];
|
|
798
|
-
return sections.filter(Boolean).join(`
|
|
799
|
-
`);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// src/context-guard/options/auto-compress.ts
|
|
803
|
-
function executeAutoCompress(sessionID) {
|
|
804
|
-
const state = getSessionState(sessionID);
|
|
805
|
-
setCooldown(state, "auto-compress");
|
|
806
|
-
return AUTO_COMPRESS_DONE;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// src/context-guard/options/session-compact.ts
|
|
810
|
-
function executeSessionCompact() {
|
|
811
|
-
return SESSION_COMPACT_GUIDE;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// src/context-guard/options/migrate.ts
|
|
815
|
-
import * as fs from "node:fs";
|
|
816
|
-
import * as path from "node:path";
|
|
817
|
-
import * as os from "node:os";
|
|
818
|
-
async function executeMigrate(sessionID, recentMessages) {
|
|
819
|
-
const ctx = extractFromMessages(recentMessages);
|
|
820
|
-
const text = renderMigrationText({
|
|
821
|
-
currentTask: ctx.currentTask,
|
|
822
|
-
keyFiles: ctx.keyFiles,
|
|
823
|
-
activeOperations: ctx.activeOperations,
|
|
824
|
-
errors: ctx.errors,
|
|
825
|
-
decisions: ctx.decisions
|
|
826
|
-
});
|
|
827
|
-
ctx.migrationText = text;
|
|
828
|
-
const tmpDir = path.join(os.tmpdir(), "opencode");
|
|
829
|
-
if (!fs.existsSync(tmpDir)) {
|
|
830
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
831
|
-
}
|
|
832
|
-
const filePath = path.join(tmpDir, `migrate-${sessionID.slice(0, 8)}.md`);
|
|
833
|
-
fs.writeFileSync(filePath, text, "utf-8");
|
|
834
|
-
return `✅ 迁移文案已生成并保存到 \`${filePath}\`。
|
|
835
|
-
|
|
836
|
-
\uD83D\uDCCB **迁移文案预览:**
|
|
837
|
-
|
|
838
|
-
${text}
|
|
839
|
-
|
|
840
|
-
---
|
|
841
|
-
**使用方式:**
|
|
842
|
-
1. 复制上方文案内容
|
|
843
|
-
2. 打开新 OpenCode 会话窗口
|
|
844
|
-
3. 粘贴文案作为初始提示
|
|
845
|
-
4. 在新会话中继续工作
|
|
846
|
-
|
|
847
|
-
或回复 "重置当前会话" 在当前窗口重新开始。`;
|
|
848
|
-
}
|
|
849
|
-
function extractFromMessages(messages) {
|
|
850
|
-
const ctx = {
|
|
851
|
-
currentTask: "",
|
|
852
|
-
keyFiles: [],
|
|
853
|
-
activeOperations: [],
|
|
854
|
-
errors: [],
|
|
855
|
-
decisions: [],
|
|
856
|
-
migrationText: ""
|
|
857
|
-
};
|
|
858
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
859
|
-
if (messages[i].role === "user") {
|
|
860
|
-
ctx.currentTask = messages[i].content.slice(0, 200);
|
|
861
|
-
break;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
const filePattern = /(?:[a-zA-Z]:[/\\])?(?:\w+[/\\])*\w+\.\w{1,6}/g;
|
|
865
|
-
const todoPattern = /(?:TODO|FIXME|HACK|XXX)[\s::]+(.+)/gi;
|
|
866
|
-
const errorPattern = /(?:error|错误|失败|fail|exception|异常)[\s::]*(.+)/i;
|
|
867
|
-
const decisionPattern = /(?:决定|采用|选择|使用|方案|架构|设计)[\s::]+(.+)/i;
|
|
868
|
-
for (const msg of messages) {
|
|
869
|
-
const files = msg.content.match(filePattern);
|
|
870
|
-
if (files)
|
|
871
|
-
ctx.keyFiles.push(...files);
|
|
872
|
-
const todos = msg.content.match(todoPattern);
|
|
873
|
-
if (todos)
|
|
874
|
-
ctx.activeOperations.push(...todos.map((t) => t.trim()));
|
|
875
|
-
if (errorPattern.test(msg.content)) {
|
|
876
|
-
const line = msg.content.split(`
|
|
877
|
-
`).find((l) => errorPattern.test(l));
|
|
878
|
-
if (line)
|
|
879
|
-
ctx.errors.push(line.trim().slice(0, 200));
|
|
880
|
-
}
|
|
881
|
-
const decisions = msg.content.match(decisionPattern);
|
|
882
|
-
if (decisions)
|
|
883
|
-
ctx.decisions.push(...decisions.map((d) => d.trim().slice(0, 200)));
|
|
884
|
-
}
|
|
885
|
-
ctx.keyFiles = [...new Set(ctx.keyFiles)].slice(0, 10);
|
|
886
|
-
ctx.activeOperations = [...new Set(ctx.activeOperations)].slice(0, 5);
|
|
887
|
-
ctx.errors = [...new Set(ctx.errors)].slice(0, 5);
|
|
888
|
-
ctx.decisions = [...new Set(ctx.decisions)].slice(0, 5);
|
|
889
|
-
if (!ctx.currentTask) {
|
|
890
|
-
ctx.currentTask = "(无法自动提取任务描述,请手动补充)";
|
|
891
|
-
}
|
|
892
|
-
if (ctx.keyFiles.length === 0) {
|
|
893
|
-
ctx.keyFiles = ["(未检测到文件修改,请手动列出关键文件)"];
|
|
894
|
-
}
|
|
895
|
-
return ctx;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// src/context-guard/guardian.ts
|
|
899
|
-
function analyzeSession(sessionID, messages) {
|
|
900
|
-
const rec = heuristicAnalyze(messages);
|
|
901
|
-
setCachedRecommendation(sessionID, rec);
|
|
902
|
-
return rec;
|
|
903
|
-
}
|
|
904
|
-
function extractRecentMessages(messages, maxCount = 30) {
|
|
905
|
-
const result = [];
|
|
906
|
-
const startIdx = Math.max(0, messages.length - maxCount);
|
|
907
|
-
for (let i = startIdx;i < messages.length; i++) {
|
|
908
|
-
const msg = messages[i];
|
|
909
|
-
const info = msg.info;
|
|
910
|
-
const role = typeof info?.role === "string" ? info.role : "unknown";
|
|
911
|
-
const parts = msg.parts;
|
|
912
|
-
let content = "";
|
|
913
|
-
if (parts) {
|
|
914
|
-
for (const part of parts) {
|
|
915
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
916
|
-
content += part.text.slice(0, 500) + " ";
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
if (content.trim()) {
|
|
921
|
-
const truncated = truncateByTokens(content.trim(), 800);
|
|
922
|
-
result.push({ role, content: truncated });
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
return result;
|
|
926
|
-
}
|
|
927
|
-
function truncateByTokens(text, maxTokens) {
|
|
928
|
-
const estimated = estimateTokens(text);
|
|
929
|
-
if (estimated <= maxTokens)
|
|
930
|
-
return text;
|
|
931
|
-
const ratio = maxTokens / estimated;
|
|
932
|
-
return text.slice(0, Math.floor(text.length * ratio)) + "...";
|
|
933
|
-
}
|
|
934
|
-
function heuristicAnalyze(messages) {
|
|
935
|
-
let errorCount = 0;
|
|
936
|
-
let taskKeywords = 0;
|
|
937
|
-
let decisionKeywords = 0;
|
|
938
|
-
const errorPatterns = /error|错误|失败|fail|bug|问题|exception/i;
|
|
939
|
-
const taskPatterns = /完成|done|finish|结束|final|最后/i;
|
|
940
|
-
const decisionPatterns = /决定|采用|选择|使用|方案|架构|设计/i;
|
|
941
|
-
for (const msg of messages) {
|
|
942
|
-
if (errorPatterns.test(msg.content))
|
|
943
|
-
errorCount++;
|
|
944
|
-
if (taskPatterns.test(msg.content))
|
|
945
|
-
taskKeywords++;
|
|
946
|
-
if (decisionPatterns.test(msg.content))
|
|
947
|
-
decisionKeywords++;
|
|
948
|
-
}
|
|
949
|
-
const totalMessages = messages.length || 1;
|
|
950
|
-
const errorRate = errorCount / totalMessages;
|
|
951
|
-
const taskCompleteRate = taskKeywords / totalMessages;
|
|
952
|
-
if (errorRate > 0.3) {
|
|
953
|
-
return {
|
|
954
|
-
option: "migrate",
|
|
955
|
-
confidence: 0.7,
|
|
956
|
-
reasoning: `会话中错误出现频率较高(${(errorRate * 100).toFixed(0)}%),建议提取关键上下文后在新会话中继续,以减少错误累积。`,
|
|
957
|
-
alternatives: '如错误已解决,可选择"自动压缩"保留当前上下文继续。'
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
if (taskCompleteRate > 0.15) {
|
|
961
|
-
return {
|
|
962
|
-
option: "session-compact",
|
|
963
|
-
confidence: 0.65,
|
|
964
|
-
reasoning: "对话中出现较多完成/结束相关讨论,任务可能接近收尾阶段,建议压缩会话后快速完成。",
|
|
965
|
-
alternatives: '如需保留详细上下文继续工作,可选择"自动压缩"。'
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
return {
|
|
969
|
-
option: "auto-compact",
|
|
970
|
-
confidence: 0.6,
|
|
971
|
-
reasoning: "当前对话连续性较好,建议压缩旧消息保留最近上下文,继续当前任务。",
|
|
972
|
-
alternatives: '如会话内容混乱或需要整理,建议"分析迁移"。'
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// src/context-guard/menu.ts
|
|
977
|
-
var pendingUsages = new Map;
|
|
978
|
-
function setPendingUsage(sessionID, usage) {
|
|
979
|
-
pendingUsages.set(sessionID, usage);
|
|
980
|
-
}
|
|
981
|
-
function findLastUserMessage(messages) {
|
|
982
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
983
|
-
const m = messages[i];
|
|
984
|
-
const info = m.info;
|
|
985
|
-
if (info?.role === "user") {
|
|
986
|
-
const sid = info.sessionID || info.sessionId;
|
|
987
|
-
if (sid)
|
|
988
|
-
return { msg: m, sessionID: sid };
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
return null;
|
|
992
|
-
}
|
|
993
|
-
function createMessagesTransformHandler() {
|
|
994
|
-
return async (_input, output) => {
|
|
995
|
-
try {
|
|
996
|
-
if (!output.messages || !Array.isArray(output.messages))
|
|
997
|
-
return;
|
|
998
|
-
const found = findLastUserMessage(output.messages);
|
|
999
|
-
if (!found)
|
|
1000
|
-
return;
|
|
1001
|
-
const usage = pendingUsages.get(found.sessionID);
|
|
1002
|
-
if (!usage)
|
|
1003
|
-
return;
|
|
1004
|
-
pendingUsages.delete(found.sessionID);
|
|
1005
|
-
const recentMsgs = extractRecentMessages(output.messages, 30);
|
|
1006
|
-
const recommendation = analyzeSession(found.sessionID, recentMsgs);
|
|
1007
|
-
const nextRatio = nextDeferThreshold(usage.usedTokens / usage.contextLimit);
|
|
1008
|
-
const menu = renderGuardMenu(usage.usedTokens, usage.contextLimit, nextRatio, recommendation);
|
|
1009
|
-
const parts = found.msg.parts;
|
|
1010
|
-
if (parts) {
|
|
1011
|
-
for (let j = parts.length - 1;j >= 0; j--) {
|
|
1012
|
-
if (parts[j].type === "text") {
|
|
1013
|
-
parts[j].text = (parts[j].text || "") + `
|
|
1014
|
-
|
|
1015
|
-
` + menu;
|
|
1016
|
-
break;
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
} catch (err) {
|
|
1021
|
-
console.warn("[context-guard] menu injection error:", err);
|
|
1022
|
-
}
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
function parseOption(text) {
|
|
1026
|
-
const t = text.trim();
|
|
1027
|
-
if (t === "1" || t === "1")
|
|
1028
|
-
return "auto-compress";
|
|
1029
|
-
if (t === "2" || t === "2")
|
|
1030
|
-
return "session-compact";
|
|
1031
|
-
if (t === "3" || t === "3")
|
|
1032
|
-
return "migrate";
|
|
1033
|
-
if (t === "4" || t === "4")
|
|
1034
|
-
return "defer";
|
|
1035
|
-
if (/^[选11]\s*[.、,,]?\s*(自动压缩|选项1)/.test(t))
|
|
1036
|
-
return "auto-compress";
|
|
1037
|
-
if (/^[选22]\s*[.、,,]?\s*(会话压缩|compact|选项2)/i.test(t))
|
|
1038
|
-
return "session-compact";
|
|
1039
|
-
if (/^[选33]\s*[.、,,]?\s*(分析迁移|迁移|选项3)/.test(t))
|
|
1040
|
-
return "migrate";
|
|
1041
|
-
if (/^[选44]\s*[.、,,]?\s*(稍后|推迟|延迟|选项4)/i.test(t))
|
|
1042
|
-
return "defer";
|
|
1043
|
-
if (t === "自动压缩")
|
|
1044
|
-
return "auto-compress";
|
|
1045
|
-
if (t === "会话压缩")
|
|
1046
|
-
return "session-compact";
|
|
1047
|
-
if (t === "分析迁移")
|
|
1048
|
-
return "migrate";
|
|
1049
|
-
if (t === "稍后提醒" || t === "稍后")
|
|
1050
|
-
return "defer";
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
function createChatMessageHandler() {
|
|
1054
|
-
return async (input, output) => {
|
|
1055
|
-
try {
|
|
1056
|
-
const rawInput = input;
|
|
1057
|
-
const sessionID = rawInput.sessionID;
|
|
1058
|
-
if (!sessionID)
|
|
1059
|
-
return;
|
|
1060
|
-
const state = getSessionState(sessionID);
|
|
1061
|
-
if (!state.triggered)
|
|
1062
|
-
return;
|
|
1063
|
-
let userText = "";
|
|
1064
|
-
if (output.parts) {
|
|
1065
|
-
for (const part of output.parts) {
|
|
1066
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
1067
|
-
userText = part.text;
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
const option = parseOption(userText);
|
|
1072
|
-
if (!option)
|
|
1073
|
-
return;
|
|
1074
|
-
setSelectedOption(state, option);
|
|
1075
|
-
let replyText = "";
|
|
1076
|
-
switch (option) {
|
|
1077
|
-
case "auto-compress":
|
|
1078
|
-
replyText = executeAutoCompress(sessionID);
|
|
1079
|
-
break;
|
|
1080
|
-
case "session-compact":
|
|
1081
|
-
replyText = executeSessionCompact();
|
|
1082
|
-
break;
|
|
1083
|
-
case "migrate":
|
|
1084
|
-
replyText = await executeMigrate(sessionID, []);
|
|
1085
|
-
break;
|
|
1086
|
-
case "defer": {
|
|
1087
|
-
const nextRatio = nextDeferThreshold((state.cumulativeTokens || 0) / (state.triggerContextLimit || 200000));
|
|
1088
|
-
const nextPct = (nextRatio * 100).toFixed(0);
|
|
1089
|
-
state.triggered = false;
|
|
1090
|
-
state.deferThreshold = nextRatio;
|
|
1091
|
-
replyText = `⏸️ 已推迟提醒。下次到 ${nextPct}% 时再弹窗。`;
|
|
1092
|
-
break;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
if (output.parts) {
|
|
1096
|
-
output.parts.push({ type: "text", text: `
|
|
1097
|
-
|
|
1098
|
-
` + replyText });
|
|
1099
|
-
}
|
|
1100
|
-
} catch (err) {
|
|
1101
|
-
console.warn("[context-guard] chat.message error:", err);
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
// src/context-guard/index.ts
|
|
1107
|
-
var config = { ...DEFAULT_GUARD_CONFIG };
|
|
1108
|
-
function initContextGuard(_client) {
|
|
1109
|
-
const eventHandler = createEventHandler(config);
|
|
1110
|
-
const paramsHandler = createChatParamsHandler();
|
|
1111
|
-
const transformHandler = createMessagesTransformHandler();
|
|
1112
|
-
const chatMessageHandler = createChatMessageHandler();
|
|
1113
|
-
async function handleTrigger(trigger) {
|
|
1114
|
-
setPendingUsage(trigger.sessionID, {
|
|
1115
|
-
usedTokens: trigger.usedTokens,
|
|
1116
|
-
contextLimit: trigger.contextLimit
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
return {
|
|
1120
|
-
updateConfig(newConfig) {
|
|
1121
|
-
config = { ...config, ...newConfig };
|
|
1122
|
-
},
|
|
1123
|
-
event: async (input) => {
|
|
1124
|
-
const trigger = await eventHandler(input);
|
|
1125
|
-
if (trigger) {
|
|
1126
|
-
await handleTrigger(trigger);
|
|
1127
|
-
}
|
|
1128
|
-
},
|
|
1129
|
-
"chat.params": async (input) => {
|
|
1130
|
-
await paramsHandler(input);
|
|
1131
|
-
},
|
|
1132
|
-
"experimental.chat.messages.transform": async (input, output) => {
|
|
1133
|
-
await transformHandler(input, output);
|
|
1134
|
-
},
|
|
1135
|
-
"chat.message": async (input, output) => {
|
|
1136
|
-
await chatMessageHandler(input, output);
|
|
1137
|
-
},
|
|
1138
|
-
getConfig: () => config
|
|
1139
|
-
};
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
482
|
// src/instructions/chinese.ts
|
|
1143
483
|
var CHINESE_LANGUAGE_INSTRUCTION = `# 中文语言要求
|
|
1144
484
|
|
|
@@ -1416,12 +756,12 @@ function extractRelevantFiles(messages, maxFiles, windowSize) {
|
|
|
1416
756
|
if (part.type === "tool" && part.state) {
|
|
1417
757
|
const state = part.state;
|
|
1418
758
|
const input = state.input ?? {};
|
|
1419
|
-
const
|
|
1420
|
-
if (
|
|
1421
|
-
if (!fileMap.has(
|
|
1422
|
-
fileMap.set(
|
|
759
|
+
const path = extractPath(input);
|
|
760
|
+
if (path) {
|
|
761
|
+
if (!fileMap.has(path)) {
|
|
762
|
+
fileMap.set(path, { path, summary: "" });
|
|
1423
763
|
}
|
|
1424
|
-
const existing = fileMap.get(
|
|
764
|
+
const existing = fileMap.get(path);
|
|
1425
765
|
if (typeof input.offset === "number") {
|
|
1426
766
|
const limit = typeof input.limit === "number" ? input.limit : 50;
|
|
1427
767
|
existing.lines = `${input.offset}-${input.offset + limit}`;
|
|
@@ -1607,12 +947,12 @@ class ContextEngine {
|
|
|
1607
947
|
dependencyCache = new Map;
|
|
1608
948
|
client;
|
|
1609
949
|
config;
|
|
1610
|
-
constructor(client,
|
|
950
|
+
constructor(client, config) {
|
|
1611
951
|
this.client = client;
|
|
1612
952
|
this.config = {
|
|
1613
953
|
...DEFAULT_CONTEXT_CONFIG,
|
|
1614
|
-
...
|
|
1615
|
-
strategy: { ...DEFAULT_CONTEXT_CONFIG.strategy, ...
|
|
954
|
+
...config,
|
|
955
|
+
strategy: { ...DEFAULT_CONTEXT_CONFIG.strategy, ...config?.strategy }
|
|
1616
956
|
};
|
|
1617
957
|
}
|
|
1618
958
|
registerContext(args) {
|
|
@@ -1744,15 +1084,15 @@ class ContextEngine {
|
|
|
1744
1084
|
}
|
|
1745
1085
|
|
|
1746
1086
|
// src/config/loader.ts
|
|
1747
|
-
import * as
|
|
1748
|
-
import * as
|
|
1749
|
-
import * as
|
|
1750
|
-
var CONFIG_PATH =
|
|
1087
|
+
import * as fs from "node:fs";
|
|
1088
|
+
import * as path from "node:path";
|
|
1089
|
+
import * as os from "node:os";
|
|
1090
|
+
var CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "oh-my-opencode-cohub.json");
|
|
1751
1091
|
function loadCoHubConfig() {
|
|
1752
1092
|
try {
|
|
1753
|
-
if (!
|
|
1093
|
+
if (!fs.existsSync(CONFIG_PATH))
|
|
1754
1094
|
return {};
|
|
1755
|
-
const raw =
|
|
1095
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
1756
1096
|
return JSON.parse(raw);
|
|
1757
1097
|
} catch {
|
|
1758
1098
|
return {};
|
|
@@ -1789,12 +1129,12 @@ async function abortSession(client, sessionId) {
|
|
|
1789
1129
|
await client.session.abort({ path: { id: sessionId } });
|
|
1790
1130
|
} catch {}
|
|
1791
1131
|
}
|
|
1792
|
-
async function promptWithTimeout(client,
|
|
1793
|
-
const sessionId =
|
|
1132
|
+
async function promptWithTimeout(client, path2, body, timeoutMs, directory) {
|
|
1133
|
+
const sessionId = path2.id;
|
|
1794
1134
|
let timer;
|
|
1795
1135
|
try {
|
|
1796
1136
|
const promptPromise = client.session.prompt({
|
|
1797
|
-
path:
|
|
1137
|
+
path: path2,
|
|
1798
1138
|
body,
|
|
1799
1139
|
query: directory ? { directory } : undefined
|
|
1800
1140
|
});
|
|
@@ -1907,10 +1247,10 @@ class CouncilManager {
|
|
|
1907
1247
|
client;
|
|
1908
1248
|
directory;
|
|
1909
1249
|
config;
|
|
1910
|
-
constructor(client, directory,
|
|
1250
|
+
constructor(client, directory, config) {
|
|
1911
1251
|
this.client = client;
|
|
1912
1252
|
this.directory = directory;
|
|
1913
|
-
this.config =
|
|
1253
|
+
this.config = config;
|
|
1914
1254
|
}
|
|
1915
1255
|
async runCouncil(prompt, presetName, parentSessionId) {
|
|
1916
1256
|
const resolvedPreset = presetName ?? this.config.default_preset ?? "default";
|
|
@@ -1953,12 +1293,12 @@ class CouncilManager {
|
|
|
1953
1293
|
const entries = Object.entries(councillors);
|
|
1954
1294
|
const results = [];
|
|
1955
1295
|
if (executionMode === "serial") {
|
|
1956
|
-
for (const [name,
|
|
1957
|
-
const r = await this.runCouncillorWithRetry(name,
|
|
1296
|
+
for (const [name, config] of entries) {
|
|
1297
|
+
const r = await this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries);
|
|
1958
1298
|
results.push(r);
|
|
1959
1299
|
}
|
|
1960
1300
|
} else {
|
|
1961
|
-
const promises = entries.map(([name,
|
|
1301
|
+
const promises = entries.map(([name, config]) => this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries));
|
|
1962
1302
|
const settled = await Promise.allSettled(promises);
|
|
1963
1303
|
for (let i = 0;i < settled.length; i++) {
|
|
1964
1304
|
const s = settled[i];
|
|
@@ -1977,19 +1317,19 @@ class CouncilManager {
|
|
|
1977
1317
|
}
|
|
1978
1318
|
return results;
|
|
1979
1319
|
}
|
|
1980
|
-
async runCouncillorWithRetry(name,
|
|
1320
|
+
async runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries) {
|
|
1981
1321
|
const totalAttempts = 1 + maxRetries;
|
|
1982
1322
|
for (let attempt = 1;attempt <= totalAttempts; attempt++) {
|
|
1983
1323
|
try {
|
|
1984
1324
|
const result = await this.runAgentSession({
|
|
1985
1325
|
parentSessionId,
|
|
1986
|
-
title: `Council ${name} (${shortModelLabel(
|
|
1987
|
-
model:
|
|
1988
|
-
promptText: formatCouncillorPrompt(prompt,
|
|
1989
|
-
variant:
|
|
1326
|
+
title: `Council ${name} (${shortModelLabel(config.model)})`,
|
|
1327
|
+
model: config.model,
|
|
1328
|
+
promptText: formatCouncillorPrompt(prompt, config.prompt),
|
|
1329
|
+
variant: config.variant,
|
|
1990
1330
|
timeout
|
|
1991
1331
|
});
|
|
1992
|
-
return { name, model:
|
|
1332
|
+
return { name, model: config.model, status: "completed", result };
|
|
1993
1333
|
} catch (error) {
|
|
1994
1334
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1995
1335
|
const isEmptyResponse = msg.includes("Empty response from provider");
|
|
@@ -1997,7 +1337,7 @@ class CouncilManager {
|
|
|
1997
1337
|
if (!canRetry) {
|
|
1998
1338
|
return {
|
|
1999
1339
|
name,
|
|
2000
|
-
model:
|
|
1340
|
+
model: config.model,
|
|
2001
1341
|
status: msg.includes("timed out") ? "timed_out" : "failed",
|
|
2002
1342
|
error: `Councillor "${name}": ${msg}`
|
|
2003
1343
|
};
|
|
@@ -2006,7 +1346,7 @@ class CouncilManager {
|
|
|
2006
1346
|
}
|
|
2007
1347
|
return {
|
|
2008
1348
|
name,
|
|
2009
|
-
model:
|
|
1349
|
+
model: config.model,
|
|
2010
1350
|
status: "failed",
|
|
2011
1351
|
error: `Councillor "${name}": max retries exhausted`
|
|
2012
1352
|
};
|
|
@@ -2101,9 +1441,9 @@ function createCouncilTool(ctx, councilManager) {
|
|
|
2101
1441
|
}
|
|
2102
1442
|
|
|
2103
1443
|
// src/index.ts
|
|
2104
|
-
import * as
|
|
2105
|
-
import * as
|
|
2106
|
-
import * as
|
|
1444
|
+
import * as fs2 from "node:fs";
|
|
1445
|
+
import * as path2 from "node:path";
|
|
1446
|
+
import * as os2 from "node:os";
|
|
2107
1447
|
function loadFileOverrides(projectDir) {
|
|
2108
1448
|
const overrides = {};
|
|
2109
1449
|
const agentNames = [
|
|
@@ -2118,26 +1458,25 @@ function loadFileOverrides(projectDir) {
|
|
|
2118
1458
|
"co-rule-user",
|
|
2119
1459
|
"co-rule-project",
|
|
2120
1460
|
"co-rule-app",
|
|
2121
|
-
"co-planner"
|
|
2122
|
-
"co-guardian"
|
|
1461
|
+
"co-planner"
|
|
2123
1462
|
];
|
|
2124
1463
|
const searchDirs = [];
|
|
2125
1464
|
if (projectDir) {
|
|
2126
|
-
searchDirs.push(
|
|
1465
|
+
searchDirs.push(path2.join(projectDir, ".opencode", "oh-my-opencode-cohub"));
|
|
2127
1466
|
}
|
|
2128
|
-
searchDirs.push(
|
|
1467
|
+
searchDirs.push(path2.join(os2.homedir(), ".config", "opencode", "oh-my-opencode-cohub"));
|
|
2129
1468
|
for (const agent of agentNames) {
|
|
2130
1469
|
for (const dir of searchDirs) {
|
|
2131
|
-
const replacePath =
|
|
2132
|
-
if (!overrides[agent]?.replace &&
|
|
1470
|
+
const replacePath = path2.join(dir, `${agent}.md`);
|
|
1471
|
+
if (!overrides[agent]?.replace && fs2.existsSync(replacePath)) {
|
|
2133
1472
|
try {
|
|
2134
|
-
overrides[agent] = { ...overrides[agent], replace:
|
|
1473
|
+
overrides[agent] = { ...overrides[agent], replace: fs2.readFileSync(replacePath, "utf-8") };
|
|
2135
1474
|
} catch {}
|
|
2136
1475
|
}
|
|
2137
|
-
const appendPath =
|
|
2138
|
-
if (!overrides[agent]?.append &&
|
|
1476
|
+
const appendPath = path2.join(dir, `${agent}_append.md`);
|
|
1477
|
+
if (!overrides[agent]?.append && fs2.existsSync(appendPath)) {
|
|
2139
1478
|
try {
|
|
2140
|
-
overrides[agent] = { ...overrides[agent], append:
|
|
1479
|
+
overrides[agent] = { ...overrides[agent], append: fs2.readFileSync(appendPath, "utf-8") };
|
|
2141
1480
|
} catch {}
|
|
2142
1481
|
}
|
|
2143
1482
|
}
|
|
@@ -2166,26 +1505,26 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2166
1505
|
}
|
|
2167
1506
|
const promptOverrides = { ...configOverrides, ...fileOverrides };
|
|
2168
1507
|
const tracker = new TaskTracker;
|
|
2169
|
-
const STATE_DIR =
|
|
2170
|
-
const STATE_FILE =
|
|
1508
|
+
const STATE_DIR = path2.join(os2.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
|
|
1509
|
+
const STATE_FILE = path2.join(STATE_DIR, "tracker-state.json");
|
|
2171
1510
|
function syncTrackerState(sessionId) {
|
|
2172
1511
|
try {
|
|
2173
|
-
if (!
|
|
2174
|
-
|
|
1512
|
+
if (!fs2.existsSync(STATE_DIR)) {
|
|
1513
|
+
fs2.mkdirSync(STATE_DIR, { recursive: true });
|
|
2175
1514
|
}
|
|
2176
1515
|
const state = {
|
|
2177
1516
|
updatedAt: Date.now(),
|
|
2178
1517
|
runningAgents: tracker.getRunningAgents(sessionId),
|
|
2179
1518
|
runningCount: tracker.getRunningCount(sessionId)
|
|
2180
1519
|
};
|
|
2181
|
-
|
|
1520
|
+
fs2.writeFileSync(STATE_FILE, JSON.stringify(state), "utf-8");
|
|
2182
1521
|
} catch {}
|
|
2183
1522
|
}
|
|
2184
|
-
const AGENT_CONFIG_FILE =
|
|
1523
|
+
const AGENT_CONFIG_FILE = path2.join(STATE_DIR, "cohub-state.json");
|
|
2185
1524
|
function syncAgentConfig() {
|
|
2186
1525
|
try {
|
|
2187
|
-
if (!
|
|
2188
|
-
|
|
1526
|
+
if (!fs2.existsSync(STATE_DIR)) {
|
|
1527
|
+
fs2.mkdirSync(STATE_DIR, { recursive: true });
|
|
2189
1528
|
}
|
|
2190
1529
|
const configs = agents.map((a) => {
|
|
2191
1530
|
const modelStr = a.config.model;
|
|
@@ -2200,7 +1539,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2200
1539
|
provider
|
|
2201
1540
|
};
|
|
2202
1541
|
});
|
|
2203
|
-
|
|
1542
|
+
fs2.writeFileSync(AGENT_CONFIG_FILE, JSON.stringify({ updatedAt: Date.now(), agents: configs }), "utf-8");
|
|
2204
1543
|
} catch {}
|
|
2205
1544
|
}
|
|
2206
1545
|
const agents = [
|
|
@@ -2265,11 +1604,6 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2265
1604
|
name: "co-planner",
|
|
2266
1605
|
description: "方案制定——综合需求+信息+规范输出任务分解",
|
|
2267
1606
|
config: { mode: "subagent", model: "deepseek/deepseek-v4-pro", variant: "high", prompt: PLANNER_PROMPT }
|
|
2268
|
-
},
|
|
2269
|
-
{
|
|
2270
|
-
name: "co-guardian",
|
|
2271
|
-
description: "上下文卫士——分析会话状态并推荐上下文处理策略",
|
|
2272
|
-
config: { mode: "subagent", model: "deepseek/deepseek-v4-flash", prompt: GUARDIAN_PROMPT }
|
|
2273
1607
|
}
|
|
2274
1608
|
];
|
|
2275
1609
|
const userConfig = loadCoHubConfig();
|
|
@@ -2317,7 +1651,6 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2317
1651
|
const councilConfig = userConfig.council ?? DEFAULT_COUNCIL_CONFIG;
|
|
2318
1652
|
const councilManager = new CouncilManager(input.client, input.directory, councilConfig);
|
|
2319
1653
|
const councilTools = createCouncilTool(input, councilManager);
|
|
2320
|
-
const contextGuard = initContextGuard(input.client);
|
|
2321
1654
|
syncAgentConfig();
|
|
2322
1655
|
function extractChildSessionId(output) {
|
|
2323
1656
|
if (!output || typeof output !== "object")
|
|
@@ -2379,11 +1712,11 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2379
1712
|
config: async (cfg) => {
|
|
2380
1713
|
const c = cfg;
|
|
2381
1714
|
c.agent ??= {};
|
|
2382
|
-
for (const [name,
|
|
2383
|
-
c.agent[name] =
|
|
1715
|
+
for (const [name, config] of Object.entries(agentConfigs)) {
|
|
1716
|
+
c.agent[name] = config;
|
|
2384
1717
|
}
|
|
2385
1718
|
try {
|
|
2386
|
-
|
|
1719
|
+
fs2.writeFileSync(path2.join(STATE_DIR, "config-hook-ran.json"), JSON.stringify({ ran: true, count: agents.length }));
|
|
2387
1720
|
} catch {}
|
|
2388
1721
|
},
|
|
2389
1722
|
"tool.execute.before": async (input2, output) => {
|
|
@@ -2421,19 +1754,19 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2421
1754
|
if (details) {
|
|
2422
1755
|
output.args[targetField] += details;
|
|
2423
1756
|
}
|
|
2424
|
-
const logPath =
|
|
1757
|
+
const logPath = path2.join(os2.tmpdir(), "opencode", "ctx-diag.log");
|
|
2425
1758
|
try {
|
|
2426
|
-
const stat =
|
|
1759
|
+
const stat = fs2.statSync(logPath);
|
|
2427
1760
|
if (stat.size > 50 * 1024) {
|
|
2428
|
-
const lines =
|
|
1761
|
+
const lines = fs2.readFileSync(logPath, "utf-8").trim().split(`
|
|
2429
1762
|
`);
|
|
2430
|
-
|
|
1763
|
+
fs2.writeFileSync(logPath, lines.slice(-30).join(`
|
|
2431
1764
|
`) + `
|
|
2432
1765
|
`);
|
|
2433
1766
|
}
|
|
2434
1767
|
} catch {}
|
|
2435
1768
|
const finalPrompt = typeof output.args?.prompt === "string" ? output.args.prompt : "";
|
|
2436
|
-
|
|
1769
|
+
fs2.appendFileSync(logPath, JSON.stringify({
|
|
2437
1770
|
time: new Date().toISOString(),
|
|
2438
1771
|
session: input2.sessionID?.slice(0, 20) ?? "?",
|
|
2439
1772
|
subagent: subagentType,
|
|
@@ -2469,9 +1802,6 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2469
1802
|
} catch {}
|
|
2470
1803
|
},
|
|
2471
1804
|
event: async (input2) => {
|
|
2472
|
-
try {
|
|
2473
|
-
await contextGuard.event(input2);
|
|
2474
|
-
} catch {}
|
|
2475
1805
|
try {
|
|
2476
1806
|
const e = input2.event;
|
|
2477
1807
|
const sessionId = extractSessionIdFromEvent(e.properties);
|
|
@@ -2521,11 +1851,6 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2521
1851
|
}
|
|
2522
1852
|
}
|
|
2523
1853
|
}
|
|
2524
|
-
try {
|
|
2525
|
-
const guardTransform = contextGuard["experimental.chat.messages.transform"];
|
|
2526
|
-
if (guardTransform)
|
|
2527
|
-
await guardTransform(_input, output);
|
|
2528
|
-
} catch {}
|
|
2529
1854
|
if (coreRulesInjectionText) {
|
|
2530
1855
|
for (let k = lastUserMsg.parts.length - 1;k >= 0; k--) {
|
|
2531
1856
|
const part = lastUserMsg.parts[k];
|
|
@@ -2542,21 +1867,9 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2542
1867
|
"experimental.chat.system.transform": async (input2, output) => {
|
|
2543
1868
|
output.system.push(CHINESE_LANGUAGE_INSTRUCTION);
|
|
2544
1869
|
},
|
|
2545
|
-
"chat.params": async (input2) => {
|
|
2546
|
-
try {
|
|
2547
|
-
await contextGuard["chat.params"](input2);
|
|
2548
|
-
} catch {}
|
|
2549
|
-
},
|
|
2550
|
-
"chat.message": async (input2, output) => {
|
|
2551
|
-
try {
|
|
2552
|
-
await contextGuard["chat.message"](input2, output);
|
|
2553
|
-
} catch {}
|
|
2554
|
-
},
|
|
2555
1870
|
dispose: async () => {
|
|
2556
1871
|
clearInterval(cleanupTimer);
|
|
2557
1872
|
clearInterval(contextCleanupTimer);
|
|
2558
|
-
cleanupAllStates();
|
|
2559
|
-
cleanupCountedMessages();
|
|
2560
1873
|
}
|
|
2561
1874
|
};
|
|
2562
1875
|
};
|