oh-my-opencode-cohub 1.6.0 → 1.8.0
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/index.js +2 -2
- package/dist/context-guard/guardian.d.ts +21 -0
- package/dist/context-guard/guardian.d.ts.map +1 -0
- package/dist/context-guard/index.d.ts +21 -0
- package/dist/context-guard/index.d.ts.map +1 -0
- package/dist/context-guard/menu.d.ts +20 -0
- package/dist/context-guard/menu.d.ts.map +1 -0
- package/dist/context-guard/monitor.d.ts +23 -0
- package/dist/context-guard/monitor.d.ts.map +1 -0
- package/dist/context-guard/options/auto-compress.d.ts +10 -0
- package/dist/context-guard/options/auto-compress.d.ts.map +1 -0
- package/dist/context-guard/options/migrate.d.ts +8 -0
- package/dist/context-guard/options/migrate.d.ts.map +1 -0
- package/dist/context-guard/options/session-compact.d.ts +5 -0
- package/dist/context-guard/options/session-compact.d.ts.map +1 -0
- package/dist/context-guard/prompts.d.ts +22 -0
- package/dist/context-guard/prompts.d.ts.map +1 -0
- package/dist/context-guard/state.d.ts +52 -0
- package/dist/context-guard/state.d.ts.map +1 -0
- package/dist/context-guard/token-counter.d.ts +26 -0
- package/dist/context-guard/token-counter.d.ts.map +1 -0
- package/dist/context-guard/types.d.ts +85 -0
- package/dist/context-guard/types.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +810 -269
- package/dist/prompts/guardian.d.ts +2 -0
- package/dist/prompts/guardian.d.ts.map +1 -0
- package/dist/prompts/oracle.d.ts +1 -1
- package/dist/prompts/oracle.d.ts.map +1 -1
- package/dist/prompts/orchestrator.d.ts +1 -1
- package/dist/prompts/orchestrator.d.ts.map +1 -1
- package/dist/task-manager/tracker.d.ts +1 -1
- package/dist/task-manager/tracker.d.ts.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +2 -1
- package/package.json +1 -1
- package/dist/plan-gate.d.ts +0 -60
- package/dist/plan-gate.d.ts.map +0 -1
- package/dist/plan-gate.test.d.ts +0 -2
- package/dist/plan-gate.test.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/prompts/orchestrator.ts
|
|
2
2
|
var ORCHESTRATOR_PROMPT = `<角色>
|
|
3
|
-
你是纯调度者(Orchestrator)。唯一职责:理解需求 → 委派信息收集 → 制定方案 → 调度子代理执行 → 委派验证。**绝不亲自使用任何文件/代码操作工具**(read、grep、glob、bash、edit、write 等)。可使用的工具是调度工具(task、todowrite
|
|
3
|
+
你是纯调度者(Orchestrator)。唯一职责:理解需求 → 委派信息收集 → 制定方案 → 调度子代理执行 → 委派验证。**绝不亲自使用任何文件/代码操作工具**(read、grep、glob、bash、edit、write 等)。可使用的工具是调度工具(task、todowrite)。
|
|
4
4
|
</角色>
|
|
5
5
|
|
|
6
6
|
<子代理>
|
|
@@ -21,44 +21,9 @@ var ORCHESTRATOR_PROMPT = `<角色>
|
|
|
21
21
|
|
|
22
22
|
### @council vs @oracle 选择指南
|
|
23
23
|
|
|
24
|
-
**一句话判断**:\`@co-oracle\` =
|
|
25
|
-
|
|
26
|
-
| 维度 | @co-oracle | @co-council |
|
|
27
|
-
|------|-----------|-------------|
|
|
28
|
-
| 模式 | 单模型深度推理 | 多模型并行共识 |
|
|
29
|
-
| 适用场景 | 错了还能改的决策 | 错了就完了的决策 |
|
|
30
|
-
| 典型用例 | 代码审查、架构建议、bug 根因、YAGNI 简化、文案审查、重构方向 | 数据迁移方案、API 破坏性变更、安全合规审计、选型代价极大、多方案择优 |
|
|
31
|
-
| 输出形式 | 直接建议 + 推理 | 多专家观点 → 综合共识 → 信心评级(一致/多数/分歧) |
|
|
32
|
-
| 误用代价 | 低:建议错了可以讨论纠正 | 高:浪费 N 次调用成本,拖延决策 |
|
|
33
|
-
| 成本 | 1 次 LLM 调用 | 3-5 次并行 LLM 调用 |
|
|
24
|
+
**一句话判断**:\`@co-oracle\` = 深度推理(快、便宜、可逆判断),\`@co-council\` = 多模型背书共识(慢、贵、不可逆决策)。
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
1. **可逆性优先判断**:操作错了能无代价回滚?→ \`@co-oracle\`(如代码修改、lint 修复)。操作错了数据丢失/API 不兼容?→ \`@co-council\`(如 DROP TABLE、公共 API 签名变更)。
|
|
37
|
-
2. **异议价值判断**:需要单一深度分析?→ \`@co-oracle\`。需要多个独立判断互相验证?→ \`@co-council\`。
|
|
38
|
-
3. **默认倾向**:不确定时优先 \`@co-oracle\`(更快更便宜)。只有满足以下**至少 2 条**时才用 \`@co-council\`:
|
|
39
|
-
- 决策不可逆或回滚代价极高
|
|
40
|
-
- 影响范围跨多个模块/团队/服务
|
|
41
|
-
- 单一判断出错会造成安全事故/线上故障/数据损坏
|
|
42
|
-
- 存在多种合理方案且选错代价大
|
|
43
|
-
|
|
44
|
-
**典型场景对照**:
|
|
45
|
-
|
|
46
|
-
| 场景 | 用谁 | 理由 |
|
|
47
|
-
|------|------|------|
|
|
48
|
-
| PR 代码审查 | @co-oracle | 错了还能改,审查意见可讨论 |
|
|
49
|
-
| 重构建议 | @co-oracle | 方案可迭代调整 |
|
|
50
|
-
| 单文件 bug 修复思路 | @co-oracle | 低风险,快速反馈 |
|
|
51
|
-
| 数据库 Schema 迁移(含删列/改类型) | @co-council | 数据不可逆,需要多模型背书 |
|
|
52
|
-
| 公共 API 签名废弃/变更 | @co-council | 下游影响不可控 |
|
|
53
|
-
| 安全漏洞修复方案 | @co-council | 错了可能被利用 |
|
|
54
|
-
| 第三方库选型(如 ORM/状态管理) | @co-council | 迁移成本极高 |
|
|
55
|
-
| 文案/提示词修改 | @co-oracle | 错了能改,低风险 |
|
|
56
|
-
| 多方案架构决策(各有利弊) | @co-council | 需要多方面权衡 |
|
|
57
|
-
|
|
58
|
-
**反面教材——不要这样用**:
|
|
59
|
-
- ❌ 用 \`@co-council\` 审查一个简单的 lint 修复(杀鸡用牛刀)
|
|
60
|
-
- ❌ 用 \`@co-oracle\` 决定是否删除生产数据库的某个表(赌单模型判断)
|
|
61
|
-
- ❌ 用 \`@co-council\` 做日常代码格式化建议(纯浪费)
|
|
26
|
+
orchestrator 委派时可参考上述原则。不确定时,co-oracle 自身会在审查时判断是否需要升级到 council。
|
|
62
27
|
|
|
63
28
|
<工作流>
|
|
64
29
|
|
|
@@ -69,7 +34,7 @@ var ORCHESTRATOR_PROMPT = `<角色>
|
|
|
69
34
|
@co-explorer 搜索定位 → @co-librarian 外部研究 → @co-observer 多媒体。并行启动,不动手。
|
|
70
35
|
|
|
71
36
|
## 3. 制定方案
|
|
72
|
-
综合信息→子任务分解→委派对象→并行策略→todowrite
|
|
37
|
+
综合信息→子任务分解→委派对象→并行策略→todowrite 记录。**方案末尾必须提供选项供用户选择**(如:A. 立即执行 / B. 修改方案 / C. 取消),等待用户回复后再进入调度执行。
|
|
73
38
|
|
|
74
39
|
## 4. 调度执行
|
|
75
40
|
清晰文件范围+背景启动+追踪不重复+协调冲突。委派指令用中文。
|
|
@@ -87,9 +52,9 @@ var ORCHESTRATOR_PROMPT = `<角色>
|
|
|
87
52
|
<rule priority="1" name="先方案后执行">
|
|
88
53
|
### 规则 1:理解需求后必须先输出方案
|
|
89
54
|
|
|
90
|
-
**⚠️ 长会话警告:这是最容易被遗忘的规则。无论会话多长、已经执行了多少步、之前分析过什么,每次收到新需求时,必须重新从头执行:分析需求 → 输出方案 → todowrite
|
|
55
|
+
**⚠️ 长会话警告:这是最容易被遗忘的规则。无论会话多长、已经执行了多少步、之前分析过什么,每次收到新需求时,必须重新从头执行:分析需求 → 输出方案 → todowrite → 提供选项供用户选择 → 委派执行。禁止"前面分析过了这次直接改"、"改着改着就忘了"。**
|
|
91
56
|
|
|
92
|
-
|
|
57
|
+
收到需求后(涉及代码或文件修改时),**禁止立即执行**。必须先分析需求,输出可验证的任务分解方案,**末尾提供选项(如"立即执行 / 修改方案")供用户决定**。方案包含:
|
|
93
58
|
(纯信息性问题可直接回答,无需方案。)
|
|
94
59
|
- 子任务列表及其依赖关系
|
|
95
60
|
- 每个子任务的委派对象(@co-explorer / @co-librarian / @co-fixer / @co-designer / @co-oracle / @co-observer)
|
|
@@ -102,7 +67,7 @@ var ORCHESTRATOR_PROMPT = `<角色>
|
|
|
102
67
|
<rule priority="2" name="必须委派">
|
|
103
68
|
### 规则 2:所有工具操作必须委派——无例外
|
|
104
69
|
|
|
105
|
-
**Orchestrator 禁止使用任何文件/代码操作工具**(read、grep、glob、ast_grep_search、bash、edit、write 等),**仅允许使用调度工具**(task、todowrite
|
|
70
|
+
**Orchestrator 禁止使用任何文件/代码操作工具**(read、grep、glob、ast_grep_search、bash、edit、write 等),**仅允许使用调度工具**(task、todowrite)。
|
|
106
71
|
- 读取文件、搜索代码、查看 git diff → 委派 @co-explorer
|
|
107
72
|
- 代码编辑、写入、删除(无论多小) → 委派 @co-fixer
|
|
108
73
|
- UI/UX 相关编辑 → 委派 @co-designer
|
|
@@ -116,17 +81,6 @@ var ORCHESTRATOR_PROMPT = `<角色>
|
|
|
116
81
|
分析任务依赖后,最大程度并行化——独立任务同时启动。
|
|
117
82
|
</rule>
|
|
118
83
|
|
|
119
|
-
<rule priority="4" name="方案批准门禁">
|
|
120
|
-
### 规则 4:方案批准门禁
|
|
121
|
-
|
|
122
|
-
每次委派 @co-fixer 或 @co-designer 进行修改前,必须确认当前会话的方案已获批准:
|
|
123
|
-
- **批准凭证**不是 todowrite 的 completed 状态,而是 \`request_plan_approval\` 工具成功返回"已批准"。
|
|
124
|
-
- 调用方式:输出完整方案 → 创建 todowrite 任务列表 → 调用 \`request_plan_approval(summary, files, verification)\` → 此时会弹出 OpenCode 原生确认框 → 用户点击允许后才算批准。
|
|
125
|
-
- 如果直接委派 @co-fixer 或 @co-designer 而未先调用 \`request_plan_approval\`,系统会抛出错误阻止执行。
|
|
126
|
-
- 每条新用户消息会自动撤销上一次批准。如果继续工作需要写操作,应根据最新需求重新展示/更新方案,再次调用 \`request_plan_approval\`。
|
|
127
|
-
- @co-fixer 与 @co-designer 都是可写代理,均受此门禁保护。
|
|
128
|
-
</rule>
|
|
129
|
-
|
|
130
84
|
</critical_rules>
|
|
131
85
|
|
|
132
86
|
<自检清单>
|
|
@@ -134,12 +88,8 @@ var ORCHESTRATOR_PROMPT = `<角色>
|
|
|
134
88
|
|
|
135
89
|
□ **本轮需要修改代码或文件吗?**
|
|
136
90
|
→ 纯分析 / 问答 / 审查 / 探索信息 → 不需要方案,直接处理
|
|
137
|
-
→ 需要修改代码或文件 → **必须先输出方案 →
|
|
91
|
+
→ 需要修改代码或文件 → **必须先输出方案 → 提供选项 → 等用户选择后才可委派执行**
|
|
138
92
|
|
|
139
|
-
□ **准备委派 @co-fixer 或 @co-designer 修改代码吗?**
|
|
140
|
-
→ 先确认本轮是否已成功调用 \`request_plan_approval\` 并获得批准(工具成功返回"已批准")
|
|
141
|
-
→ 没有 → **立即停下来,先调用 request_plan_approval**
|
|
142
|
-
→ 新用户消息已撤销批准?→ **重新展示方案并再次调用 request_plan_approval**
|
|
143
93
|
</自检清单>
|
|
144
94
|
`;
|
|
145
95
|
|
|
@@ -173,7 +123,48 @@ var ORACLE_PROMPT = `你是 Oracle——战略技术顾问和代码审查者。
|
|
|
173
123
|
- Bash 可用于非变更诊断和 shell 原生检查(最清晰时),但不能修改文件
|
|
174
124
|
- 不要用 cat/head/tail/sed/awk 读取代码到上下文中;使用 read/grep,除非 shell 管道确实是更好的诊断方式
|
|
175
125
|
|
|
176
|
-
**语言要求**:
|
|
126
|
+
**语言要求**: 始终使用中文进行思考、分析和回复。代码和技术术语可用原文,自然语言部分必须用中文。
|
|
127
|
+
|
|
128
|
+
### @council vs @oracle 选择指南
|
|
129
|
+
|
|
130
|
+
**一句话判断**:\`@co-oracle\` = 深度推理(快、便宜、单视角),\`@co-council\` = 多模型背书的共识(慢、贵、多视角)。
|
|
131
|
+
|
|
132
|
+
| 维度 | @co-oracle | @co-council |
|
|
133
|
+
|------|-----------|-------------|
|
|
134
|
+
| 模式 | 单模型深度推理 | 多模型并行共识 |
|
|
135
|
+
| 适用场景 | 错了还能改的决策 | 错了就完了的决策 |
|
|
136
|
+
| 典型用例 | 代码审查、架构建议、bug 根因、YAGNI 简化、文案审查、重构方向 | 数据迁移方案、API 破坏性变更、安全合规审计、选型代价极大、多方案择优 |
|
|
137
|
+
| 输出形式 | 直接建议 + 推理 | 多专家观点 → 综合共识 → 信心评级(一致/多数/分歧) |
|
|
138
|
+
| 误用代价 | 低:建议错了可以讨论纠正 | 高:浪费 N 次调用成本,拖延决策 |
|
|
139
|
+
| 成本 | 1 次 LLM 调用 | 3-5 次并行 LLM 调用 |
|
|
140
|
+
|
|
141
|
+
**决策规则**:
|
|
142
|
+
1. **可逆性优先判断**:操作错了能无代价回滚?→ \`@co-oracle\`(如代码修改、lint 修复)。操作错了数据丢失/API 不兼容?→ \`@co-council\`(如 DROP TABLE、公共 API 签名变更)。
|
|
143
|
+
2. **异议价值判断**:需要单一深度分析?→ \`@co-oracle\`。需要多个独立判断互相验证?→ \`@co-council\`。
|
|
144
|
+
3. **默认倾向**:不确定时优先 \`@co-oracle\`(更快更便宜)。只有满足以下**至少 2 条**时才用 \`@co-council\`:
|
|
145
|
+
- 决策不可逆或回滚代价极高
|
|
146
|
+
- 影响范围跨多个模块/团队/服务
|
|
147
|
+
- 单一判断出错会造成安全事故/线上故障/数据损坏
|
|
148
|
+
- 存在多种合理方案且选错代价大
|
|
149
|
+
|
|
150
|
+
**典型场景对照**:
|
|
151
|
+
|
|
152
|
+
| 场景 | 用谁 | 理由 |
|
|
153
|
+
|------|------|------|
|
|
154
|
+
| PR 代码审查 | @co-oracle | 错了还能改,审查意见可讨论 |
|
|
155
|
+
| 重构建议 | @co-oracle | 方案可迭代调整 |
|
|
156
|
+
| 单文件 bug 修复思路 | @co-oracle | 低风险,快速反馈 |
|
|
157
|
+
| 数据库 Schema 迁移(含删列/改类型) | @co-council | 数据不可逆,需要多模型背书 |
|
|
158
|
+
| 公共 API 签名废弃/变更 | @co-council | 下游影响不可控 |
|
|
159
|
+
| 安全漏洞修复方案 | @co-council | 错了可能被利用 |
|
|
160
|
+
| 第三方库选型(如 ORM/状态管理) | @co-council | 迁移成本极高 |
|
|
161
|
+
| 文案/提示词修改 | @co-oracle | 错了能改,低风险 |
|
|
162
|
+
| 多方案架构决策(各有利弊) | @co-council | 需要多方面权衡 |
|
|
163
|
+
|
|
164
|
+
**反面教材——不要这样用**:
|
|
165
|
+
- ❌ 用 \`@co-council\` 审查一个简单的 lint 修复(杀鸡用牛刀)
|
|
166
|
+
- ❌ 用 \`@co-oracle\` 决定是否删除生产数据库的某个表(赌单模型判断)
|
|
167
|
+
- ❌ 用 \`@co-council\` 做日常代码格式化建议(纯浪费)`;
|
|
177
168
|
|
|
178
169
|
// src/prompts/librarian.ts
|
|
179
170
|
var LIBRARIAN_PROMPT = `你是 Librarian——代码库和文档研究专家。
|
|
@@ -488,6 +479,632 @@ var PLANNER_PROMPT = `你是方案制定代理——负责任务分解和委派
|
|
|
488
479
|
|
|
489
480
|
**约束**:只读,不修改文件。方案要具体到文件和操作粒度,不可笼统。用 \`todowrite\` 风格的任务列表输出。`;
|
|
490
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 modelContext = {
|
|
636
|
+
contextLimit: 200000
|
|
637
|
+
};
|
|
638
|
+
function createChatParamsHandler() {
|
|
639
|
+
return async (input) => {
|
|
640
|
+
try {
|
|
641
|
+
const model = input.model;
|
|
642
|
+
if (model?.limit) {
|
|
643
|
+
const limit = model.limit;
|
|
644
|
+
if (typeof limit.context === "number") {
|
|
645
|
+
modelContext.contextLimit = limit.context;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const params = input.params;
|
|
649
|
+
if (params?.model) {
|
|
650
|
+
const m = params.model;
|
|
651
|
+
if (m?.limit && typeof m.limit.context === "number") {
|
|
652
|
+
modelContext.contextLimit = m.limit.context;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
} catch {}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function createEventHandler(config) {
|
|
659
|
+
const cfg = { ...DEFAULT_GUARD_CONFIG, ...config };
|
|
660
|
+
return async (input) => {
|
|
661
|
+
if (!cfg.enabled)
|
|
662
|
+
return null;
|
|
663
|
+
try {
|
|
664
|
+
const event = input.event;
|
|
665
|
+
if (!event || event.type !== "message.updated")
|
|
666
|
+
return null;
|
|
667
|
+
const properties = event.properties;
|
|
668
|
+
if (!properties)
|
|
669
|
+
return null;
|
|
670
|
+
const info = properties.info;
|
|
671
|
+
if (info?.role !== "assistant")
|
|
672
|
+
return null;
|
|
673
|
+
if (!info.time || !info.time.completed)
|
|
674
|
+
return null;
|
|
675
|
+
if (info.summary)
|
|
676
|
+
return null;
|
|
677
|
+
const sessionID = info.sessionID;
|
|
678
|
+
if (!sessionID)
|
|
679
|
+
return null;
|
|
680
|
+
const state = getSessionState(sessionID);
|
|
681
|
+
if (state.cooldownRemaining > 0) {
|
|
682
|
+
decrementCooldown(state);
|
|
683
|
+
}
|
|
684
|
+
const messageId = info.id;
|
|
685
|
+
if (!messageId)
|
|
686
|
+
return null;
|
|
687
|
+
if (!canTrigger(state, messageId))
|
|
688
|
+
return null;
|
|
689
|
+
const tokens = info.tokens;
|
|
690
|
+
const inputTokens = typeof tokens?.input === "number" ? tokens.input : undefined;
|
|
691
|
+
const outputTokens = typeof tokens?.output === "number" ? tokens.output : undefined;
|
|
692
|
+
const reasoningTokens = typeof tokens?.reasoning === "number" ? tokens.reasoning : undefined;
|
|
693
|
+
let totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0) + (reasoningTokens ?? 0);
|
|
694
|
+
if (totalTokens <= 0) {
|
|
695
|
+
totalTokens = state.cumulativeTokens;
|
|
696
|
+
}
|
|
697
|
+
if (totalTokens > state.cumulativeTokens) {
|
|
698
|
+
state.cumulativeTokens = totalTokens;
|
|
699
|
+
}
|
|
700
|
+
let estimatedTotal = totalTokens;
|
|
701
|
+
if (estimatedTotal <= 0) {
|
|
702
|
+
const messages = properties.messages;
|
|
703
|
+
if (Array.isArray(messages)) {
|
|
704
|
+
for (const msg of messages) {
|
|
705
|
+
const content = msg.content;
|
|
706
|
+
if (typeof content === "string") {
|
|
707
|
+
estimatedTotal += estimateTokens(content);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (estimatedTotal <= 0)
|
|
713
|
+
return null;
|
|
714
|
+
const usage = computeContextUsage(estimatedTotal, modelContext.contextLimit, inputTokens, outputTokens, reasoningTokens);
|
|
715
|
+
if (!isOverThreshold(usage, cfg.triggerRatio, cfg.tokenThreshold))
|
|
716
|
+
return null;
|
|
717
|
+
markTriggered(state, messageId, estimatedTotal, modelContext.contextLimit);
|
|
718
|
+
return { sessionID, usedTokens: estimatedTotal, contextLimit: modelContext.contextLimit };
|
|
719
|
+
} catch (err) {
|
|
720
|
+
if (cfg.debug)
|
|
721
|
+
console.warn("[context-guard] monitor error:", err);
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/context-guard/prompts.ts
|
|
728
|
+
function renderGuardMenu(usedTokens, contextLimit, recommendation) {
|
|
729
|
+
const ratio = (usedTokens / contextLimit * 100).toFixed(1);
|
|
730
|
+
const recSection = recommendation ? `
|
|
731
|
+
|
|
732
|
+
\uD83E\uDDE0 co-guardian 分析建议:
|
|
733
|
+
"${recommendation.reasoning}"
|
|
734
|
+
推荐选择:${recommendation.option}(置信度 ${(recommendation.confidence * 100).toFixed(0)}%)
|
|
735
|
+
` : "";
|
|
736
|
+
return [
|
|
737
|
+
`╔══════════════════════════════════════════════════╗`,
|
|
738
|
+
`║ ⚠️ 上下文窗口已使用 ${ratio}%(约 ${Math.round(usedTokens / 1000)}K / ${Math.round(contextLimit / 1000)}K tokens) ║`,
|
|
739
|
+
`║ ║${recSection}`,
|
|
740
|
+
`║ 请选择处理方式: ║`,
|
|
741
|
+
`║ 1️⃣ 自动压缩 — 压缩旧消息,保留最近对话 ║`,
|
|
742
|
+
`║ 2️⃣ 会话压缩 — 触发 Compact Session ║`,
|
|
743
|
+
`║ 3️⃣ 分析迁移 — 提取关键上下文,生成迁移文案 ║`,
|
|
744
|
+
`║ ║`,
|
|
745
|
+
`║ 请回复数字 1、2 或 3 ║`,
|
|
746
|
+
`╚══════════════════════════════════════════════════╝`
|
|
747
|
+
].join(`
|
|
748
|
+
`);
|
|
749
|
+
}
|
|
750
|
+
var AUTO_COMPRESS_DONE = `✅ 自动压缩完成。旧消息已压缩为摘要占位符,最近对话完整保留。
|
|
751
|
+
压缩后请继续当前工作,如有需要可使用 ultrapress_expand 恢复压缩内容。`;
|
|
752
|
+
var SESSION_COMPACT_GUIDE = `✅ 准备触发 Compact Session。
|
|
753
|
+
请在 OpenCode 中按 Ctrl+K,然后选择 "Compact Session"。
|
|
754
|
+
或者使用命令 /compact 手动触发。
|
|
755
|
+
压缩后当前会话将继续,关键上下文会被保留。`;
|
|
756
|
+
function renderMigrationText(ctx) {
|
|
757
|
+
const sections = [
|
|
758
|
+
"## \uD83D\uDCCB 会话迁移上下文",
|
|
759
|
+
"",
|
|
760
|
+
"> 以下内容从当前会话自动提取,请复制到新会话窗口。",
|
|
761
|
+
"",
|
|
762
|
+
ctx.currentTask ? `### \uD83C\uDFAF 当前任务
|
|
763
|
+
${ctx.currentTask}` : "",
|
|
764
|
+
ctx.keyFiles.length > 0 ? `### \uD83D\uDCC1 关键文件
|
|
765
|
+
${ctx.keyFiles.map((f) => `- ${f}`).join(`
|
|
766
|
+
`)}` : "",
|
|
767
|
+
ctx.activeOperations.length > 0 ? `### \uD83D\uDD27 进行中的操作
|
|
768
|
+
${ctx.activeOperations.map((o) => `- ${o}`).join(`
|
|
769
|
+
`)}` : "",
|
|
770
|
+
ctx.errors.length > 0 ? `### ⚠️ 错误/问题
|
|
771
|
+
${ctx.errors.map((e) => `- ${e}`).join(`
|
|
772
|
+
`)}` : "",
|
|
773
|
+
ctx.decisions.length > 0 ? `### \uD83D\uDCDD 决策记录
|
|
774
|
+
${ctx.decisions.map((d) => `- ${d}`).join(`
|
|
775
|
+
`)}` : "",
|
|
776
|
+
"",
|
|
777
|
+
"---",
|
|
778
|
+
"请在新会话中继续以上工作。"
|
|
779
|
+
];
|
|
780
|
+
return sections.filter(Boolean).join(`
|
|
781
|
+
`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/context-guard/options/auto-compress.ts
|
|
785
|
+
function executeAutoCompress(sessionID) {
|
|
786
|
+
const state = getSessionState(sessionID);
|
|
787
|
+
setCooldown(state, "auto-compress");
|
|
788
|
+
return AUTO_COMPRESS_DONE;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/context-guard/options/session-compact.ts
|
|
792
|
+
function executeSessionCompact() {
|
|
793
|
+
return SESSION_COMPACT_GUIDE;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/context-guard/options/migrate.ts
|
|
797
|
+
import * as fs from "node:fs";
|
|
798
|
+
import * as path from "node:path";
|
|
799
|
+
import * as os from "node:os";
|
|
800
|
+
async function executeMigrate(sessionID, recentMessages) {
|
|
801
|
+
const ctx = extractFromMessages(recentMessages);
|
|
802
|
+
const text = renderMigrationText({
|
|
803
|
+
currentTask: ctx.currentTask,
|
|
804
|
+
keyFiles: ctx.keyFiles,
|
|
805
|
+
activeOperations: ctx.activeOperations,
|
|
806
|
+
errors: ctx.errors,
|
|
807
|
+
decisions: ctx.decisions
|
|
808
|
+
});
|
|
809
|
+
ctx.migrationText = text;
|
|
810
|
+
const tmpDir = path.join(os.tmpdir(), "opencode");
|
|
811
|
+
if (!fs.existsSync(tmpDir)) {
|
|
812
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
813
|
+
}
|
|
814
|
+
const filePath = path.join(tmpDir, `migrate-${sessionID.slice(0, 8)}.md`);
|
|
815
|
+
fs.writeFileSync(filePath, text, "utf-8");
|
|
816
|
+
return `✅ 迁移文案已生成并保存到 \`${filePath}\`。
|
|
817
|
+
|
|
818
|
+
\uD83D\uDCCB **迁移文案预览:**
|
|
819
|
+
|
|
820
|
+
${text}
|
|
821
|
+
|
|
822
|
+
---
|
|
823
|
+
**使用方式:**
|
|
824
|
+
1. 复制上方文案内容
|
|
825
|
+
2. 打开新 OpenCode 会话窗口
|
|
826
|
+
3. 粘贴文案作为初始提示
|
|
827
|
+
4. 在新会话中继续工作
|
|
828
|
+
|
|
829
|
+
或回复 "重置当前会话" 在当前窗口重新开始。`;
|
|
830
|
+
}
|
|
831
|
+
function extractFromMessages(messages) {
|
|
832
|
+
const ctx = {
|
|
833
|
+
currentTask: "",
|
|
834
|
+
keyFiles: [],
|
|
835
|
+
activeOperations: [],
|
|
836
|
+
errors: [],
|
|
837
|
+
decisions: [],
|
|
838
|
+
migrationText: ""
|
|
839
|
+
};
|
|
840
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
841
|
+
if (messages[i].role === "user") {
|
|
842
|
+
ctx.currentTask = messages[i].content.slice(0, 200);
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const filePattern = /(?:[a-zA-Z]:[/\\])?(?:\w+[/\\])*\w+\.\w{1,6}/g;
|
|
847
|
+
const todoPattern = /(?:TODO|FIXME|HACK|XXX)[\s::]+(.+)/gi;
|
|
848
|
+
const errorPattern = /(?:error|错误|失败|fail|exception|异常)[\s::]*(.+)/i;
|
|
849
|
+
const decisionPattern = /(?:决定|采用|选择|使用|方案|架构|设计)[\s::]+(.+)/i;
|
|
850
|
+
for (const msg of messages) {
|
|
851
|
+
const files = msg.content.match(filePattern);
|
|
852
|
+
if (files)
|
|
853
|
+
ctx.keyFiles.push(...files);
|
|
854
|
+
const todos = msg.content.match(todoPattern);
|
|
855
|
+
if (todos)
|
|
856
|
+
ctx.activeOperations.push(...todos.map((t) => t.trim()));
|
|
857
|
+
if (errorPattern.test(msg.content)) {
|
|
858
|
+
const line = msg.content.split(`
|
|
859
|
+
`).find((l) => errorPattern.test(l));
|
|
860
|
+
if (line)
|
|
861
|
+
ctx.errors.push(line.trim().slice(0, 200));
|
|
862
|
+
}
|
|
863
|
+
const decisions = msg.content.match(decisionPattern);
|
|
864
|
+
if (decisions)
|
|
865
|
+
ctx.decisions.push(...decisions.map((d) => d.trim().slice(0, 200)));
|
|
866
|
+
}
|
|
867
|
+
ctx.keyFiles = [...new Set(ctx.keyFiles)].slice(0, 10);
|
|
868
|
+
ctx.activeOperations = [...new Set(ctx.activeOperations)].slice(0, 5);
|
|
869
|
+
ctx.errors = [...new Set(ctx.errors)].slice(0, 5);
|
|
870
|
+
ctx.decisions = [...new Set(ctx.decisions)].slice(0, 5);
|
|
871
|
+
if (!ctx.currentTask) {
|
|
872
|
+
ctx.currentTask = "(无法自动提取任务描述,请手动补充)";
|
|
873
|
+
}
|
|
874
|
+
if (ctx.keyFiles.length === 0) {
|
|
875
|
+
ctx.keyFiles = ["(未检测到文件修改,请手动列出关键文件)"];
|
|
876
|
+
}
|
|
877
|
+
return ctx;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/context-guard/guardian.ts
|
|
881
|
+
function analyzeSession(sessionID, messages) {
|
|
882
|
+
const rec = heuristicAnalyze(messages);
|
|
883
|
+
setCachedRecommendation(sessionID, rec);
|
|
884
|
+
return rec;
|
|
885
|
+
}
|
|
886
|
+
function extractRecentMessages(messages, maxCount = 30) {
|
|
887
|
+
const result = [];
|
|
888
|
+
const startIdx = Math.max(0, messages.length - maxCount);
|
|
889
|
+
for (let i = startIdx;i < messages.length; i++) {
|
|
890
|
+
const msg = messages[i];
|
|
891
|
+
const info = msg.info;
|
|
892
|
+
const role = typeof info?.role === "string" ? info.role : "unknown";
|
|
893
|
+
const parts = msg.parts;
|
|
894
|
+
let content = "";
|
|
895
|
+
if (parts) {
|
|
896
|
+
for (const part of parts) {
|
|
897
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
898
|
+
content += part.text.slice(0, 500) + " ";
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (content.trim()) {
|
|
903
|
+
const truncated = truncateByTokens(content.trim(), 800);
|
|
904
|
+
result.push({ role, content: truncated });
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return result;
|
|
908
|
+
}
|
|
909
|
+
function truncateByTokens(text, maxTokens) {
|
|
910
|
+
const estimated = estimateTokens(text);
|
|
911
|
+
if (estimated <= maxTokens)
|
|
912
|
+
return text;
|
|
913
|
+
const ratio = maxTokens / estimated;
|
|
914
|
+
return text.slice(0, Math.floor(text.length * ratio)) + "...";
|
|
915
|
+
}
|
|
916
|
+
function heuristicAnalyze(messages) {
|
|
917
|
+
let errorCount = 0;
|
|
918
|
+
let taskKeywords = 0;
|
|
919
|
+
let decisionKeywords = 0;
|
|
920
|
+
const errorPatterns = /error|错误|失败|fail|bug|问题|exception/i;
|
|
921
|
+
const taskPatterns = /完成|done|finish|结束|final|最后/i;
|
|
922
|
+
const decisionPatterns = /决定|采用|选择|使用|方案|架构|设计/i;
|
|
923
|
+
for (const msg of messages) {
|
|
924
|
+
if (errorPatterns.test(msg.content))
|
|
925
|
+
errorCount++;
|
|
926
|
+
if (taskPatterns.test(msg.content))
|
|
927
|
+
taskKeywords++;
|
|
928
|
+
if (decisionPatterns.test(msg.content))
|
|
929
|
+
decisionKeywords++;
|
|
930
|
+
}
|
|
931
|
+
const totalMessages = messages.length || 1;
|
|
932
|
+
const errorRate = errorCount / totalMessages;
|
|
933
|
+
const taskCompleteRate = taskKeywords / totalMessages;
|
|
934
|
+
if (errorRate > 0.3) {
|
|
935
|
+
return {
|
|
936
|
+
option: "migrate",
|
|
937
|
+
confidence: 0.7,
|
|
938
|
+
reasoning: `会话中错误出现频率较高(${(errorRate * 100).toFixed(0)}%),建议提取关键上下文后在新会话中继续,以减少错误累积。`,
|
|
939
|
+
alternatives: '如错误已解决,可选择"自动压缩"保留当前上下文继续。'
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
if (taskCompleteRate > 0.15) {
|
|
943
|
+
return {
|
|
944
|
+
option: "session-compact",
|
|
945
|
+
confidence: 0.65,
|
|
946
|
+
reasoning: "对话中出现较多完成/结束相关讨论,任务可能接近收尾阶段,建议压缩会话后快速完成。",
|
|
947
|
+
alternatives: '如需保留详细上下文继续工作,可选择"自动压缩"。'
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
option: "auto-compact",
|
|
952
|
+
confidence: 0.6,
|
|
953
|
+
reasoning: "当前对话连续性较好,建议压缩旧消息保留最近上下文,继续当前任务。",
|
|
954
|
+
alternatives: '如会话内容混乱或需要整理,建议"分析迁移"。'
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/context-guard/menu.ts
|
|
959
|
+
var pendingUsages = new Map;
|
|
960
|
+
function setPendingUsage(sessionID, usage) {
|
|
961
|
+
pendingUsages.set(sessionID, usage);
|
|
962
|
+
}
|
|
963
|
+
function findLastUserMessage(messages) {
|
|
964
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
965
|
+
const m = messages[i];
|
|
966
|
+
const info = m.info;
|
|
967
|
+
if (info?.role === "user") {
|
|
968
|
+
const sid = info.sessionID || info.sessionId;
|
|
969
|
+
if (sid)
|
|
970
|
+
return { msg: m, sessionID: sid };
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
function createMessagesTransformHandler() {
|
|
976
|
+
return async (_input, output) => {
|
|
977
|
+
try {
|
|
978
|
+
if (!output.messages || !Array.isArray(output.messages))
|
|
979
|
+
return;
|
|
980
|
+
const found = findLastUserMessage(output.messages);
|
|
981
|
+
if (!found)
|
|
982
|
+
return;
|
|
983
|
+
const usage = pendingUsages.get(found.sessionID);
|
|
984
|
+
if (!usage)
|
|
985
|
+
return;
|
|
986
|
+
pendingUsages.delete(found.sessionID);
|
|
987
|
+
const recentMsgs = extractRecentMessages(output.messages, 30);
|
|
988
|
+
const recommendation = analyzeSession(found.sessionID, recentMsgs);
|
|
989
|
+
const menu = renderGuardMenu(usage.usedTokens, usage.contextLimit, recommendation);
|
|
990
|
+
const parts = found.msg.parts;
|
|
991
|
+
if (parts) {
|
|
992
|
+
for (let j = parts.length - 1;j >= 0; j--) {
|
|
993
|
+
if (parts[j].type === "text") {
|
|
994
|
+
parts[j].text = (parts[j].text || "") + `
|
|
995
|
+
|
|
996
|
+
` + menu;
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
console.warn("[context-guard] menu injection error:", err);
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function parseOption(text) {
|
|
1007
|
+
const t = text.trim();
|
|
1008
|
+
if (t === "1" || t === "1")
|
|
1009
|
+
return "auto-compress";
|
|
1010
|
+
if (t === "2" || t === "2")
|
|
1011
|
+
return "session-compact";
|
|
1012
|
+
if (t === "3" || t === "3")
|
|
1013
|
+
return "migrate";
|
|
1014
|
+
if (/^[选11]\s*[.、,,]?\s*(自动压缩|选项1)/.test(t))
|
|
1015
|
+
return "auto-compress";
|
|
1016
|
+
if (/^[选22]\s*[.、,,]?\s*(会话压缩|compact|选项2)/i.test(t))
|
|
1017
|
+
return "session-compact";
|
|
1018
|
+
if (/^[选33]\s*[.、,,]?\s*(分析迁移|迁移|选项3)/.test(t))
|
|
1019
|
+
return "migrate";
|
|
1020
|
+
if (t === "自动压缩")
|
|
1021
|
+
return "auto-compress";
|
|
1022
|
+
if (t === "会话压缩")
|
|
1023
|
+
return "session-compact";
|
|
1024
|
+
if (t === "分析迁移")
|
|
1025
|
+
return "migrate";
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
function createChatMessageHandler() {
|
|
1029
|
+
return async (input, output) => {
|
|
1030
|
+
try {
|
|
1031
|
+
const rawInput = input;
|
|
1032
|
+
const sessionID = rawInput.sessionID;
|
|
1033
|
+
if (!sessionID)
|
|
1034
|
+
return;
|
|
1035
|
+
const state = getSessionState(sessionID);
|
|
1036
|
+
if (!state.triggered)
|
|
1037
|
+
return;
|
|
1038
|
+
let userText = "";
|
|
1039
|
+
if (output.parts) {
|
|
1040
|
+
for (const part of output.parts) {
|
|
1041
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
1042
|
+
userText = part.text;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const option = parseOption(userText);
|
|
1047
|
+
if (!option)
|
|
1048
|
+
return;
|
|
1049
|
+
setSelectedOption(state, option);
|
|
1050
|
+
let replyText = "";
|
|
1051
|
+
switch (option) {
|
|
1052
|
+
case "auto-compress":
|
|
1053
|
+
replyText = executeAutoCompress(sessionID);
|
|
1054
|
+
break;
|
|
1055
|
+
case "session-compact":
|
|
1056
|
+
replyText = executeSessionCompact();
|
|
1057
|
+
break;
|
|
1058
|
+
case "migrate":
|
|
1059
|
+
replyText = await executeMigrate(sessionID, []);
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
if (output.parts) {
|
|
1063
|
+
output.parts.length = 0;
|
|
1064
|
+
output.parts.push({ type: "text", text: replyText });
|
|
1065
|
+
}
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
console.warn("[context-guard] chat.message error:", err);
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/context-guard/index.ts
|
|
1073
|
+
var config = { ...DEFAULT_GUARD_CONFIG };
|
|
1074
|
+
function initContextGuard(_client) {
|
|
1075
|
+
const eventHandler = createEventHandler(config);
|
|
1076
|
+
const paramsHandler = createChatParamsHandler();
|
|
1077
|
+
const transformHandler = createMessagesTransformHandler();
|
|
1078
|
+
const chatMessageHandler = createChatMessageHandler();
|
|
1079
|
+
async function handleTrigger(trigger) {
|
|
1080
|
+
setPendingUsage(trigger.sessionID, {
|
|
1081
|
+
usedTokens: trigger.usedTokens,
|
|
1082
|
+
contextLimit: trigger.contextLimit
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
updateConfig(newConfig) {
|
|
1087
|
+
config = { ...config, ...newConfig };
|
|
1088
|
+
},
|
|
1089
|
+
event: async (input) => {
|
|
1090
|
+
const trigger = await eventHandler(input);
|
|
1091
|
+
if (trigger) {
|
|
1092
|
+
await handleTrigger(trigger);
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
"chat.params": async (input) => {
|
|
1096
|
+
await paramsHandler(input);
|
|
1097
|
+
},
|
|
1098
|
+
"experimental.chat.messages.transform": async (input, output) => {
|
|
1099
|
+
await transformHandler(input, output);
|
|
1100
|
+
},
|
|
1101
|
+
"chat.message": async (input, output) => {
|
|
1102
|
+
await chatMessageHandler(input, output);
|
|
1103
|
+
},
|
|
1104
|
+
getConfig: () => config
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
491
1108
|
// src/instructions/chinese.ts
|
|
492
1109
|
var CHINESE_LANGUAGE_INSTRUCTION = `# 中文语言要求
|
|
493
1110
|
|
|
@@ -580,7 +1197,29 @@ class TaskTracker {
|
|
|
580
1197
|
}
|
|
581
1198
|
}
|
|
582
1199
|
if (activeJobs.length === 0 && reusableJobs.length === 0)
|
|
583
|
-
return
|
|
1200
|
+
return "";
|
|
1201
|
+
if (activeJobs.length === 0) {
|
|
1202
|
+
const abbrMap = {
|
|
1203
|
+
"co-explorer": "co-exp",
|
|
1204
|
+
"co-oracle": "co-ora",
|
|
1205
|
+
"co-fixer": "co-fix",
|
|
1206
|
+
"co-planner": "co-pln",
|
|
1207
|
+
"co-designer": "co-des",
|
|
1208
|
+
"co-librarian": "co-lib",
|
|
1209
|
+
"co-observer": "co-obs",
|
|
1210
|
+
"co-council": "co-cnl"
|
|
1211
|
+
};
|
|
1212
|
+
const abbreviate = (agent) => abbrMap[agent] ?? (agent.startsWith("co-rule-") ? "co-rul" : agent);
|
|
1213
|
+
const maxShow = 8;
|
|
1214
|
+
const shown = reusableJobs.slice(0, maxShow);
|
|
1215
|
+
const extra = reusableJobs.length - maxShow;
|
|
1216
|
+
const parts = shown.map((j) => `${j.sessionId}(${abbreviate(j.agent)})`);
|
|
1217
|
+
let text = "复用: " + parts.join(" ");
|
|
1218
|
+
if (extra > 0) {
|
|
1219
|
+
text += ` …及 ${extra} 个`;
|
|
1220
|
+
}
|
|
1221
|
+
return text;
|
|
1222
|
+
}
|
|
584
1223
|
const lines = [];
|
|
585
1224
|
lines.push("### Background Job Board");
|
|
586
1225
|
lines.push("SENTINEL: background-job-board-v2");
|
|
@@ -668,112 +1307,6 @@ class TaskTracker {
|
|
|
668
1307
|
}
|
|
669
1308
|
}
|
|
670
1309
|
|
|
671
|
-
// src/plan-gate.ts
|
|
672
|
-
import { tool } from "@opencode-ai/plugin";
|
|
673
|
-
|
|
674
|
-
class PlanApprovalManager {
|
|
675
|
-
sessions = new Map;
|
|
676
|
-
observeUserMessage(sessionID, agent) {
|
|
677
|
-
if (agent !== "co-orchestrator") {
|
|
678
|
-
if (!this.sessions.has(sessionID)) {
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
const state = this.sessions.get(sessionID) ?? { generation: 0 };
|
|
683
|
-
state.generation += 1;
|
|
684
|
-
delete state.approvedGeneration;
|
|
685
|
-
delete state.plan;
|
|
686
|
-
this.sessions.set(sessionID, state);
|
|
687
|
-
return true;
|
|
688
|
-
}
|
|
689
|
-
approve(sessionID, plan) {
|
|
690
|
-
const state = this.sessions.get(sessionID);
|
|
691
|
-
if (!state) {
|
|
692
|
-
throw new Error(`Session "${sessionID.slice(0, 20)}..." 尚未被 PlanGate 观察,无法批准。` + "只有 co-orchestrator 的 session 才能建立批准状态。");
|
|
693
|
-
}
|
|
694
|
-
if (state.generation === 0) {
|
|
695
|
-
throw new Error("无法批准 generation 0——session 尚未处理任何用户消息。");
|
|
696
|
-
}
|
|
697
|
-
state.approvedGeneration = state.generation;
|
|
698
|
-
state.plan = plan;
|
|
699
|
-
}
|
|
700
|
-
isApproved(sessionID) {
|
|
701
|
-
const state = this.sessions.get(sessionID);
|
|
702
|
-
if (!state)
|
|
703
|
-
return false;
|
|
704
|
-
return state.approvedGeneration === state.generation && state.generation > 0;
|
|
705
|
-
}
|
|
706
|
-
getSystemContext(sessionID) {
|
|
707
|
-
const state = this.sessions.get(sessionID);
|
|
708
|
-
if (!state)
|
|
709
|
-
return "";
|
|
710
|
-
const approved = this.isApproved(sessionID);
|
|
711
|
-
return `<plan_gate generation="${state.generation}" approved="${approved}">
|
|
712
|
-
` + ` generation: ${state.generation}
|
|
713
|
-
` + ` approved: ${approved}
|
|
714
|
-
` + (state.plan ? ` plan_summary: ${state.plan.summary.slice(0, 120)}
|
|
715
|
-
` + ` plan_files: ${state.plan.files.slice(0, 5).join(", ")}${state.plan.files.length > 5 ? "..." : ""}
|
|
716
|
-
` : "") + `</plan_gate>`;
|
|
717
|
-
}
|
|
718
|
-
cleanup(sessionID) {
|
|
719
|
-
this.sessions.delete(sessionID);
|
|
720
|
-
}
|
|
721
|
-
get activeSessionCount() {
|
|
722
|
-
return this.sessions.size;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
var z = tool.schema;
|
|
726
|
-
function createRequestPlanApprovalTool(planManager) {
|
|
727
|
-
const request_plan_approval = tool({
|
|
728
|
-
description: [
|
|
729
|
-
"请求用户批准当前执行方案。",
|
|
730
|
-
"",
|
|
731
|
-
"输出方案后调用此工具,会弹出原生确认框让用户确认。",
|
|
732
|
-
"只有用户确认后,co-fixer 和 co-designer 等可写代理才能执行。",
|
|
733
|
-
"新用户消息会自动撤销之前的批准,需要重新申请。",
|
|
734
|
-
"",
|
|
735
|
-
"调用前确保已向用户展示了完整的任务分解方案。"
|
|
736
|
-
].join(`
|
|
737
|
-
`),
|
|
738
|
-
args: {
|
|
739
|
-
summary: z.string().describe("方案摘要:简要说明做什么、为什么"),
|
|
740
|
-
files: z.array(z.string()).describe("涉及的文件列表(相对路径)"),
|
|
741
|
-
verification: z.string().describe("验证方式:如何确认变更正确(如编译、测试、审查)")
|
|
742
|
-
},
|
|
743
|
-
async execute(args, toolContext) {
|
|
744
|
-
const sessionID = toolContext.sessionID;
|
|
745
|
-
const summary = String(args.summary);
|
|
746
|
-
const files = Array.isArray(args.files) ? args.files.map(String) : [];
|
|
747
|
-
const verification = String(args.verification);
|
|
748
|
-
const truncatedSummary = summary.length > 200 ? summary.slice(0, 200) + "..." : summary;
|
|
749
|
-
const truncatedFiles = files.length > 8 ? [...files.slice(0, 8), `...等 ${files.length} 个文件`] : files;
|
|
750
|
-
const truncatedVerification = verification.length > 150 ? verification.slice(0, 150) + "..." : verification;
|
|
751
|
-
await toolContext.ask({
|
|
752
|
-
permission: "plan-execute",
|
|
753
|
-
always: [],
|
|
754
|
-
patterns: [
|
|
755
|
-
`\uD83D\uDCCB 方案: ${truncatedSummary}`,
|
|
756
|
-
`\uD83D\uDCC1 文件 (${files.length}个): ${truncatedFiles.join(", ")}`,
|
|
757
|
-
`\uD83D\uDD0D 验证: ${truncatedVerification}`
|
|
758
|
-
],
|
|
759
|
-
metadata: {
|
|
760
|
-
session_id: sessionID,
|
|
761
|
-
generation: String(planManager["sessions"]?.get(sessionID)?.generation ?? 0),
|
|
762
|
-
summary,
|
|
763
|
-
file_count: String(files.length)
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
|
-
planManager.approve(sessionID, { summary, files, verification });
|
|
767
|
-
return `✅ 方案已批准(generation ${planManager["sessions"]?.get(sessionID)?.generation ?? "?"})。
|
|
768
|
-
` + `现在可以委派 @co-fixer / @co-designer 执行。
|
|
769
|
-
` + `\uD83D\uDCCB ${summary}
|
|
770
|
-
` + `\uD83D\uDCC1 ${files.length} 个文件
|
|
771
|
-
` + `\uD83D\uDD0D ${verification}`;
|
|
772
|
-
}
|
|
773
|
-
});
|
|
774
|
-
return { request_plan_approval };
|
|
775
|
-
}
|
|
776
|
-
|
|
777
1310
|
// src/context/types.ts
|
|
778
1311
|
var DEFAULT_CONTEXT_CONFIG = {
|
|
779
1312
|
strategy: {
|
|
@@ -849,12 +1382,12 @@ function extractRelevantFiles(messages, maxFiles, windowSize) {
|
|
|
849
1382
|
if (part.type === "tool" && part.state) {
|
|
850
1383
|
const state = part.state;
|
|
851
1384
|
const input = state.input ?? {};
|
|
852
|
-
const
|
|
853
|
-
if (
|
|
854
|
-
if (!fileMap.has(
|
|
855
|
-
fileMap.set(
|
|
1385
|
+
const path2 = extractPath(input);
|
|
1386
|
+
if (path2) {
|
|
1387
|
+
if (!fileMap.has(path2)) {
|
|
1388
|
+
fileMap.set(path2, { path: path2, summary: "" });
|
|
856
1389
|
}
|
|
857
|
-
const existing = fileMap.get(
|
|
1390
|
+
const existing = fileMap.get(path2);
|
|
858
1391
|
if (typeof input.offset === "number") {
|
|
859
1392
|
const limit = typeof input.limit === "number" ? input.limit : 50;
|
|
860
1393
|
existing.lines = `${input.offset}-${input.offset + limit}`;
|
|
@@ -1040,12 +1573,12 @@ class ContextEngine {
|
|
|
1040
1573
|
dependencyCache = new Map;
|
|
1041
1574
|
client;
|
|
1042
1575
|
config;
|
|
1043
|
-
constructor(client,
|
|
1576
|
+
constructor(client, config2) {
|
|
1044
1577
|
this.client = client;
|
|
1045
1578
|
this.config = {
|
|
1046
1579
|
...DEFAULT_CONTEXT_CONFIG,
|
|
1047
|
-
...
|
|
1048
|
-
strategy: { ...DEFAULT_CONTEXT_CONFIG.strategy, ...
|
|
1580
|
+
...config2,
|
|
1581
|
+
strategy: { ...DEFAULT_CONTEXT_CONFIG.strategy, ...config2?.strategy }
|
|
1049
1582
|
};
|
|
1050
1583
|
}
|
|
1051
1584
|
registerContext(args) {
|
|
@@ -1177,15 +1710,15 @@ class ContextEngine {
|
|
|
1177
1710
|
}
|
|
1178
1711
|
|
|
1179
1712
|
// src/config/loader.ts
|
|
1180
|
-
import * as
|
|
1181
|
-
import * as
|
|
1182
|
-
import * as
|
|
1183
|
-
var CONFIG_PATH =
|
|
1713
|
+
import * as fs2 from "node:fs";
|
|
1714
|
+
import * as path2 from "node:path";
|
|
1715
|
+
import * as os2 from "node:os";
|
|
1716
|
+
var CONFIG_PATH = path2.join(os2.homedir(), ".config", "opencode", "oh-my-opencode-cohub.json");
|
|
1184
1717
|
function loadCoHubConfig() {
|
|
1185
1718
|
try {
|
|
1186
|
-
if (!
|
|
1719
|
+
if (!fs2.existsSync(CONFIG_PATH))
|
|
1187
1720
|
return {};
|
|
1188
|
-
const raw =
|
|
1721
|
+
const raw = fs2.readFileSync(CONFIG_PATH, "utf-8");
|
|
1189
1722
|
return JSON.parse(raw);
|
|
1190
1723
|
} catch {
|
|
1191
1724
|
return {};
|
|
@@ -1193,8 +1726,8 @@ function loadCoHubConfig() {
|
|
|
1193
1726
|
}
|
|
1194
1727
|
|
|
1195
1728
|
// src/tools/council.ts
|
|
1196
|
-
import { tool
|
|
1197
|
-
var
|
|
1729
|
+
import { tool } from "@opencode-ai/plugin";
|
|
1730
|
+
var z = tool.schema;
|
|
1198
1731
|
|
|
1199
1732
|
class OperationTimeoutError extends Error {
|
|
1200
1733
|
constructor(message) {
|
|
@@ -1222,12 +1755,12 @@ async function abortSession(client, sessionId) {
|
|
|
1222
1755
|
await client.session.abort({ path: { id: sessionId } });
|
|
1223
1756
|
} catch {}
|
|
1224
1757
|
}
|
|
1225
|
-
async function promptWithTimeout(client,
|
|
1226
|
-
const sessionId =
|
|
1758
|
+
async function promptWithTimeout(client, path3, body, timeoutMs, directory) {
|
|
1759
|
+
const sessionId = path3.id;
|
|
1227
1760
|
let timer;
|
|
1228
1761
|
try {
|
|
1229
1762
|
const promptPromise = client.session.prompt({
|
|
1230
|
-
path:
|
|
1763
|
+
path: path3,
|
|
1231
1764
|
body,
|
|
1232
1765
|
query: directory ? { directory } : undefined
|
|
1233
1766
|
});
|
|
@@ -1340,10 +1873,10 @@ class CouncilManager {
|
|
|
1340
1873
|
client;
|
|
1341
1874
|
directory;
|
|
1342
1875
|
config;
|
|
1343
|
-
constructor(client, directory,
|
|
1876
|
+
constructor(client, directory, config2) {
|
|
1344
1877
|
this.client = client;
|
|
1345
1878
|
this.directory = directory;
|
|
1346
|
-
this.config =
|
|
1879
|
+
this.config = config2;
|
|
1347
1880
|
}
|
|
1348
1881
|
async runCouncil(prompt, presetName, parentSessionId) {
|
|
1349
1882
|
const resolvedPreset = presetName ?? this.config.default_preset ?? "default";
|
|
@@ -1386,12 +1919,12 @@ class CouncilManager {
|
|
|
1386
1919
|
const entries = Object.entries(councillors);
|
|
1387
1920
|
const results = [];
|
|
1388
1921
|
if (executionMode === "serial") {
|
|
1389
|
-
for (const [name,
|
|
1390
|
-
const r = await this.runCouncillorWithRetry(name,
|
|
1922
|
+
for (const [name, config2] of entries) {
|
|
1923
|
+
const r = await this.runCouncillorWithRetry(name, config2, prompt, parentSessionId, timeout, maxRetries);
|
|
1391
1924
|
results.push(r);
|
|
1392
1925
|
}
|
|
1393
1926
|
} else {
|
|
1394
|
-
const promises = entries.map(([name,
|
|
1927
|
+
const promises = entries.map(([name, config2]) => this.runCouncillorWithRetry(name, config2, prompt, parentSessionId, timeout, maxRetries));
|
|
1395
1928
|
const settled = await Promise.allSettled(promises);
|
|
1396
1929
|
for (let i = 0;i < settled.length; i++) {
|
|
1397
1930
|
const s = settled[i];
|
|
@@ -1410,19 +1943,19 @@ class CouncilManager {
|
|
|
1410
1943
|
}
|
|
1411
1944
|
return results;
|
|
1412
1945
|
}
|
|
1413
|
-
async runCouncillorWithRetry(name,
|
|
1946
|
+
async runCouncillorWithRetry(name, config2, prompt, parentSessionId, timeout, maxRetries) {
|
|
1414
1947
|
const totalAttempts = 1 + maxRetries;
|
|
1415
1948
|
for (let attempt = 1;attempt <= totalAttempts; attempt++) {
|
|
1416
1949
|
try {
|
|
1417
1950
|
const result = await this.runAgentSession({
|
|
1418
1951
|
parentSessionId,
|
|
1419
|
-
title: `Council ${name} (${shortModelLabel(
|
|
1420
|
-
model:
|
|
1421
|
-
promptText: formatCouncillorPrompt(prompt,
|
|
1422
|
-
variant:
|
|
1952
|
+
title: `Council ${name} (${shortModelLabel(config2.model)})`,
|
|
1953
|
+
model: config2.model,
|
|
1954
|
+
promptText: formatCouncillorPrompt(prompt, config2.prompt),
|
|
1955
|
+
variant: config2.variant,
|
|
1423
1956
|
timeout
|
|
1424
1957
|
});
|
|
1425
|
-
return { name, model:
|
|
1958
|
+
return { name, model: config2.model, status: "completed", result };
|
|
1426
1959
|
} catch (error) {
|
|
1427
1960
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1428
1961
|
const isEmptyResponse = msg.includes("Empty response from provider");
|
|
@@ -1430,7 +1963,7 @@ class CouncilManager {
|
|
|
1430
1963
|
if (!canRetry) {
|
|
1431
1964
|
return {
|
|
1432
1965
|
name,
|
|
1433
|
-
model:
|
|
1966
|
+
model: config2.model,
|
|
1434
1967
|
status: msg.includes("timed out") ? "timed_out" : "failed",
|
|
1435
1968
|
error: `Councillor "${name}": ${msg}`
|
|
1436
1969
|
};
|
|
@@ -1439,7 +1972,7 @@ class CouncilManager {
|
|
|
1439
1972
|
}
|
|
1440
1973
|
return {
|
|
1441
1974
|
name,
|
|
1442
|
-
model:
|
|
1975
|
+
model: config2.model,
|
|
1443
1976
|
status: "failed",
|
|
1444
1977
|
error: `Councillor "${name}": max retries exhausted`
|
|
1445
1978
|
};
|
|
@@ -1492,7 +2025,7 @@ class CouncilManager {
|
|
|
1492
2025
|
}
|
|
1493
2026
|
}
|
|
1494
2027
|
function createCouncilTool(ctx, councilManager) {
|
|
1495
|
-
const council_session =
|
|
2028
|
+
const council_session = tool({
|
|
1496
2029
|
description: [
|
|
1497
2030
|
"Launch a multi-LLM council session for consensus-based analysis.",
|
|
1498
2031
|
"",
|
|
@@ -1503,8 +2036,8 @@ function createCouncilTool(ctx, councilManager) {
|
|
|
1503
2036
|
].join(`
|
|
1504
2037
|
`),
|
|
1505
2038
|
args: {
|
|
1506
|
-
prompt:
|
|
1507
|
-
preset:
|
|
2039
|
+
prompt: z.string().describe("The prompt to send to all councillors"),
|
|
2040
|
+
preset: z.string().optional().describe('Council preset to use (default: "default"). Must match a preset in the council config.')
|
|
1508
2041
|
},
|
|
1509
2042
|
async execute(args, toolContext) {
|
|
1510
2043
|
const allowedAgents = ["co-council"];
|
|
@@ -1534,9 +2067,9 @@ function createCouncilTool(ctx, councilManager) {
|
|
|
1534
2067
|
}
|
|
1535
2068
|
|
|
1536
2069
|
// src/index.ts
|
|
1537
|
-
import * as
|
|
1538
|
-
import * as
|
|
1539
|
-
import * as
|
|
2070
|
+
import * as fs3 from "node:fs";
|
|
2071
|
+
import * as path3 from "node:path";
|
|
2072
|
+
import * as os3 from "node:os";
|
|
1540
2073
|
function loadFileOverrides(projectDir) {
|
|
1541
2074
|
const overrides = {};
|
|
1542
2075
|
const agentNames = [
|
|
@@ -1551,25 +2084,26 @@ function loadFileOverrides(projectDir) {
|
|
|
1551
2084
|
"co-rule-user",
|
|
1552
2085
|
"co-rule-project",
|
|
1553
2086
|
"co-rule-app",
|
|
1554
|
-
"co-planner"
|
|
2087
|
+
"co-planner",
|
|
2088
|
+
"co-guardian"
|
|
1555
2089
|
];
|
|
1556
2090
|
const searchDirs = [];
|
|
1557
2091
|
if (projectDir) {
|
|
1558
|
-
searchDirs.push(
|
|
2092
|
+
searchDirs.push(path3.join(projectDir, ".opencode", "oh-my-opencode-cohub"));
|
|
1559
2093
|
}
|
|
1560
|
-
searchDirs.push(
|
|
2094
|
+
searchDirs.push(path3.join(os3.homedir(), ".config", "opencode", "oh-my-opencode-cohub"));
|
|
1561
2095
|
for (const agent of agentNames) {
|
|
1562
2096
|
for (const dir of searchDirs) {
|
|
1563
|
-
const replacePath =
|
|
1564
|
-
if (!overrides[agent]?.replace &&
|
|
2097
|
+
const replacePath = path3.join(dir, `${agent}.md`);
|
|
2098
|
+
if (!overrides[agent]?.replace && fs3.existsSync(replacePath)) {
|
|
1565
2099
|
try {
|
|
1566
|
-
overrides[agent] = { ...overrides[agent], replace:
|
|
2100
|
+
overrides[agent] = { ...overrides[agent], replace: fs3.readFileSync(replacePath, "utf-8") };
|
|
1567
2101
|
} catch {}
|
|
1568
2102
|
}
|
|
1569
|
-
const appendPath =
|
|
1570
|
-
if (!overrides[agent]?.append &&
|
|
2103
|
+
const appendPath = path3.join(dir, `${agent}_append.md`);
|
|
2104
|
+
if (!overrides[agent]?.append && fs3.existsSync(appendPath)) {
|
|
1571
2105
|
try {
|
|
1572
|
-
overrides[agent] = { ...overrides[agent], append:
|
|
2106
|
+
overrides[agent] = { ...overrides[agent], append: fs3.readFileSync(appendPath, "utf-8") };
|
|
1573
2107
|
} catch {}
|
|
1574
2108
|
}
|
|
1575
2109
|
}
|
|
@@ -1598,28 +2132,26 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1598
2132
|
}
|
|
1599
2133
|
const promptOverrides = { ...configOverrides, ...fileOverrides };
|
|
1600
2134
|
const tracker = new TaskTracker;
|
|
1601
|
-
const
|
|
1602
|
-
const
|
|
1603
|
-
const STATE_DIR = path2.join(os2.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
|
|
1604
|
-
const STATE_FILE = path2.join(STATE_DIR, "tracker-state.json");
|
|
2135
|
+
const STATE_DIR = path3.join(os3.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
|
|
2136
|
+
const STATE_FILE = path3.join(STATE_DIR, "tracker-state.json");
|
|
1605
2137
|
function syncTrackerState(sessionId) {
|
|
1606
2138
|
try {
|
|
1607
|
-
if (!
|
|
1608
|
-
|
|
2139
|
+
if (!fs3.existsSync(STATE_DIR)) {
|
|
2140
|
+
fs3.mkdirSync(STATE_DIR, { recursive: true });
|
|
1609
2141
|
}
|
|
1610
2142
|
const state = {
|
|
1611
2143
|
updatedAt: Date.now(),
|
|
1612
2144
|
runningAgents: tracker.getRunningAgents(sessionId),
|
|
1613
2145
|
runningCount: tracker.getRunningCount(sessionId)
|
|
1614
2146
|
};
|
|
1615
|
-
|
|
2147
|
+
fs3.writeFileSync(STATE_FILE, JSON.stringify(state), "utf-8");
|
|
1616
2148
|
} catch {}
|
|
1617
2149
|
}
|
|
1618
|
-
const AGENT_CONFIG_FILE =
|
|
2150
|
+
const AGENT_CONFIG_FILE = path3.join(STATE_DIR, "cohub-state.json");
|
|
1619
2151
|
function syncAgentConfig() {
|
|
1620
2152
|
try {
|
|
1621
|
-
if (!
|
|
1622
|
-
|
|
2153
|
+
if (!fs3.existsSync(STATE_DIR)) {
|
|
2154
|
+
fs3.mkdirSync(STATE_DIR, { recursive: true });
|
|
1623
2155
|
}
|
|
1624
2156
|
const configs = agents.map((a) => {
|
|
1625
2157
|
const modelStr = a.config.model;
|
|
@@ -1634,15 +2166,13 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1634
2166
|
provider
|
|
1635
2167
|
};
|
|
1636
2168
|
});
|
|
1637
|
-
|
|
2169
|
+
fs3.writeFileSync(AGENT_CONFIG_FILE, JSON.stringify({ updatedAt: Date.now(), agents: configs }), "utf-8");
|
|
1638
2170
|
} catch {}
|
|
1639
2171
|
}
|
|
1640
2172
|
const agents = [
|
|
1641
2173
|
{
|
|
1642
2174
|
name: "co-orchestrator",
|
|
1643
|
-
config: { mode: "primary", model: "deepseek/deepseek-v4-pro", variant: "max", prompt: ORCHESTRATOR_PROMPT
|
|
1644
|
-
|
|
1645
|
-
` + CHINESE_LANGUAGE_INSTRUCTION }
|
|
2175
|
+
config: { mode: "primary", model: "deepseek/deepseek-v4-pro", variant: "max", prompt: ORCHESTRATOR_PROMPT }
|
|
1646
2176
|
},
|
|
1647
2177
|
{
|
|
1648
2178
|
name: "co-oracle",
|
|
@@ -1701,6 +2231,11 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1701
2231
|
name: "co-planner",
|
|
1702
2232
|
description: "方案制定——综合需求+信息+规范输出任务分解",
|
|
1703
2233
|
config: { mode: "subagent", model: "deepseek/deepseek-v4-pro", variant: "high", prompt: PLANNER_PROMPT }
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
name: "co-guardian",
|
|
2237
|
+
description: "上下文卫士——分析会话状态并推荐上下文处理策略",
|
|
2238
|
+
config: { mode: "subagent", model: "deepseek/deepseek-v4-flash", prompt: GUARDIAN_PROMPT }
|
|
1704
2239
|
}
|
|
1705
2240
|
];
|
|
1706
2241
|
const userConfig = loadCoHubConfig();
|
|
@@ -1741,6 +2276,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1741
2276
|
const councilConfig = userConfig.council ?? DEFAULT_COUNCIL_CONFIG;
|
|
1742
2277
|
const councilManager = new CouncilManager(input.client, input.directory, councilConfig);
|
|
1743
2278
|
const councilTools = createCouncilTool(input, councilManager);
|
|
2279
|
+
const contextGuard = initContextGuard(input.client);
|
|
1744
2280
|
syncAgentConfig();
|
|
1745
2281
|
function extractChildSessionId(output) {
|
|
1746
2282
|
if (!output || typeof output !== "object")
|
|
@@ -1798,26 +2334,18 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1798
2334
|
return {
|
|
1799
2335
|
name: "oh-my-opencode-cohub",
|
|
1800
2336
|
agent: agentConfigs,
|
|
1801
|
-
tool:
|
|
2337
|
+
tool: councilTools,
|
|
1802
2338
|
config: async (cfg) => {
|
|
1803
2339
|
const c = cfg;
|
|
1804
2340
|
c.agent ??= {};
|
|
1805
|
-
for (const [name,
|
|
1806
|
-
c.agent[name] =
|
|
2341
|
+
for (const [name, config2] of Object.entries(agentConfigs)) {
|
|
2342
|
+
c.agent[name] = config2;
|
|
1807
2343
|
}
|
|
1808
2344
|
try {
|
|
1809
|
-
|
|
2345
|
+
fs3.writeFileSync(path3.join(STATE_DIR, "config-hook-ran.json"), JSON.stringify({ ran: true, count: agents.length }));
|
|
1810
2346
|
} catch {}
|
|
1811
2347
|
},
|
|
1812
2348
|
"tool.execute.before": async (input2, output) => {
|
|
1813
|
-
if (input2.tool === "task") {
|
|
1814
|
-
const beforeArgs = output.args ?? {};
|
|
1815
|
-
const beforeSubagentType = typeof beforeArgs.subagent_type === "string" ? beforeArgs.subagent_type : "";
|
|
1816
|
-
if ((beforeSubagentType === "co-fixer" || beforeSubagentType === "co-designer") && !planManager.isApproved(input2.sessionID)) {
|
|
1817
|
-
throw new Error(`[CoHub 方案批准门禁] 当前 session 尚未通过方案批准。
|
|
1818
|
-
` + `请先:1) 输出可验证方案 → 2) todowrite 记录 → 3) 调用 request_plan_approval 弹出确认框 → ` + `4) 用户在弹窗中允许后,才能委派 ${beforeSubagentType}。`);
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
2349
|
try {
|
|
1822
2350
|
if (input2.tool === "task") {
|
|
1823
2351
|
const args = output.args ?? {};
|
|
@@ -1852,19 +2380,19 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1852
2380
|
if (details) {
|
|
1853
2381
|
output.args[targetField] += details;
|
|
1854
2382
|
}
|
|
1855
|
-
const logPath =
|
|
2383
|
+
const logPath = path3.join(os3.tmpdir(), "opencode", "ctx-diag.log");
|
|
1856
2384
|
try {
|
|
1857
|
-
const stat =
|
|
2385
|
+
const stat = fs3.statSync(logPath);
|
|
1858
2386
|
if (stat.size > 50 * 1024) {
|
|
1859
|
-
const lines =
|
|
2387
|
+
const lines = fs3.readFileSync(logPath, "utf-8").trim().split(`
|
|
1860
2388
|
`);
|
|
1861
|
-
|
|
2389
|
+
fs3.writeFileSync(logPath, lines.slice(-30).join(`
|
|
1862
2390
|
`) + `
|
|
1863
2391
|
`);
|
|
1864
2392
|
}
|
|
1865
2393
|
} catch {}
|
|
1866
2394
|
const finalPrompt = typeof output.args?.prompt === "string" ? output.args.prompt : "";
|
|
1867
|
-
|
|
2395
|
+
fs3.appendFileSync(logPath, JSON.stringify({
|
|
1868
2396
|
time: new Date().toISOString(),
|
|
1869
2397
|
session: input2.sessionID?.slice(0, 20) ?? "?",
|
|
1870
2398
|
subagent: subagentType,
|
|
@@ -1900,6 +2428,9 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1900
2428
|
} catch {}
|
|
1901
2429
|
},
|
|
1902
2430
|
event: async (input2) => {
|
|
2431
|
+
try {
|
|
2432
|
+
await contextGuard.event(input2);
|
|
2433
|
+
} catch {}
|
|
1903
2434
|
try {
|
|
1904
2435
|
const e = input2.event;
|
|
1905
2436
|
const sessionId = extractSessionIdFromEvent(e.properties);
|
|
@@ -1915,56 +2446,66 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1915
2446
|
} else if (e.type === "session.deleted") {
|
|
1916
2447
|
tracker.updateByChildSessionId(sessionId, "errored");
|
|
1917
2448
|
syncTrackerState(tracker.currentParentSessionId);
|
|
1918
|
-
planManager.cleanup(sessionId);
|
|
1919
2449
|
} else if (e.type === "session.error") {
|
|
1920
2450
|
tracker.updateByChildSessionId(sessionId, "errored");
|
|
1921
2451
|
syncTrackerState(tracker.currentParentSessionId);
|
|
1922
2452
|
}
|
|
1923
2453
|
} catch {}
|
|
1924
2454
|
},
|
|
1925
|
-
"chat.message": async (input2) => {
|
|
1926
|
-
try {
|
|
1927
|
-
planManager.observeUserMessage(input2.sessionID, input2.agent);
|
|
1928
|
-
} catch (err) {
|
|
1929
|
-
console.warn("[oh-my-opencode-cohub] chat.message hook 失败:", err);
|
|
1930
|
-
}
|
|
1931
|
-
},
|
|
1932
2455
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
1933
2456
|
try {
|
|
2457
|
+
if (!output.messages || !Array.isArray(output.messages))
|
|
2458
|
+
return;
|
|
2459
|
+
let lastUserMsg;
|
|
2460
|
+
let sessionID;
|
|
2461
|
+
for (let i = output.messages.length - 1;i >= 0; i--) {
|
|
2462
|
+
const m = output.messages[i];
|
|
2463
|
+
if (m.info.role === "user") {
|
|
2464
|
+
lastUserMsg = m;
|
|
2465
|
+
sessionID = m.info?.sessionID;
|
|
2466
|
+
break;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
if (!lastUserMsg)
|
|
2470
|
+
return;
|
|
1934
2471
|
const board = tracker.getBoardText();
|
|
1935
|
-
if (board
|
|
1936
|
-
for (let
|
|
1937
|
-
const
|
|
1938
|
-
if (
|
|
1939
|
-
|
|
1940
|
-
const part = msg.parts[j];
|
|
1941
|
-
if (part.type === "text") {
|
|
1942
|
-
part.text += `
|
|
2472
|
+
if (board) {
|
|
2473
|
+
for (let j = lastUserMsg.parts.length - 1;j >= 0; j--) {
|
|
2474
|
+
const part = lastUserMsg.parts[j];
|
|
2475
|
+
if (part.type === "text") {
|
|
2476
|
+
part.text += `
|
|
1943
2477
|
|
|
1944
2478
|
` + board;
|
|
1945
|
-
break;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
2479
|
break;
|
|
1949
2480
|
}
|
|
1950
2481
|
}
|
|
1951
2482
|
}
|
|
2483
|
+
try {
|
|
2484
|
+
const guardTransform = contextGuard["experimental.chat.messages.transform"];
|
|
2485
|
+
if (guardTransform)
|
|
2486
|
+
await guardTransform(_input, output);
|
|
2487
|
+
} catch {}
|
|
1952
2488
|
} catch (err) {
|
|
1953
2489
|
console.warn("[oh-my-opencode-cohub] messages.transform hook 失败:", err);
|
|
1954
2490
|
}
|
|
1955
2491
|
},
|
|
1956
2492
|
"experimental.chat.system.transform": async (input2, output) => {
|
|
1957
2493
|
output.system.push(CHINESE_LANGUAGE_INSTRUCTION);
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2494
|
+
},
|
|
2495
|
+
"chat.params": async (input2) => {
|
|
2496
|
+
try {
|
|
2497
|
+
await contextGuard["chat.params"](input2);
|
|
2498
|
+
} catch {}
|
|
2499
|
+
},
|
|
2500
|
+
"chat.message": async (input2, output) => {
|
|
2501
|
+
try {
|
|
2502
|
+
await contextGuard["chat.message"](input2, output);
|
|
2503
|
+
} catch {}
|
|
1964
2504
|
},
|
|
1965
2505
|
dispose: async () => {
|
|
1966
2506
|
clearInterval(cleanupTimer);
|
|
1967
2507
|
clearInterval(contextCleanupTimer);
|
|
2508
|
+
cleanupAllStates();
|
|
1968
2509
|
}
|
|
1969
2510
|
};
|
|
1970
2511
|
};
|