@genee/omp-opsx-addon 0.2.0 → 0.3.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/index.ts +30 -2
- package/lib/agent-defs.ts +36 -0
- package/lib/reachability-probe.ts +1 -1
- package/lib/status-probe.ts +288 -0
- package/lib/system-prompt.ts +4 -4
- package/lib/unified-config.ts +6 -0
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -121,6 +121,7 @@ import { DIRECT_FETCHERS } from './lib/direct-fetchers.js';
|
|
|
121
121
|
import { createUsageWidget } from './lib/usage-widget.js';
|
|
122
122
|
import { renderUsageReports, type ConsumptionTrack } from './lib/usage-render.js';
|
|
123
123
|
import { markExhausted, markAvailable, getExhaustedProviders } from './lib/provider-status.js';
|
|
124
|
+
import { scheduleStatusProbes } from './lib/status-probe.js';
|
|
124
125
|
import {
|
|
125
126
|
startSharedPoller,
|
|
126
127
|
getUnconfirmedProviders,
|
|
@@ -794,9 +795,37 @@ export default (pi: ExtensionAPI) => {
|
|
|
794
795
|
// provider's aggregated consumption RATE (all sessions, main +
|
|
795
796
|
// subagent via cache backflow), and re-renders the widget. It never
|
|
796
797
|
// recomputes a selection, never applies overrides, never setModel.
|
|
798
|
+
const cfg = buildEffectiveConfig();
|
|
797
799
|
const onTick = () => {
|
|
800
|
+
// Fire-and-forget status probe: do not block the tick.
|
|
801
|
+
const { placeholderIds, active } = resolveActiveProviders();
|
|
802
|
+
let models: { id: string; provider: string; baseUrl?: string }[] = [];
|
|
803
|
+
try {
|
|
804
|
+
if (ctx.modelRegistry && typeof ctx.modelRegistry.getAvailable === 'function') {
|
|
805
|
+
models = ctx.modelRegistry.getAvailable() as { id: string; provider: string; baseUrl?: string }[];
|
|
806
|
+
}
|
|
807
|
+
} catch {
|
|
808
|
+
if (ctx.modelRegistry && typeof ctx.modelRegistry.getAll === 'function') {
|
|
809
|
+
try {
|
|
810
|
+
models = ctx.modelRegistry.getAll() as { id: string; provider: string; baseUrl?: string }[];
|
|
811
|
+
} catch {
|
|
812
|
+
models = [];
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
void scheduleStatusProbes({
|
|
817
|
+
placeholderIds,
|
|
818
|
+
models,
|
|
819
|
+
getApiKey,
|
|
820
|
+
config: {
|
|
821
|
+
enabled: cfg.status_probe_enabled,
|
|
822
|
+
ttlMs: cfg.probe_ttl_ms,
|
|
823
|
+
timeoutMs: cfg.probe_timeout_ms,
|
|
824
|
+
excluded: cfg.excluded_providers,
|
|
825
|
+
},
|
|
826
|
+
}).then((written) => { if (written > 0) render(); }).catch(() => {});
|
|
827
|
+
|
|
798
828
|
sessionUsageRecorder.flush();
|
|
799
|
-
const { active } = resolveActiveProviders();
|
|
800
829
|
// Waveform source = per-second consumption sequence (timestamped
|
|
801
830
|
// deltas aggregated across sessions). Sample EVERY elapsed second
|
|
802
831
|
// for EVERY active provider — zeros when a second had no
|
|
@@ -822,7 +851,6 @@ export default (pi: ExtensionAPI) => {
|
|
|
822
851
|
lastSampledSec = lastSec;
|
|
823
852
|
render();
|
|
824
853
|
};
|
|
825
|
-
const cfg = buildEffectiveConfig();
|
|
826
854
|
await startSharedPoller(auth, DIRECT_FETCHERS, getApiKey, {
|
|
827
855
|
onTick: claimsOwnership ? onTick : undefined,
|
|
828
856
|
ownsUi,
|
package/lib/agent-defs.ts
CHANGED
|
@@ -14,6 +14,12 @@ tools: read, bash, glob, grep${model ? `\nmodel: ${model}` : ''}
|
|
|
14
14
|
1. 从 prompt 或上下文确定要审查的变更
|
|
15
15
|
2. 读取 proposal、design 和 tasks 文件
|
|
16
16
|
3. 运行 \`git diff HEAD~1\` 或 \`git diff --stat\` 查看改动
|
|
17
|
+
## scratchpad.md 共享缓存
|
|
18
|
+
- 审查前 MUST 先读 \`openspec/changes/<name>/scratchpad.md\`(若存在),只读不改写(工具集无 write/edit)。
|
|
19
|
+
- 以「调研与设计」+「实现探索」两区累计的涉及文件 \`path:line\` 集合为代码审查与增量验证范围:Tier 1(typecheck/lint)与 Tier 3(E2E,如适用)聚焦涉及文件改动面;Tier 2 冒烟套件仍全量执行项目现有测试套件。
|
|
20
|
+
- 无 scratchpad.md 时回退:从 proposal/design/tasks 确定审查范围。
|
|
21
|
+
- 识别 \`- [R<n> coder] supersede:\` 条目:以紧随其后的新结论为权威,旧 \`path:line\` 不再作为验证范围;只读不改写。
|
|
22
|
+
- 发现 scratchpad.md 结论与代码现状不符时,作为 P0/P1 审查发现发回,不直接改写。
|
|
17
23
|
|
|
18
24
|
## 审查维度
|
|
19
25
|
1. **规范遵循**:是否精确实现了 proposal/design/tasks 定义的内容?有无缺失或偏离?
|
|
@@ -73,6 +79,11 @@ tools: read, bash, glob, grep${model ? `\nmodel: ${model}` : ''}
|
|
|
73
79
|
## 启动
|
|
74
80
|
1. 从 prompt 或上下文确定要审查的提案名称
|
|
75
81
|
2. 读取 proposal.md、design.md(如果存在)
|
|
82
|
+
## scratchpad.md 共享缓存
|
|
83
|
+
- 审查前 MUST 先读 \`openspec/changes/<name>/scratchpad.md\`(若存在),复用其中调研结论与涉及文件,避免重复 read/grep。
|
|
84
|
+
- 只读,不改写 scratchpad.md;你的工具集不含 write/edit,物理上无法写入。
|
|
85
|
+
- 识别 \`- [R<n> coder] supersede:\` 条目:以紧随其后的新结论为权威,旧结论不再作为调研依据;只读不改写。
|
|
86
|
+
- 发现的 P0/P1 与关注点写入审查报告,由主 agent 中转回流,不直接写 scratchpad.md。
|
|
76
87
|
|
|
77
88
|
## 审查维度
|
|
78
89
|
1. **完整性**:Why/What/Capabilities/Impact 是否齐全?有无缺失章节?
|
|
@@ -131,6 +142,19 @@ skill: openspec-apply-change
|
|
|
131
142
|
- 代码全部完成后将所有未完成项标记为 \`- [x]\`,在 ACTION 报告中说明变更已全部实现
|
|
132
143
|
- 不修改 \`proposal.md\`、\`design.md\` 或 \`.openspec.yaml\`
|
|
133
144
|
- 直接用 read/write/edit/bash 等内置工具实现;不要尝试调用 task 二次委派。
|
|
145
|
+
## scratchpad.md 共享缓存
|
|
146
|
+
- 开工前 MUST 读 \`openspec/changes/<name>/scratchpad.md\`(若存在),复用已记录结论,禁止重复探索。
|
|
147
|
+
- 交付前 MUST 将本轮新探索结论 append 到「实现探索」区(关键符号/数据流、新增涉及文件 \`path:line\`、验证/构建命令、已排除假设),标注 \`- [R<n> coder] <结论>\`。
|
|
148
|
+
- 收到主 agent 中转的 code-reviewer P0/P1 关注点时,开工时先将关注点 append 进「代码审查范围」区,再开始修复。
|
|
149
|
+
- 后续轮次只做增量:仅探索未记录的文件/符号/假设,不重复 read/grep 已记录内容;append-only,不得改写他人结论。
|
|
150
|
+
|
|
151
|
+
## scratchpad.md 与代码不一致时的两档处置
|
|
152
|
+
执行中发现代码现实与 scratchpad.md 记录不一致时,按两档客观判据处置;判据仅为「偏差是否影响任何 task 的前提或产出定义」,禁止主观估量「问题大小」:
|
|
153
|
+
- **档一:事实快照过期**——偏差不影响任何 task 的前提或产出定义(纯探索性信息:路径 / 行号 / 符号 / 命令漂移)。处置:继续执行当前任务,不终止;在「实现探索」区 append supersede 修正。
|
|
154
|
+
- **档二:契约动摇**——偏差导致 tasks / proposal / design / specs 的有效性存疑(前提不成立、产出定义变、代码现状与设计决策冲突)。处置:立即停止实现,输出 STATUS: blocked(无 SESSION)并说明冲突点,交由主 agent 裁决(改提案 / 确认「现状即新设计」/ 开新 change)。
|
|
155
|
+
- supersede 标注格式:\`- [R<n> coder] supersede: <旧结论摘要>\`,随后一行以 \`- [R<n> coder]\` 标注新结论;两条均落「实现探索」分区,旧结论保留(append-only),不删除。
|
|
156
|
+
- 交付摘要 SUMMARY 义务:本轮发生过 supersede 时,输出格式中的 SUMMARY 行 MUST 提及本次 supersede 清单。
|
|
157
|
+
|
|
134
158
|
## 验证(交付门槛)
|
|
135
159
|
- 交付前必须跑过 **scoped 到本次改动面** 的 lint、typecheck 与针对性单测(proposal 约定的测试必须通过);任一未通过不得交付,先修到通过再标记 ACTION: REVIEW_REQUIRED。
|
|
136
160
|
- 项目级全量校验(全仓库 lint/typecheck/\`bun test\`)不属于自验证门槛:若失败可归因于并发 sibling 的半成品改动(与本改动无关),把失败连同「已排查与本改动无关」的证据作为上下文上报(交付摘要中注明),不要被其阻塞;最终由 code-reviewer 的全局验证兜底确认。
|
|
@@ -148,6 +172,7 @@ ACTION: REVIEW_REQUIRED
|
|
|
148
172
|
CHANGE: <change-name 或 "general">
|
|
149
173
|
STATUS: success | partial | blocked
|
|
150
174
|
SESSION: <session_name>(仅 STATUS: blocked 且需 resume 时填写)
|
|
175
|
+
SUMMARY: <交付摘要;若本轮有 supersede,必须包含「supersede: <清单>」>
|
|
151
176
|
TASKS: N/M complete
|
|
152
177
|
FILES: <逗号分隔的修改/创建文件列表>
|
|
153
178
|
---
|
|
@@ -173,6 +198,17 @@ tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: ${model}` :
|
|
|
173
198
|
## 工作范围
|
|
174
199
|
- **只负责提案文档**:proposal.md、design.md、tasks.md、specs/(openspec/changes/ 目录下)
|
|
175
200
|
- 直接用内置工具;不要尝试调用 task 二次委派。
|
|
201
|
+
## scratchpad.md 共享缓存
|
|
202
|
+
变更目录下维护 \`openspec/changes/<name>/scratchpad.md\` 作四角色共享探索缓存(与 proposal.md/design.md/tasks.md 同级),固定四阶段分区:
|
|
203
|
+
- \`## 调研与设计\`:调研结论、涉及文件(\`path:line\`)、已排除方案
|
|
204
|
+
- \`## 提案审查关注点\`:proposal-reviewer 审查报告中的 P0/P1 与关注点(经主 agent 中转回流)
|
|
205
|
+
- \`## 实现探索\`:coder 每轮 append 的关键符号/数据流、新增涉及文件、验证/构建命令、已排除假设
|
|
206
|
+
- \`## 代码审查范围\`:code-reviewer 审查报告中的 P0/P1 与验证范围结论(经主 agent 中转回流)
|
|
207
|
+
规则:
|
|
208
|
+
- **出提案时 MUST 创建** \`openspec/changes/<name>/scratchpad.md\` 并写入四阶段骨架,预填「调研与设计」初始骨架:调研结论、涉及文件、已排除方案。
|
|
209
|
+
- 后续轮次修改提案时,先读回 scratchpad.md:将 proposal-reviewer 关注点 append 进「提案审查关注点」区,新调研结论增量 append 进「调研与设计」区。
|
|
210
|
+
- 条目以 \`- [R<n> planner] <结论>\` 标注轮次与角色;append-only,不得改写或删除他人结论。
|
|
211
|
+
- 读回 scratchpad.md 时,以最新 supersede 条目为权威(识别 \`- [R<n> coder] supersede:\` 标记,其后的新结论优先于旧结论)。
|
|
176
212
|
|
|
177
213
|
## 提案拆分
|
|
178
214
|
- 接大需求先评估契约边界:能拆则拆成多个小变更,先输出拆分建议(契约边界+提案清单)待主 agent 确认后再开写。
|
|
@@ -183,7 +183,7 @@ export interface HttpProbeOptions {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/** 区域封锁判定的关键词 */
|
|
186
|
-
const REGION_BLOCK_TOKENS = ['region', 'not available', 'in your area'];
|
|
186
|
+
export const REGION_BLOCK_TOKENS = ['region', 'not available', 'in your area'];
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
189
|
* 对 TLS 可达的端点发送最小 HTTP 请求(1 token chat completion),
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider 状态探测模块。
|
|
3
|
+
*
|
|
4
|
+
* 对无 usage fetcher 的 provider(如 ArkCodingPlan)主动发最小探测请求,
|
|
5
|
+
* 区分「429 类错误 vs 正常模型」,驱动底部 widget「可用/耗尽」二元态。
|
|
6
|
+
*
|
|
7
|
+
* 探测是 fire-and-forget 异步操作,绝不阻塞渲染路径。结果写入 provider-status.ts
|
|
8
|
+
* 的进程内状态机(exhausted 集合),渲染自动读取。
|
|
9
|
+
*
|
|
10
|
+
* 探测分类:
|
|
11
|
+
* - ok(2xx)→ markAvailable(清除耗尽)
|
|
12
|
+
* - exhausted(429,或 403 非 region)→ markExhausted
|
|
13
|
+
* - region-blocked(403 + region 关键词)→ markExhausted(归耗尽显示)
|
|
14
|
+
* - auth-error(401)/ model-not-found(404)/ network-error(异常/超时)→ 不写状态
|
|
15
|
+
* (宁缺毋谎:无法区分「provider 真不可用」与「探测请求构造失真」)
|
|
16
|
+
*
|
|
17
|
+
* Cooldown ledger:进程内 Map<canonicalProvider, lastProbedAt>,复用 probe_ttl_ms。
|
|
18
|
+
* markStatusProbeStarted 在任何 await 之前写入,彻底关死 in-flight 竞态窗口。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { REGION_BLOCK_TOKENS } from './reachability-probe.js';
|
|
22
|
+
import { canonicalizeProvider } from './usage-resolver.js';
|
|
23
|
+
import { markExhausted, markAvailable } from './provider-status.js';
|
|
24
|
+
|
|
25
|
+
// ── 类型 ────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/** 状态探测的分类结果。 */
|
|
28
|
+
export type StatusProbeClass =
|
|
29
|
+
| 'ok' // 2xx:真实模型、真实 key,请求走通
|
|
30
|
+
| 'exhausted' // 429,或 403 非 region 文本
|
|
31
|
+
| 'region-blocked' // 403 + region 关键词
|
|
32
|
+
| 'auth-error' // 401
|
|
33
|
+
| 'model-not-found' // 404
|
|
34
|
+
| 'network-error'; // fetch 抛异常 / 超时
|
|
35
|
+
|
|
36
|
+
/** 状态探测结果。 */
|
|
37
|
+
export interface StatusProbeResult {
|
|
38
|
+
provider: string;
|
|
39
|
+
class: StatusProbeClass;
|
|
40
|
+
detail?: string;
|
|
41
|
+
probedAt: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 网络探测选项。 */
|
|
45
|
+
export interface StatusProbeOptions {
|
|
46
|
+
baseUrl: string;
|
|
47
|
+
model: string;
|
|
48
|
+
apiKey: string;
|
|
49
|
+
timeoutMs: number;
|
|
50
|
+
fetchImpl?: typeof fetch;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 调度配置。 */
|
|
54
|
+
export interface ScheduleConfig {
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
ttlMs: number;
|
|
57
|
+
timeoutMs: number;
|
|
58
|
+
excluded: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 调度选项。 */
|
|
62
|
+
export interface ScheduleOptions {
|
|
63
|
+
placeholderIds: string[];
|
|
64
|
+
models: { id: string; provider: string; baseUrl?: string }[];
|
|
65
|
+
getApiKey?: (provider: string) => Promise<string | undefined>;
|
|
66
|
+
config: ScheduleConfig;
|
|
67
|
+
fetchImpl?: typeof fetch;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── 纯分类函数 ─────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 纯分类:HTTP 状态 + 响应体文本 → 分类。
|
|
74
|
+
* 无 IO,直接单测。
|
|
75
|
+
*/
|
|
76
|
+
export function classifyStatusResponse(status: number, bodyText: string): StatusProbeClass {
|
|
77
|
+
// 2xx → ok
|
|
78
|
+
if (status >= 200 && status < 300) {
|
|
79
|
+
return 'ok';
|
|
80
|
+
}
|
|
81
|
+
// 429 → exhausted
|
|
82
|
+
if (status === 429) {
|
|
83
|
+
return 'exhausted';
|
|
84
|
+
}
|
|
85
|
+
// 403 + region 关键词 → region-blocked,否则 exhausted
|
|
86
|
+
if (status === 403) {
|
|
87
|
+
const lower = bodyText.toLowerCase();
|
|
88
|
+
if (REGION_BLOCK_TOKENS.some((tok) => lower.includes(tok))) {
|
|
89
|
+
return 'region-blocked';
|
|
90
|
+
}
|
|
91
|
+
return 'exhausted';
|
|
92
|
+
}
|
|
93
|
+
// 401 → auth-error
|
|
94
|
+
if (status === 401) {
|
|
95
|
+
return 'auth-error';
|
|
96
|
+
}
|
|
97
|
+
// 404 → model-not-found
|
|
98
|
+
if (status === 404) {
|
|
99
|
+
return 'model-not-found';
|
|
100
|
+
}
|
|
101
|
+
// 其余一律 → network-error(detail 记原始 status)
|
|
102
|
+
return 'network-error';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── 网络探测函数 ───────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 对单个 provider 执行状态探测。
|
|
109
|
+
* 绝不抛异常:所有错误/超时/解析失败 → network-error 结果。
|
|
110
|
+
*/
|
|
111
|
+
export async function probeProviderStatus(
|
|
112
|
+
provider: string,
|
|
113
|
+
opts: StatusProbeOptions,
|
|
114
|
+
): Promise<StatusProbeResult> {
|
|
115
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
116
|
+
const url = `${opts.baseUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetchFn(url, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
'Authorization': `Bearer ${opts.apiKey}`,
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
model: opts.model,
|
|
127
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
128
|
+
max_tokens: 1,
|
|
129
|
+
}),
|
|
130
|
+
signal: AbortSignal.timeout(opts.timeoutMs),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let bodyText = '';
|
|
134
|
+
try {
|
|
135
|
+
bodyText = await response.text();
|
|
136
|
+
} catch {
|
|
137
|
+
// 响应体读取失败 → 按空文本处理
|
|
138
|
+
bodyText = '';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const classification = classifyStatusResponse(response.status, bodyText);
|
|
142
|
+
return {
|
|
143
|
+
provider,
|
|
144
|
+
class: classification,
|
|
145
|
+
detail: classification === 'network-error' ? `status ${response.status}` : undefined,
|
|
146
|
+
probedAt: Date.now(),
|
|
147
|
+
};
|
|
148
|
+
} catch (e: unknown) {
|
|
149
|
+
// 网络/超时/AbortError → network-error,绝不抛
|
|
150
|
+
const err = e as Error & { name?: string };
|
|
151
|
+
const detail = err.name === 'AbortError' || err.name === 'TimeoutError'
|
|
152
|
+
? 'timeout'
|
|
153
|
+
: (err.message ?? String(e));
|
|
154
|
+
return {
|
|
155
|
+
provider,
|
|
156
|
+
class: 'network-error',
|
|
157
|
+
detail,
|
|
158
|
+
probedAt: Date.now(),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Cooldown ledger ─────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
const probeLedger = new Map<string, number>();
|
|
166
|
+
|
|
167
|
+
/** 检查是否已到下次探测时间。 */
|
|
168
|
+
export function isStatusProbeDue(provider: string, ttlMs: number): boolean {
|
|
169
|
+
const canonical = canonicalizeProvider(provider);
|
|
170
|
+
const lastProbedAt = probeLedger.get(canonical);
|
|
171
|
+
if (lastProbedAt === undefined) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return Date.now() - lastProbedAt >= ttlMs;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 标记探测已开始(在任何 await 之前调用,关死竞态窗口)。 */
|
|
178
|
+
export function markStatusProbeStarted(provider: string): void {
|
|
179
|
+
const canonical = canonicalizeProvider(provider);
|
|
180
|
+
probeLedger.set(canonical, Date.now());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 测试 seam:清空 ledger。 */
|
|
184
|
+
export function _resetForTest(): void {
|
|
185
|
+
probeLedger.clear();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── 调度入口 ─────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 调度状态探测。
|
|
192
|
+
*
|
|
193
|
+
* 探测是 fire-and-forget 异步操作,绝不阻塞调用路径。
|
|
194
|
+
* 返回写入计数(可能为任意非负整数),调用方可据此判断是否需要重渲染。
|
|
195
|
+
*
|
|
196
|
+
* 异常全部吞没:fetch 失败/超时/解析失败/状态机写入失败均不影响其他 provider。
|
|
197
|
+
*/
|
|
198
|
+
export async function scheduleStatusProbes(opts: ScheduleOptions): Promise<number> {
|
|
199
|
+
const { placeholderIds, models, getApiKey, config, fetchImpl } = opts;
|
|
200
|
+
|
|
201
|
+
// 快速短路:未启用或无可用的 getApiKey → 零请求,不抛 TypeError
|
|
202
|
+
if (!config.enabled || getApiKey === undefined) {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 同步过滤候选:excluded + 无 baseUrl/无模型
|
|
207
|
+
const excludedSet = new Set(config.excluded.map(canonicalizeProvider));
|
|
208
|
+
const modelMap = new Map<string, { id: string; baseUrl: string }>();
|
|
209
|
+
for (const m of models) {
|
|
210
|
+
if (m.baseUrl && !modelMap.has(canonicalizeProvider(m.provider))) {
|
|
211
|
+
modelMap.set(canonicalizeProvider(m.provider), { id: m.id, baseUrl: m.baseUrl });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const candidates = placeholderIds.filter((id) => {
|
|
216
|
+
const canonical = canonicalizeProvider(id);
|
|
217
|
+
if (excludedSet.has(canonical)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
const modelInfo = modelMap.get(canonical);
|
|
221
|
+
if (!modelInfo) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
// cooldown 检查
|
|
225
|
+
if (!isStatusProbeDue(id, config.ttlMs)) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (candidates.length === 0) {
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 对每个候选:先 markStatusProbeStarted(占位),再 await getApiKey
|
|
236
|
+
// 关死 in-flight 竞态窗口
|
|
237
|
+
const probePromises = candidates.map(async (provider) => {
|
|
238
|
+
markStatusProbeStarted(provider);
|
|
239
|
+
const apiKey = await getApiKey(provider);
|
|
240
|
+
if (!apiKey) {
|
|
241
|
+
// 无 key → 跳过请求但保留 cooldown 占位(下个 TTL 再查)
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const modelInfo = modelMap.get(canonicalizeProvider(provider));
|
|
245
|
+
if (!modelInfo) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
return probeProviderStatus(provider, {
|
|
249
|
+
baseUrl: modelInfo.baseUrl,
|
|
250
|
+
model: modelInfo.id,
|
|
251
|
+
apiKey,
|
|
252
|
+
timeoutMs: config.timeoutMs,
|
|
253
|
+
fetchImpl,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// 并发探测,单个失败不连坐
|
|
258
|
+
const results = await Promise.allSettled(probePromises);
|
|
259
|
+
let written = 0;
|
|
260
|
+
|
|
261
|
+
for (const result of results) {
|
|
262
|
+
if (result.status === 'fulfilled') {
|
|
263
|
+
const probeResult = result.value;
|
|
264
|
+
if (!probeResult) {
|
|
265
|
+
continue; // 跳过的 provider
|
|
266
|
+
}
|
|
267
|
+
const canonical = canonicalizeProvider(probeResult.provider);
|
|
268
|
+
// 映射到状态机:exhausted/region-blocked → markExhausted;ok → markAvailable
|
|
269
|
+
if (probeResult.class === 'exhausted' || probeResult.class === 'region-blocked') {
|
|
270
|
+
markExhausted(probeResult.provider);
|
|
271
|
+
written++;
|
|
272
|
+
console.info(`[omp-opsx-addon] status probe: ${canonical} -> ${probeResult.class}`);
|
|
273
|
+
} else if (probeResult.class === 'ok') {
|
|
274
|
+
markAvailable(probeResult.provider);
|
|
275
|
+
written++;
|
|
276
|
+
console.info(`[omp-opsx-addon] status probe: ${canonical} -> ok`);
|
|
277
|
+
} else {
|
|
278
|
+
// auth-error/model-not-found/network-error 不写状态
|
|
279
|
+
console.warn(
|
|
280
|
+
`[omp-opsx-addon] status probe: ${canonical} -> ${probeResult.class}${probeResult.detail ? ` (${probeResult.detail})` : ''}`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// rejected promise 在理论上不应发生(probeProviderStatus 已吞没所有异常),防御性忽略
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return written;
|
|
288
|
+
}
|
package/lib/system-prompt.ts
CHANGED
|
@@ -100,15 +100,15 @@ export function buildStaticPrompt(): string {
|
|
|
100
100
|
| 审查代码实现 | task(agent:"reviewer", task:"审阅代码 + 执行全局验证(lint/单测/e2e)") |
|
|
101
101
|
|
|
102
102
|
## 2 个 Loop
|
|
103
|
-
- **Loop 1 – Propose → Review**:planner 完成 → proposal-reviewer → P0/P1 发回 → 通过告知用户。Propose-review loop:最多 planner→reviewer 往复 2
|
|
104
|
-
- **Loop 2 – Code → Review**:coder 完成(交付前自验证)→ code-reviewer 审阅 + 全局验证 → P0/P1 发回修复 → 仅 P2+ 视为通过。Code-review loop:最多 coder→reviewer 往复 3 轮(含首次实现),reviewer
|
|
103
|
+
- **Loop 1 – Propose → Review**:planner 完成 → proposal-reviewer → P0/P1 发回 → 通过告知用户。Propose-review loop:最多 planner→reviewer 往复 2 轮。委派 planner/proposal-reviewer 的 task 描述须含『先读 openspec/changes/<name>/scratchpad.md,禁止重复探索』。
|
|
104
|
+
- **Loop 2 – Code → Review**:coder 完成(交付前自验证)→ code-reviewer 审阅 + 全局验证 → P0/P1 发回修复 → 仅 P2+ 视为通过。Code-review loop:最多 coder→reviewer 往复 3 轮(含首次实现),reviewer 兼执行全局验证。委派 coder/code-reviewer 的 task 描述须含『先读 openspec/changes/<name>/scratchpad.md,禁止重复探索』。
|
|
105
105
|
- 达上限时停止自动流转,向用户说明未解决的问题并请求决策。
|
|
106
106
|
|
|
107
107
|
## /goal 集成(可选)
|
|
108
108
|
Planner 在 proposal 末尾输出 \`## Budget Estimate\`;主 agent 提取用于 \`/goal\`。
|
|
109
109
|
|
|
110
110
|
## 自动流程协作
|
|
111
|
-
coder 输出 STATUS: blocked 且含 SESSION: → 阅读阻塞原因决策;收到 P0/P1 → 修复再审。
|
|
111
|
+
coder 输出 STATUS: blocked 且含 SESSION: → 阅读阻塞原因决策;收到 P0/P1 → 修复再审。P0/P1 发回修复时 task 描述同样须含『先读 openspec/changes/<name>/scratchpad.md,禁止重复探索』。
|
|
112
112
|
|
|
113
113
|
## 上下文卫生(主 session 预算)
|
|
114
114
|
- 代码调研/搜代码/理解架构 → task(agent:"scout");主 session 不亲自 read 全文调研。
|
|
@@ -118,7 +118,7 @@ coder 输出 STATUS: blocked 且含 SESSION: → 阅读阻塞原因决策;收到
|
|
|
118
118
|
- 主 session 上下文超过 ~50k tokens:必须先委托再继续,禁止继续亲自调研。`;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
const STATIC_LINE_COUNT = buildStaticPrompt().split('\n').length;
|
|
121
|
+
export const STATIC_LINE_COUNT = buildStaticPrompt().split('\n').length;
|
|
122
122
|
|
|
123
123
|
/** Dynamic layer: Dispatch config + change status. Placed after the static
|
|
124
124
|
* layer so only this tail is re-prefilled when it changes between turns. */
|
package/lib/unified-config.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface OpsxYamlShape {
|
|
|
66
66
|
probe_http_enabled?: boolean;
|
|
67
67
|
probe_ttl_ms?: number;
|
|
68
68
|
excluded_providers?: string[];
|
|
69
|
+
status_probe_enabled?: boolean;
|
|
69
70
|
redis_enabled?: boolean;
|
|
70
71
|
redis_host?: string;
|
|
71
72
|
redis_port?: number;
|
|
@@ -102,6 +103,8 @@ export interface ResolvedOpsxConfig {
|
|
|
102
103
|
probe_ttl_ms: number;
|
|
103
104
|
/** 静态排除的 provider 列表(零探测成本) */
|
|
104
105
|
excluded_providers: string[];
|
|
106
|
+
/** 是否开启状态探测,默认 true */
|
|
107
|
+
status_probe_enabled: boolean;
|
|
105
108
|
/** 是否启用本地 Redis 实时中转(默认 true;false 时纯文件路径)。 */
|
|
106
109
|
redis_enabled: boolean;
|
|
107
110
|
/** Redis host(默认 127.0.0.1)。 */
|
|
@@ -309,6 +312,7 @@ export function parseOpsxConfig(
|
|
|
309
312
|
probe_http_enabled: parseProbeBool(obj.probe_http_enabled, 'probe_http_enabled', false, localWarn),
|
|
310
313
|
probe_ttl_ms: parsePositiveInt(obj.probe_ttl_ms, 'probe_ttl_ms', 600_000, localWarn),
|
|
311
314
|
excluded_providers: parseStringArray(obj.excluded_providers, 'excluded_providers', localWarn),
|
|
315
|
+
status_probe_enabled: parseProbeBool(obj.status_probe_enabled, 'status_probe_enabled', true, localWarn),
|
|
312
316
|
redis_enabled: parseProbeBool(obj.redis_enabled, 'redis_enabled', true, localWarn),
|
|
313
317
|
redis_host: redisHost,
|
|
314
318
|
redis_port: parsePositiveInt(obj.redis_port, 'redis_port', 6379, localWarn),
|
|
@@ -359,6 +363,7 @@ function pickTopLevelKeys(raw: OpsxYamlShape): OpsxYamlShape {
|
|
|
359
363
|
probe_http_enabled: raw.probe_http_enabled,
|
|
360
364
|
probe_ttl_ms: raw.probe_ttl_ms,
|
|
361
365
|
excluded_providers: raw.excluded_providers,
|
|
366
|
+
status_probe_enabled: raw.status_probe_enabled,
|
|
362
367
|
redis_enabled: raw.redis_enabled,
|
|
363
368
|
redis_host: raw.redis_host,
|
|
364
369
|
redis_port: raw.redis_port,
|
|
@@ -449,6 +454,7 @@ export function readOpsxSettingsFromPaths(
|
|
|
449
454
|
merged.probe_http_enabled = projectTop.probe_http_enabled ?? globalTop.probe_http_enabled;
|
|
450
455
|
merged.probe_ttl_ms = projectTop.probe_ttl_ms ?? globalTop.probe_ttl_ms;
|
|
451
456
|
merged.excluded_providers = projectTop.excluded_providers ?? globalTop.excluded_providers;
|
|
457
|
+
merged.status_probe_enabled = projectTop.status_probe_enabled ?? globalTop.status_probe_enabled;
|
|
452
458
|
merged.redis_enabled = projectTop.redis_enabled ?? globalTop.redis_enabled;
|
|
453
459
|
merged.redis_host = projectTop.redis_host ?? globalTop.redis_host;
|
|
454
460
|
merged.redis_port = projectTop.redis_port ?? globalTop.redis_port;
|
package/package.json
CHANGED