oh-my-opencode-cohub 1.7.0 → 1.8.1
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 +6 -4
- 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 +757 -372
- 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-audit.d.ts +0 -50
- package/dist/plan-gate-audit.d.ts.map +0 -1
- package/dist/plan-gate-audit.test.d.ts +0 -2
- package/dist/plan-gate-audit.test.d.ts.map +0 -1
- package/dist/plan-gate.d.ts +0 -77
- 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 调用 |
|
|
34
|
-
|
|
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 | 需要多方面权衡 |
|
|
24
|
+
**一句话判断**:\`@co-oracle\` = 深度推理(快、便宜、可逆判断),\`@co-council\` = 多模型背书共识(慢、贵、不可逆决策)。
|
|
57
25
|
|
|
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,233 +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
|
-
audit;
|
|
677
|
-
constructor(audit) {
|
|
678
|
-
this.audit = audit;
|
|
679
|
-
}
|
|
680
|
-
getGeneration(sessionID) {
|
|
681
|
-
return this.sessions.get(sessionID)?.generation ?? 0;
|
|
682
|
-
}
|
|
683
|
-
observeUserMessage(sessionID, agent) {
|
|
684
|
-
if (agent !== "co-orchestrator") {
|
|
685
|
-
if (!this.sessions.has(sessionID)) {
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
const state = this.sessions.get(sessionID) ?? { generation: 0 };
|
|
690
|
-
const oldApprovedGeneration = state.approvedGeneration;
|
|
691
|
-
state.generation += 1;
|
|
692
|
-
delete state.approvedGeneration;
|
|
693
|
-
delete state.plan;
|
|
694
|
-
this.sessions.set(sessionID, state);
|
|
695
|
-
this.audit?.record({
|
|
696
|
-
event: "revoked",
|
|
697
|
-
session: sessionID.slice(0, 20),
|
|
698
|
-
generation: state.generation,
|
|
699
|
-
revoked: oldApprovedGeneration,
|
|
700
|
-
agent: agent && agent !== "co-orchestrator" ? agent : undefined
|
|
701
|
-
});
|
|
702
|
-
return true;
|
|
703
|
-
}
|
|
704
|
-
approve(sessionID, plan) {
|
|
705
|
-
const state = this.sessions.get(sessionID);
|
|
706
|
-
if (!state) {
|
|
707
|
-
throw new Error(`Session "${sessionID.slice(0, 20)}..." 尚未被 PlanGate 观察,无法批准。` + "只有 co-orchestrator 的 session 才能建立批准状态。");
|
|
708
|
-
}
|
|
709
|
-
if (state.generation === 0) {
|
|
710
|
-
throw new Error("无法批准 generation 0——session 尚未处理任何用户消息。");
|
|
711
|
-
}
|
|
712
|
-
state.approvedGeneration = state.generation;
|
|
713
|
-
state.plan = plan;
|
|
714
|
-
this.audit?.record({
|
|
715
|
-
event: "approved",
|
|
716
|
-
session: sessionID.slice(0, 20),
|
|
717
|
-
generation: state.generation,
|
|
718
|
-
fileCount: plan.files.length,
|
|
719
|
-
summaryLen: plan.summary.length
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
isApproved(sessionID) {
|
|
723
|
-
const state = this.sessions.get(sessionID);
|
|
724
|
-
if (!state)
|
|
725
|
-
return false;
|
|
726
|
-
return state.approvedGeneration === state.generation && state.generation > 0;
|
|
727
|
-
}
|
|
728
|
-
getSystemContext(sessionID) {
|
|
729
|
-
const state = this.sessions.get(sessionID);
|
|
730
|
-
if (!state)
|
|
731
|
-
return "";
|
|
732
|
-
const approved = this.isApproved(sessionID);
|
|
733
|
-
return `<plan_gate generation="${state.generation}" approved="${approved}">
|
|
734
|
-
` + ` generation: ${state.generation}
|
|
735
|
-
` + ` approved: ${approved}
|
|
736
|
-
` + (state.plan ? ` plan_summary: ${state.plan.summary.slice(0, 120)}
|
|
737
|
-
` + ` plan_files: ${state.plan.files.slice(0, 5).join(", ")}${state.plan.files.length > 5 ? "..." : ""}
|
|
738
|
-
` : "") + `</plan_gate>`;
|
|
739
|
-
}
|
|
740
|
-
getInjectionText(sessionID) {
|
|
741
|
-
const state = this.sessions.get(sessionID);
|
|
742
|
-
if (!state || state.generation === 0)
|
|
743
|
-
return null;
|
|
744
|
-
const approved = this.isApproved(sessionID);
|
|
745
|
-
const status = approved ? "已批准" : "未批准";
|
|
746
|
-
const lines = [];
|
|
747
|
-
lines.push("");
|
|
748
|
-
lines.push("--- 行为准则(本次 LLM 请求注入,不持久化) ---");
|
|
749
|
-
lines.push(`当前 generation: ${state.generation},方案状态: ${status}`);
|
|
750
|
-
lines.push("");
|
|
751
|
-
lines.push("核心规则:");
|
|
752
|
-
lines.push("1. 需要改代码 -> 先输出方案 -> todowrite -> request_plan_approval -> 等弹窗确认");
|
|
753
|
-
lines.push("2. 文件操作 -> 委派子代理,禁止亲自使用 read/grep/edit/bash/write");
|
|
754
|
-
lines.push("3. 未批准方案时禁止委派 co-fixer / co-designer");
|
|
755
|
-
lines.push("4. 每个新用户消息自动撤销旧批准");
|
|
756
|
-
lines.push("");
|
|
757
|
-
if (approved) {
|
|
758
|
-
lines.push("可委派 co-fixer / co-designer");
|
|
759
|
-
lines.push("");
|
|
760
|
-
}
|
|
761
|
-
lines.push("自检:需要改代码?-> 先方案 委派 fixer?-> 查状态锁");
|
|
762
|
-
lines.push("--- 注入结束 ---");
|
|
763
|
-
return lines.join(`
|
|
764
|
-
`);
|
|
765
|
-
}
|
|
766
|
-
cleanup(sessionID) {
|
|
767
|
-
this.sessions.delete(sessionID);
|
|
768
|
-
}
|
|
769
|
-
get activeSessionCount() {
|
|
770
|
-
return this.sessions.size;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
var z = tool.schema;
|
|
774
|
-
function createRequestPlanApprovalTool(planManager) {
|
|
775
|
-
const request_plan_approval = tool({
|
|
776
|
-
description: [
|
|
777
|
-
"请求用户批准当前执行方案。",
|
|
778
|
-
"",
|
|
779
|
-
"输出方案后调用此工具,会弹出原生确认框让用户确认。",
|
|
780
|
-
"只有用户确认后,co-fixer 和 co-designer 等可写代理才能执行。",
|
|
781
|
-
"新用户消息会自动撤销之前的批准,需要重新申请。",
|
|
782
|
-
"",
|
|
783
|
-
"调用前确保已向用户展示了完整的任务分解方案。"
|
|
784
|
-
].join(`
|
|
785
|
-
`),
|
|
786
|
-
args: {
|
|
787
|
-
summary: z.string().describe("方案摘要:简要说明做什么、为什么"),
|
|
788
|
-
files: z.array(z.string()).describe("涉及的文件列表(相对路径)"),
|
|
789
|
-
verification: z.string().describe("验证方式:如何确认变更正确(如编译、测试、审查)")
|
|
790
|
-
},
|
|
791
|
-
async execute(args, toolContext) {
|
|
792
|
-
const sessionID = toolContext.sessionID;
|
|
793
|
-
const summary = String(args.summary);
|
|
794
|
-
const files = Array.isArray(args.files) ? args.files.map(String) : [];
|
|
795
|
-
const verification = String(args.verification);
|
|
796
|
-
const truncatedSummary = summary.length > 200 ? summary.slice(0, 200) + "..." : summary;
|
|
797
|
-
const truncatedFiles = files.length > 8 ? [...files.slice(0, 8), `...等 ${files.length} 个文件`] : files;
|
|
798
|
-
const truncatedVerification = verification.length > 150 ? verification.slice(0, 150) + "..." : verification;
|
|
799
|
-
const currentGen = planManager.getGeneration(sessionID);
|
|
800
|
-
planManager.audit?.record({
|
|
801
|
-
event: "approval_requested",
|
|
802
|
-
session: sessionID.slice(0, 20),
|
|
803
|
-
generation: currentGen,
|
|
804
|
-
agent: "co-orchestrator",
|
|
805
|
-
fileCount: files.length,
|
|
806
|
-
summaryLen: summary.length
|
|
807
|
-
});
|
|
808
|
-
try {
|
|
809
|
-
await toolContext.ask({
|
|
810
|
-
permission: "plan-execute",
|
|
811
|
-
always: [],
|
|
812
|
-
patterns: [
|
|
813
|
-
`\uD83D\uDCCB 方案: ${truncatedSummary}`,
|
|
814
|
-
`\uD83D\uDCC1 文件 (${files.length}个): ${truncatedFiles.join(", ")}`,
|
|
815
|
-
`\uD83D\uDD0D 验证: ${truncatedVerification}`
|
|
816
|
-
],
|
|
817
|
-
metadata: {
|
|
818
|
-
session_id: sessionID,
|
|
819
|
-
generation: String(currentGen),
|
|
820
|
-
summary,
|
|
821
|
-
file_count: String(files.length)
|
|
822
|
-
}
|
|
823
|
-
});
|
|
824
|
-
} catch (err) {
|
|
825
|
-
planManager.audit?.record({
|
|
826
|
-
event: "rejected",
|
|
827
|
-
session: sessionID.slice(0, 20),
|
|
828
|
-
generation: currentGen,
|
|
829
|
-
reason: "permission_rejected"
|
|
830
|
-
});
|
|
831
|
-
throw err;
|
|
832
|
-
}
|
|
833
|
-
planManager.approve(sessionID, { summary, files, verification });
|
|
834
|
-
return `✅ 方案已批准(generation ${currentGen})。
|
|
835
|
-
` + `现在可以委派 @co-fixer / @co-designer 执行。
|
|
836
|
-
` + `\uD83D\uDCCB ${summary}
|
|
837
|
-
` + `\uD83D\uDCC1 ${files.length} 个文件
|
|
838
|
-
` + `\uD83D\uDD0D ${verification}`;
|
|
839
|
-
}
|
|
840
|
-
});
|
|
841
|
-
return { request_plan_approval };
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// src/plan-gate-audit.ts
|
|
845
|
-
import * as fs from "node:fs";
|
|
846
|
-
import * as path from "node:path";
|
|
847
|
-
|
|
848
|
-
class BoundedPlanGateAudit {
|
|
849
|
-
events = [];
|
|
850
|
-
logPath;
|
|
851
|
-
maxEvents = 50;
|
|
852
|
-
constructor(logPath) {
|
|
853
|
-
this.logPath = logPath;
|
|
854
|
-
this.restore();
|
|
855
|
-
}
|
|
856
|
-
restore() {
|
|
857
|
-
try {
|
|
858
|
-
const data = fs.readFileSync(this.logPath, "utf-8");
|
|
859
|
-
const parsed = JSON.parse(data);
|
|
860
|
-
if (parsed && parsed.version === 1 && Array.isArray(parsed.events)) {
|
|
861
|
-
this.events = parsed.events;
|
|
862
|
-
}
|
|
863
|
-
} catch {
|
|
864
|
-
this.events = [];
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
record(e) {
|
|
868
|
-
try {
|
|
869
|
-
const event = { ...e, at: new Date().toISOString() };
|
|
870
|
-
this.events.push(event);
|
|
871
|
-
if (this.events.length > this.maxEvents) {
|
|
872
|
-
this.events = this.events.slice(-this.maxEvents);
|
|
873
|
-
}
|
|
874
|
-
this.flush();
|
|
875
|
-
} catch (err) {
|
|
876
|
-
console.warn("[BoundedPlanGateAudit] record 失败:", err);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
flush() {
|
|
880
|
-
try {
|
|
881
|
-
const data = JSON.stringify({ version: 1, events: this.events });
|
|
882
|
-
const tmpPath = this.logPath + ".tmp";
|
|
883
|
-
const dir = path.dirname(this.logPath);
|
|
884
|
-
if (!fs.existsSync(dir)) {
|
|
885
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
886
|
-
}
|
|
887
|
-
fs.writeFileSync(tmpPath, data, "utf-8");
|
|
888
|
-
try {
|
|
889
|
-
fs.unlinkSync(this.logPath);
|
|
890
|
-
} catch {}
|
|
891
|
-
fs.renameSync(tmpPath, this.logPath);
|
|
892
|
-
} catch (err) {
|
|
893
|
-
console.warn("[BoundedPlanGateAudit] flush 失败:", err);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
1310
|
// src/context/types.ts
|
|
899
1311
|
var DEFAULT_CONTEXT_CONFIG = {
|
|
900
1312
|
strategy: {
|
|
@@ -1161,12 +1573,12 @@ class ContextEngine {
|
|
|
1161
1573
|
dependencyCache = new Map;
|
|
1162
1574
|
client;
|
|
1163
1575
|
config;
|
|
1164
|
-
constructor(client,
|
|
1576
|
+
constructor(client, config2) {
|
|
1165
1577
|
this.client = client;
|
|
1166
1578
|
this.config = {
|
|
1167
1579
|
...DEFAULT_CONTEXT_CONFIG,
|
|
1168
|
-
...
|
|
1169
|
-
strategy: { ...DEFAULT_CONTEXT_CONFIG.strategy, ...
|
|
1580
|
+
...config2,
|
|
1581
|
+
strategy: { ...DEFAULT_CONTEXT_CONFIG.strategy, ...config2?.strategy }
|
|
1170
1582
|
};
|
|
1171
1583
|
}
|
|
1172
1584
|
registerContext(args) {
|
|
@@ -1300,8 +1712,8 @@ class ContextEngine {
|
|
|
1300
1712
|
// src/config/loader.ts
|
|
1301
1713
|
import * as fs2 from "node:fs";
|
|
1302
1714
|
import * as path2 from "node:path";
|
|
1303
|
-
import * as
|
|
1304
|
-
var CONFIG_PATH = path2.join(
|
|
1715
|
+
import * as os2 from "node:os";
|
|
1716
|
+
var CONFIG_PATH = path2.join(os2.homedir(), ".config", "opencode", "oh-my-opencode-cohub.json");
|
|
1305
1717
|
function loadCoHubConfig() {
|
|
1306
1718
|
try {
|
|
1307
1719
|
if (!fs2.existsSync(CONFIG_PATH))
|
|
@@ -1314,8 +1726,8 @@ function loadCoHubConfig() {
|
|
|
1314
1726
|
}
|
|
1315
1727
|
|
|
1316
1728
|
// src/tools/council.ts
|
|
1317
|
-
import { tool
|
|
1318
|
-
var
|
|
1729
|
+
import { tool } from "@opencode-ai/plugin";
|
|
1730
|
+
var z = tool.schema;
|
|
1319
1731
|
|
|
1320
1732
|
class OperationTimeoutError extends Error {
|
|
1321
1733
|
constructor(message) {
|
|
@@ -1461,10 +1873,10 @@ class CouncilManager {
|
|
|
1461
1873
|
client;
|
|
1462
1874
|
directory;
|
|
1463
1875
|
config;
|
|
1464
|
-
constructor(client, directory,
|
|
1876
|
+
constructor(client, directory, config2) {
|
|
1465
1877
|
this.client = client;
|
|
1466
1878
|
this.directory = directory;
|
|
1467
|
-
this.config =
|
|
1879
|
+
this.config = config2;
|
|
1468
1880
|
}
|
|
1469
1881
|
async runCouncil(prompt, presetName, parentSessionId) {
|
|
1470
1882
|
const resolvedPreset = presetName ?? this.config.default_preset ?? "default";
|
|
@@ -1507,12 +1919,12 @@ class CouncilManager {
|
|
|
1507
1919
|
const entries = Object.entries(councillors);
|
|
1508
1920
|
const results = [];
|
|
1509
1921
|
if (executionMode === "serial") {
|
|
1510
|
-
for (const [name,
|
|
1511
|
-
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);
|
|
1512
1924
|
results.push(r);
|
|
1513
1925
|
}
|
|
1514
1926
|
} else {
|
|
1515
|
-
const promises = entries.map(([name,
|
|
1927
|
+
const promises = entries.map(([name, config2]) => this.runCouncillorWithRetry(name, config2, prompt, parentSessionId, timeout, maxRetries));
|
|
1516
1928
|
const settled = await Promise.allSettled(promises);
|
|
1517
1929
|
for (let i = 0;i < settled.length; i++) {
|
|
1518
1930
|
const s = settled[i];
|
|
@@ -1531,19 +1943,19 @@ class CouncilManager {
|
|
|
1531
1943
|
}
|
|
1532
1944
|
return results;
|
|
1533
1945
|
}
|
|
1534
|
-
async runCouncillorWithRetry(name,
|
|
1946
|
+
async runCouncillorWithRetry(name, config2, prompt, parentSessionId, timeout, maxRetries) {
|
|
1535
1947
|
const totalAttempts = 1 + maxRetries;
|
|
1536
1948
|
for (let attempt = 1;attempt <= totalAttempts; attempt++) {
|
|
1537
1949
|
try {
|
|
1538
1950
|
const result = await this.runAgentSession({
|
|
1539
1951
|
parentSessionId,
|
|
1540
|
-
title: `Council ${name} (${shortModelLabel(
|
|
1541
|
-
model:
|
|
1542
|
-
promptText: formatCouncillorPrompt(prompt,
|
|
1543
|
-
variant:
|
|
1952
|
+
title: `Council ${name} (${shortModelLabel(config2.model)})`,
|
|
1953
|
+
model: config2.model,
|
|
1954
|
+
promptText: formatCouncillorPrompt(prompt, config2.prompt),
|
|
1955
|
+
variant: config2.variant,
|
|
1544
1956
|
timeout
|
|
1545
1957
|
});
|
|
1546
|
-
return { name, model:
|
|
1958
|
+
return { name, model: config2.model, status: "completed", result };
|
|
1547
1959
|
} catch (error) {
|
|
1548
1960
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1549
1961
|
const isEmptyResponse = msg.includes("Empty response from provider");
|
|
@@ -1551,7 +1963,7 @@ class CouncilManager {
|
|
|
1551
1963
|
if (!canRetry) {
|
|
1552
1964
|
return {
|
|
1553
1965
|
name,
|
|
1554
|
-
model:
|
|
1966
|
+
model: config2.model,
|
|
1555
1967
|
status: msg.includes("timed out") ? "timed_out" : "failed",
|
|
1556
1968
|
error: `Councillor "${name}": ${msg}`
|
|
1557
1969
|
};
|
|
@@ -1560,7 +1972,7 @@ class CouncilManager {
|
|
|
1560
1972
|
}
|
|
1561
1973
|
return {
|
|
1562
1974
|
name,
|
|
1563
|
-
model:
|
|
1975
|
+
model: config2.model,
|
|
1564
1976
|
status: "failed",
|
|
1565
1977
|
error: `Councillor "${name}": max retries exhausted`
|
|
1566
1978
|
};
|
|
@@ -1613,7 +2025,7 @@ class CouncilManager {
|
|
|
1613
2025
|
}
|
|
1614
2026
|
}
|
|
1615
2027
|
function createCouncilTool(ctx, councilManager) {
|
|
1616
|
-
const council_session =
|
|
2028
|
+
const council_session = tool({
|
|
1617
2029
|
description: [
|
|
1618
2030
|
"Launch a multi-LLM council session for consensus-based analysis.",
|
|
1619
2031
|
"",
|
|
@@ -1624,8 +2036,8 @@ function createCouncilTool(ctx, councilManager) {
|
|
|
1624
2036
|
].join(`
|
|
1625
2037
|
`),
|
|
1626
2038
|
args: {
|
|
1627
|
-
prompt:
|
|
1628
|
-
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.')
|
|
1629
2041
|
},
|
|
1630
2042
|
async execute(args, toolContext) {
|
|
1631
2043
|
const allowedAgents = ["co-council"];
|
|
@@ -1657,7 +2069,7 @@ function createCouncilTool(ctx, councilManager) {
|
|
|
1657
2069
|
// src/index.ts
|
|
1658
2070
|
import * as fs3 from "node:fs";
|
|
1659
2071
|
import * as path3 from "node:path";
|
|
1660
|
-
import * as
|
|
2072
|
+
import * as os3 from "node:os";
|
|
1661
2073
|
function loadFileOverrides(projectDir) {
|
|
1662
2074
|
const overrides = {};
|
|
1663
2075
|
const agentNames = [
|
|
@@ -1672,13 +2084,14 @@ function loadFileOverrides(projectDir) {
|
|
|
1672
2084
|
"co-rule-user",
|
|
1673
2085
|
"co-rule-project",
|
|
1674
2086
|
"co-rule-app",
|
|
1675
|
-
"co-planner"
|
|
2087
|
+
"co-planner",
|
|
2088
|
+
"co-guardian"
|
|
1676
2089
|
];
|
|
1677
2090
|
const searchDirs = [];
|
|
1678
2091
|
if (projectDir) {
|
|
1679
2092
|
searchDirs.push(path3.join(projectDir, ".opencode", "oh-my-opencode-cohub"));
|
|
1680
2093
|
}
|
|
1681
|
-
searchDirs.push(path3.join(
|
|
2094
|
+
searchDirs.push(path3.join(os3.homedir(), ".config", "opencode", "oh-my-opencode-cohub"));
|
|
1682
2095
|
for (const agent of agentNames) {
|
|
1683
2096
|
for (const dir of searchDirs) {
|
|
1684
2097
|
const replacePath = path3.join(dir, `${agent}.md`);
|
|
@@ -1719,12 +2132,8 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1719
2132
|
}
|
|
1720
2133
|
const promptOverrides = { ...configOverrides, ...fileOverrides };
|
|
1721
2134
|
const tracker = new TaskTracker;
|
|
1722
|
-
const STATE_DIR = path3.join(
|
|
2135
|
+
const STATE_DIR = path3.join(os3.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
|
|
1723
2136
|
const STATE_FILE = path3.join(STATE_DIR, "tracker-state.json");
|
|
1724
|
-
const PLAN_GATE_AUDIT_PATH = path3.join(STATE_DIR, "plan-gate-audit.json");
|
|
1725
|
-
const planAudit = new BoundedPlanGateAudit(PLAN_GATE_AUDIT_PATH);
|
|
1726
|
-
const planManager = new PlanApprovalManager(planAudit);
|
|
1727
|
-
const planTools = createRequestPlanApprovalTool(planManager);
|
|
1728
2137
|
function syncTrackerState(sessionId) {
|
|
1729
2138
|
try {
|
|
1730
2139
|
if (!fs3.existsSync(STATE_DIR)) {
|
|
@@ -1763,9 +2172,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1763
2172
|
const agents = [
|
|
1764
2173
|
{
|
|
1765
2174
|
name: "co-orchestrator",
|
|
1766
|
-
config: { mode: "primary", model: "deepseek/deepseek-v4-pro", variant: "max", prompt: ORCHESTRATOR_PROMPT
|
|
1767
|
-
|
|
1768
|
-
` + CHINESE_LANGUAGE_INSTRUCTION }
|
|
2175
|
+
config: { mode: "primary", model: "deepseek/deepseek-v4-pro", variant: "max", prompt: ORCHESTRATOR_PROMPT }
|
|
1769
2176
|
},
|
|
1770
2177
|
{
|
|
1771
2178
|
name: "co-oracle",
|
|
@@ -1824,6 +2231,11 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1824
2231
|
name: "co-planner",
|
|
1825
2232
|
description: "方案制定——综合需求+信息+规范输出任务分解",
|
|
1826
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 }
|
|
1827
2239
|
}
|
|
1828
2240
|
];
|
|
1829
2241
|
const userConfig = loadCoHubConfig();
|
|
@@ -1864,6 +2276,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1864
2276
|
const councilConfig = userConfig.council ?? DEFAULT_COUNCIL_CONFIG;
|
|
1865
2277
|
const councilManager = new CouncilManager(input.client, input.directory, councilConfig);
|
|
1866
2278
|
const councilTools = createCouncilTool(input, councilManager);
|
|
2279
|
+
const contextGuard = initContextGuard(input.client);
|
|
1867
2280
|
syncAgentConfig();
|
|
1868
2281
|
function extractChildSessionId(output) {
|
|
1869
2282
|
if (!output || typeof output !== "object")
|
|
@@ -1921,41 +2334,18 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1921
2334
|
return {
|
|
1922
2335
|
name: "oh-my-opencode-cohub",
|
|
1923
2336
|
agent: agentConfigs,
|
|
1924
|
-
tool:
|
|
2337
|
+
tool: councilTools,
|
|
1925
2338
|
config: async (cfg) => {
|
|
1926
2339
|
const c = cfg;
|
|
1927
2340
|
c.agent ??= {};
|
|
1928
|
-
for (const [name,
|
|
1929
|
-
c.agent[name] =
|
|
2341
|
+
for (const [name, config2] of Object.entries(agentConfigs)) {
|
|
2342
|
+
c.agent[name] = config2;
|
|
1930
2343
|
}
|
|
1931
2344
|
try {
|
|
1932
2345
|
fs3.writeFileSync(path3.join(STATE_DIR, "config-hook-ran.json"), JSON.stringify({ ran: true, count: agents.length }));
|
|
1933
2346
|
} catch {}
|
|
1934
2347
|
},
|
|
1935
2348
|
"tool.execute.before": async (input2, output) => {
|
|
1936
|
-
if (input2.tool === "task") {
|
|
1937
|
-
const beforeArgs = output.args ?? {};
|
|
1938
|
-
const beforeSubagentType = typeof beforeArgs.subagent_type === "string" ? beforeArgs.subagent_type : "";
|
|
1939
|
-
if ((beforeSubagentType === "co-fixer" || beforeSubagentType === "co-designer") && !planManager.isApproved(input2.sessionID)) {
|
|
1940
|
-
planAudit.record({
|
|
1941
|
-
event: "task_denied",
|
|
1942
|
-
session: input2.sessionID?.slice(0, 20) ?? "?",
|
|
1943
|
-
generation: planManager.getGeneration(input2.sessionID),
|
|
1944
|
-
agent: beforeSubagentType,
|
|
1945
|
-
reason: "unapproved"
|
|
1946
|
-
});
|
|
1947
|
-
throw new Error(`[CoHub 方案批准门禁] 当前 session 尚未通过方案批准。
|
|
1948
|
-
` + `请先:1) 输出可验证方案 → 2) todowrite 记录 → 3) 调用 request_plan_approval 弹出确认框 → ` + `4) 用户在弹窗中允许后,才能委派 ${beforeSubagentType}。`);
|
|
1949
|
-
}
|
|
1950
|
-
if (beforeSubagentType === "co-fixer" || beforeSubagentType === "co-designer") {
|
|
1951
|
-
planAudit.record({
|
|
1952
|
-
event: "task_allowed",
|
|
1953
|
-
session: input2.sessionID?.slice(0, 20) ?? "?",
|
|
1954
|
-
generation: planManager.getGeneration(input2.sessionID),
|
|
1955
|
-
agent: beforeSubagentType
|
|
1956
|
-
});
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
2349
|
try {
|
|
1960
2350
|
if (input2.tool === "task") {
|
|
1961
2351
|
const args = output.args ?? {};
|
|
@@ -1990,7 +2380,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1990
2380
|
if (details) {
|
|
1991
2381
|
output.args[targetField] += details;
|
|
1992
2382
|
}
|
|
1993
|
-
const logPath = path3.join(
|
|
2383
|
+
const logPath = path3.join(os3.tmpdir(), "opencode", "ctx-diag.log");
|
|
1994
2384
|
try {
|
|
1995
2385
|
const stat = fs3.statSync(logPath);
|
|
1996
2386
|
if (stat.size > 50 * 1024) {
|
|
@@ -2038,6 +2428,9 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2038
2428
|
} catch {}
|
|
2039
2429
|
},
|
|
2040
2430
|
event: async (input2) => {
|
|
2431
|
+
try {
|
|
2432
|
+
await contextGuard.event(input2);
|
|
2433
|
+
} catch {}
|
|
2041
2434
|
try {
|
|
2042
2435
|
const e = input2.event;
|
|
2043
2436
|
const sessionId = extractSessionIdFromEvent(e.properties);
|
|
@@ -2053,20 +2446,12 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2053
2446
|
} else if (e.type === "session.deleted") {
|
|
2054
2447
|
tracker.updateByChildSessionId(sessionId, "errored");
|
|
2055
2448
|
syncTrackerState(tracker.currentParentSessionId);
|
|
2056
|
-
planManager.cleanup(sessionId);
|
|
2057
2449
|
} else if (e.type === "session.error") {
|
|
2058
2450
|
tracker.updateByChildSessionId(sessionId, "errored");
|
|
2059
2451
|
syncTrackerState(tracker.currentParentSessionId);
|
|
2060
2452
|
}
|
|
2061
2453
|
} catch {}
|
|
2062
2454
|
},
|
|
2063
|
-
"chat.message": async (input2) => {
|
|
2064
|
-
try {
|
|
2065
|
-
planManager.observeUserMessage(input2.sessionID, input2.agent);
|
|
2066
|
-
} catch (err) {
|
|
2067
|
-
console.warn("[oh-my-opencode-cohub] chat.message hook 失败:", err);
|
|
2068
|
-
}
|
|
2069
|
-
},
|
|
2070
2455
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
2071
2456
|
try {
|
|
2072
2457
|
if (!output.messages || !Array.isArray(output.messages))
|
|
@@ -2095,32 +2480,32 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2095
2480
|
}
|
|
2096
2481
|
}
|
|
2097
2482
|
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
part.text += planInject;
|
|
2104
|
-
break;
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2483
|
+
try {
|
|
2484
|
+
const guardTransform = contextGuard["experimental.chat.messages.transform"];
|
|
2485
|
+
if (guardTransform)
|
|
2486
|
+
await guardTransform(_input, output);
|
|
2487
|
+
} catch {}
|
|
2108
2488
|
} catch (err) {
|
|
2109
2489
|
console.warn("[oh-my-opencode-cohub] messages.transform hook 失败:", err);
|
|
2110
2490
|
}
|
|
2111
2491
|
},
|
|
2112
2492
|
"experimental.chat.system.transform": async (input2, output) => {
|
|
2113
2493
|
output.system.push(CHINESE_LANGUAGE_INSTRUCTION);
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
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 {}
|
|
2120
2504
|
},
|
|
2121
2505
|
dispose: async () => {
|
|
2122
2506
|
clearInterval(cleanupTimer);
|
|
2123
2507
|
clearInterval(contextCleanupTimer);
|
|
2508
|
+
cleanupAllStates();
|
|
2124
2509
|
}
|
|
2125
2510
|
};
|
|
2126
2511
|
};
|