openfeel 1.0.9 → 1.1.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/README.en.md +11 -11
- package/README.md +2 -1
- package/README.zh-CN.md +13 -13
- package/bin/openfeel.js +2 -2
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +26 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/archive.d.ts +1 -1
- package/dist/commands/flow.d.ts +1 -1
- package/dist/commands/flow.d.ts.map +1 -1
- package/dist/commands/flow.js +20 -8
- package/dist/commands/flow.js.map +1 -1
- package/dist/commands/migrate.d.ts +10 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +97 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/model.d.ts +10 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +135 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/stage.d.ts.map +1 -1
- package/dist/commands/stage.js +50 -29
- package/dist/commands/stage.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +12 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.d.ts.map +1 -1
- package/dist/commands/view.js +2 -1
- package/dist/commands/view.js.map +1 -1
- package/dist/core/archive/merge.d.ts.map +1 -1
- package/dist/core/archive/merge.js +5 -4
- package/dist/core/archive/merge.js.map +1 -1
- package/dist/core/artifact-graph/resolver.js +2 -2
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/config.d.ts +8 -8
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +22 -17
- package/dist/core/config.js.map +1 -1
- package/dist/core/flow-manager.d.ts +25 -4
- package/dist/core/flow-manager.d.ts.map +1 -1
- package/dist/core/flow-manager.js +144 -79
- package/dist/core/flow-manager.js.map +1 -1
- package/dist/core/fs/atomic-write.d.ts +31 -0
- package/dist/core/fs/atomic-write.d.ts.map +1 -0
- package/dist/core/fs/atomic-write.js +80 -0
- package/dist/core/fs/atomic-write.js.map +1 -0
- package/dist/core/fs/file-lock.d.ts +34 -0
- package/dist/core/fs/file-lock.d.ts.map +1 -0
- package/dist/core/fs/file-lock.js +142 -0
- package/dist/core/fs/file-lock.js.map +1 -0
- package/dist/core/fs/sequence.d.ts +42 -0
- package/dist/core/fs/sequence.d.ts.map +1 -0
- package/dist/core/fs/sequence.js +69 -0
- package/dist/core/fs/sequence.js.map +1 -0
- package/dist/core/global-paths.d.ts +25 -0
- package/dist/core/global-paths.d.ts.map +1 -0
- package/dist/core/global-paths.js +57 -0
- package/dist/core/global-paths.js.map +1 -0
- package/dist/core/i18n-data/en.d.ts +2 -0
- package/dist/core/i18n-data/en.d.ts.map +1 -1
- package/dist/core/i18n-data/en.js +69 -1
- package/dist/core/i18n-data/en.js.map +1 -1
- package/dist/core/i18n-data/zh-CN.d.ts +2 -0
- package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
- package/dist/core/i18n-data/zh-CN.js +69 -1
- package/dist/core/i18n-data/zh-CN.js.map +1 -1
- package/dist/core/i18n.js +4 -4
- package/dist/core/i18n.js.map +1 -1
- package/dist/core/init.d.ts +3 -3
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +50 -37
- package/dist/core/init.js.map +1 -1
- package/dist/core/managed-region.d.ts +54 -0
- package/dist/core/managed-region.d.ts.map +1 -0
- package/dist/core/managed-region.js +171 -0
- package/dist/core/managed-region.js.map +1 -0
- package/dist/core/metrics.d.ts.map +1 -1
- package/dist/core/metrics.js +6 -3
- package/dist/core/metrics.js.map +1 -1
- package/dist/core/migrate.d.ts +103 -0
- package/dist/core/migrate.d.ts.map +1 -0
- package/dist/core/migrate.js +533 -0
- package/dist/core/migrate.js.map +1 -0
- package/dist/core/model-config.d.ts +62 -0
- package/dist/core/model-config.d.ts.map +1 -0
- package/dist/core/model-config.js +336 -0
- package/dist/core/model-config.js.map +1 -0
- package/dist/core/opencode-config.d.ts +17 -0
- package/dist/core/opencode-config.d.ts.map +1 -0
- package/dist/core/opencode-config.js +123 -0
- package/dist/core/opencode-config.js.map +1 -0
- package/dist/core/plan/roadmap.d.ts.map +1 -1
- package/dist/core/plan/roadmap.js +3 -2
- package/dist/core/plan/roadmap.js.map +1 -1
- package/dist/core/plan/scheme.d.ts.map +1 -1
- package/dist/core/plan/scheme.js +35 -36
- package/dist/core/plan/scheme.js.map +1 -1
- package/dist/core/plan/stage.d.ts.map +1 -1
- package/dist/core/plan/stage.js +4 -3
- package/dist/core/plan/stage.js.map +1 -1
- package/dist/core/public-logger.d.ts +3 -3
- package/dist/core/public-logger.d.ts.map +1 -1
- package/dist/core/public-logger.js +36 -40
- package/dist/core/public-logger.js.map +1 -1
- package/dist/core/template-loader.d.ts +1 -1
- package/dist/core/template-loader.d.ts.map +1 -1
- package/dist/core/template-loader.js +2834 -2813
- package/dist/core/template-loader.js.map +1 -1
- package/dist/core/update-infos.d.ts +32 -0
- package/dist/core/update-infos.d.ts.map +1 -0
- package/dist/core/update-infos.js +153 -0
- package/dist/core/update-infos.js.map +1 -0
- package/dist/core/update-state.d.ts +18 -0
- package/dist/core/update-state.d.ts.map +1 -1
- package/dist/core/update-state.js +66 -3
- package/dist/core/update-state.js.map +1 -1
- package/dist/core/update.d.ts +12 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +335 -482
- package/dist/core/update.js.map +1 -1
- package/dist/core/view/entry.js +4 -4
- package/dist/core/view/entry.js.map +1 -1
- package/dist/core/workspace/identity.d.ts.map +1 -1
- package/dist/core/workspace/identity.js +13 -10
- package/dist/core/workspace/identity.js.map +1 -1
- package/dist/core/workspace/knowledge.d.ts.map +1 -1
- package/dist/core/workspace/knowledge.js +24 -14
- package/dist/core/workspace/knowledge.js.map +1 -1
- package/package.json +1 -1
package/dist/core/update.js
CHANGED
|
@@ -5,13 +5,19 @@
|
|
|
5
5
|
* 变更摘要 (v3-stage-04 第二轮):
|
|
6
6
|
* - 新增 instructions/core.md 创建(从 init.ts 迁移至此,职责归位适配器层)
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { resolve, dirname, join } from 'node:path';
|
|
8
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
9
|
+
import { resolve, dirname, join, basename, isAbsolute, relative } from 'node:path';
|
|
10
10
|
import { loadAgentTemplate, listAgentIds, loadTemplate } from './template-loader.js';
|
|
11
11
|
import { recordProjectLang, getGlobalConfig, getLang } from './workspace/identity.js';
|
|
12
12
|
import { getCliLang } from './i18n.js';
|
|
13
|
-
|
|
14
|
-
import {
|
|
13
|
+
import { getOpencodeGlobalDir, getGlobalAgentsDir, getGlobalSkillsDir, getGlobalCoreMdPath, getGlobalOpencodeJsoncPath, getGlobalUpdateStatePath } from './global-paths.js';
|
|
14
|
+
import { mergeGlobalOpencodeJsonc, buildProjectOpencodeJsoncObj } from './opencode-config.js';
|
|
15
|
+
import { atomicWriteFileSync } from './fs/atomic-write.js';
|
|
16
|
+
import { withFileLock, globalLockPath } from './fs/file-lock.js';
|
|
17
|
+
import { detectFileType, normalize, splitFrontmatter, parseRegion, wrapRegion, replaceRegion, mergeFrontmatter, serializeFrontmatter } from './managed-region.js';
|
|
18
|
+
import { appendUpdateInfo } from './update-infos.js';
|
|
19
|
+
// 新增:update_state.json hash 比对与冲突检测(项目 state + 全局 state)
|
|
20
|
+
import { hashContent, loadUpdateState, saveUpdateState, createUpdateState, loadGlobalUpdateState, saveGlobalUpdateState, createGlobalUpdateState, updateFileHash, markFileConflict, getOpenfeelVersion, } from './update-state.js';
|
|
15
21
|
/** AGENTS.md 语言冲突错误(由命令层捕获处理) */
|
|
16
22
|
export class AgentsMdLangConflictError extends Error {
|
|
17
23
|
projectLang;
|
|
@@ -28,7 +34,7 @@ export const supportedTools = [
|
|
|
28
34
|
{
|
|
29
35
|
id: 'opencode',
|
|
30
36
|
name: 'OpenCode',
|
|
31
|
-
description: 'OpenCode AI 编码助手 — 生成 Agent 定义和
|
|
37
|
+
description: 'OpenCode AI 编码助手 — 生成 Agent 定义和 openfeel-* 技能',
|
|
32
38
|
enabled: true,
|
|
33
39
|
},
|
|
34
40
|
// 后续扩展:
|
|
@@ -64,13 +70,13 @@ export async function selectTools() {
|
|
|
64
70
|
// ─── Agent 定义由 template-loader.ts 统一管理 ──────────────────────────
|
|
65
71
|
// ─── Skill 定义内容 ─────────────────────────────────────────────────
|
|
66
72
|
// AUTO-GENERATED-BEGIN: SKILL_DEFINITIONS
|
|
67
|
-
const SKILL_DEFINITIONS = {
|
|
68
|
-
'agent-model-check': `---
|
|
69
|
-
name: agent-model-check
|
|
70
|
-
description: Agent 模型检查与修复。当 Agent 报 "Model not found" 或需要排查模型配置时使用。涵盖 auth.json 校验、provider key 匹配、模型能力确认、
|
|
73
|
+
export const SKILL_DEFINITIONS = {
|
|
74
|
+
'openfeel-agent-model-check': `---
|
|
75
|
+
name: openfeel-agent-model-check
|
|
76
|
+
description: Agent 模型检查与修复。当 Agent 报 "Model not found" 或需要排查模型配置时使用。涵盖 auth.json 校验、provider key 匹配、模型能力确认、openfeel-vision 多模态专项指南。
|
|
71
77
|
---
|
|
72
78
|
|
|
73
|
-
# Skill: agent-model-check
|
|
79
|
+
# Skill: openfeel-agent-model-check
|
|
74
80
|
|
|
75
81
|
# Agent 模型检查与修复
|
|
76
82
|
|
|
@@ -79,7 +85,7 @@ description: Agent 模型检查与修复。当 Agent 报 "Model not found" 或
|
|
|
79
85
|
- Agent 调度时报 \`Model not found: xxx\`
|
|
80
86
|
- 需要验证某个 Agent 的模型是否可用
|
|
81
87
|
- 新增 Agent 后需确认模型配置正确
|
|
82
|
-
- 排查多模态(
|
|
88
|
+
- 排查多模态(openfeel-vision)Agent 无法处理图片的问题
|
|
83
89
|
|
|
84
90
|
## 排查流程
|
|
85
91
|
|
|
@@ -113,22 +119,22 @@ cat ~/.local/share/opencode/auth.json
|
|
|
113
119
|
|
|
114
120
|
| 能力需求 | 需确认的字段 | 示例 |
|
|
115
121
|
|----------|-------------|------|
|
|
116
|
-
| 视觉/图像分析 | Input = Yes | \`
|
|
122
|
+
| 视觉/图像分析 | Input = Yes | \`deepseek-flash\` |
|
|
117
123
|
| 工具调用 | Tool Call = Yes | \`qwen3.7-plus\` |
|
|
118
124
|
| 结构化输出 | Structured = Yes | \`qwen3.7-flash\` |
|
|
119
125
|
| 推理/思考 | Reasoning = Yes | \`qwq-plus\` |
|
|
120
126
|
|
|
121
127
|
**常见陷阱**:
|
|
122
128
|
- \`qwen3.7-plus\` 是纯文本模型,不支持图像输入
|
|
123
|
-
- \`
|
|
124
|
-
-
|
|
129
|
+
- \`deepseek-flash\` 支持多模态图像输入(openfeel-vision 现用模型)
|
|
130
|
+
- 备选视觉模型:\`qwen3-vl-plus\`(模型名中的 \`vl\` 表示 Vision-Language)
|
|
125
131
|
|
|
126
132
|
### 第四步:检查 opencode.jsonc 配置
|
|
127
133
|
|
|
128
134
|
\`\`\`jsonc
|
|
129
135
|
{
|
|
130
136
|
"agent": {
|
|
131
|
-
"vision": {
|
|
137
|
+
"openfeel-vision": {
|
|
132
138
|
"model": "{auth.json_key}/{model_id}" // 格式:provider_key/model_id
|
|
133
139
|
}
|
|
134
140
|
}
|
|
@@ -159,17 +165,18 @@ cat ~/.local/share/opencode/auth.json
|
|
|
159
165
|
| opencode.jsonc 语法 | 检查 JSON 格式 | 无语法错误 |
|
|
160
166
|
| 重启生效 | 重启 opencode 后重新测试 | 无 Model not found 报错 |
|
|
161
167
|
|
|
162
|
-
## 多模态(
|
|
168
|
+
## 多模态(openfeel-vision)Agent 专项
|
|
163
169
|
|
|
164
|
-
|
|
170
|
+
openfeel-vision Agent 必须配置多模态模型。当前默认使用 \`deepseek/deepseek-flash\`(已支持多模态图像输入):
|
|
165
171
|
|
|
166
|
-
| 模型 ID |
|
|
167
|
-
|
|
168
|
-
|
|
|
172
|
+
| 模型 ID | 完整引用 | 上下文 | 图像输入 |
|
|
173
|
+
|---------|---------|--------|---------|
|
|
174
|
+
| deepseek-flash | \`deepseek/deepseek-flash\` | — | ✅ |
|
|
175
|
+
| qwen3-vl-plus | \`alibaba-cn/qwen3-vl-plus\` | 262K | ✅(备选) |
|
|
169
176
|
| qwen-vl-plus | \`alibaba-cn/qwen-vl-plus\` | 131K | ❌(旧版) |
|
|
170
177
|
| qwen-vl-max | \`alibaba-cn/qwen-vl-max\` | 131K | ❌(旧版) |
|
|
171
178
|
|
|
172
|
-
**推荐**:优先使用 \`qwen3-vl-plus
|
|
179
|
+
**推荐**:优先使用 \`deepseek-flash\`(已内置多模态);如需更大上下文可备选 \`qwen3-vl-plus\`。
|
|
173
180
|
|
|
174
181
|
## 常见错误与修复
|
|
175
182
|
|
|
@@ -178,10 +185,10 @@ Vision Agent 必须配置多模态模型。Alibaba 系列视觉模型:
|
|
|
178
185
|
| \`Model not found: Alibaba(China)/xxx\` | 使用了自定义 provider name 而非 auth.json key | 改为 auth.json 中的实际 key |
|
|
179
186
|
| \`Model not found: alibaba/xxx\` | 内置 key 与实际注册的 key 不一致 | 检查 auth.json,使用实际 key |
|
|
180
187
|
| \`Model not found: xxx. Did you mean: yyy\` | 模型名拼写错误或不存在 | 使用 \`Did you mean\` 建议的名称 |
|
|
181
|
-
| Agent 调度成功但无法处理图片 | 配置了纯文本模型 |
|
|
188
|
+
| Agent 调度成功但无法处理图片 | 配置了纯文本模型 | 改为支持图像输入的模型(如 \`deepseek-flash\`) |
|
|
182
189
|
`,
|
|
183
|
-
'bug-acceptance': `---
|
|
184
|
-
name: bug-acceptance
|
|
190
|
+
'openfeel-bug-acceptance': `---
|
|
191
|
+
name: openfeel-bug-acceptance
|
|
185
192
|
description: 标准化 Bug 验收流程,供测试 Agent 或代码 Agent(自测后自查)调用。
|
|
186
193
|
---
|
|
187
194
|
|
|
@@ -233,8 +240,8 @@ description: 标准化 Bug 验收流程,供测试 Agent 或代码 Agent(自
|
|
|
233
240
|
|
|
234
241
|
验收通过后,核心结论写入 \`.openfeel/bugs/{module}.md\`,并在公共日志简要记录。
|
|
235
242
|
`,
|
|
236
|
-
'check-kb': `---
|
|
237
|
-
name: check-kb
|
|
243
|
+
'openfeel-check-kb': `---
|
|
244
|
+
name: openfeel-check-kb
|
|
238
245
|
description: 渐进式查阅 .openfeel/kb/ 项目知识库,按当前任务需求返回最相关的参考信息。精确匹配无结果时自动触发语义检索回退,避免一次加载全量内容。
|
|
239
246
|
---
|
|
240
247
|
|
|
@@ -284,7 +291,7 @@ description: 渐进式查阅 .openfeel/kb/ 项目知识库,按当前任务需
|
|
|
284
291
|
|
|
285
292
|
### 5. 语义检索回退(自动)
|
|
286
293
|
|
|
287
|
-
当精确匹配未找到任何相关条目(无 \`[+]\` 条目或所有条目与当前任务无关)时,**本技能自行执行语义检索**,无需调用方另行加载 \`search-kb\`:
|
|
294
|
+
当精确匹配未找到任何相关条目(无 \`[+]\` 条目或所有条目与当前任务无关)时,**本技能自行执行语义检索**,无需调用方另行加载 \`openfeel-search-kb\`:
|
|
288
295
|
|
|
289
296
|
1. **索引就绪检查**:检查 \`.openfeel/tmp/vectors/index.json\` 是否存在
|
|
290
297
|
2. **索引缺失时**:输出以下提示后结束,不再继续回退:
|
|
@@ -323,7 +330,7 @@ description: 渐进式查阅 .openfeel/kb/ 项目知识库,按当前任务需
|
|
|
323
330
|
💡 如需确保索引为最新,可运行: python scripts/build_kb_index.py --dry-run
|
|
324
331
|
\`\`\`
|
|
325
332
|
|
|
326
|
-
> 注:语义检索是 \`check-kb\` 的内置回退能力。调用方只需 \`skill("check-kb")\` 一次,无需再手动调用 \`search-kb\`。
|
|
333
|
+
> 注:语义检索是 \`openfeel-check-kb\` 的内置回退能力。调用方只需 \`skill("openfeel-check-kb")\` 一次,无需再手动调用 \`openfeel-search-kb\`。
|
|
327
334
|
|
|
328
335
|
### 6. 强制检索标记
|
|
329
336
|
|
|
@@ -335,8 +342,8 @@ Skill 加载后,根据步骤 3 的匹配结果决定是否追加提示:
|
|
|
335
342
|
|
|
336
343
|
- **若步骤 3 无相关条目**:不追加强制提示,静默返回(已在步骤 4 输出"知识库中暂无相关记录,可继续执行")。
|
|
337
344
|
`,
|
|
338
|
-
'get-bugs': `---
|
|
339
|
-
name: get-bugs
|
|
345
|
+
'openfeel-get-bugs': `---
|
|
346
|
+
name: openfeel-get-bugs
|
|
340
347
|
description: 获取当前模块下状态为 open 或 fixing 的 Bug 列表,供代码 Agent 会话启动或承接时使用。
|
|
341
348
|
---
|
|
342
349
|
|
|
@@ -372,9 +379,9 @@ description: 获取当前模块下状态为 open 或 fixing 的 Bug 列表,供
|
|
|
372
379
|
|
|
373
380
|
输出:\`模块 [模块名] 当前无待处理 Bug。\`
|
|
374
381
|
`,
|
|
375
|
-
'get-stage-status': `---
|
|
376
|
-
name: get-stage-status
|
|
377
|
-
description: 读取 .openfeel/plan/{series}/{stage}/status.md,判断当前子计划状态、责任 Agent、是否允许自动推进以及下一步建议。用于
|
|
382
|
+
'openfeel-get-stage-status': `---
|
|
383
|
+
name: openfeel-get-stage-status
|
|
384
|
+
description: 读取 .openfeel/plan/{series}/{stage}/status.md,判断当前子计划状态、责任 Agent、是否允许自动推进以及下一步建议。用于 openfeel-reviewer/openfeel-executor/openfeel-feel-tester 在处理阶段任务前确认流程状态。
|
|
378
385
|
---
|
|
379
386
|
|
|
380
387
|
# 获取子计划状态
|
|
@@ -479,29 +486,29 @@ description: 读取 .openfeel/plan/{series}/{stage}/status.md,判断当前子
|
|
|
479
486
|
{若依赖就绪且可自动推进,列出同批次可并行启动的其他阶段}
|
|
480
487
|
|
|
481
488
|
## 下一步建议
|
|
482
|
-
{根据状态给出下一步,例如:启动 Code、等待用户、启动
|
|
489
|
+
{根据状态给出下一步,例如:启动 Code、等待用户、启动 openfeel-feel-tester、停止流程;若存在并行候选则建议批量启动}
|
|
483
490
|
\`\`\`
|
|
484
491
|
|
|
485
492
|
## 状态到下一步映射
|
|
486
493
|
|
|
487
494
|
| 状态 | 下一步建议 |
|
|
488
495
|
|------|------------|
|
|
489
|
-
| \`planned\` | 等待用户确认或
|
|
490
|
-
| \`ready_for_code\` |
|
|
491
|
-
| \`coding\` |
|
|
492
|
-
| \`ready_for_review\` |
|
|
493
|
-
| \`review_failed\` |
|
|
496
|
+
| \`planned\` | 等待用户确认或 openfeel-planner 细化计划 |
|
|
497
|
+
| \`ready_for_code\` | openfeel-planner 可启动 openfeel-executor |
|
|
498
|
+
| \`coding\` | openfeel-executor 正在开发 |
|
|
499
|
+
| \`ready_for_review\` | openfeel-executor 可启动 openfeel-reviewer 审查,或等待用户触发 |
|
|
500
|
+
| \`review_failed\` | openfeel-reviewer 可启动 openfeel-executor 修复审查问题 |
|
|
494
501
|
| \`review_passed\` | Feel 可推进到 ready_for_test |
|
|
495
|
-
| \`ready_for_test\` | Feel 可启动
|
|
496
|
-
| \`test_writing\` |
|
|
497
|
-
| \`testing\` |
|
|
498
|
-
| \`bug_found\` |
|
|
499
|
-
| \`bug_fixing\` |
|
|
502
|
+
| \`ready_for_test\` | Feel 可启动 openfeel-feel-tester |
|
|
503
|
+
| \`test_writing\` | openfeel-feel-tester 正在写测试 |
|
|
504
|
+
| \`testing\` | openfeel-feel-tester 正在测试 |
|
|
505
|
+
| \`bug_found\` | openfeel-feel-tester 可启动 openfeel-executor 修复 Bug |
|
|
506
|
+
| \`bug_fixing\` | openfeel-executor 正在修复 Bug |
|
|
500
507
|
| \`done\` | 流程完成,停止 |
|
|
501
508
|
| \`paused\` | 等待用户处理暂停原因 |
|
|
502
509
|
`,
|
|
503
|
-
'health': `---
|
|
504
|
-
name: health
|
|
510
|
+
'openfeel-health': `---
|
|
511
|
+
name: openfeel-health
|
|
505
512
|
description: 加载流水线健康检查结果,供 Agent 判断 flow.json 与工作区状态是否一致。
|
|
506
513
|
---
|
|
507
514
|
|
|
@@ -521,8 +528,8 @@ description: 加载流水线健康检查结果,供 Agent 判断 flow.json 与
|
|
|
521
528
|
|
|
522
529
|
健康检查摘要:通过项数、失败项列表及原因,失败时给出修复建议
|
|
523
530
|
`,
|
|
524
|
-
'model-check': `---
|
|
525
|
-
name: model-check
|
|
531
|
+
'openfeel-model-check': `---
|
|
532
|
+
name: openfeel-model-check
|
|
526
533
|
description: Feel 自检时检查所有 Agent 的模型配置状态,识别期望模型 vs 实际模型的差距,引导用户在目标工具中完成配置。首次配置后存储为部署模板,新项目可直接复用。
|
|
527
534
|
---
|
|
528
535
|
|
|
@@ -567,14 +574,14 @@ Feel Agent 在以下时机加载本 Skill:
|
|
|
567
574
|
| Agent 文件 | 默认模型角色 |
|
|
568
575
|
|------------|-------------|
|
|
569
576
|
| \`feel.md\` | \`primary_reasoning\`(主力推理) |
|
|
570
|
-
| \`planner.md\` | \`reasoning\`(推理) |
|
|
571
|
-
| \`executor.md\` | \`fast\`(快速) |
|
|
572
|
-
| \`reviewer.md\` | \`cross_model\`(异种推理) |
|
|
573
|
-
| \`archiver.md\` | \`reasoning\`(推理) |
|
|
574
|
-
| \`schemer.md\` | \`reasoning\`(推理) |
|
|
575
|
-
| \`feel-tester.md\` | \`reasoning\`(推理) |
|
|
576
|
-
| \`utility.md\` | \`fast\`(快速) |
|
|
577
|
-
| \`vision.md\` | \`multimodal\`(多模态) |
|
|
577
|
+
| \`openfeel-planner.md\` | \`reasoning\`(推理) |
|
|
578
|
+
| \`openfeel-executor.md\` | \`fast\`(快速) |
|
|
579
|
+
| \`openfeel-reviewer.md\` | \`cross_model\`(异种推理) |
|
|
580
|
+
| \`openfeel-archiver.md\` | \`reasoning\`(推理) |
|
|
581
|
+
| \`openfeel-schemer.md\` | \`reasoning\`(推理) |
|
|
582
|
+
| \`openfeel-feel-tester.md\` | \`reasoning\`(推理) |
|
|
583
|
+
| \`openfeel-utility.md\` | \`fast\`(快速) |
|
|
584
|
+
| \`openfeel-vision.md\` | \`multimodal\`(多模态) |
|
|
578
585
|
|
|
579
586
|
### 3. 检查 config.yaml 模型配置
|
|
580
587
|
|
|
@@ -589,14 +596,14 @@ models:
|
|
|
589
596
|
base_url: https://api.deepseek.com
|
|
590
597
|
api_key_env: DEEPSEEK_API_KEY
|
|
591
598
|
agents: # Agent 级覆盖(可选)
|
|
592
|
-
reviewer:
|
|
599
|
+
openfeel-reviewer:
|
|
593
600
|
provider: anthropic
|
|
594
601
|
model_name: claude-sonnet-4-20250514
|
|
595
602
|
base_url: https://api.anthropic.com
|
|
596
603
|
api_key_env: ANTHROPIC_API_KEY
|
|
597
|
-
executor:
|
|
604
|
+
openfeel-executor:
|
|
598
605
|
provider: deepseek
|
|
599
|
-
model_name: deepseek-
|
|
606
|
+
model_name: deepseek-flash
|
|
600
607
|
roles: # 角色级覆盖(可选)
|
|
601
608
|
cross_model:
|
|
602
609
|
provider: openai
|
|
@@ -629,8 +636,8 @@ models:
|
|
|
629
636
|
| Agent | 角色要求 | 实际模型 | 配置来源 | 状态 |
|
|
630
637
|
|-------|----------|----------|----------|:--:|
|
|
631
638
|
| Feel | 主力推理 | deepseek-v4-pro | default | ⚠️ |
|
|
632
|
-
|
|
|
633
|
-
|
|
|
639
|
+
| openfeel-reviewer | **异种推理** | deepseek-v4-pro | default | ❌ 与主力相同! |
|
|
640
|
+
| openfeel-executor | 快速 | deepseek-v4-pro | default | ⚠️ 未使用快速模型 |
|
|
634
641
|
\`\`\`
|
|
635
642
|
|
|
636
643
|
### 5. 输出检查报告
|
|
@@ -673,8 +680,8 @@ models:
|
|
|
673
680
|
| 触发条件 | 引导内容 |
|
|
674
681
|
|----------|----------|
|
|
675
682
|
| \`models\` 节不存在 | "未检测到模型配置。你需要为不同角色分配模型吗?" → 引导创建 |
|
|
676
|
-
|
|
|
677
|
-
|
|
|
683
|
+
| openfeel-reviewer 使用与主力相同模型 | "⚠️ openfeel-reviewer 当前与 Feel 使用相同模型,异种交叉审查的核心优势无法发挥。建议为 openfeel-reviewer 配置不同的模型系列。" |
|
|
684
|
+
| openfeel-executor 使用推理模型 | "⚠️ openfeel-executor 建议使用快速模型以节省成本。是否配置?" |
|
|
678
685
|
| 关键 Agent 无任何配置 | "以下 Agent 无模型配置:{列表}。请配置。" |
|
|
679
686
|
|
|
680
687
|
### 7. 写入配置
|
|
@@ -701,7 +708,7 @@ models:
|
|
|
701
708
|
model_name: xxx
|
|
702
709
|
...
|
|
703
710
|
agents:
|
|
704
|
-
reviewer:
|
|
711
|
+
openfeel-reviewer:
|
|
705
712
|
provider: xxx
|
|
706
713
|
...
|
|
707
714
|
roles:
|
|
@@ -718,12 +725,12 @@ models:
|
|
|
718
725
|
- 报告语言:中文
|
|
719
726
|
- 每次检查后将结果摘要写入 \`.openfeel/log/\`(仅首次发现关键问题时)
|
|
720
727
|
`,
|
|
721
|
-
'model-config': `---
|
|
722
|
-
name: model-config
|
|
723
|
-
description: 查找和配置 Agent 模型。当 Agent 报 "Model not found" 或需要调整/新增 Agent 模型时使用。覆盖 opencode.jsonc 配置、模型名查找方法、多模态模型(
|
|
728
|
+
'openfeel-model-config': `---
|
|
729
|
+
name: openfeel-model-config
|
|
730
|
+
description: 查找和配置 Agent 模型。当 Agent 报 "Model not found" 或需要调整/新增 Agent 模型时使用。覆盖 opencode.jsonc 配置、模型名查找方法、多模态模型(openfeel-vision)特殊注意事项。
|
|
724
731
|
---
|
|
725
732
|
|
|
726
|
-
# Skill: model-config
|
|
733
|
+
# Skill: openfeel-model-config
|
|
727
734
|
|
|
728
735
|
# Agent 模型查找与配置
|
|
729
736
|
|
|
@@ -732,7 +739,7 @@ description: 查找和配置 Agent 模型。当 Agent 报 "Model not found" 或
|
|
|
732
739
|
- Agent 调用时报 \`Model not found: xxx\`
|
|
733
740
|
- 需要为 Agent 更换或指定模型
|
|
734
741
|
- 新增 Agent 后需要配置其模型
|
|
735
|
-
-
|
|
742
|
+
- openfeel-vision / 多模态模型无法正常调用
|
|
736
743
|
|
|
737
744
|
## 配置位置
|
|
738
745
|
|
|
@@ -741,8 +748,8 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
|
|
|
741
748
|
\`\`\`jsonc
|
|
742
749
|
{
|
|
743
750
|
"agent": {
|
|
744
|
-
"vision": {
|
|
745
|
-
"model": "
|
|
751
|
+
"openfeel-vision": {
|
|
752
|
+
"model": "deepseek/deepseek-flash" // 模型名格式:provider/model-id 或 model-id
|
|
746
753
|
}
|
|
747
754
|
}
|
|
748
755
|
}
|
|
@@ -750,6 +757,8 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
|
|
|
750
757
|
|
|
751
758
|
> ⚠️ **配置修改后必须重启 opencode 才能生效**。运行中的会话使用启动时加载的配置。
|
|
752
759
|
|
|
760
|
+
> 💡 **推荐**:可用 \`openfeel model set/get/list --scope default|global|project\` 一键读写三层级模型(工具默认 / 全局 / 当前项目),无需手工定位文件。
|
|
761
|
+
|
|
753
762
|
## 查找可用模型名
|
|
754
763
|
|
|
755
764
|
当收到 \`"Model not found: xxx. Did you mean: aaa, bbb?"\` 错误时:
|
|
@@ -759,17 +768,21 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
|
|
|
759
768
|
|
|
760
769
|
## Agent 定义文件中的 model 字段
|
|
761
770
|
|
|
762
|
-
\`.opencode/agents/<name>.md\`
|
|
771
|
+
\`.opencode/agents/<name>.md\` 的 frontmatter \`model:\` 字段**是生效的,且优先于** \`opencode.jsonc\` 的 \`agent.<name>.model\`(stage-40 REV-1606 实测)。opencode 模型解析优先级链:
|
|
772
|
+
|
|
773
|
+
\`\`\`
|
|
774
|
+
项目 agents frontmatter > 全局 agents frontmatter > 项目 opencode.jsonc agent.model > 全局 opencode.jsonc agent.model > opencode 默认
|
|
775
|
+
\`\`\`
|
|
763
776
|
|
|
764
|
-
|
|
765
|
-
1. 修改
|
|
766
|
-
2.
|
|
777
|
+
因此修改模型时以 frontmatter 为准(只改 jsonc 会被 frontmatter 遮蔽),建议两处同步保持一致:
|
|
778
|
+
1. 修改 \`.opencode/agents/<name>.md\` 的 frontmatter \`model:\`(模板源 \`src/core/templates-data/opencode/agents/\` 同步)
|
|
779
|
+
2. (可选,保持一致)同步修改 \`opencode.jsonc\` 的 \`agent.<name>.model\`
|
|
767
780
|
|
|
768
|
-
## 多模态(
|
|
781
|
+
## 多模态(openfeel-vision)模型特殊规则
|
|
769
782
|
|
|
770
783
|
- Feel 的主力模型(DeepSeek V4 Pro)不支持图片输入
|
|
771
|
-
- 遇到图片输入时 Feel 会自动委托
|
|
772
|
-
-
|
|
784
|
+
- 遇到图片输入时 Feel 会自动委托 openfeel-vision Agent
|
|
785
|
+
- openfeel-vision Agent 需要配置多模态模型(现用 \`deepseek-flash\`,备选 \`qwen3-vl-plus\`)
|
|
773
786
|
- 模型名不要随意添加前缀(如 \`alibaba/\`),以平台提示的可用名为准
|
|
774
787
|
- **模型引用格式**:\`{auth.json中的key}/{模型ID}\`,不是 \`provider.name\` 也不是 \`provider.id\`
|
|
775
788
|
- 读取 \`~/.local/share/opencode/auth.json\` 确认实际 provider key(常见:\`alibaba-cn\`、\`deepseek\`、\`zhipuai\`)
|
|
@@ -780,17 +793,17 @@ Agent 模型配置在 **\`opencode.jsonc\`**(项目根目录)中:
|
|
|
780
793
|
| Agent | 模型类型 | 备注 |
|
|
781
794
|
|-------|---------|------|
|
|
782
795
|
| Feel(总统领) | 推理模型 | DeepSeek V4 Pro — 不支持多模态 |
|
|
783
|
-
|
|
|
784
|
-
|
|
|
785
|
-
|
|
|
786
|
-
|
|
|
787
|
-
|
|
|
796
|
+
| openfeel-planner | 推理模型 | — |
|
|
797
|
+
| openfeel-schemer | 推理模型 | — |
|
|
798
|
+
| openfeel-executor | 快速模型 (Flash) | — |
|
|
799
|
+
| openfeel-reviewer | 异种推理模型 (GLM) | — |
|
|
800
|
+
| openfeel-feel-tester | 推理模型 | — |
|
|
788
801
|
| 事务官 | 快速模型 (Flash) | — |
|
|
789
|
-
|
|
|
790
|
-
|
|
|
802
|
+
| openfeel-vision | 多模态模型 | 需配 deepseek-flash |
|
|
803
|
+
| openfeel-archiver | 推理模型 | — |
|
|
791
804
|
`,
|
|
792
|
-
'recover': `---
|
|
793
|
-
name: recover
|
|
805
|
+
'openfeel-recover': `---
|
|
806
|
+
name: openfeel-recover
|
|
794
807
|
description: 跨会话上下文恢复,供 Agent 在会话启动时重建流水线状态。
|
|
795
808
|
---
|
|
796
809
|
|
|
@@ -810,8 +823,8 @@ description: 跨会话上下文恢复,供 Agent 在会话启动时重建流水
|
|
|
810
823
|
|
|
811
824
|
恢复摘要:流水线状态 + 阻塞项 + 待处理任务列表
|
|
812
825
|
`,
|
|
813
|
-
'roadmap': `---
|
|
814
|
-
name: roadmap
|
|
826
|
+
'openfeel-roadmap': `---
|
|
827
|
+
name: openfeel-roadmap
|
|
815
828
|
description: 加载项目路线图,供 Agent 查看版本规划和里程碑。
|
|
816
829
|
---
|
|
817
830
|
|
|
@@ -831,8 +844,8 @@ description: 加载项目路线图,供 Agent 查看版本规划和里程碑。
|
|
|
831
844
|
|
|
832
845
|
格式化路线图摘要:版本清单 + 各版本阶段进度
|
|
833
846
|
`,
|
|
834
|
-
'search-kb': `---
|
|
835
|
-
name: search-kb
|
|
847
|
+
'openfeel-search-kb': `---
|
|
848
|
+
name: openfeel-search-kb
|
|
836
849
|
description: 语义检索 .openfeel/kb/ 项目知识库。当精确匹配无结果或任务描述模糊时,通过向量相似度搜索语义相关的知识条目。支持图谱遍历返回关联条目。
|
|
837
850
|
---
|
|
838
851
|
|
|
@@ -951,16 +964,16 @@ description: 语义检索 .openfeel/kb/ 项目知识库。当精确匹配无结
|
|
|
951
964
|
|
|
952
965
|
- 向量索引是缓存层,文件系统始终是 single source of truth。若检索结果与预期不符,检查索引是否过期(运行 \`--dry-run\` 查看变更文件)
|
|
953
966
|
- 图谱链接亦是缓存层,Markdown 文件中 \`[[wikilink]]\` 是真实数据源;图谱可随时通过 \`python scripts/build_kb_index.py --graph\` 重建
|
|
954
|
-
- 语义检索适合模糊查询和探索性搜索,精确关键词匹配优先使用 \`check-kb\`
|
|
955
|
-
- 此技能是 \`check-kb\` 的回退方案——当 \`check-kb\` 精确匹配无结果时可自动调用
|
|
967
|
+
- 语义检索适合模糊查询和探索性搜索,精确关键词匹配优先使用 \`openfeel-check-kb\`
|
|
968
|
+
- 此技能是 \`openfeel-check-kb\` 的回退方案——当 \`openfeel-check-kb\` 精确匹配无结果时可自动调用
|
|
956
969
|
- 图遍历返回的关联条目仅基于已建立的 wikilink 链接,若条目未引用或被引用其他条目,图谱中不会出现对应关联
|
|
957
970
|
`,
|
|
958
|
-
'sync-status': `---
|
|
959
|
-
name: sync-status
|
|
971
|
+
'openfeel-sync-status': `---
|
|
972
|
+
name: openfeel-sync-status
|
|
960
973
|
description: 聚合所有成员的任务进度视图,供任意 Agent 快速了解项目整体协作状态。
|
|
961
974
|
---
|
|
962
975
|
|
|
963
|
-
# Skill: sync-status
|
|
976
|
+
# Skill: openfeel-sync-status
|
|
964
977
|
|
|
965
978
|
# 聚合任务进度
|
|
966
979
|
|
|
@@ -1023,10 +1036,9 @@ description: 聚合所有成员的任务进度视图,供任意 Agent 快速了
|
|
|
1023
1036
|
|
|
1024
1037
|
格式化后的 Markdown 进度摘要,不含文件修改。
|
|
1025
1038
|
|
|
1026
|
-
Base directory for this skill: file:///C:/Code/AI/AI_Prompt/.kilo/skills/sync-status
|
|
1027
1039
|
`,
|
|
1028
|
-
'update-stage-status': `---
|
|
1029
|
-
name: update-stage-status
|
|
1040
|
+
'openfeel-update-stage-status': `---
|
|
1041
|
+
name: openfeel-update-stage-status
|
|
1030
1042
|
description: 标准化更新 .openfeel/plan/{series}/{stage}/status.md 的子计划状态、责任 Agent 和状态记录,避免各 Agent 随意改写状态文件。适用于自动闭环和人工流程中的阶段状态变更。
|
|
1031
1043
|
---
|
|
1032
1044
|
|
|
@@ -1124,10 +1136,10 @@ planned | ready_for_code | coding | ready_for_review | review_failed | review_pa
|
|
|
1124
1136
|
|
|
1125
1137
|
### 6. 合并状态处理
|
|
1126
1138
|
|
|
1127
|
-
当子计划状态变为 \`done\` 或 \`review_passed\`(且
|
|
1139
|
+
当子计划状态变为 \`done\` 或 \`review_passed\`(且 openfeel-reviewer 验收完毕)时,根据 \`.openfeel/config.yaml\` 中的 \`merge_mode\` 决定合并行为:
|
|
1128
1140
|
|
|
1129
1141
|
- **\`merge_mode=auto\`**:
|
|
1130
|
-
1. 将 \`合并状态\` 更新为 \`merged\`(实际合并由
|
|
1142
|
+
1. 将 \`合并状态\` 更新为 \`merged\`(实际合并由 openfeel-executor 执行 git 操作,Skill 仅更新状态字段)
|
|
1131
1143
|
2. 将 \`合并状态\` 更新为 \`cleaned\`
|
|
1132
1144
|
3. 在状态记录中注明"自动合并"
|
|
1133
1145
|
- **\`merge_mode=manual\`**:
|
|
@@ -1145,8 +1157,8 @@ planned | ready_for_code | coding | ready_for_review | review_failed | review_pa
|
|
|
1145
1157
|
- 自动推进:保持 {enabled/disabled}
|
|
1146
1158
|
\`\`\`
|
|
1147
1159
|
`,
|
|
1148
|
-
'wizard': `---
|
|
1149
|
-
name: wizard
|
|
1160
|
+
'openfeel-wizard': `---
|
|
1161
|
+
name: openfeel-wizard
|
|
1150
1162
|
description: 交互式流水线向导,供 Agent 在终端中逐步推进流水线阶段。
|
|
1151
1163
|
---
|
|
1152
1164
|
|
|
@@ -1170,301 +1182,97 @@ description: 交互式流水线向导,供 Agent 在终端中逐步推进流水
|
|
|
1170
1182
|
`,
|
|
1171
1183
|
};
|
|
1172
1184
|
// AUTO-GENERATED-END: SKILL_DEFINITIONS
|
|
1173
|
-
// ─── 新增的 Skill 名称列表 ─────────────────────────────────────────
|
|
1174
|
-
const NEW_SKILL_NAMES = [
|
|
1175
|
-
'bug-acceptance',
|
|
1176
|
-
'check-kb',
|
|
1177
|
-
'get-bugs',
|
|
1178
|
-
'get-stage-status',
|
|
1179
|
-
'model-check',
|
|
1180
|
-
'search-kb',
|
|
1181
|
-
'sync-status',
|
|
1182
|
-
'update-stage-status',
|
|
1183
|
-
];
|
|
1184
1185
|
// ─── 辅助函数 ──────────────────────────────────────────────────────
|
|
1186
|
+
/** 写全文时的受管内容组合:frontmatter 序列化 + 正文标记包裹 */
|
|
1187
|
+
function composeManagedContent(content, type) {
|
|
1188
|
+
const split = type === 'markdown' ? splitFrontmatter(content) : null;
|
|
1189
|
+
if (split) {
|
|
1190
|
+
return serializeFrontmatter(split.frontmatter) + wrapRegion(split.body, type);
|
|
1191
|
+
}
|
|
1192
|
+
return wrapRegion(content, type);
|
|
1193
|
+
}
|
|
1185
1194
|
/**
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
* - 文件不存在 → created
|
|
1190
|
-
* - 文件已存在 + hash 匹配 update_state 中记录 → 安全覆盖 → updated
|
|
1191
|
-
* - 文件已存在 + hash 不匹配 → 拒绝覆盖 → conflicts
|
|
1192
|
-
* - 文件已存在 + 不在 update_state 管理中 → 安全覆盖 → updated(降级)
|
|
1193
|
-
* - 文件已存在 + update_state 不存在或损坏 → 安全覆盖 → updated(降级为旧行为)
|
|
1194
|
-
*
|
|
1195
|
-
* REV-001:此函数仅负责分类(created/updated/skipped/conflicts),
|
|
1196
|
-
* hash 的实际更新在 updateProject() 末尾统一处理,
|
|
1197
|
-
* 确保"冲突路径非冲突文件 hash 同步更新"。
|
|
1195
|
+
* 受管文件写入(控制区优先 + hash 兜底 三态)。
|
|
1196
|
+
* 分派依据 detectFileType;jsonc 不走本函数(调用方走深度合并)。
|
|
1197
|
+
* target 供 update_infos 路径记录:全局资产 { isGlobal: true },项目资产 { isGlobal: false, projectRoot }。
|
|
1198
1198
|
*/
|
|
1199
|
-
function
|
|
1199
|
+
function writeManagedFile(filePath, content, stateKey, updateState, target) {
|
|
1200
|
+
const type = detectFileType(filePath);
|
|
1201
|
+
if (type === null || type === 'jsonc') {
|
|
1202
|
+
// 无策略 / JSONC:不应到达(jsonc 由 updateProject 单独走深度合并)
|
|
1203
|
+
throw new Error(`writeManagedFile: 不支持的类型 ${filePath}`);
|
|
1204
|
+
}
|
|
1205
|
+
// 拆 incoming 的 frontmatter + 正文(gitignore 无 frontmatter,body=content)
|
|
1206
|
+
const incomingSplit = type === 'markdown' ? splitFrontmatter(content) : null;
|
|
1207
|
+
const incomingFm = incomingSplit?.frontmatter ?? null;
|
|
1208
|
+
const incomingBody = incomingSplit?.body ?? content;
|
|
1209
|
+
// 不存在 → created(写全文:frontmatter + 标记包裹正文)
|
|
1200
1210
|
if (!existsSync(filePath)) {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
1204
|
-
created.push(relativePath);
|
|
1205
|
-
return;
|
|
1211
|
+
atomicWriteFileSync(filePath, composeManagedContent(content, type));
|
|
1212
|
+
return 'created';
|
|
1206
1213
|
}
|
|
1207
1214
|
const existing = readFileSync(filePath, 'utf-8');
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
if (
|
|
1219
|
-
|
|
1220
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
1221
|
-
updated.push(relativePath);
|
|
1222
|
-
return;
|
|
1215
|
+
const existingSplit = type === 'markdown' ? splitFrontmatter(existing) : null;
|
|
1216
|
+
const existingFm = existingSplit?.frontmatter ?? null;
|
|
1217
|
+
const existingBody = existingSplit?.body ?? existing;
|
|
1218
|
+
const region = parseRegion(existingBody, type);
|
|
1219
|
+
if (region.status === 'ok') {
|
|
1220
|
+
// 含标记:frontmatter 合并 + 正文区内替换,全文比对(REV-901 skip 判定)
|
|
1221
|
+
// incoming 无 frontmatter(如 core.md/AGENTS.md 模板)时保留用户已有 frontmatter,避免写入丢失(REV-1101)
|
|
1222
|
+
const newFm = existingFm && incomingFm ? mergeFrontmatter(existingFm, incomingFm) : (existingFm ?? incomingFm);
|
|
1223
|
+
const newBody = replaceRegion(existingBody, incomingBody, type);
|
|
1224
|
+
const newContent = newFm ? serializeFrontmatter(newFm) + newBody : newBody;
|
|
1225
|
+
if (newContent === normalize(existing)) {
|
|
1226
|
+
return 'skipped';
|
|
1223
1227
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
return;
|
|
1228
|
+
atomicWriteFileSync(filePath, newContent);
|
|
1229
|
+
return 'updated';
|
|
1227
1230
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
*/
|
|
1236
|
-
function parseJsonc(text) {
|
|
1237
|
-
// 去除 \r 控制字符(避免 Windows CRLF 导致 JSON 解析报 "bad control character")
|
|
1238
|
-
const normalized = text.replace(/\r/g, '');
|
|
1239
|
-
// 逐字符处理,去除行注释(跳过字符串内部,避免匹配到 https:// 等 URL 中的 //)
|
|
1240
|
-
let result = '';
|
|
1241
|
-
let inString = false;
|
|
1242
|
-
let inEscape = false;
|
|
1243
|
-
let i = 0;
|
|
1244
|
-
while (i < normalized.length) {
|
|
1245
|
-
const ch = normalized[i];
|
|
1246
|
-
const next = normalized[i + 1];
|
|
1247
|
-
// 处理转义字符
|
|
1248
|
-
if (inEscape) {
|
|
1249
|
-
result += ch;
|
|
1250
|
-
inEscape = false;
|
|
1251
|
-
i++;
|
|
1252
|
-
continue;
|
|
1253
|
-
}
|
|
1254
|
-
if (ch === '\\' && inString) {
|
|
1255
|
-
result += ch;
|
|
1256
|
-
inEscape = true;
|
|
1257
|
-
i++;
|
|
1258
|
-
continue;
|
|
1259
|
-
}
|
|
1260
|
-
// 字符串边界
|
|
1261
|
-
if (ch === '"') {
|
|
1262
|
-
inString = !inString;
|
|
1263
|
-
result += ch;
|
|
1264
|
-
i++;
|
|
1265
|
-
continue;
|
|
1231
|
+
if (region.status === 'none') {
|
|
1232
|
+
// 无标记:hash 兜底(N3)
|
|
1233
|
+
const fileState = updateState?.files[stateKey];
|
|
1234
|
+
if (fileState && hashContent(existing) === fileState.hash) {
|
|
1235
|
+
// adopt:存量框架文件未改 → 直接写带标记的新框架内容(等价 created 记为 updated)
|
|
1236
|
+
atomicWriteFileSync(filePath, composeManagedContent(content, type));
|
|
1237
|
+
return 'updated';
|
|
1266
1238
|
}
|
|
1267
|
-
//
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
}
|
|
1274
|
-
// 保留换行符
|
|
1275
|
-
if (i < normalized.length && normalized[i] === '\n') {
|
|
1276
|
-
result += '\n';
|
|
1277
|
-
i++;
|
|
1278
|
-
}
|
|
1279
|
-
continue;
|
|
1280
|
-
}
|
|
1281
|
-
result += ch;
|
|
1282
|
-
i++;
|
|
1283
|
-
}
|
|
1284
|
-
return JSON.parse(result);
|
|
1285
|
-
}
|
|
1286
|
-
/**
|
|
1287
|
-
* 获取 opencode.jsonc 更新后的完整内容
|
|
1288
|
-
* 读取现有文件(若存在),合并更新后生成新的 JSONC 文本
|
|
1289
|
-
*/
|
|
1290
|
-
function buildUpdatedJsonc(projectPath) {
|
|
1291
|
-
const jsoncPath = resolve(projectPath, 'opencode.jsonc');
|
|
1292
|
-
let jsoncObj;
|
|
1293
|
-
let rawContent;
|
|
1294
|
-
if (existsSync(jsoncPath)) {
|
|
1295
|
-
// 已有文件 → 解析并合并,保留原始字符串用于后续补丁式更新
|
|
1296
|
-
rawContent = readFileSync(jsoncPath, 'utf-8');
|
|
1297
|
-
jsoncObj = parseJsonc(rawContent);
|
|
1298
|
-
}
|
|
1299
|
-
else {
|
|
1300
|
-
// 新文件 → 基础结构
|
|
1301
|
-
jsoncObj = {
|
|
1302
|
-
$schema: 'https://opencode.openfeel/config.json',
|
|
1303
|
-
default_agent: 'feel',
|
|
1304
|
-
instructions: ['AGENTS.md', '.opencode/instructions/core.md'],
|
|
1305
|
-
skills: {},
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
// 更新 default_agent
|
|
1309
|
-
jsoncObj.default_agent = 'feel';
|
|
1310
|
-
// 确保 instructions 存在
|
|
1311
|
-
if (!jsoncObj.instructions) {
|
|
1312
|
-
jsoncObj.instructions = ['AGENTS.md', '.opencode/instructions/core.md'];
|
|
1313
|
-
}
|
|
1314
|
-
// 合并 skills:保留原有 skill,添加新的 /opfx:* skill
|
|
1315
|
-
const skills = jsoncObj.skills || {};
|
|
1316
|
-
for (const name of NEW_SKILL_NAMES) {
|
|
1317
|
-
skills[name] = `.opencode/skills/${name}`;
|
|
1239
|
+
// 追加受管区(标记包裹正文,不触碰 frontmatter),并记录待会话启动复核
|
|
1240
|
+
const appendedRegion = wrapRegion(incomingBody, type);
|
|
1241
|
+
const sep = existing.endsWith('\n') ? '' : '\n';
|
|
1242
|
+
atomicWriteFileSync(filePath, `${existing}${sep}${appendedRegion}`);
|
|
1243
|
+
appendUpdateInfo('appended', target.isGlobal ? { absolutePath: filePath } : { projectRoot: target.projectRoot, relativePath: stateKey });
|
|
1244
|
+
return 'appended';
|
|
1318
1245
|
}
|
|
1319
|
-
|
|
1320
|
-
//
|
|
1321
|
-
|
|
1246
|
+
// malformed(>1 对 / 不成对):不追加、不写盘,仅记录异常条目待人工修复标记;
|
|
1247
|
+
// 修复后下次 update 走 ok(区内替换)/ none(hash 兜底)正常路径,杜绝重复追加死循环(REV-1001 修订)
|
|
1248
|
+
appendUpdateInfo('anomaly', target.isGlobal ? { absolutePath: filePath } : { projectRoot: target.projectRoot, relativePath: stateKey });
|
|
1249
|
+
return 'skipped';
|
|
1322
1250
|
}
|
|
1323
1251
|
/**
|
|
1324
|
-
*
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1252
|
+
* 写单个全局资产文件(REV-1205 最小侵入抽取,供 migrate 复用)。
|
|
1253
|
+
* 等价 writeManagedFile(filePath, content, filePath, state, { isGlobal: true }),
|
|
1254
|
+
* 即 stateKey = 绝对路径、target 为全局资产。
|
|
1255
|
+
* @returns 动作分类,供调用方报告。
|
|
1327
1256
|
*/
|
|
1328
|
-
function
|
|
1329
|
-
|
|
1330
|
-
if (originalContent) {
|
|
1331
|
-
let result = originalContent;
|
|
1332
|
-
// 替换 "default_agent" 的值
|
|
1333
|
-
result = result.replace(/("default_agent"\s*:\s*)("[^"]*")/, `$1"${obj.default_agent}"`);
|
|
1334
|
-
// 替换 "skills" 块
|
|
1335
|
-
const skills = obj.skills;
|
|
1336
|
-
result = replaceSkillsFieldInJsonc(result, skills);
|
|
1337
|
-
return result;
|
|
1338
|
-
}
|
|
1339
|
-
// 无原始内容 → 从头构建(新文件场景)
|
|
1340
|
-
return buildJsoncFromObject(obj);
|
|
1257
|
+
export function deployGlobalAsset(filePath, content, state) {
|
|
1258
|
+
return writeManagedFile(filePath, content, filePath, state, { isGlobal: true });
|
|
1341
1259
|
}
|
|
1342
|
-
/**
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
// 找到匹配的 },处理字符串内的大括号
|
|
1357
|
-
let depth = 1;
|
|
1358
|
-
let closeBraceIdx = openBraceIdx;
|
|
1359
|
-
let inString = false;
|
|
1360
|
-
let inEscape = false;
|
|
1361
|
-
for (let i = openBraceIdx + 1; i < content.length && depth > 0; i++) {
|
|
1362
|
-
const ch = content[i];
|
|
1363
|
-
if (inEscape) {
|
|
1364
|
-
inEscape = false;
|
|
1365
|
-
continue;
|
|
1366
|
-
}
|
|
1367
|
-
if (ch === '\\' && inString) {
|
|
1368
|
-
inEscape = true;
|
|
1369
|
-
continue;
|
|
1370
|
-
}
|
|
1371
|
-
if (ch === '"') {
|
|
1372
|
-
inString = !inString;
|
|
1373
|
-
continue;
|
|
1374
|
-
}
|
|
1375
|
-
if (!inString) {
|
|
1376
|
-
if (ch === '{') {
|
|
1377
|
-
depth++;
|
|
1378
|
-
}
|
|
1379
|
-
else if (ch === '}') {
|
|
1380
|
-
depth--;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
if (depth === 0) {
|
|
1384
|
-
closeBraceIdx = i;
|
|
1260
|
+
/** 将 writeManagedFile 的动作分发到对应结果数组 */
|
|
1261
|
+
function pushAction(action, stateKey, created, updated, skipped, appended) {
|
|
1262
|
+
switch (action) {
|
|
1263
|
+
case 'created':
|
|
1264
|
+
created.push(stateKey);
|
|
1265
|
+
break;
|
|
1266
|
+
case 'updated':
|
|
1267
|
+
updated.push(stateKey);
|
|
1268
|
+
break;
|
|
1269
|
+
case 'skipped':
|
|
1270
|
+
skipped.push(stateKey);
|
|
1271
|
+
break;
|
|
1272
|
+
case 'appended':
|
|
1273
|
+
appended.push(stateKey);
|
|
1385
1274
|
break;
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
// 检查 } 后面是否有逗号:若 } 后紧跟(仅跳过空白)有逗号,则将逗号及其后空白纳入 endIdx
|
|
1389
|
-
let endIdx = closeBraceIdx + 1;
|
|
1390
|
-
let scanIdx = endIdx;
|
|
1391
|
-
while (scanIdx < content.length && /[ \t]/.test(content[scanIdx])) {
|
|
1392
|
-
scanIdx++;
|
|
1393
|
-
}
|
|
1394
|
-
if (scanIdx < content.length && content[scanIdx] === ',') {
|
|
1395
|
-
scanIdx++; // 跳过逗号
|
|
1396
|
-
// 跳过逗号后的空白(包括换行)
|
|
1397
|
-
while (scanIdx < content.length && /[\s\n\r]/.test(content[scanIdx])) {
|
|
1398
|
-
scanIdx++;
|
|
1399
|
-
}
|
|
1400
|
-
endIdx = scanIdx;
|
|
1401
|
-
}
|
|
1402
|
-
// 确定替换后是否需要末尾逗号:检查 skills 之后是否还有其他顶层字段
|
|
1403
|
-
let hasMoreFields = false;
|
|
1404
|
-
if (endIdx < content.length) {
|
|
1405
|
-
// 从 endIdx 找到内容末尾前最后一个 }(根对象闭合)
|
|
1406
|
-
const remaining = content.slice(endIdx);
|
|
1407
|
-
const rootCloseIdx = remaining.lastIndexOf('}');
|
|
1408
|
-
if (rootCloseIdx > 0) {
|
|
1409
|
-
const between = remaining.slice(0, rootCloseIdx);
|
|
1410
|
-
hasMoreFields = /[^\s]/.test(between);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
// 构建新的 skills 块内容
|
|
1414
|
-
const skillEntries = Object.entries(skills);
|
|
1415
|
-
const skillLines = skillEntries.map(([key, value], idx) => {
|
|
1416
|
-
const comma = idx < skillEntries.length - 1 ? ',' : '';
|
|
1417
|
-
return ` "${key}": "${value}"${comma}`;
|
|
1418
|
-
});
|
|
1419
|
-
const blockContent = skillLines.join('\n');
|
|
1420
|
-
const newBlock = `"skills": {\n${blockContent}\n }${hasMoreFields ? ',' : ''}`;
|
|
1421
|
-
return content.slice(0, keyStart) + newBlock + content.slice(endIdx);
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* 从对象从头构建 JSONC(无原始内容时的回退路径)
|
|
1425
|
-
*/
|
|
1426
|
-
function buildJsoncFromObject(obj) {
|
|
1427
|
-
const skills = obj.skills;
|
|
1428
|
-
// 构建 skills 条目行
|
|
1429
|
-
const skillEntries = Object.entries(skills);
|
|
1430
|
-
const skillsLines = skillEntries.map(([key, value], index) => {
|
|
1431
|
-
const comma = index < skillEntries.length - 1 ? ',' : '';
|
|
1432
|
-
return ` "${key}": "${value}"${comma}`;
|
|
1433
|
-
});
|
|
1434
|
-
const instructions = obj.instructions;
|
|
1435
|
-
const instructionLines = instructions.map((inst, index) => {
|
|
1436
|
-
const comma = index < instructions.length - 1 ? ',' : '';
|
|
1437
|
-
return ` "${inst}"${comma}`;
|
|
1438
|
-
});
|
|
1439
|
-
// 构建完整 JSONC
|
|
1440
|
-
const hasExperimental = typeof obj.experimental === 'object' && obj.experimental !== null;
|
|
1441
|
-
const lines = [
|
|
1442
|
-
'{',
|
|
1443
|
-
' "$schema": "https://opencode.openfeel/config.json",',
|
|
1444
|
-
` "default_agent": "${obj.default_agent}",`,
|
|
1445
|
-
' "instructions": [',
|
|
1446
|
-
...instructionLines,
|
|
1447
|
-
' ],',
|
|
1448
|
-
' "skills": {',
|
|
1449
|
-
...skillsLines,
|
|
1450
|
-
hasExperimental ? ' },' : ' }',
|
|
1451
|
-
];
|
|
1452
|
-
// 如果有 experimental 字段,保留它
|
|
1453
|
-
if (hasExperimental) {
|
|
1454
|
-
const exp = obj.experimental;
|
|
1455
|
-
const expEntries = Object.entries(exp);
|
|
1456
|
-
const expLines = expEntries.map(([key, value], index) => {
|
|
1457
|
-
const comma = index < expEntries.length - 1 ? ',' : '';
|
|
1458
|
-
const val = typeof value === 'string' ? `"${value}"` : String(value);
|
|
1459
|
-
return ` "${key}": ${val}${comma}`;
|
|
1460
|
-
});
|
|
1461
|
-
lines.push(' "experimental": {');
|
|
1462
|
-
lines.push(...expLines);
|
|
1463
|
-
lines.push(' }');
|
|
1464
1275
|
}
|
|
1465
|
-
lines.push('}');
|
|
1466
|
-
lines.push(''); // 末尾换行符
|
|
1467
|
-
return lines.join('\n');
|
|
1468
1276
|
}
|
|
1469
1277
|
// ─── 主函数 ────────────────────────────────────────────────────────
|
|
1470
1278
|
/**
|
|
@@ -1478,22 +1286,33 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
|
|
|
1478
1286
|
const created = [];
|
|
1479
1287
|
const updated = [];
|
|
1480
1288
|
const skipped = [];
|
|
1481
|
-
|
|
1289
|
+
const appended = [];
|
|
1290
|
+
// ── 增量更新:加载项目 state(AGENTS.md / 项目 opencode.jsonc)与全局 state(框架资产)──
|
|
1482
1291
|
const state = loadUpdateState(projectPath);
|
|
1292
|
+
const globalState = loadGlobalUpdateState();
|
|
1483
1293
|
const conflicts = [];
|
|
1484
|
-
const agentsDir = resolve(projectPath, '.opencode', 'agents');
|
|
1485
|
-
const skillsDir = resolve(projectPath, '.opencode', 'skills');
|
|
1486
|
-
// 确保目标目录存在
|
|
1487
|
-
mkdirSync(agentsDir, { recursive: true });
|
|
1488
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
1489
1294
|
// 过滤:仅处理选中的工具
|
|
1490
1295
|
if (!selectedTools.includes('opencode')) {
|
|
1491
|
-
return { created: [], updated: [], skipped: [], conflicts: [] };
|
|
1296
|
+
return { created: [], updated: [], skipped: [], conflicts: [], appended: [] };
|
|
1492
1297
|
}
|
|
1298
|
+
// N8:检测项目内遗留 .opencode/{agents,skills,instructions},只提示不迁移
|
|
1299
|
+
const legacyDirs = ['agents', 'skills', 'instructions']
|
|
1300
|
+
.filter((d) => existsSync(resolve(projectPath, '.opencode', d)));
|
|
1301
|
+
if (legacyDirs.length > 0) {
|
|
1302
|
+
console.warn(`[update] 检测到项目内旧布局 .opencode/{${legacyDirs.join(',')}},请运行 openfeel migrate(stage-39 提供)迁移。`);
|
|
1303
|
+
}
|
|
1304
|
+
// 全局部署目标(D1)
|
|
1305
|
+
const agentsDir = getGlobalAgentsDir();
|
|
1306
|
+
const skillsDir = getGlobalSkillsDir();
|
|
1307
|
+
// 项目内旧布局 agents 目录:仅用于 AGENTS.md 首次部署判定,保持原语义(项目级)
|
|
1308
|
+
const projectLegacyAgentsDir = resolve(projectPath, '.opencode', 'agents');
|
|
1309
|
+
// 确保全局目标目录存在
|
|
1310
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
1311
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
1493
1312
|
// ── AGENTS.md 语言同步逻辑 ──
|
|
1494
1313
|
let agentsDirHasContent = false;
|
|
1495
1314
|
try {
|
|
1496
|
-
agentsDirHasContent = existsSync(
|
|
1315
|
+
agentsDirHasContent = existsSync(projectLegacyAgentsDir) && readdirSync(projectLegacyAgentsDir).length > 0;
|
|
1497
1316
|
}
|
|
1498
1317
|
catch {
|
|
1499
1318
|
agentsDirHasContent = false;
|
|
@@ -1501,18 +1320,18 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
|
|
|
1501
1320
|
const agentsMdPath = resolve(projectPath, 'AGENTS.md');
|
|
1502
1321
|
const agentsMdExists = existsSync(agentsMdPath);
|
|
1503
1322
|
if (!agentsDirHasContent && !agentsMdExists) {
|
|
1504
|
-
// 情况 1:首次部署 → 使用全局默认语言部署 AGENTS.md
|
|
1323
|
+
// 情况 1:首次部署 → 使用全局默认语言部署 AGENTS.md(受管区标记包裹)
|
|
1505
1324
|
const globalConfig = getGlobalConfig();
|
|
1506
1325
|
const deployLang = lang ?? globalConfig.lang ?? 'zh-CN';
|
|
1507
1326
|
const agentsMdContent = loadTemplate(deployLang, 'agents-md');
|
|
1508
|
-
|
|
1327
|
+
atomicWriteFileSync(agentsMdPath, composeManagedContent(agentsMdContent, 'markdown'));
|
|
1509
1328
|
created.push('AGENTS.md');
|
|
1510
1329
|
}
|
|
1511
1330
|
else if (!agentsMdExists && options?.lang) {
|
|
1512
1331
|
// 情况 2b:已有 agents 目录但 AGENTS.md 被手动删除 + --lang 指定
|
|
1513
|
-
// → 用指定语言重新创建 AGENTS.md
|
|
1332
|
+
// → 用指定语言重新创建 AGENTS.md(受管区标记包裹)
|
|
1514
1333
|
const agentsMdContent = loadTemplate(options.lang, 'agents-md');
|
|
1515
|
-
|
|
1334
|
+
atomicWriteFileSync(agentsMdPath, composeManagedContent(agentsMdContent, 'markdown'));
|
|
1516
1335
|
created.push('AGENTS.md');
|
|
1517
1336
|
}
|
|
1518
1337
|
else if (agentsMdExists && options?.lang) {
|
|
@@ -1522,9 +1341,9 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
|
|
|
1522
1341
|
if (requestedLang !== projectLang) {
|
|
1523
1342
|
// 语言不同 → 提示确认
|
|
1524
1343
|
if (options?.force) {
|
|
1525
|
-
// --force →
|
|
1344
|
+
// --force → 跳过确认,直接覆盖(受管区标记包裹)
|
|
1526
1345
|
const agentsMdContent = loadTemplate(requestedLang, 'agents-md');
|
|
1527
|
-
|
|
1346
|
+
atomicWriteFileSync(agentsMdPath, composeManagedContent(agentsMdContent, 'markdown'));
|
|
1528
1347
|
updated.push('AGENTS.md');
|
|
1529
1348
|
}
|
|
1530
1349
|
else if (options?.interactive) {
|
|
@@ -1539,29 +1358,17 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
|
|
|
1539
1358
|
}
|
|
1540
1359
|
}
|
|
1541
1360
|
else {
|
|
1542
|
-
// 语言相同 →
|
|
1361
|
+
// 语言相同 → 走受管文件三态(含标记区内替换 / 无标记 hash 兜底 / 追加)
|
|
1543
1362
|
const templateContent = loadTemplate(requestedLang, 'agents-md');
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
writeFileSync(agentsMdPath, templateContent, 'utf-8');
|
|
1547
|
-
updated.push('AGENTS.md');
|
|
1548
|
-
}
|
|
1549
|
-
else {
|
|
1550
|
-
skipped.push('AGENTS.md (language unchanged)');
|
|
1551
|
-
}
|
|
1363
|
+
const action = writeManagedFile(agentsMdPath, templateContent, 'AGENTS.md', state, { isGlobal: false, projectRoot: projectPath });
|
|
1364
|
+
pushAction(action, 'AGENTS.md', created, updated, skipped, appended);
|
|
1552
1365
|
}
|
|
1553
1366
|
}
|
|
1554
1367
|
else if (agentsMdExists) {
|
|
1555
|
-
// 情况 3:已有项目 + 无 --lang 参数 →
|
|
1368
|
+
// 情况 3:已有项目 + 无 --lang 参数 → 走受管文件三态
|
|
1556
1369
|
const templateContent = loadTemplate(lang, 'agents-md');
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
writeFileSync(agentsMdPath, templateContent, 'utf-8');
|
|
1560
|
-
updated.push('AGENTS.md');
|
|
1561
|
-
}
|
|
1562
|
-
else {
|
|
1563
|
-
skipped.push('AGENTS.md (use existing)');
|
|
1564
|
-
}
|
|
1370
|
+
const action = writeManagedFile(agentsMdPath, templateContent, 'AGENTS.md', state, { isGlobal: false, projectRoot: projectPath });
|
|
1371
|
+
pushAction(action, 'AGENTS.md', created, updated, skipped, appended);
|
|
1565
1372
|
}
|
|
1566
1373
|
// 记录项目语言映射到全局配置
|
|
1567
1374
|
// 使用 options?.lang(用户通过 --lang 显式指定的语言)优先,
|
|
@@ -1572,76 +1379,123 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
|
|
|
1572
1379
|
catch {
|
|
1573
1380
|
// 记录失败不影响主流程
|
|
1574
1381
|
}
|
|
1575
|
-
//
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
const coreInstructionsPath =
|
|
1382
|
+
// 全局 state 可写实例:首次 null → 降级重建(后续全量写入全局资产,防静默不部署)
|
|
1383
|
+
const newGlobalState = globalState ?? createGlobalUpdateState({});
|
|
1384
|
+
// 0. 生成全局框架约束 core.md(~/.config/opencode/openfeel/core.md)
|
|
1385
|
+
const coreInstructionsPath = getGlobalCoreMdPath();
|
|
1579
1386
|
const coreContent = loadTemplate(lang, 'core-instructions');
|
|
1580
|
-
|
|
1581
|
-
|
|
1387
|
+
{
|
|
1388
|
+
const action = deployGlobalAsset(coreInstructionsPath, coreContent, newGlobalState);
|
|
1389
|
+
pushAction(action, coreInstructionsPath, created, updated, skipped, appended);
|
|
1390
|
+
}
|
|
1391
|
+
// 1. 生成 Agent 定义文件到全局 agents 目录(绝对路径作 state key)
|
|
1582
1392
|
for (const name of listAgentIds(lang)) {
|
|
1583
1393
|
const content = loadAgentTemplate(lang, name);
|
|
1584
1394
|
const filePath = join(agentsDir, `${name}.md`);
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1395
|
+
const action = deployGlobalAsset(filePath, content, newGlobalState);
|
|
1396
|
+
pushAction(action, filePath, created, updated, skipped, appended);
|
|
1587
1397
|
}
|
|
1588
|
-
// 2. 生成 Skill
|
|
1398
|
+
// 2. 生成 Skill 定义文件到全局 skills 目录(每个 Skill 一个子目录,包含 SKILL.md)
|
|
1589
1399
|
for (const [name, content] of Object.entries(SKILL_DEFINITIONS)) {
|
|
1590
1400
|
const skillSubDir = join(skillsDir, name);
|
|
1591
1401
|
mkdirSync(skillSubDir, { recursive: true });
|
|
1592
1402
|
const filePath = join(skillSubDir, 'SKILL.md');
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1403
|
+
const action = deployGlobalAsset(filePath, content, newGlobalState);
|
|
1404
|
+
pushAction(action, filePath, created, updated, skipped, appended);
|
|
1405
|
+
}
|
|
1406
|
+
// 3. 全局 opencode.jsonc:深度合并(保留用户字段),加全局锁 + 原子写
|
|
1407
|
+
const globalJsoncPath = getGlobalOpencodeJsoncPath();
|
|
1408
|
+
// REV-1805:read-merge-write 全部置于锁内,消除与 model-config 锁内读写的 TOCTOU 竞态
|
|
1409
|
+
const globalJsoncNew = withFileLock(globalLockPath('global-opencode-jsonc'), () => {
|
|
1410
|
+
const current = existsSync(globalJsoncPath) ? readFileSync(globalJsoncPath, 'utf-8') : '{}\n';
|
|
1411
|
+
let merged;
|
|
1412
|
+
// 合并可能因块注释 parse 失败抛异常(parseJsonc 边界):降级为「跳过合并、保留原文件」并告警
|
|
1413
|
+
try {
|
|
1414
|
+
merged = mergeGlobalOpencodeJsonc(current);
|
|
1415
|
+
}
|
|
1416
|
+
catch (err) {
|
|
1417
|
+
console.warn(`[update] 全局 opencode.jsonc 解析失败(可能含块注释),跳过合并保留原文件: ${err.message}`);
|
|
1418
|
+
merged = current;
|
|
1419
|
+
}
|
|
1420
|
+
atomicWriteFileSync(globalJsoncPath, merged);
|
|
1421
|
+
return merged;
|
|
1422
|
+
});
|
|
1423
|
+
updateFileHash(newGlobalState, globalJsoncPath, globalJsoncNew);
|
|
1424
|
+
// 4. 项目 opencode.jsonc:不存在则写最小 { $schema };已存在则保留(旧非法字段清理属 stage-39)
|
|
1425
|
+
const projectJsoncPath = resolve(projectPath, 'opencode.jsonc');
|
|
1426
|
+
if (!existsSync(projectJsoncPath)) {
|
|
1427
|
+
atomicWriteFileSync(projectJsoncPath, JSON.stringify(buildProjectOpencodeJsoncObj(), null, 2) + '\n');
|
|
1428
|
+
created.push('opencode.jsonc');
|
|
1429
|
+
}
|
|
1430
|
+
else {
|
|
1431
|
+
skipped.push('opencode.jsonc (use existing)');
|
|
1595
1432
|
}
|
|
1596
|
-
//
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
if (updated.some(f => f.startsWith('.opencode/agents/')) && process.stdout.isTTY) {
|
|
1603
|
-
const lang = getCliLang(projectPath);
|
|
1604
|
-
console.log(lang === 'en'
|
|
1605
|
-
? 'opencode agent configuration updated. Please restart opencode to load the new configuration.'
|
|
1606
|
-
: 'opencode agent 配置已更新,请重启 opencode 以加载新配置。');
|
|
1433
|
+
// 重启提醒(仅在 opencode 全局 agent 配置更新时,且为交互模式)
|
|
1434
|
+
if (updated.some((f) => f.startsWith(getGlobalAgentsDir())) && process.stdout.isTTY) {
|
|
1435
|
+
const reminderLang = getCliLang(projectPath);
|
|
1436
|
+
console.log(reminderLang === 'en'
|
|
1437
|
+
? 'opencode global configuration updated. Please restart opencode to load the new configuration.'
|
|
1438
|
+
: 'opencode 全局配置已更新,请重启 opencode 以加载新配置。');
|
|
1607
1439
|
}
|
|
1608
1440
|
// ── 写入冲突标记文件 ──
|
|
1609
1441
|
if (conflicts.length > 0) {
|
|
1610
1442
|
const openfeelVersion = getOpenfeelVersion();
|
|
1611
|
-
for (const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
? readFileSync(
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1443
|
+
for (const conflictPath of conflicts) {
|
|
1444
|
+
// 全局冲突(绝对路径)写 ~/.openfeel/update_conflicts/;项目冲突写项目 .openfeel/update_conflicts/
|
|
1445
|
+
if (isAbsolute(conflictPath)) {
|
|
1446
|
+
const currentContent = existsSync(conflictPath) ? readFileSync(conflictPath, 'utf-8') : '';
|
|
1447
|
+
const incomingContent = getIncomingContent(conflictPath, lang);
|
|
1448
|
+
const relFromGlobal = relative(getOpencodeGlobalDir(), conflictPath);
|
|
1449
|
+
// 全局冲突根目录 ~/.openfeel/update_conflicts/(与项目 .openfeel/update_conflicts/ 对称)
|
|
1450
|
+
const globalConflictsBase = resolve(dirname(getGlobalUpdateStatePath()), 'update_conflicts');
|
|
1451
|
+
writeConflictFile(projectPath, relFromGlobal, currentContent, incomingContent, openfeelVersion, globalConflictsBase);
|
|
1452
|
+
}
|
|
1453
|
+
else {
|
|
1454
|
+
const absPath = resolve(projectPath, conflictPath);
|
|
1455
|
+
const currentContent = existsSync(absPath) ? readFileSync(absPath, 'utf-8') : '';
|
|
1456
|
+
// 获取 incoming 内容:冲突未实际写入,需单独获取
|
|
1457
|
+
const incomingContent = getIncomingContent(conflictPath, lang);
|
|
1458
|
+
writeConflictFile(projectPath, conflictPath, currentContent, incomingContent, openfeelVersion);
|
|
1459
|
+
}
|
|
1620
1460
|
}
|
|
1621
1461
|
}
|
|
1622
|
-
// ── REV-001
|
|
1462
|
+
// ── REV-001:统一更新双 update_state.json ──
|
|
1623
1463
|
// 规则:
|
|
1624
1464
|
// 1. 无论有无冲突,所有 created/updated 文件的 hash 都必须更新到 state
|
|
1625
1465
|
// 2. 有冲突时,额外标记冲突文件
|
|
1626
1466
|
// 3. 首次 update(state === null)→ 创建新 state
|
|
1627
1467
|
const newState = state ?? createUpdateState(projectPath, {});
|
|
1628
|
-
//
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
if (
|
|
1632
|
-
|
|
1633
|
-
|
|
1468
|
+
// 更新非冲突文件 hash:项目资产(相对路径)→ 项目 state;全局资产(绝对路径)→ 全局 state
|
|
1469
|
+
// appended 追加后同样记录 clean + 新 hash(D38-1:追加即建区,下次走区内替换而非误判无记录)
|
|
1470
|
+
for (const p of [...created, ...updated, ...appended]) {
|
|
1471
|
+
if (isAbsolute(p)) {
|
|
1472
|
+
if (existsSync(p)) {
|
|
1473
|
+
updateFileHash(newGlobalState, p, readFileSync(p, 'utf-8'));
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
else {
|
|
1477
|
+
const absPath = resolve(projectPath, p);
|
|
1478
|
+
if (existsSync(absPath)) {
|
|
1479
|
+
updateFileHash(newState, p, readFileSync(absPath, 'utf-8'));
|
|
1480
|
+
}
|
|
1634
1481
|
}
|
|
1635
1482
|
}
|
|
1636
|
-
//
|
|
1637
|
-
for (const
|
|
1638
|
-
|
|
1483
|
+
// 标记冲突文件(按绝对/相对路由到对应 state)
|
|
1484
|
+
for (const p of conflicts) {
|
|
1485
|
+
if (isAbsolute(p)) {
|
|
1486
|
+
markFileConflict(newGlobalState, p);
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
markFileConflict(newState, p);
|
|
1490
|
+
}
|
|
1639
1491
|
}
|
|
1640
1492
|
// 更新时间戳
|
|
1641
1493
|
newState.last_update = new Date().toISOString();
|
|
1642
|
-
|
|
1494
|
+
newGlobalState.last_update = new Date().toISOString();
|
|
1495
|
+
// 持久化双 state
|
|
1643
1496
|
saveUpdateState(projectPath, newState);
|
|
1644
|
-
|
|
1497
|
+
saveGlobalUpdateState(newGlobalState);
|
|
1498
|
+
return { created, updated, skipped, conflicts, appended };
|
|
1645
1499
|
}
|
|
1646
1500
|
/**
|
|
1647
1501
|
* 写入冲突标记文件到 .openfeel/update_conflicts/{relativePath}
|
|
@@ -1649,10 +1503,11 @@ export function updateProject(projectPath, selectedTools = ["opencode"], lang =
|
|
|
1649
1503
|
*
|
|
1650
1504
|
* REV-006:update_conflicts/ 目录已加入 .gitignore,不纳入版本管理。
|
|
1651
1505
|
*/
|
|
1652
|
-
function writeConflictFile(projectPath, relativePath, currentContent, incomingContent, openfeelVersion) {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1506
|
+
function writeConflictFile(projectPath, relativePath, currentContent, incomingContent, openfeelVersion, conflictsBase) {
|
|
1507
|
+
// 全局冲突传全局 base(~/.openfeel),项目冲突默认项目 .openfeel/update_conflicts
|
|
1508
|
+
const base = conflictsBase ?? resolve(projectPath, '.openfeel', 'update_conflicts');
|
|
1509
|
+
mkdirSync(base, { recursive: true });
|
|
1510
|
+
const conflictPath = resolve(base, relativePath);
|
|
1656
1511
|
mkdirSync(dirname(conflictPath), { recursive: true });
|
|
1657
1512
|
const conflictContent = [
|
|
1658
1513
|
`<<<<<<< CURRENT (用户修改版)`,
|
|
@@ -1661,39 +1516,37 @@ function writeConflictFile(projectPath, relativePath, currentContent, incomingCo
|
|
|
1661
1516
|
incomingContent,
|
|
1662
1517
|
`>>>>>>> INCOMING (openfeel v${openfeelVersion} 更新)`,
|
|
1663
1518
|
].join('\n');
|
|
1664
|
-
|
|
1519
|
+
atomicWriteFileSync(conflictPath, conflictContent);
|
|
1665
1520
|
}
|
|
1666
1521
|
/**
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1669
|
-
* -
|
|
1670
|
-
* -
|
|
1671
|
-
* -
|
|
1672
|
-
* - opencode.jsonc →
|
|
1522
|
+
* 根据路径获取 incoming(openfeel 期望写入)内容
|
|
1523
|
+
* relativePath 现为全局绝对路径(框架资产)或项目相对路径(AGENTS.md / opencode.jsonc):
|
|
1524
|
+
* - getGlobalCoreMdPath() → loadTemplate(lang, 'core-instructions')
|
|
1525
|
+
* - getGlobalAgentsDir()/{name}.md → loadAgentTemplate(lang, name)
|
|
1526
|
+
* - getGlobalSkillsDir()/{name}/SKILL.md → SKILL_DEFINITIONS[name]
|
|
1527
|
+
* - opencode.jsonc → buildProjectOpencodeJsoncObj()
|
|
1673
1528
|
* - AGENTS.md → loadTemplate(lang, 'agents-md')
|
|
1674
1529
|
*/
|
|
1675
|
-
function getIncomingContent(
|
|
1676
|
-
//
|
|
1677
|
-
if (relativePath ===
|
|
1530
|
+
function getIncomingContent(relativePath, lang) {
|
|
1531
|
+
// 全局框架约束 core.md
|
|
1532
|
+
if (relativePath === getGlobalCoreMdPath()) {
|
|
1678
1533
|
return loadTemplate(lang, 'core-instructions');
|
|
1679
1534
|
}
|
|
1680
|
-
//
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
const name = agentMatch[1];
|
|
1535
|
+
// 全局 agent:位于 getGlobalAgentsDir() 下的 {name}.md
|
|
1536
|
+
if (relativePath.startsWith(getGlobalAgentsDir())) {
|
|
1537
|
+
const name = basename(relativePath, '.md');
|
|
1684
1538
|
return loadAgentTemplate(lang, name);
|
|
1685
1539
|
}
|
|
1686
|
-
//
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const name = skillMatch[1];
|
|
1540
|
+
// 全局 skill:位于 getGlobalSkillsDir() 下的 {name}/SKILL.md(跨平台 basename 匹配)
|
|
1541
|
+
if (relativePath.startsWith(getGlobalSkillsDir()) && basename(relativePath) === 'SKILL.md') {
|
|
1542
|
+
const name = basename(dirname(relativePath));
|
|
1690
1543
|
return SKILL_DEFINITIONS[name] ?? '';
|
|
1691
1544
|
}
|
|
1692
|
-
// opencode.jsonc
|
|
1545
|
+
// 项目 opencode.jsonc
|
|
1693
1546
|
if (relativePath === 'opencode.jsonc') {
|
|
1694
|
-
return
|
|
1547
|
+
return JSON.stringify(buildProjectOpencodeJsoncObj(), null, 2) + '\n';
|
|
1695
1548
|
}
|
|
1696
|
-
// AGENTS.md
|
|
1549
|
+
// 项目 AGENTS.md
|
|
1697
1550
|
if (relativePath === 'AGENTS.md') {
|
|
1698
1551
|
return loadTemplate(lang, 'agents-md');
|
|
1699
1552
|
}
|